commit aa56926258100136e85d445477efab13b0e31130 Author: CaiXiang <939387484@qq.com> Date: Sat Nov 30 18:36:13 2024 +0800 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8e645bf --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC0-1.0 + +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Default settings +# Each of the following settings can be overwritten for any file type / path. +[*] +charset = utf-8 +# Use spaces instead of tabs +indent_style = space +# 2 space indentation by default +indent_size = 2 +# 4 space continuation indentation (might not be supported for all file types/domains) +continuation_indent_size = 4 +# Use Unix-style newlines +end_of_line = lf +# New line at the end of each file +insert_final_newline = true +# Trim trailing whitespaces +trim_trailing_whitespace = true + +[*.bat] +end_of_line = crlf diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5016d01 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC0-1.0 + +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.adoc text +*.gradle text +*.java text +*.puml text + +# Declare files that will always have CRLF line endings on checkout. +*.bat text eol=crlf + +# Declare files that will always have LF line endings on checkout. +*.directory text eol=lf +*.desktop text eol=lf +*.menu text eol=lf +*.sh text eol=lf +gradlew text eol=lf + +# Denote all files that are truly binary and should not be modified. +*.gif binary +*.jar binary +*.jpg binary +*.png binary +*.tar.gz binary diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..4c55945 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,47 @@ + + +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +## Affected version + +_Name the affected version, i.e. "release x.y.z" or "revision "._ + +... + +## Steps to reproduce + +_How can the incorrect behaviour be reproduced?_ + +1. ... +2. ... +3. ... + +### Expected behaviour + +_What would be the correct/expected behaviour?_ + +... + +### Actual behaviour + +_What behaviour can actually be observed instead of the correct behaviour?_ + +... + +### Additional information + +_Other data that can help with fixing the defect, e.g. logs, screenshots etc._ + +* Operating system and version: _e.g. Ubuntu Linux 20.04, Windows 10, ..._ +* Java distribution version: _e.g. Oracle Java Runtime Environment 13_ +* ... diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..314a135 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT + +--- +blank_issues_enabled: false +contact_links: + - name: Ask a question or get support + url: https://github.com/opentcs/opentcs/discussions + about: Ask a question or request support for using openTCS diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..9310cba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ + + +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +## User story / use case + +_A description of the proposed functionality. What is the purpose of this proposal? What problems would it solve? Are there already ideas for a solution?_ diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..c5a3f9e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,51 @@ + + +## Description + + + +... + +## Checklist + +- [ ] You have the right to contribute the code/documentation/assets to this project, and you agree to contribute it under the terms of the project's license(s). +- [ ] All changes have been made on a separate branch (not master) in a fork. +- [ ] The whole project compiles correctly and all tests pass, i.e. `gradlew clean build` succeeds with the changes made, and without unavoidable compiler/toolchain warnings. +- [ ] New tests covering the change have been added, if it is possible / makes sense. +- [ ] The documentation has been extended, if necessary. +- [ ] The PR contains a proposal for a _well-formed_ commit message that mentions all authors who contributed to the PR. + (The commits in the PR will be squashed into one commit on the base branch, and your proposed message is intended to be used for this commit. See below for a template you can use for the message. See [this blog post](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) and [this one](https://chris.beams.io/posts/git-commit/#seven-rules) for more on well-formed commit messages.) + +## Proposed squash commit message + + +``` +A short one-line summary (max. 50 characters) + +A more detailed explanation of the changes introduced by this merge +request. + +* You can use lists here, too. +* Each line should not exceed 72 characters. + +Co-authored-by: NAME +``` + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..ff6fb86 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT + +name: Gradle Build + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-22.04 + + steps: + - name: Check out repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # (=v4.1.1) + - name: Set up JDK 21 + uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # (=v4.0.0) + with: + java-version: '21' + distribution: 'temurin' + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@699bb18358f12c5b78b37bb0111d3a0e2276e0e2 # (=v2.1.1) + - name: Cache Gradle packages + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # (=v4.0.0) + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Build with Gradle + run: ./gradlew build + - name: Check code formatting + run: ./gradlew spotlessCheck + - name: Cleanup Gradle Cache + # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. + # Restoring these files from a GitHub Actions cache might cause problems for future builds. + run: | + rm -f ~/.gradle/caches/modules-2/modules-2.lock + rm -f ~/.gradle/caches/modules-2/gc.properties diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab4efd0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC0-1.0 + +/.idea/* +!/.idea/codeStyles +!/.idea/runConfigurations +/.gradle/ +/build/ +/opentcs-*/build/ +*~ + +# Ignore approval test result files. +*Test*.received.txt diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..bdd3c02 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,222 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT + +default: + image: eclipse-temurin:21-jdk-jammy + tags: + - opentcs-runner + before_script: + # Put GRADLE_USER_HOME into the cache directory so the wrapper and dependencies are not + # re-downloaded for every job. + - export GRADLE_USER_HOME=`pwd`/.gradle + timeout: 15 minutes + +include: '.gitlab/docker/codequality/version.yml' + +workflow: + rules: + - if: $CI_MERGE_REQUEST_IID + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + - if: $CI_PIPELINE_SOURCE == "web" + +variables: + GRADLE_OPTS: "-Dorg.gradle.daemon=false" + DO_BUILD_CODEQUALITY_IMAGE: + value: "false" + description: "Whether ('true') or not ('false') to (re-)build the Docker image for code quality jobs." + DO_DEPLOY_PRIVATE: + value: "false" + description: "Whether ('true') or not ('false') to deploy artifacts to private (GitLab) repository." + DO_DEPLOY_OSSRH: + value: "false" + description: "Whether ('true') or not ('false') to deploy artifacts to the public OSSRH repository, e.g. for release builds." + NO_BUILD_NUMBER: + value: "false" + description: "Whether ('true') or not ('false') to exclude the build number in the artifacts' version number, e.g. for release builds." + SIGNING_KEY: + value: "" + description: "The key to use for signing artifacts." + SIGNING_PASSWORD: + value: "" + description: "The password to use for signing artifacts." + DEPLOY_REPO_OSSRH_USERNAME: + value: "" + description: "User name for logging in with the public OSSRH repository." + DEPLOY_REPO_OSSRH_PASSWORD: + value: "" + description: "Password for logging in with the public OSSRH repository." + +stages: + - build + - test + - deploy + +build_code_quality_image: + stage: build + rules: + - if: $DO_BUILD_CODEQUALITY_IMAGE == "true" + image: + name: gcr.io/kaniko-project/executor:v1.9.1-debug + entrypoint: [""] + dependencies: [] + script: + - echo "Building code quality image with tag ${CODEQUALITY_IMAGE_TAG}..." + - mkdir -p /kaniko/.docker + - echo "{\"auths\":{\"${CI_REGISTRY}\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json + - '/kaniko/executor + --context "${CI_PROJECT_DIR}/.gitlab/docker/codequality" + --dockerfile "${CI_PROJECT_DIR}/.gitlab/docker/codequality/Dockerfile" + --destination "${CI_REGISTRY_IMAGE}/codequality:${CODEQUALITY_IMAGE_TAG}"' + +build: + stage: build + interruptible: true + dependencies: [] + script: + - ./gradlew -x check release testClasses -PNO_BUILD_NUMBER="$NO_BUILD_NUMBER" + artifacts: + name: 'opentcs-build-b$CI_PIPELINE_IID' + paths: + - .gradle + - opentcs-*/build/classes/ + - opentcs-*/build/generated/ + - opentcs-*/build/libs/ + - opentcs-*/build/resources/ + - opentcs-*/build/tmp/ + exclude: + - .gradle/.tmp/**/* + - .gradle/caches/**/* + - .gradle/wrapper/**/* + expire_in: 1 week + cache: + key: build_test_deploy-$(date +%Y-%m) + paths: + - .gradle + +test: + stage: test + interruptible: true + dependencies: + - build + script: + - echo "Touching build results to prevent Gradle rebuilding them..." + - find .gradle | xargs touch + - find . -regex '\./opentcs-[^/]+/build/.*' | xargs touch + - echo "Running unit tests..." + - ./gradlew test jacocoLogAggregatedCoverage -PNO_BUILD_NUMBER="$NO_BUILD_NUMBER" + coverage: '/Branch Coverage: ([0-9.]+)%/' + artifacts: + name: 'opentcs-test-b$CI_PIPELINE_IID' + paths: + # Results of (failed) approval tests. + - opentcs-*/**/*.received.txt + reports: + junit: opentcs-*/build/test-results/test/TEST-*.xml + when: always + expire_in: 1 week + cache: + key: build_test_deploy-$(date +%Y-%m) + paths: + - .gradle + +lint-spotless: + stage: test + interruptible: true + dependencies: [] + script: + - echo "Running Spotless check..." + - ./gradlew spotlessCheck + cache: + key: lint_spotless-$(date +%Y-%m) + paths: + - .gradle + +lint-checkstyle: + stage: test + interruptible: true + image: ${CI_REGISTRY_IMAGE}/codequality:${CODEQUALITY_IMAGE_TAG} + variables: + CHANGELOG_FILE: opentcs-documentation/src/docs/release-notes/changelog.adoc + MALFORMED_HEADERS_FILE: malformed_section_headers.txt + CODE_CLIMATE_FILE: gl-code-quality-report.json + dependencies: + - build + script: + - echo "Touching build results to prevent Gradle rebuilding them..." + - find .gradle | xargs touch + - find . -regex '\./opentcs-[^/]+/build/.*' | xargs touch + - echo "Checking for changelog headers that do not match 'Version x.y.z (yyyy-mm-dd)'..." + - '( grep --extended-regexp "^==[^=]" $CHANGELOG_FILE + | grep --invert-match --extended-regexp "^== Version [0-9]+\.[0-9]+(\.[0-9]+)? +\([0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]\)$" + > $MALFORMED_HEADERS_FILE ) + || true' + - 'if [ -s $MALFORMED_HEADERS_FILE -a $NO_BUILD_NUMBER = "true" ] ; then + echo "Found malformed changelog headers:" ; + cat $MALFORMED_HEADERS_FILE ; + exit 1 ; + fi' + - rm -f $MALFORMED_HEADERS_FILE + - echo "Running CheckStyle checks..." + - ./gradlew checkstyleMain checkstyleTest checkstyleGuiceConfig + - echo "Converting CheckStyle reports to CodeClimate report..." + - violations-command-line -cc $CODE_CLIMATE_FILE -print-violations false -diff-print-violations true -v "CHECKSTYLE" "." ".*checkstyle/.*\.xml$" "Checkstyle" + - sed -i.bak -e "s,$CI_PROJECT_DIR/,,g" $CODE_CLIMATE_FILE + artifacts: + reports: + codequality: $CODE_CLIMATE_FILE + when: always + expire_in: 1 week + cache: + key: lint_checkstyle-$(date +%Y-%m) + paths: + - .gradle + +lint-reuse: + stage: test + interruptible: true + dependencies: [] + image: + name: fsfe/reuse:4.0.3 + entrypoint: [""] + script: + - reuse lint + +deploy_private: + stage: deploy + rules: + - if: $DO_DEPLOY_PRIVATE == "true" + dependencies: + - build + script: + - './gradlew -x check publish + -PDO_DEPLOY_PRIVATE="true" + -PDO_DEPLOY_OSSRH="false" + -PNO_BUILD_NUMBER="$NO_BUILD_NUMBER" + -PSIGNING_KEY="$SIGNING_KEY" + -PSIGNING_PASSWORD="$SIGNING_PASSWORD"' + cache: + key: build_test_deploy-$(date +%Y-%m) + paths: + - .gradle + policy: pull + +deploy_ossrh: + stage: deploy + rules: + - if: $DO_DEPLOY_OSSRH == "true" + dependencies: + - build + script: + - './gradlew -x check publishToSonatype closeAndReleaseStagingRepositories + -PDO_DEPLOY_PRIVATE="false" + -PDO_DEPLOY_OSSRH="true" + -PNO_BUILD_NUMBER="$NO_BUILD_NUMBER" + -PSIGNING_KEY="$SIGNING_KEY" + -PSIGNING_PASSWORD="$SIGNING_PASSWORD" + -PDEPLOY_REPO_OSSRH_USERNAME="$DEPLOY_REPO_OSSRH_USERNAME" + -PDEPLOY_REPO_OSSRH_PASSWORD="$DEPLOY_REPO_OSSRH_PASSWORD"' + cache: + key: build_test_deploy-$(date +%Y-%m) + paths: + - .gradle + policy: pull diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS new file mode 100644 index 0000000..e5328a5 --- /dev/null +++ b/.gitlab/CODEOWNERS @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC0-1.0 + +# Reviews should be done by these. +* @mgrzenia @stwalter diff --git a/.gitlab/docker/codequality/Dockerfile b/.gitlab/docker/codequality/Dockerfile new file mode 100644 index 0000000..00ac25d --- /dev/null +++ b/.gitlab/docker/codequality/Dockerfile @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT + +# +# Remember to update version.yml when you change the image contents here! +# +FROM eclipse-temurin:21-jdk-jammy + +RUN DEBIAN_FRONTEND=noninteractive apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ + bash \ + nodejs \ + npm \ + sed \ + && rm -rf /var/lib/apt/lists/* + +RUN npm install -g violations-command-line@1.25.3 + +CMD ["/bin/bash"] diff --git a/.gitlab/docker/codequality/version.yml b/.gitlab/docker/codequality/version.yml new file mode 100644 index 0000000..68c0495 --- /dev/null +++ b/.gitlab/docker/codequality/version.yml @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT + +variables: + # Remember to update this whenever you change the image contents in Dockerfile! + CODEQUALITY_IMAGE_TAG: "1.2.0" diff --git a/.gitlab/issue_templates/Defect.md b/.gitlab/issue_templates/Defect.md new file mode 100644 index 0000000..19606f0 --- /dev/null +++ b/.gitlab/issue_templates/Defect.md @@ -0,0 +1,48 @@ + + +## Defect + + + +### Affected version + + + +### Steps to reproduce + + + +1. ... +2. ... +3. ... + +### Expected behaviour + + + +... + +### Actual behaviour + + + +... + +### Additional information (logs, screenshots etc.) + + + +... + +### Acceptance criteria + + + +* Actual behaviour corrected +* New tests added or existing tests improved to help prevent future defects +* Changelog entry added + +/label ~defect diff --git a/.gitlab/issue_templates/Enhancement.md b/.gitlab/issue_templates/Enhancement.md new file mode 100644 index 0000000..82f8922 --- /dev/null +++ b/.gitlab/issue_templates/Enhancement.md @@ -0,0 +1,32 @@ + + +## Enhancement + + + +### Current behaviour + + + +... + +### Improved behaviour + + + +... + +### Acceptance criteria + + + +* Behaviour improved +* New tests added or existing tests updated to help prevent defects of the changed code +* User documentation covers changed behaviour +* Developer documentation covers new feature +* Changelog entry added + +/label ~enhancement diff --git a/.gitlab/issue_templates/Feature.md b/.gitlab/issue_templates/Feature.md new file mode 100644 index 0000000..deb5fd3 --- /dev/null +++ b/.gitlab/issue_templates/Feature.md @@ -0,0 +1,26 @@ + + +## New feature + + + +### User story / use case + + + +... + +### Acceptance criteria + + + +* New feature implemented +* New tests added to help prevent defects of the new code +* User documentation covers new feature +* Developer documentation covers new feature +* Changelog entry added + +/label ~feature diff --git a/.gitlab/merge_request_templates/Default.md b/.gitlab/merge_request_templates/Default.md new file mode 100644 index 0000000..1e208e7 --- /dev/null +++ b/.gitlab/merge_request_templates/Default.md @@ -0,0 +1,48 @@ + + +## Related issues + + + +... + +## To do + + + +- [ ] Implement code changes +- [ ] Update documentation +- [ ] Add changelog entry + +## Proposed squash commit message + + +``` +A short one-line summary (max. 50 characters) + +* A more detailed explanation of the changes introduced by this merge + request. +* Each line should not exceed 72 characters. + +Co-authored-by: NAME +Reviewed-by: NAME +Acked-by: NAME +``` + diff --git a/.gitlab/renovate.json5 b/.gitlab/renovate.json5 new file mode 100644 index 0000000..d9add91 --- /dev/null +++ b/.gitlab/renovate.json5 @@ -0,0 +1,9 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ], + "assigneesFromCodeOwners": true, + "assigneesSampleSize": 1, + "dependencyDashboardApproval": true +} diff --git a/.gitlab/renovate.json5.license b/.gitlab/renovate.json5.license new file mode 100644 index 0000000..dd385f8 --- /dev/null +++ b/.gitlab/renovate.json5.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: The openTCS Authors +SPDX-License-Identifier: CC0-1.0 diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..481aafe --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,72 @@ + + + + \ No newline at end of file diff --git a/.idea/codeStyles/README.adoc b/.idea/codeStyles/README.adoc new file mode 100644 index 0000000..98cd3aa --- /dev/null +++ b/.idea/codeStyles/README.adoc @@ -0,0 +1,21 @@ += IntelliJ IDEA code style preferences + +This project uses https://github.com/diffplug/spotless[Spotless] for consistent formatting of the project's code and integrates the corresponding Gradle plugin, which can be used to apply the configured formatting rules. +Although this is already sufficient to enable development in this project, executing a Gradle task to format (new) code does not necessarily provide a fluent development experience (as this can take a few seconds). +To improve this situation when working with IntelliJ IDEA, IDE-specific configuration files can be found in the directory this file is located. +With these files, IDE-specific formatting is configured to be as close as possible to formatting with Spotless. + +== Updating IntelliJ IDEA code style preferences + +The formatting rules that Spotless applies are defined in `config/eclipse-formatter-preferences.xml`. +If the content of this file changes, the IDE-specific configuration files probably have to be updated as well. +To do this, go to `Settings -> Editor -> Code Style -> Java`, click on the settings icon next to the scheme selection and select `Import Scheme -> Eclipse XML Profile`. +Select the `eclipse-formatter-preferences.xml` file and be sure to select `To: Current Scheme` in the "Import Scheme" dialog. +In order for the updated configuration to be persisted in the configuration files, be sure to manually adjust one (any) setting, click "Apply", reset it to the original value and click "Apply" again. + +== Manual adjustments to the IntelliJ IDEA code style preferences + +Unfortunately, despite the import described above, a complete match between the formatting applied by Spotless and the IDE-specific formatting cannot be achieved. +The following code style preferences may have to be adjusted manually after an import: + +* `Wrapping and Braces -> Throws list -> Wrap if long` diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/openTCS_Kernel.xml b/.idea/runConfigurations/openTCS_Kernel.xml new file mode 100644 index 0000000..7d41bc0 --- /dev/null +++ b/.idea/runConfigurations/openTCS_Kernel.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + diff --git a/.idea/runConfigurations/openTCS_KernelControlCenter.xml b/.idea/runConfigurations/openTCS_KernelControlCenter.xml new file mode 100644 index 0000000..8b9ae75 --- /dev/null +++ b/.idea/runConfigurations/openTCS_KernelControlCenter.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + diff --git a/.idea/runConfigurations/openTCS_ModelEditor.xml b/.idea/runConfigurations/openTCS_ModelEditor.xml new file mode 100644 index 0000000..c4ff864 --- /dev/null +++ b/.idea/runConfigurations/openTCS_ModelEditor.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + diff --git a/.idea/runConfigurations/openTCS_OperationsDesk.xml b/.idea/runConfigurations/openTCS_OperationsDesk.xml new file mode 100644 index 0000000..f2c6b7f --- /dev/null +++ b/.idea/runConfigurations/openTCS_OperationsDesk.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc new file mode 100644 index 0000000..9a8fda8 --- /dev/null +++ b/CHANGELOG.adoc @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + += Changelog + +The changelog is maintained in link:./opentcs-documentation/src/docs/release-notes/changelog.adoc[opentcs-documentation/src/docs/release-notes/changelog.adoc]. diff --git a/CODE_OF_CONDUCT.adoc b/CODE_OF_CONDUCT.adoc new file mode 100644 index 0000000..4c679d5 --- /dev/null +++ b/CODE_OF_CONDUCT.adoc @@ -0,0 +1,129 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + += openTCS Code of Conduct + +== Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +== Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +== Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +== Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +== Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +stefan.walter@iml.fraunhofer.de and/or martin.grzenia@iml.fraunhofer.de. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +== Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +=== 1. Correction + +*Community Impact*: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +*Consequence*: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +=== 2. Warning + +*Community Impact*: A violation through a single incident or series +of actions. + +*Consequence*: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +=== 3. Temporary Ban + +*Community Impact*: A serious violation of community standards, including +sustained inappropriate behavior. + +*Consequence*: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +=== 4. Permanent Ban + +*Community Impact*: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +*Consequence*: A permanent ban from any sort of public interaction within +the community. + +== Attribution + +This Code of Conduct is adapted from the https://www.contributor-covenant.org[Contributor Covenant], +version 2.1, available at +https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. + +Community Impact Guidelines were inspired by +https://github.com/mozilla/diversity[Mozilla's code of conduct enforcement ladder]. + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available +at https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc new file mode 100644 index 0000000..ad5aa6d --- /dev/null +++ b/CONTRIBUTING.adoc @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + += Contributing to openTCS + +The following is a set of guidelines for contributing to openTCS. + +This project is maintained by the openTCS development team of https://www.iml.fraunhofer.de/en.html[Fraunhofer IML]. +A public mirror of the development repository is available at https://github.com/opentcs/opentcs[GitHub]. + +You are very welcome to contribute to this project when you find a bug, want to suggest an improvement, or have an idea for a useful feature. +For this, please always create an issue and/or a pull request, and follow our style guides as described below. + +== Issues + +It is required to create an issue if you want to integrate a bugfix, improvement, or feature. +Briefly and clearly describe the purpose of your contribution in the corresponding issue, using the appropriate template for it. + +== Pull requests / merge requests + +Pull requests fixing bugs, adding new features or improving the quality of the code and/or the documentation are very welcome! +To contribute changes, please follow these steps: + +. Before putting any significant amount of work into changes you want to make, get in touch with the maintainers. + Informing and coordinating helps to avoid conflicts with other changes made elsewhere. + If there already is an issue or a forum discussion related to the intended changes, announce your will to work on it there; if there isn't, yet, create one. + (Specific bug reports and feature/improvement suggestions should be handled in issues; loose ideas should be discussed in the forum first.) +. Fork the repository and create a new branch for your changes. +. Make your changes in the new branch, adhering to the coding guidelines. +. If your changes add a new feature, consider adding documentation for it to the user's guide or the developer's guide. + If your changes change an existing feature's behaviour, check these documents and update them if necessary. +. If your changes affect users, document them in the link:opentcs-documentation/src/docs/release-notes/changelog.adoc[changelog]. +. Push your branch to your forked repository. +. Open a pull request against the main repository's main branch. +.. Fill out the pull request template and provide a clear description of your changes and the problem they solve. +.. Provide a commit message for the pull request, adhering to the commit message guidelines described below. + +When contributing, keep unrelated changes in separate pull requests! + +=== Commit message guidelines + +When writing commit messages, please follow these guidelines: + +* Use descriptive and meaningful commit messages. +* Start the subject line with a verb in the imperative mood (e.g., "Add," "Fix," "Update"). +* Keep the subject line short and concise (preferably 50 characters or less). +* Provide additional details in the body if needed. +* Separate the subject from the body with a blank line. +* Keep the length of body lines less than or equal to 72. +* Reference relevant commit hashes in the commit message if applicable. + +Example: + +---- +Make vehicle energy level thresholds configurable + +Allow a vehicle's set of energy level thresholds to be modified during +runtime via the Operations Desk application and the web API. + +Co-authored-by: Martin Grzenia +Co-authored-by: Stefan Walter +---- + +== Coding guidelines + +In general, please adhere to the following guidelines to maintain code consistency, readability, and quality: + +* Write lean and efficient code. + Avoid unnecessary complexity or redundant logic. +* Use meaningful and descriptive names for classes, methods and variables. +* Write clear and concise comments when necessary, but strive for code that is self-explanatory. +* Prioritize code readability over cleverness. + +The following subsections contain a few more detailed guidelines we consider important in this project. +Note that parts of the existing code may not fully adhere to these rules, yet. +When updating such code, do improve it by applying the guidelines where it makes sense, but avoid modifying large unrelated sections. + +=== Primary formatting rules + +For consistent formatting of the project's code, https://github.com/diffplug/spotless[Spotless] is used. +After making changes, make sure you run `./gradlew spotlessApply` to re-format the code. + +=== Automatic tests + +* New or changed non-trivial code should be covered by tests. +* This project uses https://junit.org/[JUnit] for unit testing. +* JUnit test classes and methods should omit the `public` modifier unless there is a technical reason for adding it. +* For assertions, `assertThat()` should be preferred over `assertTrue()`, as the former provides more information when failing. + +=== Check preconditions in subroutines + +Methods belonging to a class's interface, i.e. `public` or `protected` methods, should check their preconditions. +They should at least check their input parameters for validity: + +* Reference types that may not be `null` should be checked using `java.util.Objects.requireNonNull()`. + It also makes sense to mark them using the `@Nonnull` annotation to document that they may not be `null`. +* Parameters of numeric types for which only certain ranges of values are acceptable should be checked using `org.opentcs.util.Assertions.checkInRange()`. +* Parameters for which only certain values are acceptable should be checked using `org.opentcs.util.Assertions.checkArgument()`. + +Checking the object's internal state may also make sense for a method. +For such checks, `org.opentcs.util.Assertions.checkState()` should be used. + +=== Avoid single-use local variables + +Declaring local variables in subroutines that are then used only once creates unnecessary noise for the reader of the code. +In many cases, eliminating the variable by inlining its value improves the readability of the code. + +== Development setup + +=== IDE: NetBeans + +To build the project from NetBeans, register a Java platform named "JDK 21 - openTCS" (without the quotes) within NetBeans. +This JDK will be used by NetBeans for running the build process. diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 0000000..137069b --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSES/CC-BY-4.0.txt b/LICENSES/CC-BY-4.0.txt new file mode 100644 index 0000000..13ca539 --- /dev/null +++ b/LICENSES/CC-BY-4.0.txt @@ -0,0 +1,156 @@ +Creative Commons Attribution 4.0 International + + Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. + +Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors. + +Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public. + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. + +Section 1 – Definitions. + + a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. + + d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. + + g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. + + i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. + +Section 2 – Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: + + A. reproduce and Share the Licensed Material, in whole or in part; and + + B. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. + + 3. Term. The term of this Public License is specified in Section 6(a). + + 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. + + 5. Downstream recipients. + + A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. + + B. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. + + 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). + +b. Other rights. + + 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this Public License. + + 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. + +Section 3 – License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified form), You must: + + A. retain the following if it is supplied by the Licensor with the Licensed Material: + + i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of warranties; + + v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; + + B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and + + C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. + + 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. + +Section 4 – Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; + + b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. +For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. + +Section 5 – Disclaimer of Warranties and Limitation of Liability. + + a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. + + b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. + + c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. + +Section 6 – Term and Termination. + + a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or + + 2. upon express reinstatement by the Licensor. + + c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. + + d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. + + e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. + +Section 7 – Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. + +Section 8 – Interpretation. + + a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. + + c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. + + d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. + +Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSES/CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/LICENSES/LGPL-2.1-only.txt b/LICENSES/LGPL-2.1-only.txt new file mode 100644 index 0000000..c6487f4 --- /dev/null +++ b/LICENSES/LGPL-2.1-only.txt @@ -0,0 +1,176 @@ +GNU LESSER GENERAL PUBLIC LICENSE + +Version 2.1, February 1999 + +Copyright (C) 1991, 1999 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] + +Preamble + +The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. + +This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. + +When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. + +We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. + +Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. + +When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. + +We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. + +For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. + +The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. + +GNU LESSER GENERAL PUBLIC LICENSE +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. + +You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + +3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. + +However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. + +When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. + +If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: + + a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. + + e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. + + b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + +This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + +NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). + +To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + one line to give the library's name and an idea of what it does. + Copyright (C) year name of author + + This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in +the library `Frob' (a library for tweaking knobs) written +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 +Ty Coon, President of Vice +That's all there is to it! diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 0000000..2071b23 --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSES/OFL-1.1.txt b/LICENSES/OFL-1.1.txt new file mode 100644 index 0000000..6fe84ee --- /dev/null +++ b/LICENSES/OFL-1.1.txt @@ -0,0 +1,43 @@ +SIL OPEN FONT LICENSE + +Version 1.1 - 26 February 2007 + +PREAMBLE + +The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. + +DEFINITIONS + +"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the copyright statement(s). + +"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, or substituting — in part or in whole — any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS + +Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. + +5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION + +This license becomes null and void if any of the above conditions are not met. + +DISCLAIMER + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..77eb519 --- /dev/null +++ b/README.adoc @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + += openTCS + +image:https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg[Contributor Covenant,link=CODE_OF_CONDUCT.md] + +* Homepage: https://www.opentcs.org/ +* Changelog: link:./opentcs-documentation/src/docs/release-notes/changelog.adoc[changelog.adoc] + +openTCS (short for _open Transportation Control System_) is a free platform for controlling fleets of https://en.wikipedia.org/wiki/Automated_guided_vehicle[automated guided vehicles (AGVs)] and mobile robots. +It should generally be possible to control any automatic vehicle with communication capabilities with it, but AGVs are the main target. + +openTCS is being maintained by the openTCS team at the https://www.iml.fraunhofer.de/[Fraunhofer Institute for Material Flow and Logistics]. + +The software runs on the Java platform version 21, with the recommended Java distribution being the one provided by the https://adoptium.net/[Adoptium project]. +All libraries required for compiling and/or using it are freely available, too. + +openTCS itself is not a complete product you can use out-of-the-box to control AGVs with. +Primarily, it is a framework/an implementation of the basic data structures and algorithms (routing, dispatching, scheduling) needed for running an AGV system with more than one vehicle. +It tries to be as generic as possible to allow interoperation with vehicles of practically any vendor. +Thus it is usually necessary to at least create and integrate a vehicle driver (called _communication adapter_ in openTCS-speak) that translates between the abstract interface of the openTCS kernel and the communication protocol your vehicle understands. +Depending on your needs, it might also be necessary to adapt algorithms or add project-specific strategies. + +== Getting started + +To get started with openTCS, please refer to the user's guide, the developer's guide and the API documentation. +These documents are included in the binary distribution and can also be read online on the https://www.opentcs.org/[openTCS homepage]. + +== Licensing + +This work is licensed under multiple licences. +Because keeping this section up-to-date is challenging, here is a brief summary as of November 2024: + +* All original source code is licensed under link:./LICENSES/MIT.txt[MIT]. +* All original assets, including documentation, is licensed under link:./LICENSES/CC-BY-4.0.txt[CC-BY-4.0]. +* Some configuration and data files are licensed under link:./LICENSES/CC0-1.0.txt[CC0-1.0]. +* Some third-party assets are licensed under link:./LICENSES/Apache-2.0.txt[Apache-2.0] or link:./LICENSES/OFL-1.1.txt[OFL-1.1]. + +For more accurate information, check the individual files as well as the `REUSE.toml` files. + +== Contributing + +You are very welcome to contribute to this project. +Please see link:./CONTRIBUTING.adoc[CONTRIBUTING.adoc] for a few guidelines related to this. diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000..36676c1 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC0-1.0 + +version = 1 + +[[annotations]] +path = ["**/gradle.properties"] +precedence = "closest" +SPDX-FileCopyrightText = "The openTCS Authors" +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = ["**/*Test*.approved.txt"] +precedence = "closest" +SPDX-FileCopyrightText = "The openTCS Authors" +SPDX-License-Identifier = "CC-BY-4.0" + +[[annotations]] +path = ["**/*.form"] +precedence = "closest" +SPDX-FileCopyrightText = "The openTCS Authors" +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = [".idea/**"] +precedence = "closest" +SPDX-FileCopyrightText = "The openTCS Authors" +SPDX-License-Identifier = "CC0-1.0" diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..f3c11a2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +buildscript { + repositories { + mavenLocal() + mavenCentral() + } +} + +plugins { + id 'maven-publish' + id 'signing' + id 'org.barfuin.gradle.jacocolog' version '3.1.0' + id 'com.github.jk1.dependency-license-report' version '2.9' + id 'com.diffplug.spotless' version '7.0.0.BETA4' + id 'io.github.gradle-nexus.publish-plugin' version '2.0.0' +} + +import com.github.jk1.license.filter.LicenseBundleNormalizer +import com.github.jk1.license.render.CsvReportRenderer +import com.github.jk1.license.render.InventoryHtmlReportRenderer + +apply plugin: 'base' // To add "clean" task to the root project. +apply plugin: 'distribution' + +apply from: "${rootDir}/gradle/common.gradle" +apply from: "${rootDir}/gradle/publishing-gitlab.gradle" +apply from: "${rootDir}/gradle/publishing-ossrh.gradle" + +subprojects { + apply from: rootProject.file('gradle/common.gradle') +} + +repositories { + mavenLocal() + mavenCentral() +} + +distributions { + main { + contents.from { + project(':opentcs-kernel').ext.collectableDistDir + } + contents.from { + project(':opentcs-kernelcontrolcenter').ext.collectableDistDir + } + contents.from { + project(':opentcs-modeleditor').ext.collectableDistDir + } + contents.from { + project(':opentcs-operationsdesk').ext.collectableDistDir + } + contents.from { + project(':opentcs-documentation').ext.collectableDistDir + } + } +} + +task subDists { + dependsOn(':opentcs-kernel:installDist') + dependsOn(':opentcs-kernelcontrolcenter:installDist') + dependsOn(':opentcs-modeleditor:installDist') + dependsOn(':opentcs-operationsdesk:installDist') + dependsOn(':opentcs-documentation:installDist') +} + +installDist.dependsOn subDists + +distZip { + archiveClassifier = 'bin' + dependsOn subDists +} + +distTar { + enabled = false + archiveClassifier = 'bin' + dependsOn subDists +} + + + +task distSrcZip(type: Zip) { + archiveClassifier = 'src' + from "${rootDir}" + + includes << 'config/**' + includes << 'gradle/**' + includes << 'opentcs-*/**' + includes << 'src/**' + includes << '*.gradle' + includes << 'gradlew' + includes << 'gradlew.bat' + + excludes << '.gitlab' + excludes << '.gitlab-ci.yml' + excludes << '.gradle' + excludes << '**/build' +} + +build { + subprojects.each { dependsOn("${it.name}:build") } + dependsOn installDist +} + +task release { + dependsOn build + subprojects.each { dependsOn("${it.name}:release") } + dependsOn distZip + dependsOn distSrcZip +} + +licenseReport { + outputDir = "${buildDir}/license-report" + configurations = ['runtimeClasspath', 'guiceConfigRuntimeClasspath'] + excludeBoms = true + filters = [new LicenseBundleNormalizer(bundlePath: "$projectDir/config/license-normalizer-bundle.json")] + renderers = [ + new CsvReportRenderer('third-party-licenses.csv'), + new InventoryHtmlReportRenderer('third-party-licenses.html') + ] +} diff --git a/config/checkstyle/checkstyle-noframes-severity-sorted.xsl b/config/checkstyle/checkstyle-noframes-severity-sorted.xsl new file mode 100644 index 0000000..1a80526 --- /dev/null +++ b/config/checkstyle/checkstyle-noframes-severity-sorted.xsl @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

CheckStyle Audit

Designed for use with CheckStyle and Ant.
+
+ + + +
+ + + +
+ + + + +
+ + + + +
+ + + + +

Files

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameErrorsWarningsInfos
+
+ + + + +

File

+ + + + + + + + + + + + + + + + +
SeverityError DescriptionLine
+ Back to top +
+ + + +

Summary

+ + + + + + + + + + + + + + + + + + +
FilesErrorsWarningsInfos
+
+ + + + a + b + + +
diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..517199b --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,332 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/eclipse-formatter-preferences.xml b/config/eclipse-formatter-preferences.xml new file mode 100644 index 0000000..181874c --- /dev/null +++ b/config/eclipse-formatter-preferences.xml @@ -0,0 +1,401 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/eclipse-formatter-preferences.xml.license b/config/eclipse-formatter-preferences.xml.license new file mode 100644 index 0000000..dd385f8 --- /dev/null +++ b/config/eclipse-formatter-preferences.xml.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: The openTCS Authors +SPDX-License-Identifier: CC0-1.0 diff --git a/config/license-normalizer-bundle.json b/config/license-normalizer-bundle.json new file mode 100644 index 0000000..79f978c --- /dev/null +++ b/config/license-normalizer-bundle.json @@ -0,0 +1,10 @@ +{ + "bundles": [ + ], + "transformationRules": [ + { + "bundleName": "LGPL-2.1-only", + "licenseNamePattern": "GNU Lesser General Public License Version 2.1, February 1999" + } + ] +} diff --git a/config/license-normalizer-bundle.json.license b/config/license-normalizer-bundle.json.license new file mode 100644 index 0000000..dd385f8 --- /dev/null +++ b/config/license-normalizer-bundle.json.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: The openTCS Authors +SPDX-License-Identifier: CC0-1.0 diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..c38dd20 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,9 @@ +netbeans.license=default +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=100 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project +netbeans.hint.jdkPlatform=JDK_21_-_openTCS diff --git a/gradle/common.gradle b/gradle/common.gradle new file mode 100644 index 0000000..a942648 --- /dev/null +++ b/gradle/common.gradle @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +// +// This file is to be applied to every subproject. +// + +// If we do not have a build number, we're building on a developer's system, so +// mark the artifact as a snapshot build. +def versionBuild = "SNAPSHOT" +if (System.env.BUILD_NUMBER) { + versionBuild = "b" + System.env.BUILD_NUMBER +} +else if (System.env.CI_PIPELINE_IID) { + versionBuild = "b" + System.env.CI_PIPELINE_IID +} + +// Semantic versioning: +// - The major version number should be incremented with major API-breaking +// changes. +// - The minor version number should be incremented when new feature were added. +// - The patch level should be incremented with every small change to the code +// (e.g. bugfixes). +project.version = "6.2.0" +if (!(project.hasProperty("NO_BUILD_NUMBER") + && Boolean.valueOf(project.getProperties().get("NO_BUILD_NUMBER")))) { + project.version += "-$versionBuild" +} + +project.ext.buildDate = new Date().format('yyyy-MM-dd HH:mm:ss') + +group = 'org.opentcs' + +task createFolders(description: 'Creates the source folders if they do not exist.') doLast { + sourceSets*.allSource*.srcDirs*.each { File srcDir -> + if (!srcDir.isDirectory()) { + println "Creating source folder: ${srcDir}" + srcDir.mkdirs() + } + } +} diff --git a/gradle/guice-application.gradle b/gradle/guice-application.gradle new file mode 100644 index 0000000..6bcc9d8 --- /dev/null +++ b/gradle/guice-application.gradle @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply from: "${rootDir}/gradle/guice-project.gradle" +apply plugin: 'application' + +run { + dependsOn installDist + workingDir new File(new File(buildDir, 'install'), project.name) + enableAssertions true + classpath sourceSets.guiceConfig.output +} + +task(debug, type:JavaExec) { + dependsOn installDist + + doFirst { + workingDir = run.workingDir + enableAssertions = run.enableAssertions + + main = run.main + args = run.args + classpath = run.classpath + jvmArgs = run.jvmArgs + systemProperties = run.systemProperties + + debug true + } +} diff --git a/gradle/guice-project.gradle b/gradle/guice-project.gradle new file mode 100644 index 0000000..087df49 --- /dev/null +++ b/gradle/guice-project.gradle @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +sourceSets { + guiceConfig +} + +configurations { + guiceConfigApi.extendsFrom api + guiceConfigImplementation.extendsFrom implementation +} + +dependencies { + guiceConfigImplementation sourceSets.main.runtimeClasspath +} + +// Attributes for the AsciiDoc documentation to include code from source files +ext.guiceSrcDir = sourceSets.guiceConfig.java.srcDirs[0] + +compileGuiceConfigJava { + options.release = 21 + options.compilerArgs << "-Werror" + options.compilerArgs << "-Xlint:all" + options.compilerArgs << "-Xlint:-serial" +} + +jar { + from sourceSets.guiceConfig.output + // This merely tells NetBeans where to look for classes in case of other + // subprojects depending on this one. By default, it only scans 'main'. + ext.netBeansSourceSets = [sourceSets.guiceConfig, sourceSets.main] +} diff --git a/gradle/java-codequality.gradle b/gradle/java-codequality.gradle new file mode 100644 index 0000000..810d7d2 --- /dev/null +++ b/gradle/java-codequality.gradle @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply plugin: 'checkstyle' + +checkstyle { + toolVersion = '10.18.2' + configFile = rootProject.file("config/checkstyle/checkstyle.xml") + showViolations = false +} + +project.afterEvaluate { project -> + project.tasks.withType(Checkstyle) { + reports { + html.stylesheet resources.text.fromFile(rootProject.file("config/checkstyle/checkstyle-noframes-severity-sorted.xsl")) + } + } +} diff --git a/gradle/java-project.gradle b/gradle/java-project.gradle new file mode 100644 index 0000000..864771a --- /dev/null +++ b/gradle/java-project.gradle @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply plugin: 'java-library' +apply plugin: 'jacoco' +apply plugin: 'com.diffplug.spotless' + +base.archivesName = name.toLowerCase() + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation group: 'org.slf4j', name: 'slf4j-api', version: '2.0.16' + + compileOnly group: 'jakarta.inject', name: 'jakarta.inject-api', version: '2.0.1' + + compileOnly group: 'jakarta.annotation', name: 'jakarta.annotation-api', version: '3.0.0' + testCompileOnly group: 'jakarta.annotation', name: 'jakarta.annotation-api', version: '3.0.0' + + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.11.2' + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: '5.11.2' + testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.11.2' + testRuntimeOnly group: 'org.junit.platform', name: 'junit-platform-launcher', version: '1.11.2' + + testImplementation group: 'org.hamcrest', name: 'hamcrest', version: '3.0' + testImplementation group: 'org.mockito', name: 'mockito-core', version: '5.14.2' + testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.26.3' + testImplementation group: 'com.approvaltests', name: 'approvaltests', version: '24.8.0' + + testRuntimeOnly group: 'org.slf4j', name: 'slf4j-jdk14', version: '2.0.16' +} + +compileJava { + options.release = 21 + options.compilerArgs << "-Werror" + options.compilerArgs << "-Xlint:all" + options.compilerArgs << "-Xlint:-serial" +} + +compileTestJava { + options.release = 21 + options.compilerArgs << "-Werror" + options.compilerArgs << "-Xlint:all" + options.compilerArgs << "-Xlint:-serial" +} + +javadoc { + title = "openTCS ${project.version} API documentation: ${project.name}" + + options { + header = "openTCS ${project.version}" + overview = "${projectDir}/src/main/java/overview.html" + addBooleanOption('Werror', true) + addBooleanOption('Xdoclint:all,-missing', true) + } +} + +task sourcesJar(type: Jar, dependsOn: classes, description: 'Creates a jar from the source files.') { + archiveClassifier = 'sources' + from sourceSets.main.allSource +} + +test { + useJUnitPlatform() + // ignoreFailures = true + systemProperties.put("java.awt.headless", "true") +} + +ext { + // Attributes for the AsciiDoc documentation to include code from source files + javaSrcDir = sourceSets.main.java.srcDirs[0] + javaClassesDir = sourceSets.main.output.classesDirs + testSrcDir = sourceSets.test.java.srcDirs[0] +} + +spotless { + java { + // Use the default import order configuration + importOrder() + + // Use the Eclipse JDT formatter + eclipse('4.26').configFile("${rootDir}/config/eclipse-formatter-preferences.xml") + } +} diff --git a/gradle/publishing-gitlab.gradle b/gradle/publishing-gitlab.gradle new file mode 100644 index 0000000..addebed --- /dev/null +++ b/gradle/publishing-gitlab.gradle @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +publishing { + repositories { + if (Boolean.valueOf(project.findProperty('DO_DEPLOY_PRIVATE')) + && System.getenv('CI_API_V4_URL') != null + && System.getenv('CI_PROJECT_ID') != null + && System.getenv('CI_JOB_TOKEN') != null) { + + maven { + name = 'deploy-repo-gitlab' + url = "${System.env.CI_API_V4_URL}/projects/${System.env.CI_PROJECT_ID}/packages/maven" + + credentials(HttpHeaderCredentials) { + name = 'Job-Token' + value = "${System.env.CI_JOB_TOKEN}" + } + authentication { + header(HttpHeaderAuthentication) + } + } + + } + } +} diff --git a/gradle/publishing-java.gradle b/gradle/publishing-java.gradle new file mode 100644 index 0000000..c87e280 --- /dev/null +++ b/gradle/publishing-java.gradle @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply from: "${rootDir}/gradle/signing.gradle" + +// Enable javadoc and sources JARs to be created. +java { + withJavadocJar() + withSourcesJar() +} + +publishing { + publications { + create(project.name + '_mavenJava', MavenPublication) { + from(components.java) + + pom { + // Override artifactId since project.name is used by default and is mixed-case. + artifactId = project.name.toLowerCase() + + name = project.name + description = project.name + url = "https://www.opentcs.org/" + + licenses { + license { + name = "MIT License" + url = "https://opensource.org/license/mit" + } + } + + developers { + developer { + name = "The openTCS Authors" + email = "info@opentcs.org" + organization = "The open Transportation Control System" + organizationUrl = "https://www.opentcs.org/" + } + } + + scm { + connection = "scm:git:git://github.com/opentcs/opentcs.git" + developerConnection = "scm:git:ssh://github.com:opentcs/opentcs.git" + url = "https://github.com/opentcs/opentcs" + } + } + } + } +} diff --git a/gradle/publishing-ossrh.gradle b/gradle/publishing-ossrh.gradle new file mode 100644 index 0000000..4cc7740 --- /dev/null +++ b/gradle/publishing-ossrh.gradle @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +nexusPublishing { + if (Boolean.valueOf(project.findProperty('DO_DEPLOY_OSSRH')) + && project.hasProperty('DEPLOY_REPO_OSSRH_USERNAME') + && project.hasProperty('DEPLOY_REPO_OSSRH_PASSWORD')) { + repositories { + sonatype { + nexusUrl.set(uri('https://s01.oss.sonatype.org/service/local/')) + snapshotRepositoryUrl.set(uri('https://s01.oss.sonatype.org/content/repositories/snapshots/')) + + username = project.property('DEPLOY_REPO_OSSRH_USERNAME') + password = project.property('DEPLOY_REPO_OSSRH_PASSWORD') + } + } + } +} diff --git a/gradle/signing.gradle b/gradle/signing.gradle new file mode 100644 index 0000000..989932a --- /dev/null +++ b/gradle/signing.gradle @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +signing { + useInMemoryPgpKeys( + project.hasProperty('SIGNING_KEY') ? project.property('SIGNING_KEY') : '', + project.hasProperty('SIGNING_PASSWORD') ? project.property('SIGNING_PASSWORD') : '' + ) + sign publishing.publications +} + +tasks.withType(Sign) { + onlyIf { + project.hasProperty('SIGNING_KEY') && !project.property('SIGNING_KEY').toString().isEmpty() \ + && project.hasProperty('SIGNING_PASSWORD') && !project.property('SIGNING_PASSWORD').toString().isEmpty() + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.jar.license b/gradle/wrapper/gradle-wrapper.jar.license new file mode 100644 index 0000000..770d9ab --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.jar.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: The Gradle Authors +SPDX-License-Identifier: Apache-2.0 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..df97d72 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradle/wrapper/gradle-wrapper.properties.license b/gradle/wrapper/gradle-wrapper.properties.license new file mode 100644 index 0000000..770d9ab --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: The Gradle Authors +SPDX-License-Identifier: Apache-2.0 diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..f5feea6 --- /dev/null +++ b/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/opentcs-api-base/build.gradle b/opentcs-api-base/build.gradle new file mode 100644 index 0000000..f516d8d --- /dev/null +++ b/opentcs-api-base/build.gradle @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply from: "${rootDir}/gradle/java-project.gradle" +apply from: "${rootDir}/gradle/java-codequality.gradle" +apply from: "${rootDir}/gradle/publishing-java.gradle" + +task release { + dependsOn build +} diff --git a/opentcs-api-base/gradle.properties b/opentcs-api-base/gradle.properties new file mode 100644 index 0000000..14431ac --- /dev/null +++ b/opentcs-api-base/gradle.properties @@ -0,0 +1,40 @@ +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineMethodParams=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAfterDotInChainedMethodCalls=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineDisjunctiveCatchTypes=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineFor=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineImplements=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.sortMembersByVisibility=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.visibilityOrder=PUBLIC;PROTECTED;DEFAULT;PRIVATE +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.enable-indent=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineArrayInit=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineCallArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.keepGettersAndSettersTogether=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classMembersOrder=STATIC FIELD;FIELD;STATIC_INIT;CONSTRUCTOR;INSTANCE_INIT;STATIC METHOD;METHOD;STATIC CLASS;CLASS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapCommentText=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.importGroupsOrder=* +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize=4 +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineAnnotationArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineTryResources=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.preserveNewLinesInComments=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineParenthesized=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineThrows=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=100 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/CredentialsException.java b/opentcs-api-base/src/main/java/org/opentcs/access/CredentialsException.java new file mode 100644 index 0000000..0f74611 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/CredentialsException.java @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access; + +import java.io.Serializable; + +/** + * Thrown when there are insufficient user permissions to perform an operation. + */ +public class CredentialsException + extends + KernelRuntimeException + implements + Serializable { + + /** + * Constructs a CredentialsException with no detail message. + */ + public CredentialsException() { + super(); + } + + /** + * Constructs a CredentialsException with the specified detail message. + * + * @param message The detail message. + */ + public CredentialsException(String message) { + super(message); + } + + /** + * Constructs a CredentialsException with the specified detail message and + * cause. + * + * @param message The detail message. + * @param cause The exception's cause. + */ + public CredentialsException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a CredentialsException with the specified cause and a detail + * message of (cause == null ? null : cause.toString()) (which + * typically contains the class and detail message of cause). + * + * @param cause The exception's cause. + */ + public CredentialsException(Throwable cause) { + super(cause); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/Kernel.java b/opentcs-api-base/src/main/java/org/opentcs/access/Kernel.java new file mode 100644 index 0000000..d480640 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/Kernel.java @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access; + +/** + * Declares the methods an openTCS kernel implements. + */ +public interface Kernel { + + /** + * The default name used for the empty model created on startup. + */ + String DEFAULT_MODEL_NAME = "unnamed"; + + /** + * Returns the current state of the kernel. + * + * @return The current state of the kernel. + * @throws CredentialsException If the calling client is not allowed to + * execute this method. + */ + State getState() + throws CredentialsException; + + /** + * Sets the current state of the kernel. + *

+ * Note: This method should only be used internally by the Kernel application. + *

+ * + * @param newState The state the kernel is to be set to. + * @throws IllegalArgumentException If setting the new state is not possible, + * e.g. because a transition from the current to the new state is not allowed. + * @throws CredentialsException If the calling client is not allowed to + * execute this method. + */ + void setState(State newState) + throws IllegalArgumentException, + CredentialsException; + + /** + * The various states a kernel instance may be running in. + */ + enum State { + + /** + * The state in which the model/topology is created and parameters are set. + */ + MODELLING, + /** + * The normal mode of operation in which transport orders may be accepted + * and dispatched to vehicles. + */ + OPERATING, + /** + * A transitional state the kernel is in while shutting down. + */ + SHUTDOWN + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/KernelException.java b/opentcs-api-base/src/main/java/org/opentcs/access/KernelException.java new file mode 100644 index 0000000..0c128d4 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/KernelException.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access; + +import java.io.Serializable; + +/** + * An exception thrown by the openTCS kernel. + */ +public class KernelException + extends + Exception + implements + Serializable { + + /** + * Constructs a new instance with no detail message. + */ + public KernelException() { + super(); + } + + /** + * Constructs a new instance with the specified detail message. + * + * @param message The detail message. + */ + public KernelException(String message) { + super(message); + } + + /** + * Constructs a new instance with the specified detail message and cause. + * + * @param message The detail message. + * @param cause The exception's cause. + */ + public KernelException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new instance with the specified cause and a detail + * message of (cause == null ? null : cause.toString()) (which + * typically contains the class and detail message of cause). + * + * @param cause The exception's cause. + */ + public KernelException(Throwable cause) { + super(cause); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/KernelRuntimeException.java b/opentcs-api-base/src/main/java/org/opentcs/access/KernelRuntimeException.java new file mode 100644 index 0000000..a66aa16 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/KernelRuntimeException.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access; + +import java.io.Serializable; + +/** + * A runtime exception thrown by the openTCS kernel. + */ +public class KernelRuntimeException + extends + RuntimeException + implements + Serializable { + + /** + * Constructs a new instance with no detail message. + */ + public KernelRuntimeException() { + super(); + } + + /** + * Constructs a new instance with the specified detail message. + * + * @param message The detail message. + */ + public KernelRuntimeException(String message) { + super(message); + } + + /** + * Constructs a new instance with the specified detail message and cause. + * + * @param message The detail message. + * @param cause The exception's cause. + */ + public KernelRuntimeException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new instance with the specified cause and a detail + * message of (cause == null ? null : cause.toString()) (which + * typically contains the class and detail message of cause). + * + * @param cause The exception's cause. + */ + public KernelRuntimeException(Throwable cause) { + super(cause); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/KernelServicePortal.java b/opentcs-api-base/src/main/java/org/opentcs/access/KernelServicePortal.java new file mode 100644 index 0000000..4675d7f --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/KernelServicePortal.java @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access; + +import jakarta.annotation.Nonnull; +import java.util.List; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.components.kernel.services.NotificationService; +import org.opentcs.components.kernel.services.PeripheralDispatcherService; +import org.opentcs.components.kernel.services.PeripheralJobService; +import org.opentcs.components.kernel.services.PeripheralService; +import org.opentcs.components.kernel.services.PlantModelService; +import org.opentcs.components.kernel.services.QueryService; +import org.opentcs.components.kernel.services.RouterService; +import org.opentcs.components.kernel.services.TransportOrderService; +import org.opentcs.components.kernel.services.VehicleService; + +/** + * Provides clients access to kernel services. + */ +public interface KernelServicePortal { + + /** + * Logs in with/establishes a connection to the remote kernel service portal. + * + * @param hostName The host on which the remote portal is running. + * @param port The port at which we can reach the remote RMI registry. + * @throws KernelRuntimeException If there was a problem logging in with the remote portal. + */ + void login( + @Nonnull + String hostName, + int port + ) + throws KernelRuntimeException; + + /** + * Logs out from/clears the connection to the remote kernel service portal. + * + * @throws KernelRuntimeException If there was a problem logging out from the remote portal, i.e. + * it is no longer available. + */ + void logout() + throws KernelRuntimeException; + + /** + * Returns the current state of the kernel. + * + * @return The current state of the kernel. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + Kernel.State getState() + throws KernelRuntimeException; + + /** + * Fetches events buffered for the client. + * + * @param timeout A timeout (in ms) for which to wait for events to arrive. + * @return A list of events (in the order they arrived). + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + List fetchEvents(long timeout) + throws KernelRuntimeException; + + /** + * Publishes an event. + * + * @param event The event to be published. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void publishEvent(Object event) + throws KernelRuntimeException; + + /** + * Returns the service a client can use to access methods regarding the plant model. + * + * @return The service a client can use to access methods regarding the plant model. + */ + @Nonnull + PlantModelService getPlantModelService(); + + /** + * Returns the service a client can use to access methods regarding transport orders and order + * sequences. + * + * @return The service a client can use to access methods regarding transport orders and order + * sequences. + */ + @Nonnull + TransportOrderService getTransportOrderService(); + + /** + * Returns the service a client can use to access methods regarding vehicles. + * + * @return The service a client can use to access methods regarding vehicles. + */ + @Nonnull + VehicleService getVehicleService(); + + /** + * Returns the service a client can use to access methods regarding user notifications. + * + * @return The service a client can use to access methods regarding user notifications. + */ + @Nonnull + NotificationService getNotificationService(); + + /** + * Returns the service a client can use to access methods regarding the dispatcher. + * + * @return The service a client can use to access methods regarding the dispatcher. + */ + @Nonnull + DispatcherService getDispatcherService(); + + /** + * Returns the service a client can use to access methods regarding the router. + * + * @return The service a client can use to access methods regarding the router. + */ + @Nonnull + RouterService getRouterService(); + + /** + * Returns the service a client can use to access methods for generic queries. + * + * @return The service a client can use to access methods for generic queries. + */ + @Nonnull + QueryService getQueryService(); + + /** + * Returns the service a client can use to access methods regarding peripherals. + * + * @return The service a client can use to access methods regarding peripherals. + */ + @Nonnull + PeripheralService getPeripheralService(); + + /** + * Returns the service a client can use to access methods regarding peripheral jobs. + * + * @return The service a client can use to access methods regarding peripheral jobs. + */ + @Nonnull + PeripheralJobService getPeripheralJobService(); + + /** + * Returns the service a client can use to access methods regarding the peripheral dispatcher. + * + * @return The service a client can use to access methods regarding the peripheral dispatcher. + */ + @Nonnull + PeripheralDispatcherService getPeripheralDispatcherService(); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/KernelStateTransitionEvent.java b/opentcs-api-base/src/main/java/org/opentcs/access/KernelStateTransitionEvent.java new file mode 100644 index 0000000..f2c9215 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/KernelStateTransitionEvent.java @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access; + +import java.io.Serializable; + +/** + * Emitted by/for kernel state changes. + */ +public class KernelStateTransitionEvent + implements + Serializable { + + /** + * The old state the kernel is leaving. + */ + private final Kernel.State leftState; + /** + * The new state the kernel is in transition to. + */ + private final Kernel.State enteredState; + /** + * Whether the transition to the entered state is finished or not. + */ + private final boolean transitionFinished; + + /** + * Creates a new TCSKernelStateEvent. + * + * @param leftState The previous state of the kernel. + * @param enteredState The new state of the kernel. + * @param transitionFinished Whether the transistion is finished, yet. + */ + public KernelStateTransitionEvent( + Kernel.State leftState, + Kernel.State enteredState, + boolean transitionFinished + ) { + this.leftState = leftState; + this.enteredState = enteredState; + this.transitionFinished = transitionFinished; + } + + /** + * Returns the state the kernel is leaving. + * + * @return The state the kernel is leaving. + */ + public Kernel.State getLeftState() { + return leftState; + } + + /** + * Returns the state for which this event was generated. + * + * @return The state for which this event was generated. + */ + public Kernel.State getEnteredState() { + return enteredState; + } + + /** + * Returns true if, and only if, the transition to the new kernel + * state is finished. + * + * @return true if, and only if, the transition to the new kernel + * state is finished. + */ + public boolean isTransitionFinished() { + return transitionFinished; + } + + @Override + public String toString() { + return getClass().getSimpleName() + + '{' + + "leftState=" + leftState + + ", enteredState=" + enteredState + + ", transitionFinished=" + transitionFinished + + '}'; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/LocalKernel.java b/opentcs-api-base/src/main/java/org/opentcs/access/LocalKernel.java new file mode 100644 index 0000000..23629a9 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/LocalKernel.java @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access; + +import org.opentcs.components.Lifecycle; +import org.opentcs.components.kernel.KernelExtension; + +/** + * Declares the methods the openTCS kernel must provide which are not accessible + * to remote peers. + */ +public interface LocalKernel + extends + Kernel, + Lifecycle { + + /** + * Adds a KernelExtension to this kernel. + * + * @param newExtension The extension to be added. + */ + void addKernelExtension(KernelExtension newExtension); + + /** + * Removes a KernelExtension from this kernel. + * + * @param rmExtension The extension to be removed. + */ + void removeKernelExtension(KernelExtension rmExtension); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/ModelTransitionEvent.java b/opentcs-api-base/src/main/java/org/opentcs/access/ModelTransitionEvent.java new file mode 100644 index 0000000..3d1332e --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/ModelTransitionEvent.java @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access; + +import java.io.Serializable; + +/** + * Emitted when the kernel loads a model. + */ +public class ModelTransitionEvent + implements + Serializable { + + /** + * The old model the kernel is leaving. + */ + private final String oldModelName; + /** + * The new model the kernel is in transition to. + */ + private final String newModelName; + /** + * Whether the content of the model actually changed with the transition. + */ + private final boolean modelContentChanged; + /** + * Whether the transition to the entered state is finished or not. + */ + private final boolean transitionFinished; + + /** + * Creates a new TCSModelTransitionEvent. + * + * @param oldModelName The name of the previously loaded model. + * @param newModelName The name of the new model. + * @param modelContentChanged Whether the content of the model actually + * changed with the transition. + * @param transitionFinished Whether the transition to the new model is + * finished, yet. + */ + public ModelTransitionEvent( + String oldModelName, + String newModelName, + boolean modelContentChanged, + boolean transitionFinished + ) { + this.oldModelName = oldModelName; + this.newModelName = newModelName; + this.modelContentChanged = modelContentChanged; + this.transitionFinished = transitionFinished; + } + + /** + * Returns the model name the kernel is leaving. + * + * @return The model the kernel is leaving. + */ + public String getOldModelName() { + return oldModelName; + } + + /** + * Returns the model for which this event was generated. + * + * @return The model for which this event was generated. + */ + public String getNewModelName() { + return newModelName; + } + + /** + * Returns true if, and only if, the content of the model + * actually changed with the transition. + * + * @return true if, and only if, the content of the model + * actually changed with the transition. + */ + public boolean hasModelContentChanged() { + return modelContentChanged; + } + + /** + * Returns true if, and only if, the transition to the new kernel + * state is finished. + * + * @return true if, and only if, the transition to the new kernel + * state is finished. + */ + public boolean isTransitionFinished() { + return transitionFinished; + } + + @Override + public String toString() { + return getClass().getSimpleName() + + '{' + + "oldModelName=" + oldModelName + + ", newModelName=" + newModelName + + ", modelContentChanged=" + modelContentChanged + + ", transitionFinished=" + transitionFinished + + '}'; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/NotificationPublicationEvent.java b/opentcs-api-base/src/main/java/org/opentcs/access/NotificationPublicationEvent.java new file mode 100644 index 0000000..439d457 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/NotificationPublicationEvent.java @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access; + +import static java.util.Objects.requireNonNull; + +import java.io.Serializable; +import org.opentcs.data.notification.UserNotification; + +/** + * Instances of this class represent events emitted by/for notifications being published. + */ +public class NotificationPublicationEvent + implements + Serializable { + + /** + * The published message. + */ + private final UserNotification notification; + + /** + * Creates a new instance. + * + * @param message The message being published. + */ + public NotificationPublicationEvent(UserNotification message) { + this.notification = requireNonNull(message, "notification"); + } + + /** + * Returns the message being published. + * + * @return The message being published. + */ + public UserNotification getNotification() { + return notification; + } + + @Override + public String toString() { + return getClass().getSimpleName() + + '{' + + "notification=" + notification + + '}'; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/SharedKernelServicePortal.java b/opentcs-api-base/src/main/java/org/opentcs/access/SharedKernelServicePortal.java new file mode 100644 index 0000000..7bdf5bb --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/SharedKernelServicePortal.java @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access; + +/** + * Provides access to a shared {@link KernelServicePortal} instance. + */ +public interface SharedKernelServicePortal + extends + AutoCloseable { + + @Override + void close(); + + /** + * Indicates whether this instance is closed/unregistered from the shared portal pool. + * + * @return {@code true} if, and only if, this instance is closed. + */ + boolean isClosed(); + + /** + * Returns the {@link KernelServicePortal} instance being shared. + * + * @return The portal instance being shared. + * @throws IllegalStateException If this instance is closed. + */ + KernelServicePortal getPortal() + throws IllegalStateException; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/SharedKernelServicePortalProvider.java b/opentcs-api-base/src/main/java/org/opentcs/access/SharedKernelServicePortalProvider.java new file mode 100644 index 0000000..65c1a39 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/SharedKernelServicePortalProvider.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access; + +import org.opentcs.components.kernel.services.ServiceUnavailableException; + +/** + * Pools access to a {@link KernelServicePortal} instance for multiple clients. + */ +public interface SharedKernelServicePortalProvider { + + /** + * Creates and registers a new client with this access pool. + * This is a convenience method that supports try-with-ressources and does not require a + * preexisting client. + * + * @return The {@link SharedKernelServicePortal}. + * @throws ServiceUnavailableException in case of connection falure with the portal. + */ + SharedKernelServicePortal register() + throws ServiceUnavailableException; + + /** + * Checks whether a kernel reference is currently being shared. + * + * @return {@code true} if, and only if, a portal reference is currently being shared, meaning + * that at least one client is registered and a usable portal reference exists. + */ + boolean portalShared(); + + /** + * Returns a description for the portal currently being shared. + * + * @return A description for the portal currently being shared, or the empty string, if none is + * currently being shared. + */ + String getPortalDescription(); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/SslParameterSet.java b/opentcs-api-base/src/main/java/org/opentcs/access/SslParameterSet.java new file mode 100644 index 0000000..7aacc9b --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/SslParameterSet.java @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.io.File; +import java.io.Serializable; + +/** + * A set of parameters to be used for SSL-encrypted socket connections. + */ +public class SslParameterSet + implements + Serializable { + + /** + * The default type used for truststore and keystore files. + */ + public static final String DEFAULT_KEYSTORE_TYPE = "PKCS12"; + + /** + * The type used for keystore and truststore. + */ + private final String keystoreType; + /** + * The file path of the keystore. + */ + private final File keystoreFile; + /** + * The password for the keystore file. + */ + private final String keystorePassword; + /** + * The file path of the truststore. + */ + private final File truststoreFile; + /** + * The password for the truststore file. + */ + private final String truststorePassword; + + /** + * Creates a new instance. + * + * @param keystoreType The type used for keystore and truststore + * @param keystoreFile The keystore file + * @param truststoreFile The truststore file + * @param keystorePassword The keystore file password + * @param truststorePassword The truststore file password + */ + public SslParameterSet( + @Nonnull + String keystoreType, + @Nullable + File keystoreFile, + @Nullable + String keystorePassword, + @Nullable + File truststoreFile, + @Nullable + String truststorePassword + ) { + this.keystoreType = requireNonNull(keystoreType, "keystoreType"); + this.keystoreFile = keystoreFile; + this.keystorePassword = keystorePassword; + this.truststoreFile = truststoreFile; + this.truststorePassword = truststorePassword; + } + + /** + * Returns the keystoreType used to decrypt the keystore and truststore. + * + * @return The keystoreType used to decrypt the keystore and truststore + */ + @Nonnull + public String getKeystoreType() { + return keystoreType; + } + + /** + * Returns the file path of the keystore file. + * + * @return The file path of the keystore file + */ + @Nullable + public File getKeystoreFile() { + return keystoreFile; + } + + /** + * Returns the password for the keystore file. + * + * @return The password for the keystore file + */ + @Nullable + public String getKeystorePassword() { + return keystorePassword; + } + + /** + * Returns the file path of the truststore file. + * + * @return The file path of the truststore file + */ + @Nullable + public File getTruststoreFile() { + return truststoreFile; + } + + /** + * Returns the password for the truststore file. + * + * @return The password for the truststore file + */ + @Nullable + public String getTruststorePassword() { + return truststorePassword; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/access/package-info.java new file mode 100644 index 0000000..cdc79b2 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/package-info.java @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Interfaces and classes for accessing the kernel from outside, for instance + * from a remote client or a communication adapter. + */ +package org.opentcs.access; diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/ClientID.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/ClientID.java new file mode 100644 index 0000000..ae3cd66 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/ClientID.java @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import java.util.UUID; + +/** + * Identifies a remote client unambiguously. + */ +public class ClientID + implements + Serializable { + /** + * The client's name. + */ + private final String clientName; + /** + * The client's UUID. + */ + private final UUID uuid; + + /** + * Creates a new ClientID. + * + * @param clientName The client's name. + */ + public ClientID( + @Nonnull + String clientName + ) { + this.clientName = requireNonNull(clientName, "clientName"); + uuid = UUID.randomUUID(); + } + + /** + * Return the client's name. + * + * @return The client's name. + */ + @Nonnull + public String getClientName() { + return clientName; + } + + /** + * Checks if this object equals another one. + * + * @param otherObject The other object to be compared with this object. + * @return true if, and only if, the given object is also a + * ClientID and contains the same name and the same UUID as this + * one. + */ + @Override + public boolean equals(Object otherObject) { + if (otherObject instanceof ClientID) { + ClientID otherID = (ClientID) otherObject; + return clientName.equals(otherID.clientName) && uuid.equals(otherID.uuid); + } + else { + return false; + } + } + + @Override + public int hashCode() { + return uuid.hashCode(); + } + + @Override + public String toString() { + return clientName + ":" + uuid; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/KernelServicePortalBuilder.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/KernelServicePortalBuilder.java new file mode 100644 index 0000000..766beb3 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/KernelServicePortalBuilder.java @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.function.Predicate; +import org.opentcs.access.CredentialsException; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.rmi.factories.NullSocketFactoryProvider; +import org.opentcs.access.rmi.factories.SocketFactoryProvider; +import org.opentcs.access.rmi.services.RemoteKernelServicePortalProxy; +import org.opentcs.components.kernel.services.ServiceUnavailableException; +import org.opentcs.util.ClassMatcher; + +/** + * Builds {@link KernelServicePortal} instances for connections to remote portals. + */ +public class KernelServicePortalBuilder { + + /** + * Provides socket factories used for RMI. + */ + private SocketFactoryProvider socketFactoryProvider = new NullSocketFactoryProvider(); + /** + * The user name for logging in. + */ + private final String userName; + /** + * The password for logging in. + */ + private final String password; + /** + * The event filter to be applied for the built portal. + */ + private Predicate eventFilter = new ClassMatcher(Object.class); + + /** + * Creates a new instance. + * + * @param userName The user name to use for logging in. + * @param password The password to use for logging in. + */ + public KernelServicePortalBuilder(String userName, String password) { + this.userName = requireNonNull(userName, "userName"); + this.password = requireNonNull(password, "password"); + } + + /** + * Returns the socket factory provider used for RMI. + * + * @return The socket factory provider used for RMI. + */ + public SocketFactoryProvider getSocketFactoryProvider() { + return socketFactoryProvider; + } + + /** + * Sets the socket factory provider used for RMI. + * + * @param socketFactoryProvider The socket factory provider. + * @return This instance. + */ + public KernelServicePortalBuilder setSocketFactoryProvider( + @Nonnull + SocketFactoryProvider socketFactoryProvider + ) { + this.socketFactoryProvider = requireNonNull(socketFactoryProvider, "socketFactoryProvider"); + return this; + } + + /** + * Returns the user name used for logging in. + * + * @return The user name used for logging in. + */ + public String getUserName() { + return userName; + } + + /** + * Returns the password used for logging in. + * + * @return The password used for logging in. + */ + public String getPassword() { + return password; + } + + /** + * Returns the event filter to be applied for the built portal. + * + * @return The event filter to be applied for the built portal. + */ + public Predicate getEventFilter() { + return eventFilter; + } + + /** + * Sets the event filter to be applied for the built portal. + * + * @param eventFilter The event filter. + * @return This instance. + */ + public KernelServicePortalBuilder setEventFilter( + @Nonnull + Predicate eventFilter + ) { + this.eventFilter = requireNonNull(eventFilter, "eventFilter"); + return this; + } + + /** + * Builds and returns a {@link KernelServicePortal} with the configured parameters. + * + * @return A {@link KernelServicePortal} instance. + * @throws ServiceUnavailableException If the remote portal is not reachable for some reason. + * @throws CredentialsException If the client login with the remote portal failed, e.g. because of + * incorrect login data. + */ + public KernelServicePortal build() + throws ServiceUnavailableException, + CredentialsException { + return new RemoteKernelServicePortalProxy( + userName, + password, + socketFactoryProvider, + eventFilter + ); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/factories/CustomSslRMIClientSocketFactory.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/factories/CustomSslRMIClientSocketFactory.java new file mode 100644 index 0000000..19228dd --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/factories/CustomSslRMIClientSocketFactory.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.factories; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.io.Serializable; +import java.net.Socket; +import java.rmi.server.RMIClientSocketFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.rmi.ssl.SslRMIClientSocketFactory; + +/** + * This implementation is similar to {@link SslRMIClientSocketFactory} but allows the use of a + * custom SSLConext. + */ +class CustomSslRMIClientSocketFactory + implements + RMIClientSocketFactory, + Serializable { + + /** + * Provides an instance of {@link SSLContext} used to get the actual socket factory. + */ + private final SecureSslContextFactory secureSslContextFactory; + + /** + * Creates a new instance. + * + * @param secureSslContextFactory Provides an instance of {@link SSLContext} used to get the + * actual socket factory. + */ + CustomSslRMIClientSocketFactory(SecureSslContextFactory secureSslContextFactory) { + this.secureSslContextFactory = requireNonNull( + secureSslContextFactory, + "secureSslContextFactory" + ); + } + + @Override + public Socket createSocket(String host, int port) + throws IOException { + SSLContext context = secureSslContextFactory.createClientContext(); + SSLSocketFactory sf = context.getSocketFactory(); + SSLSocket socket = (SSLSocket) sf.createSocket(host, port); + SSLParameters param = context.getSupportedSSLParameters(); + socket.setEnabledCipherSuites(param.getCipherSuites()); + socket.setEnabledProtocols(param.getProtocols()); + return socket; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/factories/NullSocketFactoryProvider.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/factories/NullSocketFactoryProvider.java new file mode 100644 index 0000000..577dcd7 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/factories/NullSocketFactoryProvider.java @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.factories; + +import jakarta.inject.Inject; +import java.rmi.registry.Registry; +import java.rmi.server.RMIClientSocketFactory; +import java.rmi.server.RMIServerSocketFactory; + +/** + * Provides {@code null} for both client and server socket factories. + * By using this provider, the default client-side/server-side socket factory will be used in + * {@link Registry} stubs. + */ +public class NullSocketFactoryProvider + implements + SocketFactoryProvider { + + @Inject + public NullSocketFactoryProvider() { + } + + @Override + public RMIClientSocketFactory getClientSocketFactory() { + return null; + } + + @Override + public RMIServerSocketFactory getServerSocketFactory() { + return null; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/factories/SecureSocketFactoryProvider.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/factories/SecureSocketFactoryProvider.java new file mode 100644 index 0000000..5708350 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/factories/SecureSocketFactoryProvider.java @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.factories; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.rmi.server.RMIClientSocketFactory; +import java.rmi.server.RMIServerSocketFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.rmi.ssl.SslRMIServerSocketFactory; +import org.opentcs.access.SslParameterSet; + +/** + * Provides instances of {@link RMIClientSocketFactory} and {@link RMIServerSocketFactory} that are + * implemented over the SSL or TLS protocols. + * Since these factories don't support anonymous cipher suites a keystore on the server-side and a + * truststore on the client-side is necessary. + */ +public class SecureSocketFactoryProvider + implements + SocketFactoryProvider { + + /** + * Provides methods for creating client-side and server-side {@link SSLContext} instances. + */ + private final SecureSslContextFactory secureSslContextFactory; + + /** + * Creates a new instance. + * + * @param sslParameterSet The SSL parameters to be used for creating socket factories. + */ + @Inject + public SecureSocketFactoryProvider(SslParameterSet sslParameterSet) { + requireNonNull(sslParameterSet, "sslParameterSet"); + this.secureSslContextFactory = new SecureSslContextFactory(sslParameterSet); + } + + @Override + public RMIClientSocketFactory getClientSocketFactory() { + return new CustomSslRMIClientSocketFactory(secureSslContextFactory); + } + + @Override + public RMIServerSocketFactory getServerSocketFactory() { + SSLContext context = secureSslContextFactory.createServerContext(); + SSLParameters param = context.getSupportedSSLParameters(); + return new SslRMIServerSocketFactory( + context, + param.getCipherSuites(), + param.getProtocols(), + param.getWantClientAuth() + ); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/factories/SecureSslContextFactory.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/factories/SecureSslContextFactory.java new file mode 100644 index 0000000..abdb961 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/factories/SecureSslContextFactory.java @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.factories; + +import static java.util.Objects.requireNonNull; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.Serializable; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import org.opentcs.access.SslParameterSet; + +/** + * Provides methods for creating client-side and server-side {@link SSLContext} instances. + */ +class SecureSslContextFactory + implements + Serializable { + + /** + * The name of the algorithm to use for the {@link KeyManagerFactory} and + * {@link TrustManagerFactory}. + */ + private static final String KEY_TRUST_MANAGEMENT_ALGORITHM = "SunX509"; + /** + * The protocol to use with the ssl context. + */ + private static final String SSL_CONTEXT_PROTOCOL = "TLSv1.2"; + /** + * The ssl parameters to be used for creating the ssl context. + */ + private final SslParameterSet sslParameterSet; + + /** + * Creates a new instance. + * + * @param sslParameterSet The ssl parameters to be used for creating the ssl context. + */ + SecureSslContextFactory(SslParameterSet sslParameterSet) { + this.sslParameterSet = requireNonNull(sslParameterSet, "sslParameterSet"); + } + + /** + * Creates an instance of {@link SSLContext} for the client. + * + * @return The ssl context. + * @throws IllegalStateException If the creation of the ssl context fails. + */ + public SSLContext createClientContext() + throws IllegalStateException { + SSLContext context = null; + + try { + KeyStore ts = KeyStore.getInstance(sslParameterSet.getKeystoreType()); + ts.load( + new FileInputStream(sslParameterSet.getTruststoreFile()), + sslParameterSet.getTruststorePassword().toCharArray() + ); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(KEY_TRUST_MANAGEMENT_ALGORITHM); + tmf.init(ts); + + context = SSLContext.getInstance(SSL_CONTEXT_PROTOCOL); + context.init(null, tmf.getTrustManagers(), null); + } + catch (NoSuchAlgorithmException | KeyStoreException | CertificateException | IOException + | KeyManagementException ex) { + throw new IllegalStateException("Error creating the client's ssl context", ex); + } + + return context; + } + + /** + * Creates an instance of {@link SSLContext} for the server. + * + * @return The ssl context. + * @throws IllegalStateException If the creation of the ssl context fails. + */ + public SSLContext createServerContext() + throws IllegalStateException { + SSLContext context = null; + + try { + KeyStore ks = KeyStore.getInstance(sslParameterSet.getKeystoreType()); + ks.load( + new FileInputStream(sslParameterSet.getKeystoreFile()), + sslParameterSet.getKeystorePassword().toCharArray() + ); + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KEY_TRUST_MANAGEMENT_ALGORITHM); + kmf.init(ks, sslParameterSet.getKeystorePassword().toCharArray()); + + context = SSLContext.getInstance(SSL_CONTEXT_PROTOCOL); + context.init(kmf.getKeyManagers(), null, null); + + } + catch (NoSuchAlgorithmException | KeyStoreException | CertificateException | IOException + | KeyManagementException | UnrecoverableKeyException ex) { + throw new IllegalStateException("Error creating the server's ssl context", ex); + } + + return context; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/factories/SocketFactoryProvider.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/factories/SocketFactoryProvider.java new file mode 100644 index 0000000..c24a2d4 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/factories/SocketFactoryProvider.java @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.factories; + +import jakarta.annotation.Nullable; +import java.rmi.server.RMIClientSocketFactory; +import java.rmi.server.RMIServerSocketFactory; + +/** + * A provider for instances of {@link RMIClientSocketFactory} and {@link RMIServerSocketFactory}. + * Generally one provider should provide compatible factories for clients and servers. + */ +public interface SocketFactoryProvider { + + /** + * Returns a {@link RMIClientSocketFactory}. + * + * @return A {@link RMIClientSocketFactory}. + * May be null to indicate that a default factory implementation is to be used. + */ + @Nullable + RMIClientSocketFactory getClientSocketFactory(); + + /** + * Returns a {@link RMIServerSocketFactory}. + * + * @return A {@link RMIServerSocketFactory}. + * May be null to indicate that a default factory implementation is to be used. + */ + @Nullable + RMIServerSocketFactory getServerSocketFactory(); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/factories/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/factories/package-info.java new file mode 100644 index 0000000..a8ef7e8 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/factories/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Interfaces and classes for configuration of the RMI runtime used by openTCS kernel and clients. + */ +package org.opentcs.access.rmi.factories; diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/package-info.java new file mode 100644 index 0000000..a180502 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/package-info.java @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Interfaces and classes for transparently providing an openTCS kernel's + * functionality via RMI. + */ +package org.opentcs.access.rmi; diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/AbstractRemoteServiceProxy.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/AbstractRemoteServiceProxy.java new file mode 100644 index 0000000..2e91f2a --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/AbstractRemoteServiceProxy.java @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.Remote; +import java.rmi.RemoteException; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.components.kernel.services.ServiceUnavailableException; +import org.opentcs.data.ObjectExistsException; +import org.opentcs.data.ObjectUnknownException; + +/** + * A base class for remote service proxy implementations. + * + * @param The remote service's type. + */ +abstract class AbstractRemoteServiceProxy { + + /** + * The message to log when a service is unavailable. + */ + private static final String SERVICE_UNAVAILABLE_MESSAGE = "Remote service unreachable"; + /** + * The client using this service. + */ + private ClientID clientId; + /** + * The corresponding remote service. + */ + private R remoteService; + /** + * The listener that is interested in updates of this service. + */ + private ServiceListener serviceListener; + + /** + * Returns the client id using this service. + * + * @return The client id using this service. + */ + ClientID getClientId() { + return clientId; + } + + /** + * Sets the client id using this service. + * + * @param clientId The client id. + * @return This remote service proxy. + */ + AbstractRemoteServiceProxy setClientId(ClientID clientId) { + this.clientId = clientId; + return this; + } + + /** + * Returns the remote service to delegate method invocations to. + * + * @return The remote service to delegate method invocations to. + */ + R getRemoteService() { + return remoteService; + } + + /** + * Sets the remote service to delegate method invocations to. + * + * @param remoteService The remote service. + * @return This remote service proxy. + */ + AbstractRemoteServiceProxy setRemoteService(R remoteService) { + this.remoteService = remoteService; + return this; + } + + /** + * Returns the listener that is interested in updates of this service. + * + * @return The listener that is interested in updates of this service. + */ + ServiceListener getServiceListener() { + return serviceListener; + } + + /** + * Sets the listener that is interested in updates of this service. + * + * @param serviceListener The service listener. + * @return This remote service proxy. + */ + public AbstractRemoteServiceProxy setServiceListener(ServiceListener serviceListener) { + this.serviceListener = serviceListener; + return this; + } + + /** + * Checks whether this service is logged in or not. + * + * @return {@code true} if, and only if, this service has both a client id and a remote service + * associated to it. + */ + boolean isLoggedIn() { + return getClientId() != null && getRemoteService() != null; + } + + /** + * Ensures that this service is available to be used. + * + * @throws ServiceUnavailableException If the service is not available. + */ + void checkServiceAvailability() + throws ServiceUnavailableException { + if (!isLoggedIn()) { + throw new ServiceUnavailableException(SERVICE_UNAVAILABLE_MESSAGE); + } + } + + /** + * Returns a suitable {@link RuntimeException} for the given {@link RemoteException}. + * + * @param ex The exception to find a runtime exception for. + * @return The runtime exception. + */ + RuntimeException findSuitableExceptionFor(RemoteException ex) { + if (ex.getCause() instanceof ObjectUnknownException) { + return (ObjectUnknownException) ex.getCause(); + } + if (ex.getCause() instanceof ObjectExistsException) { + return (ObjectExistsException) ex.getCause(); + } + if (ex.getCause() instanceof IllegalArgumentException) { + return (IllegalArgumentException) ex.getCause(); + } + + if (getServiceListener() != null) { + getServiceListener().onServiceUnavailable(); + } + + return new ServiceUnavailableException(SERVICE_UNAVAILABLE_MESSAGE, ex); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/KernelStateEventListener.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/KernelStateEventListener.java new file mode 100644 index 0000000..ed81bfe --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/KernelStateEventListener.java @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import org.opentcs.access.Kernel; + +/** + * A listener for events concerning kernel state changes. + */ +interface KernelStateEventListener { + + /** + * Called when the kernel state changes to {@link Kernel.State#SHUTDOWN}. + */ + void onKernelShutdown(); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RegistrationName.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RegistrationName.java new file mode 100644 index 0000000..abf1d7c --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RegistrationName.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +/** + * Defines the names used for binding the remote services in the RMI registry. + */ +public interface RegistrationName { + + /** + * The name the {@link RemoteKernelServicePortal} registers itself with a RMI registry. + */ + String REMOTE_KERNEL_CLIENT_PORTAL = RemoteKernelServicePortal.class.getCanonicalName(); + /** + * The name the {@link RemotePlantModelService} registers itself with a RMI registry. + */ + String REMOTE_PLANT_MODEL_SERVICE = RemotePlantModelService.class.getCanonicalName(); + /** + * The name the {@link RemoteTransportOrderService} registers itself with a RMI registry. + */ + String REMOTE_TRANSPORT_ORDER_SERVICE = RemoteTransportOrderService.class.getCanonicalName(); + /** + * The name the {@link RemoteVehicleService} registers itself with a RMI registry. + */ + String REMOTE_VEHICLE_SERVICE = RemoteVehicleService.class.getCanonicalName(); + /** + * The name the {@link RemoteNotificationService} registers itself with a RMI registry. + */ + String REMOTE_NOTIFICATION_SERVICE = RemoteNotificationService.class.getCanonicalName(); + /** + * The name the {@link RemoteRouterService} registers itself with a RMI registry. + */ + String REMOTE_ROUTER_SERVICE = RemoteRouterService.class.getCanonicalName(); + /** + * The name the {@link RemoteDispatcherService} registers itself with a RMI registry. + */ + String REMOTE_DISPATCHER_SERVICE = RemoteDispatcherService.class.getCanonicalName(); + /** + * The name the {@link RemoteQueryService} registers itself with a RMI registry. + */ + String REMOTE_QUERY_SERVICE = RemoteQueryService.class.getCanonicalName(); + /** + * The name the {@link RemotePeripheralService} registers itself with a RMI registry. + */ + String REMOTE_PERIPHERAL_SERVICE = RemotePeripheralService.class.getCanonicalName(); + /** + * The name the {@link RemotePeripheralJobService} registers itself with a RMI registry. + */ + String REMOTE_PERIPHERAL_JOB_SERVICE = RemotePeripheralJobService.class.getCanonicalName(); + /** + * The name the {@link RemotePeripheralDispatcherService} registers itself with a RMI registry. + */ + String REMOTE_PERIPHERAL_DISPATCHER_SERVICE + = RemotePeripheralDispatcherService.class.getCanonicalName(); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteDispatcherService.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteDispatcherService.java new file mode 100644 index 0000000..800a3c7 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteDispatcherService.java @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.Remote; +import java.rmi.RemoteException; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.data.order.TransportOrder; + +/** + * Declares the methods provided by the {@link DispatcherService} via RMI. + * + *

+ * The majority of the methods declared here have signatures analogous to their counterparts in + * {@link DispatcherService}, with an additional {@link ClientID} parameter which serves the purpose + * of identifying the calling client and determining its permissions. + *

+ *

+ * To avoid redundancy, the semantics of methods that only pass through their arguments are not + * explicitly documented here again. See the corresponding API documentation in + * {@link DispatcherService} for these, instead. + *

+ */ +public interface RemoteDispatcherService + extends + Remote { + + // CHECKSTYLE:OFF + void dispatch(ClientID clientId) + throws RemoteException; + + void withdrawByVehicle( + ClientID clientId, + TCSObjectReference ref, + boolean immediateAbort + ) + throws RemoteException; + + void withdrawByTransportOrder( + ClientID clientId, + TCSObjectReference ref, + boolean immediateAbort + ) + throws RemoteException; + + void reroute(ClientID clientId, TCSObjectReference ref, ReroutingType reroutingType) + throws RemoteException; + + void rerouteAll(ClientID clientId, ReroutingType reroutingType) + throws RemoteException; + + void assignNow(ClientID clientId, TCSObjectReference ref) + throws RemoteException; + // CHECKSTYLE:ON +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteDispatcherServiceProxy.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteDispatcherServiceProxy.java new file mode 100644 index 0000000..9434353 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteDispatcherServiceProxy.java @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.RemoteException; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.data.order.TransportOrder; + +/** + * The default implementation of the dispatcher service. + * Delegates method invocations to the corresponding remote service. + */ +class RemoteDispatcherServiceProxy + extends + AbstractRemoteServiceProxy + implements + DispatcherService { + + /** + * Creates a new instance. + */ + RemoteDispatcherServiceProxy() { + } + + @Override + public void dispatch() + throws KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().dispatch(getClientId()); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void withdrawByVehicle( + TCSObjectReference vehicleRef, + boolean immediateAbort + ) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().withdrawByVehicle( + getClientId(), + vehicleRef, + immediateAbort + ); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void withdrawByTransportOrder( + TCSObjectReference ref, + boolean immediateAbort + ) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().withdrawByTransportOrder( + getClientId(), + ref, + immediateAbort + ); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void reroute(TCSObjectReference ref, ReroutingType reroutingType) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().reroute(getClientId(), ref, reroutingType); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void rerouteAll(ReroutingType reroutingType) { + checkServiceAvailability(); + + try { + getRemoteService().rerouteAll(getClientId(), reroutingType); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void assignNow(TCSObjectReference ref) + throws KernelRuntimeException { + checkServiceAvailability(); + try { + getRemoteService().assignNow(getClientId(), ref); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteKernelServicePortal.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteKernelServicePortal.java new file mode 100644 index 0000000..bf702e0 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteKernelServicePortal.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.util.List; +import java.util.function.Predicate; +import org.opentcs.access.CredentialsException; +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.rmi.ClientID; + +/** + * Declares the methods provided by the {@link KernelServicePortal} via RMI. + */ +public interface RemoteKernelServicePortal + extends + Remote { + + /** + * Introduce the calling client to the server and authenticate for operations. + * + * @param userName The user's name. + * @param password The user's password. + * @param eventFilter The event filter to be applied to events on the server side. + * @return An identification object that is required for subsequent method calls. + * @throws CredentialsException If authentication with the given username and password failed. + * @throws RemoteException If there was an RMI-related problem. + */ + ClientID login(String userName, String password, Predicate eventFilter) + throws CredentialsException, + RemoteException; + + void logout(ClientID clientId) + throws RemoteException; + + Kernel.State getState(ClientID clientId) + throws RemoteException; + + List fetchEvents(ClientID clientId, long timeout) + throws RemoteException; + + void publishEvent(ClientID clientId, Object event) + throws RemoteException; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteKernelServicePortalProxy.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteKernelServicePortalProxy.java new file mode 100644 index 0000000..ef3b7b8 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteKernelServicePortalProxy.java @@ -0,0 +1,388 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.access.rmi.services.RegistrationName.REMOTE_DISPATCHER_SERVICE; +import static org.opentcs.access.rmi.services.RegistrationName.REMOTE_KERNEL_CLIENT_PORTAL; +import static org.opentcs.access.rmi.services.RegistrationName.REMOTE_NOTIFICATION_SERVICE; +import static org.opentcs.access.rmi.services.RegistrationName.REMOTE_PERIPHERAL_DISPATCHER_SERVICE; +import static org.opentcs.access.rmi.services.RegistrationName.REMOTE_PERIPHERAL_JOB_SERVICE; +import static org.opentcs.access.rmi.services.RegistrationName.REMOTE_PERIPHERAL_SERVICE; +import static org.opentcs.access.rmi.services.RegistrationName.REMOTE_PLANT_MODEL_SERVICE; +import static org.opentcs.access.rmi.services.RegistrationName.REMOTE_QUERY_SERVICE; +import static org.opentcs.access.rmi.services.RegistrationName.REMOTE_ROUTER_SERVICE; +import static org.opentcs.access.rmi.services.RegistrationName.REMOTE_TRANSPORT_ORDER_SERVICE; +import static org.opentcs.access.rmi.services.RegistrationName.REMOTE_VEHICLE_SERVICE; + +import jakarta.annotation.Nonnull; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.util.List; +import java.util.function.Predicate; +import org.opentcs.access.CredentialsException; +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.rmi.factories.SocketFactoryProvider; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.components.kernel.services.NotificationService; +import org.opentcs.components.kernel.services.PeripheralDispatcherService; +import org.opentcs.components.kernel.services.PeripheralJobService; +import org.opentcs.components.kernel.services.PeripheralService; +import org.opentcs.components.kernel.services.PlantModelService; +import org.opentcs.components.kernel.services.QueryService; +import org.opentcs.components.kernel.services.RouterService; +import org.opentcs.components.kernel.services.ServiceUnavailableException; +import org.opentcs.components.kernel.services.TransportOrderService; +import org.opentcs.components.kernel.services.VehicleService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The default implementation for the {@link KernelServicePortal}. + */ +public class RemoteKernelServicePortalProxy + extends + AbstractRemoteServiceProxy + implements + KernelServicePortal, + ServiceListener { + + /** + * This class' logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(RemoteKernelServicePortalProxy.class); + /** + * The user name used with the remote portal. + */ + private final String userName; + /** + * The password used with the remote portal. + */ + private final String password; + /** + * Provides socket factories used for RMI. + */ + private final SocketFactoryProvider socketFactoryProvider; + /** + * The event filter to be applied to events on the server side (before polling). + */ + private final Predicate eventFilter; + /** + * The plant model service. + */ + private final RemotePlantModelServiceProxy plantModelService + = new RemotePlantModelServiceProxy(); + /** + * The transport order service. + */ + private final RemoteTransportOrderServiceProxy transportOrderService + = new RemoteTransportOrderServiceProxy(); + /** + * The vehicle service. + */ + private final RemoteVehicleServiceProxy vehicleService = new RemoteVehicleServiceProxy(); + /** + * The notification service. + */ + private final RemoteNotificationServiceProxy notificationService + = new RemoteNotificationServiceProxy(); + /** + * The dispatcher service. + */ + private final RemoteDispatcherServiceProxy dispatcherService + = new RemoteDispatcherServiceProxy(); + /** + * The router service. + */ + private final RemoteRouterServiceProxy routerService = new RemoteRouterServiceProxy(); + /** + * The query service. + */ + private final RemoteQueryServiceProxy queryService = new RemoteQueryServiceProxy(); + /** + * The peripheral service. + */ + private final RemotePeripheralServiceProxy peripheralService = new RemotePeripheralServiceProxy(); + /** + * The peripheral job service. + */ + private final RemotePeripheralJobServiceProxy peripheralJobService + = new RemotePeripheralJobServiceProxy(); + /** + * The peripheral dispatcher service. + */ + private final RemotePeripheralDispatcherServiceProxy peripheralDispatcherService + = new RemotePeripheralDispatcherServiceProxy(); + + /** + * Creates a new instance. + * + * @param userName The user name used with the remote portal. + * @param password The password used with the remote portal. + * @param socketFactoryProvider Provides socket factories used for RMI. + * @param eventFilter The event filter to be applied to events on the server side. + */ + public RemoteKernelServicePortalProxy( + @Nonnull + String userName, + @Nonnull + String password, + @Nonnull + SocketFactoryProvider socketFactoryProvider, + @Nonnull + Predicate eventFilter + ) { + this.userName = requireNonNull(userName, "userName"); + this.password = requireNonNull(password, "password"); + this.socketFactoryProvider = requireNonNull(socketFactoryProvider, "socketFactoryProvider"); + this.eventFilter = requireNonNull(eventFilter, "eventFilter"); + } + + @Override + public ServiceListener getServiceListener() { + return this; + } + + @Override + public void onServiceUnavailable() { + resetServiceLogins(); + } + + @Override + public void login( + @Nonnull + String hostName, + int port + ) + throws CredentialsException, + ServiceUnavailableException { + requireNonNull(hostName, "hostName"); + + if (isLoggedIn()) { + LOG.warn("Already logged in, doing nothing."); + return; + } + + try { + // Look up the remote portal with the RMI registry. + Registry registry + = LocateRegistry.getRegistry( + hostName, + port, + socketFactoryProvider.getClientSocketFactory() + ); + + setRemoteService((RemoteKernelServicePortal) registry.lookup(REMOTE_KERNEL_CLIENT_PORTAL)); + // Login and save the client ID. + setClientId(getRemoteService().login(userName, password, eventFilter)); + // Get notified when a service call on us fails. + setServiceListener(this); + + // Look up the remote services with the RMI registry and update the other service logins. + updateServiceLogins(registry); + } + catch (RemoteException | NotBoundException exc) { + resetServiceLogins(); + throw new ServiceUnavailableException( + "Exception logging in with remote kernel client portal", + exc + ); + } + } + + @Override + public void logout() { + if (!isLoggedIn()) { + LOG.warn("Not logged in, doing nothing."); + return; + } + + try { + getRemoteService().logout(getClientId()); + } + catch (RemoteException ex) { + throw new ServiceUnavailableException("Remote kernel client portal unavailable", ex); + } + + resetServiceLogins(); + } + + @Override + public Kernel.State getState() + throws KernelRuntimeException { + checkServiceAvailability(); + + try { + return getRemoteService().getState(getClientId()); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public List fetchEvents(long timeout) + throws KernelRuntimeException { + checkServiceAvailability(); + + try { + return getRemoteService().fetchEvents(getClientId(), timeout); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void publishEvent(Object event) + throws KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().publishEvent(getClientId(), event); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + @Nonnull + public PlantModelService getPlantModelService() { + return plantModelService; + } + + @Override + @Nonnull + public TransportOrderService getTransportOrderService() { + return transportOrderService; + } + + @Override + @Nonnull + public VehicleService getVehicleService() { + return vehicleService; + } + + @Override + @Nonnull + public NotificationService getNotificationService() { + return notificationService; + } + + @Override + @Nonnull + public DispatcherService getDispatcherService() { + return dispatcherService; + } + + @Override + @Nonnull + public RouterService getRouterService() { + return routerService; + } + + @Override + @Nonnull + public QueryService getQueryService() { + return queryService; + } + + @Override + @Nonnull + public PeripheralService getPeripheralService() { + return peripheralService; + } + + @Override + @Nonnull + public PeripheralJobService getPeripheralJobService() { + return peripheralJobService; + } + + @Override + @Nonnull + public PeripheralDispatcherService getPeripheralDispatcherService() { + return peripheralDispatcherService; + } + + private void updateServiceLogins(Registry registry) + throws RemoteException, + NotBoundException { + plantModelService + .setClientId(getClientId()) + .setRemoteService((RemotePlantModelService) registry.lookup(REMOTE_PLANT_MODEL_SERVICE)) + .setServiceListener(this); + + transportOrderService + .setClientId(getClientId()) + .setRemoteService( + (RemoteTransportOrderService) registry.lookup(REMOTE_TRANSPORT_ORDER_SERVICE) + ) + .setServiceListener(this); + + vehicleService + .setClientId(getClientId()) + .setRemoteService((RemoteVehicleService) registry.lookup(REMOTE_VEHICLE_SERVICE)) + .setServiceListener(this); + + notificationService + .setClientId(getClientId()) + .setRemoteService((RemoteNotificationService) registry.lookup(REMOTE_NOTIFICATION_SERVICE)) + .setServiceListener(this); + + dispatcherService + .setClientId(getClientId()) + .setRemoteService((RemoteDispatcherService) registry.lookup(REMOTE_DISPATCHER_SERVICE)) + .setServiceListener(this); + + routerService + .setClientId(getClientId()) + .setRemoteService((RemoteRouterService) registry.lookup(REMOTE_ROUTER_SERVICE)) + .setServiceListener(this); + + queryService + .setClientId(getClientId()) + .setRemoteService((RemoteQueryService) registry.lookup(REMOTE_QUERY_SERVICE)) + .setServiceListener(this); + + peripheralService + .setClientId(getClientId()) + .setRemoteService((RemotePeripheralService) registry.lookup(REMOTE_PERIPHERAL_SERVICE)) + .setServiceListener(this); + + peripheralJobService + .setClientId(getClientId()) + .setRemoteService( + (RemotePeripheralJobService) registry.lookup(REMOTE_PERIPHERAL_JOB_SERVICE) + ) + .setServiceListener(this); + + peripheralDispatcherService + .setClientId(getClientId()) + .setRemoteService( + (RemotePeripheralDispatcherService) registry.lookup( + REMOTE_PERIPHERAL_DISPATCHER_SERVICE + ) + ) + .setServiceListener(this); + } + + private void resetServiceLogins() { + this.setClientId(null).setRemoteService(null).setServiceListener(null); + plantModelService.setClientId(null).setRemoteService(null).setServiceListener(null); + transportOrderService.setClientId(null).setRemoteService(null).setServiceListener(null); + vehicleService.setClientId(null).setRemoteService(null).setServiceListener(null); + notificationService.setClientId(null).setRemoteService(null).setServiceListener(null); + dispatcherService.setClientId(null).setRemoteService(null).setServiceListener(null); + routerService.setClientId(null).setRemoteService(null).setServiceListener(null); + queryService.setClientId(null).setRemoteService(null).setServiceListener(null); + peripheralService.setClientId(null).setRemoteService(null).setServiceListener(null); + peripheralJobService.setClientId(null).setRemoteService(null).setServiceListener(null); + peripheralDispatcherService.setClientId(null).setRemoteService(null).setServiceListener(null); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteNotificationService.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteNotificationService.java new file mode 100644 index 0000000..b833f2c --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteNotificationService.java @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.util.List; +import java.util.function.Predicate; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.components.kernel.services.NotificationService; +import org.opentcs.data.notification.UserNotification; + +/** + * Declares the methods provided by the {@link NotificationService} via RMI. + * + *

+ * The majority of the methods declared here have signatures analogous to their counterparts in + * {@link NotificationService}, with an additional {@link ClientID} parameter which serves the + * purpose of identifying the calling client and determining its permissions. + *

+ *

+ * To avoid redundancy, the semantics of methods that only pass through their arguments are not + * explicitly documented here again. See the corresponding API documentation in + * {@link NotificationService} for these, instead. + *

+ */ +public interface RemoteNotificationService + extends + Remote { + + // CHECKSTYLE:OFF + List fetchUserNotifications( + ClientID clientId, + Predicate predicate + ) + throws RemoteException; + + void publishUserNotification(ClientID clientId, UserNotification notification) + throws RemoteException; + // CHECKSTYLE:ON +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteNotificationServiceProxy.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteNotificationServiceProxy.java new file mode 100644 index 0000000..efbab19 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteNotificationServiceProxy.java @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.RemoteException; +import java.util.List; +import java.util.function.Predicate; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.components.kernel.services.NotificationService; +import org.opentcs.data.notification.UserNotification; + +/** + * The default implementation of the notification service. + * Delegates method invocations to the corresponding remote service. + */ +class RemoteNotificationServiceProxy + extends + AbstractRemoteServiceProxy + implements + NotificationService { + + /** + * Creates a new instance. + */ + RemoteNotificationServiceProxy() { + } + + @Override + public List fetchUserNotifications(Predicate predicate) + throws KernelRuntimeException { + checkServiceAvailability(); + + try { + return getRemoteService().fetchUserNotifications(getClientId(), predicate); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void publishUserNotification(UserNotification notification) + throws KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().publishUserNotification(getClientId(), notification); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePeripheralDispatcherService.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePeripheralDispatcherService.java new file mode 100644 index 0000000..dc481cb --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePeripheralDispatcherService.java @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.Remote; +import java.rmi.RemoteException; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.components.kernel.services.PeripheralDispatcherService; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.peripherals.PeripheralJob; + +/** + * Declares the methods provided by the {@link PeripheralDispatcherService} via RMI. + * + *

+ * The majority of the methods declared here have signatures analogous to their counterparts in + * {@link PeripheralDispatcherService}, with an additional {@link ClientID} parameter which serves + * the purpose of identifying the calling client and determining its permissions. + *

+ *

+ * To avoid redundancy, the semantics of methods that only pass through their arguments are not + * explicitly documented here again. See the corresponding API documentation in + * {@link PeripheralDispatcherService} for these, instead. + *

+ */ +public interface RemotePeripheralDispatcherService + extends + Remote { + + // CHECKSTYLE:OFF + void dispatch(ClientID clientId) + throws RemoteException; + + void withdrawByLocation(ClientID clientId, TCSResourceReference ref) + throws RemoteException; + + void withdrawByPeripheralJob(ClientID clientId, TCSObjectReference ref) + throws RemoteException; + // CHECKSTYLE:ON +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePeripheralDispatcherServiceProxy.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePeripheralDispatcherServiceProxy.java new file mode 100644 index 0000000..52fa512 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePeripheralDispatcherServiceProxy.java @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.RemoteException; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.components.kernel.services.PeripheralDispatcherService; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.peripherals.PeripheralJob; + +/** + * The default implementation of the peripheral dispatcher service. + * Delegates method invocations to the corresponding remote service. + */ +class RemotePeripheralDispatcherServiceProxy + extends + AbstractRemoteServiceProxy + implements + PeripheralDispatcherService { + + /** + * Creates a new instance. + */ + RemotePeripheralDispatcherServiceProxy() { + } + + @Override + public void dispatch() + throws KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().dispatch(getClientId()); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void withdrawByLocation(TCSResourceReference locationRef) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().withdrawByLocation(getClientId(), locationRef); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void withdrawByPeripheralJob(TCSObjectReference jobRef) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().withdrawByPeripheralJob(getClientId(), jobRef); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePeripheralJobService.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePeripheralJobService.java new file mode 100644 index 0000000..f98cbe7 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePeripheralJobService.java @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.Remote; +import java.rmi.RemoteException; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.access.to.peripherals.PeripheralJobCreationTO; +import org.opentcs.components.kernel.services.PeripheralJobService; +import org.opentcs.data.peripherals.PeripheralJob; + +/** + * Declares the methods provided by the {@link PeripheralJobService} via RMI. + * + *

+ * The majority of the methods declared here have signatures analogous to their counterparts in + * {@link PeripheralJobService}, with an additional {@link ClientID} parameter which serves the + * purpose of identifying the calling client and determining its permissions. + *

+ *

+ * To avoid redundancy, the semantics of methods that only pass through their arguments are not + * explicitly documented here again. See the corresponding API documentation in + * {@link PeripheralJobService} for these, instead. + *

+ */ +public interface RemotePeripheralJobService + extends + RemoteTCSObjectService, + Remote { + + // CHECKSTYLE:OFF + PeripheralJob createPeripheralJob(ClientID clientId, PeripheralJobCreationTO to) + throws RemoteException; + // CHECKSTYLE:ON +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePeripheralJobServiceProxy.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePeripheralJobServiceProxy.java new file mode 100644 index 0000000..95aede0 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePeripheralJobServiceProxy.java @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.RemoteException; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.to.peripherals.PeripheralJobCreationTO; +import org.opentcs.components.kernel.services.PeripheralJobService; +import org.opentcs.data.ObjectExistsException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.peripherals.PeripheralJob; + +/** + * The default implementation of the peripheral job service. + * Delegates method invocations to the corresponding remote service. + */ +class RemotePeripheralJobServiceProxy + extends + RemoteTCSObjectServiceProxy + implements + PeripheralJobService { + + /** + * Creates a new instance. + */ + RemotePeripheralJobServiceProxy() { + } + + @Override + public PeripheralJob createPeripheralJob(PeripheralJobCreationTO to) + throws ObjectUnknownException, + ObjectExistsException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + return getRemoteService().createPeripheralJob(getClientId(), to); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePeripheralService.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePeripheralService.java new file mode 100644 index 0000000..9bc7817 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePeripheralService.java @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.Remote; +import java.rmi.RemoteException; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.components.kernel.services.PeripheralService; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.drivers.peripherals.PeripheralAdapterCommand; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; +import org.opentcs.drivers.peripherals.PeripheralProcessModel; +import org.opentcs.drivers.peripherals.management.PeripheralAttachmentInformation; + +/** + * Declares the methods provided by the {@link PeripheralService} via RMI. + * + *

+ * The majority of the methods declared here have signatures analogous to their counterparts in + * {@link PeripheralService}, with an additional {@link ClientID} parameter which serves the purpose + * of identifying the calling client and determining its permissions. + *

+ *

+ * To avoid redundancy, the semantics of methods that only pass through their arguments are not + * explicitly documented here again. See the corresponding API documentation in + * {@link PeripheralService} for these, instead. + *

+ */ +public interface RemotePeripheralService + extends + RemoteTCSObjectService, + Remote { + + // CHECKSTYLE:OFF + void attachCommAdapter( + ClientID clientId, + TCSResourceReference ref, + PeripheralCommAdapterDescription description + ) + throws RemoteException; + + void disableCommAdapter(ClientID clientId, TCSResourceReference ref) + throws RemoteException; + + void enableCommAdapter(ClientID clientId, TCSResourceReference ref) + throws RemoteException; + + PeripheralAttachmentInformation fetchAttachmentInformation( + ClientID clientId, + TCSResourceReference ref + ) + throws RemoteException; + + PeripheralProcessModel fetchProcessModel(ClientID clientId, TCSResourceReference ref) + throws RemoteException; + + void sendCommAdapterCommand( + ClientID clientId, + TCSResourceReference ref, + PeripheralAdapterCommand command + ) + throws RemoteException; + // CHECKSTYLE:ON +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePeripheralServiceProxy.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePeripheralServiceProxy.java new file mode 100644 index 0000000..3971332 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePeripheralServiceProxy.java @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.RemoteException; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.components.kernel.services.PeripheralService; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.drivers.peripherals.PeripheralAdapterCommand; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; +import org.opentcs.drivers.peripherals.PeripheralProcessModel; +import org.opentcs.drivers.peripherals.management.PeripheralAttachmentInformation; + +/** + * The default implementation of the vehicle service. + * Delegates method invocations to the corresponding remote service. + */ +class RemotePeripheralServiceProxy + extends + RemoteTCSObjectServiceProxy + implements + PeripheralService { + + /** + * Creates a new instance. + */ + RemotePeripheralServiceProxy() { + } + + @Override + public void attachCommAdapter( + TCSResourceReference ref, + PeripheralCommAdapterDescription description + ) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().attachCommAdapter(getClientId(), ref, description); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void disableCommAdapter(TCSResourceReference ref) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().disableCommAdapter(getClientId(), ref); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void enableCommAdapter(TCSResourceReference ref) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().enableCommAdapter(getClientId(), ref); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public PeripheralAttachmentInformation fetchAttachmentInformation( + TCSResourceReference ref + ) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + return getRemoteService().fetchAttachmentInformation(getClientId(), ref); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public PeripheralProcessModel fetchProcessModel(TCSResourceReference ref) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + return getRemoteService().fetchProcessModel(getClientId(), ref); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void sendCommAdapterCommand( + TCSResourceReference ref, + PeripheralAdapterCommand command + ) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().sendCommAdapterCommand(getClientId(), ref, command); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePlantModelService.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePlantModelService.java new file mode 100644 index 0000000..48490a6 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePlantModelService.java @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.util.Map; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.components.kernel.services.PlantModelService; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.PlantModel; + +/** + * Declares the methods provided by the {@link PlantModelService} via RMI. + * + *

+ * The majority of the methods declared here have signatures analogous to their counterparts in + * {@link PlantModelService}, with an additional {@link ClientID} parameter which serves the purpose + * of identifying the calling client and determining its permissions. + *

+ *

+ * To avoid redundancy, the semantics of methods that only pass through their arguments are not + * explicitly documented here again. See the corresponding API documentation in + * {@link PlantModelService} for these, instead. + *

+ */ +public interface RemotePlantModelService + extends + RemoteTCSObjectService, + Remote { + + // CHECKSTYLE:OFF + PlantModel getPlantModel(ClientID clientId) + throws RemoteException; + + void createPlantModel(ClientID clientId, PlantModelCreationTO to) + throws RemoteException; + + String getModelName(ClientID clientId) + throws RemoteException; + + Map getModelProperties(ClientID clientId) + throws RemoteException; + + void updateLocationLock(ClientID clientId, TCSObjectReference ref, boolean locked) + throws RemoteException; + + void updatePathLock(ClientID clientId, TCSObjectReference ref, boolean locked) + throws RemoteException; + // CHECKSTYLE:ON +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePlantModelServiceProxy.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePlantModelServiceProxy.java new file mode 100644 index 0000000..02d3839 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemotePlantModelServiceProxy.java @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.RemoteException; +import java.util.Map; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.components.kernel.services.PlantModelService; +import org.opentcs.data.ObjectExistsException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.PlantModel; + +/** + * The default implementation of the plant model service. + * Delegates method invocations to the corresponding remote service. + */ +class RemotePlantModelServiceProxy + extends + RemoteTCSObjectServiceProxy + implements + PlantModelService { + + /** + * Creates a new instance. + */ + RemotePlantModelServiceProxy() { + } + + @Override + public PlantModel getPlantModel() + throws KernelRuntimeException { + checkServiceAvailability(); + + try { + return getRemoteService().getPlantModel(getClientId()); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void createPlantModel(PlantModelCreationTO to) + throws ObjectUnknownException, + ObjectExistsException, + KernelRuntimeException, + IllegalStateException { + checkServiceAvailability(); + + try { + getRemoteService().createPlantModel(getClientId(), to); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public String getModelName() + throws KernelRuntimeException { + checkServiceAvailability(); + + try { + return getRemoteService().getModelName(getClientId()); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public Map getModelProperties() + throws KernelRuntimeException { + checkServiceAvailability(); + + try { + return getRemoteService().getModelProperties(getClientId()); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void updateLocationLock(TCSObjectReference ref, boolean locked) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().updateLocationLock(getClientId(), ref, locked); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void updatePathLock(TCSObjectReference ref, boolean locked) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().updatePathLock(getClientId(), ref, locked); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteQueryService.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteQueryService.java new file mode 100644 index 0000000..b753b98 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteQueryService.java @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.Remote; +import java.rmi.RemoteException; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.components.kernel.Query; +import org.opentcs.components.kernel.services.QueryService; + +/** + * Declares the methods provided by the {@link QueryService} via RMI. + * + *

+ * The majority of the methods declared here have signatures analogous to their counterparts in + * {@link QueryService}, with an additional {@link ClientID} parameter which serves the purpose + * of identifying the calling client and determining its permissions. + *

+ *

+ * To avoid redundancy, the semantics of methods that only pass through their arguments are not + * explicitly documented here again. See the corresponding API documentation in + * {@link QueryService} for these, instead. + *

+ */ +public interface RemoteQueryService + extends + Remote { + + // CHECKSTYLE:OFF + T query(ClientID clientId, Query query) + throws RemoteException; + // CHECKSTYLE:ON +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteQueryServiceProxy.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteQueryServiceProxy.java new file mode 100644 index 0000000..20d3a97 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteQueryServiceProxy.java @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.RemoteException; +import org.opentcs.components.kernel.Query; +import org.opentcs.components.kernel.services.QueryService; + +/** + * The default implementation of the query service. + * Delegates method invocations to the corresponding remote service. + */ +class RemoteQueryServiceProxy + extends + AbstractRemoteServiceProxy + implements + QueryService { + + /** + * Creates a new instance. + */ + RemoteQueryServiceProxy() { + } + + @Override + public T query(Query query) { + checkServiceAvailability(); + + try { + return getRemoteService().query(getClientId(), query); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteRouterService.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteRouterService.java new file mode 100644 index 0000000..3de0a67 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteRouterService.java @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.util.Map; +import java.util.Set; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.components.kernel.services.RouterService; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.Route; + +/** + * Declares the methods provided by the {@link RouterService} via RMI. + * + *

+ * The majority of the methods declared here have signatures analogous to their counterparts in + * {@link RouterService}, with an additional {@link ClientID} parameter which serves the purpose + * of identifying the calling client and determining its permissions. + *

+ *

+ * To avoid redundancy, the semantics of methods that only pass through their arguments are not + * explicitly documented here again. See the corresponding API documentation in + * {@link RouterService} for these, instead. + *

+ */ +public interface RemoteRouterService + extends + Remote { + + // CHECKSTYLE:OFF + public void updateRoutingTopology(ClientID clientId, Set> refs) + throws RemoteException; + + public Map, Route> computeRoutes( + ClientID clientId, + TCSObjectReference vehicleRef, + TCSObjectReference sourcePointRef, + Set> destinationPointRefs, + Set> resourcesToAvoid + ) + throws RemoteException; + // CHECKSTYLE:ON +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteRouterServiceProxy.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteRouterServiceProxy.java new file mode 100644 index 0000000..d82c3e8 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteRouterServiceProxy.java @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.RemoteException; +import java.util.Map; +import java.util.Set; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.components.kernel.services.RouterService; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.Route; + +/** + * The default implementation of the router service. + * Delegates method invocations to the corresponding remote service. + */ +class RemoteRouterServiceProxy + extends + AbstractRemoteServiceProxy + implements + RouterService { + + /** + * Creates a new instance. + */ + RemoteRouterServiceProxy() { + } + + @Override + public void updateRoutingTopology(Set> refs) + throws KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().updateRoutingTopology(getClientId(), refs); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public Map, Route> computeRoutes( + TCSObjectReference vehicleRef, + TCSObjectReference sourcePointRef, + Set> destinationPointRefs, + Set> resourcesToAvoid + ) + throws KernelRuntimeException { + checkServiceAvailability(); + + try { + return getRemoteService().computeRoutes( + getClientId(), + vehicleRef, + sourcePointRef, + destinationPointRefs, + resourcesToAvoid + ); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteTCSObjectService.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteTCSObjectService.java new file mode 100644 index 0000000..684e145 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteTCSObjectService.java @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.util.Set; +import java.util.function.Predicate; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectReference; + +/** + * Declares the methods provided by the {@link TCSObjectService} via RMI. + * + *

+ * The majority of the methods declared here have signatures analogous to their counterparts in + * {@link TCSObjectService}, with an additional {@link ClientID} parameter which serves the purpose + * of identifying the calling client and determining its permissions. + *

+ *

+ * To avoid redundancy, the semantics of methods that only pass through their arguments are not + * explicitly documented here again. See the corresponding API documentation in + * {@link TCSObjectService} for these, instead. + *

+ */ +public interface RemoteTCSObjectService + extends + Remote { + + // CHECKSTYLE:OFF + > T fetchObject( + ClientID clientId, + Class clazz, + TCSObjectReference ref + ) + throws RemoteException; + + > T fetchObject(ClientID clientId, Class clazz, String name) + throws RemoteException; + + > Set fetchObjects(ClientID clientId, Class clazz) + throws RemoteException; + + > Set fetchObjects( + ClientID clientId, + Class clazz, + Predicate predicate + ) + throws RemoteException; + + void updateObjectProperty( + ClientID clientId, + TCSObjectReference ref, + String key, + String value + ) + throws RemoteException; + + void appendObjectHistoryEntry( + ClientID clientId, + TCSObjectReference ref, + ObjectHistory.Entry entry + ) + throws RemoteException; + // CHECKSTYLE:ON +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteTCSObjectServiceProxy.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteTCSObjectServiceProxy.java new file mode 100644 index 0000000..6cd87be --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteTCSObjectServiceProxy.java @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.RemoteException; +import java.util.Set; +import java.util.function.Predicate; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectReference; + +/** + * The default implementation of the tcs object service. + * Delegates method invocations to the corresponding remote service. + * + * @param The remote service's type. + */ +abstract class RemoteTCSObjectServiceProxy + extends + AbstractRemoteServiceProxy + implements + TCSObjectService { + + @Override + public > T fetchObject(Class clazz, TCSObjectReference ref) + throws KernelRuntimeException { + checkServiceAvailability(); + + try { + return getRemoteService().fetchObject(getClientId(), clazz, ref); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public > T fetchObject(Class clazz, String name) + throws KernelRuntimeException { + checkServiceAvailability(); + + try { + return getRemoteService().fetchObject(getClientId(), clazz, name); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public > Set fetchObjects(Class clazz) + throws KernelRuntimeException { + checkServiceAvailability(); + + try { + return getRemoteService().fetchObjects(getClientId(), clazz); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public > Set fetchObjects( + Class clazz, + Predicate predicate + ) + throws KernelRuntimeException { + checkServiceAvailability(); + + try { + return getRemoteService().fetchObjects(getClientId(), clazz, predicate); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void updateObjectProperty(TCSObjectReference ref, String key, String value) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().updateObjectProperty(getClientId(), ref, key, value); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void appendObjectHistoryEntry(TCSObjectReference ref, ObjectHistory.Entry entry) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().appendObjectHistoryEntry(getClientId(), ref, entry); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteTransportOrderService.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteTransportOrderService.java new file mode 100644 index 0000000..acb2875 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteTransportOrderService.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.Remote; +import java.rmi.RemoteException; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.access.to.order.OrderSequenceCreationTO; +import org.opentcs.access.to.order.TransportOrderCreationTO; +import org.opentcs.components.kernel.services.TransportOrderService; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; + +/** + * Declares the methods provided by the {@link TransportOrderService} via RMI. + * + *

+ * The majority of the methods declared here have signatures analogous to their counterparts in + * {@link TransportOrderService}, with an additional {@link ClientID} parameter which serves the + * purpose of identifying the calling client and determining its permissions. + *

+ *

+ * To avoid redundancy, the semantics of methods that only pass through their arguments are not + * explicitly documented here again. See the corresponding API documentation in + * {@link TransportOrderService} for these, instead. + *

+ */ +public interface RemoteTransportOrderService + extends + RemoteTCSObjectService, + Remote { + + // CHECKSTYLE:OFF + OrderSequence createOrderSequence(ClientID clientId, OrderSequenceCreationTO to) + throws RemoteException; + + TransportOrder createTransportOrder(ClientID clientId, TransportOrderCreationTO to) + throws RemoteException; + + void markOrderSequenceComplete(ClientID clientId, TCSObjectReference ref) + throws RemoteException; + + void updateTransportOrderIntendedVehicle( + ClientID clientId, + TCSObjectReference orderRef, + TCSObjectReference vehicleRef + ) + throws RemoteException; + // CHECKSTYLE:ON +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteTransportOrderServiceProxy.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteTransportOrderServiceProxy.java new file mode 100644 index 0000000..16993e9 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteTransportOrderServiceProxy.java @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.RemoteException; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.to.order.OrderSequenceCreationTO; +import org.opentcs.access.to.order.TransportOrderCreationTO; +import org.opentcs.components.kernel.services.TransportOrderService; +import org.opentcs.data.ObjectExistsException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; + +/** + * The default implementation of the transport order service. + * Delegates method invocations to the corresponding remote service. + */ +class RemoteTransportOrderServiceProxy + extends + RemoteTCSObjectServiceProxy + implements + TransportOrderService { + + /** + * Creates a new instance. + */ + RemoteTransportOrderServiceProxy() { + } + + @Override + public OrderSequence createOrderSequence(OrderSequenceCreationTO to) + throws KernelRuntimeException { + checkServiceAvailability(); + + try { + return getRemoteService().createOrderSequence(getClientId(), to); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public TransportOrder createTransportOrder(TransportOrderCreationTO to) + throws ObjectUnknownException, + ObjectExistsException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + return getRemoteService().createTransportOrder(getClientId(), to); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void markOrderSequenceComplete(TCSObjectReference ref) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().markOrderSequenceComplete(getClientId(), ref); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void updateTransportOrderIntendedVehicle( + TCSObjectReference orderRef, + TCSObjectReference vehicleRef + ) + throws ObjectUnknownException, + IllegalArgumentException { + checkServiceAvailability(); + + try { + getRemoteService().updateTransportOrderIntendedVehicle(getClientId(), orderRef, vehicleRef); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteVehicleService.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteVehicleService.java new file mode 100644 index 0000000..1af8e1a --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteVehicleService.java @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.util.Set; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.components.kernel.services.VehicleService; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.model.Vehicle.EnergyLevelThresholdSet; +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; +import org.opentcs.drivers.vehicle.management.VehicleAttachmentInformation; +import org.opentcs.drivers.vehicle.management.VehicleProcessModelTO; +import org.opentcs.util.annotations.ScheduledApiChange; + +/** + * Declares the methods provided by the {@link VehicleService} via RMI. + * + *

+ * The majority of the methods declared here have signatures analogous to their counterparts in + * {@link VehicleService}, with an additional {@link ClientID} parameter which serves the purpose + * of identifying the calling client and determining its permissions. + *

+ *

+ * To avoid redundancy, the semantics of methods that only pass through their arguments are not + * explicitly documented here again. See the corresponding API documentation in + * {@link VehicleService} for these, instead. + *

+ */ +public interface RemoteVehicleService + extends + RemoteTCSObjectService, + Remote { + + // CHECKSTYLE:OFF + void attachCommAdapter( + ClientID clientId, TCSObjectReference ref, + VehicleCommAdapterDescription description + ) + throws RemoteException; + + void disableCommAdapter(ClientID clientId, TCSObjectReference ref) + throws RemoteException; + + void enableCommAdapter(ClientID clientId, TCSObjectReference ref) + throws RemoteException; + + VehicleAttachmentInformation fetchAttachmentInformation( + ClientID clientId, + TCSObjectReference ref + ) + throws RemoteException; + + VehicleProcessModelTO fetchProcessModel(ClientID clientId, TCSObjectReference ref) + throws RemoteException; + + void sendCommAdapterCommand( + ClientID clientId, + TCSObjectReference ref, + AdapterCommand command + ) + throws RemoteException; + + void sendCommAdapterMessage( + ClientID clientId, + TCSObjectReference vehicleRef, + Object message + ) + throws RemoteException; + + void updateVehicleIntegrationLevel( + ClientID clientId, + TCSObjectReference ref, + Vehicle.IntegrationLevel integrationLevel + ) + throws RemoteException; + + void updateVehiclePaused( + ClientID clientId, + TCSObjectReference ref, + boolean paused + ) + throws RemoteException; + + @ScheduledApiChange(when = "7.0", details = "Default implementation will be removed.") + default void updateVehicleEnergyLevelThresholdSet( + ClientID clientId, + TCSObjectReference ref, + EnergyLevelThresholdSet energyLevelThresholdSet + ) + throws RemoteException { + throw new UnsupportedOperationException("Not yet implemented."); + } + + void updateVehicleAllowedOrderTypes( + ClientID clientId, + TCSObjectReference ref, + Set allowedOrderTypes + ) + throws RemoteException; + + void updateVehicleEnvelopeKey( + ClientID clientId, + TCSObjectReference ref, + String envelopeKey + ) + throws RemoteException; + // CHECKSTYLE:ON +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteVehicleServiceProxy.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteVehicleServiceProxy.java new file mode 100644 index 0000000..2bdb43a --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/RemoteVehicleServiceProxy.java @@ -0,0 +1,221 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import java.rmi.RemoteException; +import java.util.Set; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.components.kernel.services.VehicleService; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.model.Vehicle.EnergyLevelThresholdSet; +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; +import org.opentcs.drivers.vehicle.management.VehicleAttachmentInformation; +import org.opentcs.drivers.vehicle.management.VehicleProcessModelTO; + +/** + * The default implementation of the vehicle service. + * Delegates method invocations to the corresponding remote service. + */ +class RemoteVehicleServiceProxy + extends + RemoteTCSObjectServiceProxy + implements + VehicleService { + + /** + * Creates a new instance. + */ + RemoteVehicleServiceProxy() { + } + + @Override + public void attachCommAdapter( + TCSObjectReference ref, + VehicleCommAdapterDescription description + ) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().attachCommAdapter(getClientId(), ref, description); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void disableCommAdapter(TCSObjectReference ref) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().disableCommAdapter(getClientId(), ref); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void enableCommAdapter(TCSObjectReference ref) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().enableCommAdapter(getClientId(), ref); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public VehicleAttachmentInformation fetchAttachmentInformation(TCSObjectReference ref) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + return getRemoteService().fetchAttachmentInformation(getClientId(), ref); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public VehicleProcessModelTO fetchProcessModel(TCSObjectReference ref) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + return getRemoteService().fetchProcessModel(getClientId(), ref); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void sendCommAdapterCommand(TCSObjectReference ref, AdapterCommand command) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().sendCommAdapterCommand(getClientId(), ref, command); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void sendCommAdapterMessage(TCSObjectReference ref, Object message) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().sendCommAdapterMessage(getClientId(), ref, message); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void updateVehicleIntegrationLevel( + TCSObjectReference ref, + Vehicle.IntegrationLevel integrationLevel + ) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().updateVehicleIntegrationLevel(getClientId(), ref, integrationLevel); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void updateVehiclePaused(TCSObjectReference ref, boolean paused) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().updateVehiclePaused(getClientId(), ref, paused); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void updateVehicleEnergyLevelThresholdSet( + TCSObjectReference ref, + EnergyLevelThresholdSet energyLevelThresholdSet + ) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().updateVehicleEnergyLevelThresholdSet( + getClientId(), + ref, + energyLevelThresholdSet + ); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void updateVehicleAllowedOrderTypes( + TCSObjectReference ref, + Set allowedOrderTypes + ) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().updateVehicleAllowedOrderTypes( + getClientId(), + ref, + allowedOrderTypes + ); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } + + @Override + public void updateVehicleEnvelopeKey(TCSObjectReference ref, String envelopeKey) + throws ObjectUnknownException, + KernelRuntimeException { + checkServiceAvailability(); + + try { + getRemoteService().updateVehicleEnvelopeKey(getClientId(), ref, envelopeKey); + } + catch (RemoteException ex) { + throw findSuitableExceptionFor(ex); + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/ServiceListener.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/ServiceListener.java new file mode 100644 index 0000000..8d6820f --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/ServiceListener.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +/** + * Provides callback methods for instances interested in service updates. + */ +public interface ServiceListener { + + /** + * Notifies a listener that the service is unavailable, i.e. is not in a usable state. + */ + void onServiceUnavailable(); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/package-info.java new file mode 100644 index 0000000..6d60e6f --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/rmi/services/package-info.java @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Interfaces and classes for transparently providing an openTCS kernel's service functionality via + * RMI. + */ +package org.opentcs.access.rmi.services; diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/to/CreationTO.java b/opentcs-api-base/src/main/java/org/opentcs/access/to/CreationTO.java new file mode 100644 index 0000000..9ae0c39 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/to/CreationTO.java @@ -0,0 +1,180 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.to; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The base class for all creation transfer objects. + */ +public class CreationTO + implements + Serializable { + + /** + * The name of this transfer object. + */ + @Nonnull + private final String name; + + /** + * The properties of this transfer object. + */ + @Nonnull + private final Map properties; + + /** + * Creates a new instance. + * + * @param name The name of this transfer object. + */ + public CreationTO( + @Nonnull + String name + ) { + this.name = requireNonNull(name, "name"); + this.properties = Map.of(); + } + + protected CreationTO( + @Nonnull + String name, + @Nonnull + Map properties + ) { + this.name = requireNonNull(name, "name"); + this.properties = requireNonNull(properties, "properties"); + } + + /** + * Returns the name of this transfer object. + * + * @return The name of this transfer object. + */ + @Nonnull + public String getName() { + return name; + } + + /** + * Creates a copy of this object with the given name. + * + * @param name the new name + * @return A copy of this object, differing in the given value. + */ + public CreationTO withName( + @Nonnull + String name + ) { + return new CreationTO( + name, + properties + ); + } + + /** + * Returns the properties of this transfer object in an unmodifiable map. + * + * @return The properties of this transfer object in an unmodifiable map. + */ + @Nonnull + public Map getProperties() { + return Collections.unmodifiableMap(properties); + } + + /** + * Returns the properties of this transfer object. + * + * @return The properties of this transfer object. + */ + protected Map getModifiableProperties() { + return properties; + } + + /** + * Creates a copy of this object with the given properties. + * + * @param properties The properties. + * @return A copy of this object with the given properties. + */ + public CreationTO withProperties( + @Nonnull + Map properties + ) { + return new CreationTO(name, properties); + } + + /** + * Creates a copy of this object with the given property. + * If value == null is true then the key-value pair is removed from the properties. + * + * @param key the key. + * @param value the value + * @return A copy of this object that includes the given property or + * removes the entry, if value == null. + */ + public CreationTO withProperty( + @Nonnull + String key, + @Nonnull + String value + ) { + return new CreationTO( + name, + propertiesWith(key, value) + ); + } + + protected final Map propertiesWith(String key, String value) { + return mapWithMapping(properties, key, value); + } + + /** + * Returns a new map, with the mappings of the given map and the given mapping added to it. + * + * @param The type of the map's keys. + * @param The type of the map's values. + * @param map The map to be extended. + * @param key The key. + * @param value The value. May be null to remove the mapping from the given map. + * @return a new map, with the mappings of the given map and the given mapping added to it. + */ + protected static final Map mapWithMapping(Map map, K key, V value) { + requireNonNull(map, "map"); + requireNonNull(key, "key"); + + Map result = new HashMap<>(map); + + if (value == null) { + result.remove(key); + } + else { + result.put(key, value); + } + + return result; + } + + /** + * Returns a new list, with the elements of the given list and the given element added to it. + * + * @param The element type of the list. + * @param list The list to be extended. + * @param newElement The element to be added to the list. + * @return A new list, consisting of the given list and the given element added to it. + */ + protected static final List listWithAppendix(List list, T newElement) { + List result = new ArrayList<>(list.size() + 1); + result.addAll(list); + result.add(newElement); + return result; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/to/model/BlockCreationTO.java b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/BlockCreationTO.java new file mode 100644 index 0000000..f3295cf --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/BlockCreationTO.java @@ -0,0 +1,291 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.to.model; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.awt.Color; +import java.io.Serializable; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import org.opentcs.access.to.CreationTO; +import org.opentcs.data.model.Block; + +/** + * A transfer object describing a block in the plant model. + */ +public class BlockCreationTO + extends + CreationTO + implements + Serializable { + + /** + * This block's type. + */ + @Nonnull + private final Block.Type type; + /** + * This block's member names. + */ + @Nonnull + private final Set memberNames; + /** + * The information regarding the grahical representation of this block. + */ + private final Layout layout; + + /** + * Creates a new instance. + * + * @param name The name of this block. + */ + public BlockCreationTO(String name) { + super(name); + this.type = Block.Type.SINGLE_VEHICLE_ONLY; + this.memberNames = Set.of(); + this.layout = new Layout(); + } + + /** + * Creates a new block. + * + * @param name the name of the new block. + * @param memberNames the names of the block's members. + * @param properties the properties. + */ + private BlockCreationTO( + @Nonnull + String name, + @Nonnull + Map properties, + @Nonnull + Block.Type type, + @Nonnull + Set memberNames, + @Nonnull + Layout layout + ) { + super(name, properties); + this.type = requireNonNull(type, "type"); + this.memberNames = requireNonNull(memberNames, "memberNames"); + this.layout = requireNonNull(layout, "layout"); + } + + /** + * Creates a copy of this object with the given name. + * + * @param name The new name. + * @return A copy of this object, differing in the given name. + */ + @Override + public BlockCreationTO withName( + @Nonnull + String name + ) { + return new BlockCreationTO( + name, + getModifiableProperties(), + type, + memberNames, + layout + ); + } + + /** + * Creates a copy of this object with the given properties. + * + * @param properties The new properties. + * @return A copy of this object, differing in the given properties. + */ + @Override + public BlockCreationTO withProperties( + @Nonnull + Map properties + ) { + return new BlockCreationTO( + getName(), + properties, + type, + memberNames, + layout + ); + } + + /** + * Creates a copy of this object and adds the given property. + * If value == null, then the key-value pair is removed from the properties. + * + * @param key the key. + * @param value the value + * @return A copy of this object that either + * includes the given entry in it's current properties, if value != null or + * excludes the entry otherwise. + */ + @Override + public BlockCreationTO withProperty( + @Nonnull + String key, + @Nonnull + String value + ) { + return new BlockCreationTO( + getName(), + propertiesWith(key, value), + type, + memberNames, + layout + ); + } + + /** + * Returns the type of this block. + * + * @return The type of this block. + */ + @Nonnull + public Block.Type getType() { + return type; + } + + /** + * Creates a copy of this object with the given type. + * + * @param type The new type. + * @return A copy of this object, differing in the given type. + */ + public BlockCreationTO withType( + @Nonnull + Block.Type type + ) { + return new BlockCreationTO( + getName(), + getModifiableProperties(), + type, + memberNames, + layout + ); + } + + /** + * Returns the names of this block's members. + * + * @return The names of this block's members. + */ + @Nonnull + public Set getMemberNames() { + return Collections.unmodifiableSet(memberNames); + } + + /** + * Creates a copy of this object with the given members. + * + * @param memberNames The names of the block's members. + * @return A copy of this object, differing in the given value. + */ + public BlockCreationTO withMemberNames( + @Nonnull + Set memberNames + ) { + return new BlockCreationTO( + getName(), + getModifiableProperties(), + type, + memberNames, + layout + ); + } + + /** + * Returns the information regarding the grahical representation of this block. + * + * @return The information regarding the grahical representation of this block. + */ + public Layout getLayout() { + return layout; + } + + /** + * Creates a copy of this object, with the given layout. + * + * @param layout The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public BlockCreationTO withLayout(Layout layout) { + return new BlockCreationTO( + getName(), + getModifiableProperties(), + type, + memberNames, + layout + ); + } + + @Override + public String toString() { + return "BlockCreationTO{" + + "name=" + getName() + + ", type=" + type + + ", memberNames=" + memberNames + + ", layout=" + layout + + ", properties=" + getProperties() + + '}'; + } + + /** + * Contains information regarding the grahical representation of a block. + */ + public static class Layout + implements + Serializable { + + /** + * The color in which block elements are to be emphasized. + */ + private final Color color; + + /** + * Creates a new instance. + */ + public Layout() { + this(Color.RED); + } + + /** + * Creates a new instance. + * + * @param color The color in which block elements are to be emphasized. + */ + public Layout(Color color) { + this.color = requireNonNull(color, "color"); + } + + /** + * Returns the color in which block elements are to be emphasized. + * + * @return The color in which block elements are to be emphasized. + */ + public Color getColor() { + return color; + } + + /** + * Creates a copy of this object, with the given color. + * + * @param color The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withColor(Color color) { + return new Layout(color); + } + + @Override + public String toString() { + return "Layout{" + + "color=" + color + + '}'; + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/to/model/BoundingBoxCreationTO.java b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/BoundingBoxCreationTO.java new file mode 100644 index 0000000..f857025 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/BoundingBoxCreationTO.java @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.to.model; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkInRange; + +import java.io.Serializable; + +/** + * A transfer object describing a bounding box. + */ +public class BoundingBoxCreationTO + implements + Serializable { + + private final long length; + private final long width; + private final long height; + private final CoupleCreationTO referenceOffset; + + /** + * Creates a new instance with a (0, 0) reference offset. + * + * @param length The bounding box's length. + * @param width The bounding box's width. + * @param height The bounding box's height. + */ + public BoundingBoxCreationTO(long length, long width, long height) { + this(length, width, height, new CoupleCreationTO(0, 0)); + } + + private BoundingBoxCreationTO( + long length, + long width, + long height, + CoupleCreationTO referenceOffset + ) { + this.length = checkInRange(length, 1, Long.MAX_VALUE, "length"); + this.width = checkInRange(width, 1, Long.MAX_VALUE, "width"); + this.height = checkInRange(height, 1, Long.MAX_VALUE, "height"); + this.referenceOffset = requireNonNull(referenceOffset, "referenceOffset"); + } + + /** + * Returns the bounding box's length. + * + * @return The bounding box's length. + */ + public long getLength() { + return length; + } + + /** + * Creates a copy of this object, with the given length. + * + * @param length The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public BoundingBoxCreationTO withLength(long length) { + return new BoundingBoxCreationTO(length, width, height, referenceOffset); + } + + /** + * Returns the bounding box's width. + * + * @return The bounding box's width. + */ + public long getWidth() { + return width; + } + + /** + * Creates a copy of this object, with the given width. + * + * @param width The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public BoundingBoxCreationTO withWidth(long width) { + return new BoundingBoxCreationTO(length, width, height, referenceOffset); + } + + /** + * Returns the bounding box's height. + * + * @return The bounding box's height. + */ + public long getHeight() { + return height; + } + + /** + * Creates a copy of this object, with the given height. + * + * @param height The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public BoundingBoxCreationTO withHeight(long height) { + return new BoundingBoxCreationTO(length, width, height, referenceOffset); + } + + /** + * Returns the bounding box's reference offset. + * + * @return The bounding box's reference offset. + */ + public CoupleCreationTO getReferenceOffset() { + return referenceOffset; + } + + /** + * Creates a copy of this object, with the given reference offset. + * + * @param referenceOffset The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public BoundingBoxCreationTO withReferenceOffset(CoupleCreationTO referenceOffset) { + return new BoundingBoxCreationTO(length, width, height, referenceOffset); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/to/model/CoupleCreationTO.java b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/CoupleCreationTO.java new file mode 100644 index 0000000..019c61d --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/CoupleCreationTO.java @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.to.model; + +import java.io.Serializable; + +/** + * A transfer object describing generic 2-tuple of long integer values. + */ +public class CoupleCreationTO + implements + Serializable { + + private final long x; + private final long y; + + /** + * Creates a new instance. + * + * @param x The X coordinate. + * @param y The Y coordinate. + */ + public CoupleCreationTO(long x, long y) { + this.x = x; + this.y = y; + } + + /** + * Returns the x coordinate. + * + * @return The x coordinate. + */ + public long getX() { + return x; + } + + /** + * Creates a copy of this object, with the given x coordinate. + * + * @param x The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public CoupleCreationTO withX(long x) { + return new CoupleCreationTO(x, y); + } + + /** + * Returns the y coordinate. + * + * @return The y coordinate. + */ + public long getY() { + return y; + } + + /** + * Creates a copy of this object, with the given y coordinate. + * + * @param y The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public CoupleCreationTO withY(long y) { + return new CoupleCreationTO(x, y); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/to/model/LocationCreationTO.java b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/LocationCreationTO.java new file mode 100644 index 0000000..d1fedc1 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/LocationCreationTO.java @@ -0,0 +1,515 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.to.model; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import org.opentcs.access.to.CreationTO; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Triple; +import org.opentcs.data.model.visualization.LocationRepresentation; + +/** + * A transfer object describing a location in a plant model. + */ +public class LocationCreationTO + extends + CreationTO + implements + Serializable { + + /** + * The name of this location's type. + */ + @Nonnull + private final String typeName; + /** + * This location's position (in mm). + */ + @Nonnull + private final Triple position; + /** + * The links attaching points to this location. + * This is a map of point names to allowed operations. + */ + @Nonnull + private final Map> links; + /** + * A flag for marking this location as locked (i.e. to prevent vehicles from using it). + */ + private final boolean locked; + /** + * The information regarding the grahical representation of this location. + */ + private final Layout layout; + + /** + * Creates a new instance. + * + * @param name The name of this location. + * @param typeName The name of this location's type. + * @param position The position of this location. + */ + public LocationCreationTO( + @Nonnull + String name, + @Nonnull + String typeName, + @Nonnull + Triple position + ) { + super(name); + this.typeName = requireNonNull(typeName, "typeName"); + this.position = position; + this.links = Map.of(); + this.locked = false; + this.layout = new Layout(); + } + + private LocationCreationTO( + @Nonnull + String name, + @Nonnull + Map properties, + @Nonnull + String typeName, + @Nonnull + Triple position, + @Nonnull + Map> links, + boolean locked, + @Nonnull + Layout layout + ) { + super(name, properties); + this.typeName = requireNonNull(typeName, "typeName"); + this.position = requireNonNull(position, "position"); + this.links = requireNonNull(links, "links"); + this.locked = locked; + this.layout = requireNonNull(layout, "layout"); + } + + /** + * Creates a copy of this object with the given name. + * + * @param name The new name. + * @return A copy of this object, differing in the given name. + */ + @Override + public LocationCreationTO withName( + @Nonnull + String name + ) { + return new LocationCreationTO( + name, + getModifiableProperties(), + typeName, + position, + links, + locked, + layout + ); + } + + /** + * Returns the name of this location's type. + * + * @return The name of this location's type. + */ + @Nonnull + public String getTypeName() { + return typeName; + } + + /** + * Creates a copy of this object with the location's type. + * + * @param typeName The location type. + * @return A copy of this object, differing in the given type. + */ + public LocationCreationTO withTypeName( + @Nonnull + String typeName + ) { + return new LocationCreationTO( + getName(), + getModifiableProperties(), + typeName, + position, + links, + locked, + layout + ); + } + + /** + * Returns the position of this location (in mm). + * + * @return The position of this location (in mm). + */ + @Nonnull + public Triple getPosition() { + return position; + } + + /** + * Creates a copy of this object with the given position (in mm). + * + * @param position the new position of this location (in mm). + * @return A copy of this object, differing in the given position. + */ + public LocationCreationTO withPosition( + @Nonnull + Triple position + ) { + return new LocationCreationTO( + getName(), + getModifiableProperties(), + typeName, + position, + links, + locked, + layout + ); + } + + /** + * Returns the links attaching points to this location. + * This is a map of point names to allowed operations. + * + * @return The links attaching points to this location. + */ + @Nonnull + public Map> getLinks() { + return Collections.unmodifiableMap(links); + } + + /** + * Creates a copy of this object with the given links that attach points to this location. + * + * @param links the new links. This is supposed to be a map of point names to allowed operations. + * @return A copy of this object, differing in the given links. + */ + public LocationCreationTO withLinks( + @Nonnull + Map> links + ) { + return new LocationCreationTO( + getName(), + getModifiableProperties(), + typeName, + position, + links, + locked, + layout + ); + } + + /** + * Creates a copy of this object with the given links that attach points to this location. + * + * @param pointName The name of the point linked to. + * @param allowedOperations The operations allowed at the point. + * @return A copy of this object, differing in the given link. + */ + public LocationCreationTO withLink( + @Nonnull + String pointName, + @Nonnull + Set allowedOperations + ) { + return new LocationCreationTO( + getName(), + getModifiableProperties(), + typeName, + position, + mapWithMapping(links, pointName, allowedOperations), + locked, + layout + ); + } + + /** + * Returns the lock status of this location (i.e. whether it my be used by vehicles or not). + * + * @return {@code true} if this location is currently locked (i.e. it may not be used + * by vehicles), else {@code false}. + */ + public boolean isLocked() { + return locked; + } + + /** + * Creates a copy of this object with the given locked flag. + * + * @param locked The new locked attribute. + * @return A copy of this object, differing in the locked attribute. + */ + public LocationCreationTO withLocked(boolean locked) { + return new LocationCreationTO( + getName(), + getModifiableProperties(), + typeName, + position, + links, + locked, + layout + ); + } + + /** + * Creates a copy of this object with the given properties. + * + * @param properties The new properties. + * @return A copy of this object, differing in the given properties. + */ + @Override + public LocationCreationTO withProperties( + @Nonnull + Map properties + ) { + return new LocationCreationTO( + getName(), + properties, + typeName, + position, + links, + locked, + layout + ); + } + + /** + * Creates a copy of this object and adds the given property. + * If value == null, then the key-value pair is removed from the properties. + * + * @param key the key. + * @param value the value + * @return A copy of this object that either + * includes the given entry in it's current properties, if value != null or + * excludes the entry otherwise. + */ + @Override + public LocationCreationTO withProperty( + @Nonnull + String key, + @Nonnull + String value + ) { + return new LocationCreationTO( + getName(), + propertiesWith(key, value), + typeName, + position, + links, + locked, + layout + ); + } + + /** + * Returns the information regarding the grahical representation of this location. + * + * @return The information regarding the grahical representation of this location. + */ + public Layout getLayout() { + return layout; + } + + /** + * Creates a copy of this object, with the given layout. + * + * @param layout The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public LocationCreationTO withLayout(Layout layout) { + return new LocationCreationTO( + getName(), + getModifiableProperties(), + typeName, + position, + links, + locked, + layout + ); + } + + @Override + public String toString() { + return "LocationCreationTO{" + + "name=" + getName() + + ", typeName=" + typeName + + ", position=" + position + + ", links=" + links + + ", locked=" + locked + + ", layout=" + layout + + ", properties=" + getProperties() + + '}'; + } + + /** + * Contains information regarding the grahical representation of a location. + */ + public static class Layout + implements + Serializable { + + /** + * The coordinates at which the location is to be drawn (in mm). + */ + private final Couple position; + /** + * The offset of the label's position to the location's position (in lu). + */ + private final Couple labelOffset; + /** + * The location representation to use. + */ + private final LocationRepresentation locationRepresentation; + /** + * The ID of the layer on which the location is to be drawn. + */ + private final int layerId; + + /** + * Creates a new instance. + */ + public Layout() { + this(new Couple(0, 0), new Couple(0, 0), LocationRepresentation.DEFAULT, 0); + } + + /** + * Creates a new instance. + * + * @param position The coordinates at which the location is to be drawn (in mm). + * @param labelOffset The offset of the label's location to the point's position (in lu). + * @param locationRepresentation The location representation to use. + * @param layerId The ID of the layer on which the location is to be drawn. + */ + public Layout( + Couple position, + Couple labelOffset, + LocationRepresentation locationRepresentation, + int layerId + ) { + this.position = requireNonNull(position, "position"); + this.labelOffset = requireNonNull(labelOffset, "labelOffset"); + this.locationRepresentation = requireNonNull( + locationRepresentation, + "locationRepresentation" + ); + this.layerId = layerId; + } + + /** + * Returns the coordinates at which the location is to be drawn (in mm). + * + * @return The coordinates at which the location is to be drawn (in mm). + */ + public Couple getPosition() { + return position; + } + + /** + * Creates a copy of this object, with the given position. + * + * @param position The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withPosition(Couple position) { + return new Layout( + position, + labelOffset, + locationRepresentation, + layerId + ); + } + + /** + * Returns the offset of the label's position to the location's position (in lu). + * + * @return The offset of the label's position to the location's position (in lu). + */ + public Couple getLabelOffset() { + return labelOffset; + } + + /** + * Creates a copy of this object, with the given X label offset. + * + * @param labelOffset The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withLabelOffset(Couple labelOffset) { + return new Layout( + position, + labelOffset, + locationRepresentation, + layerId + ); + } + + /** + * Returns the location representation to use. + * + * @return The location representation to use. + */ + public LocationRepresentation getLocationRepresentation() { + return locationRepresentation; + } + + /** + * Creates a copy of this object, with the given location representation. + * + * @param locationRepresentation The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withLocationRepresentation(LocationRepresentation locationRepresentation) { + return new Layout( + position, + labelOffset, + locationRepresentation, + layerId + ); + } + + /** + * Returns the ID of the layer on which the location is to be drawn. + * + * @return The layer ID. + */ + public int getLayerId() { + return layerId; + } + + /** + * Creates a copy of this object, with the given layer ID. + * + * @param layerId The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withLayerId(int layerId) { + return new Layout( + position, + labelOffset, + locationRepresentation, + layerId + ); + } + + @Override + public String toString() { + return "Layout{" + + "position=" + position + + ", labelOffset=" + labelOffset + + ", locationRepresentation=" + locationRepresentation + + ", layerId=" + layerId + + '}'; + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/to/model/LocationTypeCreationTO.java b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/LocationTypeCreationTO.java new file mode 100644 index 0000000..06faa22 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/LocationTypeCreationTO.java @@ -0,0 +1,291 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.to.model; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.opentcs.access.to.CreationTO; +import org.opentcs.data.model.visualization.LocationRepresentation; + +/** + * A transfer object describing a location type in the plant model. + */ +public class LocationTypeCreationTO + extends + CreationTO + implements + Serializable { + + /** + * The allowed operations for this location type. + */ + private final List allowedOperations; + /** + * The allowed peripheral operations for this location type. + */ + private final List allowedPeripheralOperations; + /** + * The information regarding the grahical representation of this location type. + */ + private final Layout layout; + + /** + * Creates a new instance. + * + * @param name The name of this location type. + */ + public LocationTypeCreationTO( + @Nonnull + String name + ) { + super(name); + this.allowedOperations = List.of(); + this.allowedPeripheralOperations = List.of(); + this.layout = new Layout(); + } + + private LocationTypeCreationTO( + @Nonnull + String name, + @Nonnull + Map properties, + @Nonnull + List allowedOperations, + @Nonnull + List allowedPeripheralOperations, + @Nonnull + Layout layout + ) { + super(name, properties); + this.allowedOperations = requireNonNull(allowedOperations, "allowedOperations"); + this.allowedPeripheralOperations = requireNonNull( + allowedPeripheralOperations, + "allowedPeripheralOperations" + ); + this.layout = requireNonNull(layout, "layout"); + } + + /** + * Returns the allowed operations for this location type. + * + * @return The allowed operations for this location type. + */ + @Nonnull + public List getAllowedOperations() { + return Collections.unmodifiableList(allowedOperations); + } + + /** + * Creates a copy of this object with the given allowed operations. + * + * @param allowedOperations the new allowed operations. + * @return A copy of this object, differing in the given value. + */ + public LocationTypeCreationTO withAllowedOperations( + @Nonnull + List allowedOperations + ) { + return new LocationTypeCreationTO( + getName(), + getModifiableProperties(), + allowedOperations, + allowedPeripheralOperations, + layout + ); + } + + /** + * Returns the allowed peripheral operations for this location type. + * + * @return The allowed peripheral operations for this location type. + */ + @Nonnull + public List getAllowedPeripheralOperations() { + return Collections.unmodifiableList(allowedPeripheralOperations); + } + + /** + * Creates a copy of this object with the given allowed peripheral operations. + * + * @param allowedPeripheralOperations the new allowed peripheral operations. + * @return A copy of this object, differing in the given value. + */ + public LocationTypeCreationTO withAllowedPeripheralOperations( + @Nonnull + List allowedPeripheralOperations + ) { + return new LocationTypeCreationTO( + getName(), + getModifiableProperties(), + allowedOperations, + allowedPeripheralOperations, + layout + ); + } + + /** + * Creates a copy of this object with the given name. + * + * @param name The new name. + * @return A copy of this object, differing in the given name. + */ + @Override + public LocationTypeCreationTO withName( + @Nonnull + String name + ) { + return new LocationTypeCreationTO( + name, + getProperties(), + allowedOperations, + allowedPeripheralOperations, + layout + ); + } + + /** + * Creates a copy of this object with the given properties. + * + * @param properties The new properties. + * @return A copy of this object, differing in the given properties. + */ + @Override + public LocationTypeCreationTO withProperties( + @Nonnull + Map properties + ) { + return new LocationTypeCreationTO( + getName(), + properties, + allowedOperations, + allowedPeripheralOperations, + layout + ); + } + + /** + * Creates a copy of this object and adds the given property. + * If value == null, then the key-value pair is removed from the properties. + * + * @param key the key. + * @param value the value + * @return A copy of this object that either + * includes the given entry in it's current properties, if value != null or + * excludes the entry otherwise. + */ + @Override + public LocationTypeCreationTO withProperty( + @Nonnull + String key, + @Nonnull + String value + ) { + return new LocationTypeCreationTO( + getName(), + propertiesWith(key, value), + allowedOperations, + allowedPeripheralOperations, + layout + ); + } + + /** + * Returns the information regarding the grahical representation of this location type. + * + * @return The information regarding the grahical representation of this location type. + */ + public Layout getLayout() { + return layout; + } + + /** + * Creates a copy of this object, with the given layout. + * + * @param layout The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public LocationTypeCreationTO withLayout(Layout layout) { + return new LocationTypeCreationTO( + getName(), + getModifiableProperties(), + allowedOperations, + allowedPeripheralOperations, + layout + ); + } + + @Override + public String toString() { + return "LocationTypeCreationTO{" + + "name=" + getName() + + ", allowedOperations=" + allowedOperations + + ", allowedPeripheralOperations" + allowedPeripheralOperations + + ", layout=" + layout + + ", properties=" + getProperties() + + '}'; + } + + /** + * Contains information regarding the grahical representation of a location type. + */ + public static class Layout + implements + Serializable { + + /** + * The location representation to use for locations with this location type. + */ + private final LocationRepresentation locationRepresentation; + + /** + * Creates a new instance. + */ + public Layout() { + this(LocationRepresentation.NONE); + } + + /** + * Creates a new instance. + * + * @param locationRepresentation The location representation to use for locations with this + * location type. + */ + public Layout(LocationRepresentation locationRepresentation) { + this.locationRepresentation = requireNonNull( + locationRepresentation, + "locationRepresentation" + ); + } + + /** + * Returns the location representation to use for locations with this location type. + * + * @return The location representation to use for locations with this location type. + */ + public LocationRepresentation getLocationRepresentation() { + return locationRepresentation; + } + + /** + * Creates a copy of this object, with the given location representation. + * + * @param locationRepresentation The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withLocationRepresentation(LocationRepresentation locationRepresentation) { + return new Layout(locationRepresentation); + } + + @Override + public String toString() { + return "Layout{" + + "locationRepresentation=" + locationRepresentation + + '}'; + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/to/model/PathCreationTO.java b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/PathCreationTO.java new file mode 100644 index 0000000..8825ff7 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/PathCreationTO.java @@ -0,0 +1,651 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.to.model; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.opentcs.access.to.CreationTO; +import org.opentcs.access.to.peripherals.PeripheralOperationCreationTO; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Envelope; +import org.opentcs.data.model.Path.Layout.ConnectionType; + +/** + * A transfer object describing a path in the plant model. + */ +public class PathCreationTO + extends + CreationTO + implements + Serializable { + + /** + * The point name this path originates in. + */ + @Nonnull + private final String srcPointName; + /** + * The point name this path ends in. + */ + @Nonnull + private final String destPointName; + /** + * This path's length (in mm). + */ + private final long length; + /** + * The absolute maximum allowed forward velocity on this path (in mm/s). + * A value of 0 (default) means forward movement is not allowed on this path. + */ + private final int maxVelocity; + /** + * The absolute maximum allowed reverse velocity on this path (in mm/s). + * A value of 0 (default) means reverse movement is not allowed on this path. + */ + private final int maxReverseVelocity; + /** + * The peripheral operations to be performed when a vehicle travels along this path. + */ + private final List peripheralOperations; + /** + * A flag for marking this path as locked (i.e. to prevent vehicles from using it). + */ + private final boolean locked; + /** + * A map of envelope keys to envelopes that vehicles traversing this path may occupy. + */ + private final Map vehicleEnvelopes; + /** + * The information regarding the grahical representation of this path. + */ + private final Layout layout; + + /** + * Creates a new instance. + * + * @param name The name of this path. + * @param srcPointName The point name this path originates in. + * @param destPointName The point name this path ends in. + */ + public PathCreationTO( + @Nonnull + String name, + @Nonnull + String srcPointName, + @Nonnull + String destPointName + ) { + super(name); + this.srcPointName = requireNonNull(srcPointName, "srcPointName"); + this.destPointName = requireNonNull(destPointName, "destPointName"); + this.length = 1; + this.maxVelocity = 0; + this.maxReverseVelocity = 0; + this.peripheralOperations = List.of(); + this.locked = false; + this.vehicleEnvelopes = Map.of(); + this.layout = new Layout(); + } + + private PathCreationTO( + String name, + @Nonnull + String srcPointName, + @Nonnull + String destPointName, + @Nonnull + Map properties, + long length, + int maxVelocity, + int maxReverseVelocity, + List peripheralOperations, + boolean locked, + @Nonnull + Map vehicleEnvelopes, + @Nonnull + Layout layout + ) { + super(name, properties); + this.srcPointName = requireNonNull(srcPointName, "srcPointName"); + this.destPointName = requireNonNull(destPointName, "destPointName"); + this.length = length; + this.maxVelocity = maxVelocity; + this.maxReverseVelocity = maxReverseVelocity; + this.peripheralOperations = new ArrayList<>( + requireNonNull( + peripheralOperations, + "peripheralOperations" + ) + ); + this.locked = locked; + this.vehicleEnvelopes = requireNonNull(vehicleEnvelopes, "vehicleEnvelopes"); + this.layout = requireNonNull(layout, "layout"); + } + + /** + * Creates a copy of this object with the given name. + * + * @param name The new name. + * @return A copy of this object, differing in the given name. + */ + @Override + public PathCreationTO withName( + @Nonnull + String name + ) { + return new PathCreationTO( + name, + srcPointName, + destPointName, + getModifiableProperties(), + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + /** + * Returns the point name this path originates in. + * + * @return The point name this path originates in. + */ + @Nonnull + public String getSrcPointName() { + return srcPointName; + } + + /** + * Creates a copy of this object with the given point name this path originates in. + * + * @param srcPointName The new source point name. + * @return A copy of this object, differing in the given source point. + */ + public PathCreationTO withSrcPointName( + @Nonnull + String srcPointName + ) { + return new PathCreationTO( + getName(), + srcPointName, + destPointName, + getModifiableProperties(), + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + /** + * Returns the point name this path ends in. + * + * @return The point name this path ends in. + */ + @Nonnull + public String getDestPointName() { + return destPointName; + } + + /** + * Creates a copy of this object with the given destination point. + * + * @param destPointName The new source point. + * @return A copy of this object, differing in the given value. + */ + public PathCreationTO withDestPointName( + @Nonnull + String destPointName + ) { + return new PathCreationTO( + getName(), + srcPointName, + destPointName, + getModifiableProperties(), + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + /** + * Returns the length of this path (in mm). + * + * @return The length of this path (in mm). + */ + public long getLength() { + return length; + } + + /** + * Creates a copy of this object with the given path length (in mm). + * + * @param length the new length (in mm). Must be a positive value. + * @return A copy of this object, differing in the given length. + */ + public PathCreationTO withLength(long length) { + checkArgument(length > 0, "length must be a positive value: " + length); + return new PathCreationTO( + getName(), + srcPointName, + destPointName, + getModifiableProperties(), + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + /** + * Returns the maximum allowed forward velocity (in mm/s) for this path. + * + * @return The maximum allowed forward velocity (in mm/s). A value of 0 means forward movement is + * not allowed on this path. + */ + public int getMaxVelocity() { + return maxVelocity; + } + + /** + * Creates a copy of this object with the maximum allowed forward velocity (in mm/s) for this + * path. + * + * @param maxVelocity The new maximum allowed velocity (in mm/s). May not be a negative value. + * @return A copy of this object, differing in the given maximum velocity. + */ + public PathCreationTO withMaxVelocity(int maxVelocity) { + checkArgument( + maxVelocity >= 0, + "maxVelocity may not be a negative value: " + maxVelocity + ); + return new PathCreationTO( + getName(), + srcPointName, + destPointName, + getModifiableProperties(), + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + /** + * Returns the maximum allowed reverse velocity (in mm/s) for this path. + * + * @return The maximum allowed reverse velocity (in mm/s). A value of 0 means reverse movement is + * not allowed on this path. + */ + public int getMaxReverseVelocity() { + return maxReverseVelocity; + } + + /** + * Creates a copy of this object with the allowed maximum reverse velocity (in mm/s). + * + * @param maxReverseVelocity The new maximum allowed reverse velocity (in mm/s). Must not be a + * negative value. + * @return A copy of this object, differing in the given maximum reverse velocity. + */ + public PathCreationTO withMaxReverseVelocity(int maxReverseVelocity) { + checkArgument( + maxReverseVelocity >= 0, + "maxReverseVelocity may not be a negative value: " + maxReverseVelocity + ); + return new PathCreationTO( + getName(), + srcPointName, + destPointName, + getModifiableProperties(), + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + /** + * Returns the peripheral operations to be performed when a vehicle travels along this path. + * + * @return The peripheral operations to be performed when a vehicle travels along this path. + */ + public List getPeripheralOperations() { + return Collections.unmodifiableList(peripheralOperations); + } + + /** + * Creates a copy of this object with the given peripheral operations. + * + * @param peripheralOperations The peripheral operations. + * @return A copy of this object, differing in the given peripheral operations. + */ + public PathCreationTO withPeripheralOperations( + @Nonnull + List peripheralOperations + ) { + return new PathCreationTO( + getName(), + srcPointName, + destPointName, + getModifiableProperties(), + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + /** + * Returns the lock status of this path (i.e. whether this path my be used by vehicles or not). + * + * @return {@code true} if this path is currently locked (i.e. it may not be used by vehicles), + * else {@code false}. + */ + public boolean isLocked() { + return locked; + } + + /** + * Creates a copy of this object that is locked if {@code locked==true} and unlocked otherwise. + * + * @param locked If {@code true}, this path will be locked when the method call returns; if + * {@code false}, this path will be unlocked. + * @return a copy of this object, differing in the locked attribute. + */ + public PathCreationTO withLocked(boolean locked) { + return new PathCreationTO( + getName(), + srcPointName, + destPointName, + getModifiableProperties(), + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + /** + * Creates a copy of this object with the given properties. + * + * @param properties The new properties. + * @return A copy of this object, differing in the given properties. + */ + @Override + public PathCreationTO withProperties( + @Nonnull + Map properties + ) { + return new PathCreationTO( + getName(), + srcPointName, + destPointName, + properties, + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + /** + * Creates a copy of this object and adds the given property. + * If value == null, then the key-value pair is removed from the properties. + * + * @param key the key. + * @param value the value + * @return A copy of this object that either + * includes the given entry in it's current properties, if value != null or + * excludes the entry otherwise. + */ + @Override + public PathCreationTO withProperty( + @Nonnull + String key, + @Nonnull + String value + ) { + return new PathCreationTO( + getName(), + srcPointName, + destPointName, + propertiesWith(key, value), + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + /** + * Returns a map of envelope keys to envelopes that vehicles traversing this path may occupy. + * + * @return A map of envelope keys to envelopes that vehicles traversing this path may occupy. + */ + public Map getVehicleEnvelopes() { + return vehicleEnvelopes; + } + + /** + * Creates a copy of this object, with the given vehicle envelopes. + * + * @param vehicleEnvelopes The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PathCreationTO withVehicleEnvelopes( + @Nonnull + Map vehicleEnvelopes + ) { + return new PathCreationTO( + getName(), + srcPointName, + destPointName, + getModifiableProperties(), + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + /** + * Returns the information regarding the grahical representation of this path. + * + * @return The information regarding the grahical representation of this path. + */ + public Layout getLayout() { + return layout; + } + + /** + * Creates a copy of this object, with the given layout. + * + * @param layout The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PathCreationTO withLayout(Layout layout) { + return new PathCreationTO( + getName(), + srcPointName, + destPointName, + getModifiableProperties(), + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + @Override + public String toString() { + return "PathCreationTO{" + + "name=" + getName() + + ", srcPointName=" + srcPointName + + ", destPointName=" + destPointName + + ", length=" + length + + ", maxVelocity=" + maxVelocity + + ", maxReverseVelocity=" + maxReverseVelocity + + ", peripheralOperations=" + peripheralOperations + + ", locked=" + locked + + ", layout=" + layout + + ", vehicleEnvelopes=" + vehicleEnvelopes + + ", properties=" + getProperties() + + '}'; + } + + /** + * Contains information regarding the grahical representation of a path. + */ + public static class Layout + implements + Serializable { + + /** + * The connection type the path is represented as. + */ + private final ConnectionType connectionType; + /** + * Control points describing the way the path is drawn (if the connection type + * is {@link ConnectionType#BEZIER}, {@link ConnectionType#BEZIER_3} + * or {@link ConnectionType#POLYPATH}). + */ + private final List controlPoints; + /** + * The ID of the layer on which the path is to be drawn. + */ + private final int layerId; + + /** + * Creates a new instance. + */ + public Layout() { + this(ConnectionType.DIRECT, new ArrayList<>(), 0); + } + + /** + * Creates a new instance. + * + * @param connectionType The connection type a path is represented as. + * @param controlPoints Control points describing the way the path is drawn. + * @param layerId The ID of the layer on which the path is to be drawn. + */ + public Layout(ConnectionType connectionType, List controlPoints, int layerId) { + this.connectionType = connectionType; + this.controlPoints = requireNonNull(controlPoints, "controlPoints"); + this.layerId = layerId; + } + + /** + * Returns the connection type the path is represented as. + * + * @return The connection type the path is represented as. + */ + public ConnectionType getConnectionType() { + return connectionType; + } + + /** + * Creates a copy of this object, with the given connection type. + * + * @param connectionType The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withConnectionType(ConnectionType connectionType) { + return new Layout(connectionType, controlPoints, layerId); + } + + /** + * Returns the control points describing the way the path is drawn. + * Returns an empty list if connection type is not {@link ConnectionType#BEZIER}, + * {@link ConnectionType#BEZIER_3} or {@link ConnectionType#POLYPATH}. + * + * @return The control points describing the way the path is drawn. + */ + public List getControlPoints() { + return Collections.unmodifiableList(controlPoints); + } + + /** + * Creates a copy of this object, with the given control points. + * + * @param controlPoints The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withControlPoints(List controlPoints) { + return new Layout(connectionType, controlPoints, layerId); + } + + /** + * Returns the ID of the layer on which the path is to be drawn. + * + * @return The layer ID. + */ + public int getLayerId() { + return layerId; + } + + /** + * Creates a copy of this object, with the given layer ID. + * + * @param layerId The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withLayer(int layerId) { + return new Layout(connectionType, controlPoints, layerId); + } + + @Override + public String toString() { + return "Layout{" + + "connectionType=" + connectionType + + ", controlPoints=" + controlPoints + + ", layerId=" + layerId + + '}'; + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/to/model/PlantModelCreationTO.java b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/PlantModelCreationTO.java new file mode 100644 index 0000000..adc0538 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/PlantModelCreationTO.java @@ -0,0 +1,598 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.to.model; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.opentcs.access.to.CreationTO; +import org.opentcs.data.model.ModelConstants; +import org.opentcs.data.model.visualization.Layer; +import org.opentcs.data.model.visualization.LayerGroup; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A transfer object describing a plant model. + */ +public class PlantModelCreationTO + extends + CreationTO + implements + Serializable { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PlantModelCreationTO.class); + + /** + * The plant model's points. + */ + private final List points; + /** + * The plant model's paths. + */ + private final List paths; + /** + * The plant model's location types. + */ + private final List locationTypes; + /** + * The plant model's locations. + */ + private final List locations; + /** + * The plant model's blocks. + */ + private final List blocks; + /** + * The plant model's vehicles. + */ + private final List vehicles; + /** + * The plant model's visual layout. + */ + private final VisualLayoutCreationTO visualLayout; + + /** + * Creates a new instance. + * + * @param name The name of this plant model. + */ + public PlantModelCreationTO(String name) { + super(name); + this.points = List.of(); + this.paths = List.of(); + this.locationTypes = List.of(); + this.locations = List.of(); + this.blocks = List.of(); + this.vehicles = List.of(); + this.visualLayout = defaultVisualLayout(); + } + + private PlantModelCreationTO( + @Nonnull + String name, + @Nonnull + Map properties, + @Nonnull + List points, + @Nonnull + List paths, + @Nonnull + List locationTypes, + @Nonnull + List locations, + @Nonnull + List blocks, + @Nonnull + List vehicles, + @Nonnull + VisualLayoutCreationTO visualLayout + ) { + super(name, properties); + this.points = requireNonNull(points, "points"); + this.paths = requireNonNull(paths, "paths"); + this.locationTypes = requireNonNull(locationTypes, "locationTypes"); + this.locations = requireNonNull(locations, "locations"); + this.blocks = requireNonNull(blocks, "blocks"); + this.vehicles = requireNonNull(vehicles, "vehicles"); + this.visualLayout = requireNonNull(visualLayout, "visualLayout"); + } + + /** + * Returns this plant model's points. + * + * @return This plant model's points. + */ + public List getPoints() { + return Collections.unmodifiableList(points); + } + + /** + * Creates a copy of this object with the given points. + * + * @param points The new points. + * @return A copy of this model, differing in the given points. + */ + public PlantModelCreationTO withPoints( + @Nonnull + List points + ) { + return new PlantModelCreationTO( + getName(), + getModifiableProperties(), + points, + paths, + locationTypes, + locations, + blocks, + vehicles, + visualLayout + ); + } + + /** + * Creates a copy of this object that includes the given point in the list of points. + * + * @param point the new point. + * @return A copy of this model that also includes the given point. + */ + public PlantModelCreationTO withPoint( + @Nonnull + PointCreationTO point + ) { + requireNonNull(point, "point"); + return new PlantModelCreationTO( + getName(), + getModifiableProperties(), + listWithAppendix(points, point), + paths, + locationTypes, + locations, + blocks, + vehicles, + visualLayout + ); + } + + /** + * Returns this plant model's paths. + * + * @return This plant model's paths. + */ + public List getPaths() { + return Collections.unmodifiableList(paths); + } + + /** + * Creates a copy of this object with the given paths. + * + * @param paths The new paths. + * @return A copy of this model, differing in the given paths. + */ + public PlantModelCreationTO withPaths( + @Nonnull + List paths + ) { + return new PlantModelCreationTO( + getName(), + getModifiableProperties(), + points, + paths, + locationTypes, + locations, + blocks, + vehicles, + visualLayout + ); + } + + /** + * Creates a copy of this object that includes the given path in the list of paths. + * + * @param path the new path. + * @return A copy of this model that also includes the given path. + */ + public PlantModelCreationTO withPath( + @Nonnull + PathCreationTO path + ) { + requireNonNull(path, "path"); + return new PlantModelCreationTO( + getName(), + getModifiableProperties(), + points, + listWithAppendix(paths, path), + locationTypes, + locations, + blocks, + vehicles, + visualLayout + ); + } + + /** + * Returns this plant model's location types. + * + * @return This plant model's location types. + */ + public List getLocationTypes() { + return Collections.unmodifiableList(locationTypes); + } + + /** + * Creates a copy of this object with the given location type. + * + * @param locationTypes The new location types. + * @return A copy of this model, differing in the given location types. + */ + public PlantModelCreationTO withLocationTypes( + @Nonnull + List locationTypes + ) { + return new PlantModelCreationTO( + getName(), + getModifiableProperties(), + points, + paths, + locationTypes, + locations, + blocks, + vehicles, + visualLayout + ); + } + + /** + * Creates a copy of this object that includes the given path in the list of location types. + * + * @param locationType the new location type. + * @return A copy of this model that also includes the given location type. + */ + public PlantModelCreationTO withLocationType( + @Nonnull + LocationTypeCreationTO locationType + ) { + requireNonNull(locationType, "locationType"); + return new PlantModelCreationTO( + getName(), + getModifiableProperties(), + points, + paths, + listWithAppendix(locationTypes, locationType), + locations, + blocks, + vehicles, + visualLayout + ); + } + + /** + * Returns this plant model's locations. + * + * @return This plant model's locations. + */ + public List getLocations() { + return Collections.unmodifiableList(locations); + } + + /** + * Creates a copy of this object with the given locations. + * + * @param locations The new locations. + * @return A copy of this model, differing in the given locations. + */ + public PlantModelCreationTO withLocations( + @Nonnull + List locations + ) { + return new PlantModelCreationTO( + getName(), + getModifiableProperties(), + points, + paths, + locationTypes, + locations, + blocks, + vehicles, + visualLayout + ); + } + + /** + * Creates a copy of this object that includes the given block in the list of locations. + * + * @param location the new location. + * @return A copy of this model that also includes the given location. + */ + public PlantModelCreationTO withLocation( + @Nonnull + LocationCreationTO location + ) { + requireNonNull(location, "location"); + return new PlantModelCreationTO( + getName(), + getModifiableProperties(), + points, + paths, + locationTypes, + listWithAppendix(locations, location), + blocks, + vehicles, + visualLayout + ); + } + + /** + * Returns this plant model's blocks. + * + * @return This plant model's blocks. + */ + public List getBlocks() { + return Collections.unmodifiableList(blocks); + } + + /** + * Creates a copy of this object with the given blocks. + * + * @param blocks The new blocks. + * @return A copy of this model, differing in the given blocks. + */ + public PlantModelCreationTO withBlocks( + @Nonnull + List blocks + ) { + return new PlantModelCreationTO( + getName(), + getModifiableProperties(), + points, + paths, + locationTypes, + locations, + blocks, + vehicles, + visualLayout + ); + } + + /** + * Creates a copy of this object that includes the given block in the list of blocks. + * + * @param block the new block. + * @return A copy of this model that also includes the given block. + */ + public PlantModelCreationTO withBlock( + @Nonnull + BlockCreationTO block + ) { + requireNonNull(block, "block"); + return new PlantModelCreationTO( + getName(), + getModifiableProperties(), + points, + paths, + locationTypes, + locations, + CreationTO.listWithAppendix(blocks, block), + vehicles, + visualLayout + ); + } + + /** + * Returns this plant model's vehicles. + * + * @return This plant model's vehicles. + */ + public List getVehicles() { + return Collections.unmodifiableList(vehicles); + } + + /** + * Creates a copy of this object with the given vehicles. + * + * @param vehicles The new vehicles. + * @return A copy of this model, differing in the given vehicles. + */ + public PlantModelCreationTO withVehicles( + @Nonnull + List vehicles + ) { + return new PlantModelCreationTO( + getName(), + getModifiableProperties(), + points, + paths, + locationTypes, + locations, + blocks, + vehicles, + visualLayout + ); + } + + /** + * Creates a copy of this object that includes the given vehicle in the list of vehicles. + * + * @param vehicle the new vehicle. + * @return A copy of this model that also includes the given vehicle. + */ + public PlantModelCreationTO withVehicle( + @Nonnull + VehicleCreationTO vehicle + ) { + requireNonNull(vehicle, "vehicle"); + return new PlantModelCreationTO( + getName(), + getModifiableProperties(), + points, + paths, + locationTypes, + locations, + blocks, + listWithAppendix(vehicles, vehicle), + visualLayout + ); + } + + /** + * Returns this plant model's visual layout. + * + * @return This plant model's visual layout. + */ + public VisualLayoutCreationTO getVisualLayout() { + return visualLayout; + } + + /** + * Creates a copy of this object with the given visual layout. + * + * @param visualLayout the new visual layout. + * @return A copy of this model with the given visual layout. + */ + public PlantModelCreationTO withVisualLayout( + @Nonnull + VisualLayoutCreationTO visualLayout + ) { + requireNonNull(visualLayout, "visualLayout"); + return new PlantModelCreationTO( + getName(), + getModifiableProperties(), + points, + paths, + locationTypes, + locations, + blocks, + vehicles, + ensureValidity(visualLayout) + ); + } + + /** + * Creates a copy of this object with the given properties. + * + * @param properties The new properties. + * @return A copy of this object, differing in the given properties. + */ + @Override + public PlantModelCreationTO withProperties( + @Nonnull + Map properties + ) { + return new PlantModelCreationTO( + getName(), + properties, + points, + paths, + locationTypes, + locations, + blocks, + vehicles, + visualLayout + ); + } + + /** + * Creates a copy of this object and adds the given property. + * If value == null, then the key-value pair is removed from the properties. + * + * @param key the key. + * @param value the value + * @return A copy of this object that either + * includes the given entry in it's current properties, if value != null or + * excludes the entry otherwise. + */ + @Override + public PlantModelCreationTO withProperty( + @Nonnull + String key, + @Nonnull + String value + ) { + return new PlantModelCreationTO( + getName(), + propertiesWith(key, value), + points, + paths, + locationTypes, + locations, + blocks, + vehicles, + visualLayout + ); + } + + @Override + public String toString() { + return "PlantModelCreationTO{" + + "name=" + getName() + + ", points=" + points + + ", paths=" + paths + + ", locationTypes=" + locationTypes + + ", locations=" + locations + + ", blocks=" + blocks + + ", vehicles=" + vehicles + + ", visualLayout=" + visualLayout + + ", properties=" + getProperties() + + '}'; + } + + private VisualLayoutCreationTO defaultVisualLayout() { + return new VisualLayoutCreationTO(ModelConstants.DEFAULT_VISUAL_LAYOUT_NAME) + .withLayer( + new Layer( + ModelConstants.DEFAULT_LAYER_ID, + ModelConstants.DEFAULT_LAYER_ORDINAL, + true, + ModelConstants.DEFAULT_LAYER_NAME, + ModelConstants.DEFAULT_LAYER_GROUP_ID + ) + ) + .withLayerGroup( + new LayerGroup( + ModelConstants.DEFAULT_LAYER_GROUP_ID, + ModelConstants.DEFAULT_LAYER_GROUP_NAME, + true + ) + ); + } + + private VisualLayoutCreationTO ensureValidity( + @Nonnull + VisualLayoutCreationTO visualLayout + ) { + VisualLayoutCreationTO vLayout = visualLayout; + + if (visualLayout.getLayers().isEmpty()) { + LOG.warn("Adding default layer to visual layout with no layers..."); + vLayout = visualLayout.withLayer( + new Layer( + ModelConstants.DEFAULT_LAYER_ID, + ModelConstants.DEFAULT_LAYER_ORDINAL, + true, + ModelConstants.DEFAULT_LAYER_NAME, + ModelConstants.DEFAULT_LAYER_GROUP_ID + ) + ); + } + + if (visualLayout.getLayerGroups().isEmpty()) { + LOG.warn("Adding default layer group to visual layout with no layer groups..."); + vLayout = vLayout.withLayerGroup( + new LayerGroup( + ModelConstants.DEFAULT_LAYER_GROUP_ID, + ModelConstants.DEFAULT_LAYER_GROUP_NAME, + true + ) + ); + } + + return vLayout; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/to/model/PointCreationTO.java b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/PointCreationTO.java new file mode 100644 index 0000000..85fe911 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/PointCreationTO.java @@ -0,0 +1,444 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.to.model; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import java.util.Map; +import org.opentcs.access.to.CreationTO; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Envelope; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Pose; +import org.opentcs.data.model.Triple; + +/** + * A transfer object describing a point in the plant model. + */ +public class PointCreationTO + extends + CreationTO + implements + Serializable { + + /** + * The pose of the vehicle at this point. + */ + private final Pose pose; + /** + * This point's type. + */ + @Nonnull + private final Point.Type type; + /** + * A map of envelope keys to envelopes that vehicles located at this point may occupy. + */ + private final Map vehicleEnvelopes; + /** + * The maximum bounding box (in mm) that a vehicle at this point is allowed to have. + */ + private final BoundingBoxCreationTO maxVehicleBoundingBox; + /** + * The information regarding the graphical representation of this point. + */ + private final Layout layout; + + /** + * Creates a new instance. + * + * @param name The name of this point. + */ + public PointCreationTO( + @Nonnull + String name + ) { + super(name); + this.pose = new Pose(new Triple(0, 0, 0), Double.NaN); + this.type = Point.Type.HALT_POSITION; + this.vehicleEnvelopes = Map.of(); + this.maxVehicleBoundingBox + = new BoundingBoxCreationTO(1000, 1000, 1000); + this.layout = new Layout(); + } + + private PointCreationTO( + @Nonnull + String name, + @Nonnull + Map properties, + @Nonnull + Pose pose, + @Nonnull + Point.Type type, + @Nonnull + Map vehicleEnvelopes, + @Nonnull + BoundingBoxCreationTO maxVehicleBoundingBox, + @Nonnull + Layout layout + ) { + super(name, properties); + this.pose = requireNonNull(pose, "pose"); + requireNonNull(pose.getPosition(), "A point requires a pose with a position."); + this.type = requireNonNull(type, "type"); + this.vehicleEnvelopes = requireNonNull(vehicleEnvelopes, "vehicleEnvelopes"); + this.maxVehicleBoundingBox = requireNonNull(maxVehicleBoundingBox, "maxVehicleBoundingBox"); + this.layout = requireNonNull(layout, "layout"); + } + + /** + * Creates a copy of this object with the given name. + * + * @param name The new name. + * @return A copy of this object, differing in the given name. + */ + @Override + public PointCreationTO withName( + @Nonnull + String name + ) { + return new PointCreationTO( + name, + getModifiableProperties(), + pose, + type, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + /** + * Returns the pose of the vehicle at this point. + * + * @return The pose of the vehicle at this point. + */ + @Nonnull + public Pose getPose() { + return pose; + } + + /** + * Creates a copy of this object with the given pose. + * + * @param pose The new pose. + * @return A copy of this object, differing in the given position. + */ + public PointCreationTO withPose( + @Nonnull + Pose pose + ) { + return new PointCreationTO( + getName(), + getModifiableProperties(), + pose, + type, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + /** + * Returns the type of this point. + * + * @return The type of this point. + */ + @Nonnull + public Point.Type getType() { + return type; + } + + /** + * Creates a copy of this object with the given type. + * + * @param type The new type. + * @return A copy of this object, differing in the given type. + */ + public PointCreationTO withType( + @Nonnull + Point.Type type + ) { + return new PointCreationTO( + getName(), + getProperties(), + pose, + type, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + /** + * Creates a copy of this object with the given properties. + * + * @param properties The new properties. + * @return A copy of this object, differing in the given properties. + */ + @Override + public PointCreationTO withProperties( + @Nonnull + Map properties + ) { + return new PointCreationTO( + getName(), + properties, + pose, + type, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + /** + * Creates a copy of this object and adds the given property. + * If value == null, then the key-value pair is removed from the properties. + * + * @param key the key. + * @param value the value + * @return A copy of this object that either + * includes the given entry in its current properties, if value != null or + * excludes the entry otherwise. + */ + @Override + public PointCreationTO withProperty( + @Nonnull + String key, + @Nonnull + String value + ) { + return new PointCreationTO( + getName(), + propertiesWith(key, value), + pose, + type, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + /** + * Returns a map of envelope keys to envelopes that vehicles located at this point may occupy. + * + * @return A map of envelope keys to envelopes that vehicles located at this point may occupy. + */ + public Map getVehicleEnvelopes() { + return vehicleEnvelopes; + } + + /** + * Creates a copy of this object, with the given vehicle envelopes. + * + * @param vehicleEnvelopes The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PointCreationTO withVehicleEnvelopes( + @Nonnull + Map vehicleEnvelopes + ) { + return new PointCreationTO( + getName(), + getModifiableProperties(), + pose, + type, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + /** + * Returns the maximum bounding box (in mm) that a vehicle at this point is allowed to have. + * + * @return The maximum bounding box (in mm) that a vehicle at this point is allowed to have. + */ + public BoundingBoxCreationTO getMaxVehicleBoundingBox() { + return maxVehicleBoundingBox; + } + + /** + * Creates a copy of this object, with the given maximum vehicle bounding box. + * + * @param maxVehicleBoundingBox The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PointCreationTO withMaxVehicleBoundingBox(BoundingBoxCreationTO maxVehicleBoundingBox) { + return new PointCreationTO( + getName(), + getModifiableProperties(), + pose, + type, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + /** + * Returns the information regarding the graphical representation of this point. + * + * @return The information regarding the graphical representation of this point. + */ + public Layout getLayout() { + return layout; + } + + /** + * Creates a copy of this object, with the given layout. + * + * @param layout The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PointCreationTO withLayout(Layout layout) { + return new PointCreationTO( + getName(), + getModifiableProperties(), + pose, + type, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + @Override + public String toString() { + return "PointCreationTO{" + + "name=" + getName() + + ", pose=" + pose + + ", type=" + type + + ", vehicleEnvelopes=" + vehicleEnvelopes + + ", layout=" + layout + + ", maxVehicleBoundingBox=" + maxVehicleBoundingBox + + ", properties=" + getProperties() + + '}'; + } + + /** + * Contains information regarding the graphical representation of a point. + */ + public static class Layout + implements + Serializable { + + /** + * The coordinates at which the point is to be drawn (in mm). + */ + private final Couple position; + /** + * The offset of the label's position to the point's position (in lu). + */ + private final Couple labelOffset; + /** + * The ID of the layer on which the point is to be drawn. + */ + private final int layerId; + + /** + * Creates a new instance. + */ + public Layout() { + this(new Couple(0, 0), new Couple(0, 0), 0); + } + + /** + * Creates a new instance. + * + * @param position The coordinates at which the point is to be drawn (in mm). + * @param labelOffset The offset of the label's position to the point's position (in lu). + * @param layerId The ID of the layer on which the point is to be drawn. + */ + public Layout( + Couple position, + Couple labelOffset, + int layerId + ) { + this.position = requireNonNull(position, "position"); + this.labelOffset = requireNonNull(labelOffset, "labelOffset"); + this.layerId = layerId; + } + + /** + * Returns the coordinates at which the point is to be drawn (in mm). + * + * @return The coordinates at which the point is to be drawn (in mm). + */ + public Couple getPosition() { + return position; + } + + /** + * Creates a copy of this object, with the given position. + * + * @param position The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withPosition(Couple position) { + return new Layout( + position, + labelOffset, + layerId + ); + } + + /** + * Returns the offset of the label's position to the point's position (in lu). + * + * @return The offset of the label's position to the point's position (in lu). + */ + public Couple getLabelOffset() { + return labelOffset; + } + + /** + * Creates a copy of this object, with the given X label offset. + * + * @param labelOffset The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withLabelOffset(Couple labelOffset) { + return new Layout( + position, + labelOffset, + layerId + ); + } + + /** + * Returns the ID of the layer on which the point is to be drawn. + * + * @return The layer ID. + */ + public int getLayerId() { + return layerId; + } + + /** + * Creates a copy of this object, with the given layer ID. + * + * @param layerId The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withLayerId(int layerId) { + return new Layout( + position, + labelOffset, + layerId + ); + } + + @Override + public String toString() { + return "Layout{" + + "position=" + position + + ", labelOffset=" + labelOffset + + ", layerId=" + layerId + + '}'; + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/to/model/VehicleCreationTO.java b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/VehicleCreationTO.java new file mode 100644 index 0000000..477a65c --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/VehicleCreationTO.java @@ -0,0 +1,729 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.to.model; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkInRange; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.awt.Color; +import java.io.Serializable; +import java.util.Map; +import org.opentcs.access.to.CreationTO; +import org.opentcs.util.annotations.ScheduledApiChange; + +/** + * A transfer object describing a block in the plant model. + */ +public class VehicleCreationTO + extends + CreationTO + implements + Serializable { + + /** + * The vehicle's bounding box (in mm). + */ + private final BoundingBoxCreationTO boundingBox; + /** + * Contains information regarding the energy level threshold values of the vehicle. + */ + private final EnergyLevelThresholdSet energyLevelThresholdSet; + /** + * The vehicle's maximum velocity (in mm/s). + */ + private final int maxVelocity; + /** + * The vehicle's maximum reverse velocity (in mm/s). + */ + private final int maxReverseVelocity; + /** + * The key for selecting the envelope to be used for resources the vehicle occupies. + */ + private final String envelopeKey; + /** + * The information regarding the graphical representation of this vehicle. + */ + private final Layout layout; + + /** + * Creates a new instance. + * + * @param name The name of this vehicle. + */ + public VehicleCreationTO( + @Nonnull + String name + ) { + super(name); + this.boundingBox = new BoundingBoxCreationTO(1000, 1000, 1000); + this.energyLevelThresholdSet = new EnergyLevelThresholdSet(30, 90, 30, 90); + this.maxVelocity = 1000; + this.maxReverseVelocity = 1000; + this.envelopeKey = null; + this.layout = new Layout(); + } + + private VehicleCreationTO( + @Nonnull + String name, + @Nonnull + Map properties, + @Nonnull + BoundingBoxCreationTO boundingBox, + @Nonnull + EnergyLevelThresholdSet energyLevelThresholdSet, + int maxVelocity, + int maxReverseVelocity, + @Nullable + String envelopeKey, + @Nonnull + Layout layout + ) { + super(name, properties); + this.boundingBox = requireNonNull(boundingBox, "boundingBox"); + this.energyLevelThresholdSet + = requireNonNull(energyLevelThresholdSet, "energyLevelThresholdSet"); + this.maxVelocity = maxVelocity; + this.maxReverseVelocity = maxReverseVelocity; + this.envelopeKey = envelopeKey; + this.layout = requireNonNull(layout, "layout"); + } + + /** + * Creates a copy of this object with the given name. + * + * @param name The new instance. + * @return A copy of this object, differing in the given name. + */ + @Override + public VehicleCreationTO withName( + @Nonnull + String name + ) { + return new VehicleCreationTO( + name, + getModifiableProperties(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + envelopeKey, + layout + ); + } + + /** + * Creates a copy of this object with the given properties. + * + * @param properties The new properties. + * @return A copy of this object, differing in the given properties. + */ + @Override + public VehicleCreationTO withProperties( + @Nonnull + Map properties + ) { + return new VehicleCreationTO( + getName(), + properties, + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + envelopeKey, + layout + ); + } + + /** + * Creates a copy of this object and adds the given property. + * If value == null, then the key-value pair is removed from the properties. + * + * @param key the key. + * @param value the value + * @return A copy of this object that either + * includes the given entry in its current properties, if value != null or + * excludes the entry otherwise. + */ + @Override + public VehicleCreationTO withProperty( + @Nonnull + String key, + @Nonnull + String value + ) { + return new VehicleCreationTO( + getName(), + propertiesWith(key, value), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + envelopeKey, + layout + ); + } + + /** + * Returns the vehicle's current bounding box (in mm). + * + * @return The vehicle's current bounding box (in mm). + */ + public BoundingBoxCreationTO getBoundingBox() { + return boundingBox; + } + + /** + * Creates a copy of this object, with the given bounding box (in mm). + * + * @param boundingBox The new bounding box. + * @return A copy of this object, differing in the given vehicle bounding box. + */ + public VehicleCreationTO withBoundingBox(BoundingBoxCreationTO boundingBox) { + return new VehicleCreationTO( + getName(), + getModifiableProperties(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + envelopeKey, + layout + ); + } + + /** + * Returns the vehicle's length (in mm). + * + * @return The vehicle's length (in mm). + * @deprecated Use {@link #getBoundingBox()} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public int getLength() { + return (int) boundingBox.getLength(); + } + + /** + * Creates a copy of this object with the vehicle's given length (in mm). + * + * @param length The new length. Must be at least 1. + * @return A copy of this object, differing in the given vehicle length. + * @deprecated Use {@link #withBoundingBox(BoundingBoxCreationTO)} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public VehicleCreationTO withLength(int length) { + return withBoundingBox(boundingBox.withLength(length)); + } + + /** + * Returns this vehicle's critical energy level (in percent of the maximum). + * The critical energy level is the one at/below which the vehicle should be recharged. + * + * @return This vehicle's critical energy level. + * @deprecated Use {@link #getEnergyLevelThresholdSet()} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public int getEnergyLevelCritical() { + return energyLevelThresholdSet.getEnergyLevelCritical(); + } + + /** + * Creates a copy of this object with the given critical energy level. + * The critical energy level is the one at/below which the vehicle should be recharged. + * + * @param energyLevelCritical The new critical energy level. Must not be smaller than 0 or + * greater than 100. + * @return A copy of this object, differing in the given value. + * @deprecated Use {@link #withEnergyLevelThresholdSet(EnergyLevelThresholdSet)} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public VehicleCreationTO withEnergyLevelCritical(int energyLevelCritical) { + return withEnergyLevelThresholdSet( + getEnergyLevelThresholdSet().withEnergyLevelCritical(energyLevelCritical) + ); + } + + /** + * Returns this vehicle's good energy level (in percent of the maximum). + * The good energy level is the one at/above which the vehicle can be dispatched again when + * charging. + * + * @return This vehicle's good energy level. + * @deprecated Use {@link #getEnergyLevelThresholdSet()} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public int getEnergyLevelGood() { + return energyLevelThresholdSet.getEnergyLevelGood(); + } + + /** + * Creates a copy of this object with the vehicle's good energy level (in percent of the maximum). + * The good energy level is the one at/above which the vehicle can be dispatched again when + * charging. + * + * @param energyLevelGood The new good energy level. Must not be smaller than 0 or greater than + * 100. + * @return A copy of this object, differing in the given value. + * @deprecated Use {@link #withEnergyLevelThresholdSet(EnergyLevelThresholdSet)} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public VehicleCreationTO withEnergyLevelGood(int energyLevelGood) { + return withEnergyLevelThresholdSet( + getEnergyLevelThresholdSet().withEnergyLevelGood(energyLevelGood) + ); + } + + /** + * Returns this vehicle's fully recharged energy level (in percent of the maximum). + * + * @return This vehicle's fully recharged energy level. + * @deprecated Use {@link #getEnergyLevelThresholdSet()} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public int getEnergyLevelFullyRecharged() { + return energyLevelThresholdSet.getEnergyLevelFullyRecharged(); + } + + /** + * Creates a copy of this object with the vehicle's fully recharged energy level (in percent of + * the maximum). + * + * @param energyLevelFullyRecharged The new fully recharged energy level. + * Must not be smaller than 0 or greater than 100. + * @return A copy of this object, differing in the given value. + * @deprecated Use {@link #withEnergyLevelThresholdSet(EnergyLevelThresholdSet)} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public VehicleCreationTO withEnergyLevelFullyRecharged(int energyLevelFullyRecharged) { + return withEnergyLevelThresholdSet( + getEnergyLevelThresholdSet().withEnergyLevelFullyRecharged(energyLevelFullyRecharged) + ); + } + + /** + * Returns this vehicle's sufficiently recharged energy level (in percent of the maximum). + * + * @return This vehicle's sufficiently recharged energy level. + * @deprecated Use {@link #getEnergyLevelThresholdSet()} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public int getEnergyLevelSufficientlyRecharged() { + return energyLevelThresholdSet.getEnergyLevelSufficientlyRecharged(); + } + + /** + * Creates a copy of this object with the vehicle's sufficiently recharged energy level (in + * percent of the maximum). + * + * @param energyLevelSufficientlyRecharged The new sufficiently recharged energy level. + * Must not be smaller than 0 or greater than 100. + * @return A copy of this object, differing in the given value. + * @deprecated Use {@link #withEnergyLevelThresholdSet(EnergyLevelThresholdSet)} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public VehicleCreationTO withEnergyLevelSufficientlyRecharged( + int energyLevelSufficientlyRecharged + ) { + return withEnergyLevelThresholdSet( + getEnergyLevelThresholdSet() + .withEnergyLevelSufficientlyRecharged(energyLevelSufficientlyRecharged) + ); + } + + /** + * Returns this vehicle's energy level threshold set. + * + * @return This vehicle's energy level threshold set. + */ + @Nonnull + public EnergyLevelThresholdSet getEnergyLevelThresholdSet() { + return energyLevelThresholdSet; + } + + /** + * Creates a copy of this object, with the given EnergyLevelThresholdSet. + * + * @param energyLevelThresholdSet The new EnergyLevelThresholdSet. + * @return A copy of this object, differing in the given value. + */ + public VehicleCreationTO withEnergyLevelThresholdSet( + @Nonnull + EnergyLevelThresholdSet energyLevelThresholdSet + ) { + return new VehicleCreationTO( + getName(), + getModifiableProperties(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + envelopeKey, + layout + ); + } + + public int getMaxVelocity() { + return maxVelocity; + } + + /** + * Creates a copy of this object with the given maximum velocity (in mm/s). + * + * @param maxVelocity the new max velocity. + * @return A copy of this object, differing in the given value. + */ + public VehicleCreationTO withMaxVelocity(int maxVelocity) { + checkInRange(maxVelocity, 0, Integer.MAX_VALUE); + return new VehicleCreationTO( + getName(), + getModifiableProperties(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + envelopeKey, + layout + ); + } + + public int getMaxReverseVelocity() { + return maxReverseVelocity; + } + + /** + * Creates a copy of this object with the given maximum reverse velocity (in mm/s). + * + * @param maxReverseVelocity the new maximum reverse velocity. + * @return A copy of this object, differing in the given value. + */ + public VehicleCreationTO withMaxReverseVelocity(int maxReverseVelocity) { + checkInRange(maxReverseVelocity, 0, Integer.MAX_VALUE); + return new VehicleCreationTO( + getName(), + getModifiableProperties(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + envelopeKey, + layout + ); + } + + /** + * Returns the key for selecting the envelope to be used for resources the vehicle occupies. + * + * @return The key for selecting the envelope to be used for resources the vehicle occupies. + */ + @ScheduledApiChange(when = "7.0", details = "Envelope key will become non-null.") + @Nullable + public String getEnvelopeKey() { + return envelopeKey; + } + + /** + * + * Creates a copy of this object, with the given envelope key. + * + * @param envelopeKey The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + @ScheduledApiChange(when = "7.0", details = "Envelope key will become non-null.") + public VehicleCreationTO withEnvelopeKey( + @Nullable + String envelopeKey + ) { + return new VehicleCreationTO( + getName(), + getModifiableProperties(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + envelopeKey, + layout + ); + } + + /** + * Returns the information regarding the graphical representation of this vehicle. + * + * @return The information regarding the graphical representation of this vehicle. + */ + public Layout getLayout() { + return layout; + } + + /** + * Creates a copy of this object, with the given layout. + * + * @param layout The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public VehicleCreationTO withLayout(Layout layout) { + return new VehicleCreationTO( + getName(), + getModifiableProperties(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + envelopeKey, + layout + ); + } + + @Override + public String toString() { + return "VehicleCreationTO{" + + "name=" + getName() + + ", boundingBox=" + boundingBox + + ", energyLevelThresholdSet=" + energyLevelThresholdSet + + ", maxVelocity=" + maxVelocity + + ", maxReverseVelocity=" + maxReverseVelocity + + ", envelopeKey=" + envelopeKey + + ", layout=" + layout + + ", properties=" + getProperties() + + '}'; + } + + /** + * Contains information regarding the graphical representation of a vehicle. + */ + public static class Layout + implements + Serializable { + + /** + * The color in which vehicle routes are to be emphasized. + */ + private final Color routeColor; + + /** + * Creates a new instance. + */ + public Layout() { + this(Color.RED); + } + + /** + * Creates a new instance. + * + * @param routeColor The color in which vehicle routes are to be emphasized. + */ + public Layout(Color routeColor) { + this.routeColor = requireNonNull(routeColor, "routeColor"); + } + + /** + * Returns the color in which vehicle routes are to be emphasized. + * + * @return The color in which vehicle routes are to be emphasized. + */ + public Color getRouteColor() { + return routeColor; + } + + /** + * Creates a copy of this object, with the given color. + * + * @param routeColor The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withRouteColor(Color routeColor) { + return new Layout(routeColor); + } + + @Override + public String toString() { + return "Layout{" + + "routeColor=" + routeColor + + '}'; + } + } + + /** + * Contains information regarding the energy level threshold values of a vehicle. + */ + public static class EnergyLevelThresholdSet + implements + Serializable { + + private final int energyLevelCritical; + private final int energyLevelGood; + private final int energyLevelSufficientlyRecharged; + private final int energyLevelFullyRecharged; + + /** + * Creates a new instance. + * + * @param energyLevelCritical The value at/below which the vehicle's energy level is considered + * "critical". + * @param energyLevelGood The value at/above which the vehicle's energy level is considered + * "good". + * @param energyLevelSufficientlyRecharged The value at/above which the vehicle's energy level + * is considered fully recharged. + * @param energyLevelFullyRecharged The value at/above which the vehicle's energy level is + * considered sufficiently recharged. + */ + public EnergyLevelThresholdSet( + int energyLevelCritical, + int energyLevelGood, + int energyLevelSufficientlyRecharged, + int energyLevelFullyRecharged + ) { + this.energyLevelCritical = checkInRange( + energyLevelCritical, + 0, + 100, + "energyLevelCritical" + ); + this.energyLevelGood = checkInRange( + energyLevelGood, + 0, + 100, + "energyLevelGood" + ); + this.energyLevelSufficientlyRecharged = checkInRange( + energyLevelSufficientlyRecharged, + 0, + 100, + "energyLevelSufficientlyRecharged" + ); + this.energyLevelFullyRecharged = checkInRange( + energyLevelFullyRecharged, + 0, + 100, + "energyLevelFullyRecharged" + ); + } + + /** + * Returns the vehicle's critical energy level (in percent of the maximum). + *

+ * The critical energy level is the one at/below which the vehicle should be recharged. + *

+ * + * @return The vehicle's critical energy level. + */ + public int getEnergyLevelCritical() { + return energyLevelCritical; + } + + /** + * Creates a copy of this object, with the given critical energy level. + * + * @param energyLevelCritical The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public EnergyLevelThresholdSet withEnergyLevelCritical(int energyLevelCritical) { + return new EnergyLevelThresholdSet( + energyLevelCritical, + energyLevelGood, + energyLevelSufficientlyRecharged, + energyLevelFullyRecharged + ); + } + + /** + * Returns the vehicle's good energy level (in percent of the maximum). + *

+ * The good energy level is the one at/above which the vehicle can be dispatched again when + * charging. + *

+ * + * @return The vehicle's good energy level. + */ + public int getEnergyLevelGood() { + return energyLevelGood; + } + + /** + * Creates a copy of this object, with the given good energy level. + * + * @param energyLevelGood The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public EnergyLevelThresholdSet withEnergyLevelGood(int energyLevelGood) { + return new EnergyLevelThresholdSet( + energyLevelCritical, + energyLevelGood, + energyLevelSufficientlyRecharged, + energyLevelFullyRecharged + ); + } + + /** + * Returns the vehicle's energy level for being sufficiently recharged (in percent of the + * maximum). + * + * @return This vehicle's sufficiently recharged energy level. + */ + public int getEnergyLevelSufficientlyRecharged() { + return energyLevelSufficientlyRecharged; + } + + /** + * Creates a copy of this object, with the given sufficiently recharged energy level. + * + * @param energyLevelSufficientlyRecharged The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public EnergyLevelThresholdSet withEnergyLevelSufficientlyRecharged( + int energyLevelSufficientlyRecharged + ) { + return new EnergyLevelThresholdSet( + energyLevelCritical, + energyLevelGood, + energyLevelSufficientlyRecharged, + energyLevelFullyRecharged + ); + } + + /** + * Returns the vehicle's energy level for being fully recharged (in percent of the maximum). + * + * @return The vehicle's fully recharged threshold. + */ + public int getEnergyLevelFullyRecharged() { + return energyLevelFullyRecharged; + } + + /** + * Creates a copy of this object, with the given fully recharged energy level. + * + * @param energyLevelFullyRecharged The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public EnergyLevelThresholdSet withEnergyLevelFullyRecharged(int energyLevelFullyRecharged) { + return new EnergyLevelThresholdSet( + energyLevelCritical, + energyLevelGood, + energyLevelSufficientlyRecharged, + energyLevelFullyRecharged + ); + } + + @Override + public String toString() { + return "EnergyLevelThresholdSet{" + + "energyLevelCritical=" + energyLevelCritical + + ", energyLevelGood=" + energyLevelGood + + ", energyLevelSufficientlyRecharged=" + energyLevelSufficientlyRecharged + + ", energyLevelFullyRecharged=" + energyLevelFullyRecharged + + '}'; + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/to/model/VisualLayoutCreationTO.java b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/VisualLayoutCreationTO.java new file mode 100644 index 0000000..67b05a6 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/VisualLayoutCreationTO.java @@ -0,0 +1,311 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.to.model; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.opentcs.access.to.CreationTO; +import org.opentcs.data.model.visualization.Layer; +import org.opentcs.data.model.visualization.LayerGroup; + +/** + * A transfer object describing a visual layout in the plant model. + */ +public class VisualLayoutCreationTO + extends + CreationTO + implements + Serializable { + + /** + * This layout's scale on the X axis (in mm/pixel). + */ + private final double scaleX; + /** + * This layout's scale on the Y axis (in mm/pixel). + */ + private final double scaleY; + /** + * This layout's layers. + */ + private final List layers; + /** + * The layout's layer groups. + */ + private final List layerGroups; + + /** + * Creates a new instance. + * + * @param name The name of this visual layout. + */ + public VisualLayoutCreationTO( + @Nonnull + String name + ) { + super(name); + + this.scaleX = 50.0; + this.scaleY = 50.0; + this.layers = List.of(); + this.layerGroups = List.of(); + } + + private VisualLayoutCreationTO( + @Nonnull + String name, + @Nonnull + Map properties, + double scaleX, + double scaleY, + @Nonnull + List layers, + @Nonnull + List layerGroups + ) { + super(name, properties); + this.scaleX = scaleX; + this.scaleY = scaleY; + this.layers = requireNonNull(layers, "layers"); + this.layerGroups = requireNonNull(layerGroups, "layerGroups"); + } + + /** + * Creates a copy of this object with the given name. + * + * @param name the new name of the instance. + * @return A copy of this object, differing in the given value. + */ + @Override + public VisualLayoutCreationTO withName( + @Nonnull + String name + ) { + return new VisualLayoutCreationTO( + name, + getModifiableProperties(), + scaleX, + scaleY, + layers, + layerGroups + ); + } + + /** + * Creates a copy of this object with the given properties. + * + * @param properties The new properties. + * @return A copy of this object, differing in the given value. + */ + @Override + public VisualLayoutCreationTO withProperties( + @Nonnull + Map properties + ) { + return new VisualLayoutCreationTO( + getName(), + properties, + scaleX, + scaleY, + layers, + layerGroups + ); + } + + /** + * Creates a copy of this object and adds the given property. + * If value == null, then the key-value pair is removed from the properties. + * + * @param key the key. + * @param value the value + * @return A copy of this object that either + * includes the given entry in it's current properties, if value != null or + * excludes the entry otherwise. + */ + @Override + public VisualLayoutCreationTO withProperty( + @Nonnull + String key, + @Nonnull + String value + ) { + return new VisualLayoutCreationTO( + getName(), + propertiesWith(key, value), + scaleX, + scaleY, + layers, + layerGroups + ); + } + + /** + * Returns this layout's scale on the X axis (in mm/pixel). + * + * @return This layout's scale on the X axis. + */ + public double getScaleX() { + return scaleX; + } + + /** + * Creates a copy of this object with the layout's scale on the X axis (in mm/pixel). + * + * @param scaleX The new scale. + * @return A copy of this object, differing in the given value. + */ + public VisualLayoutCreationTO withScaleX(double scaleX) { + return new VisualLayoutCreationTO( + getName(), + getModifiableProperties(), + scaleX, + scaleY, + layers, + layerGroups + ); + } + + /** + * Returns this layout's scale on the Y axis (in mm/pixel). + * + * @return This layout's scale on the Y axis. + */ + public double getScaleY() { + return scaleY; + } + + /** + * Creates a copy of this object with the given layout's scale on the Y axis (in mm/pixel). + * + * @param scaleY The new scale. + * @return A copy of this object, differing in the given value. + */ + public VisualLayoutCreationTO withScaleY(double scaleY) { + return new VisualLayoutCreationTO( + getName(), + getModifiableProperties(), + scaleX, + scaleY, + layers, + layerGroups + ); + } + + /** + * Returns the layers of this visual layout. + * + * @return The layers of this visual layout. + */ + @Nonnull + public List getLayers() { + return Collections.unmodifiableList(layers); + } + + /** + * Creates a copy of this object, with the given layers. + * + * @param layers The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public VisualLayoutCreationTO withLayers( + @Nonnull + List layers + ) { + return new VisualLayoutCreationTO( + getName(), + getModifiableProperties(), + scaleX, + scaleY, + layers, + layerGroups + ); + } + + /** + * Creates a copy of this object, with the given layer. + * + * @param layer The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public VisualLayoutCreationTO withLayer( + @Nonnull + Layer layer + ) { + return new VisualLayoutCreationTO( + getName(), + getModifiableProperties(), + scaleX, + scaleY, + listWithAppendix(layers, layer), + layerGroups + ); + } + + /** + * Returns the layer groups of this visual layout. + * + * @return The layer groups of this visual layout. + */ + @Nonnull + public List getLayerGroups() { + return Collections.unmodifiableList(layerGroups); + } + + /** + * Creates a copy of this object, with the given layer groups. + * + * @param layerGroups The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public VisualLayoutCreationTO withLayerGroups( + @Nonnull + List layerGroups + ) { + return new VisualLayoutCreationTO( + getName(), + getModifiableProperties(), + scaleX, + scaleY, + layers, + layerGroups + ); + } + + /** + * Creates a copy of this object, with the given layer group. + * + * @param layerGroup The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public VisualLayoutCreationTO withLayerGroup( + @Nonnull + LayerGroup layerGroup + ) { + return new VisualLayoutCreationTO( + getName(), + getModifiableProperties(), + scaleX, + scaleY, + layers, + listWithAppendix(layerGroups, layerGroup) + ); + } + + @Override + public String toString() { + return "VisualLayoutCreationTO{" + + "name=" + getName() + + ", scaleX=" + scaleX + + ", scaleY=" + scaleY + + ", layers=" + layers + + ", layerGroups=" + layerGroups + + ", properties=" + getProperties() + + '}'; + } + +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/to/model/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/package-info.java new file mode 100644 index 0000000..b070f69 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/to/model/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Transfer object classes for plant model objects. + */ +package org.opentcs.access.to.model; diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/to/order/DestinationCreationTO.java b/opentcs-api-base/src/main/java/org/opentcs/access/to/order/DestinationCreationTO.java new file mode 100644 index 0000000..f6c0d84 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/to/order/DestinationCreationTO.java @@ -0,0 +1,177 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.to.order; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import java.util.Map; +import org.opentcs.access.to.CreationTO; + +/** + * A transfer object describing a destination of a drive order. + */ +public class DestinationCreationTO + extends + CreationTO + implements + Serializable { + + /** + * The name of the destination location (or point). + */ + @Nonnull + private final String destLocationName; + /** + * The operation to be performed at the destination. + */ + @Nonnull + private final String destOperation; + + /** + * Creates a new instance. + * + * @param destLocationName The name of the destination location (or destination point). + * @param destOperation The operation to be performed at the destination. + */ + public DestinationCreationTO( + @Nonnull + String destLocationName, + @Nonnull + String destOperation + ) { + super(""); + this.destLocationName = requireNonNull(destLocationName, "destLocationName"); + this.destOperation = requireNonNull(destOperation, "destOperation"); + } + + private DestinationCreationTO( + @Nonnull + String destLocationName, + @Nonnull + String destOperation, + @Nonnull + String name, + @Nonnull + Map properties + ) { + super(name, properties); + this.destLocationName = requireNonNull(destLocationName, "destLocationName"); + this.destOperation = requireNonNull(destOperation, "destOperation"); + } + + /** + * Creates a copy of this object with the given name. + * + * @param name the new name of the instance. + * @return A copy of this object, differing in the given value. + */ + @Override + public DestinationCreationTO withName( + @Nonnull + String name + ) { + return new DestinationCreationTO( + destLocationName, + destOperation, + name, + getModifiableProperties() + ); + } + + /** + * Creates a copy of this object with the given properties. + * + * @param properties The new properties. + * @return A copy of this object, differing in the given value. + */ + @Override + public DestinationCreationTO withProperties( + @Nonnull + Map properties + ) { + return new DestinationCreationTO(destLocationName, destOperation, getName(), properties); + } + + /** + * Creates a copy of this object and adds the given property. + * If value == null, then the key-value pair is removed from the properties. + * + * @param key the key. + * @param value the value + * @return A copy of this object that either + * includes the given entry in it's current properties, if value != null or + * excludes the entry otherwise. + */ + @Override + public DestinationCreationTO withProperty( + @Nonnull + String key, + @Nonnull + String value + ) { + return new DestinationCreationTO( + destLocationName, + destOperation, + getName(), + propertiesWith(key, value) + ); + } + + /** + * Returns the destination location (or point) name. + * + * @return The destination location (or point) name. + */ + @Nonnull + public String getDestLocationName() { + return destLocationName; + } + + /** + * Creates a copy of this object with the given destination location (or point) name. + * + * @param desLocationName The destination location (or point) name. + * @return A copy of this object, differing in the given destination. + */ + public DestinationCreationTO withDestLocationName( + @Nonnull + String desLocationName + ) { + return new DestinationCreationTO( + destLocationName, + destOperation, + getName(), + getModifiableProperties() + ); + } + + /** + * Returns the operation to be performed at the destination. + * + * @return The operation to be performed at the destination. + */ + @Nonnull + public String getDestOperation() { + return destOperation; + } + + /** + * Creates a copy of this object with the given operation to be performed at the destination. + * + * @param destOperation The operation. + * @return A copy of this object, differing in the given destination operation. + */ + public DestinationCreationTO withDestOperation( + @Nonnull + String destOperation + ) { + return new DestinationCreationTO( + destLocationName, + destOperation, + getName(), + getModifiableProperties() + ); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/to/order/OrderSequenceCreationTO.java b/opentcs-api-base/src/main/java/org/opentcs/access/to/order/OrderSequenceCreationTO.java new file mode 100644 index 0000000..80c8fb6 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/to/order/OrderSequenceCreationTO.java @@ -0,0 +1,266 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.to.order; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.io.Serializable; +import java.util.Map; +import org.opentcs.access.to.CreationTO; +import org.opentcs.data.order.OrderConstants; + +/** + * A transfer object describing a transport order. + */ +public class OrderSequenceCreationTO + extends + CreationTO + implements + Serializable { + + /** + * Indicates whether the name is incomplete and requires to be completed when creating the actual + * order sequence. + * (How exactly this is done is decided by the kernel.) + */ + private final boolean incompleteName; + /** + * The type of the order sequence. + */ + private final String type; + /** + * The (optional) name of the vehicle that is supposed to execute the transport order. + */ + @Nullable + private final String intendedVehicleName; + /** + * Whether failure of one transport order in the sequence makes subsequent ones fail, too. + */ + private final boolean failureFatal; + + /** + * Creates a new instance. + * + * @param name The name of this transport order. + */ + public OrderSequenceCreationTO( + @Nonnull + String name + ) { + super(name); + this.incompleteName = false; + this.type = OrderConstants.TYPE_NONE; + this.intendedVehicleName = null; + this.failureFatal = false; + } + + private OrderSequenceCreationTO( + @Nonnull + String name, + @Nonnull + Map properties, + boolean incompleteName, + @Nonnull + String type, + @Nullable + String intendedVehicleName, + boolean failureFatal + ) { + super(name, properties); + this.incompleteName = incompleteName; + this.type = requireNonNull(type, "type"); + this.intendedVehicleName = intendedVehicleName; + this.failureFatal = failureFatal; + } + + /** + * Creates a copy of this object with the given name. + * + * @param name the new name of the instance. + * @return A copy of this object, differing in the given name. + */ + @Override + public OrderSequenceCreationTO withName( + @Nonnull + String name + ) { + return new OrderSequenceCreationTO( + name, + getModifiableProperties(), + incompleteName, + type, + intendedVehicleName, + failureFatal + ); + } + + /** + * Creates a copy of this object with the given properties. + * + * @param properties The new properties. + * @return A copy of this object, differing in the given value. + */ + @Override + public OrderSequenceCreationTO withProperties( + @Nonnull + Map properties + ) { + return new OrderSequenceCreationTO( + getName(), + properties, + incompleteName, + type, + intendedVehicleName, + failureFatal + ); + } + + /** + * Creates a copy of this object and adds the given property. + * If value == null, then the key-value pair is removed from the properties. + * + * @param key the key. + * @param value the value + * @return A copy of this object that either + * includes the given entry in it's current properties, if value != null or + * excludes the entry otherwise. + */ + @Override + public OrderSequenceCreationTO withProperty( + @Nonnull + String key, + @Nonnull + String value + ) { + return new OrderSequenceCreationTO( + getName(), + propertiesWith(key, value), + incompleteName, + type, + intendedVehicleName, + failureFatal + ); + } + + /** + * Indicates whether the name is incomplete and requires to be completed when creating the actual + * order sequence. + * (How exactly this is done is decided by the kernel.) + * + * @return true if, and only if, the name is incomplete and requires to be completed + * by the kernel. + */ + public boolean hasIncompleteName() { + return incompleteName; + } + + /** + * Creates a copy of this object with the given nameIncomplete flag. + * + * @param incompleteName Whether the name is incomplete and requires to be completed when creating + * the actual order sequence. + * + * @return A copy of this object, differing in the given value. + */ + public OrderSequenceCreationTO withIncompleteName(boolean incompleteName) { + return new OrderSequenceCreationTO( + getName(), + getModifiableProperties(), + incompleteName, + type, + intendedVehicleName, + failureFatal + ); + } + + /** + * Returns the (optional) type of the order sequence. + * + * @return The (optional) type of the order sequence. + */ + @Nonnull + public String getType() { + return type; + } + + /** + * Creates a copy of this object with the given type. + * + * @param type The type. + * @return A copy of this object, differing in the given type. + */ + public OrderSequenceCreationTO withType( + @Nonnull + String type + ) { + return new OrderSequenceCreationTO( + getName(), + getModifiableProperties(), + incompleteName, + type, + intendedVehicleName, + failureFatal + ); + } + + /** + * Returns the (optional) name of the vehicle that is supposed to execute the transport order. + * + * @return The (optional) name of the vehicle that is supposed to execute the transport order. + */ + @Nullable + public String getIntendedVehicleName() { + return intendedVehicleName; + } + + /** + * Creates a copy of this object with the given + * (optional) name of the vehicle that is supposed to execute the transport order. + * + * @param intendedVehicleName The vehicle name. + * @return A copy of this object, differing in the given name of the intended vehicle. + */ + public OrderSequenceCreationTO withIntendedVehicleName( + @Nullable + String intendedVehicleName + ) { + return new OrderSequenceCreationTO( + getName(), + getModifiableProperties(), + incompleteName, + type, + intendedVehicleName, + failureFatal + ); + } + + /** + * Returns whether failure of one transport order in the sequence makes subsequent ones fail, too. + * + * @return Whether failure of one transport order in the sequence makes subsequent ones fail, too. + */ + public boolean isFailureFatal() { + return failureFatal; + } + + /** + * Creates a copy of this object with the given failureFatal flag. + * + * @param failureFatal Whether failure of one transport order in the sequence makes subsequent + * ones fail, too. + * + * @return A copy of this object, differing in the given value. + */ + public OrderSequenceCreationTO withFailureFatal(boolean failureFatal) { + return new OrderSequenceCreationTO( + getName(), + getModifiableProperties(), + incompleteName, + type, + intendedVehicleName, + failureFatal + ); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/to/order/TransportOrderCreationTO.java b/opentcs-api-base/src/main/java/org/opentcs/access/to/order/TransportOrderCreationTO.java new file mode 100644 index 0000000..1ef278f --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/to/order/TransportOrderCreationTO.java @@ -0,0 +1,533 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.to.order; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.io.Serializable; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.opentcs.access.to.CreationTO; +import org.opentcs.data.order.OrderConstants; + +/** + * A transfer object describing a transport order. + */ +public class TransportOrderCreationTO + extends + CreationTO + implements + Serializable { + + /** + * Indicates whether the name is incomplete and requires to be completed when creating the actual + * transport order. + * (How exactly this is done is decided by the kernel.) + */ + private final boolean incompleteName; + /** + * The destinations that need to be travelled to. + */ + @Nonnull + private final List destinations; + /** + * An optional token for reserving peripheral devices while processing this transport order. + */ + @Nullable + private final String peripheralReservationToken; + /** + * The (optional) name of the order sequence the transport order belongs to. + */ + @Nullable + private final String wrappingSequence; + /** + * The (optional) names of transport orders the transport order depends on. + */ + @Nonnull + private final Set dependencyNames; + /** + * The (optional) name of the vehicle that is supposed to execute the transport order. + */ + @Nullable + private final String intendedVehicleName; + /** + * The type of the transport order. + */ + @Nonnull + private final String type; + /** + * The point of time at which execution of the transport order is supposed to be finished. + */ + @Nonnull + private final Instant deadline; + /** + * Whether the transport order is dispensable or not. + */ + private final boolean dispensable; + + /** + * Creates a new instance. + * + * @param name The name of this transport order. + * @param destinations The destinations that need to be travelled to. + */ + public TransportOrderCreationTO( + @Nonnull + String name, + @Nonnull + List destinations + ) { + super(name); + this.incompleteName = false; + this.destinations = requireNonNull(destinations, "destinations"); + this.peripheralReservationToken = null; + this.wrappingSequence = null; + this.dependencyNames = Set.of(); + this.intendedVehicleName = null; + this.type = OrderConstants.TYPE_NONE; + this.deadline = Instant.MAX; + this.dispensable = false; + } + + private TransportOrderCreationTO( + @Nonnull + String name, + @Nonnull + Map properties, + boolean incompleteName, + @Nonnull + List destinations, + @Nullable + String peripheralReservationToken, + @Nullable + String wrappingSequence, + @Nonnull + Set dependencyNames, + @Nullable + String intendedVehicleName, + @Nonnull + String type, + @Nonnull + Instant deadline, + boolean dispensable + ) { + super(name, properties); + this.incompleteName = incompleteName; + this.destinations = requireNonNull(destinations, "destinations"); + this.peripheralReservationToken = peripheralReservationToken; + this.wrappingSequence = wrappingSequence; + this.dependencyNames = requireNonNull(dependencyNames, "dependencyNames"); + this.intendedVehicleName = intendedVehicleName; + this.type = requireNonNull(type, "type"); + this.deadline = requireNonNull(deadline, "deadline"); + this.dispensable = dispensable; + } + + /** + * Creates a copy of this object with the given name. + * + * @param name The new name of the instance. + * @return A copy of this object, differing in the given name. + */ + @Override + public TransportOrderCreationTO withName( + @Nonnull + String name + ) { + return new TransportOrderCreationTO( + name, + getModifiableProperties(), + incompleteName, + destinations, + peripheralReservationToken, + wrappingSequence, + dependencyNames, + intendedVehicleName, + type, + deadline, + dispensable + ); + } + + /** + * Creates a copy of this object with the given properties. + * + * @param properties The new properties. + * @return A copy of this object, differing in the given value. + */ + @Override + public TransportOrderCreationTO withProperties( + @Nonnull + Map properties + ) { + return new TransportOrderCreationTO( + getName(), + properties, + incompleteName, + destinations, + peripheralReservationToken, + wrappingSequence, + dependencyNames, + intendedVehicleName, + type, + deadline, + dispensable + ); + } + + /** + * Creates a copy of this object and adds the given property. + * If value == null, then the key-value pair is removed from the properties. + * + * @param key the key. + * @param value the value + * @return A copy of this object that either + * includes the given entry in it's current properties, if value != null or + * excludes the entry otherwise. + */ + @Override + public TransportOrderCreationTO withProperty( + @Nonnull + String key, + @Nonnull + String value + ) { + return new TransportOrderCreationTO( + getName(), + propertiesWith(key, value), + incompleteName, + destinations, + peripheralReservationToken, + wrappingSequence, + dependencyNames, + intendedVehicleName, + type, + deadline, + dispensable + ); + } + + /** + * Indicates whether the name is incomplete and requires to be completed when creating the actual + * transport order. + * (How exactly this is done is decided by the kernel.) + * + * @return true if, and only if, the name is incomplete and requires to be completed + * by the kernel. + */ + public boolean hasIncompleteName() { + return incompleteName; + } + + /** + * Creates a copy of this object with the given nameIncomplete flag. + * + * @param incompleteName Whether the name is incomplete and requires to be completed when creating + * the actual transport order. + * + * @return A copy of this object, differing in the given value. + */ + public TransportOrderCreationTO withIncompleteName(boolean incompleteName) { + return new TransportOrderCreationTO( + getName(), + getModifiableProperties(), + incompleteName, + destinations, + peripheralReservationToken, + wrappingSequence, + dependencyNames, + intendedVehicleName, + type, + deadline, + dispensable + ); + } + + /** + * Returns the destinations that need to be travelled to. + * + * @return The destinations that need to be travelled to. + */ + @Nonnull + public List getDestinations() { + return Collections.unmodifiableList(destinations); + } + + /** + * Creates a copy of this object with the given destinations that need to be travelled to. + * + * @param destinations The destinations. + * @return A copy of this object, differing in the given derstinations. + */ + public TransportOrderCreationTO withDestinations( + @Nonnull + List destinations + ) { + return new TransportOrderCreationTO( + getName(), + getModifiableProperties(), + incompleteName, + destinations, + peripheralReservationToken, + wrappingSequence, + dependencyNames, + intendedVehicleName, + type, + deadline, + dispensable + ); + } + + /** + * Returns an optional token for reserving peripheral devices while processing this transport + * order. + * + * @return An optional token for reserving peripheral devices while processing this transport + * order. + */ + @Nullable + public String getPeripheralReservationToken() { + return peripheralReservationToken; + } + + /** + * Creates a copy of this object with the given (optional) peripheral reservation token. + * + * @param peripheralReservationToken The token. + * @return A copy of this object, differing in the given peripheral reservation token. + */ + public TransportOrderCreationTO withPeripheralReservationToken( + @Nullable + String peripheralReservationToken + ) { + return new TransportOrderCreationTO( + getName(), + getModifiableProperties(), + incompleteName, + destinations, + peripheralReservationToken, + wrappingSequence, + dependencyNames, + intendedVehicleName, + type, + deadline, + dispensable + ); + } + + /** + * Returns the (optional) name of the order sequence the transport order belongs to. + * + * @return The (optional) name of the order sequence the transport order belongs to. + */ + @Nullable + public String getWrappingSequence() { + return wrappingSequence; + } + + /** + * Creates a copy of this object with the given + * (optional) name of the order sequence the transport order belongs to. + * + * @param wrappingSequence The name of the sequence. + * @return A copy of this object, differing in the given name of the sequence. + */ + public TransportOrderCreationTO withWrappingSequence( + @Nullable + String wrappingSequence + ) { + return new TransportOrderCreationTO( + getName(), + getModifiableProperties(), + incompleteName, + destinations, + peripheralReservationToken, + wrappingSequence, + dependencyNames, + intendedVehicleName, + type, + deadline, + dispensable + ); + } + + /** + * Returns the (optional) names of transport orders the transport order depends on. + * + * @return The (optional) names of transport orders the transport order depends on. + */ + @Nonnull + public Set getDependencyNames() { + return Collections.unmodifiableSet(dependencyNames); + } + + /** + * Creates a copy of this object with the given + * (optional) names of transport orders the transport order depends on. + * + * @param dependencyNames The dependency names. + * @return A copy of this object, differing in the given dependency names. + */ + public TransportOrderCreationTO withDependencyNames( + @Nonnull + Set dependencyNames + ) { + return new TransportOrderCreationTO( + getName(), + getModifiableProperties(), + incompleteName, + destinations, + peripheralReservationToken, + wrappingSequence, + dependencyNames, + intendedVehicleName, + type, + deadline, + dispensable + ); + } + + /** + * Returns the (optional) name of the vehicle that is supposed to execute the transport order. + * + * @return The (optional) name of the vehicle that is supposed to execute the transport order. + */ + @Nullable + public String getIntendedVehicleName() { + return intendedVehicleName; + } + + /** + * Creates a copy of this object with the given + * (optional) name of the vehicle that is supposed to execute the transport order. + * + * @param intendedVehicleName The vehicle name. + * @return A copy of this object, differing in the given vehicle's name. + */ + public TransportOrderCreationTO withIntendedVehicleName( + @Nullable + String intendedVehicleName + ) { + return new TransportOrderCreationTO( + getName(), + getModifiableProperties(), + incompleteName, + destinations, + peripheralReservationToken, + wrappingSequence, + dependencyNames, + intendedVehicleName, + type, + deadline, + dispensable + ); + } + + /** + * Returns the (optional) type of the transport order. + * + * @return The (optional) type of the transport order. + */ + @Nonnull + public String getType() { + return type; + } + + /** + * Creates a copy of this object with the given (optional) type of the transport order. + * + * @param type The type. + * @return A copy of this object, differing in the given type. + */ + public TransportOrderCreationTO withType( + @Nonnull + String type + ) { + return new TransportOrderCreationTO( + getName(), + getModifiableProperties(), + incompleteName, + destinations, + peripheralReservationToken, + wrappingSequence, + dependencyNames, + intendedVehicleName, + type, + deadline, + dispensable + ); + } + + /** + * Returns the point of time at which execution of the transport order is supposed to be finished. + * + * @return The point of time at which execution of the transport order is supposed to be finished. + */ + @Nonnull + public Instant getDeadline() { + return deadline; + } + + /** + * Creates a copy of this object with the given + * point of time at which execution of the transport order is supposed to be finished. + * + * @param deadline The deadline. + * @return A copy of this object, differing in the given deadline. + */ + public TransportOrderCreationTO withDeadline( + @Nonnull + Instant deadline + ) { + return new TransportOrderCreationTO( + getName(), + getModifiableProperties(), + incompleteName, + destinations, + peripheralReservationToken, + wrappingSequence, + dependencyNames, + intendedVehicleName, + type, + deadline, + dispensable + ); + } + + /** + * Returns whether the transport order is dispensable or not. + * + * @return Whether the transport order is dispensable or not. + */ + public boolean isDispensable() { + return dispensable; + } + + /** + * Creates a copy of this object with the + * given indication whether the transport order is dispensable or not. + * + * @param dispensable The dispensable flag. + * @return A copy of this object, differing in the given dispensable flag. + */ + public TransportOrderCreationTO withDispensable(boolean dispensable) { + return new TransportOrderCreationTO( + getName(), + getModifiableProperties(), + incompleteName, + destinations, + peripheralReservationToken, + wrappingSequence, + dependencyNames, + intendedVehicleName, + type, + deadline, + dispensable + ); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/to/order/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/access/to/order/package-info.java new file mode 100644 index 0000000..e5a537a --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/to/order/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Transfer object classes for transport order objects. + */ +package org.opentcs.access.to.order; diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/to/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/access/to/package-info.java new file mode 100644 index 0000000..9dccc66 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/to/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Transfer object classes for domain objects. + */ +package org.opentcs.access.to; diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/to/peripherals/PeripheralJobCreationTO.java b/opentcs-api-base/src/main/java/org/opentcs/access/to/peripherals/PeripheralJobCreationTO.java new file mode 100644 index 0000000..b2995f1 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/to/peripherals/PeripheralJobCreationTO.java @@ -0,0 +1,295 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.to.peripherals; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.io.Serializable; +import java.util.Map; +import org.opentcs.access.to.CreationTO; + +/** + * A transfer object describing a peripheral job. + */ +public class PeripheralJobCreationTO + extends + CreationTO + implements + Serializable { + + /** + * Indicates whether the name is incomplete and requires to be completed when creating the actual + * peripheral job. + * (How exactly this is done is decided by the kernel.) + */ + private final boolean incompleteName; + /** + * A token that may be used to reserve a peripheral device. + * A peripheral device that is reserved for a specific token can only process jobs which match + * that reservation token. + * This string may not be empty. + */ + @Nonnull + private final String reservationToken; + /** + * The name of the vehicle for which this peripheral job is to be created. + */ + @Nullable + private final String relatedVehicleName; + /** + * The name of the transport order for which this peripheral job is to be created. + */ + @Nullable + private final String relatedTransportOrderName; + /** + * The operation that is to be perfromed by the pripheral device. + */ + @Nonnull + private final PeripheralOperationCreationTO peripheralOperation; + + /** + * Creates a new instance. + * + * @param name The name of this peripheral job. + * @param reservationToken The reservation token to be used. + * @param peripheralOperation The peripheral operation to be performed. + */ + public PeripheralJobCreationTO( + @Nonnull + String name, + @Nonnull + String reservationToken, + @Nonnull + PeripheralOperationCreationTO peripheralOperation + ) { + super(name); + this.incompleteName = false; + this.reservationToken = requireNonNull(reservationToken, "reservationToken"); + this.relatedVehicleName = null; + this.relatedTransportOrderName = null; + this.peripheralOperation = requireNonNull(peripheralOperation, "peripheralOperation"); + } + + private PeripheralJobCreationTO( + @Nonnull + String name, + @Nonnull + Map properties, + boolean incompleteName, + @Nonnull + String reservationToken, + @Nullable + String relatedVehicleName, + @Nullable + String relatedTransportOrderName, + @Nonnull + PeripheralOperationCreationTO peripheralOperation + ) { + super(name, properties); + this.incompleteName = incompleteName; + this.reservationToken = requireNonNull(reservationToken, "reservationToken"); + this.relatedVehicleName = relatedVehicleName; + this.relatedTransportOrderName = relatedTransportOrderName; + this.peripheralOperation = requireNonNull(peripheralOperation, "peripheralOperation"); + } + + @Override + public PeripheralJobCreationTO withName( + @Nonnull + String name + ) { + return new PeripheralJobCreationTO( + name, + getModifiableProperties(), + incompleteName, + reservationToken, + relatedVehicleName, + relatedTransportOrderName, + peripheralOperation + ); + } + + @Override + public PeripheralJobCreationTO withProperties( + @Nonnull + Map properties + ) { + return new PeripheralJobCreationTO( + getName(), + properties, + incompleteName, + reservationToken, + relatedVehicleName, + relatedTransportOrderName, + peripheralOperation + ); + } + + @Override + public PeripheralJobCreationTO withProperty( + @Nonnull + String key, + @Nonnull + String value + ) { + return new PeripheralJobCreationTO( + getName(), + propertiesWith(key, value), + incompleteName, + reservationToken, + relatedVehicleName, + relatedTransportOrderName, + peripheralOperation + ); + } + + /** + * Indicates whether the name is incomplete and requires to be completed when creating the actual + * transport order. + * (How exactly this is done is decided by the kernel.) + * + * @return {@code true} if, and only if, the name is incomplete and requires to be completed + * by the kernel. + */ + public boolean hasIncompleteName() { + return incompleteName; + } + + /** + * Creates a copy of this object, with the given incomplete name flag. + * + * @param incompleteName The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralJobCreationTO withIncompleteName(boolean incompleteName) { + return new PeripheralJobCreationTO( + getName(), + getModifiableProperties(), + incompleteName, + reservationToken, + relatedVehicleName, + relatedTransportOrderName, + peripheralOperation + ); + } + + /** + * Returns the token that may be used to reserve a peripheral device. + * + * @return The token that may be used to reserve a peripheral device. + */ + public String getReservationToken() { + return reservationToken; + } + + /** + * Creates a copy of this object, with the given reservation token. + * + * @param reservationToken The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralJobCreationTO withReservationToken(String reservationToken) { + return new PeripheralJobCreationTO( + getName(), + getModifiableProperties(), + incompleteName, + reservationToken, + relatedVehicleName, + relatedTransportOrderName, + peripheralOperation + ); + } + + /** + * Returns the name of the vehicle for which this peripheral job is to be created. + * + * @return The name of the vehicle for which this peripheral job is to be created. + */ + @Nullable + public String getRelatedVehicleName() { + return relatedVehicleName; + } + + /** + * Creates a copy of this object, with the given related vehicle name. + * + * @param relatedVehicleName The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralJobCreationTO withRelatedVehicleName( + @Nullable + String relatedVehicleName + ) { + return new PeripheralJobCreationTO( + getName(), + getModifiableProperties(), + incompleteName, + reservationToken, + relatedVehicleName, + relatedTransportOrderName, + peripheralOperation + ); + } + + /** + * Returns the name of the transport order for which this peripheral job is to be created. + * + * @return The name of the transport order for which this peripheral job is to be created. + */ + @Nullable + public String getRelatedTransportOrderName() { + return relatedTransportOrderName; + } + + /** + * Creates a copy of this object, with the given related transport order name. + * + * @param relatedTransportOrderName The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralJobCreationTO withRelatedTransportOrderName( + @Nullable + String relatedTransportOrderName + ) { + return new PeripheralJobCreationTO( + getName(), + getModifiableProperties(), + incompleteName, + reservationToken, + relatedVehicleName, + relatedTransportOrderName, + peripheralOperation + ); + } + + /** + * Returns the operation that is to be performed by the pripheral device. + * + * @return The operation that is to be performed by the pripheral device. + */ + public PeripheralOperationCreationTO getPeripheralOperation() { + return peripheralOperation; + } + + /** + * Creates a copy of this object, with the given peripheral operation. + * + * @param peripheralOperation The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralJobCreationTO withPeripheralOperation( + PeripheralOperationCreationTO peripheralOperation + ) { + return new PeripheralJobCreationTO( + getName(), + getModifiableProperties(), + incompleteName, + reservationToken, + relatedVehicleName, + relatedTransportOrderName, + peripheralOperation + ); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/access/to/peripherals/PeripheralOperationCreationTO.java b/opentcs-api-base/src/main/java/org/opentcs/access/to/peripherals/PeripheralOperationCreationTO.java new file mode 100644 index 0000000..b207bd5 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/access/to/peripherals/PeripheralOperationCreationTO.java @@ -0,0 +1,252 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.to.peripherals; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import java.util.Map; +import org.opentcs.access.to.CreationTO; +import org.opentcs.data.peripherals.PeripheralOperation; + +/** + * A transfer object describing an operation to be performed by a peripheral device. + */ +public class PeripheralOperationCreationTO + extends + CreationTO + implements + Serializable { + + /** + * The operation to be performed by the peripheral device. + */ + private final String operation; + /** + * The name of the location the peripheral device is associated with. + */ + @Nonnull + private final String locationName; + /** + * The moment at which this operation is to be performed. + */ + @Nonnull + private final PeripheralOperation.ExecutionTrigger executionTrigger; + /** + * Whether the completion of this operation is required to allow a vehicle to continue driving. + */ + private final boolean completionRequired; + + /** + * Creates a new instance with {@code executionTrigger} set to + * {@link PeripheralOperation.ExecutionTrigger#IMMEDIATE} and {@code completionRequired} + * set to {@code false}. + * + * @param operation The operation to be performed by the peripheral device. + * @param locationName The name of the location the peripheral device is associated with. + */ + public PeripheralOperationCreationTO( + @Nonnull + String operation, + @Nonnull + String locationName + ) { + super(""); + this.operation = requireNonNull(operation, "operation"); + this.locationName = requireNonNull(locationName, "locationName"); + this.executionTrigger = PeripheralOperation.ExecutionTrigger.IMMEDIATE; + this.completionRequired = false; + } + + private PeripheralOperationCreationTO( + @Nonnull + String name, + @Nonnull + Map properties, + @Nonnull + String operation, + @Nonnull + String locationName, + @Nonnull + PeripheralOperation.ExecutionTrigger executionTrigger, + boolean completionRequired + ) { + super(name, properties); + this.operation = requireNonNull(operation, "operation"); + this.locationName = requireNonNull(locationName, "locationName"); + this.executionTrigger = requireNonNull(executionTrigger, "executionTrigger"); + this.completionRequired = completionRequired; + } + + @Override + public PeripheralOperationCreationTO withName( + @Nonnull + String name + ) { + return new PeripheralOperationCreationTO( + name, + getModifiableProperties(), + operation, + locationName, + executionTrigger, + completionRequired + ); + } + + @Override + public PeripheralOperationCreationTO withProperties( + @Nonnull + Map properties + ) { + return new PeripheralOperationCreationTO( + getName(), + properties, + operation, + locationName, + executionTrigger, + completionRequired + ); + } + + @Override + public PeripheralOperationCreationTO withProperty( + @Nonnull + String key, + @Nonnull + String value + ) { + return new PeripheralOperationCreationTO( + getName(), + propertiesWith(key, value), + operation, + locationName, + executionTrigger, + completionRequired + ); + } + + /** + * Returns the operation to be performed by the peripheral device. + * + * @return The operation to be performed by the peripheral device. + */ + @Nonnull + public String getOperation() { + return operation; + } + + /** + * Creates a copy of this object, with the given operation. + * + * @param operation The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralOperationCreationTO withOperation( + @Nonnull + String operation + ) { + return new PeripheralOperationCreationTO( + getName(), + getModifiableProperties(), + operation, + locationName, + executionTrigger, + completionRequired + ); + } + + /** + * Returns the name of the location the peripheral device is associated with. + * + * @return The name of the location the peripheral device is associated with. + */ + @Nonnull + public String getLocationName() { + return locationName; + } + + /** + * Creates a copy of this object, with the given location name. + * + * @param locationName The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralOperationCreationTO withLocationName( + @Nonnull + String locationName + ) { + return new PeripheralOperationCreationTO( + getName(), + getModifiableProperties(), + operation, + locationName, + executionTrigger, + completionRequired + ); + } + + /** + * Returns the moment at which this operation is to be performed. + * + * @return The moment at which this operation is to be performed. + */ + @Nonnull + public PeripheralOperation.ExecutionTrigger getExecutionTrigger() { + return executionTrigger; + } + + /** + * Creates a copy of this object, with the given execution trigger. + *

+ * This method should only be used by the vehicle controller component of the baseline project. + *

+ * + * @param executionTrigger The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralOperationCreationTO withExecutionTrigger( + @Nonnull + PeripheralOperation.ExecutionTrigger executionTrigger + ) { + return new PeripheralOperationCreationTO( + getName(), + getModifiableProperties(), + operation, + locationName, + executionTrigger, + completionRequired + ); + } + + /** + * Returns whether the completion of this operation is required to allow a vehicle to continue + * driving. + * + * @return Whether the completion of this operation is required to allow a vehicle to continue + * driving. + */ + public boolean isCompletionRequired() { + return completionRequired; + } + + /** + * Creates a copy of this object, with the given completion required flag. + *

+ * This method should only be used by the vehicle controller component of the baseline project. + *

+ * + * @param completionRequired The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralOperationCreationTO withCompletionRequired(boolean completionRequired) { + return new PeripheralOperationCreationTO( + getName(), + getModifiableProperties(), + operation, + locationName, + executionTrigger, + completionRequired + ); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/Lifecycle.java b/opentcs-api-base/src/main/java/org/opentcs/components/Lifecycle.java new file mode 100644 index 0000000..cf17379 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/Lifecycle.java @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components; + +/** + * Defines methods for controlling a generic component's lifecycle. + */ +public interface Lifecycle { + + /** + * (Re-)Initializes this component before it is being used. + */ + void initialize(); + + /** + * Checks whether this component is initialized. + * + * @return true if, and only if, this component is initialized. + */ + boolean isInitialized(); + + /** + * Terminates the instance and frees resources. + */ + void terminate(); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/Dispatcher.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/Dispatcher.java new file mode 100644 index 0000000..b3ffe9b --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/Dispatcher.java @@ -0,0 +1,152 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel; + +import jakarta.annotation.Nonnull; +import org.opentcs.components.Lifecycle; +import org.opentcs.components.kernel.dipatching.TransportOrderAssignmentException; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.data.order.TransportOrder; + +/** + * This interface declares the methods a dispatcher module for the openTCS + * kernel must implement. + *

+ * A dispatcher manages the distribution of transport orders among the vehicles + * in a system. It is basically event-driven, where an event can be a new + * transport order being introduced into the system or a vehicle becoming + * available for processing existing orders. + *

+ */ +public interface Dispatcher + extends + Lifecycle { + + /** + * The key of a parking position property defining its priority. + *

+ * Whether and in what way this is respected for assigning a parking position to a vehicle is + * implementation-specific. + *

+ */ + String PROPKEY_PARKING_POSITION_PRIORITY = "tcs:parkingPositionPriority"; + /** + * The key of a vehicle property defining the name of the vehicle's assigned parking position. + *

+ * Whether and in what way this is respected for selecting a parking position is + * implementation-specific. + *

+ */ + String PROPKEY_ASSIGNED_PARKING_POSITION = "tcs:assignedParkingPosition"; + /** + * The key of a vehicle property defining the name of the vehicle's preferred parking position. + *

+ * Whether and in what way this is respected for selecting a parking position is + * implementation-specific. + *

+ */ + String PROPKEY_PREFERRED_PARKING_POSITION = "tcs:preferredParkingPosition"; + /** + * The key of a vehicle property defining the name of the vehicle's assigned recharge location. + *

+ * Whether and in what way this is respected for selecting a recharge location is + * implementation-specific. + *

+ */ + String PROPKEY_ASSIGNED_RECHARGE_LOCATION = "tcs:assignedRechargeLocation"; + /** + * The key of a vehicle property defining the name of the vehicle's preferred recharge location. + *

+ * Whether and in what way this is respected for selecting a recharge location is + * implementation-specific. + *

+ */ + String PROPKEY_PREFERRED_RECHARGE_LOCATION = "tcs:preferredRechargeLocation"; + + /** + * Notifies the dispatcher that it should start the dispatching process. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ */ + void dispatch(); + + /** + * Notifies the dispatcher that the given transport order is to be withdrawn/aborted. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param order The transport order to be withdrawn/aborted. + * @param immediateAbort Whether the order should be aborted immediately instead of withdrawn. + */ + void withdrawOrder( + @Nonnull + TransportOrder order, + boolean immediateAbort + ); + + /** + * Notifies the dispatcher that any order a given vehicle might be processing is to be withdrawn. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param vehicle The vehicle whose order is withdrawn. + * @param immediateAbort Whether the vehicle's order should be aborted immediately instead of + * withdrawn. + */ + void withdrawOrder( + @Nonnull + Vehicle vehicle, + boolean immediateAbort + ); + + /** + * Notifies the dispatcher of a request to reroute the given vehicle considering the given + * rerouting type. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param vehicle The vehicle to be rerouted. + * @param reroutingType The type of the requested rerouting. + */ + void reroute( + @Nonnull + Vehicle vehicle, + @Nonnull + ReroutingType reroutingType + ); + + /** + * Notifies the dispatcher to reroute all vehicles considering the given rerouting type. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param reroutingType The type of the requested rerouting. + */ + void rerouteAll( + @Nonnull + ReroutingType reroutingType + ); + + /** + * Notifies the dispatcher that it should assign the given transport order (to its intended + * vehicle) now. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param transportOrder The transport order to be assigned. + * @throws TransportOrderAssignmentException If the given transport order could not be assigned + * to its intended vehicle. + */ + void assignNow( + @Nonnull + TransportOrder transportOrder + ) + throws TransportOrderAssignmentException; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/KernelExtension.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/KernelExtension.java new file mode 100644 index 0000000..4b50110 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/KernelExtension.java @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel; + +import org.opentcs.components.Lifecycle; + +/** + * Declares the methods that a generic kernel extension must implement. + */ +public interface KernelExtension + extends + Lifecycle { +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/ObjectNameProvider.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/ObjectNameProvider.java new file mode 100644 index 0000000..e9bb825 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/ObjectNameProvider.java @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel; + +import java.util.function.Function; +import org.opentcs.access.to.CreationTO; + +/** + * Provides names for {@link CreationTO}s. + */ +public interface ObjectNameProvider + extends + Function { + +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/OrderSequenceCleanupApproval.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/OrderSequenceCleanupApproval.java new file mode 100644 index 0000000..b2b8d1a --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/OrderSequenceCleanupApproval.java @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel; + +import java.util.function.Predicate; +import org.opentcs.data.order.OrderSequence; + +/** + * Implementations of this interface check whether an order sequence may be removed. + */ +public interface OrderSequenceCleanupApproval + extends + Predicate { + +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/PeripheralJobCleanupApproval.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/PeripheralJobCleanupApproval.java new file mode 100644 index 0000000..1bf4c7f --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/PeripheralJobCleanupApproval.java @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel; + +import java.util.function.Predicate; +import org.opentcs.data.peripherals.PeripheralJob; + +/** + * Implementations of this interface check whether a peripheral job may be removed. + */ +public interface PeripheralJobCleanupApproval + extends + Predicate { + +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/PeripheralJobDispatcher.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/PeripheralJobDispatcher.java new file mode 100644 index 0000000..06aa384 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/PeripheralJobDispatcher.java @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel; + +import jakarta.annotation.Nonnull; +import org.opentcs.components.Lifecycle; +import org.opentcs.data.model.Location; +import org.opentcs.data.peripherals.PeripheralJob; + +/** + * This interface declares the methods a peripheral job dispatcher module for the openTCS kernel + * must implement. + *

+ * A peripheral job dispatcher manages the distribution of peripheral jobs among the peripheral + * devices represented by locations in a system. It is basically event-driven, where an event can + * be a new peripheral job being introduced into the system or a peripheral device becoming + * available for processing existing jobs. + *

+ */ +public interface PeripheralJobDispatcher + extends + Lifecycle { + + /** + * Notifies the dispatcher that it should start the dispatching process. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ */ + void dispatch(); + + /** + * Notifies the dispatcher that any job a peripheral device (represented by the given location) + * might be processing is to be withdrawn. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param location The location representing a peripheral device whose job is withdrawn. + * @throws IllegalArgumentException If the given peripheral's current job is already in a final + * state, or if it is related to a transport order and this transport order is not in a final + * state. + */ + void withdrawJob( + @Nonnull + Location location + ) + throws IllegalArgumentException; + + /** + * Notifies the dispatcher that the given peripheral job is to be withdrawn. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param job The job to be withdrawn. + * @throws IllegalArgumentException If the given peripheral job is already in a final state, or if + * it is related to a transport order and this transport order is not in a final state. + */ + void withdrawJob( + @Nonnull + PeripheralJob job + ) + throws IllegalArgumentException; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/Query.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/Query.java new file mode 100644 index 0000000..07cb652 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/Query.java @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel; + +import java.io.Serializable; + +/** + * Marks a query (parameter) object. + * + * @param The result type. + */ +public interface Query { + + /** + * A convenience class to be used as the result type for queries that do not return any result. + */ + class Void + implements + Serializable { + + /** + * Creates a new instance. + */ + public Void() { + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/QueryResponder.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/QueryResponder.java new file mode 100644 index 0000000..7d3dbcb --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/QueryResponder.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel; + +/** + * A responder for generic queries. + */ +public interface QueryResponder { + + /** + * Executes the specified query. + * + * @param The query/result type. + * @param query The query/parameter object. + * @return The query result. + */ + T query(Query query); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/ResourceAllocationException.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/ResourceAllocationException.java new file mode 100644 index 0000000..2681043 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/ResourceAllocationException.java @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel; + +import org.opentcs.access.KernelException; + +/** + * Thrown when allocating resources for a {@link Scheduler.Client Scheduler.Client} is impossible. + */ +public class ResourceAllocationException + extends + KernelException { + + /** + * Creates a new ResourceAllocationException with the given detail message. + * + * @param message The detail message. + */ + public ResourceAllocationException(String message) { + super(message); + } + + /** + * Creates a new ResourceAllocationException with the given detail message and + * cause. + * + * @param message The detail message. + * @param cause The cause. + */ + public ResourceAllocationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/Router.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/Router.java new file mode 100644 index 0000000..fe8e197 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/Router.java @@ -0,0 +1,233 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.opentcs.components.Lifecycle; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.util.annotations.ScheduledApiChange; + +/** + * This interface declares the methods a router module for the openTCS + * kernel must implement. + *

+ * A router finds routes from a start point to an end point, rating them + * according to implementation specific criteria/costs parameters. + *

+ */ +public interface Router + extends + Lifecycle { + + /** + * The key of a vehicle property defining the group of vehicles that may share the same routing. + *

+ * The value is expected to be an integer. + *

+ */ + String PROPKEY_ROUTING_GROUP = "tcs:routingGroup"; + /** + * The key (prefix) of a path property defining the routing cost when its travelled in forward + * direction. + *

+ * The value is expected to be a (long) integer. + *

+ */ + String PROPKEY_ROUTING_COST_FORWARD = "tcs:routingCostForward"; + /** + * The key (prefix) of a path property defining the routing cost when its travelled in reverse + * direction. + *

+ * The value is expected to be a (long) integer. + *

+ */ + String PROPKEY_ROUTING_COST_REVERSE = "tcs:routingCostReverse"; + + /** + * Notifies the router to update its routing topology with respect to the given paths. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param paths The paths to update in the routing topology. An empty set of paths results in the + * router updating the entire routing topology. + */ + void updateRoutingTopology( + @Nonnull + Set paths + ); + + /** + * Checks the routability of a given transport order. + *

+ * The check for routability is affected by path properties and configured edge evaluators. This + * means that whether a transport order is considered routable can change between + * consecutive calls to this method. + *

+ *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param order The transport order to check for routability. + * @return A set of vehicles for which a route for the given transport order + * would be computable. + */ + @Nonnull + Set checkRoutability( + @Nonnull + TransportOrder order + ); + + /** + * Checks the general routability of a given transport order. + *

+ * The check for general routability is not affected by any path properties or any + * configured edge evaluators. This means that whether a transport order is considered generally + * routable will not change between consecutive calls to this method. + *

+ *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param order The transport order to check for routability. + * @return {@code true}, if the transport order is generally routable, otherwise {@code false}. + */ + @ScheduledApiChange(when = "7.0", details = "Default implementation will be removed.") + default boolean checkGeneralRoutability( + @Nonnull + TransportOrder order + ) { + return false; + } + + /** + * Returns a complete route for a given vehicle that starts on a specified + * point and allows the vehicle to process a given transport order. + * The route is encapsulated into drive orders which correspond to those drive + * orders that the transport order is composed of. The transport order itself + * is not modified. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param vehicle The vehicle for which the calculated route must be passable. + * @param sourcePoint The position at which the vehicle would start processing + * the transport order (i.e. the vehicle's current position). + * @param transportOrder The transport order to be processed by the vehicle. + * @return A list of drive orders containing the complete calculated route for + * the given transport order, passable the given vehicle and starting on the + * given point, or the empty optional, if no such route exists. + */ + @Nonnull + Optional> getRoute( + @Nonnull + Vehicle vehicle, + @Nonnull + Point sourcePoint, + @Nonnull + TransportOrder transportOrder + ); + + /** + * Returns a route from one point to another, passable for a given vehicle. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param vehicle The vehicle for which the route must be passable. + * @param sourcePoint The starting point of the route to calculate. + * @param destinationPoint The end point of the route to calculate. + * @param resourcesToAvoid Resources to avoid when calculating the route. + * @return The calculated route, or the empty optional, if a route between the + * given points does not exist. + */ + @Nonnull + Optional getRoute( + @Nonnull + Vehicle vehicle, + @Nonnull + Point sourcePoint, + @Nonnull + Point destinationPoint, + @Nonnull + Set> resourcesToAvoid + ); + + /** + * Returns the costs for travelling a route from one point to another with a + * given vehicle. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param vehicle The vehicle for which the route must be passable. + * @param sourcePoint The starting point of the route. + * @param destinationPoint The end point of the route. + * @param resourcesToAvoid Resources to avoid when calculating the route. + * @return The costs of the route, or Long.MAX_VALUE, if no such + * route exists. + */ + long getCosts( + @Nonnull + Vehicle vehicle, + @Nonnull + Point sourcePoint, + @Nonnull + Point destinationPoint, + @Nonnull + Set> resourcesToAvoid + ); + + /** + * Notifies the router of a route being selected for a vehicle. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param vehicle The vehicle for which a route is being selected. + * @param driveOrders The drive orders encapsulating the route being selected, + * or null, if no route is being selected for the vehicle (i.e. + * an existing entry for the given vehicle would be removed). + */ + void selectRoute( + @Nonnull + Vehicle vehicle, + @Nullable + List driveOrders + ); + + /** + * Returns an unmodifiable view on the selected routes the router knows about. + * The returned map contains an entry for each vehicle for which a selected + * route is known. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @return An unmodifiable view on the selected routes the router knows about. + */ + @Nonnull + Map> getSelectedRoutes(); + + /** + * Returns all points which are currently targeted by any vehicle. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @return A set of all points currently targeted by any vehicle. + */ + @Nonnull + Set getTargetedPoints(); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/Scheduler.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/Scheduler.java new file mode 100644 index 0000000..5b92115 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/Scheduler.java @@ -0,0 +1,402 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.opentcs.components.Lifecycle; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.TCSResource; +import org.opentcs.data.model.Vehicle; + +/** + * Manages resources used by clients (vehicles) to help prevent both collisions and deadlocks. + *

+ * Every client usually interacts with the Scheduler according to the following + * workflow: + *

+ *
    + *
  1. + * Initially, the client calls + * {@link #allocateNow(org.opentcs.components.kernel.Scheduler.Client, java.util.Set) allocateNow()} + * when a vehicle pops up somewhere in the driving course. + * This usually happens either upon kernel startup or when a vehicle communicates its current + * position to the kernel for the first time. + *
  2. + *
  3. + * Once a transport order is assigned to a vehicle, the client calls + * {@link #claim(org.opentcs.components.kernel.Scheduler.Client, java.util.List) claim()} with the + * complete sequence of resource sets the vehicle needs to process the transport order - usually + * each containing a point and the path leading to it. + *
  4. + *
  5. + * As the vehicle processes the transport order, the client subsequently calls + * {@link #allocate(org.opentcs.components.kernel.Scheduler.Client, java.util.Set) allocate()} for + * resources needed next (for reaching the next point on the route). + * The Scheduler asynchronously calls back either + * {@link Client#allocationSuccessful(java.util.Set)} or + * {@link Client#allocationFailed(java.util.Set)}, informing the client about the result. + * Upon allocating the resources for the client, it also implicitly removes them from the head of + * the client's claim sequence. + *
  6. + *
  7. + * As the vehicle passes points (and paths) on the route, the client calls + * {@link #free(org.opentcs.components.kernel.Scheduler.Client, java.util.Set) free()} for resources + * it does not need any more, allowing these resources to be allocated by other clients. + *
  8. + *
+ *

+ * At the end of this process, the client's claim sequence is empty, and only the most recently + * allocated resources are still assigned to it, reflecting the vehicle's current position. + * (If the vehicle has disappeared from the driving course after processing the transport order, the + * client would call {@link #freeAll(org.opentcs.components.kernel.Scheduler.Client) freeAll()} to + * inform the Scheduler about this.) + *

+ */ +public interface Scheduler + extends + Lifecycle { + + /** + * The key of a path property defining the direction in which a vehicle is entering a block when + * it's taking the path. + */ + String PROPKEY_BLOCK_ENTRY_DIRECTION = "tcs:blockEntryDirection"; + + /** + * Sets/Updates the resource claim for a vehicle. + *

+ * Claimed resources are resources that a vehicle will eventually require for executing + * its movements in the future, but for which it does not request allocation, yet. + * Claiming resources provides information to the scheduler about future allocations and their + * intended order, allowing the scheduler to consider these information for its resource + * planning. + *

+ *

+ * Resources can be claimed by multiple vehicles at the same time. + * This is different from allocations: + * Only a single vehicle can allocate a resource at the same time. + *

+ *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param client The client claiming the resources. + * @param resourceSequence The sequence of resources claimed. May be empty to clear the client's + * claim. + */ + void claim( + @Nonnull + Client client, + @Nonnull + List>> resourceSequence + ); + + /** + * Requests allocation of the given resources. + * The client will be informed via a callback to + * {@link Client#allocationSuccessful(java.util.Set)} or + * {@link Client#allocationFailed(java.util.Set)} whether the allocation was successful or not. + *
    + *
  • + * Clients may only allocate resources in the order they have previously + * {@link #claim(org.opentcs.components.kernel.Scheduler.Client, java.util.List) claim()}ed them. + *
  • + *
  • + * Upon allocation, the scheduler will implicitly remove the set of allocated resources from (the + * head of) the client's claim sequence. + *
  • + *
  • + * As a result, a client may only allocate the set of resources at the head of its claim sequence. + *
  • + *
+ *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param client The client requesting the resources. + * @param resources The resources to be allocated. + * @throws IllegalArgumentException If the set of resources to be allocated is not equal to the + * next set in the sequence of currently claimed resources, or if the client has already + * requested resources that have not yet been granted. + * @see #claim(org.opentcs.components.kernel.Scheduler.Client, java.util.List) + */ + void allocate( + @Nonnull + Client client, + @Nonnull + Set> resources + ) + throws IllegalArgumentException; + + /** + * Checks if the resulting system state is safe if the given set of resources + * would be allocated by the given client immediately. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param client The client requesting the resources. + * @param resources The requested resources. + * @return {@code true} if the given resources are safe to be allocated by the given client, + * otherwise {@code false}. + */ + boolean mayAllocateNow( + @Nonnull + Client client, + @Nonnull + Set> resources + ); + + /** + * Informs the scheduler that a set of resources are to be allocated for the given client + * immediately. + *

+ * Note the following: + *

+ *
    + *
  • + * This method should only be called in urgent/emergency cases, for instance if a vehicle has been + * moved to a different point manually, which has to be reflected by resource allocation in the + * scheduler. + *
  • + *
  • + * Unlike + * {@link #allocate(org.opentcs.components.kernel.Scheduler.Client, java.util.Set) allocate()}, + * this method does not block, i.e. the operation happens synchronously. + *
  • + *
  • + * This method does not implicitly deallocate or unclaim any other resources for the + * client. + *
  • + *
+ *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param client The client requesting the resources. + * @param resources The resources requested. + * @throws ResourceAllocationException If it's impossible to allocate the given set of resources + * for the given client. + */ + void allocateNow( + @Nonnull + Client client, + @Nonnull + Set> resources + ) + throws ResourceAllocationException; + + /** + * Releases a set of resources allocated by a client. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param client The client releasing the resources. + * @param resources The resources released. Any resources in the given set not allocated by the + * given client are ignored. + */ + void free( + @Nonnull + Client client, + @Nonnull + Set> resources + ); + + /** + * Releases all resources allocated by the given client. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param client The client. + */ + void freeAll( + @Nonnull + Client client + ); + + /** + * Releases all pending resource allocations for the given client. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param client The client. + */ + void clearPendingAllocations( + @Nonnull + Client client + ); + + /** + * Explicitly triggers a rescheduling run during which the scheduler tries to allocate resources + * for all waiting clients. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ */ + void reschedule(); + + /** + * Returns all resource allocations as a map of client IDs to resources. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @return All resource allocations as a map of client IDs to resources. + */ + @Nonnull + Map>> getAllocations(); + + /** + * Informs the scheduler that a set of resources was successfully prepared in order of allocating + * them to a client. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param module The module a preparation was necessary for. + * @param client The client that requested the preparation/allocation. + * @param resources The resources that are now prepared for the client. + */ + void preparationSuccessful( + @Nonnull + Module module, + @Nonnull + Client client, + @Nonnull + Set> resources + ); + + /** + * Defines callback methods for clients of the resource scheduler. + */ + interface Client { + + /** + * Returns an ID string for this client. + * The returned string should be unique among all clients in the system. + * + * @return An unique ID string for this client. + */ + @Nonnull + String getId(); + + /** + * Returns a reference to the {@link Vehicle} that this client is related to. + * + * @return A reference to the {@link Vehicle} that this client is related to or {@code null}, if + * this client is not related to any {@link Vehicle}. + */ + @Nullable + TCSObjectReference getRelatedVehicle(); + + /** + * Called when resources have been reserved for this client. + * + * @param resources The resources reserved. + * @return true if, and only if, this client accepts the resources allocated. A + * return value of false indicates this client does not need the given resources + * (any more), freeing them implicitly, but not restoring any previous claim. + */ + boolean allocationSuccessful( + @Nonnull + Set> resources + ); + + /** + * Called if it was impossible to allocate a requested set of resources for this client. + * + * @param resources The resources which could not be reserved. + */ + void allocationFailed( + @Nonnull + Set> resources + ); + } + + /** + * A scheduler module. + */ + interface Module + extends + Lifecycle { + + /** + * Informs this module about a client's current allocation state. + * + * @param client The client. + * @param alloc The client's currently allocated resources. + * @param remainingClaim The client's remaining claim. + */ + void setAllocationState( + @Nonnull + Client client, + @Nonnull + Set> alloc, + @Nonnull + List>> remainingClaim + ); + + /** + * Checks if the resulting system state is safe if the given set of resources + * would be allocated by the given resource user. + * + * @param client The ResourceUser requesting resources set. + * @param resources The requested resources. + * @return true if this module thinks the given resources may be allocated for the + * given client. + */ + boolean mayAllocate( + @Nonnull + Client client, + @Nonnull + Set> resources + ); + + /** + * Lets this module prepare the given resources so they can be allocated to a client. + * + * @param client The client the resources are being prepared for. + * @param resources The resources to be prepared. + */ + void prepareAllocation( + @Nonnull + Client client, + @Nonnull + Set> resources + ); + + /** + * Checks if this module is done preparing the given resources for a client. + * + * @param client The client the resources are being prepared for. + * @param resources The resources to be checked. + * @return true if the resoruces are prepared for a client. + */ + boolean hasPreparedAllocation( + @Nonnull + Client client, + @Nonnull + Set> resources + ); + + /** + * Informs this module about resources being fully released by a client. + * + * @param client The client releasing the resources. + * @param resources The resources being released. + */ + void allocationReleased( + @Nonnull + Client client, + @Nonnull + Set> resources + ); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/TransportOrderCleanupApproval.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/TransportOrderCleanupApproval.java new file mode 100644 index 0000000..a43acdf --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/TransportOrderCleanupApproval.java @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel; + +import java.util.function.Predicate; +import org.opentcs.data.order.TransportOrder; + +/** + * Implementations of this interface check whether a transport order may be removed. + */ +public interface TransportOrderCleanupApproval + extends + Predicate { + +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/dipatching/TransportOrderAssignmentException.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/dipatching/TransportOrderAssignmentException.java new file mode 100644 index 0000000..9836e0d --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/dipatching/TransportOrderAssignmentException.java @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.dipatching; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; + +/** + * Thrown when a {@link TransportOrder} could not be assigned to a {@link Vehicle}. + */ +public class TransportOrderAssignmentException + extends + KernelRuntimeException { + + private final TCSObjectReference transportOrder; + private final TCSObjectReference vehicle; + private final TransportOrderAssignmentVeto transportOrderAssignmentVeto; + + /** + * Creates a new instance. + * + * @param transportOrder The transport order. + * @param vehicle The vehicle. + * @param transportOrderAssignmentVeto The reason why the transport order could not be assigned + * to the vehicle. + */ + public TransportOrderAssignmentException( + @Nonnull + TCSObjectReference transportOrder, + @Nullable + TCSObjectReference vehicle, + @Nonnull + TransportOrderAssignmentVeto transportOrderAssignmentVeto + ) { + super( + "Could not assign transport order '" + transportOrder.getName() + "' to vehicle '" + + (vehicle != null ? vehicle.getName() : "null") + "': " + + transportOrderAssignmentVeto.name() + ); + // This exception is reasonable only for actual assignment vetos. + checkArgument( + transportOrderAssignmentVeto != TransportOrderAssignmentVeto.NO_VETO, + "Invalid assignment veto for exception: " + transportOrderAssignmentVeto + ); + this.transportOrder = requireNonNull(transportOrder, "transportOrder"); + this.vehicle = vehicle; + this.transportOrderAssignmentVeto = requireNonNull( + transportOrderAssignmentVeto, + "transportOrderAssignmentVeto" + ); + } + + /** + * Returns the transport order. + * + * @return The transport order. + */ + @Nonnull + public TCSObjectReference getTransportOrder() { + return transportOrder; + } + + /** + * Returns the vehicle. + * + * @return The vehicle. + */ + @Nullable + public TCSObjectReference getVehicle() { + return vehicle; + } + + /** + * Returns the reason why a transport order assignment was not possible. + * + * @return The reason why a transport order assignment was not possible. + */ + @Nonnull + public TransportOrderAssignmentVeto getTransportOrderAssignmentVeto() { + return transportOrderAssignmentVeto; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/dipatching/TransportOrderAssignmentVeto.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/dipatching/TransportOrderAssignmentVeto.java new file mode 100644 index 0000000..2b8c8c8 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/dipatching/TransportOrderAssignmentVeto.java @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.dipatching; + +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; + +/** + * Defines reasons for a transport order assignment not being possible. + */ +public enum TransportOrderAssignmentVeto { + + /** + * There is no reason that prevents the transport order assignment. + */ + NO_VETO, + /** + * The transport order's {@link TransportOrder.State} is invalid (e.g. because it's not in state + * {@link TransportOrder.State#DISPATCHABLE}). + */ + TRANSPORT_ORDER_STATE_INVALID, + /** + * The transport order is part of an {@link OrderSequence}. + */ + TRANSPORT_ORDER_PART_OF_ORDER_SEQUENCE, + /** + * The transport order has its intended vehicle not set. + */ + TRANSPORT_ORDER_INTENDED_VEHICLE_NOT_SET, + /** + * The {@link Vehicle.ProcState} of the vehicle to assign the transport order to is invalid (e.g. + * because it's not {@link Vehicle.ProcState#IDLE}). + */ + VEHICLE_PROCESSING_STATE_INVALID, + /** + * The {@link Vehicle.State} of the vehicle to assign the transport order to is invalid (e.g. + * because it's neither {@link Vehicle.State#IDLE} nor {@link Vehicle.State#CHARGING}). + */ + VEHICLE_STATE_INVALID, + /** + * The {@link Vehicle.IntegrationLevel} of the vehicle to assign the transport order to is invalid + * (e.g. because it's not {@link Vehicle.IntegrationLevel#TO_BE_UTILIZED}). + */ + VEHICLE_INTEGRATION_LEVEL_INVALID, + /** + * The current position of the vehicle to assign the transport order to is unknown. + */ + VEHICLE_CURRENT_POSITION_UNKNOWN, + /** + * The vehicle to assign the transport order to is processing an {@link OrderSequence}. + */ + VEHICLE_PROCESSING_ORDER_SEQUENCE, + /** + * A generic (dispatcher implementation-specific) reason that prevents the transport order + * assignment. + */ + GENERIC_VETO; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/package-info.java new file mode 100644 index 0000000..df4bf5a --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Interfaces for pluggable strategies and other components for a kernel application. + */ +package org.opentcs.components.kernel; diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/routing/Edge.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/routing/Edge.java new file mode 100644 index 0000000..c20950d --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/routing/Edge.java @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.routing; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import org.opentcs.data.model.Path; + +/** + * A wrapper for {@link Path}s that can be used to build routing graphs. + */ +public class Edge { + + /** + * The path in the model that is traversed on this edge. + */ + private final Path path; + /** + * Whether the path is travelled in reverse direction. + */ + private final boolean travellingReverse; + + /** + * Creates a new instance. + * + * @param modelPath The path in the model that is traversed on this edge. + * @param travellingReverse Whether the path is travelled in reverse direction. + */ + public Edge( + @Nonnull + Path modelPath, + boolean travellingReverse + ) { + this.path = requireNonNull(modelPath, "modelPath"); + this.travellingReverse = travellingReverse; + } + + /** + * Returns the path in the model that is traversed on this edge. + * + * @return The path in the model that is traversed on this edge. + */ + public Path getPath() { + return path; + } + + /** + * Indicates whether the path is travelled in reverse direction. + * + * @return Whether the path is travelled in reverse direction. + */ + public boolean isTravellingReverse() { + return travellingReverse; + } + + /** + * Returns the source vertex of this edge. + * + * @return The source vertex of this edge. + */ + public String getSourceVertex() { + return isTravellingReverse() + ? path.getDestinationPoint().getName() + : path.getSourcePoint().getName(); + } + + /** + * Returns the target vertex of this edge. + * + * @return The target vertex of this edge. + */ + public String getTargetVertex() { + return isTravellingReverse() + ? path.getSourcePoint().getName() + : path.getDestinationPoint().getName(); + } + + @Override + public String toString() { + return "Edge{" + + "path=" + path + ", " + + "travellingReverse=" + travellingReverse + ", " + + "sourceVertex=" + getSourceVertex() + ", " + + "targetVertex=" + getTargetVertex() + + '}'; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/routing/EdgeEvaluator.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/routing/EdgeEvaluator.java new file mode 100644 index 0000000..e0e14ce --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/routing/EdgeEvaluator.java @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.routing; + +import jakarta.annotation.Nonnull; +import org.opentcs.data.model.Vehicle; + +/** + * Computes the weight of edges in the routing graph. + */ +public interface EdgeEvaluator { + + /** + * Called when/before computation of a routing graph starts. + * + * @param vehicle The vehicle for which the routing graph is computed. + */ + void onGraphComputationStart( + @Nonnull + Vehicle vehicle + ); + + /** + * Called when/after a computation of a routing graph is done. + * + * @param vehicle The vehicle for which the routing graph is computed. + */ + void onGraphComputationEnd( + @Nonnull + Vehicle vehicle + ); + + /** + * Computes the weight of an edge in the routing graph. + * + * @param edge The edge. + * @param vehicle The vehicle for which to compute the edge's weight. + * @return The computed weight of the given edge. + * A value of {@code Double.POSITIVE_INFINITY} indicates that the edge is to be excluded from + * routing. + * Note that negative weights might not be handled well by the respective routing algorithm used. + */ + double computeWeight( + @Nonnull + Edge edge, + @Nonnull + Vehicle vehicle + ); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/routing/GroupMapper.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/routing/GroupMapper.java new file mode 100644 index 0000000..7aeb349 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/routing/GroupMapper.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.routing; + +import java.util.function.Function; +import org.opentcs.data.model.Vehicle; + +/** + * Determines the routing group for a {@link Vehicle} instance. + */ +public interface GroupMapper + extends + Function { +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/DispatcherService.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/DispatcherService.java new file mode 100644 index 0000000..e47d663 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/DispatcherService.java @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.services; + +import jakarta.annotation.Nonnull; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.components.kernel.Dispatcher; +import org.opentcs.components.kernel.dipatching.TransportOrderAssignmentException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.order.TransportOrder.State; + +/** + * Provides methods concerning the {@link Dispatcher}. + */ +public interface DispatcherService { + + /** + * Explicitly trigger the dispatching process. + *

+ * If called within the kernel application, this method is supposed to be called only on the + * kernel executor thread. + *

+ * + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void dispatch() + throws KernelRuntimeException; + + /** + * Withdraw any order that a vehicle might be processing. + *

+ * If called within the kernel application, this method is supposed to be called only on the + * kernel executor thread. + *

+ * + * @param ref A reference to the vehicle to be modified. + * @param immediateAbort If {@code false}, this method once will initiate the withdrawal, leaving + * the transport order assigned to the vehicle until it has finished the movements that it has + * already been ordered to execute. The transport order's state will change to + * {@link State#WITHDRAWN}. If {@code true}, the dispatcher will withdraw the order from the + * vehicle without further waiting. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void withdrawByVehicle(TCSObjectReference ref, boolean immediateAbort) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Withdraw the referenced order. + *

+ * If called within the kernel application, this method is supposed to be called only on the + * kernel executor thread. + *

+ * + * @param ref A reference to the transport order to be withdrawn. + * @param immediateAbort If {@code false}, this method once will initiate the withdrawal, leaving + * the transport order assigned to the vehicle until it has finished the movements that it has + * already been ordered to execute. The transport order's state will change to + * {@link State#WITHDRAWN}. If {@code true}, the dispatcher will withdraw the order from the + * vehicle without further waiting. + * @throws ObjectUnknownException If the referenced transport order does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void withdrawByTransportOrder(TCSObjectReference ref, boolean immediateAbort) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Explicitly trigger a rerouting for the given vehicles. + *

+ * If called within the kernel application, this method is supposed to be called only on the + * kernel executor thread. + *

+ * + * @param ref The vehicle to be rerouted. + * @param reroutingType The type of the requested rerouting. + */ + void reroute( + @Nonnull + TCSObjectReference ref, + @Nonnull + ReroutingType reroutingType + ) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Explicitly trigger a rerouting for all vehicles. + *

+ * If called within the kernel application, this method is supposed to be called only on the + * kernel executor thread. + *

+ * + * @param reroutingType The type of rerouting. + */ + void rerouteAll( + @Nonnull + ReroutingType reroutingType + ); + + /** + * Assign the referenced transport order (to its intended vehicle) now. + *

+ * If called within the kernel application, this method is supposed to be called only on the + * kernel executor thread. + *

+ * + * @param ref The transport order to be assigned. + * @throws ObjectUnknownException If the referenced transport order does not exist. + * @throws TransportOrderAssignmentException If the given transport order could not be assigned + * to its intended vehicle. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void assignNow(TCSObjectReference ref) + throws ObjectUnknownException, + TransportOrderAssignmentException, + KernelRuntimeException; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/InternalPeripheralJobService.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/InternalPeripheralJobService.java new file mode 100644 index 0000000..139467f --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/InternalPeripheralJobService.java @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.services; + +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.peripherals.PeripheralJob; + +/** + * Declares the methods the peripheral job service must provide which are not accessible to remote + * peers. + */ +public interface InternalPeripheralJobService + extends + PeripheralJobService { + + /** + * Updates a peripheral job's state. + * Note that peripheral job states are intended to be manipulated by the peripheral job + * dispatcher only. + * Calling this method from any other parts of the kernel may result in undefined behaviour. + * + * @param ref A reference to the peripheral job to be modified. + * @param state The peripheral job's new state. + * @throws ObjectUnknownException If the referenced peripheral job does not exist. + */ + void updatePeripheralJobState(TCSObjectReference ref, PeripheralJob.State state) + throws ObjectUnknownException; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/InternalPeripheralService.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/InternalPeripheralService.java new file mode 100644 index 0000000..179c623 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/InternalPeripheralService.java @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.services; + +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.PeripheralInformation; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.peripherals.PeripheralJob; + +/** + * Declares the methods the peripheral service must provide which are not accessible to remote + * peers. + */ +public interface InternalPeripheralService + extends + PeripheralService { + + /** + * Updates a peripheral's processing state. + * + * @param ref A reference to the location to be modified. + * @param state The peripheral's new processing state. + * @throws ObjectUnknownException If the referenced location does not exist. + */ + void updatePeripheralProcState( + TCSResourceReference ref, + PeripheralInformation.ProcState state + ) + throws ObjectUnknownException; + + /** + * Updates a peripheral's reservation token. + * + * @param ref A reference to the location to be modified. + * @param reservationToken The peripheral's new reservation token. + * @throws ObjectUnknownException If the referenced location does not exist. + */ + void updatePeripheralReservationToken( + TCSResourceReference ref, + String reservationToken + ) + throws ObjectUnknownException; + + /** + * Updates a peripheral's state. + * + * @param ref A reference to the location to be modified. + * @param state The peripheral's new state. + * @throws ObjectUnknownException If the referenced location does not exist. + */ + void updatePeripheralState( + TCSResourceReference ref, + PeripheralInformation.State state + ) + throws ObjectUnknownException; + + /** + * Updates a peripheral's current peripheral job. + * + * @param ref A reference to the location to be modified. + * @param peripheralJob A reference to the peripheral job the peripheral device processes. + * @throws ObjectUnknownException If the referenced location does not exist. + */ + void updatePeripheralJob( + TCSResourceReference ref, + TCSObjectReference peripheralJob + ) + throws ObjectUnknownException; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/InternalPlantModelService.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/InternalPlantModelService.java new file mode 100644 index 0000000..5455f3c --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/InternalPlantModelService.java @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.services; + +import java.util.Set; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.model.TCSResource; +import org.opentcs.data.model.TCSResourceReference; + +/** + * Declares the methods the plant model service must provide which are not accessible to remote + * peers. + */ +public interface InternalPlantModelService + extends + PlantModelService { + + /** + * Expands a set of resources A to a set of resources B. + * B contains the resources in A with blocks expanded to their actual members. + * The given set is not modified. + * + * @param resources The set of resources to be expanded. + * @return The given set with resources expanded. + * @throws ObjectUnknownException If any of the referenced objects does not exist. + */ + Set> expandResources(Set> resources) + throws ObjectUnknownException; + + /** + * Loads the saved model into the kernel. + * If there is no saved model, a new empty model will be loaded. + * + * @throws IllegalStateException If the model cannot be loaded. + */ + void loadPlantModel() + throws IllegalStateException; + + /** + * Saves the current model under the given name. + * If there is a saved model, it will be overwritten. + * + * @throws IllegalStateException If the model could not be persisted for some reason. + */ + void savePlantModel() + throws IllegalStateException; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/InternalQueryService.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/InternalQueryService.java new file mode 100644 index 0000000..c0f7a16 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/InternalQueryService.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.services; + +import jakarta.annotation.Nonnull; +import org.opentcs.components.kernel.Query; +import org.opentcs.components.kernel.QueryResponder; + +/** + * Declares query-related methods not accessible to remote peers. + */ +public interface InternalQueryService + extends + QueryService { + + /** + * Registers the given responder for handling queries of the given type. + * + * @param clazz The query type. + * @param responder The responder to handle the queries. + */ + void registerResponder( + @Nonnull + Class> clazz, + @Nonnull + QueryResponder responder + ); + + /** + * Unregisters the responder for the given type. + * + * @param clazz The query type. + */ + void unregisterResponder( + @Nonnull + Class> clazz + ); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/InternalTransportOrderService.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/InternalTransportOrderService.java new file mode 100644 index 0000000..835ebf6 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/InternalTransportOrderService.java @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.services; + +import java.util.List; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; + +/** + * Declares the methods the transport order service must provide which are not accessible to remote + * peers. + */ +public interface InternalTransportOrderService + extends + TransportOrderService { + + /** + * Sets an order sequence's finished flag. + * + * @param ref A reference to the order sequence to be modified. + * @throws ObjectUnknownException If the referenced transport order is not in this pool. + */ + void markOrderSequenceFinished(TCSObjectReference ref) + throws ObjectUnknownException; + + /** + * Updates an order sequence's finished index. + * + * @param ref A reference to the order sequence to be modified. + * @param index The sequence's new finished index. + * @throws ObjectUnknownException If the referenced transport order is not in this pool. + */ + void updateOrderSequenceFinishedIndex(TCSObjectReference ref, int index) + throws ObjectUnknownException; + + /** + * Updates an order sequence's processing vehicle. + * + * @param seqRef A reference to the order sequence to be modified. + * @param vehicleRef A reference to the vehicle processing the order sequence. + * @throws ObjectUnknownException If the referenced transport order is not in this pool. + */ + void updateOrderSequenceProcessingVehicle( + TCSObjectReference seqRef, + TCSObjectReference vehicleRef + ) + throws ObjectUnknownException; + + /** + * Updates a transport order's list of drive orders. + * + * @param ref A reference to the transport order to be modified. + * @param driveOrders The drive orders containing the data to be copied into this transport + * order's drive orders. + * @throws ObjectUnknownException If the referenced transport order does not exist. + */ + void updateTransportOrderDriveOrders( + TCSObjectReference ref, + List driveOrders + ) + throws ObjectUnknownException; + + /** + * Updates a transport order's current drive order. + * Marks the current drive order as finished, adds it to the list of past drive orders and sets + * the current drive order to the next one of the list of future drive orders (or {@code null}, + * if that list is empty). + * Also implicitly sets the {@link TransportOrder#getCurrentRouteStepIndex() transport order's + * current route step index} to {@link TransportOrder#ROUTE_STEP_INDEX_DEFAULT}. + * If the current drive order is {@code null} because all drive orders have been finished + * already or none has been started, yet, nothing happens. + * + * @param ref A reference to the transport order to be modified. + * @throws ObjectUnknownException If the referenced transport order is not in this pool. + */ + void updateTransportOrderNextDriveOrder(TCSObjectReference ref) + throws ObjectUnknownException; + + /** + * Updates a transport order's index of the last route step travelled for the currently processed + * drive order. + * + * @param ref A reference to the transport order to be modified. + * @param index The new index. + * @throws ObjectUnknownException If the referenced transport order does not exist. + */ + void updateTransportOrderCurrentRouteStepIndex( + TCSObjectReference ref, + int index + ) + throws ObjectUnknownException; + + /** + * Updates a transport order's processing vehicle. + * + * @param orderRef A reference to the transport order to be modified. + * @param vehicleRef A reference to the vehicle processing the order. + * @param driveOrders The drive orders containing the data to be copied into this transport + * order's drive orders. + * @throws ObjectUnknownException If the referenced transport order does not exist. + * @throws IllegalArgumentException If the destinations of the given drive orders do not match + * the destinations of the drive orders in this transport order. + */ + void updateTransportOrderProcessingVehicle( + TCSObjectReference orderRef, + TCSObjectReference vehicleRef, + List driveOrders + ) + throws ObjectUnknownException, + IllegalArgumentException; + + /** + * Updates a transport order's state. + * Note that transport order states are intended to be manipulated by the dispatcher only. + * Calling this method from any other parts of the kernel may result in undefined behaviour. + * + * @param ref A reference to the transport order to be modified. + * @param state The transport order's new state. + * @throws ObjectUnknownException If the referenced transport order does not exist. + */ + void updateTransportOrderState( + TCSObjectReference ref, + TransportOrder.State state + ) + throws ObjectUnknownException; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/InternalVehicleService.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/InternalVehicleService.java new file mode 100644 index 0000000..abdef64 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/InternalVehicleService.java @@ -0,0 +1,232 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.services; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Set; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.BoundingBox; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Pose; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Triple; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.drivers.vehicle.LoadHandlingDevice; +import org.opentcs.util.annotations.ScheduledApiChange; + +/** + * Declares the methods the vehicle service must provide which are not accessible to remote peers. + */ +public interface InternalVehicleService + extends + VehicleService { + + /** + * Updates a vehicle's energy level. + * + * @param ref A reference to the vehicle to be modified. + * @param energyLevel The vehicle's new energy level. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + void updateVehicleEnergyLevel(TCSObjectReference ref, int energyLevel) + throws ObjectUnknownException; + + /** + * Updates a vehicle's load handling devices. + * + * @param ref A reference to the vehicle to be modified. + * @param devices The vehicle's new load handling devices. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + void updateVehicleLoadHandlingDevices( + TCSObjectReference ref, + List devices + ) + throws ObjectUnknownException; + + /** + * Updates the point which a vehicle is expected to occupy next. + * + * @param vehicleRef A reference to the vehicle to be modified. + * @param pointRef A reference to the point which the vehicle is expected to occupy next. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + void updateVehicleNextPosition( + TCSObjectReference vehicleRef, + TCSObjectReference pointRef + ) + throws ObjectUnknownException; + + /** + * Updates a vehicle's order sequence. + * + * @param vehicleRef A reference to the vehicle to be modified. + * @param sequenceRef A reference to the order sequence the vehicle processes. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + void updateVehicleOrderSequence( + TCSObjectReference vehicleRef, + TCSObjectReference sequenceRef + ) + throws ObjectUnknownException; + + /** + * Updates the vehicle's current orientation angle (-360..360 degrees, or {@link Double#NaN}, if + * the vehicle doesn't provide an angle). + * + * @param ref A reference to the vehicle to be modified. + * @param angle The vehicle's orientation angle. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + * @deprecated Use {@link #updateVehiclePose(TCSObjectReference,Pose)} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + void updateVehicleOrientationAngle(TCSObjectReference ref, double angle) + throws ObjectUnknownException; + + /** + * Places a vehicle on a point. + * + * @param vehicleRef A reference to the vehicle to be modified. + * @param pointRef A reference to the point on which the vehicle is to be placed. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + void updateVehiclePosition( + TCSObjectReference vehicleRef, + TCSObjectReference pointRef + ) + throws ObjectUnknownException; + + /** + * Updates the vehicle's current precise position in mm. + * + * @param ref A reference to the vehicle to be modified. + * @param position The vehicle's precise position in mm. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + * @deprecated Use {@link #updateVehiclePose(TCSObjectReference,Pose)} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + void updateVehiclePrecisePosition(TCSObjectReference ref, Triple position) + throws ObjectUnknownException; + + /** + * Updates the vehicle's pose. + * + * @param ref A reference to the vehicle to be modified. + * @param pose The vehicle's new pose. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + @ScheduledApiChange(when = "7.0", details = "Default implementation will be removed.") + default void updateVehiclePose(TCSObjectReference ref, Pose pose) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(pose, "pose"); + + updateVehiclePrecisePosition(ref, pose.getPosition()); + updateVehicleOrientationAngle(ref, pose.getOrientationAngle()); + } + + /** + * Updates a vehicle's processing state. + * + * @param ref A reference to the vehicle to be modified. + * @param state The vehicle's new processing state. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + void updateVehicleProcState(TCSObjectReference ref, Vehicle.ProcState state) + throws ObjectUnknownException; + + /** + * Updates a vehicle's recharge operation. + * + * @param ref A reference to the vehicle to be modified. + * @param rechargeOperation The vehicle's new recharge action. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + void updateVehicleRechargeOperation(TCSObjectReference ref, String rechargeOperation) + throws ObjectUnknownException; + + /** + * Updates a vehicle's claimed resources. + * + * @param ref A reference to the vehicle to be modified. + * @param resources The new resources. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + void updateVehicleClaimedResources( + TCSObjectReference ref, + List>> resources + ) + throws ObjectUnknownException; + + /** + * Updates a vehicle's allocated resources. + * + * @param ref A reference to the vehicle to be modified. + * @param resources The new resources. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + void updateVehicleAllocatedResources( + TCSObjectReference ref, + List>> resources + ) + throws ObjectUnknownException; + + /** + * Updates a vehicle's state. + * + * @param ref A reference to the vehicle to be modified. + * @param state The vehicle's new state. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + void updateVehicleState(TCSObjectReference ref, Vehicle.State state) + throws ObjectUnknownException; + + /** + * Updates a vehicle's length. + * + * @param ref A reference to the vehicle to be modified. + * @param length The vehicle's new length. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + * @deprecated Use {@link #updateVehicleBoundingBox(TCSObjectReference, BoundingBox)} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + void updateVehicleLength(TCSObjectReference ref, int length) + throws ObjectUnknownException; + + /** + * Updates the vehicle's bounding box. + * + * @param ref A reference to the vehicle. + * @param boundingBox The vehicle's new bounding box (in mm). + * @throws ObjectUnknownException If the referenced vehicle does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + @ScheduledApiChange(when = "7.0", details = "Default implementation will be removed.") + default void updateVehicleBoundingBox(TCSObjectReference ref, BoundingBox boundingBox) + throws ObjectUnknownException, + KernelRuntimeException { + throw new UnsupportedOperationException("Not yet implemented."); + } + + /** + * Updates a vehicle's transport order. + * + * @param vehicleRef A reference to the vehicle to be modified. + * @param orderRef A reference to the transport order the vehicle processes. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + void updateVehicleTransportOrder( + TCSObjectReference vehicleRef, + TCSObjectReference orderRef + ) + throws ObjectUnknownException; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/NotificationService.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/NotificationService.java new file mode 100644 index 0000000..65aefc9 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/NotificationService.java @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.services; + +import java.util.List; +import java.util.function.Predicate; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.data.notification.UserNotification; + +/** + * Provides methods concerning {@link UserNotification}s. + */ +public interface NotificationService { + + /** + * Returns a list of user notifications. + * + * @param predicate A filter predicate that accepts the user notifications to be returned. May be + * {@code null} to return all existing user notifications. + * @return A list of user notifications, in the order in which they were published. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + List fetchUserNotifications(Predicate predicate) + throws KernelRuntimeException; + + /** + * Publishes a user notification. + * + * @param notification The notification to be published. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void publishUserNotification(UserNotification notification) + throws KernelRuntimeException; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/PeripheralDispatcherService.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/PeripheralDispatcherService.java new file mode 100644 index 0000000..20ba0a0 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/PeripheralDispatcherService.java @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.services; + +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.components.kernel.PeripheralJobDispatcher; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.peripherals.PeripheralJob; + +/** + * Provides methods concerning the {@link PeripheralJobDispatcher}. + */ +public interface PeripheralDispatcherService { + + /** + * Explicitly trigger the dispatching process for peripheral jobs. + *

+ * If called within the kernel application, this method is supposed to be called only on the + * kernel executor thread. + *

+ * + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void dispatch() + throws KernelRuntimeException; + + /** + * Withdraw any job that a peripheral device (represented by the given location) might be + * processing. + *

+ * If called within the kernel application, this method is supposed to be called only on the + * kernel executor thread. + *

+ * + * @param ref A reference to the location representing the peripheral device. + * @throws ObjectUnknownException If the referenced location does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void withdrawByLocation(TCSResourceReference ref) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Withdraw the given peripheral job. + *

+ * If called within the kernel application, this method is supposed to be called only on the + * kernel executor thread. + *

+ * + * @param ref A reference to the peripheral job to be withdrawn. + * @throws ObjectUnknownException If the referenced peripheral job does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void withdrawByPeripheralJob(TCSObjectReference ref) + throws ObjectUnknownException, + KernelRuntimeException; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/PeripheralJobService.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/PeripheralJobService.java new file mode 100644 index 0000000..531f1b1 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/PeripheralJobService.java @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.services; + +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.to.peripherals.PeripheralJobCreationTO; +import org.opentcs.data.ObjectExistsException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.peripherals.PeripheralJob; + +/** + * Provides methods concerning {@link PeripheralJob}s. + */ +public interface PeripheralJobService + extends + TCSObjectService { + + /** + * Creates a peripheral job. + * A new peripheral job is created with a generated unique ID and all other attributes taken from + * the given transfer object. + * A copy of the newly created transport order is then returned. + * + * @param to Describes the peripheral job to be created. + * @return A copy of the newly created peripheral job. + * @throws ObjectUnknownException If any referenced object does not exist. + * @throws ObjectExistsException If an object with the same name already exists in the model. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + PeripheralJob createPeripheralJob(PeripheralJobCreationTO to) + throws ObjectUnknownException, + ObjectExistsException, + KernelRuntimeException; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/PeripheralService.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/PeripheralService.java new file mode 100644 index 0000000..c7ef657 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/PeripheralService.java @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.services; + +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.drivers.peripherals.PeripheralAdapterCommand; +import org.opentcs.drivers.peripherals.PeripheralCommAdapter; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; +import org.opentcs.drivers.peripherals.PeripheralProcessModel; +import org.opentcs.drivers.peripherals.management.PeripheralAttachmentInformation; + +/** + * Provides methods concerning peripheral devices represented by {@link Location}s. + */ +public interface PeripheralService + extends + TCSObjectService { + + /** + * Attaches the described comm adapter to the referenced location. + * + * @param ref A reference to the location. + * @param description The description for the comm adapter to be attached. + * @throws ObjectUnknownException If the referenced location does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void attachCommAdapter( + TCSResourceReference ref, + PeripheralCommAdapterDescription description + ) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Disables the comm adapter attached to the referenced location. + * + * @param ref A reference to the location the comm adapter is attached to. + * @throws ObjectUnknownException If the referenced location does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void disableCommAdapter(TCSResourceReference ref) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Enables the comm adapter attached to the referenced location. + * + * @param ref A reference to the location the comm adapter is attached to. + * @throws ObjectUnknownException If the referenced location does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void enableCommAdapter(TCSResourceReference ref) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Returns attachment information for the referenced location. + * + * @param ref A reference to the location. + * @return The attachment information. + * @throws ObjectUnknownException If the referenced location does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + PeripheralAttachmentInformation fetchAttachmentInformation(TCSResourceReference ref) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Returns the process model for the referenced location. + * + * @param ref A reference to the location. + * @return The process model. + * @throws ObjectUnknownException If the referenced location does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + PeripheralProcessModel fetchProcessModel(TCSResourceReference ref) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Sends a {@link PeripheralAdapterCommand} to the comm adapter attached to the referenced + * location. + *

+ * If called within the kernel application, this method is supposed to be called only on the + * kernel executor thread. + *

+ * + * @see PeripheralAdapterCommand#execute(PeripheralCommAdapter) + * @param ref A reference to the location. + * @param command The adapter command to send. + * @throws ObjectUnknownException If the referenced location does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void sendCommAdapterCommand(TCSResourceReference ref, PeripheralAdapterCommand command) + throws ObjectUnknownException, + KernelRuntimeException; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/PlantModelService.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/PlantModelService.java new file mode 100644 index 0000000..d3dee79 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/PlantModelService.java @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.services; + +import java.util.Map; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.data.ObjectExistsException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.PlantModel; + +/** + * Provides methods concerning the plant model. + */ +public interface PlantModelService + extends + TCSObjectService { + + /** + * Returns a representation of the plant model's current state. + * + * @return The complete plant model. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + PlantModel getPlantModel() + throws KernelRuntimeException; + + /** + * Creates a new plant model with the objects described in the given transfer object. + * Implicitly saves/persists the new plant model. + * + * @param to The transfer object describing the plant model objects to be created. + * @throws ObjectUnknownException If any referenced object does not exist. + * @throws ObjectExistsException If an object with the same name already exists in the model. + * @throws KernelRuntimeException In case there is an exception executing this method. + * @throws IllegalStateException If there was a problem persisting the model. + */ + void createPlantModel(PlantModelCreationTO to) + throws ObjectUnknownException, + ObjectExistsException, + KernelRuntimeException, + IllegalStateException; + + /** + * Returns the name of the model that is currently loaded in the kernel. + * + * @return The name of the currently loaded model. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + String getModelName() + throws KernelRuntimeException; + + /** + * Returns the model's properties. + * + * @return The model's properties. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + Map getModelProperties() + throws KernelRuntimeException; + + /** + * Updates a location's lock state. + * + * @param ref A reference to the location to be updated. + * @param locked Indicates whether the location is to be locked ({@code true}) or unlocked + * ({@code false}). + * @throws ObjectUnknownException If the referenced location does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void updateLocationLock(TCSObjectReference ref, boolean locked) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Updates a path's lock state. + * + * @param ref A reference to the path to be updated. + * @param locked Indicates whether the path is to be locked ({@code true}) or unlocked + * ({@code false}). + * @throws ObjectUnknownException If the referenced path does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void updatePathLock(TCSObjectReference ref, boolean locked) + throws ObjectUnknownException, + KernelRuntimeException; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/QueryService.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/QueryService.java new file mode 100644 index 0000000..250ca53 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/QueryService.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.services; + +import org.opentcs.components.kernel.Query; + +/** + * Provides generic/pluggable query functionality. + */ +public interface QueryService { + + /** + * Executes a query with the kernel and delivers the result. + * + * @param The query/result type. + * @param query The query/parameter object. + * @return The query result. + */ + T query(Query query); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/RouterService.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/RouterService.java new file mode 100644 index 0000000..de8b126 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/RouterService.java @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.services; + +import java.util.Map; +import java.util.Set; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.components.kernel.Router; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.Route; + +/** + * Provides methods concerning the {@link Router}. + */ +public interface RouterService { + + /** + * Notifies the router that the topology has changed with respect to the given paths and needs to + * be re-evaluated. + *

+ * If called within the kernel application, this method is supposed to be called only on the + * kernel executor thread. + *

+ * + * @param refs References to paths that have changed in the routing topology. An empty set of + * path references results in the router updating the entire routing topology. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void updateRoutingTopology(Set> refs) + throws KernelRuntimeException; + + /** + * Computes routes for the given vehicle from a source point to a set of destination points. + *

+ * If called within the kernel application, this method is supposed to be called only on the + * kernel executor thread. + *

+ * + * @param vehicleRef A reference to the vehicle to calculate the routes for. + * @param sourcePointRef A reference to the source point. + * @param destinationPointRefs A set of references to the destination points. + * @param resourcesToAvoid A set of references to resources that are to be avoided. + * @return A map of destination points to the corresponding computed routes or {@code null}, if + * no route could be determined for a specific destination point. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + Map, Route> computeRoutes( + TCSObjectReference vehicleRef, + TCSObjectReference sourcePointRef, + Set> destinationPointRefs, + Set> resourcesToAvoid + ) + throws KernelRuntimeException; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/ServiceUnavailableException.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/ServiceUnavailableException.java new file mode 100644 index 0000000..4603ab3 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/ServiceUnavailableException.java @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.services; + +import org.opentcs.access.KernelRuntimeException; + +/** + * Thrown when a (remote) service is not available for processing a request. + */ +public class ServiceUnavailableException + extends + KernelRuntimeException { + + /** + * Creates a new ServiceUnavailableException with the given detail message. + * + * @param message The detail message. + */ + public ServiceUnavailableException(String message) { + super(message); + } + + /** + * Creates a new ServiceUnavailableException with the given detail message and cause. + * + * @param message The detail message. + * @param cause The cause. + */ + public ServiceUnavailableException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/TCSObjectService.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/TCSObjectService.java new file mode 100644 index 0000000..1474617 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/TCSObjectService.java @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.services; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.Set; +import java.util.function.Predicate; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectReference; + +/** + * Provides methods concerning {@link TCSObject}s. + */ +public interface TCSObjectService { + + /** + * Returns a single {@link TCSObject} of the given class. + * + * @param The TCSObject's actual type. + * @param clazz The class of the object to be returned. + * @param ref A reference to the object to be returned. + * @return A copy of the referenced object, or {@code null} if no such object exists or if an + * object exists but is not an instance of the given class. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + > T fetchObject(Class clazz, TCSObjectReference ref) + throws KernelRuntimeException; + + /** + * Returns a single {@link TCSObject} of the given class. + * + * @param The TCSObject's actual type. + * @param clazz The class of the object to be returned. + * @param name The name of the object to be returned. + * @return A copy of the named object, or {@code null} if no such object exists or if an object + * exists but is not an instance of the given class. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + > T fetchObject(Class clazz, String name) + throws KernelRuntimeException; + + /** + * Returns all existing {@link TCSObject}s of the given class. + * + * @param The TCSObjects' actual type. + * @param clazz The class of the objects to be returned. + * @return Copies of all existing objects of the given class. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + > Set fetchObjects(Class clazz) + throws KernelRuntimeException; + + /** + * Returns all existing {@link TCSObject}s of the given class for which the given predicate is + * true. + * + * @param The TCSObjects' actual type. + * @param clazz The class of the objects to be returned. + * @param predicate The predicate that must be true for returned objects. + * @return Copies of all existing objects of the given class for which the given predicate is + * true. If no such objects exist, the returned set will be empty. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + > Set fetchObjects( + @Nonnull + Class clazz, + @Nonnull + Predicate predicate + ) + throws KernelRuntimeException; + + /** + * Updates a {@link TCSObject}'s property. + * + * @param ref A reference to the TCSObject to be modified. + * @param key The property's key. + * @param value The property's (new) value. If {@code null}, removes the property from the object. + * @throws ObjectUnknownException If the referenced object does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void updateObjectProperty( + TCSObjectReference ref, + String key, + @Nullable + String value + ) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Appends a history entry to a {@link TCSObject}. + * + * @param ref A reference to the TCSObject to be modified. + * @param entry The history entry to be appended. + * @throws ObjectUnknownException If the referenced object does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void appendObjectHistoryEntry(TCSObjectReference ref, ObjectHistory.Entry entry) + throws ObjectUnknownException, + KernelRuntimeException; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/TransportOrderService.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/TransportOrderService.java new file mode 100644 index 0000000..961d113 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/TransportOrderService.java @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.services; + +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.to.order.OrderSequenceCreationTO; +import org.opentcs.access.to.order.TransportOrderCreationTO; +import org.opentcs.data.ObjectExistsException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; + +/** + * Provides methods concerning {@link TransportOrder}s and {@link OrderSequence}s. + */ +public interface TransportOrderService + extends + TCSObjectService { + + /** + * Creates a new order sequence. + * A new order sequence is created with a generated unique ID and all other attributes taken from + * the given transfer object. + * A copy of the newly created order sequence is then returned. + * + * @param to Describes the order sequence to be created. + * @return A copy of the newly created order sequence. + * @throws ObjectUnknownException If any referenced object does not exist. + * @throws ObjectExistsException If an object with the same name already exists in the model. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + OrderSequence createOrderSequence(OrderSequenceCreationTO to) + throws ObjectUnknownException, + ObjectExistsException, + KernelRuntimeException; + + /** + * Creates a new transport order. + * A new transport order is created with a generated unique ID and all other attributes taken from + * the given transfer object. + * This method also implicitly adds the transport order to its wrapping sequence, if any. + * A copy of the newly created transport order is then returned. + * + * @param to Describes the transport order to be created. + * @return A copy of the newly created transport order. + * @throws ObjectUnknownException If any referenced object does not exist. + * @throws ObjectExistsException If an object with the same name already exists in the model. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + TransportOrder createTransportOrder(TransportOrderCreationTO to) + throws ObjectUnknownException, + ObjectExistsException, + KernelRuntimeException; + + /** + * Marks an order sequence as complete by setting its complete flag. + * + * @param ref A reference to the order sequence to be modified. + * @throws ObjectUnknownException If the referenced order sequence does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void markOrderSequenceComplete(TCSObjectReference ref) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Updates a transport order's intended vehicle. + * + * @param orderRef A reference to the transport order to be modified. + * @param vehicleRef A reference to the vehicle that is intended for the transport order. + * @throws ObjectUnknownException If the referenced transport order does not exist or + * if the vehicle does not exist. + * @throws IllegalArgumentException If the transport order has already being assigned to + * a vehicle. + */ + void updateTransportOrderIntendedVehicle( + TCSObjectReference orderRef, + TCSObjectReference vehicleRef + ) + throws ObjectUnknownException, + IllegalArgumentException; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/VehicleService.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/VehicleService.java new file mode 100644 index 0000000..4dae8de --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/VehicleService.java @@ -0,0 +1,201 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernel.services; + +import java.util.Set; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.model.Vehicle.EnergyLevelThresholdSet; +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; +import org.opentcs.drivers.vehicle.management.VehicleAttachmentInformation; +import org.opentcs.drivers.vehicle.management.VehicleProcessModelTO; +import org.opentcs.util.annotations.ScheduledApiChange; + +/** + * Provides methods concerning {@link Vehicle}s. + */ +public interface VehicleService + extends + TCSObjectService { + + /** + * Attaches the described comm adapter to the referenced vehicle. + * + * @param ref A reference to the vehicle. + * @param description The description for the comm adapter to be attached. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void attachCommAdapter( + TCSObjectReference ref, + VehicleCommAdapterDescription description + ) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Disables the comm adapter attached to the referenced vehicle. + * + * @param ref A reference to the vehicle the comm adapter is attached to. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void disableCommAdapter(TCSObjectReference ref) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Enables the comm adapter attached to the referenced vehicle. + * + * @param ref A reference to the vehicle the comm adapter is attached to. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void enableCommAdapter(TCSObjectReference ref) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Returns attachment information for the referenced vehicle. + * + * @param ref A reference to the vehicle. + * @return The attachment information. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + VehicleAttachmentInformation fetchAttachmentInformation(TCSObjectReference ref) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Returns the process model for the referenced vehicle. + * + * @param ref A reference to the vehicle. + * @return The process model. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + VehicleProcessModelTO fetchProcessModel(TCSObjectReference ref) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Sends an {@link AdapterCommand} to the comm adapter attached to the referenced vehicle. + *

+ * If called within the kernel application, this method is supposed to be called only on the + * kernel executor thread. + *

+ * + * @param ref A reference to the vehicle. + * @param command The adapter command to send. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + * @see VehicleCommAdapter#execute(AdapterCommand) + */ + void sendCommAdapterCommand(TCSObjectReference ref, AdapterCommand command) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Sends a message to the communication adapter associated with the referenced vehicle. + * This method provides a generic one-way communication channel to the communication adapter of a + * vehicle. Note that there is no return value and no guarantee that the communication adapter + * will understand the message; clients cannot even know which communication adapter is attached + * to a vehicle, so it's entirely possible that the communication adapter receiving the message + * does not understand it. + * + * @param ref The vehicle whose communication adapter shall receive the message. + * @param message The message to be delivered. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + * @throws KernelRuntimeException If the calling client is not allowed to execute this method. + * @see VehicleCommAdapter#processMessage(java.lang.Object) + */ + void sendCommAdapterMessage(TCSObjectReference ref, Object message) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Updates the vehicle's integration level. + * + * @param ref A reference to the vehicle. + * @param integrationLevel The vehicle's new integration level. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + * @throws IllegalArgumentException If changing the vehicle's integration level to + * {@code integrationLevel} is not allowed from its current integration level. + */ + void updateVehicleIntegrationLevel( + TCSObjectReference ref, + Vehicle.IntegrationLevel integrationLevel + ) + throws ObjectUnknownException, + KernelRuntimeException, + IllegalArgumentException; + + /** + * Updates the vehicle's paused state. + * + * @param ref A reference to the vehicle. + * @param paused The vehicle's new paused state. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void updateVehiclePaused( + TCSObjectReference ref, + boolean paused + ) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Updates the vehicle's energy level threshold set. + * + * @param ref A reference to the vehicle to be modified. + * @param energyLevelThresholdSet The vehicle's new energy level threshold set. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + @ScheduledApiChange(when = "7.0", details = "Default implementation will be removed.") + default void updateVehicleEnergyLevelThresholdSet( + TCSObjectReference ref, + EnergyLevelThresholdSet energyLevelThresholdSet + ) + throws ObjectUnknownException, + KernelRuntimeException { + throw new UnsupportedOperationException("Not yet implemented."); + } + + /** + * Updates the types of transport orders a vehicle is allowed to process. + * + * @param ref A reference to the vehicle to be modified. + * @param allowedOrderTypes A set of transport order types. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void updateVehicleAllowedOrderTypes( + TCSObjectReference ref, + Set allowedOrderTypes + ) + throws ObjectUnknownException, + KernelRuntimeException; + + /** + * Updates the vehicle's envelope key. + * + * @param ref A reference to the vehicle. + * @param envelopeKey The vehicle's new envelope key. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + * @throws IllegalArgumentException If the referenced vehicle is processing a transport order or + * is currently claiming/allocating resources. + * @throws KernelRuntimeException In case there is an exception executing this method. + */ + void updateVehicleEnvelopeKey(TCSObjectReference ref, String envelopeKey) + throws ObjectUnknownException, + IllegalArgumentException, + KernelRuntimeException; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/package-info.java new file mode 100644 index 0000000..4e23204 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernel/services/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Interfaces and classes defining internal and external interfaces for the openTCS kernel. + */ +package org.opentcs.components.kernel.services; diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/kernelcontrolcenter/ControlCenterPanel.java b/opentcs-api-base/src/main/java/org/opentcs/components/kernelcontrolcenter/ControlCenterPanel.java new file mode 100644 index 0000000..e665cfe --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/kernelcontrolcenter/ControlCenterPanel.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.kernelcontrolcenter; + +import javax.swing.JPanel; +import org.opentcs.components.Lifecycle; + +/** + * A panel that can be plugged into the kernel control center. + */ +public abstract class ControlCenterPanel + extends + JPanel + implements + Lifecycle { + + /** + * Returns a title for this panel. + * + * @return A title for this panel. + */ + public String getTitle() { + return getAccessibleContext().getAccessibleName(); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/components/package-info.java new file mode 100644 index 0000000..bc4a8a7 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Interfaces and base classes for exchangeable components of openTCS applications. + */ +package org.opentcs.components; diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/LocationTheme.java b/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/LocationTheme.java new file mode 100644 index 0000000..040ac88 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/LocationTheme.java @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.plantoverview; + +import jakarta.annotation.Nonnull; +import java.awt.Image; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.visualization.LocationRepresentation; + +/** + * Provides a location theme. + */ +public interface LocationTheme { + + /** + * Returns the image for the given location representation. + * + * @param representation The representation for which to return the image. + * @return The image for the given location representation. + */ + @Nonnull + Image getImageFor( + @Nonnull + LocationRepresentation representation + ); + + /** + * Returns the image for the given location (type). + * + * @param location The location to base the image on. + * @param locationType The location type for the location. + * @return The image for the give location. + */ + @Nonnull + Image getImageFor( + @Nonnull + Location location, + @Nonnull + LocationType locationType + ); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/ObjectHistoryEntryFormatter.java b/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/ObjectHistoryEntryFormatter.java new file mode 100644 index 0000000..513a215 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/ObjectHistoryEntryFormatter.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.plantoverview; + +import java.util.Optional; +import java.util.function.Function; +import org.opentcs.data.ObjectHistory; + +/** + * A formatter for {@link ObjectHistory} entries, mapping an entry to a user-presentable string, if + * possible. + * Indicates that it cannot map an entry by returning an empty {@code Optional}. + */ +public interface ObjectHistoryEntryFormatter + extends + Function> { + +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/OrderTypeSuggestions.java b/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/OrderTypeSuggestions.java new file mode 100644 index 0000000..f1d3aba --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/OrderTypeSuggestions.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.plantoverview; + +import java.util.Set; + +/** + * Implementations of this class provide suggestions for transport order types. + */ +public interface OrderTypeSuggestions { + + /** + * Returns a set of types that can be assigned to a transport order. + * + * @return A set of types that can be assigned to a transport order. + */ + Set getTypeSuggestions(); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/PlantModelExporter.java b/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/PlantModelExporter.java new file mode 100644 index 0000000..f49f881 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/PlantModelExporter.java @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.plantoverview; + +import jakarta.annotation.Nonnull; +import java.io.IOException; +import org.opentcs.access.to.model.PlantModelCreationTO; + +/** + * Implementations provide a way to export plant model data, for instance to write it to a file in a + * third-party format or to a database. + */ +public interface PlantModelExporter { + + /** + * Exports the given plant model data. + * + * @param model The plant model data to be exported. + * @throws IOException If there was a problem exporting plant model data. + */ + void exportPlantModel( + @Nonnull + PlantModelCreationTO model + ) + throws IOException; + + /** + * Returns a (localized) short textual description of this importer. + * + * @return A (localized) short textual description of this importer. + */ + @Nonnull + String getDescription(); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/PlantModelImporter.java b/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/PlantModelImporter.java new file mode 100644 index 0000000..f1c7b52 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/PlantModelImporter.java @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.plantoverview; + +import jakarta.annotation.Nonnull; +import java.io.IOException; +import java.util.Optional; +import org.opentcs.access.to.model.PlantModelCreationTO; + +/** + * Implementations provide a way to import plant model data that is read from some external source + * or generated. + */ +public interface PlantModelImporter { + + /** + * Imports (or generates) plant model data. + * + * @return The imported plant model data. May be empty if the user aborted the import. + * @throws IOException If there was a problem importing plant model data. + */ + @Nonnull + Optional importPlantModel() + throws IOException; + + /** + * Returns a (localized) short textual description of this importer. + * + * @return A (localized) short textual description of this importer. + */ + @Nonnull + String getDescription(); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/PluggablePanel.java b/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/PluggablePanel.java new file mode 100644 index 0000000..d0860b0 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/PluggablePanel.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.plantoverview; + +import javax.swing.JPanel; +import org.opentcs.components.Lifecycle; + +/** + * Declares methods that a pluggable panel should provide for the enclosing + * application. + */ +public abstract class PluggablePanel + extends + JPanel + implements + Lifecycle { + +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/PluggablePanelFactory.java b/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/PluggablePanelFactory.java new file mode 100644 index 0000000..24f03da --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/PluggablePanelFactory.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.plantoverview; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.opentcs.access.Kernel; + +/** + * Produces plugin panels to extend an openTCS user interface. + */ +public interface PluggablePanelFactory { + + /** + * Checks whether this factory produces panels that are available in the + * passed Kernel.State. + * + * @param state The kernel state. + * @return true if, and only if, this factory returns panels that + * are available in the passed kernel state. + */ + boolean providesPanel(Kernel.State state); + + /** + * Returns a string describing the factory/the panels provided. + * This should be a short string that can be displayed e.g. as a menu item for + * selecting a factory/plugin panel to be displayed. + * + * @return A string describing the factory/the panels provided. + */ + @Nonnull + String getPanelDescription(); + + /** + * Returns a newly created panel. + * If a reference to the kernel provider has not been set, yet, or has been + * set to null, this method returns null. + * + * @param state The kernel state for which to create the panel. + * @return A newly created panel. + */ + @Nullable + PluggablePanel createPanel(Kernel.State state); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/PropertySuggestions.java b/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/PropertySuggestions.java new file mode 100644 index 0000000..4d2959b --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/PropertySuggestions.java @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.plantoverview; + +import jakarta.annotation.Nonnull; +import java.util.HashSet; +import java.util.Set; + +/** + * Objects implementing this interface provide a set for suggested property keys and values each. + */ +public interface PropertySuggestions { + + /** + * Returns suggested property keys. + * + * @return Suggested property keys. + */ + @Nonnull + Set getKeySuggestions(); + + /** + * Returns suggested property values. + * + * @return Suggested property values. + */ + @Nonnull + Set getValueSuggestions(); + + /** + * Returns suggested property values that are specified for the key. + * + * @param key A key suggestion for which value suggestions are requested. + * @return A set of property value suggestions. + */ + default Set getValueSuggestionsFor(String key) { + return new HashSet<>(); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/VehicleTheme.java b/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/VehicleTheme.java new file mode 100644 index 0000000..98dbdd3 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/VehicleTheme.java @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.components.plantoverview; + +import jakarta.annotation.Nonnull; +import java.awt.Color; +import java.awt.Font; +import java.awt.Image; +import org.opentcs.data.model.Vehicle; + +/** + * Provides a vehicle theme. + */ +public interface VehicleTheme { + + /** + * Returns an image for the given vehicle, disregarding its current state. + * + * @param vehicle The vehicle for which to return the image. + * @return An image for the given vehicle. + */ + Image statelessImage( + @Nonnull + Vehicle vehicle + ); + + /** + * Returns an image for the given vehicle, representing its current state. + * + * @param vehicle The vehicle for which to return the image. + * @return An image for the given vehicle. + */ + Image statefulImage( + @Nonnull + Vehicle vehicle + ); + + /** + * Provides a label that describes this vehicle. + * Usually this is the name of the vehicle or an abbreviation. + * + * @param vehicle The vehicle to provide a label for. + * @return A label that describes the given vehicle. + */ + String label(Vehicle vehicle); + + /** + * Provides the vertical offset of the label relative to the center of the vehicle figure. + * + * @return The horizontal offset. + */ + int labelOffsetX(); + + /** + * Provides the vertical offset of the label relative to the center of the vehicle figure. + * + * @return The vertical offset. + */ + int labelOffsetY(); + + /** + * Provides the color to be used for drawing the label. + * + * @return The color to be used for drawing the label. + */ + Color labelColor(); + + /** + * Provides the font to be used for drawing the label. + * + * @return The font to be used for drawing the label. + */ + Font labelFont(); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/package-info.java new file mode 100644 index 0000000..02ec985 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/components/plantoverview/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Interfaces for pluggable panels, themes and other components for a plant overview application. + */ +package org.opentcs.components.plantoverview; diff --git a/opentcs-api-base/src/main/java/org/opentcs/configuration/ConfigurationBindingProvider.java b/opentcs-api-base/src/main/java/org/opentcs/configuration/ConfigurationBindingProvider.java new file mode 100644 index 0000000..b219477 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/configuration/ConfigurationBindingProvider.java @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.configuration; + +/** + * A provider to get bindings (implementations) for configuration interfaces. + */ +public interface ConfigurationBindingProvider { + + /** + * Returns a binding for a configuration interface. + * + * @param The configuration interface to get an instance for. + * @param prefix Relative path to configuration values. + * @param type The class for {@literal }. + * @return The corresponding binding. + */ + T get(String prefix, Class type); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/configuration/ConfigurationEntry.java b/opentcs-api-base/src/main/java/org/opentcs/configuration/ConfigurationEntry.java new file mode 100644 index 0000000..d58e74a --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/configuration/ConfigurationEntry.java @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.configuration; + +import static java.lang.annotation.ElementType.METHOD; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks an interface's method that provides a configuration value. + */ +@Target({METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ConfigurationEntry { + + /** + * Returns a description for the data type of this configuration key's values. + * + * @return A description for the data type of this configuration key's values. + */ + String type(); + + /** + * Returns a list of paragraphs describing what the key/value configures. + * + * @return A list of paragraphs describing what the key/value configures. + */ + String[] description(); + + /** + * Indicates when changes to the configuration entry's value are applied. + * + * @return A value indicating when changes to the configuration entry's value are applied. + */ + ChangesApplied changesApplied() default ChangesApplied.UNSPECIFIED; + + /** + * Returns the optional ordering key that this entry belongs to (for grouping/sorting of entries). + * + * @return The optional ordering key that this entry belongs to (for grouping/sorting of entries). + */ + String orderKey() default ""; + + /** + * Indicates when changes to the configuration entry's value are applied. + */ + enum ChangesApplied { + /** + * When a configuration change is applied is not explicitly specified. + */ + UNSPECIFIED, + /** + * Changes to the configuration value are picked up when the application is (re)started. + */ + ON_APPLICATION_START, + /** + * Changes to the configuration value are picked up when/after a plant model is loaded. + */ + ON_NEW_PLANT_MODEL, + /** + * Changes to the configuration value are picked up during runtime instantly, without + * requiring an explicit trigger. + */ + INSTANTLY + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/configuration/ConfigurationException.java b/opentcs-api-base/src/main/java/org/opentcs/configuration/ConfigurationException.java new file mode 100644 index 0000000..76657af --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/configuration/ConfigurationException.java @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.configuration; + +/** + * Thrown when a configuration error occured. + */ +public class ConfigurationException + extends + RuntimeException { + + /** + * Constructs a new instance with no detail message. + */ + public ConfigurationException() { + } + + /** + * Constructs a new instance with the specified detail message. + * + * @param message The detail message. + */ + public ConfigurationException(String message) { + super(message); + } + + /** + * Constructs a new instance with the specified detail message and cause. + * + * @param message The detail message. + * @param cause The exception's cause. + */ + public ConfigurationException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new instance with the specified cause and a detail + * message of (cause == null ? null : cause.toString()) (which + * typically contains the class and detail message of cause). + * + * @param cause The exception's cause. + */ + public ConfigurationException(Throwable cause) { + super(cause); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/configuration/ConfigurationPrefix.java b/opentcs-api-base/src/main/java/org/opentcs/configuration/ConfigurationPrefix.java new file mode 100644 index 0000000..85e459e --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/configuration/ConfigurationPrefix.java @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.configuration; + +import static java.lang.annotation.ElementType.TYPE; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks an interface that provides some configuration for a specific class. + */ +@Target({TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ConfigurationPrefix { + + /** + * Returns the name of the class the interface configures. + * + * @return The name of the class the interface configures. + */ + String value(); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/configuration/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/configuration/package-info.java new file mode 100644 index 0000000..5dca0a9 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/configuration/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Configuration-related interfaces and annotations. + */ +package org.opentcs.configuration; diff --git a/opentcs-api-base/src/main/java/org/opentcs/customizations/ApplicationEventBus.java b/opentcs-api-base/src/main/java/org/opentcs/customizations/ApplicationEventBus.java new file mode 100644 index 0000000..b357c5b --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/customizations/ApplicationEventBus.java @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.customizations; + +import jakarta.inject.Qualifier; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.opentcs.util.event.EventBus; + +/** + * Annotation type to mark a single application-wide injectable {@link EventBus}. + */ +@Qualifier +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApplicationEventBus { +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/customizations/ApplicationHome.java b/opentcs-api-base/src/main/java/org/opentcs/customizations/ApplicationHome.java new file mode 100644 index 0000000..4ed9f51 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/customizations/ApplicationHome.java @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.customizations; + +import jakarta.inject.Qualifier; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A binding annotation that can be used to have the application's home directory injected. + * In classes participating in dependency injection, annotate an injected java.io.File + * with this annotation to get a reference to the application's home directory. + *

+ * Example: + *

+ *
+ * public MyClass(@ApplicationHome File applicationHome) { ... }
+ * 
+ */ +@Qualifier +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApplicationHome { +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/customizations/ServiceCallWrapper.java b/opentcs-api-base/src/main/java/org/opentcs/customizations/ServiceCallWrapper.java new file mode 100644 index 0000000..e331e1a --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/customizations/ServiceCallWrapper.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.customizations; + +import jakarta.inject.Qualifier; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.opentcs.util.CallWrapper; + +/** + * Annotation type to mark an injectable {@link CallWrapper} that wraps method calls on kernel + * services. + */ +@Qualifier +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ServiceCallWrapper { +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/customizations/controlcenter/ActiveInModellingMode.java b/opentcs-api-base/src/main/java/org/opentcs/customizations/controlcenter/ActiveInModellingMode.java new file mode 100644 index 0000000..0fc6144 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/customizations/controlcenter/ActiveInModellingMode.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.customizations.controlcenter; + +import jakarta.inject.Qualifier; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation type for binding injectable components meant to be used in modelling mode. + */ +@Qualifier +@Target(value = {ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) +@Retention(value = RetentionPolicy.RUNTIME) +public @interface ActiveInModellingMode { +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/customizations/controlcenter/ActiveInOperatingMode.java b/opentcs-api-base/src/main/java/org/opentcs/customizations/controlcenter/ActiveInOperatingMode.java new file mode 100644 index 0000000..730a423 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/customizations/controlcenter/ActiveInOperatingMode.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.customizations.controlcenter; + +import jakarta.inject.Qualifier; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation type for binding injectable components meant to be used in operating mode. + */ +@Qualifier +@Target(value = {ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) +@Retention(value = RetentionPolicy.RUNTIME) +public @interface ActiveInOperatingMode { +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/customizations/controlcenter/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/customizations/controlcenter/package-info.java new file mode 100644 index 0000000..fafad86 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/customizations/controlcenter/package-info.java @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Components supporting extension and customization of the openTCS kernel control center + * application. + */ +package org.opentcs.customizations.controlcenter; diff --git a/opentcs-api-base/src/main/java/org/opentcs/customizations/kernel/ActiveInAllModes.java b/opentcs-api-base/src/main/java/org/opentcs/customizations/kernel/ActiveInAllModes.java new file mode 100644 index 0000000..d937b73 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/customizations/kernel/ActiveInAllModes.java @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.customizations.kernel; + +import jakarta.inject.Qualifier; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation type for binding injectable components meant to be used in all kernel modes. + */ +@Qualifier +@Target(value = {ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) +@Retention(value = RetentionPolicy.RUNTIME) +public @interface ActiveInAllModes { + +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/customizations/kernel/ActiveInModellingMode.java b/opentcs-api-base/src/main/java/org/opentcs/customizations/kernel/ActiveInModellingMode.java new file mode 100644 index 0000000..4bde468 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/customizations/kernel/ActiveInModellingMode.java @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.customizations.kernel; + +import jakarta.inject.Qualifier; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation type for binding injectable components meant to be used in modelling mode. + */ +@Qualifier +@Target(value = {ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) +@Retention(value = RetentionPolicy.RUNTIME) +public @interface ActiveInModellingMode { + +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/customizations/kernel/ActiveInOperatingMode.java b/opentcs-api-base/src/main/java/org/opentcs/customizations/kernel/ActiveInOperatingMode.java new file mode 100644 index 0000000..2998788 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/customizations/kernel/ActiveInOperatingMode.java @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.customizations.kernel; + +import jakarta.inject.Qualifier; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation type for binding injectable components meant to be used in operating mode. + */ +@Qualifier +@Target(value = {ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) +@Retention(value = RetentionPolicy.RUNTIME) +public @interface ActiveInOperatingMode { + +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/customizations/kernel/GlobalSyncObject.java b/opentcs-api-base/src/main/java/org/opentcs/customizations/kernel/GlobalSyncObject.java new file mode 100644 index 0000000..8acf1ee --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/customizations/kernel/GlobalSyncObject.java @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.customizations.kernel; + +import jakarta.inject.Qualifier; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation type to mark an injectable synchronization object for the kernel. + */ +@Qualifier +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface GlobalSyncObject { + // Nothing here. +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/customizations/kernel/KernelExecutor.java b/opentcs-api-base/src/main/java/org/opentcs/customizations/kernel/KernelExecutor.java new file mode 100644 index 0000000..16364d0 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/customizations/kernel/KernelExecutor.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.customizations.kernel; + +import jakarta.inject.Qualifier; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation type to mark a central injectable {@code ScheduledExecutorService}. + */ +@Qualifier +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface KernelExecutor { +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/customizations/kernel/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/customizations/kernel/package-info.java new file mode 100644 index 0000000..4b8bac2 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/customizations/kernel/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Components supporting extension and customization of the openTCS kernel application. + */ +package org.opentcs.customizations.kernel; diff --git a/opentcs-api-base/src/main/java/org/opentcs/customizations/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/customizations/package-info.java new file mode 100644 index 0000000..269a5cc --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/customizations/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Classes utilized for extending and customizing openTCS applications. + */ +package org.opentcs.customizations; diff --git a/opentcs-api-base/src/main/java/org/opentcs/customizations/plantoverview/ApplicationFrame.java b/opentcs-api-base/src/main/java/org/opentcs/customizations/plantoverview/ApplicationFrame.java new file mode 100644 index 0000000..2d2374b --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/customizations/plantoverview/ApplicationFrame.java @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.customizations.plantoverview; + +import jakarta.inject.Qualifier; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation type to mark the application's main frame. + */ +@Qualifier +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApplicationFrame { + // Nothing here. +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/ObjectExistsException.java b/opentcs-api-base/src/main/java/org/opentcs/data/ObjectExistsException.java new file mode 100644 index 0000000..377fc5d --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/ObjectExistsException.java @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data; + +import org.opentcs.access.KernelRuntimeException; + +/** + * Thrown when an object was supposed to be created or renamed, but another + * object with the same ID/name/attributes already exists. + */ +public class ObjectExistsException + extends + KernelRuntimeException { + + /** + * Creates a new ObjectExistsException with the given detail message. + * + * @param message The detail message. + */ + public ObjectExistsException(String message) { + super(message); + } + + /** + * Creates a new ObjectExistsException with the given detail message and + * cause. + * + * @param message The detail message. + * @param cause The cause. + */ + public ObjectExistsException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/ObjectHistory.java b/opentcs-api-base/src/main/java/org/opentcs/data/ObjectHistory.java new file mode 100644 index 0000000..4ebfa8e --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/ObjectHistory.java @@ -0,0 +1,190 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A history of events related to an object. + */ +public class ObjectHistory + implements + Serializable { + + /** + * The actual history entries. + */ + private final List entries; + + /** + * Creates a new instance. + */ + public ObjectHistory() { + this(List.of()); + } + + /** + * Creates a new instance with the given list of entries. + * + * @param entries + */ + private ObjectHistory(List entries) { + this.entries = Collections.unmodifiableList(requireNonNull(entries, "entries")); + } + + /** + * Returns this history's entries. + * + * @return This history's entries. + */ + public List getEntries() { + return entries; + } + + /** + * Returns a copy of this object, with the given entries. + * + * @param entries The entries. + * @return A copy of this object, with the given entries. + */ + public ObjectHistory withEntries(List entries) { + return new ObjectHistory(entries); + } + + /** + * Returns a copy of this object, with the given entry appended. + * + * @param entry The entry. + * @return A copy of this object, with the given entry appended. + */ + public ObjectHistory withEntryAppended(Entry entry) { + requireNonNull(entry, "entry"); + + List newEntries = new ArrayList<>(entries.size() + 1); + newEntries.addAll(entries); + newEntries.add(entry); + return new ObjectHistory(newEntries); + } + + @Override + public String toString() { + return "ObjectHistory{" + "entries=" + entries + '}'; + } + + /** + * An entry/event in a history. + */ + public static class Entry + implements + Serializable { + + /** + * The point of time at which the event occured. + */ + private final Instant timestamp; + /** + * A code identifying the event that occured. + */ + private final String eventCode; + /** + * Supplementary information about the event. + * How this information is to be interpreted (if at all) depends on the respective event code. + */ + private final Object supplement; + + /** + * Creates a new instance. + * + * @param timestamp The point of time at which the event occured. + * @param eventCode A code identifying the event that occured. + * @param supplement Supplementary information about the event. + * Must be {@link Serializable} and should provide a human-readable default representation from + * its {@code toString()} method. + */ + public Entry(Instant timestamp, String eventCode, Object supplement) { + this.timestamp = requireNonNull(timestamp, "timestamp"); + this.eventCode = requireNonNull(eventCode, "eventCode"); + this.supplement = requireNonNull(supplement, "supplement"); + checkArgument(supplement instanceof Serializable, "supplement is not serializable"); + } + + /** + * Creates a new instance with an empty supplement. + * + * @param timestamp The point of time at which the event occured. + * @param eventCode A code identifying the event that occured. + */ + public Entry(Instant timestamp, String eventCode) { + this(timestamp, eventCode, ""); + } + + /** + * Creates a new instance with the timestamp set to the current point of time. + * + * @param eventCode A code identifying the event that occured. + * @param supplement Supplementary information about the event. + * Must be {@link Serializable} and should provide a human-readable default representation from + * its {@code toString()} method. + */ + public Entry(String eventCode, Object supplement) { + this(Instant.now(), eventCode, supplement); + } + + /** + * Creates a new instance with the timestamp set to the current point of time and an empty + * supplement. + * + * @param eventCode A code identifying the event that occured. + */ + public Entry(String eventCode) { + this(eventCode, ""); + } + + /** + * Returns this entry's timestamp. + * + * @return This entry's timestamp. + */ + @Nonnull + public Instant getTimestamp() { + return timestamp; + } + + /** + * Returns this entry's event code. + * + * @return This entry's event code. + */ + @Nonnull + public String getEventCode() { + return eventCode; + } + + /** + * Returns a supplemental object providing details about the event. + * + * @return A supplemental object providing details about the event. + */ + @Nonnull + public Object getSupplement() { + return supplement; + } + + @Override + public String toString() { + return "Entry{" + + "timestamp=" + timestamp + + ", eventCode=" + eventCode + + ", supplement=" + supplement + + '}'; + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/ObjectPropConstants.java b/opentcs-api-base/src/main/java/org/opentcs/data/ObjectPropConstants.java new file mode 100644 index 0000000..9023867 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/ObjectPropConstants.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data; + +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.model.visualization.LocationRepresentation; +import org.opentcs.data.order.TransportOrder; + +/** + * Defines some reserved/commonly used property keys and values. + */ +public interface ObjectPropConstants { + + /** + * A property key for models used to store the last-modified time stamp. + *

+ * Type: A time stamp, encoded using ISO 8601. (Can be parsed using {@code java.time.Instant}.) + *

+ */ + String MODEL_FILE_LAST_MODIFIED = "tcs:modelFileLastModified"; + /** + * A property key for {@link LocationType} instances used to provide a hint for the visualization + * how locations of the type should be visualized. + *

+ * Type: String (any element of {@link LocationRepresentation}) + *

+ */ + String LOCTYPE_DEFAULT_REPRESENTATION = "tcs:defaultLocationTypeSymbol"; + /** + * A property key for {@link Location} instances used to provide a hint for the visualization how + * the locations should be visualized. + *

+ * Type: String (any element of {@link LocationRepresentation}) + *

+ */ + String LOC_DEFAULT_REPRESENTATION = "tcs:defaultLocationSymbol"; + /** + * A property key for {@link TransportOrder} instances used to define resources (i.e., points, + * paths or locations) that should be avoided by vehicles processing transport orders with such a + * property. + *

+ * Type: String (a comma-separated list of resource names) + *

+ */ + String TRANSPORT_ORDER_RESOURCES_TO_AVOID = "tcs:resourcesToAvoid"; + /** + * A property key for {@link Vehicle} instances used to select the data transformer to be used. + *

+ * Type: String (the name of a data transformer factory) + *

+ */ + String VEHICLE_DATA_TRANSFORMER = "tcs:vehicleDataTransformer"; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/ObjectUnknownException.java b/opentcs-api-base/src/main/java/org/opentcs/data/ObjectUnknownException.java new file mode 100644 index 0000000..3648cab --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/ObjectUnknownException.java @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data; + +import org.opentcs.access.KernelRuntimeException; + +/** + * Thrown when an object was supposed to be returned/removed/modified, but could + * not be found. + */ +public class ObjectUnknownException + extends + KernelRuntimeException { + + /** + * Creates a new ObjectExistsException with the given detail message. + * + * @param message The detail message. + */ + public ObjectUnknownException(String message) { + super(message); + } + + /** + * Creates a new ObjectExistsException for the given object reference. + * + * @param ref The object reference. + */ + public ObjectUnknownException(TCSObjectReference ref) { + super("Object unknown: " + (ref == null ? "" : ref.toString())); + } + + /** + * Creates a new ObjectExistsException with the given detail message and + * cause. + * + * @param message The detail message. + * @param cause The cause. + */ + public ObjectUnknownException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/TCSObject.java b/opentcs-api-base/src/main/java/org/opentcs/data/TCSObject.java new file mode 100644 index 0000000..ad917ca --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/TCSObject.java @@ -0,0 +1,279 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Describes the base behaviour of TCS data objects. + * + * @param The actual object class. + */ +public abstract class TCSObject> + implements + Serializable { + + /** + * A transient reference to this business object. + */ + protected TCSObjectReference reference; + /** + * A set of properties (key-value pairs) associated with this object. + */ + private final Map properties; + /** + * An unmodifiable view on this object's properties. + * This mainly exists for {@link #getProperties()}, as the alternative of + * creating ad-hoc copies or unmodifiable views can lead to performance issues + * related to garbage collection in situations where {@link #getProperties()} + * is called often. + */ + private final Map propertiesReadOnly; + /** + * The name of the business object. + */ + private final String name; + /** + * A history of events related to this object. + */ + private final ObjectHistory history; + + /** + * Creates a new TCSObject. + * + * @param objectName The new object's name. + */ + protected TCSObject( + @Nonnull + String objectName + ) { + this(objectName, new HashMap<>(), new ObjectHistory()); + } + + /** + * Creates a new TCSObject. + * + * @param objectName The new object's name. + * @param properties A set of properties (key-value pairs) associated with this object. + * @param history A history of events related to this object. + */ + @SuppressWarnings("this-escape") + protected TCSObject( + @Nonnull + String objectName, + @Nonnull + Map properties, + @Nonnull + ObjectHistory history + ) { + this.name = requireNonNull(objectName, "objectName"); + this.properties = mapWithoutNullValues(properties); + this.propertiesReadOnly = Collections.unmodifiableMap(this.properties); + this.reference = new TCSObjectReference<>(this); + this.history = requireNonNull(history, "history"); + } + + /** + * Returns this object's name. + * + * @return This object's name. + */ + @Nonnull + public String getName() { + return name; + } + + /** + * Returns a transient/soft reference to this object. + * + * @return A transient/soft reference to this object. + */ + public TCSObjectReference getReference() { + return reference; + } + + /** + * Returns an unmodifiable view on this object's properties. + * + * @return This object's properties. + */ + @Nonnull + public Map getProperties() { + return propertiesReadOnly; + } + + /** + * Returns the property value for the given key. + * This is basically a shortcut for getProperties().get(key). + * + * @param key The property's key. + * @return The property value for the given key, or null, if there is none. + */ + @Nullable + public String getProperty(String key) { + return properties.get(key); + } + + /** + * Creates a copy of this object, with the given property integrated. + * + * @param key The key of the property to be changed. + * @param value The new value of the property, or null, if the property is to be + * removed. + * @return A copy of this object, with the given property integrated. + */ + public abstract TCSObject withProperty(String key, String value); + + /** + * Creates a copy of this object, with the given properties. + * + * @param properties The properties. + * @return A copy of this object, with the given properties. + */ + public abstract TCSObject withProperties(Map properties); + + public ObjectHistory getHistory() { + return history; + } + + /** + * Creates a copy of this object, with the given history entry integrated. + * + * @param entry The history entry to be integrated. + * @return A copy of this object, with the given history entry integrated. + */ + public abstract TCSObject withHistoryEntry(ObjectHistory.Entry entry); + + /** + * Creates a copy of this object, with the given history. + * + * @param history The history. + * @return A copy of this object, with the given history. + */ + public abstract TCSObject withHistory(ObjectHistory history); + + @Override + public String toString() { + return getClass().getSimpleName() + "{name=" + name + '}'; + } + + /** + * Checks if this object is equal to another one. + * Two TCSObjects are equal if both their names and their runtime classes are equal. + * + * @param obj The object to compare this one to. + * @return true if, and only if, obj is also a TCSObject + * and both its name and runtime class equal those of this object. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof TCSObject)) { + return false; + } + + return Objects.equals(getClass(), obj.getClass()) + && Objects.equals(getName(), ((TCSObject) obj).getName()); + } + + /** + * Returns this object's hashcode. + * A TCSObject's hashcode is calculated by XORing its ID's + * hashcode and the hashcode of its runtime class's name. + * + * @return This object's hashcode. + */ + @Override + public int hashCode() { + return getName().hashCode() + ^ this.getClass().getName().hashCode(); + } + + /** + * Returns a new map of this object's properties, with the given property integrated. + * + * @param key The key of the property to be changed. + * @param value The new value of the property, or null, if the property is to be + * removed. + * @return A new map of this object's properties, with the given property integrated. + */ + protected final Map propertiesWith(String key, String value) { + requireNonNull(key, "key"); + + Map result = new HashMap<>(properties); + if (value == null) { + result.remove(key); + } + else { + result.put(key, value); + } + return result; + } + + /** + * Returns a new map with the entries from the given map but all entries with null + * values removed. + * + * @param The type of the map's keys. + * @param The type of the map's values. + * @param original The original map. + * @return A new map with the entries from the given map but all entries with null + * values removed. + */ + protected static final Map mapWithoutNullValues(Map original) { + requireNonNull(original, "original"); + + Map result = new HashMap<>(); + for (Map.Entry entry : original.entrySet()) { + if (entry.getValue() != null) { + result.put(entry.getKey(), entry.getValue()); + } + } + return result; + } + + /** + * Returns a new list with the values from the given list but all null values + * removed. + * + * @param The type of the list's values. + * @param original The original list. + * @return A new list with the values from the given list but all null values + * removed. + */ + protected static final List listWithoutNullValues(List original) { + requireNonNull(original, "original"); + + return original.stream() + .filter(value -> value != null) + .collect(Collectors.toList()); + } + + /** + * Returns a new set with the values from the given set but all null values removed. + * + * @param The type of the set's values. + * @param original The original set. + * @return A new set with the values from the given set but all null values removed. + */ + protected static final Set setWithoutNullValues(Set original) { + requireNonNull(original, "original"); + + return original.stream() + .filter(value -> value != null) + .collect(Collectors.toSet()); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/TCSObjectEvent.java b/opentcs-api-base/src/main/java/org/opentcs/data/TCSObjectEvent.java new file mode 100644 index 0000000..1bc5afd --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/TCSObjectEvent.java @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data; + +import static java.util.Objects.requireNonNull; + +import java.io.Serializable; + +/** + * Instances of this class represent events emitted by/for business objects. + */ +public class TCSObjectEvent + implements + Serializable { + + /** + * The current state of the object for which this event was created. + */ + private final TCSObject currentObjectState; + /** + * The previous state of the object for which this event was created. + */ + private final TCSObject previousObjectState; + /** + * This event's type. + */ + private final Type type; + + /** + * Creates a new TCSObjectEvent. + * + * @param currentObjectState The current state of the object for which this + * event was created. Value is irrelevant/may be null if + * eventType is OBJECT_REMOVED. + * @param previousObjectState The previous state of the object for which this + * event was created.Value is irrelevant/may be null if + * eventType is OBJECT_CREATED. + * @param eventType The event's type. + * @throws NullPointerException If eventType is + * null. + * @throws IllegalArgumentException If either currentObjectState + * or previousObjectState is null while + * eventType does not have an appropriate value. + */ + public TCSObjectEvent( + TCSObject currentObjectState, + TCSObject previousObjectState, + Type eventType + ) { + this.type = requireNonNull(eventType, "eventType"); + if (currentObjectState == null && !Type.OBJECT_REMOVED.equals(eventType)) { + throw new IllegalArgumentException( + "currentObjectState == null but eventType != OBJECT_REMOVED" + ); + } + if (previousObjectState == null && !Type.OBJECT_CREATED.equals(eventType)) { + throw new IllegalArgumentException( + "previousObjectState == null but eventType != OBJECT_CREATED" + ); + } + this.currentObjectState = currentObjectState; + this.previousObjectState = previousObjectState; + } + + /** + * Returns the current state of the object for which this event was created. + * + * @return The current state of the object for which this event was created. + */ + public TCSObject getCurrentObjectState() { + return currentObjectState; + } + + /** + * Returns the previous state of the object for which this event was created. + * + * @return The previous state of the object for which this event was created. + */ + public TCSObject getPreviousObjectState() { + return previousObjectState; + } + + /** + * Returns the current state of the object for which this event was created, + * or, if the current state is null, the previous state. + * + * @return The current or the previous state of the object for which this + * event was created. + */ + public TCSObject getCurrentOrPreviousObjectState() { + if (currentObjectState != null) { + return currentObjectState; + } + else { + return previousObjectState; + } + } + + /** + * Returns this event's type. + * + * @return This event's type. + */ + public Type getType() { + return type; + } + + @Override + public String toString() { + return "TCSObjectEvent{" + + "type=" + type + + ", currentObjectState=" + currentObjectState + + ", previousObjectState=" + previousObjectState + + '}'; + } + + /** + * Indicates the type of an event, which can be helpful with filtering events. + */ + public enum Type { + + /** + * Indicates that the referenced object has been newly created. + */ + OBJECT_CREATED, + /** + * Indicates that the referenced object has been modified. + */ + OBJECT_MODIFIED, + /** + * Indicates that the referenced object is no longer a valid kernel object. + */ + OBJECT_REMOVED; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/TCSObjectReference.java b/opentcs-api-base/src/main/java/org/opentcs/data/TCSObjectReference.java new file mode 100644 index 0000000..2d91bd6 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/TCSObjectReference.java @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import java.util.Objects; + +/** + * A transient reference to a {@link TCSObject}. + * + * @param The actual object class. + */ +public class TCSObjectReference> + implements + Serializable { + + /** + * The referenced object's class. + */ + private final Class referentClass; + /** + * The referenced object's name. + */ + private final String name; + + /** + * Creates a new TCSObjectReference. + * + * @param referent The object this reference references. + */ + protected TCSObjectReference( + @Nonnull + TCSObject referent + ) { + requireNonNull(referent, "newReferent"); + + referentClass = referent.getClass(); + name = referent.getName(); + } + + /** + * Returns the referenced object's class. + * + * @return The referenced object's class. + */ + public Class getReferentClass() { + return referentClass; + } + + /** + * Returns the referenced object's name. + * + * @return The referenced object's name. + */ + public final String getName() { + return name; + } + + @Override + public boolean equals(Object otherObj) { + if (otherObj == this) { + return true; + } + if (!(otherObj instanceof TCSObjectReference)) { + return false; + } + + TCSObjectReference otherRef = (TCSObjectReference) otherObj; + return Objects.equals(referentClass, otherRef.referentClass) + && Objects.equals(name, otherRef.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public String toString() { + return "TCSObjectReference{" + + "referentClass=" + referentClass + + ", name=" + name + + '}'; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/Block.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/Block.java new file mode 100644 index 0000000..4ad2483 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/Block.java @@ -0,0 +1,255 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model; + +import static java.util.Objects.requireNonNull; + +import java.awt.Color; +import java.io.Serializable; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.TCSObject; + +/** + * An aggregation of resources with distinct usage rules depending on the block's type. + * + * @see TCSResource + */ +public class Block + extends + TCSResource + implements + Serializable { + + /** + * This block's type. + */ + private final Type type; + /** + * The resources aggregated in this block. + */ + private final Set> members; + /** + * The information regarding the grahical representation of this block. + */ + private final Layout layout; + + /** + * Creates an empty block. + * + * @param name This block's name. + */ + public Block(String name) { + super(name); + this.type = Type.SINGLE_VEHICLE_ONLY; + this.members = new HashSet<>(); + this.layout = new Layout(); + } + + private Block( + String name, + Map properties, + ObjectHistory history, + Type type, + Set> members, + Layout layout + ) { + super(name, properties, history); + this.type = type; + this.members = new HashSet<>(requireNonNull(members, "members")); + this.layout = requireNonNull(layout, "layout"); + } + + @Override + public Block withProperty(String key, String value) { + return new Block( + getName(), + propertiesWith(key, value), + getHistory(), + type, + members, + layout + ); + } + + @Override + public Block withProperties(Map properties) { + return new Block( + getName(), + properties, + getHistory(), + type, + members, + layout + ); + } + + @Override + public TCSObject withHistoryEntry(ObjectHistory.Entry entry) { + return new Block( + getName(), + getProperties(), + getHistory().withEntryAppended(entry), + type, + members, + layout + ); + } + + @Override + public TCSObject withHistory(ObjectHistory history) { + return new Block( + getName(), + getProperties(), + history, + type, + members, + layout + ); + } + + /** + * Retruns the type of this block. + * + * @return The type of this block. + */ + public Type getType() { + return type; + } + + /** + * Creates a copy of this object, with the given type. + * + * @param type The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Block withType(Type type) { + return new Block( + getName(), + getProperties(), + getHistory(), + type, + members, + layout + ); + } + + /** + * Returns an unmodifiable set of all members of this block. + * + * @return An unmodifiable set of all members of this block. + */ + public Set> getMembers() { + return Collections.unmodifiableSet(members); + } + + /** + * Creates a copy of this object, with the given members. + * + * @param members The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Block withMembers(Set> members) { + return new Block( + getName(), + getProperties(), + getHistory(), + type, + members, + layout + ); + } + + /** + * Returns the information regarding the grahical representation of this block. + * + * @return The information regarding the grahical representation of this block. + */ + public Layout getLayout() { + return layout; + } + + /** + * Creates a copy of this object, with the given layout. + * + * @param layout The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Block withLayout(Layout layout) { + return new Block( + getName(), + getProperties(), + getHistory(), + type, + members, + layout + ); + } + + /** + * Describes the types of blocks in a driving course. + */ + public enum Type { + + /** + * The resources aggregated in this block can only be used by one vehicle at the same time. + */ + SINGLE_VEHICLE_ONLY, + /** + * The resources aggregated in this block can be used by multiple vehicles, but only if they + * enter the block in the same direction. + */ + SAME_DIRECTION_ONLY; + } + + /** + * Contains information regarding the grahical representation of a block. + */ + public static class Layout + implements + Serializable { + + /** + * The color in which block elements are to be emphasized. + */ + private final Color color; + + /** + * Creates a new instance. + */ + public Layout() { + this(Color.RED); + } + + /** + * Creates a new instance. + * + * @param color The color in which block elements are to be emphasized. + */ + public Layout(Color color) { + this.color = requireNonNull(color, "color"); + } + + /** + * Returns the color in which block elements are to be emphasized. + * + * @return The color in which block elements are to be emphasized. + */ + public Color getColor() { + return color; + } + + /** + * Creates a copy of this object, with the given color. + * + * @param color The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withColor(Color color) { + return new Layout(color); + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/BoundingBox.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/BoundingBox.java new file mode 100644 index 0000000..20397f3 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/BoundingBox.java @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkInRange; + +import java.io.Serializable; +import java.util.Objects; + +/** + * A bounding box that can be used, for example, to describe an object's physical dimensions. + *

+ * A bounding box is characterised by a reference point that is located in the center of the + * bounding box's base (i.e. at height 0). Therefore, the length and width of the bounding box are + * symmetrical in relation to the reference point and the height is measured from the base of the + * bounding box. + * Additionally, an offset to the reference point describes the position of the bounding box in + * relation to another point. This is useful when describing the bounding box of an object whose + * reference point is not at its geometric center. The coordinates of the reference offset refer to + * a coordinate system whose origin is located at the bounding box's reference point and whose axes + * run along the longitudinal and transverse axes of the bounding box (i.e. the x-coordinate of the + * reference offset runs along the length and the y-coordinate along the width of the bounding box). + *

+ */ +public class BoundingBox + implements + Serializable { + + private final long length; + private final long width; + private final long height; + private final Couple referenceOffset; + + /** + * Creates a new instance with a (0, 0) reference offset. + * + * @param length The bounding box's length. + * @param width The bounding box's width. + * @param height The bounding box's height. + */ + public BoundingBox(long length, long width, long height) { + this(length, width, height, new Couple(0, 0)); + } + + private BoundingBox(long length, long width, long height, Couple referenceOffset) { + this.length = checkInRange(length, 1, Long.MAX_VALUE, "length"); + this.width = checkInRange(width, 1, Long.MAX_VALUE, "width"); + this.height = checkInRange(height, 1, Long.MAX_VALUE, "height"); + this.referenceOffset = requireNonNull(referenceOffset, "referenceOffset"); + } + + /** + * Returns the bounding box's length. + * + * @return The bounding box's length. + */ + public long getLength() { + return length; + } + + /** + * Creates a copy of this object, with the given length. + * + * @param length The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public BoundingBox withLength(long length) { + return new BoundingBox(length, width, height, referenceOffset); + } + + /** + * Returns the bounding box's width. + * + * @return The bounding box's width. + */ + public long getWidth() { + return width; + } + + /** + * Creates a copy of this object, with the given width. + * + * @param width The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public BoundingBox withWidth(long width) { + return new BoundingBox(length, width, height, referenceOffset); + } + + /** + * Returns the bounding box's height. + * + * @return The bounding box's height. + */ + public long getHeight() { + return height; + } + + /** + * Creates a copy of this object, with the given height. + * + * @param height The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public BoundingBox withHeight(long height) { + return new BoundingBox(length, width, height, referenceOffset); + } + + /** + * Returns the bounding box's reference offset. + * + * @return The bounding box's reference offset. + */ + public Couple getReferenceOffset() { + return referenceOffset; + } + + /** + * Creates a copy of this object, with the given reference offset. + * + * @param referenceOffset The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public BoundingBox withReferenceOffset(Couple referenceOffset) { + return new BoundingBox(length, width, height, referenceOffset); + } + + @Override + public final boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof BoundingBox other)) { + return false; + } + + return length == other.length + && width == other.width + && height == other.height + && Objects.equals(referenceOffset, other.referenceOffset); + } + + @Override + public int hashCode() { + return Objects.hash(length, width, height, referenceOffset); + } + + @Override + public String toString() { + return "BoundingBox{" + + "length=" + length + + ", width=" + width + + ", height=" + height + + ", referenceOffset=" + referenceOffset + + '}'; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/Couple.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/Couple.java new file mode 100644 index 0000000..1137c0d --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/Couple.java @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model; + +import java.io.Serializable; + +/** + * A generic 2-tuple of long integer values, usable for 2D coordinates and vectors, for instance. + */ +public class Couple + implements + Serializable { + + /** + * The X coordinate. + */ + private final long x; + /** + * The Y coordinate. + */ + private final long y; + + /** + * Creates a new instance. + * + * @param x The X coordinate. + * @param y The Y coordinate. + */ + public Couple(long x, long y) { + this.x = x; + this.y = y; + } + + /** + * Returns the x coordinate. + * + * @return x + */ + public long getX() { + return x; + } + + /** + * Returns the y coordinate. + * + * @return y + */ + public long getY() { + return y; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Couple)) { + return false; + } + Couple other = (Couple) obj; + if (this.x != other.x) { + return false; + } + if (this.y != other.y) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return (int) (x ^ y); + } + + @Override + public String toString() { + return "Couple{" + "x=" + x + ", y=" + y + '}'; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/Envelope.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/Envelope.java new file mode 100644 index 0000000..9355c54 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/Envelope.java @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import java.util.List; +import java.util.Objects; + +/** + * A sequence of vertices that, when connected in their defined order, represent the area that may + * be occupied by an object. + *

+ * Since an envelope represents a closed geometry, it is expected that the last vertex matches the + * first one. + *

+ * Note that an envelope with less than four vertices is not reasonable, since it cannot span a + * two-dimensional plane. Such envelopes are therefore considered empty. + */ +public class Envelope + implements + Serializable { + + private final List vertices; + + /** + * Creates a new instance. + * + * @param vertices The sequence of vertices the envelope consists of. + * @throws IllegalArgumentException If the sequence of vertices is empty or if the last vertext + * in the sequence does not match the first one. + */ + public Envelope( + @Nonnull + List vertices + ) { + this.vertices = requireNonNull(vertices, "vertices"); + checkArgument(!vertices.isEmpty(), "An envelope must contain some vertices."); + checkArgument( + Objects.equals(vertices.get(0), vertices.get(vertices.size() - 1)), + "An envelope's last vertex must match the first one." + ); + } + + /** + * Returns the sequence of vertices the envelope consists of. + * + * @return The sequence of vertices the envelope consists of. + */ + public List getVertices() { + return vertices; + } + + @Override + public int hashCode() { + int hash = 3; + hash = 89 * hash + Objects.hashCode(this.vertices); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Envelope)) { + return false; + } + + Envelope other = (Envelope) obj; + return Objects.equals(this.vertices, other.vertices); + } + + @Override + public String toString() { + return "Envelope{" + "vertices=" + vertices + '}'; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/Location.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/Location.java new file mode 100644 index 0000000..0f6bda6 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/Location.java @@ -0,0 +1,611 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.visualization.LocationRepresentation; + +/** + * A location at which a {@link Vehicle} may perform an action. + *

+ * A location must be linked to at least one {@link Point} to be reachable for a vehicle. + * It may be linked to multiple points. + * As long as a link's specific set of allowed operations is empty (which is the default), all + * operations defined by the location's referenced {@link LocationType} are allowed at the linked + * point. + * If the link's set of allowed operations is not empty, only the operations contained in it are + * allowed at the linked point. + *

+ * + * @see LocationType + */ +public class Location + extends + TCSResource + implements + Serializable { + + /** + * This location's position in mm. + */ + private final Triple position; + /** + * A reference to this location's type. + */ + private final TCSObjectReference type; + /** + * A set of links attached to this location. + */ + private final Set attachedLinks; + /** + * A flag for marking this location as locked (i.e. to prevent transport orders leading to it + * from being assigned to vehicles). + */ + private final boolean locked; + /** + * Details about the peripheral devices this location may represent. + */ + private final PeripheralInformation peripheralInformation; + /** + * The information regarding the grahical representation of this location. + */ + private final Layout layout; + + /** + * Creates a new Location. + * + * this.locked = false; + * + * @param name The new location's name. + * @param type The new location's type. + */ + public Location(String name, TCSObjectReference type) { + super(name); + this.type = requireNonNull(type, "type"); + this.position = new Triple(0, 0, 0); + this.attachedLinks = new HashSet<>(); + this.locked = false; + this.peripheralInformation = new PeripheralInformation(); + this.layout = new Layout(); + } + + private Location( + String name, + Map properties, + ObjectHistory history, + TCSObjectReference locationType, + Triple position, + Set attachedLinks, + boolean locked, + PeripheralInformation peripheralInformation, + Layout layout + ) { + super(name, properties, history); + this.type = requireNonNull(locationType, "locationType"); + this.position = requireNonNull(position, "position"); + this.attachedLinks = new HashSet<>(requireNonNull(attachedLinks, "attachedLinks")); + this.locked = locked; + this.peripheralInformation = requireNonNull(peripheralInformation, "peripheralInformation"); + this.layout = requireNonNull(layout, "layout"); + } + + @Override + public Location withProperty(String key, String value) { + return new Location( + getName(), + propertiesWith(key, value), + getHistory(), + type, + position, + attachedLinks, + locked, + peripheralInformation, + layout + ); + } + + @Override + public Location withProperties(Map properties) { + return new Location( + getName(), + properties, + getHistory(), + type, + position, + attachedLinks, + locked, + peripheralInformation, + layout + ); + } + + @Override + public TCSObject withHistoryEntry(ObjectHistory.Entry entry) { + return new Location( + getName(), + getProperties(), + getHistory().withEntryAppended(entry), + type, + position, + attachedLinks, + locked, + peripheralInformation, + layout + ); + } + + @Override + public TCSObject withHistory(ObjectHistory history) { + return new Location( + getName(), + getProperties(), + history, + type, + position, + attachedLinks, + locked, + peripheralInformation, + layout + ); + } + + /** + * Returns the physical coordinates of this location in mm. + * + * @return The physical coordinates of this location in mm. + */ + public Triple getPosition() { + return position; + } + + /** + * Creates a copy of this object, with the given position. + * + * @param position The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Location withPosition(Triple position) { + return new Location( + getName(), + getProperties(), + getHistory(), + type, + position, + attachedLinks, + locked, + peripheralInformation, + layout + ); + } + + /** + * Returns a reference to the type of this location. + * + * @return A reference to the type of this location. + */ + public TCSObjectReference getType() { + return type; + } + + /** + * Returns a set of links attached to this location. + * + * @return A set of links attached to this location. + */ + public Set getAttachedLinks() { + return Collections.unmodifiableSet(attachedLinks); + } + + /** + * Creates a copy of this object, with the given attached links. + * + * @param attachedLinks The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Location withAttachedLinks( + @Nonnull + Set attachedLinks + ) { + return new Location( + getName(), + getProperties(), + getHistory(), + type, + position, + attachedLinks, + locked, + peripheralInformation, + layout + ); + } + + /** + * Returns details about the peripheral devices this location may represent. + * + * @return Details about the peripheral devices this location may represent. + */ + @Nonnull + public PeripheralInformation getPeripheralInformation() { + return peripheralInformation; + } + + /** + * Creates a copy of this object, with the given peripheral information. + * + * @param peripheralInformation The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Location withPeripheralInformation( + @Nonnull + PeripheralInformation peripheralInformation + ) { + return new Location( + getName(), + getProperties(), + getHistory(), + type, + position, + attachedLinks, + locked, + peripheralInformation, + layout + ); + } + + /** + * Returns the lock status of this location (i.e. whether it my be used by vehicles or not). + * + * @return {@code true} if this location is currently locked (i.e. it may not be used + * by vehicles), else {@code false}. + */ + public boolean isLocked() { + return locked; + } + + /** + * Creates a copy of this object, with the given locked flag. + * + * @param locked The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Location withLocked(boolean locked) { + return new Location( + getName(), + getProperties(), + getHistory(), + type, + position, + attachedLinks, + locked, + peripheralInformation, + layout + ); + } + + /** + * Returns the information regarding the grahical representation of this location. + * + * @return The information regarding the grahical representation of this location. + */ + public Layout getLayout() { + return layout; + } + + /** + * Creates a copy of this object, with the given layout. + * + * @param layout The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Location withLayout(Layout layout) { + return new Location( + getName(), + getProperties(), + getHistory(), + type, + position, + attachedLinks, + locked, + peripheralInformation, + layout + ); + } + + /** + * A link connecting a point and a location, expressing that the location is + * reachable from the point. + */ + public static class Link + implements + Serializable { + + /** + * A reference to the location end of this link. + */ + private final TCSResourceReference location; + /** + * A reference to the point end of this link. + */ + private final TCSResourceReference point; + /** + * The operations allowed at this link. + */ + private final Set allowedOperations; + + /** + * Creates a new Link. + * + * @param location A reference to the location end of this link. + * @param point A reference to the point end of this link. + */ + public Link( + TCSResourceReference location, + TCSResourceReference point + ) { + this.location = requireNonNull(location, "location"); + this.point = requireNonNull(point, "point"); + this.allowedOperations = new TreeSet<>(); + } + + private Link( + TCSResourceReference location, + TCSResourceReference point, + Set allowedOperations + ) { + this.location = requireNonNull(location, "location"); + this.point = requireNonNull(point, "point"); + this.allowedOperations = new TreeSet<>( + requireNonNull( + allowedOperations, + "allowedOperations" + ) + ); + } + + /** + * Returns a reference to the location end of this link. + * + * @return A reference to the location end of this link. + */ + public TCSResourceReference getLocation() { + return location; + } + + /** + * Returns a reference to the point end of this link. + * + * @return A reference to the point end of this link. + */ + public TCSResourceReference getPoint() { + return point; + } + + /** + * Returns the operations allowed at this link. + * + * @return The operations allowed at this link. + */ + public Set getAllowedOperations() { + return Collections.unmodifiableSet(allowedOperations); + } + + /** + * Checks if a vehicle is allowed to execute a given operation at this link. + * + * @param operation The operation to be checked. + * @return true if, and only if, vehicles are allowed to + * execute the given operation at his link. + */ + public boolean hasAllowedOperation(String operation) { + requireNonNull(operation, "operation"); + return allowedOperations.contains(operation); + } + + /** + * Creates a copy of this object, with the given allowed operations. + * + * @param allowedOperations The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Link withAllowedOperations(Set allowedOperations) { + return new Link(location, point, allowedOperations); + } + + /** + * Checks if this object is equal to another one. + * Two Links are equal if they both reference the same location + * and point ends. + * + * @param obj The object to compare this one to. + * @return true if, and only if, obj is also a + * Link and reference the same location and point ends as this + * one. + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof Link) { + Link other = (Link) obj; + return point.equals(other.getPoint()) + && location.equals(other.getLocation()); + } + else { + return false; + } + } + + /** + * Returns a hash code for this link. + * The hash code of a Location.Link is computed as the + * exclusive OR (XOR) of the hash codes of the associated location and point + * references. + * + * @return A hash code for this link. + */ + @Override + public int hashCode() { + return location.hashCode() ^ point.hashCode(); + } + } + + /** + * Contains information regarding the grahical representation of a location. + */ + public static class Layout + implements + Serializable { + + /** + * The coordinates at which the location is to be drawn (in mm). + */ + private final Couple position; + /** + * The offset of the label's position to the location's position (in lu). + */ + private final Couple labelOffset; + /** + * The location representation to use. + */ + private final LocationRepresentation locationRepresentation; + /** + * The ID of the layer on which the location is to be drawn. + */ + private final int layerId; + + /** + * Creates a new instance. + */ + public Layout() { + this(new Couple(0, 0), new Couple(0, 0), LocationRepresentation.DEFAULT, 0); + } + + /** + * Creates a new instance. + * + * @param position The coordinates at which the location is to be drawn (in mm). + * @param labelOffset The offset of the label's location to the point's position (in lu). + * @param locationRepresentation The location representation to use. + * @param layerId The ID of the layer on which the location is to be drawn. + */ + public Layout( + Couple position, + Couple labelOffset, + LocationRepresentation locationRepresentation, + int layerId + ) { + this.position = requireNonNull(position, "position"); + this.labelOffset = requireNonNull(labelOffset, "labelOffset"); + this.locationRepresentation = requireNonNull( + locationRepresentation, + "locationRepresentation" + ); + this.layerId = layerId; + } + + /** + * Returns the coordinates at which the location is to be drawn (in mm). + * + * @return The coordinates at which the location is to be drawn (in mm). + */ + public Couple getPosition() { + return position; + } + + /** + * Creates a copy of this object, with the given position. + * + * @param position The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withPosition(Couple position) { + return new Layout( + position, + labelOffset, + locationRepresentation, + layerId + ); + } + + /** + * Returns the offset of the label's position to the location's position (in lu). + * + * @return The offset of the label's position to the location's position (in lu). + */ + public Couple getLabelOffset() { + return labelOffset; + } + + /** + * Creates a copy of this object, with the given X label offset. + * + * @param labelOffset The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withLabelOffset(Couple labelOffset) { + return new Layout( + position, + labelOffset, + locationRepresentation, + layerId + ); + } + + /** + * Returns the location representation to use. + * + * @return The location representation to use. + */ + public LocationRepresentation getLocationRepresentation() { + return locationRepresentation; + } + + /** + * Creates a copy of this object, with the given location representation. + * + * @param locationRepresentation The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withLocationRepresentation(LocationRepresentation locationRepresentation) { + return new Layout( + position, + labelOffset, + locationRepresentation, + layerId + ); + } + + /** + * Returns the ID of the layer on which the location is to be drawn. + * + * @return The layer ID. + */ + public int getLayerId() { + return layerId; + } + + /** + * Creates a copy of this object, with the given layer ID. + * + * @param layerId The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withLayerId(int layerId) { + return new Layout( + position, + labelOffset, + locationRepresentation, + layerId + ); + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/LocationType.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/LocationType.java new file mode 100644 index 0000000..ceeabc1 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/LocationType.java @@ -0,0 +1,263 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model; + +import static java.util.Objects.requireNonNull; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.TCSObject; +import org.opentcs.data.model.visualization.LocationRepresentation; + +/** + * Describes the type of a {@link Location}. + */ +public class LocationType + extends + TCSObject + implements + Serializable { + + /** + * The operations allowed at locations of this type. + */ + private final List allowedOperations; + /** + * The peripheral operations allowed at locations of this type. + */ + private final List allowedPeripheralOperations; + /** + * The information regarding the grahical representation of this location type. + */ + private final Layout layout; + + /** + * Creates a new LocationType. + * + * @param name The new location type's name. + */ + public LocationType(String name) { + super(name); + this.allowedOperations = List.of(); + this.allowedPeripheralOperations = List.of(); + this.layout = new Layout(); + } + + private LocationType( + String name, + Map properties, + ObjectHistory history, + List allowedOperations, + List allowedPeripheralOperations, + Layout layout + ) { + super(name, properties, history); + this.allowedOperations = listWithoutNullValues( + requireNonNull( + allowedOperations, + "allowedOperations" + ) + ); + this.allowedPeripheralOperations + = listWithoutNullValues( + requireNonNull( + allowedPeripheralOperations, + "allowedPeripheralOperations" + ) + ); + this.layout = requireNonNull(layout, "layout"); + } + + @Override + public LocationType withProperty(String key, String value) { + return new LocationType( + getName(), + propertiesWith(key, value), + getHistory(), + allowedOperations, + allowedPeripheralOperations, + layout + ); + } + + @Override + public LocationType withProperties(Map properties) { + return new LocationType( + getName(), + properties, + getHistory(), + allowedOperations, + allowedPeripheralOperations, + layout + ); + } + + @Override + public TCSObject withHistoryEntry(ObjectHistory.Entry entry) { + return new LocationType( + getName(), + getProperties(), + getHistory().withEntryAppended(entry), + allowedOperations, + allowedPeripheralOperations, + layout + ); + } + + @Override + public TCSObject withHistory(ObjectHistory history) { + return new LocationType( + getName(), + getProperties(), + history, + allowedOperations, + allowedPeripheralOperations, + layout + ); + } + + /** + * Returns a set of operations allowed with locations of this type. + * + * @return A set of operations allowed with locations of this type. + */ + public List getAllowedOperations() { + return Collections.unmodifiableList(allowedOperations); + } + + /** + * Checks if a given operation is allowed with locations of this type. + * + * @param operation The operation to be checked for. + * @return true if, and only if, the given operation is allowed + * with locations of this type. + */ + public boolean isAllowedOperation(String operation) { + requireNonNull(operation, "operation"); + return allowedOperations.contains(operation); + } + + /** + * Creates a copy of this object, with the given allowed operations. + * + * @param allowedOperations The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public LocationType withAllowedOperations(List allowedOperations) { + return new LocationType( + getName(), + getProperties(), + getHistory(), + allowedOperations, + allowedPeripheralOperations, + layout + ); + } + + /** + * Returns a set of peripheral operations allowed with locations of this type. + * + * @return A set of peripheral operations allowed with locations of this type. + */ + public List getAllowedPeripheralOperations() { + return Collections.unmodifiableList(allowedPeripheralOperations); + } + + /** + * Creates a copy of this object, with the given allowed peripheral operations. + * + * @param allowedPeripheralOperations The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public LocationType withAllowedPeripheralOperations(List allowedPeripheralOperations) { + return new LocationType( + getName(), + getProperties(), + getHistory(), + allowedOperations, + allowedPeripheralOperations, + layout + ); + } + + /** + * Returns the information regarding the grahical representation of this location type. + * + * @return The information regarding the grahical representation of this location type. + */ + public Layout getLayout() { + return layout; + } + + /** + * Creates a copy of this object, with the given layout. + * + * @param layout The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public LocationType withLayout(Layout layout) { + return new LocationType( + getName(), + getProperties(), + getHistory(), + allowedOperations, + allowedPeripheralOperations, + layout + ); + } + + /** + * Contains information regarding the grahical representation of a location type. + */ + public static class Layout + implements + Serializable { + + /** + * The location representation to use for locations with this location type. + */ + private final LocationRepresentation locationRepresentation; + + /** + * Creates a new instance. + */ + public Layout() { + this(LocationRepresentation.NONE); + } + + /** + * Creates a new instance. + * + * @param locationRepresentation The location representation to use for locations with this + * location type. + */ + public Layout(LocationRepresentation locationRepresentation) { + this.locationRepresentation = requireNonNull( + locationRepresentation, + "locationRepresentation" + ); + } + + /** + * Returns the location representation to use for locations with this location type. + * + * @return The location representation to use for locations with this location type. + */ + public LocationRepresentation getLocationRepresentation() { + return locationRepresentation; + } + + /** + * Creates a copy of this object, with the given location representation. + * + * @param locationRepresentation The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withLocationRepresentation(LocationRepresentation locationRepresentation) { + return new Layout(locationRepresentation); + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/ModelConstants.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/ModelConstants.java new file mode 100644 index 0000000..2db0538 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/ModelConstants.java @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model; + +/** + * Defines some constants used for plant models. + */ +public interface ModelConstants { + + /** + * The default name for the visual layout. + */ + String DEFAULT_VISUAL_LAYOUT_NAME = "VLayout"; + /** + * The name of the default layer. + */ + String DEFAULT_LAYER_NAME = "Default layer"; + /** + * The ID of the default layer. + */ + int DEFAULT_LAYER_ID = 0; + /** + * The ordinal of the default layer. + */ + int DEFAULT_LAYER_ORDINAL = 0; + /** + * The name of the default layer group. + */ + String DEFAULT_LAYER_GROUP_NAME = "Default layer group"; + /** + * The ID of the default layer group. + */ + int DEFAULT_LAYER_GROUP_ID = 0; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/Path.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/Path.java new file mode 100644 index 0000000..ebf7e9d --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/Path.java @@ -0,0 +1,625 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkInRange; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.peripherals.PeripheralOperation; + +/** + * Describes a connection between two {@link Point}s which a {@link Vehicle} may traverse. + */ +public class Path + extends + TCSResource + implements + Serializable { + + /** + * A reference to the point which this point originates in. + */ + private final TCSObjectReference sourcePoint; + /** + * A reference to the point which this point ends in. + */ + private final TCSObjectReference destinationPoint; + /** + * The length of this path (in mm). + */ + private final long length; + /** + * The absolute maximum allowed forward velocity on this path (in mm/s). + * A value of 0 (default) means forward movement is not allowed on this path. + */ + private final int maxVelocity; + /** + * The absolute maximum allowed reverse velocity on this path (in mm/s). + * A value of 0 (default) means reverse movement is not allowed on this path. + */ + private final int maxReverseVelocity; + /** + * The peripheral operations to be performed when a vehicle travels along this path. + */ + private final List peripheralOperations; + /** + * A flag for marking this path as locked (i.e. to prevent vehicles from using it). + */ + private final boolean locked; + /** + * A map of envelope keys to envelopes that vehicles traversing this path may occupy. + */ + private final Map vehicleEnvelopes; + /** + * The information regarding the grahical representation of this path. + */ + private final Layout layout; + + /** + * Creates a new Path. + * + * @param name The new path's name. + * @param sourcePoint A reference to this path's starting point. + * @param destinationPoint A reference to this path's destination point. + */ + public Path( + String name, + TCSObjectReference sourcePoint, + TCSObjectReference destinationPoint + ) { + super(name); + this.sourcePoint = requireNonNull(sourcePoint, "sourcePoint"); + this.destinationPoint = requireNonNull(destinationPoint, "destinationPoint"); + this.length = 1; + this.maxVelocity = 1000; + this.maxReverseVelocity = 1000; + this.peripheralOperations = List.of(); + this.locked = false; + this.vehicleEnvelopes = Map.of(); + this.layout = new Layout(); + } + + private Path( + String name, + Map properties, + ObjectHistory history, + TCSObjectReference sourcePoint, + TCSObjectReference destinationPoint, + long length, + int maxVelocity, + int maxReverseVelocity, + List peripheralOperations, + boolean locked, + Map vehicleEnvelopes, + Layout layout + ) { + super(name, properties, history); + this.sourcePoint = requireNonNull(sourcePoint, "sourcePoint"); + this.destinationPoint = requireNonNull(destinationPoint, "destinationPoint"); + this.length = checkInRange(length, 1, Long.MAX_VALUE, "length"); + this.maxVelocity = checkInRange(maxVelocity, 0, Integer.MAX_VALUE, "maxVelocity"); + this.maxReverseVelocity = checkInRange( + maxReverseVelocity, + 0, + Integer.MAX_VALUE, + "maxReverseVelocity" + ); + this.peripheralOperations = new ArrayList<>( + requireNonNull( + peripheralOperations, + "peripheralOperations" + ) + ); + this.locked = locked; + this.vehicleEnvelopes = requireNonNull(vehicleEnvelopes, "vehicleEnvelopes"); + this.layout = requireNonNull(layout, "layout"); + } + + @Override + public Path withProperty(String key, String value) { + return new Path( + getName(), + propertiesWith(key, value), + getHistory(), + sourcePoint, + destinationPoint, + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + @Override + public Path withProperties(Map properties) { + return new Path( + getName(), + properties, + getHistory(), + sourcePoint, + destinationPoint, + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + @Override + public TCSObject withHistoryEntry(ObjectHistory.Entry entry) { + return new Path( + getName(), + getProperties(), + getHistory().withEntryAppended(entry), + sourcePoint, + destinationPoint, + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + @Override + public TCSObject withHistory(ObjectHistory history) { + return new Path( + getName(), + getProperties(), + history, + sourcePoint, + destinationPoint, + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + /** + * Return the length of this path (in mm). + * + * @return The length of this path (in mm). + */ + public long getLength() { + return length; + } + + /** + * Creates a copy of this object, with the given length. + * + * @param length The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Path withLength(long length) { + return new Path( + getName(), + getProperties(), + getHistory(), + sourcePoint, + destinationPoint, + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + /** + * Returns a reference to the point which this path originates in. + * + * @return A reference to the point which this path originates in. + */ + public TCSObjectReference getSourcePoint() { + return sourcePoint; + } + + /** + * Returns a reference to the point which this path ends in. + * + * @return A reference to the point which this path ends in. + */ + public TCSObjectReference getDestinationPoint() { + return destinationPoint; + } + + /** + * Return the maximum allowed forward velocity (in mm/s) for this path. + * + * @return The maximum allowed forward velocity (in mm/s). A value of 0 means + * forward movement is not allowed on this path. + */ + public int getMaxVelocity() { + return maxVelocity; + } + + /** + * Creates a copy of this object, with the given maximum velocity. + * + * @param maxVelocity The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Path withMaxVelocity(int maxVelocity) { + return new Path( + getName(), + getProperties(), + getHistory(), + sourcePoint, + destinationPoint, + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + /** + * Return the maximum allowed reverse velocity (in mm/s) for this path. + * + * @return The maximum allowed reverse velocity (in mm/s). A value of 0 means + * reverse movement is not allowed on this path. + */ + public int getMaxReverseVelocity() { + return maxReverseVelocity; + } + + /** + * Creates a copy of this object, with the given maximum reverse velocity. + * + * @param maxReverseVelocity The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Path withMaxReverseVelocity(int maxReverseVelocity) { + return new Path( + getName(), + getProperties(), + getHistory(), + sourcePoint, + destinationPoint, + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + /** + * Returns the peripheral operations to be performed when a vehicle travels along this path. + * + * @return The peripheral operations to be performed when a vehicle travels along this path. + */ + public List getPeripheralOperations() { + return Collections.unmodifiableList(peripheralOperations); + } + + /** + * Creates a copy of this object, with the given peripheral operations. + * + * @param peripheralOperations The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Path withPeripheralOperations( + @Nonnull + List peripheralOperations + ) { + return new Path( + getName(), + getProperties(), + getHistory(), + sourcePoint, + destinationPoint, + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + /** + * Return the lock status of this path (i.e. whether this path my be used by + * vehicles or not). + * + * @return true if this path is currently locked (i.e. it may not + * be used by vehicles), else false. + */ + public boolean isLocked() { + return locked; + } + + /** + * Creates a copy of this object, with the given locked flag. + * + * @param locked The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Path withLocked(boolean locked) { + return new Path( + getName(), + getProperties(), + getHistory(), + sourcePoint, + destinationPoint, + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + /** + * Returns a map of envelope keys to envelopes that vehicles traversing this path may occupy. + * + * @return A map of envelope keys to envelopes that vehicles traversing this path may occupy. + */ + public Map getVehicleEnvelopes() { + return vehicleEnvelopes; + } + + /** + * + * Creates a copy of this object, with the given vehicle envelopes. + * + * @param vehicleEnvelopes The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Path withVehicleEnvelopes(Map vehicleEnvelopes) { + return new Path( + getName(), + getProperties(), + getHistory(), + sourcePoint, + destinationPoint, + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + /** + * Returns the information regarding the grahical representation of this path. + * + * @return The information regarding the grahical representation of this path. + */ + public Layout getLayout() { + return layout; + } + + /** + * Creates a copy of this object, with the given layout. + * + * @param layout The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Path withLayout(Layout layout) { + return new Path( + getName(), + getProperties(), + getHistory(), + sourcePoint, + destinationPoint, + length, + maxVelocity, + maxReverseVelocity, + peripheralOperations, + locked, + vehicleEnvelopes, + layout + ); + } + + /** + * Checks whether this path is navigable in forward direction. + * + * @return true if, and only if, this path is not locked and its + * maximum forward velocity is not zero. + */ + public boolean isNavigableForward() { + return !locked && maxVelocity != 0; + } + + /** + * Checks whether this path is navigable in backward/reverse direction. + * + * @return true if, and only if, this path is not locked and its + * maximum reverse velocity is not zero. + */ + public boolean isNavigableReverse() { + return !locked && maxReverseVelocity != 0; + } + + /** + * Checks whether this path is navigable towards the given point. + * + * @param navPoint The point. + * @return If navPoint is this path's destination point, returns + * isNavigableForward(); if navPoint is this path's + * source point, returns isNavigableReverse(). + * @throws IllegalArgumentException If the given point is neither the source + * point nor the destination point of this path. + */ + public boolean isNavigableTo(TCSObjectReference navPoint) + throws IllegalArgumentException { + if (Objects.equals(navPoint, destinationPoint)) { + return isNavigableForward(); + } + else if (Objects.equals(navPoint, sourcePoint)) { + return isNavigableReverse(); + } + else { + throw new IllegalArgumentException(navPoint + " is not an end point of " + this); + } + } + + /** + * Contains information regarding the grahical representation of a path. + */ + public static class Layout + implements + Serializable { + + /** + * The connection type the path is represented as. + */ + private final ConnectionType connectionType; + /** + * Control points describing the way the path is drawn (if the connection type + * is {@link ConnectionType#BEZIER}, {@link ConnectionType#BEZIER_3} + * or {@link ConnectionType#POLYPATH}). + */ + private final List controlPoints; + /** + * The ID of the layer on which the path is to be drawn. + */ + private final int layerId; + + /** + * Creates a new instance. + */ + public Layout() { + this(ConnectionType.DIRECT, new ArrayList<>(), 0); + } + + /** + * Creates a new instance. + * + * @param connectionType The connection type a path is represented as. + * @param controlPoints Control points describing the way the path is drawn. + * @param layerId The ID of the layer on which the path is to be drawn. + */ + public Layout(ConnectionType connectionType, List controlPoints, int layerId) { + this.connectionType = connectionType; + this.controlPoints = requireNonNull(controlPoints, "controlPoints"); + this.layerId = layerId; + } + + /** + * Returns the connection type the path is represented as. + * + * @return The connection type the path is represented as. + */ + public ConnectionType getConnectionType() { + return connectionType; + } + + /** + * Creates a copy of this object, with the given connection type. + * + * @param connectionType The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withConnectionType(ConnectionType connectionType) { + return new Layout(connectionType, controlPoints, layerId); + } + + /** + * Returns the control points describing the way the path is drawn. + * Returns an empty list if connection type is not {@link ConnectionType#BEZIER}, + * {@link ConnectionType#BEZIER_3} or {@link ConnectionType#POLYPATH}. + * + * @return The control points describing the way the path is drawn. + */ + public List getControlPoints() { + return Collections.unmodifiableList(controlPoints); + } + + /** + * Creates a copy of this object, with the given control points. + * + * @param controlPoints The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withControlPoints(List controlPoints) { + return new Layout(connectionType, controlPoints, layerId); + } + + /** + * Returns the ID of the layer on which the path is to be drawn. + * + * @return The layer ID. + */ + public int getLayerId() { + return layerId; + } + + /** + * Creates a copy of this object, with the given layer ID. + * + * @param layerId The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withLayer(int layerId) { + return new Layout(connectionType, controlPoints, layerId); + } + + /** + * The connection type a path is represented as. + */ + public enum ConnectionType { + + /** + * A direct connection. + */ + DIRECT, + /** + * An elbow connection. + */ + ELBOW, + /** + * A slanted connection. + */ + SLANTED, + /** + * A polygon path with any number of vertecies. + */ + POLYPATH, + /** + * A bezier curve with 2 control points. + */ + BEZIER, + /** + * A bezier curve with 3 control points. + */ + BEZIER_3; + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/PeripheralInformation.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/PeripheralInformation.java new file mode 100644 index 0000000..9540a6e --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/PeripheralInformation.java @@ -0,0 +1,202 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.io.Serializable; +import java.util.Objects; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.peripherals.PeripheralJob; + +/** + * Contains details about a peripheral device a location may represent. + */ +public class PeripheralInformation + implements + Serializable { + + /** + * A token for which a location/peripheral device is currently reserved. + */ + @Nullable + private final String reservationToken; + /** + * This peripheral device's current state. + */ + @Nonnull + private final State state; + /** + * This peripheral device's current processing state. + */ + @Nonnull + private final ProcState procState; + /** + * A reference to the peripheral job this peripheral device is currently processing. + */ + @Nullable + private final TCSObjectReference peripheralJob; + + /** + * Creates a new instance. + */ + public PeripheralInformation() { + this(null, State.NO_PERIPHERAL, ProcState.IDLE, null); + } + + private PeripheralInformation( + @Nullable + String reservationToken, + @Nonnull + State state, + @Nonnull + ProcState procState, + @Nullable + TCSObjectReference peripheralJob + ) { + this.reservationToken = reservationToken; + this.state = Objects.requireNonNull(state, "state"); + this.procState = Objects.requireNonNull(procState, "procState"); + this.peripheralJob = peripheralJob; + } + + /** + * Returns a token for which a location/peripheral device is currently reserved. + * + * @return A token for which a location/peripheral device is currently reserved. + */ + @Nullable + public String getReservationToken() { + return reservationToken; + } + + /** + * Creates a copy of this object, with the given reservation token. + * + * @param reservationToken The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralInformation withReservationToken( + @Nullable + String reservationToken + ) { + return new PeripheralInformation(reservationToken, state, procState, peripheralJob); + } + + /** + * Returns the peripheral device's current state. + * + * @return The peripheral device's current state. + */ + @Nonnull + public State getState() { + return state; + } + + /** + * Creates a copy of this object, with the given state. + * + * @param state The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralInformation withState( + @Nonnull + State state + ) { + return new PeripheralInformation(reservationToken, state, procState, peripheralJob); + } + + /** + * Returns the peripheral device's current processing state. + * + * @return The peripheral device's current processing state. + */ + @Nonnull + public ProcState getProcState() { + return procState; + } + + /** + * Creates a copy of this object, with the given processing state. + * + * @param procState The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralInformation withProcState( + @Nonnull + ProcState procState + ) { + return new PeripheralInformation(reservationToken, state, procState, peripheralJob); + } + + /** + * Returns a reference to the peripheral job this peripheral device is currently processing. + * + * @return A reference to the peripheral job this peripheral device is currently processing, + * or {@code null}, it is not processing any peripheral job at the moment. + */ + @Nullable + public TCSObjectReference getPeripheralJob() { + return peripheralJob; + } + + /** + * Creates a copy of this object, with the given peripheral job. + * + * @param peripheralJob The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralInformation withPeripheralJob( + @Nullable + TCSObjectReference peripheralJob + ) { + return new PeripheralInformation(reservationToken, state, procState, peripheralJob); + } + + /** + * The elements of this enumeration describe the various possible states of a peripheral device. + */ + public enum State { + /** + * Indicates that the location the {@link PeripheralInformation} belongs to doesn't represent + * a peripheral device. + */ + NO_PERIPHERAL, + /** + * The peripheral device's current state is unknown, e.g. because communication with + * it is currently not possible for some reason. + */ + UNKNOWN, + /** + * The peripheral device's state is known and it's not in an error state, but it is + * not available for receiving jobs. + */ + UNAVAILABLE, + /** + * There is a problem with the peripheral device. + */ + ERROR, + /** + * The peripheral device is currently idle/available for processing jobs. + */ + IDLE, + /** + * The peripheral device is processing a job. + */ + EXECUTING + } + + /** + * A peripheral device's processing state as seen by the peripheral job dispatcher. + */ + public enum ProcState { + /** + * The peripheral device is currently not processing a job. + */ + IDLE, + /** + * The peripheral device is currently processing a job. + */ + PROCESSING_JOB + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/PlantModel.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/PlantModel.java new file mode 100644 index 0000000..aa06c52 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/PlantModel.java @@ -0,0 +1,389 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.opentcs.data.model.visualization.Layer; +import org.opentcs.data.model.visualization.LayerGroup; +import org.opentcs.data.model.visualization.VisualLayout; + +/** + * An immutable representation of a complete plant model's state. + */ +public class PlantModel + implements + Serializable { + + private final String name; + private final Map properties; + private final Set points; + private final Set paths; + private final Set locationTypes; + private final Set locations; + private final Set blocks; + private final Set vehicles; + private final VisualLayout visualLayout; + + /** + * Creates a new instance. + * + * @param name The model's name. + */ + public PlantModel( + @Nonnull + String name + ) { + this( + name, Map.of(), Set.of(), Set.of(), Set.of(), Set.of(), Set.of(), Set.of(), + defaultVisualLayout() + ); + } + + private PlantModel( + @Nonnull + String name, + @Nonnull + Map properties, + @Nonnull + Set points, + @Nonnull + Set paths, + @Nonnull + Set locationTypes, + @Nonnull + Set locations, + @Nonnull + Set blocks, + @Nonnull + Set vehicles, + @Nonnull + VisualLayout visualLayout + ) { + this.name = requireNonNull(name, "name"); + this.properties = Map.copyOf(properties); + this.points = Set.copyOf(points); + this.paths = Set.copyOf(paths); + this.locationTypes = Set.copyOf(locationTypes); + this.locations = Set.copyOf(locations); + this.blocks = Set.copyOf(blocks); + this.vehicles = Set.copyOf(vehicles); + this.visualLayout = requireNonNull(visualLayout, "visualLayout"); + } + + /** + * Returns the name of the plant model. + * + * @return The name of the plant model. + */ + @Nonnull + public String getName() { + return name; + } + + /** + * Returns the plant model's properties. + * + * @return The plant model's properties. + */ + @Nonnull + public Map getProperties() { + return properties; + } + + /** + * Returns a copy of this plant model, with its properties replaced by the given ones. + * + * @param properties The properties. + * @return A copy of this plant model, with its properties replaced by the given ones. + */ + public PlantModel withProperties(Map properties) { + return new PlantModel( + name, + properties, + points, + paths, + locationTypes, + locations, + blocks, + vehicles, + visualLayout + ); + } + + /** + * Returns the points in this plant model. + * + * @return The points in this plant model. + */ + @Nonnull + public Set getPoints() { + return points; + } + + /** + * Returns a copy of this plant model, with its points replaced by the given ones. + * + * @param points The points. + * @return A copy of this plant model, with its points replaced by the given ones. + */ + public PlantModel withPoints( + @Nonnull + Set points + ) { + return new PlantModel( + name, + properties, + points, + paths, + locationTypes, + locations, + blocks, + vehicles, + visualLayout + ); + } + + /** + * Returns the paths in this plant model. + * + * @return The paths in this plant model. + */ + @Nonnull + public Set getPaths() { + return paths; + } + + /** + * Returns a copy of this plant model, with its paths replaced by the given ones. + * + * @param paths The paths. + * @return A copy of this plant model, with its paths replaced by the given ones. + */ + public PlantModel withPaths( + @Nonnull + Set paths + ) { + return new PlantModel( + name, + properties, + points, + paths, + locationTypes, + locations, + blocks, + vehicles, + visualLayout + ); + } + + /** + * Returns the location types in this plant model. + * + * @return The location types in this plant model. + */ + @Nonnull + public Set getLocationTypes() { + return locationTypes; + } + + /** + * Returns a copy of this plant model, with its location types replaced by the given ones. + * + * @param locationTypes The location types. + * @return A copy of this plant model, with its location types replaced by the given ones. + */ + public PlantModel withLocationTypes( + @Nonnull + Set locationTypes + ) { + return new PlantModel( + name, + properties, + points, + paths, + locationTypes, + locations, + blocks, + vehicles, + visualLayout + ); + } + + /** + * Returns the locations in this plant model. + * + * @return The locations in this plant model. + */ + @Nonnull + public Set getLocations() { + return locations; + } + + /** + * Returns a copy of this plant model, with its locations replaced by the given ones. + * + * @param locations The locations. + * @return A copy of this plant model, with its locations replaced by the given ones. + */ + public PlantModel withLocations( + @Nonnull + Set locations + ) { + return new PlantModel( + name, + properties, + points, + paths, + locationTypes, + locations, + blocks, + vehicles, + visualLayout + ); + } + + /** + * Returns the blocks in this plant model. + * + * @return The blocks in this plant model. + */ + @Nonnull + public Set getBlocks() { + return blocks; + } + + /** + * Returns a copy of this plant model, with its blocks replaced by the given ones. + * + * @param blocks The blocks. + * @return A copy of this plant model, with its blocks replaced by the given ones. + */ + public PlantModel withBlocks( + @Nonnull + Set blocks + ) { + return new PlantModel( + name, + properties, + points, + paths, + locationTypes, + locations, + blocks, + vehicles, + visualLayout + ); + } + + /** + * Returns the vehicles in this plant model. + * + * @return The vehicles in this plant model. + */ + @Nonnull + public Set getVehicles() { + return vehicles; + } + + /** + * Returns a copy of this plant model, with its vehicles replaced by the given ones. + * + * @param vehicles The vehicles. + * @return A copy of this plant model, with its vehicles replaced by the given ones. + */ + public PlantModel withVehicles( + @Nonnull + Set vehicles + ) { + return new PlantModel( + name, + properties, + points, + paths, + locationTypes, + locations, + blocks, + vehicles, + visualLayout + ); + } + + /** + * Returns the visual layout in this plant model. + * + * @return The visual layout in this plant model. + */ + @Nonnull + public VisualLayout getVisualLayout() { + return visualLayout; + } + + /** + * Returns a copy of this plant model, with its visual layout replaced by the given one. + * + * @param visualLayout The visual layout to be set. + * @return A copy of this plant model, with its visual layout replaced by the given one. + */ + public PlantModel withVisualLayout( + @Nonnull + VisualLayout visualLayout + ) { + return new PlantModel( + name, + properties, + points, + paths, + locationTypes, + locations, + blocks, + vehicles, + visualLayout + ); + } + + @Override + public String toString() { + return "PlantModel{" + + "name=" + name + + ", properties=" + properties + + ", points=" + points + + ", paths=" + paths + + ", locationTypes=" + locationTypes + + ", locations=" + locations + + ", blocks=" + blocks + + ", vehicles=" + vehicles + + ", visualLayout=" + visualLayout + + '}'; + } + + private static VisualLayout defaultVisualLayout() { + return new VisualLayout(ModelConstants.DEFAULT_VISUAL_LAYOUT_NAME) + .withLayers( + List.of( + new Layer( + ModelConstants.DEFAULT_LAYER_ID, + ModelConstants.DEFAULT_LAYER_ORDINAL, + true, + ModelConstants.DEFAULT_LAYER_NAME, + ModelConstants.DEFAULT_LAYER_GROUP_ID + ) + ) + ) + .withLayerGroups( + List.of( + new LayerGroup( + ModelConstants.DEFAULT_LAYER_GROUP_ID, + ModelConstants.DEFAULT_LAYER_GROUP_NAME, + true + ) + ) + ); + } + +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/Point.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/Point.java new file mode 100644 index 0000000..8e4a68a --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/Point.java @@ -0,0 +1,652 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model; + +import static java.util.Objects.requireNonNull; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.util.annotations.ScheduledApiChange; + +/** + * A point in the driving course at which a {@link Vehicle} may be located. + * + * @see Path + */ +public class Point + extends + TCSResource + implements + Serializable { + + /** + * The pose of the vehicle at this point. + */ + private final Pose pose; + /** + * This point's type. + */ + private final Type type; + /** + * A set of references to paths ending in this point. + */ + private final Set> incomingPaths; + /** + * A set of references to paths originating in this point. + */ + private final Set> outgoingPaths; + /** + * A set of links attached to this point. + */ + private final Set attachedLinks; + /** + * A reference to the vehicle occupying this point. + */ + private final TCSObjectReference occupyingVehicle; + /** + * A map of envelope keys to envelopes that vehicles located at this point may occupy. + */ + private final Map vehicleEnvelopes; + /** + * The maximum bounding box (in mm) that a vehicle at this point is allowed to have. + */ + private final BoundingBox maxVehicleBoundingBox; + /** + * The information regarding the graphical representation of this point. + */ + private final Layout layout; + + /** + * Creates a new point with the given name. + * + * @param name This point's name. + */ + public Point(String name) { + super(name); + this.pose = new Pose(new Triple(0, 0, 0), Double.NaN); + this.type = Type.HALT_POSITION; + this.incomingPaths = new HashSet<>(); + this.outgoingPaths = new HashSet<>(); + this.attachedLinks = new HashSet<>(); + this.occupyingVehicle = null; + this.vehicleEnvelopes = Map.of(); + this.maxVehicleBoundingBox = new BoundingBox(1000, 1000, 1000); + this.layout = new Layout(); + } + + private Point( + String name, + Map properties, + ObjectHistory history, + Pose pose, + Type type, + Set> incomingPaths, + Set> outgoingPaths, + Set attachedLinks, + TCSObjectReference occupyingVehicle, + Map vehicleEnvelopes, + BoundingBox maxVehicleBoundingBox, + Layout layout + ) { + super(name, properties, history); + this.pose = requireNonNull(pose, "pose"); + requireNonNull(pose.getPosition(), "A point requires a pose with a position."); + this.type = requireNonNull(type, "type"); + this.incomingPaths = setWithoutNullValues(requireNonNull(incomingPaths, "incomingPaths")); + this.outgoingPaths = setWithoutNullValues(requireNonNull(outgoingPaths, "outgoingPaths")); + this.attachedLinks = setWithoutNullValues(requireNonNull(attachedLinks, "attachedLinks")); + this.occupyingVehicle = occupyingVehicle; + this.vehicleEnvelopes = requireNonNull(vehicleEnvelopes, "vehicleEnvelopes"); + this.maxVehicleBoundingBox = requireNonNull(maxVehicleBoundingBox, "maxVehicleBoundingBox"); + this.layout = requireNonNull(layout, "layout"); + } + + @Override + public Point withProperty(String key, String value) { + return new Point( + getName(), + propertiesWith(key, value), + getHistory(), + pose, + type, + incomingPaths, + outgoingPaths, + attachedLinks, + occupyingVehicle, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + @Override + public Point withProperties(Map properties) { + return new Point( + getName(), + properties, + getHistory(), + pose, + type, + incomingPaths, + outgoingPaths, + attachedLinks, + occupyingVehicle, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + @Override + public TCSObject withHistoryEntry(ObjectHistory.Entry entry) { + return new Point( + getName(), + getProperties(), + getHistory().withEntryAppended(entry), + pose, + type, + incomingPaths, + outgoingPaths, + attachedLinks, + occupyingVehicle, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + @Override + public TCSObject withHistory(ObjectHistory history) { + return new Point( + getName(), + getProperties(), + history, + pose, + type, + incomingPaths, + outgoingPaths, + attachedLinks, + occupyingVehicle, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + /** + * Returns the pose of the vehicle at this point. + * + * @return The pose of the vehicle at this point. + */ + public Pose getPose() { + return pose; + } + + /** + * Creates a copy of this object, with the given pose. + * + * @param pose The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Point withPose(Pose pose) { + return new Point( + getName(), + getProperties(), + getHistory(), + pose, + type, + incomingPaths, + outgoingPaths, + attachedLinks, + occupyingVehicle, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + /** + * Returns this point's type. + * + * @return This point's type. + */ + public Type getType() { + return type; + } + + /** + * Creates a copy of this object, with the given type. + * + * @param type The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Point withType(Type type) { + return new Point( + getName(), + getProperties(), + getHistory(), + pose, + type, + incomingPaths, + outgoingPaths, + attachedLinks, + occupyingVehicle, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + /** + * Checks whether parking a vehicle on this point is allowed. + *

+ * This method is a convenience method; its return value is equal to + * getType().equals(Point.Type.PARK_POSITION). + *

+ * + * @return true if, and only if, parking is allowed on this + * point. + */ + public boolean isParkingPosition() { + return type.equals(Type.PARK_POSITION); + } + + /** + * Checks whether halting on this point is allowed. + *

+ * This method is a convenience method; its return value is equal to + * getType().equals(Point.Type.PARK_POSITION) || + * getType().equals(Point.Type.HALT_POSITION). + *

+ * + * @return true if, and only if, halting is allowed on this + * point. + * @deprecated Will be removed without replacement. With openTCS 6.0, the point type + * {@code REPORT_POSITION} was removed, which makes this method redundant, as all remaining point + * types allow halting. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public boolean isHaltingPosition() { + return type.equals(Type.PARK_POSITION) || type.equals(Type.HALT_POSITION); + } + + /** + * Returns a reference to the vehicle occupying this point. + * + * @return A reference to the vehicle occupying this point, or + * null, if this point isn't currently occupied by any vehicle. + */ + public TCSObjectReference getOccupyingVehicle() { + return occupyingVehicle; + } + + /** + * Creates a copy of this object, with the given occupying vehicle. + * + * @param occupyingVehicle The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Point withOccupyingVehicle(TCSObjectReference occupyingVehicle) { + return new Point( + getName(), + getProperties(), + getHistory(), + pose, + type, + incomingPaths, + outgoingPaths, + attachedLinks, + occupyingVehicle, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + /** + * Returns a set of references to paths ending in this point. + * + * @return A set of references to paths ending in this point. + */ + public Set> getIncomingPaths() { + return Collections.unmodifiableSet(incomingPaths); + } + + /** + * Creates a copy of this object, with the given incoming paths. + * + * @param incomingPaths The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Point withIncomingPaths(Set> incomingPaths) { + return new Point( + getName(), + getProperties(), + getHistory(), + pose, + type, + incomingPaths, + outgoingPaths, + attachedLinks, + occupyingVehicle, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + /** + * Returns a set of references to paths originating in this point. + * + * @return A set of references to paths originating in this point. + */ + public Set> getOutgoingPaths() { + return Collections.unmodifiableSet(outgoingPaths); + } + + /** + * Creates a copy of this object, with the given outgoing paths. + * + * @param outgoingPaths The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Point withOutgoingPaths(Set> outgoingPaths) { + return new Point( + getName(), + getProperties(), + getHistory(), + pose, + type, + incomingPaths, + outgoingPaths, + attachedLinks, + occupyingVehicle, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + /** + * Returns a set of links attached to this point. + * + * @return A set of links attached to this point. + */ + public Set getAttachedLinks() { + return Collections.unmodifiableSet(attachedLinks); + } + + /** + * Creates a copy of this object, with the given attached links. + * + * @param attachedLinks The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Point withAttachedLinks(Set attachedLinks) { + return new Point( + getName(), + getProperties(), + getHistory(), + pose, + type, + incomingPaths, + outgoingPaths, + attachedLinks, + occupyingVehicle, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + /** + * Returns a map of envelope keys to envelopes that vehicles located at this point may occupy. + * + * @return A map of envelope keys to envelopes that vehicles located at this point may occupy. + */ + public Map getVehicleEnvelopes() { + return vehicleEnvelopes; + } + + /** + * Creates a copy of this object, with the given vehicle envelopes. + * + * @param vehicleEnvelopes The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Point withVehicleEnvelopes(Map vehicleEnvelopes) { + return new Point( + getName(), + getProperties(), + getHistory(), + pose, + type, + incomingPaths, + outgoingPaths, + attachedLinks, + occupyingVehicle, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + /** + * Returns the maximum bounding box (in mm) that a vehicle at this point is allowed to have. + *

+ * The bounding box is oriented according to the orientation angle of this point so that the + * longitudinal axis of the bounding box runs parallel to the longitudinal axis of a vehicle + * located at this point. For the reference point offset, positive x values indicate an offset + * in the forward direction of the vehicle, positive y values an offset towards the lefthand + * side. + *

+ * + * @return The maximum bounding box (in mm) that a vehicle at this point is allowed to have. + */ + public BoundingBox getMaxVehicleBoundingBox() { + return maxVehicleBoundingBox; + } + + /** + * Creates a copy of this object, with the given maximum vehicle bounding box. + *

+ * The bounding box is oriented according to the orientation angle of this point so that the + * longitudinal axis of the bounding box runs parallel to the longitudinal axis of a vehicle + * located at this point. For the reference point offset, positive x values indicate an offset + * in the forward direction of the vehicle, positive y values an offset towards the lefthand + * side. + *

+ * + * @param maxVehicleBoundingBox The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Point withMaxVehicleBoundingBox(BoundingBox maxVehicleBoundingBox) { + return new Point( + getName(), + getProperties(), + getHistory(), + pose, + type, + incomingPaths, + outgoingPaths, + attachedLinks, + occupyingVehicle, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + /** + * Returns the information regarding the graphical representation of this point. + * + * @return The information regarding the graphical representation of this point. + */ + public Layout getLayout() { + return layout; + } + + /** + * Creates a copy of this object, with the given layout. + * + * @param layout The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Point withLayout(Layout layout) { + return new Point( + getName(), + getProperties(), + getHistory(), + pose, + type, + incomingPaths, + outgoingPaths, + attachedLinks, + occupyingVehicle, + vehicleEnvelopes, + maxVehicleBoundingBox, + layout + ); + } + + /** + * Describes the types of positions in a driving course. + */ + public enum Type { + + /** + * Indicates a position at which a vehicle may halt temporarily, e.g. for executing an + * operation. + * The vehicle is also expected to report in when it arrives at such a position. + * It may not park here for longer than necessary, though. + */ + HALT_POSITION, + /** + * Indicates a position at which a vehicle may halt for longer periods of time when it is not + * processing orders. + * The vehicle is also expected to report in when it arrives at such a position. + */ + PARK_POSITION; + } + + /** + * Contains information regarding the graphical representation of a point. + */ + public static class Layout + implements + Serializable { + + /** + * The coordinates at which the point is to be drawn (in mm). + */ + private final Couple position; + /** + * The offset of the label's position to the point's position (in lu). + */ + private final Couple labelOffset; + /** + * The ID of the layer on which the point is to be drawn. + */ + private final int layerId; + + /** + * Creates a new instance. + */ + public Layout() { + this(new Couple(0, 0), new Couple(0, 0), 0); + } + + /** + * Creates a new instance. + * + * @param position The coordinates at which the point is to be drawn (in mm). + * @param labelOffset The offset of the label's position to the point's position (in lu). + * @param layerId The ID of the layer on which the point is to be drawn. + */ + public Layout( + Couple position, + Couple labelOffset, + int layerId + ) { + this.position = requireNonNull(position, "position"); + this.labelOffset = requireNonNull(labelOffset, "labelOffset"); + this.layerId = layerId; + } + + /** + * Returns the coordinates at which the point is to be drawn (in mm). + * + * @return The coordinates at which the point is to be drawn (in mm). + */ + public Couple getPosition() { + return position; + } + + /** + * Creates a copy of this object, with the given position. + * + * @param position The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withPosition(Couple position) { + return new Layout( + position, + labelOffset, + layerId + ); + } + + /** + * Returns the offset of the label's position to the point's position (in lu). + * + * @return The offset of the label's position to the point's position (in lu). + */ + public Couple getLabelOffset() { + return labelOffset; + } + + /** + * Creates a copy of this object, with the given X label offset. + * + * @param labelOffset The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withLabelOffset(Couple labelOffset) { + return new Layout( + position, + labelOffset, + layerId + ); + } + + /** + * Returns the ID of the layer on which the point is to be drawn. + * + * @return The layer ID. + */ + public int getLayerId() { + return layerId; + } + + /** + * Creates a copy of this object, with the given layer ID. + * + * @param layerId The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withLayerId(int layerId) { + return new Layout( + position, + labelOffset, + layerId + ); + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/Pose.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/Pose.java new file mode 100644 index 0000000..3a14710 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/Pose.java @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model; + +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nullable; +import java.io.Serializable; +import java.util.Objects; + +/** + * A pose consisting of a position and an orientation angle. + */ +public class Pose + implements + Serializable { + + /** + * The position/coordinates in mm. + */ + private final Triple position; + /** + * The orientation angle in degrees (-360..360). May be Double.NaN if unknown/undefined. + */ + private final double orientationAngle; + + /** + * Creates a new instance. + * + * @param position The position/coordinates in mm. + * @param orientationAngle The orientation angle in degrees (-360..360). May be Double.NaN if + * unknown/undefined. + */ + public Pose( + @Nullable + Triple position, + double orientationAngle + ) { + this.position = position; + checkArgument( + Double.isNaN(orientationAngle) + || (orientationAngle >= -360.0 && orientationAngle <= 360.0), + "orientationAngle not Double.NaN or in [-360..360]: %s", + orientationAngle + ); + this.orientationAngle = orientationAngle; + } + + /** + * The position/coordinates in mm. + * + * @return The position/coordinates in mm. + */ + @Nullable + public Triple getPosition() { + return position; + } + + /** + * Creates a copy of this object, with the given position. + * + * @param position The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Pose withPosition( + @Nullable + Triple position + ) { + return new Pose(position, orientationAngle); + } + + /** + * The orientation angle in degrees (-360..360). May be Double.NaN if unknown/undefined. + * + * @return The orientation angle in degrees, or Double.NaN. + */ + public double getOrientationAngle() { + return orientationAngle; + } + + /** + * Creates a copy of this object, with the given orientation angle. + * + * @param orientationAngle The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Pose withOrientationAngle(double orientationAngle) { + return new Pose(position, orientationAngle); + } + + @Override + public int hashCode() { + int hash = 5; + hash = 47 * hash + Objects.hashCode(this.position); + hash = 47 * hash + + (int) (Double.doubleToLongBits(this.orientationAngle) + ^ (Double.doubleToLongBits(this.orientationAngle) >>> 32)); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Pose)) { + return false; + } + if (this == obj) { + return true; + } + final Pose other = (Pose) obj; + if (Double.doubleToLongBits(this.orientationAngle) != Double.doubleToLongBits( + other.orientationAngle + )) { + return false; + } + return Objects.equals(this.position, other.position); + } + + @Override + public String toString() { + return "Pose{" + "position=" + position + ", orientationAngle=" + orientationAngle + '}'; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/TCSResource.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/TCSResource.java new file mode 100644 index 0000000..3811439 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/TCSResource.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model; + +import java.io.Serializable; +import java.util.Map; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.TCSObject; + +/** + * Describes a resource that {@link Vehicle}s may claim for exclusive usage. + * + * @see Scheduler + * @param The actual resource class. + */ +public abstract class TCSResource> + extends + TCSObject + implements + Serializable { + + /** + * Creates a new TCSResource. + * + * @param name The new resource's name. + */ + @SuppressWarnings("this-escape") + protected TCSResource(String name) { + super(name); + reference = new TCSResourceReference<>(this); + } + + /** + * Creates a new TCSResource. + * + * @param name The new resource's name. + * @param properties A set of properties (key-value pairs) associated with this object. + * @param history A history of events related to this object. + */ + @SuppressWarnings("this-escape") + protected TCSResource(String name, Map properties, ObjectHistory history) { + super(name, properties, history); + reference = new TCSResourceReference<>(this); + } + + // Methods inherited from TCSObject start here. + @Override + public TCSResourceReference getReference() { + return (TCSResourceReference) reference; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/TCSResourceReference.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/TCSResourceReference.java new file mode 100644 index 0000000..4de2fa5 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/TCSResourceReference.java @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model; + +import java.io.Serializable; +import org.opentcs.data.TCSObjectReference; + +/** + * A transient reference to a {@link TCSResource}. + * + * @param The actual resource class. + */ +public class TCSResourceReference> + extends + TCSObjectReference + implements + Serializable { + + /** + * Creates a new TCSResourceReference. + * + * @param newReferent The resource this reference references. + */ + protected TCSResourceReference(TCSResource newReferent) { + super(newReferent); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/Triple.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/Triple.java new file mode 100644 index 0000000..f3a0ddd --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/Triple.java @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model; + +import java.io.Serializable; + +/** + * A generic 3-tuple of long integer values, usable for 3D coordinates and vectors, for instance. + */ +public class Triple + implements + Serializable { + + /** + * The X coordinate. + */ + private final long x; + /** + * The Y coordinate. + */ + private final long y; + /** + * The Z coordinate. + */ + private final long z; + + /** + * Creates a new Triple with the given values. + * + * @param x The X coordinate. + * @param y The Y coordinate. + * @param z The Z coordindate. + */ + public Triple(long x, long y, long z) { + this.x = x; + this.y = y; + this.z = z; + } + + /** + * Returns the x coordinate. + * + * @return x + */ + public long getX() { + return x; + } + + /** + * Returns the y coordinate. + * + * @return y + */ + public long getY() { + return y; + } + + /** + * Returns the z coordinate. + * + * @return z + */ + public long getZ() { + return z; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Triple)) { + return false; + } + if (this == obj) { + return true; + } + Triple other = (Triple) obj; + if (this.x != other.x) { + return false; + } + if (this.y != other.y) { + return false; + } + if (this.z != other.z) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return (int) (x ^ y ^ z); + } + + @Override + public String toString() { + return "Triple{" + "x=" + x + ", y=" + y + ", z=" + z + '}'; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/Vehicle.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/Vehicle.java new file mode 100644 index 0000000..c2a64fe --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/Vehicle.java @@ -0,0 +1,1959 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; +import static org.opentcs.util.Assertions.checkInRange; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.awt.Color; +import java.io.Serializable; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.order.OrderConstants; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.drivers.vehicle.LoadHandlingDevice; +import org.opentcs.util.annotations.ScheduledApiChange; + +/** + * Describes a vehicle's current state. + */ +public class Vehicle + extends + TCSObject + implements + Serializable { + + /** + * The key for a property to store the class name of the preferred communication adapter (factory) + * for this vehicle. + */ + public static final String PREFERRED_ADAPTER = "tcs:preferredAdapterClass"; + /** + * The vehicle's bounding box (in mm). + */ + private final BoundingBox boundingBox; + /** + * Contains information regarding the energy level threshold values of the vehicle. + */ + private final EnergyLevelThresholdSet energyLevelThresholdSet; + /** + * This vehicle's remaining energy (in percent of the maximum). + */ + private final int energyLevel; + /** + * This vehicle's maximum velocity (in mm/s). + */ + private final int maxVelocity; + /** + * This vehicle's maximum reverse velocity (in mm/s). + */ + private final int maxReverseVelocity; + /** + * The operation the vehicle's current communication adapter accepts as a command to recharge the + * vehicle. + */ + private final String rechargeOperation; + /** + * The current (state of the) load handling devices of this vehicle. + */ + private final List loadHandlingDevices; + /** + * This vehicle's current state. + */ + private final State state; + /** + * This vehicle's current processing state. + */ + private final ProcState procState; + /** + * This vehicle's integration level. + */ + private final IntegrationLevel integrationLevel; + /** + * Whether this vehicle is currently paused. + */ + private final boolean paused; + /** + * A reference to the transport order this vehicle is currently processing. + */ + private final TCSObjectReference transportOrder; + /** + * A reference to the order sequence this vehicle is currently processing. + */ + private final TCSObjectReference orderSequence; + /** + * The set of transport order types this vehicle is allowed to process. + */ + private final Set allowedOrderTypes; + /** + * The resources this vehicle has claimed for future allocation. + */ + private final List>> claimedResources; + /** + * The resources this vehicle has allocated. + */ + private final List>> allocatedResources; + /** + * A reference to the point which this vehicle currently occupies. + */ + private final TCSObjectReference currentPosition; + /** + * A reference to the point which this vehicle is expected to be seen at next. + */ + private final TCSObjectReference nextPosition; + /** + * The vehicle's pose containing its precise position and current orientation angle. + */ + private final Pose pose; + /** + * The key for selecting the envelope to be used for resources the vehicle occupies. + */ + private final String envelopeKey; + /** + * The information regarding the graphical representation of this vehicle. + */ + private final Layout layout; + + /** + * Creates a new vehicle. + * + * @param name The new vehicle's name. + */ + public Vehicle(String name) { + super(name); + this.boundingBox = new BoundingBox(1000, 1000, 1000); + this.energyLevelThresholdSet = new EnergyLevelThresholdSet(30, 90, 30, 90); + this.maxVelocity = 1000; + this.maxReverseVelocity = 1000; + this.rechargeOperation = "CHARGE"; + this.procState = ProcState.IDLE; + this.transportOrder = null; + this.orderSequence = null; + this.allowedOrderTypes = new HashSet<>(Arrays.asList(OrderConstants.TYPE_ANY)); + this.claimedResources = List.of(); + this.allocatedResources = List.of(); + this.state = State.UNKNOWN; + this.integrationLevel = IntegrationLevel.TO_BE_RESPECTED; + this.paused = false; + this.currentPosition = null; + this.nextPosition = null; + this.pose = new Pose(null, Double.NaN); + this.energyLevel = 100; + this.loadHandlingDevices = List.of(); + this.envelopeKey = null; + this.layout = new Layout(); + } + + private Vehicle( + String name, + Map properties, + ObjectHistory history, + BoundingBox boundingBox, + EnergyLevelThresholdSet energyLevelThresholdSet, + int maxVelocity, + int maxReverseVelocity, + String rechargeOperation, + ProcState procState, + TCSObjectReference transportOrder, + TCSObjectReference orderSequence, + Set allowedOrderTypes, + List>> claimedResources, + List>> allocatedResources, + State state, + IntegrationLevel integrationLevel, + boolean paused, + TCSObjectReference currentPosition, + TCSObjectReference nextPosition, + Pose pose, + int energyLevel, + List loadHandlingDevices, + String envelopeKey, + Layout layout + ) { + super(name, properties, history); + this.boundingBox = requireNonNull(boundingBox, "boundingBox"); + this.energyLevelThresholdSet = requireNonNull( + energyLevelThresholdSet, "energyLevelThresholdSet" + ); + checkArgument( + energyLevelThresholdSet.getEnergyLevelCritical() + <= energyLevelThresholdSet.getEnergyLevelGood(), + "energyLevelCritical (%s) not <= energyLevelGood (%s)", + energyLevelThresholdSet.getEnergyLevelCritical(), + energyLevelThresholdSet.getEnergyLevelGood() + ); + checkArgument( + energyLevelThresholdSet.getEnergyLevelSufficientlyRecharged() + <= energyLevelThresholdSet.getEnergyLevelFullyRecharged(), + "energyLevelSufficientlyRecharged (%s) not <= energyLevelFullyRecharged (%s)", + energyLevelThresholdSet.getEnergyLevelSufficientlyRecharged(), + energyLevelThresholdSet.getEnergyLevelFullyRecharged() + ); + this.maxVelocity = checkInRange(maxVelocity, 0, Integer.MAX_VALUE, "maxVelocity"); + this.maxReverseVelocity = checkInRange( + maxReverseVelocity, + 0, + Integer.MAX_VALUE, + "maxReverseVelocity" + ); + this.rechargeOperation = requireNonNull(rechargeOperation, "rechargeOperation"); + this.procState = requireNonNull(procState, "procState"); + this.transportOrder = transportOrder; + this.orderSequence = orderSequence; + this.allowedOrderTypes = requireNonNull(allowedOrderTypes, "allowedOrderTypes"); + this.claimedResources = requireNonNull(claimedResources, "claimedResources"); + this.allocatedResources = requireNonNull(allocatedResources, "allocatedResources"); + this.state = requireNonNull(state, "state"); + this.integrationLevel = requireNonNull(integrationLevel, "integrationLevel"); + this.paused = paused; + this.currentPosition = currentPosition; + this.nextPosition = nextPosition; + this.pose = requireNonNull(pose, "pose"); + this.energyLevel = checkInRange(energyLevel, 0, 100, "energyLevel"); + this.loadHandlingDevices = listWithoutNullValues( + requireNonNull( + loadHandlingDevices, + "loadHandlingDevices" + ) + ); + this.envelopeKey = envelopeKey; + this.layout = requireNonNull(layout, "layout"); + } + + @Override + public Vehicle withProperty(String key, String value) { + return new Vehicle( + getName(), + propertiesWith(key, value), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + @Override + public Vehicle withProperties(Map properties) { + return new Vehicle( + getName(), + properties, + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + @Override + public TCSObject withHistoryEntry(ObjectHistory.Entry entry) { + return new Vehicle( + getName(), + getProperties(), + getHistory().withEntryAppended(entry), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + @Override + public TCSObject withHistory(ObjectHistory history) { + return new Vehicle( + getName(), + getProperties(), + history, + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Returns this vehicle's remaining energy (in percent of the maximum). + * + * @return This vehicle's remaining energy. + */ + public int getEnergyLevel() { + return energyLevel; + } + + /** + * Creates a copy of this object, with the given energy level. + * + * @param energyLevel The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Vehicle withEnergyLevel(int energyLevel) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Checks whether the vehicle's energy level is critical. + * + * @return true if, and only if, the vehicle's energy level is + * critical. + */ + public boolean isEnergyLevelCritical() { + return energyLevel <= energyLevelThresholdSet.getEnergyLevelCritical(); + } + + /** + * Checks whether the vehicle's energy level is degraded (not good + * any more). + * + * @return true if, and only if, the vehicle's energy level is + * degraded. + */ + public boolean isEnergyLevelDegraded() { + return energyLevel <= energyLevelThresholdSet.getEnergyLevelGood(); + } + + /** + * Checks whether the vehicle's energy level is good. + * + * @return true if, and only if, the vehicle's energy level is + * good. + */ + public boolean isEnergyLevelGood() { + return energyLevel > energyLevelThresholdSet.getEnergyLevelGood(); + } + + /** + * Checks whether the vehicle's energy level is fully recharged. + * + * @return true if, and only if, the vehicle's energy level is + * fully recharged. + */ + public boolean isEnergyLevelFullyRecharged() { + return energyLevel >= energyLevelThresholdSet.getEnergyLevelFullyRecharged(); + } + + /** + * Checks whether the vehicle's energy level is sufficiently recharged. + * + * @return true if, and only if, the vehicle's energy level is + * sufficiently recharged. + */ + public boolean isEnergyLevelSufficientlyRecharged() { + return energyLevel >= energyLevelThresholdSet.getEnergyLevelSufficientlyRecharged(); + } + + /** + * Returns this vehicle's critical energy level (in percent of the maximum). + * The critical energy level is the one at/below which the vehicle should be + * recharged. + * + * @return This vehicle's critical energy level. + * @deprecated Use {@link #getEnergyLevelThresholdSet()} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public int getEnergyLevelCritical() { + return energyLevelThresholdSet.getEnergyLevelCritical(); + } + + /** + * Creates a copy of this object, with the given critical energy level. + * + * @param energyLevelCritical The value to be set in the copy. + * @return A copy of this object, differing in the given value. + * @deprecated Use {@link #withEnergyLevelThresholdSet(EnergyLevelThresholdSet)} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public Vehicle withEnergyLevelCritical(int energyLevelCritical) { + return withEnergyLevelThresholdSet( + getEnergyLevelThresholdSet().withEnergyLevelCritical(energyLevelCritical) + ); + } + + /** + * Returns this vehicle's good energy level (in percent of the maximum). + * The good energy level is the one at/above which the vehicle can be + * dispatched again when charging. + * + * @return This vehicle's good energy level. + * @deprecated Use {@link #getEnergyLevelThresholdSet()} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public int getEnergyLevelGood() { + return energyLevelThresholdSet.getEnergyLevelGood(); + } + + /** + * Creates a copy of this object, with the given good energy level. + * + * @param energyLevelGood The value to be set in the copy. + * @return A copy of this object, differing in the given value. + * @deprecated Use {@link #withEnergyLevelThresholdSet(EnergyLevelThresholdSet)} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public Vehicle withEnergyLevelGood(int energyLevelGood) { + return withEnergyLevelThresholdSet( + getEnergyLevelThresholdSet().withEnergyLevelGood(energyLevelGood) + ); + } + + /** + * Returns this vehicle's energy level for being fully recharged (in percent of the maximum). + * + * @return This vehicle's fully recharged threshold. + * @deprecated Use {@link #getEnergyLevelThresholdSet()} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public int getEnergyLevelFullyRecharged() { + return energyLevelThresholdSet.getEnergyLevelFullyRecharged(); + } + + /** + * Creates a copy of this object, with the given fully recharged energy level. + * + * @param energyLevelFullyRecharged The value to be set in the copy. + * @return A copy of this object, differing in the given value. + * @deprecated Use {@link #withEnergyLevelThresholdSet(EnergyLevelThresholdSet)} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public Vehicle withEnergyLevelFullyRecharged(int energyLevelFullyRecharged) { + return withEnergyLevelThresholdSet( + getEnergyLevelThresholdSet().withEnergyLevelFullyRecharged(energyLevelFullyRecharged) + ); + } + + /** + * Returns this vehicle's energy level for being sufficiently recharged (in percent of the + * maximum). + * + * @return This vehicle's sufficiently recharged energy level. + * @deprecated Use {@link #getEnergyLevelThresholdSet()} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public int getEnergyLevelSufficientlyRecharged() { + return energyLevelThresholdSet.getEnergyLevelSufficientlyRecharged(); + } + + /** + * Creates a copy of this object, with the given sufficiently recharged energy level. + * + * @param energyLevelSufficientlyRecharged The value to be set in the copy. + * @return A copy of this object, differing in the given value. + * @deprecated Use {@link #withEnergyLevelThresholdSet(EnergyLevelThresholdSet)} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public Vehicle withEnergyLevelSufficientlyRecharged(int energyLevelSufficientlyRecharged) { + return withEnergyLevelThresholdSet( + getEnergyLevelThresholdSet() + .withEnergyLevelSufficientlyRecharged(energyLevelSufficientlyRecharged) + ); + } + + /** + * Returns this vehicle's energy level threshold set. + * + * @return This vehicle's energy level threshold set. + */ + public EnergyLevelThresholdSet getEnergyLevelThresholdSet() { + return energyLevelThresholdSet; + } + + /** + * Creates a copy of this object, with the given EnergyLevelThresholdSet. + * + * @param energyLevelThresholdSet The new EnergyLevelThresholdSet. + * @return A copy of this object, differing in the given value. + */ + public Vehicle withEnergyLevelThresholdSet(EnergyLevelThresholdSet energyLevelThresholdSet) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Returns the operation that the vehicle's current communication adapter + * accepts as a command to recharge the vehicle. + * + * @return The operation that the vehicle's current communication adapter + * accepts as a command to recharge the vehicle. + */ + public String getRechargeOperation() { + return rechargeOperation; + } + + /** + * Creates a copy of this object, with the given recharge operation. + * + * @param rechargeOperation The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Vehicle withRechargeOperation(String rechargeOperation) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Returns the current (state of the) load handling devices of this vehicle. + * + * @return The current (state of the) load handling devices of this vehicle. + */ + public List getLoadHandlingDevices() { + return loadHandlingDevices; + } + + /** + * Creates a copy of this object, with the given load handling devices. + * + * @param loadHandlingDevices The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Vehicle withLoadHandlingDevices(List loadHandlingDevices) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Returns this vehicle's maximum velocity (in mm/s). + * + * @return This vehicle's maximum velocity (in mm/s). + */ + public int getMaxVelocity() { + return maxVelocity; + } + + /** + * Creates a copy of this object, with the given maximum velocity. + * + * @param maxVelocity The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Vehicle withMaxVelocity(int maxVelocity) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Returns this vehicle's maximum reverse velocity (in mm/s). + * + * @return This vehicle's maximum reverse velocity (in mm/s). + */ + public int getMaxReverseVelocity() { + return maxReverseVelocity; + } + + /** + * Creates a copy of this object, with the given maximum reverse velocity. + * + * @param maxReverseVelocity The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Vehicle withMaxReverseVelocity(int maxReverseVelocity) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Returns this vehicle's current state. + * + * @return This vehicle's current state. + */ + public State getState() { + return state; + } + + /** + * Checks if this vehicle's current state is equal to the given one. + * + * @param otherState The state to compare to this vehicle's one. + * @return true if, and only if, the given state is equal to this + * vehicle's one. + */ + public boolean hasState(State otherState) { + return state.equals(otherState); + } + + /** + * Creates a copy of this object, with the given state. + * + * @param state The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Vehicle withState(State state) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Returns this vehicle's current processing state. + * + * @return This vehicle's current processing state. + */ + public ProcState getProcState() { + return procState; + } + + /** + * Returns this vehicle's integration level. + * + * @return This vehicle's integration level. + */ + public IntegrationLevel getIntegrationLevel() { + return integrationLevel; + } + + /** + * Creates a copy of this object, with the given integration level. + * + * @param integrationLevel The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Vehicle withIntegrationLevel(IntegrationLevel integrationLevel) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Indicates whether this vehicle is paused. + * + * @return Whether this vehicle is paused. + */ + public boolean isPaused() { + return paused; + } + + /** + * Creates a copy of this object, with the given paused state. + * + * @param paused The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Vehicle withPaused(boolean paused) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Checks if this vehicle's current processing state is equal to the given + * one. + * + * @param otherState The state to compare to this vehicle's one. + * @return true if, and only if, the given state is equal to this + * vehicle's one. + */ + public boolean hasProcState(ProcState otherState) { + return procState.equals(otherState); + } + + /** + * Creates a copy of this object, with the given processing state. + * + * @param procState The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Vehicle withProcState(ProcState procState) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Returns the vehicle's current bounding box (in mm). + *

+ * The bounding box is oriented so that its longitudinal axis runs parallel to the longitudinal + * axis of the vehicle. For the reference point offset, positive x values indicate an offset in + * the forward direction of the vehicle, positive y values an offset towards the left-hand side. + *

+ * + * @return The vehicle's current bounding box (in mm). + */ + public BoundingBox getBoundingBox() { + return boundingBox; + } + + /** + * Creates a copy of this object, with the given bounding box (in mm). + *

+ * The bounding box is oriented so that its longitudinal axis runs parallel to the longitudinal + * axis of the vehicle. For the reference point offset, positive x values indicate an offset in + * the forward direction of the vehicle, positive y values an offset towards the left-hand side. + *

+ * + * @param boundingBox The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Vehicle withBoundingBox(BoundingBox boundingBox) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Returns this vehicle's current length. + * + * @return this vehicle's current length. + * @deprecated Use {@link #getBoundingBox()} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public int getLength() { + return (int) boundingBox.getLength(); + } + + /** + * Creates a copy of this object, with the given length. + * + * @param length The value to be set in the copy. + * @return A copy of this object, differing in the given value. + * @deprecated Use {@link #withBoundingBox(BoundingBox)} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public Vehicle withLength(int length) { + return withBoundingBox(boundingBox.withLength(length)); + } + + /** + * Returns a reference to the transport order this vehicle is currently + * processing. + * + * @return A reference to the transport order this vehicle is currently + * processing, or null, if it is not processing any transport + * order at the moment. + */ + public TCSObjectReference getTransportOrder() { + return transportOrder; + } + + /** + * Creates a copy of this object, with the given transport order. + * + * @param transportOrder The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Vehicle withTransportOrder(TCSObjectReference transportOrder) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Returns a reference to the order sequence this vehicle is currently + * processing. + * + * @return A reference to the order sequence this vehicle is currently + * processing, or null, if it is not processing any order + * sequence at the moment. + */ + public TCSObjectReference getOrderSequence() { + return orderSequence; + } + + /** + * Creates a copy of this object, with the given order sequence. + * + * @param orderSequence The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Vehicle withOrderSequence(TCSObjectReference orderSequence) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Returns the set of order types this vehicle is allowed to process. + * + * @return The set of order types this vehicle is allowed to process. + */ + public Set getAllowedOrderTypes() { + return allowedOrderTypes; + } + + /** + * Creates a copy of this object, with the given set of allowed order types. + * + * @param allowedOrderTypes The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Vehicle withAllowedOrderTypes(Set allowedOrderTypes) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Returns the resources this vehicle has claimed for future allocation. + * + * @return The resources this vehicle has claimed for future allocation. + */ + public List>> getClaimedResources() { + return claimedResources; + } + + /** + * Creates a copy of this object, with the given claimed resources. + * + * @param claimedResources The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Vehicle withClaimedResources(List>> claimedResources) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Returns the resources this vehicle has allocated. + * + * @return The resources this vehicle has allocated. + */ + public List>> getAllocatedResources() { + return allocatedResources; + } + + /** + * Creates a copy of this object, with the given allocated resources. + * + * @param allocatedResources The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Vehicle withAllocatedResources(List>> allocatedResources) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Returns a reference to the point this vehicle currently occupies. + * + * @return A reference to the point this vehicle currently occupies, or + * null, if this vehicle's position is unknown or the vehicle is + * currently not in the system. + */ + public TCSObjectReference getCurrentPosition() { + return currentPosition; + } + + /** + * Creates a copy of this object, with the given current position. + * + * @param currentPosition The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Vehicle withCurrentPosition(TCSObjectReference currentPosition) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Returns a reference to the point this vehicle is expected to be seen at + * next. + * + * @return A reference to the point this vehicle is expected to be seen at + * next, or null, if this vehicle's next position is unknown. + */ + public TCSObjectReference getNextPosition() { + return nextPosition; + } + + /** + * Creates a copy of this object, with the given next position. + * + * @param nextPosition The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Vehicle withNextPosition(TCSObjectReference nextPosition) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Returns the vehicle's position in world coordinates [mm], independent + * from logical positions/point names. May be null if the vehicle + * hasn't provided a precise position. + * + * @return The vehicle's precise position in mm. + * @deprecated Use {@link #getPose()} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public Triple getPrecisePosition() { + return pose.getPosition(); + } + + /** + * Creates a copy of this object, with the given precise position. + * + * @param precisePosition The value to be set in the copy. + * @return A copy of this object, differing in the given value. + * @deprecated Use {@link #withPose(Pose)} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public Vehicle withPrecisePosition(Triple precisePosition) { + return withPose(pose.withPosition(precisePosition)); + } + + /** + * Returns the vehicle's current orientation angle (-360..360). + * May be Double.NaN if the vehicle hasn't provided an + * orientation angle. + * + * @return The vehicle's current orientation angle. + * @deprecated Use {@link #getPose()} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public double getOrientationAngle() { + return pose.getOrientationAngle(); + } + + /** + * Creates a copy of this object, with the given orientation angle. + * + * @param orientationAngle The value to be set in the copy. + * @return A copy of this object, differing in the given value. + * @deprecated Use {@link #withPose(Pose)} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public Vehicle withOrientationAngle(double orientationAngle) { + return withPose(pose.withOrientationAngle(orientationAngle)); + } + + /** + * Returns the vehicle's pose containing the precise position and orientation angle. + * + * @return The vehicle's pose. + */ + @Nonnull + public Pose getPose() { + return pose; + } + + /** + * Creates a copy of this object, with the given pose. + * + * @param pose The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Vehicle withPose( + @Nonnull + Pose pose + ) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Returns the key for selecting the envelope to be used for resources the vehicle occupies. + * + * @return The key for selecting the envelope to be used for resources the vehicle occupies. + */ + @ScheduledApiChange(when = "7.0", details = "Envelope key will become non-null.") + @Nullable + public String getEnvelopeKey() { + return envelopeKey; + } + + /** + * Creates a copy of this object, with the given envelope key. + * + * @param envelopeKey The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + @ScheduledApiChange(when = "7.0", details = "Envelope key will become non-null.") + public Vehicle withEnvelopeKey( + @Nullable + String envelopeKey + ) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Returns the information regarding the graphical representation of this vehicle. + * + * @return The information regarding the graphical representation of this vehicle. + */ + public Layout getLayout() { + return layout; + } + + /** + * Creates a copy of this object, with the given layout. + * + * @param layout The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Vehicle withLayout(Layout layout) { + return new Vehicle( + getName(), + getProperties(), + getHistory(), + boundingBox, + energyLevelThresholdSet, + maxVelocity, + maxReverseVelocity, + rechargeOperation, + procState, + transportOrder, + orderSequence, + allowedOrderTypes, + claimedResources, + allocatedResources, + state, + integrationLevel, + paused, + currentPosition, + nextPosition, + pose, + energyLevel, + loadHandlingDevices, + envelopeKey, + layout + ); + } + + /** + * Checks if this vehicle is currently processing any transport order. + * + * @return true if, and only if, this vehicle is currently + * processing a transport order. + */ + public boolean isProcessingOrder() { + return transportOrder != null; + } + + @Override + public String toString() { + return "Vehicle{" + + "name=" + getName() + + ", procState=" + procState + + ", integrationLevel=" + integrationLevel + + ", paused=" + paused + + ", state=" + state + + ", energyLevel=" + energyLevel + + ", currentPosition=" + currentPosition + + ", pose=" + pose + + ", nextPosition=" + nextPosition + + ", loadHandlingDevices=" + loadHandlingDevices + + ", boundingBox=" + boundingBox + + ", transportOrder=" + transportOrder + + ", claimedResources=" + claimedResources + + ", allocatedResources=" + allocatedResources + + ", orderSequence=" + orderSequence + + ", energyLevelThresholdSet=" + energyLevelThresholdSet + + ", maxVelocity=" + maxVelocity + + ", maxReverseVelocity=" + maxReverseVelocity + + ", rechargeOperation=" + rechargeOperation + + ", allowedOrderTypes=" + allowedOrderTypes + + ", envelopeKey=" + envelopeKey + + '}'; + } + + /** + * The elements of this enumeration describe the various possible states of a + * vehicle. + */ + public enum State { + + /** + * The vehicle's current state is unknown, e.g. because communication with + * it is currently not possible for some reason. + */ + UNKNOWN, + /** + * The vehicle's state is known and it's not in an error state, but it is + * not available for receiving orders. + */ + UNAVAILABLE, + /** + * There is a problem with the vehicle. + */ + ERROR, + /** + * The vehicle is currently idle/available for processing movement orders. + */ + IDLE, + /** + * The vehicle is processing a movement order. + */ + EXECUTING, + /** + * The vehicle is currently recharging its battery/refilling fuel. + */ + CHARGING + } + + /** + * A vehicle's state of integration into the system. + */ + public enum IntegrationLevel { + + /** + * The vehicle's reported position is ignored. + */ + TO_BE_IGNORED, + /** + * The vehicle's reported position is noticed, meaning that resources will not be reserved for + * it. + */ + TO_BE_NOTICED, + /** + * The vehicle's reported position is respected, meaning that resources will be reserved for it. + */ + TO_BE_RESPECTED, + /** + * The vehicle is fully integrated and may be assigned to transport orders. + */ + TO_BE_UTILIZED + } + + /** + * A vehicle's processing state as seen by the dispatcher. + */ + public enum ProcState { + + /** + * The vehicle is currently not processing a transport order. + */ + IDLE, + /** + * The vehicle is currently processing a transport order and is waiting for + * the next drive order to be assigned to it. + */ + AWAITING_ORDER, + /** + * The vehicle is currently processing a drive order. + */ + PROCESSING_ORDER + } + + /** + * The elements of this enumeration represent the possible orientations of a + * vehicle. + */ + public enum Orientation { + + /** + * Indicates that the vehicle is driving/standing oriented towards its + * front. + */ + FORWARD, + /** + * Indicates that the vehicle is driving/standing oriented towards its + * back. + */ + BACKWARD, + /** + * Indicates that the vehicle's orientation is undefined/unknown. + */ + UNDEFINED + } + + /** + * Contains information regarding the graphical representation of a vehicle. + */ + public static class Layout + implements + Serializable { + + /** + * The color in which vehicle routes are to be emphasized. + */ + private final Color routeColor; + + /** + * Creates a new instance. + */ + public Layout() { + this(Color.RED); + } + + /** + * Creates a new instance. + * + * @param routeColor The color in which vehicle routes are to be emphasized. + */ + public Layout(Color routeColor) { + this.routeColor = requireNonNull(routeColor, "routeColor"); + } + + /** + * Returns the color in which vehicle routes are to be emphasized. + * + * @return The color in which vehicle routes are to be emphasized. + */ + public Color getRouteColor() { + return routeColor; + } + + /** + * Creates a copy of this object, with the given color. + * + * @param routeColor The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layout withRouteColor(Color routeColor) { + return new Layout(routeColor); + } + } + + /** + * Contains information regarding the energy level threshold values of a vehicle. + */ + public static class EnergyLevelThresholdSet + implements + Serializable { + + private final int energyLevelCritical; + private final int energyLevelGood; + private final int energyLevelSufficientlyRecharged; + private final int energyLevelFullyRecharged; + + /** + * Creates a new instance. + * + * @param energyLevelCritical The value at/below which the vehicle's energy level is considered + * "critical". + * @param energyLevelGood The value at/above which the vehicle's energy level is considered + * "good". + * @param energyLevelSufficientlyRecharged The value at/above which the vehicle's energy level + * is considered fully recharged. + * @param energyLevelFullyRecharged The value at/above which the vehicle's energy level is + * considered sufficiently recharged. + */ + public EnergyLevelThresholdSet( + int energyLevelCritical, + int energyLevelGood, + int energyLevelSufficientlyRecharged, + int energyLevelFullyRecharged + ) { + this.energyLevelCritical = checkInRange( + energyLevelCritical, + 0, + 100, + "energyLevelCritical" + ); + this.energyLevelGood = checkInRange( + energyLevelGood, + 0, + 100, + "energyLevelGood" + ); + this.energyLevelSufficientlyRecharged = checkInRange( + energyLevelSufficientlyRecharged, + 0, + 100, + "energyLevelSufficientlyRecharged" + ); + this.energyLevelFullyRecharged = checkInRange( + energyLevelFullyRecharged, + 0, + 100, + "energyLevelFullyRecharged" + ); + } + + /** + * Returns the vehicle's critical energy level (in percent of the maximum). + *

+ * The critical energy level is the one at/below which the vehicle should be recharged. + *

+ * + * @return The vehicle's critical energy level. + */ + public int getEnergyLevelCritical() { + return energyLevelCritical; + } + + /** + * Creates a copy of this object, with the given critical energy level. + * + * @param energyLevelCritical The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public EnergyLevelThresholdSet withEnergyLevelCritical(int energyLevelCritical) { + return new EnergyLevelThresholdSet( + energyLevelCritical, + energyLevelGood, + energyLevelSufficientlyRecharged, + energyLevelFullyRecharged + ); + } + + /** + * Returns the vehicle's good energy level (in percent of the maximum). + *

+ * The good energy level is the one at/above which the vehicle can be dispatched again when + * charging. + *

+ * + * @return The vehicle's good energy level. + */ + public int getEnergyLevelGood() { + return energyLevelGood; + } + + /** + * Creates a copy of this object, with the given good energy level. + * + * @param energyLevelGood The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public EnergyLevelThresholdSet withEnergyLevelGood(int energyLevelGood) { + return new EnergyLevelThresholdSet( + energyLevelCritical, + energyLevelGood, + energyLevelSufficientlyRecharged, + energyLevelFullyRecharged + ); + } + + /** + * Returns the vehicle's energy level for being sufficiently recharged (in percent of the + * maximum). + * + * @return This vehicle's sufficiently recharged energy level. + */ + public int getEnergyLevelSufficientlyRecharged() { + return energyLevelSufficientlyRecharged; + } + + /** + * Creates a copy of this object, with the given sufficiently recharged energy level. + * + * @param energyLevelSufficientlyRecharged The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public EnergyLevelThresholdSet withEnergyLevelSufficientlyRecharged( + int energyLevelSufficientlyRecharged + ) { + return new EnergyLevelThresholdSet( + energyLevelCritical, + energyLevelGood, + energyLevelSufficientlyRecharged, + energyLevelFullyRecharged + ); + } + + /** + * Returns the vehicle's energy level for being fully recharged (in percent of the maximum). + * + * @return The vehicle's fully recharged threshold. + */ + public int getEnergyLevelFullyRecharged() { + return energyLevelFullyRecharged; + } + + /** + * Creates a copy of this object, with the given fully recharged energy level. + * + * @param energyLevelFullyRecharged The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public EnergyLevelThresholdSet withEnergyLevelFullyRecharged(int energyLevelFullyRecharged) { + return new EnergyLevelThresholdSet( + energyLevelCritical, + energyLevelGood, + energyLevelSufficientlyRecharged, + energyLevelFullyRecharged + ); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof EnergyLevelThresholdSet other)) { + return false; + } + + return energyLevelCritical == other.getEnergyLevelCritical() + && energyLevelGood == other.getEnergyLevelGood() + && energyLevelSufficientlyRecharged == other.getEnergyLevelSufficientlyRecharged() + && energyLevelFullyRecharged == other.getEnergyLevelFullyRecharged(); + } + + @Override + public int hashCode() { + return Objects.hash( + energyLevelCritical, + energyLevelGood, + energyLevelSufficientlyRecharged, + energyLevelFullyRecharged + ); + } + + @Override + public String toString() { + return "EnergyLevelThresholdSet{" + + "energyLevelCritical=" + energyLevelCritical + + ", energyLevelGood=" + energyLevelGood + + ", energyLevelSufficientlyRecharged=" + energyLevelSufficientlyRecharged + + ", energyLevelFullyRecharged=" + energyLevelFullyRecharged + + '}'; + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/package-info.java new file mode 100644 index 0000000..1d56f54 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/package-info.java @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Classes for maintaining the (mainly static) structure and content of openTCS + * course layouts and the attributes and state of vehicles. + */ +package org.opentcs.data.model; diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/visualization/ElementPropKeys.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/visualization/ElementPropKeys.java new file mode 100644 index 0000000..7cb208e --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/visualization/ElementPropKeys.java @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model.visualization; + +/** + * Defines some reserved/commonly used property keys of elements in a {@link VisualLayout}. + */ +public interface ElementPropKeys { + + /** + * X coordinate at which the point is to be drawn (in mm). + * Type: int. + * Default value: Physical coordinate of the point. + */ + String POINT_POS_X = "POSITION_X"; + /** + * Y coordinate at which the point is to be drawn (in mm). + * Type: int. + * Default value: Physical coordinate of the point. + */ + String POINT_POS_Y = "POSITION_Y"; + /** + * X offset of the label's position to the object's position (in lu). + * Type: int. + * Default value: ?? + */ + String POINT_LABEL_OFFSET_X = "LABEL_OFFSET_X"; + /** + * Y offset of the label's position to the object's position (in lu). + * Type: int. + * Default value: ?? + */ + String POINT_LABEL_OFFSET_Y = "LABEL_OFFSET_Y"; + /** + * Orientation angle of the label (in degrees). + * Type: int [0..360]. + * Default value: 0. + */ + String POINT_LABEL_ORIENTATION_ANGLE = "LABEL_ORIENTATION_ANGLE"; + /** + * X coordinate at which the location is to be drawn (in mm). + * Type: int. + * Default value: ??. + */ + String LOC_POS_X = "POSITION_X"; + /** + * Y coordinate at which the location is to be drawn (in mm). + * Type: int. + * Default value: ??. + */ + String LOC_POS_Y = "POSITION_Y"; + /** + * X offset of the label's position to the object's position (in lu). + * Type: int. + * Default value: ?? + */ + String LOC_LABEL_OFFSET_X = "LABEL_OFFSET_X"; + /** + * Y offset of the label's position to the object's position (in lu). + * Type: int. + * Default value: ?? + */ + String LOC_LABEL_OFFSET_Y = "LABEL_OFFSET_Y"; + /** + * Orientation angle of the label (in degrees). + * Type: int [0..360]. + * Default value: 0. + */ + String LOC_LABEL_ORIENTATION_ANGLE = "LABEL_ORIENTATION_ANGLE"; + /** + * The drawing type of the path. + * Type: String {Elbow, Slanted, Curved, Bezier, Direct} + * Default value: DIRECT. + */ + String PATH_CONN_TYPE = "CONN_TYPE"; + /** + * Control points describing the way the connection is being drawn (if the + * connection type is not Direct). + * Type: String. (List of comma-separated X/Y pairs, with the pairs being + * separated by semicola. Example: "x1,x1;x2,y2;x3,y3") + * Default value: empty. + */ + String PATH_CONTROL_POINTS = "CONTROL_POINTS"; + /** + * Color in which block elements are to be emphasized. + * Type: String (pattern: #rrggbb). + * Default value: Automatically assigned. + */ + String BLOCK_COLOR = "COLOR"; + /** + * Color in which vehicle routes are to be emphasized. + * Type: String (pattern: #rrggbb). + * Default value: Color.RED + */ + String VEHICLE_ROUTE_COLOR = "ROUTE_COLOR"; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/visualization/Layer.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/visualization/Layer.java new file mode 100644 index 0000000..4734cf3 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/visualization/Layer.java @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model.visualization; + +import static java.util.Objects.requireNonNull; + +import java.io.Serializable; + +/** + * Describes a layer in a plant model which is used to group model elements. + */ +public class Layer + implements + Serializable { + + /** + * The unique ID of this layer. + */ + private final int id; + /** + * The ordinal of this layer. + * Layers with a higher ordinal are positioned above layers with a lower ordinal. + */ + private final int ordinal; + /** + * Whether this layer is visible or not. + */ + private final boolean visible; + /** + * The name of this layer. + */ + private final String name; + /** + * The ID of the layer group this layer is assigned to. + */ + private final int groupId; + + /** + * Creates a new instance. + * + * @param id The unique ID of the layer. + * @param ordinal The ordinal of the layer. + * @param visible Whether the layer is visible or not. + * @param name The name of the layer. + * @param groupId The ID of the layer group the layer is assigned to. + */ + public Layer(int id, int ordinal, boolean visible, String name, int groupId) { + this.id = id; + this.ordinal = ordinal; + this.visible = visible; + this.name = requireNonNull(name, "name"); + this.groupId = groupId; + } + + /** + * Returns the unique ID of this layer. + * + * @return The unique Id of this layer. + */ + public int getId() { + return id; + } + + /** + * Returns the ordinal of this layer. + * Layers with a higher ordinal are positioned above layers with a lower ordinal. + * + * @return The ordinal of this layer. + */ + public int getOrdinal() { + return ordinal; + } + + /** + * Creates a copy of this object, with the given ordinal. + * + * @param ordinal The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layer withOrdinal(int ordinal) { + return new Layer(id, ordinal, visible, name, groupId); + } + + /** + * Returns whether this layer is visible or not. + * + * @return Whether this layer is visible or not. + */ + public boolean isVisible() { + return visible; + } + + /** + * Creates a copy of this object, with the given visible state. + * + * @param visible The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layer withVisible(boolean visible) { + return new Layer(id, ordinal, visible, name, groupId); + } + + /** + * Returns the name of this layer. + * + * @return The name of this layer. + */ + public String getName() { + return name; + } + + /** + * Creates a copy of this object, with the given name. + * + * @param name The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layer withName(String name) { + return new Layer(id, ordinal, visible, name, groupId); + } + + /** + * Returns the ID of the layer group this layer is assigned to. + * + * @return The ID of the layer group this layer is assigned to. + */ + public int getGroupId() { + return groupId; + } + + /** + * Creates a copy of this object, with the given group ID. + * + * @param groupId The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Layer withGroupId(int groupId) { + return new Layer(id, ordinal, visible, name, groupId); + } + + @Override + public String toString() { + return "Layer{" + "" + + "id=" + id + ", " + + "ordinal=" + ordinal + ", " + + "visible=" + visible + ", " + + "name=" + name + ", " + + "groupId=" + groupId + '}'; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/visualization/LayerGroup.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/visualization/LayerGroup.java new file mode 100644 index 0000000..338fcfa --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/visualization/LayerGroup.java @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model.visualization; + +import static java.util.Objects.requireNonNull; + +import java.io.Serializable; + +/** + * Describes a layer group in a plant model. + */ +public class LayerGroup + implements + Serializable { + + /** + * The unique ID of this layer group. + */ + private final int id; + /** + * The name of this layer group. + */ + private final String name; + /** + * Whether this layer group is visible or not. + */ + private final boolean visible; + + /** + * Creates a new instance. + * + * @param id The unique ID of the layer group. + * @param name The name of the layer group. + * @param visible Whether the layer group is visible or not. + */ + public LayerGroup(int id, String name, boolean visible) { + this.id = id; + this.name = requireNonNull(name, "name"); + this.visible = visible; + } + + /** + * Returns the unique ID of this layer group. + * + * @return The unique Id of this layer group. + */ + public int getId() { + return id; + } + + /** + * Returns whether this layer group is visible or not. + * + * @return Whether this layer group is visible or not. + */ + public boolean isVisible() { + return visible; + } + + /** + * Creates a copy of this object, with the given visible state. + * + * @param visible The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public LayerGroup withVisible(boolean visible) { + return new LayerGroup(id, name, visible); + } + + /** + * Returns the name of this layer group. + * + * @return The name of this layer group. + */ + public String getName() { + return name; + } + + /** + * Creates a copy of this object, with the given name. + * + * @param name The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public LayerGroup withName(String name) { + return new LayerGroup(id, name, visible); + } + + @Override + public String toString() { + return "LayerGroup{" + "" + + "id=" + id + ", " + + "name=" + name + ", " + + "visible=" + visible + '}'; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/visualization/LocationRepresentation.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/visualization/LocationRepresentation.java new file mode 100644 index 0000000..a0456a6 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/visualization/LocationRepresentation.java @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model.visualization; + +/** + * Common location representations. + * + * @see org.opentcs.data.ObjectPropConstants#LOCTYPE_DEFAULT_REPRESENTATION + * @see org.opentcs.data.ObjectPropConstants#LOC_DEFAULT_REPRESENTATION + */ +public enum LocationRepresentation { + + /** + * A (empty) location without any representation. + */ + NONE, + /** + * The default representation inherited from the assigned location type. + */ + DEFAULT, + /** + * A location for vehicle load transfer, generic variant. + */ + LOAD_TRANSFER_GENERIC, + /** + * A location for vehicle load transfers, alternative variant 1. + */ + LOAD_TRANSFER_ALT_1, + /** + * A location for vehicle load transfers, alternative variant 2. + */ + LOAD_TRANSFER_ALT_2, + /** + * A location for vehicle load transfers, alternative variant 3. + */ + LOAD_TRANSFER_ALT_3, + /** + * A location for vehicle load transfers, alternative variant 4. + */ + LOAD_TRANSFER_ALT_4, + /** + * A location for vehicle load transfers, alternative variant 5. + */ + LOAD_TRANSFER_ALT_5, + /** + * A location for some generic processing, generic variant. + */ + WORKING_GENERIC, + /** + * A location for some generic processing, alternative variant 1. + */ + WORKING_ALT_1, + /** + * A location for some generic processing, alternative variant 2. + */ + WORKING_ALT_2, + /** + * A location for recharging a vehicle, generic variant. + */ + RECHARGE_GENERIC, + /** + * A location for recharging a vehicle, alternative variant 1. + */ + RECHARGE_ALT_1, + /** + * A location for recharging a vehicle, alternative variant 2. + */ + RECHARGE_ALT_2 +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/visualization/VisualLayout.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/visualization/VisualLayout.java new file mode 100644 index 0000000..60c9b69 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/visualization/VisualLayout.java @@ -0,0 +1,233 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model.visualization; + +import static java.util.Objects.requireNonNull; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.TCSObject; + +/** + * Describes the visual attributes of a model. + */ +public class VisualLayout + extends + TCSObject + implements + Serializable { + + /** + * This layout's scale on the X axis (in mm/pixel). + */ + private final double scaleX; + /** + * This layout's scale on the Y axis (in mm/pixel). + */ + private final double scaleY; + /** + * The layers in this model. + */ + private final List layers; + /** + * The layer groups in this model. + */ + private final List layerGroups; + + /** + * Creates a new VisualLayout. + * + * @param name This visual layout's name. + */ + public VisualLayout(String name) { + super(name); + this.scaleX = 50.0; + this.scaleY = 50.0; + this.layers = List.of(); + this.layerGroups = List.of(); + } + + /** + * Creates a new VisualLayout. + * + * @param name This visual layout's name. + */ + private VisualLayout( + String name, + Map properties, + ObjectHistory history, + double scaleX, + double scaleY, + List layers, + List layerGroups + ) { + super(name, properties, history); + this.scaleX = scaleX; + this.scaleY = scaleY; + this.layers = new ArrayList<>(requireNonNull(layers, "layers")); + this.layerGroups = new ArrayList<>(requireNonNull(layerGroups, "layerGroups")); + } + + @Override + public VisualLayout withProperty(String key, String value) { + return new VisualLayout( + getName(), + propertiesWith(key, value), + getHistory(), + scaleX, + scaleY, + layers, + layerGroups + ); + } + + @Override + public VisualLayout withProperties(Map properties) { + return new VisualLayout( + getName(), + properties, + getHistory(), + scaleX, + scaleY, + layers, + layerGroups + ); + } + + @Override + public TCSObject withHistoryEntry(ObjectHistory.Entry entry) { + return new VisualLayout( + getName(), + getProperties(), + getHistory().withEntryAppended(entry), + scaleX, + scaleY, + layers, + layerGroups + ); + } + + @Override + public TCSObject withHistory(ObjectHistory history) { + return new VisualLayout( + getName(), + getProperties(), + history, + scaleX, + scaleY, + layers, + layerGroups + ); + } + + /** + * Returns this layout's scale on the X axis (in mm/pixel). + * + * @return This layout's scale on the X axis. + */ + public double getScaleX() { + return scaleX; + } + + /** + * Creates a copy of this object, with the given scaleX. + * + * @param scaleX The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public VisualLayout withScaleX(double scaleX) { + return new VisualLayout( + getName(), + getProperties(), + getHistory(), + scaleX, + scaleY, + layers, + layerGroups + ); + } + + /** + * Returns this layout's scale on the Y axis (in mm/pixel). + * + * @return This layout's scale on the Y axis. + */ + public double getScaleY() { + return scaleY; + } + + /** + * Creates a copy of this object, with the given scaleY. + * + * @param scaleY The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public VisualLayout withScaleY(double scaleY) { + return new VisualLayout( + getName(), + getProperties(), + getHistory(), + scaleX, + scaleY, + layers, + layerGroups + ); + } + + /** + * Returns the layers of this layout. + * + * @return The layers of this layout. + */ + public List getLayers() { + return layers; + } + + /** + * Creates a copy of this object, with the given layers. + * + * @param layers The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public VisualLayout withLayers(List layers) { + return new VisualLayout( + getName(), + getProperties(), + getHistory(), + scaleX, + scaleY, + layers, + layerGroups + ); + } + + /** + * Returns the layer groups of this layout. + * + * @return The layer groups of this layout. + */ + public List getLayerGroups() { + return layerGroups; + } + + /** + * Creates a copy of this object, with the given layer groups. + * + * @param layerGroups The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public VisualLayout withLayerGroups(List layerGroups) { + return new VisualLayout( + getName(), + getProperties(), + getHistory(), + scaleX, + scaleY, + layers, + layerGroups + ); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/model/visualization/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/data/model/visualization/package-info.java new file mode 100644 index 0000000..9ae4260 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/model/visualization/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Classes describing the visual attributes of a model. + */ +package org.opentcs.data.model.visualization; diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/notification/UserNotification.java b/opentcs-api-base/src/main/java/org/opentcs/data/notification/UserNotification.java new file mode 100644 index 0000000..d28ff03 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/notification/UserNotification.java @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.notification; + +import jakarta.annotation.Nullable; +import java.io.Serializable; +import java.time.Instant; +import java.util.Objects; + +/** + * A notification to be read by a user. + */ +public class UserNotification + implements + Serializable { + + /** + * An identifier of the notification's source. + */ + @Nullable + private final String source; + /** + * This message's text. + */ + private final String text; + /** + * This message's type. + */ + private final Level level; + /** + * This message's creation timestamp. + */ + private final Instant timestamp; + + /** + * Creates a new Message. + * + * @param source An identifier of the notification's source. + * @param text The actual message text. + * @param level The new message's level. + */ + public UserNotification( + @Nullable + String source, + String text, + Level level + ) { + this.source = source; + this.text = Objects.requireNonNull(text, "text"); + this.level = Objects.requireNonNull(level, "level"); + this.timestamp = Instant.now(); + } + + /** + * Creates a new Message. + * + * @param text The actual message text. + * @param level The new message's level. + */ + public UserNotification(String text, Level level) { + this(null, text, level); + } + + /** + * Returns this notification's (optional) source. + * + * @return This notification's (optional) source. + */ + @Nullable + public String getSource() { + return source; + } + + /** + * Returns this message's text. + * + * @return This message's text. + */ + public String getText() { + return text; + } + + /** + * Returns this message's type. + * + * @return This message's type. + */ + public Level getLevel() { + return level; + } + + /** + * Returns this message's creation timestamp. + * + * @return This message's creation timestamp. + */ + public Instant getTimestamp() { + return timestamp; + } + + @Override + public int hashCode() { + int hash = 5; + hash = 89 * hash + Objects.hashCode(this.text); + hash = 89 * hash + Objects.hashCode(this.level); + hash = 89 * hash + Objects.hashCode(this.timestamp); + return hash; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof UserNotification) { + UserNotification other = (UserNotification) o; + return Objects.equals(this.source, other.source) + && Objects.equals(this.text, other.text) + && Objects.equals(this.level, other.level) + && Objects.equals(this.timestamp, other.timestamp); + } + return false; + } + + @Override + public String toString() { + return "UserNotification{" + + "source=" + source + + ", timestamp=" + timestamp + + ", level=" + level + + ", text=" + text + + '}'; + } + + /** + * Defines the possible message types. + */ + public enum Level { + + /** + * Marks usual, informational content. + */ + INFORMATIONAL, + /** + * Marks unusual content a user would probably be interested in. + */ + NOTEWORTHY, + /** + * Marks important content the user should not miss. + */ + IMPORTANT + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/notification/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/data/notification/package-info.java new file mode 100644 index 0000000..91ec174 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/notification/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Classes for storing user-targeted notifications. + */ +package org.opentcs.data.notification; diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/order/DriveOrder.java b/opentcs-api-base/src/main/java/org/opentcs/data/order/DriveOrder.java new file mode 100644 index 0000000..7359823 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/order/DriveOrder.java @@ -0,0 +1,350 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.order; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; + +/** + * Describes a sequence of movements and an optional operation at the end that a {@link Vehicle} is + * supposed to execute. + * + * @see TransportOrder + */ +public class DriveOrder + implements + Serializable { + + /** + * This drive order's destination. + */ + private final Destination destination; + /** + * A back-reference to the transport order this drive order belongs to. + */ + private TCSObjectReference transportOrder; + /** + * This drive order's route. + */ + private Route route; + /** + * This drive order's current state. + */ + private State state; + + /** + * Creates a new DriveOrder. + * + * @param destination This drive order's destination. + */ + public DriveOrder( + @Nonnull + Destination destination + ) { + this.destination = requireNonNull(destination, "destination"); + this.transportOrder = null; + this.route = null; + this.state = State.PRISTINE; + } + + private DriveOrder( + @Nonnull + Destination destination, + @Nullable + TCSObjectReference transportOrder, + @Nullable + Route route, + @Nonnull + State state + ) { + this.destination = requireNonNull(destination, "destination"); + this.transportOrder = transportOrder; + this.route = route; + this.state = requireNonNull(state, "state"); + } + + /** + * Returns this drive order's destination. + * + * @return This drive order's destination. + */ + @Nonnull + public Destination getDestination() { + return destination; + } + + /** + * Returns a reference to the transport order this drive order belongs to. + * + * @return A reference to the transport order this drive order belongs to. + */ + @Nullable + public TCSObjectReference getTransportOrder() { + return transportOrder; + } + + /** + * Creates a copy of this object, with the given transport order. + * + * @param transportOrder The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public DriveOrder withTransportOrder( + @Nullable + TCSObjectReference transportOrder + ) { + return new DriveOrder(destination, transportOrder, route, state); + } + + /** + * Returns this drive order's route. + * + * @return This drive order's route. May be null if this drive + * order's route hasn't been calculated, yet. + */ + @Nullable + public Route getRoute() { + return route; + } + + /** + * Creates a copy of this object, with the given route. + * + * @param route The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public DriveOrder withRoute( + @Nullable + Route route + ) { + return new DriveOrder(destination, transportOrder, route, state); + } + + /** + * Returns this drive order's state. + * + * @return This drive order's state. + */ + @Nonnull + public State getState() { + return state; + } + + /** + * Creates a copy of this object, with the given state. + * + * @param state The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public DriveOrder withState( + @Nonnull + State state + ) { + return new DriveOrder(destination, transportOrder, route, state); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof DriveOrder)) { + return false; + } + + final DriveOrder other = (DriveOrder) obj; + return Objects.equals(destination, other.destination) + && Objects.equals(transportOrder, other.transportOrder); + } + + @Override + public int hashCode() { + return destination.hashCode() ^ transportOrder.hashCode(); + } + + @Override + public String toString() { + return route + " -> " + destination; + } + + /** + * Describes the destination of a drive order. + */ + public static class Destination + implements + Serializable { + + /** + * An operation constant for doing nothing. + */ + public static final String OP_NOP = "NOP"; + /** + * An operation constant for parking the vehicle. + */ + public static final String OP_PARK = "PARK"; + /** + * An operation constant for sending the vehicle to a point without a location associated to it. + */ + public static final String OP_MOVE = "MOVE"; + /** + * The actual destination (point or location). + */ + private final TCSObjectReference destination; + /** + * The operation to be performed at the destination location. + */ + private final String operation; + /** + * Properties of this destination. + * May contain parameters for the operation, for instance. + */ + private final Map properties; + + /** + * Creates a new instance. + * + * @param destination The actual destination (must be a reference to a location or point). + */ + @SuppressWarnings("unchecked") + public Destination( + @Nonnull + TCSObjectReference destination + ) { + checkArgument( + destination.getReferentClass() == Location.class + || destination.getReferentClass() == Point.class, + "Not a reference on a location or point: %s", + destination + ); + + this.destination = requireNonNull(destination, "destination"); + this.operation = OP_NOP; + this.properties = Collections.unmodifiableMap(new HashMap<>()); + } + + private Destination( + @Nonnull + TCSObjectReference destination, + @Nonnull + Map properties, + @Nonnull + String operation + ) { + this.destination = requireNonNull(destination, "destination"); + this.operation = requireNonNull(operation, "operation"); + this.properties = Collections.unmodifiableMap(new HashMap<>(properties)); + } + + /** + * Returns the actual destination (a location or point). + * + * @return The actual destination (a location or point). + */ + @Nonnull + public TCSObjectReference getDestination() { + return destination; + } + + /** + * Returns the operation to be performed at the destination location. + * + * @return The operation to be performed at the destination location. + */ + @Nonnull + public String getOperation() { + return operation; + } + + /** + * Creates a copy of this object, with the given operation. + * + * @param operation The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Destination withOperation( + @Nonnull + String operation + ) { + return new Destination(destination, properties, operation); + } + + /** + * Returns the properties of this destination. + * + * @return The properties of this destination. + */ + @Nonnull + public Map getProperties() { + return properties; + } + + /** + * Creates a copy of this object, with the given properties. + * + * @param properties The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public Destination withProperties(Map properties) { + return new Destination(destination, properties, operation); + } + + @Override + public boolean equals(Object o) { + if (o instanceof Destination) { + Destination other = (Destination) o; + return destination.equals(other.destination) + && operation.equals(other.operation) + && properties.equals(other.properties); + } + else { + return false; + } + } + + @Override + public int hashCode() { + return destination.hashCode() ^ operation.hashCode(); + } + + @Override + public String toString() { + return destination.getName() + ":" + operation; + } + } + + /** + * Defines the various potential states of a drive order. + */ + public enum State { + + /** + * A drive order's initial state, indicating it being still untouched/not being processed. + */ + PRISTINE, + /** + * Indicates the vehicle processing the order is currently moving to its destination. + */ + TRAVELLING, + /** + * Indicates the vehicle processing the order is currently executing an operation. + */ + OPERATING, + /** + * Marks a drive order as successfully completed. + */ + FINISHED, + /** + * Marks a drive order as failed. + */ + FAILED + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/order/OrderConstants.java b/opentcs-api-base/src/main/java/org/opentcs/data/order/OrderConstants.java new file mode 100644 index 0000000..4392370 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/order/OrderConstants.java @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.order; + +/** + * Defines some constants for {@link TransportOrder}s and {@link OrderSequence}s. + */ +public interface OrderConstants { + + /** + * The string representing the any type. + * Primarily intended to be used for a vehicle to indicate there are no restrictions to its + * allowed oder types. + */ + String TYPE_ANY = "*"; + /** + * The default type of orders. + */ + String TYPE_NONE = "-"; + /** + * A type for charge orders. + */ + String TYPE_CHARGE = "Charge"; + /** + * A type for park orders. + */ + String TYPE_PARK = "Park"; + /** + * A type for transport orders. + */ + String TYPE_TRANSPORT = "Transport"; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/order/OrderSequence.java b/opentcs-api-base/src/main/java/org/opentcs/data/order/OrderSequence.java new file mode 100644 index 0000000..8ee659d --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/order/OrderSequence.java @@ -0,0 +1,543 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.order; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.data.order.OrderSequenceHistoryCodes.SEQUENCE_COMPLETED; +import static org.opentcs.data.order.OrderSequenceHistoryCodes.SEQUENCE_CREATED; +import static org.opentcs.data.order.OrderSequenceHistoryCodes.SEQUENCE_FINISHED; +import static org.opentcs.data.order.OrderSequenceHistoryCodes.SEQUENCE_ORDER_APPENDED; +import static org.opentcs.data.order.OrderSequenceHistoryCodes.SEQUENCE_PROCESSING_VEHICLE_CHANGED; +import static org.opentcs.util.Assertions.checkArgument; +import static org.opentcs.util.Assertions.checkInRange; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; + +/** + * Describes a process spanning multiple {@link TransportOrder}s which are to be executed + * subsequently by the same {@link Vehicle}. + *

+ * The most important rules for order sequence processing are: + *

+ *
    + *
  • Only transport orders that have not yet been activated may be added to an order sequence. + * Allowing them to be added at a later point of time would imply that, due to concurrency in the + * kernel, a transport order might happen to be dispatched at the same time or shortly after it is + * added to a sequence, regardless of if its predecessors in the sequence have already been finished + * or not.
  • + *
  • The intendedVehicle of a transport order being added to an order sequence must be + * the same as that of the sequence itself. + * If it is null in the sequence, a vehicle that will process all orders in the + * sequence will be chosen automatically once the first order in the sequence is dispatched.
  • + *
  • If an order sequence is marked as complete and all transport orders belonging to it + * have arrived in state FINISHED or FAILED, it will be marked as + * finished implicitly.
  • + *
  • If a transport order belonging to an order sequence fails and the sequence's + * failureFatal flag is set, all subsequent orders in the sequence will automatically be + * considered (and marked as) failed, too, and the order sequence will implicitly be marked as + * complete (and finished).
  • + *
+ */ +public class OrderSequence + extends + TCSObject + implements + Serializable { + + /** + * The type of this order sequence. + * An order sequence and all transport orders it contains share the same type. + */ + @Nonnull + private final String type; + /** + * Transport orders belonging to this sequence that still need to be processed. + */ + private final List> orders; + /** + * The index of the order that was last finished in the sequence. + * -1 if none was finished, yet. + */ + private final int finishedIndex; + /** + * Indicates whether this order sequence is complete and will not be extended by more orders. + */ + private final boolean complete; + /** + * Indicates whether this order sequence has been processed completely. + */ + private final boolean finished; + /** + * Indicates whether the failure of one order in this sequence is fatal to all subsequent orders. + */ + private final boolean failureFatal; + /** + * The vehicle that is intended to process this order sequence. + * If this sequence is free to be processed by any vehicle, this is null. + */ + private final TCSObjectReference intendedVehicle; + /** + * The vehicle processing this order sequence, or null, if no vehicle has been + * assigned to it, yet. + */ + private final TCSObjectReference processingVehicle; + + /** + * Creates a new OrderSequence. + * + * @param name This sequence's name. + */ + public OrderSequence(String name) { + super( + name, + new HashMap<>(), + new ObjectHistory().withEntryAppended(new ObjectHistory.Entry(SEQUENCE_CREATED)) + ); + this.type = OrderConstants.TYPE_NONE; + this.orders = List.of(); + this.finishedIndex = -1; + this.complete = false; + this.finished = false; + this.failureFatal = false; + this.intendedVehicle = null; + this.processingVehicle = null; + } + + private OrderSequence( + String name, + Map properties, + ObjectHistory history, + String type, + TCSObjectReference intendedVehicle, + List> orders, + int finishedIndex, + boolean complete, + boolean failureFatal, + boolean finished, + TCSObjectReference processingVehicle + ) { + super(name, properties, history); + this.type = requireNonNull(type, "type"); + this.intendedVehicle = intendedVehicle; + this.orders = new ArrayList<>(requireNonNull(orders, "orders")); + this.finishedIndex = finishedIndex; + this.complete = complete; + this.failureFatal = failureFatal; + this.finished = finished; + this.processingVehicle = processingVehicle; + } + + @Override + public OrderSequence withProperty(String key, String value) { + return new OrderSequence( + getName(), + propertiesWith(key, value), + getHistory(), + type, + intendedVehicle, + orders, + finishedIndex, + complete, + failureFatal, + finished, + processingVehicle + ); + } + + @Override + public OrderSequence withProperties(Map properties) { + return new OrderSequence( + getName(), + properties, + getHistory(), + type, + intendedVehicle, + orders, + finishedIndex, + complete, + failureFatal, + finished, + processingVehicle + ); + } + + @Override + public TCSObject withHistoryEntry(ObjectHistory.Entry entry) { + return new OrderSequence( + getName(), + getProperties(), + getHistory().withEntryAppended(entry), + type, + intendedVehicle, + orders, + finishedIndex, + complete, + failureFatal, + finished, + processingVehicle + ); + } + + @Override + public TCSObject withHistory(ObjectHistory history) { + return new OrderSequence( + getName(), + getProperties(), + history, + type, + intendedVehicle, + orders, + finishedIndex, + complete, + failureFatal, + finished, + processingVehicle + ); + } + + /** + * Returns this order sequence's type. + * + * @return This order sequence's type. + */ + @Nonnull + public String getType() { + return type; + } + + /** + * Creates a copy of this object, with the given type. + * + * @param type The type to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public OrderSequence withType(String type) { + return new OrderSequence( + getName(), + getProperties(), + getHistory(), + type, + intendedVehicle, + orders, + finishedIndex, + complete, + failureFatal, + finished, + processingVehicle + ); + } + + /** + * Returns the list of orders making up this sequence. + * + * @return The list of orders making up this sequence. + */ + public List> getOrders() { + return Collections.unmodifiableList(orders); + } + + /** + * Creates a copy of this object, with the given order. + * + * @param order The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public OrderSequence withOrder(TCSObjectReference order) { + checkArgument(!complete, "Sequence complete, cannot add order"); + checkArgument(!orders.contains(order), "Sequence already contains order %s", order); + + return new OrderSequence( + getName(), + getProperties(), + historyForAppendedOrder(order), + type, + intendedVehicle, + ordersWithAppended(order), + finishedIndex, + complete, + failureFatal, + finished, + processingVehicle + ); + } + + /** + * Returns the next order in the sequence that hasn't been finished, yet. + * + * @return null if this sequence has been finished already or + * currently doesn't have any unfinished orders, else the order after the one + * that was last finished. + */ + public TCSObjectReference getNextUnfinishedOrder() { + // If the whole sequence has been finished already, return null. + if (finished) { + return null; + } + // If the sequence has not been marked as finished but the last order in the + // list has been, return null, too. + else if (finishedIndex + 1 >= orders.size()) { + return null; + } + // Otherwise just get the order after the one that was last finished. + else { + return orders.get(finishedIndex + 1); + } + } + + /** + * Returns the index of the order that was last finished in the sequence, or + * -1, if none was finished, yet. + * + * @return the index of the order that was last finished in the sequence. + */ + public int getFinishedIndex() { + return finishedIndex; + } + + /** + * Creates a copy of this object, with the given finished index. + * + * @param finishedIndex The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public OrderSequence withFinishedIndex(int finishedIndex) { + checkInRange(finishedIndex, 0, orders.size() - 1, "finishedIndex"); + + return new OrderSequence( + getName(), + getProperties(), + getHistory(), + type, + intendedVehicle, + orders, + finishedIndex, + complete, + failureFatal, + finished, + processingVehicle + ); + } + + /** + * Indicates whether this order sequence is complete and will not be extended + * by more orders. + * + * @return true if, and only if, this order sequence is complete + * and will not be extended by more orders. + */ + public boolean isComplete() { + return complete; + } + + /** + * Creates a copy of this object, with the given complete flag. + * + * @param complete The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public OrderSequence withComplete(boolean complete) { + return new OrderSequence( + getName(), + getProperties(), + historyForComplete(complete), + type, + intendedVehicle, + orders, + finishedIndex, + complete, + failureFatal, + finished, + processingVehicle + ); + } + + /** + * Indicates whether this order sequence has been processed completely. + * (Note that processed completely does not necessarily mean + * finished successfully; it is possible that one or more transport + * orders belonging to this sequence have failed.) + * + * @return true if, and only if, this order sequence has been + * processed completely. + */ + public boolean isFinished() { + return finished; + } + + /** + * Creates a copy of this object, with the given finished flag. + * + * @param finished The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public OrderSequence withFinished(boolean finished) { + return new OrderSequence( + getName(), + getProperties(), + historyForFinished(finished), + type, + intendedVehicle, + orders, + finishedIndex, + complete, + failureFatal, + finished, + processingVehicle + ); + } + + /** + * Indicates whether the failure of a single order in this sequence implies + * that all subsequent orders in this sequence are to be considered failed, + * too. + * + * @return true if, and only if, the failure of an order in this + * sequence implies the failure of all subsequent orders. + */ + public boolean isFailureFatal() { + return failureFatal; + } + + /** + * Creates a copy of this object, with the given failure-fatal flag. + * + * @param failureFatal The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public OrderSequence withFailureFatal(boolean failureFatal) { + return new OrderSequence( + getName(), + getProperties(), + getHistory(), + type, + intendedVehicle, + orders, + finishedIndex, + complete, + failureFatal, + finished, + processingVehicle + ); + } + + /** + * Returns a reference to the vehicle that is intended to process this + * order sequence. + * + * @return A reference to the vehicle that is intended to process this + * order sequence. If this sequence is free to be processed by any vehicle, + * null is returned. + */ + public TCSObjectReference getIntendedVehicle() { + return intendedVehicle; + } + + /** + * Creates a copy of this object, with the given intended vehicle. + * + * @param intendedVehicle The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public OrderSequence withIntendedVehicle(TCSObjectReference intendedVehicle) { + return new OrderSequence( + getName(), + getProperties(), + getHistory(), + type, + intendedVehicle, + orders, + finishedIndex, + complete, + failureFatal, + finished, + processingVehicle + ); + } + + /** + * Returns a reference to the vehicle currently processing this sequence. + * + * @return A reference to the vehicle currently processing this sequence. If + * this sequence has not been processed, yet, null is + * returned. + */ + public TCSObjectReference getProcessingVehicle() { + return processingVehicle; + } + + /** + * Creates a copy of this object, with the given processing vehicle. + * + * @param processingVehicle The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public OrderSequence withProcessingVehicle(TCSObjectReference processingVehicle) { + return new OrderSequence( + getName(), + getProperties(), + historyForNewProcessingVehicle(processingVehicle), + type, + intendedVehicle, + orders, + finishedIndex, + complete, + failureFatal, + finished, + processingVehicle + ); + } + + private List> ordersWithAppended( + @Nonnull + TCSObjectReference order + ) { + List> result = new ArrayList<>(orders.size() + 1); + result.addAll(orders); + result.add(order); + return result; + } + + private ObjectHistory historyForNewProcessingVehicle(TCSObjectReference ref) { + return getHistory().withEntryAppended( + new ObjectHistory.Entry( + SEQUENCE_PROCESSING_VEHICLE_CHANGED, + ref == null ? "" : ref.getName() + ) + ); + } + + private ObjectHistory historyForAppendedOrder(TCSObjectReference ref) { + return getHistory().withEntryAppended( + new ObjectHistory.Entry( + SEQUENCE_ORDER_APPENDED, + ref == null ? "" : ref.getName() + ) + ); + } + + private ObjectHistory historyForFinished(boolean finished) { + return finished + ? getHistory().withEntryAppended( + new ObjectHistory.Entry(SEQUENCE_FINISHED) + ) + : getHistory(); + } + + private ObjectHistory historyForComplete(boolean complete) { + return complete + ? getHistory().withEntryAppended( + new ObjectHistory.Entry(SEQUENCE_COMPLETED) + ) + : getHistory(); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/order/OrderSequenceHistoryCodes.java b/opentcs-api-base/src/main/java/org/opentcs/data/order/OrderSequenceHistoryCodes.java new file mode 100644 index 0000000..821bdf1 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/order/OrderSequenceHistoryCodes.java @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.order; + +/** + * Defines constants for basic history event codes related to {@link OrderSequence}s and documents + * how the respective supplementary information is to be interpreted. + */ +public interface OrderSequenceHistoryCodes { + + /** + * An event code indicating a order sequence has been created. + *

+ * The history entry's supplement is empty. + *

+ */ + String SEQUENCE_CREATED = "tcs:history:sequenceCreated"; + + /** + * An event code indicating a transport order has been appended to an order sequence. + *

+ * The history entry's supplement contains the name of the transport order that was appended. + *

+ */ + String SEQUENCE_ORDER_APPENDED = "tcs:history:sequenceOrderAppended"; + + /** + * An event code indicating an order sequence's processing vehicle changed. + *

+ * The history entry's supplement contains the name of the new processing vehicle, or the empty + * string, if the processing vehicle was unset. + *

+ */ + String SEQUENCE_PROCESSING_VEHICLE_CHANGED = "tcs:history:sequenceProcVehicleChanged"; + + /** + * An event code indicating an order sequence has been completed and will not be extended by more + * orders. + *

+ * The history entry's supplement is empty. + *

+ */ + String SEQUENCE_COMPLETED = "tcs:history:sequenceCompleted"; + + /** + * An event code indicating an order sequence has been processed completely. + *

+ * The history entry's supplement is empty. + *

+ */ + String SEQUENCE_FINISHED = "tcs:history:sequenceFinished"; + +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/order/ReroutingType.java b/opentcs-api-base/src/main/java/org/opentcs/data/order/ReroutingType.java new file mode 100644 index 0000000..b41019b --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/order/ReroutingType.java @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.order; + +import org.opentcs.data.model.Vehicle; + +/** + * Defines the different types {@link Vehicle}s can be rerouted. + */ +public enum ReroutingType { + + /** + * Vehicles get rerouted with respect to their current resource allocations. + */ + REGULAR, + /** + * Vehicles get (forcefully) rerouted from their current position (disregarding any resources that + * might have already been allocated). + */ + FORCED; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/order/Route.java b/opentcs-api-base/src/main/java/org/opentcs/data/order/Route.java new file mode 100644 index 0000000..f3d39f7 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/order/Route.java @@ -0,0 +1,347 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.order; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; + +/** + * A route for a {@link Vehicle}, consisting of a sequence of steps (pairs of {@link Path}s and + * {@link Point}s) that need to be processed in their given order. + */ +public class Route + implements + Serializable { + + /** + * The sequence of steps this route consists of, in the order they are to be processed. + */ + private final List steps; + /** + * The costs for travelling this route. + */ + private final long costs; + + /** + * Creates a new Route. + * + * @param routeSteps The sequence of steps this route consists of. + * @param routeCosts The costs for travelling this route. + */ + public Route( + @Nonnull + List routeSteps, + long routeCosts + ) { + requireNonNull(routeSteps, "routeSteps"); + checkArgument(!routeSteps.isEmpty(), "routeSteps may not be empty"); + steps = Collections.unmodifiableList(new ArrayList<>(routeSteps)); + costs = routeCosts; + } + + /** + * Returns the sequence of steps this route consists of. + * + * @return The sequence of steps this route consists of. + * May be empty. + * The returned List is unmodifiable. + */ + @Nonnull + public List getSteps() { + return steps; + } + + /** + * Returns the costs for travelling this route. + * + * @return The costs for travelling this route. + */ + public long getCosts() { + return costs; + } + + /** + * Returns the final destination point that is reached by travelling this route. + * (I.e. returns the destination point of this route's last step.) + * + * @return The final destination point that is reached by travelling this route. + */ + @Nonnull + public Point getFinalDestinationPoint() { + return steps.get(steps.size() - 1).getDestinationPoint(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Route)) { + return false; + } + final Route other = (Route) o; + return costs == other.costs + && Objects.equals(steps, other.steps); + } + + @Override + public int hashCode() { + return Objects.hash(steps, costs); + } + + @Override + public String toString() { + return "Route{" + "steps=" + steps + ", costs=" + costs + '}'; + } + + /** + * A single step in a route. + */ + public static class Step + implements + Serializable { + + /** + * The path to travel. + */ + private final Path path; + /** + * The point that the vehicle is starting from. + */ + private final Point sourcePoint; + /** + * The point that is reached by travelling the path. + */ + private final Point destinationPoint; + /** + * The direction into which the vehicle is supposed to travel. + */ + private final Vehicle.Orientation vehicleOrientation; + /** + * This step's index in the vehicle's route. + */ + private final int routeIndex; + /** + * Whether execution of this step is allowed. + */ + private final boolean executionAllowed; + /** + * Marks this {@link Step} as the origin of a recalculated route and indicates which + * {@link ReroutingType} was used to determine the (new) route. + *

+ * Might be {@code null}, if this {@link Step} is not the origin of a recalculated route. + */ + private final ReroutingType reroutingType; + + /** + * Creates a new instance. + * + * @param path The path to travel. + * @param srcPoint The point that the vehicle is starting from. + * @param destPoint The point that is reached by travelling the path. + * @param orientation The vehicle's orientation on this step. + * @param routeIndex This step's index in the vehicle's route. + * @param executionAllowed Whether execution of this step is allowed. + * @param reroutingType Marks this step as the origin of a recalculated route. + */ + public Step( + @Nullable + Path path, + @Nullable + Point srcPoint, + @Nonnull + Point destPoint, + @Nonnull + Vehicle.Orientation orientation, + int routeIndex, + boolean executionAllowed, + @Nullable + ReroutingType reroutingType + ) { + this.path = path; + this.sourcePoint = srcPoint; + this.destinationPoint = requireNonNull(destPoint, "destPoint"); + this.vehicleOrientation = requireNonNull(orientation, "orientation"); + this.routeIndex = routeIndex; + this.executionAllowed = executionAllowed; + this.reroutingType = reroutingType; + } + + /** + * Creates a new instance. + * + * @param path The path to travel. + * @param srcPoint The point that the vehicle is starting from. + * @param destPoint The point that is reached by travelling the path. + * @param orientation The vehicle's orientation on this step. + * @param routeIndex This step's index in the vehicle's route. + * @param executionAllowed Whether execution of this step is allowed. + */ + public Step( + @Nullable + Path path, + @Nullable + Point srcPoint, + @Nonnull + Point destPoint, + @Nonnull + Vehicle.Orientation orientation, + int routeIndex, + boolean executionAllowed + ) { + this(path, srcPoint, destPoint, orientation, routeIndex, executionAllowed, null); + } + + /** + * Creates a new instance. + * + * @param path The path to travel. + * @param srcPoint The point that the vehicle is starting from. + * @param destPoint The point that is reached by travelling the path. + * @param orientation The vehicle's orientation on this step. + * @param routeIndex This step's index in the vehicle's route. + */ + public Step( + @Nullable + Path path, + @Nullable + Point srcPoint, + @Nonnull + Point destPoint, + @Nonnull + Vehicle.Orientation orientation, + int routeIndex + ) { + this(path, srcPoint, destPoint, orientation, routeIndex, true, null); + } + + /** + * Returns the path to travel. + * + * @return The path to travel. May be null if the vehicle does + * not really have to move. + */ + @Nullable + public Path getPath() { + return path; + } + + /** + * Returns the point that the vehicle is starting from. + * + * @return The point that the vehicle is starting from. + * May be null if the vehicle does not really have to move. + */ + @Nullable + public Point getSourcePoint() { + return sourcePoint; + } + + /** + * Returns the point that is reached by travelling the path. + * + * @return The point that is reached by travelling the path. + */ + @Nonnull + public Point getDestinationPoint() { + return destinationPoint; + } + + /** + * Returns the direction into which the vehicle is supposed to travel. + * + * @return The direction into which the vehicle is supposed to travel. + */ + @Nonnull + public Vehicle.Orientation getVehicleOrientation() { + return vehicleOrientation; + } + + /** + * Returns this step's index in the vehicle's route. + * + * @return This step's index in the vehicle's route. + */ + public int getRouteIndex() { + return routeIndex; + } + + /** + * Returns whether execution of this step is allowed. + * + * @return {@code true}, if execution of this step is allowed, otherwise {@code false}. + */ + public boolean isExecutionAllowed() { + return executionAllowed; + } + + /** + * Returns the {@link ReroutingType} of this step. + *

+ * Idicates whether this step is the origin of a recalculated route, and if so, which + * {@link ReroutingType} was used to determine the (new) route. + *

+ * Might return {@code null}, if this step is not the origin of a recalculated route. + * + * @return The {@link ReroutingType} of this step. + */ + @Nullable + public ReroutingType getReroutingType() { + return reroutingType; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Step)) { + return false; + } + Step other = (Step) o; + return Objects.equals(path, other.path) + && Objects.equals(sourcePoint, other.sourcePoint) + && Objects.equals(destinationPoint, other.destinationPoint) + && Objects.equals(vehicleOrientation, other.vehicleOrientation) + && routeIndex == other.routeIndex + && executionAllowed == other.executionAllowed + && reroutingType == other.reroutingType; + } + + /** + * Compares the given step to this step, ignoring rerouting-related properties. + * + * @param step The step to compare to. + * @return {@code true}, if the given step is equal to this step (ignoring rerouting-related + * properties), otherwise {@code false}. + */ + public boolean equalsInMovement(Step step) { + if (step == null) { + return false; + } + return Objects.equals(this.getSourcePoint(), step.getSourcePoint()) + && Objects.equals(this.getDestinationPoint(), step.getDestinationPoint()) + && Objects.equals(this.getPath(), step.getPath()) + && Objects.equals(this.getVehicleOrientation(), step.getVehicleOrientation()) + && Objects.equals(this.getRouteIndex(), step.getRouteIndex()); + } + + @Override + public int hashCode() { + return Objects.hash( + path, sourcePoint, destinationPoint, vehicleOrientation, routeIndex, + executionAllowed, reroutingType + ); + } + + @Override + public String toString() { + return destinationPoint.getName(); + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/order/TransportOrder.java b/opentcs-api-base/src/main/java/org/opentcs/data/order/TransportOrder.java new file mode 100644 index 0000000..da53023 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/order/TransportOrder.java @@ -0,0 +1,1045 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.order; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.data.order.TransportOrderHistoryCodes.ORDER_CREATED; +import static org.opentcs.data.order.TransportOrderHistoryCodes.ORDER_DRIVE_ORDER_FINISHED; +import static org.opentcs.data.order.TransportOrderHistoryCodes.ORDER_PROCESSING_VEHICLE_CHANGED; +import static org.opentcs.data.order.TransportOrderHistoryCodes.ORDER_REACHED_FINAL_STATE; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.io.Serializable; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; + +/** + * Represents a sequence of movements and operations that are to be executed by a {@link Vehicle}. + *

+ * A TransportOrder basically encapsulates a list of {@link DriveOrder} instances. + *

+ *

+ * Transport orders may depend on other transport orders in the systems, which means they may not be + * processed before the orders they depend on have been processed. + *

+ */ +public class TransportOrder + extends + TCSObject + implements + Serializable { + + /** + * A value indicating that no route steps have been travelled for a drive order, yet. + */ + public static final int ROUTE_STEP_INDEX_DEFAULT = -1; + /** + * The type of this transport order. + */ + @Nonnull + private final String type; + /** + * A set of TransportOrders that must have been finished before this one may + * be processed. + */ + @Nonnull + private final Set> dependencies; + /** + * The drive orders this transport order consists of. + */ + @Nonnull + private final List driveOrders; + /** + * An optional token for reserving peripheral devices while processing this transport order. + */ + @Nullable + private final String peripheralReservationToken; + /** + * The index of the currently processed drive order. + */ + private final int currentDriveOrderIndex; + /** + * The index of the last route step travelled for the currently processed drive order. + */ + private final int currentRouteStepIndex; + /** + * This transport order's current state. + */ + @Nonnull + private final State state; + /** + * The point of time at which this transport order was created. + */ + private final Instant creationTime; + /** + * The point of time at which processing of this transport order must be finished. + */ + private final Instant deadline; + /** + * The point of time at which processing of this transport order was finished. + */ + private final Instant finishedTime; + /** + * A reference to the vehicle that is intended to process this transport + * order. If this order is free to be processed by any vehicle, this is + * null. + */ + @Nullable + private final TCSObjectReference intendedVehicle; + /** + * A reference to the vehicle currently processing this transport order. If + * this transport order is not being processed at the moment, this is + * null. + */ + @Nullable + private final TCSObjectReference processingVehicle; + /** + * The order sequence this transport order belongs to. May be + * null in case this order isn't part of any sequence. + */ + @Nullable + private final TCSObjectReference wrappingSequence; + /** + * Whether this order is dispensable (may be withdrawn automatically). + */ + private final boolean dispensable; + + /** + * Creates a new TransportOrder. + * + * @param name This transport order's name. + * @param driveOrders A list of drive orders to be processed when processing this transport + * order. + */ + public TransportOrder(String name, List driveOrders) { + super( + name, + new HashMap<>(), + new ObjectHistory().withEntryAppended(new ObjectHistory.Entry(ORDER_CREATED)) + ); + this.type = OrderConstants.TYPE_NONE; + this.driveOrders = requireNonNull(driveOrders, "driveOrders"); + this.peripheralReservationToken = null; + this.currentDriveOrderIndex = -1; + this.currentRouteStepIndex = ROUTE_STEP_INDEX_DEFAULT; + this.state = State.RAW; + this.creationTime = Instant.EPOCH; + this.intendedVehicle = null; + this.processingVehicle = null; + this.deadline = Instant.MAX; + this.finishedTime = Instant.MAX; + this.dispensable = false; + this.wrappingSequence = null; + this.dependencies = new LinkedHashSet<>(); + } + + /** + * Creates a new TransportOrder. + * + * @param objectID This transport order's ID. + * @param name This transport order's name. + * @param destinations A list of destinations that are to be travelled to + * when processing this transport order. + * @param creationTime The creation time stamp to be set. + */ + private TransportOrder( + String name, + Map properties, + ObjectHistory history, + String type, + List driveOrders, + String peripheralReservationToken, + int currentDriveOrderIndex, + int currentRouteStepIndex, + Instant creationTime, + TCSObjectReference intendedVehicle, + Instant deadline, + boolean dispensable, + TCSObjectReference wrappingSequence, + Set> dependencies, + TCSObjectReference processingVehicle, + State state, + Instant finishedTime + ) { + super(name, properties, history); + + this.type = requireNonNull(type, "type"); + + requireNonNull(driveOrders, "driveOrders"); + this.driveOrders = new ArrayList<>(driveOrders.size()); + for (DriveOrder driveOrder : driveOrders) { + this.driveOrders.add(driveOrder.withTransportOrder(this.getReference())); + } + + this.peripheralReservationToken = peripheralReservationToken; + this.currentDriveOrderIndex = currentDriveOrderIndex; + this.currentRouteStepIndex = currentRouteStepIndex; + this.creationTime = requireNonNull(creationTime, "creationTime"); + this.intendedVehicle = intendedVehicle; + this.deadline = requireNonNull(deadline, "deadline"); + this.dispensable = dispensable; + this.wrappingSequence = wrappingSequence; + this.dependencies = requireNonNull(dependencies, "dependencies"); + this.processingVehicle = processingVehicle; + this.state = requireNonNull(state, "state"); + this.finishedTime = requireNonNull(finishedTime, "finishedTime"); + } + + @Override + public TransportOrder withProperty(String key, String value) { + return new TransportOrder( + getName(), + propertiesWith(key, value), + getHistory(), + type, + driveOrders, + peripheralReservationToken, + currentDriveOrderIndex, + currentRouteStepIndex, + creationTime, + intendedVehicle, + deadline, + dispensable, + wrappingSequence, + dependencies, + processingVehicle, + state, + finishedTime + ); + } + + @Override + public TransportOrder withProperties(Map properties) { + return new TransportOrder( + getName(), + properties, + getHistory(), + type, + driveOrders, + peripheralReservationToken, + currentDriveOrderIndex, + currentRouteStepIndex, + creationTime, + intendedVehicle, + deadline, + dispensable, + wrappingSequence, + dependencies, + processingVehicle, + state, + finishedTime + ); + } + + @Override + public TransportOrder withHistoryEntry(ObjectHistory.Entry entry) { + return new TransportOrder( + getName(), + getProperties(), + getHistory().withEntryAppended(entry), + type, + driveOrders, + peripheralReservationToken, + currentDriveOrderIndex, + currentRouteStepIndex, + creationTime, + intendedVehicle, + deadline, + dispensable, + wrappingSequence, + dependencies, + processingVehicle, + state, + finishedTime + ); + } + + @Override + public TransportOrder withHistory(ObjectHistory history) { + return new TransportOrder( + getName(), + getProperties(), + history, + type, + driveOrders, + peripheralReservationToken, + currentDriveOrderIndex, + currentRouteStepIndex, + creationTime, + intendedVehicle, + deadline, + dispensable, + wrappingSequence, + dependencies, + processingVehicle, + state, + finishedTime + ); + } + + /** + * Retruns this transport order's type. + * + * @return This transport order's type. + */ + public String getType() { + return type; + } + + /** + * Creates a copy of this obejct, with the given type. + * + * @param type The tpye to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public TransportOrder withType(String type) { + return new TransportOrder( + getName(), + getProperties(), + getHistory(), + type, + driveOrders, + peripheralReservationToken, + currentDriveOrderIndex, + currentRouteStepIndex, + creationTime, + intendedVehicle, + deadline, + dispensable, + wrappingSequence, + dependencies, + processingVehicle, + state, + finishedTime + ); + } + + /** + * Returns this transport order's current state. + * + * @return This transport order's current state. + */ + public State getState() { + return state; + } + + /** + * Checks if this transport order's current state is equal to the given one. + * + * @param otherState The state to compare to this transport order's one. + * @return true if, and only if, the given state is equal to this + * transport order's one. + */ + public boolean hasState(State otherState) { + requireNonNull(otherState, "otherState"); + return this.state.equals(otherState); + } + + /** + * Creates a copy of this object, with the given state. + * + * @param state The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public TransportOrder withState( + @Nonnull + State state + ) { + // XXX Finished time should probably not be set implicitly. + return new TransportOrder( + getName(), + getProperties(), + historyForNewState(state), + type, + driveOrders, + peripheralReservationToken, + currentDriveOrderIndex, + currentRouteStepIndex, + creationTime, + intendedVehicle, + deadline, + dispensable, + wrappingSequence, + dependencies, + processingVehicle, + state, + state == State.FINISHED ? Instant.now() : finishedTime + ); + } + + /** + * Returns this transport order's creation time. + * + * @return This transport order's creation time. + */ + public Instant getCreationTime() { + return creationTime; + } + + /** + * Creates a copy of this object, with the given creation time. + * + * @param creationTime The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public TransportOrder withCreationTime(Instant creationTime) { + return new TransportOrder( + getName(), + getProperties(), + getHistory(), + type, + driveOrders, + peripheralReservationToken, + currentDriveOrderIndex, + currentRouteStepIndex, + creationTime, + intendedVehicle, + deadline, + dispensable, + wrappingSequence, + dependencies, + processingVehicle, + state, + finishedTime + ); + } + + /** + * Returns this transport order's deadline. If the value of transport order's + * deadline was not changed, the initial value {@link Instant#MAX} is returned. + * + * @return This transport order's deadline or the initial deadline value.{@link Instant#MAX}, if + * the deadline was not changed. + */ + public Instant getDeadline() { + return deadline; + } + + /** + * Creates a copy of this object, with the given deadline. + * + * @param deadline The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public TransportOrder withDeadline(Instant deadline) { + return new TransportOrder( + getName(), + getProperties(), + getHistory(), + type, + driveOrders, + peripheralReservationToken, + currentDriveOrderIndex, + currentRouteStepIndex, + creationTime, + intendedVehicle, + deadline, + dispensable, + wrappingSequence, + dependencies, + processingVehicle, + state, + finishedTime + ); + } + + /** + * Returns the point of time at which this transport order was finished. + * If the transport order has not been finished, yet, {@link Instant#MAX} is returned. + * + * @return The point of time at which this transport order was finished, or {@link Instant#MAX}, + * if the transport order has not been finished, yet. + */ + public Instant getFinishedTime() { + return finishedTime; + } + + /** + * Creates a copy of this object, with the given finished time. + * + * @param finishedTime The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public TransportOrder withFinishedTime(Instant finishedTime) { + return new TransportOrder( + getName(), + getProperties(), + getHistory(), + type, + driveOrders, + peripheralReservationToken, + currentDriveOrderIndex, + currentRouteStepIndex, + creationTime, + intendedVehicle, + deadline, + dispensable, + wrappingSequence, + dependencies, + processingVehicle, + state, + finishedTime + ); + } + + /** + * Returns a reference to the vehicle that is intended to process this + * transport order. + * + * @return A reference to the vehicle that is intended to process this + * transport order. If this order is free to be processed by any vehicle, + * null is returned. + */ + @Nullable + public TCSObjectReference getIntendedVehicle() { + return intendedVehicle; + } + + /** + * Creates a copy of this object, with the given intended vehicle. + * + * @param intendedVehicle The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public TransportOrder withIntendedVehicle( + @Nullable + TCSObjectReference intendedVehicle + ) { + return new TransportOrder( + getName(), + getProperties(), + getHistory(), + type, + driveOrders, + peripheralReservationToken, + currentDriveOrderIndex, + currentRouteStepIndex, + creationTime, + intendedVehicle, + deadline, + dispensable, + wrappingSequence, + dependencies, + processingVehicle, + state, + finishedTime + ); + } + + /** + * Returns a reference to the vehicle currently processing this transport + * order. + * + * @return A reference to the vehicle currently processing this transport + * order. If this transport order is not currently being processed, + * null is returned. + */ + @Nullable + public TCSObjectReference getProcessingVehicle() { + return processingVehicle; + } + + /** + * Creates a copy of this object, with the given processing vehicle. + * + * @param processingVehicle The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public TransportOrder withProcessingVehicle( + @Nullable + TCSObjectReference processingVehicle + ) { + return new TransportOrder( + getName(), + getProperties(), + historyForNewProcessingVehicle(processingVehicle), + type, + driveOrders, + peripheralReservationToken, + currentDriveOrderIndex, + currentRouteStepIndex, + creationTime, + intendedVehicle, + deadline, + dispensable, + wrappingSequence, + dependencies, + processingVehicle, + state, + finishedTime + ); + } + + /** + * Returns the set of transport orders this order depends on. + * + * @return The set of transport orders this order depends on. + */ + public Set> getDependencies() { + return dependencies; + } + + /** + * Creates a copy of this object, with the given dependencies. + * + * @param dependencies The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public TransportOrder withDependencies( + @Nonnull + Set> dependencies + ) { + return new TransportOrder( + getName(), + getProperties(), + getHistory(), + type, + driveOrders, + peripheralReservationToken, + currentDriveOrderIndex, + currentRouteStepIndex, + creationTime, + intendedVehicle, + deadline, + dispensable, + wrappingSequence, + dependencies, + processingVehicle, + state, + finishedTime + ); + } + + /** + * Returns a list of DriveOrders that have been processed already. + * + * @return A list of DriveOrders that have been processed already. + */ + @Nonnull + public List getPastDriveOrders() { + List result = new ArrayList<>(); + for (int i = 0; i < currentDriveOrderIndex; i++) { + result.add(driveOrders.get(i)); + } + return result; + } + + /** + * Returns a list of DriveOrders that still need to be processed. + * + * @return A list of DriveOrders that still need to be processed. + */ + @Nonnull + public List getFutureDriveOrders() { + List result = new ArrayList<>(); + for (int i = currentDriveOrderIndex + 1; i < driveOrders.size(); i++) { + result.add(driveOrders.get(i)); + } + return result; + } + + /** + * Creates a copy of this object, with the given drive orders. + * + * @param driveOrders The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public TransportOrder withDriveOrders( + @Nonnull + List driveOrders + ) { + requireNonNull(driveOrders, "driveOrders"); + return new TransportOrder( + getName(), + getProperties(), + getHistory(), + type, + driveOrders, + peripheralReservationToken, + currentDriveOrderIndex, + currentRouteStepIndex, + creationTime, + intendedVehicle, + deadline, + dispensable, + wrappingSequence, + dependencies, + processingVehicle, + state, + finishedTime + ); + } + + /** + * Returns the current drive order, or null, if no drive order is + * currently being processed. + * + * @return the current drive order, or null, if no drive order is + * currently being processed. + */ + @Nullable + public DriveOrder getCurrentDriveOrder() { + return (currentDriveOrderIndex >= 0 && currentDriveOrderIndex < driveOrders.size()) + ? driveOrders.get(currentDriveOrderIndex) + : null; + } + + /** + * Returns a list of all drive orders, i.e. the past, current and future drive + * orders. + * + * @return A list of all drive orders, i.e. the past, current and future drive + * orders. If no drive orders exist, the returned list is empty. + */ + @Nonnull + public List getAllDriveOrders() { + return new ArrayList<>(driveOrders); + } + + /** + * Returns an optional token for reserving peripheral devices while processing this transport + * order. + * + * @return An optional token for reserving peripheral devices while processing this transport + * order. + */ + @Nullable + public String getPeripheralReservationToken() { + return peripheralReservationToken; + } + + /** + * Creates a copy of this object, with the given reservation token. + * + * @param peripheralReservationToken The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public TransportOrder withPeripheralReservationToken( + @Nullable + String peripheralReservationToken + ) { + return new TransportOrder( + getName(), + getProperties(), + getHistory(), + type, + driveOrders, + peripheralReservationToken, + currentDriveOrderIndex, + currentRouteStepIndex, + creationTime, + intendedVehicle, + deadline, + dispensable, + wrappingSequence, + dependencies, + processingVehicle, + state, + finishedTime + ); + } + + /** + * Returns the index of the currently processed drive order. + * + * @return The index of the currently processed drive order. + */ + public int getCurrentDriveOrderIndex() { + return currentDriveOrderIndex; + } + + /** + * Creates a copy of this object, with the given drive order index. + * + * @param currentDriveOrderIndex The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public TransportOrder withCurrentDriveOrderIndex(int currentDriveOrderIndex) { + return new TransportOrder( + getName(), + getProperties(), + getHistory(), + type, + driveOrders, + peripheralReservationToken, + currentDriveOrderIndex, + currentRouteStepIndex, + creationTime, + intendedVehicle, + deadline, + dispensable, + wrappingSequence, + dependencies, + processingVehicle, + state, + finishedTime + ); + } + + /** + * Returns the index of the last route step travelled for the currently processed drive order. + * + * @return The index of the last route step travelled for the currently processed drive order. + */ + public int getCurrentRouteStepIndex() { + return currentRouteStepIndex; + } + + /** + * Creates a copy of this object, with the given route step index. + * + * @param currentRouteStepIndex The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public TransportOrder withCurrentRouteStepIndex(int currentRouteStepIndex) { + return new TransportOrder( + getName(), + getProperties(), + getHistory(), + type, + driveOrders, + peripheralReservationToken, + currentDriveOrderIndex, + currentRouteStepIndex, + creationTime, + intendedVehicle, + deadline, + dispensable, + wrappingSequence, + dependencies, + processingVehicle, + state, + finishedTime + ); + } + + /** + * Creates a copy of this object, with the given current drive order state. + * + * @param driveOrderState The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public TransportOrder withCurrentDriveOrderState( + @Nonnull + DriveOrder.State driveOrderState + ) { + requireNonNull(driveOrderState, "driveOrderState"); + + List newDriveOrders = new ArrayList<>(this.driveOrders); + newDriveOrders.set( + currentDriveOrderIndex, + newDriveOrders.get(currentDriveOrderIndex).withState(driveOrderState) + ); + + return new TransportOrder( + getName(), + getProperties(), + historyForNewDriveOrderState(driveOrderState), + type, + newDriveOrders, + peripheralReservationToken, + currentDriveOrderIndex, + currentRouteStepIndex, + creationTime, + intendedVehicle, + deadline, + dispensable, + wrappingSequence, + dependencies, + processingVehicle, + state, + finishedTime + ); + } + + /** + * Returns the order sequence this order belongs to, or null, if + * it doesn't belong to any sequence. + * + * @return The order sequence this order belongs to, or null, if + * it doesn't belong to any sequence. + */ + @Nullable + public TCSObjectReference getWrappingSequence() { + return wrappingSequence; + } + + /** + * Creates a copy of this object, with the given wrapping sequence. + * + * @param wrappingSequence The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public TransportOrder withWrappingSequence( + @Nullable + TCSObjectReference wrappingSequence + ) { + return new TransportOrder( + getName(), + getProperties(), + getHistory(), + type, + driveOrders, + peripheralReservationToken, + currentDriveOrderIndex, + currentRouteStepIndex, + creationTime, + intendedVehicle, + deadline, + dispensable, + wrappingSequence, + dependencies, + processingVehicle, + state, + finishedTime + ); + } + + /** + * Checks if this order is dispensable. + * + * @return true if, and only if, this order is dispensable. + */ + public boolean isDispensable() { + return dispensable; + } + + /** + * Creates a copy of this object, with the given dispensable flag. + * + * @param dispensable The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public TransportOrder withDispensable(boolean dispensable) { + return new TransportOrder( + getName(), + getProperties(), + getHistory(), + type, + driveOrders, + peripheralReservationToken, + currentDriveOrderIndex, + currentRouteStepIndex, + creationTime, + intendedVehicle, + deadline, + dispensable, + wrappingSequence, + dependencies, + processingVehicle, + state, + finishedTime + ); + } + + @Override + public String toString() { + return "TransportOrder{" + + "name=" + getName() + + ", state=" + state + + ", intendedVehicle=" + intendedVehicle + + ", processingVehicle=" + processingVehicle + + ", creationTime=" + creationTime + + ", deadline=" + deadline + + ", finishedTime=" + finishedTime + + ", wrappingSequence=" + wrappingSequence + + ", dispensable=" + dispensable + + ", type=" + type + + ", peripheralReservationToken=" + peripheralReservationToken + + ", dependencies=" + dependencies + + ", driveOrders=" + driveOrders + + ", currentDriveOrderIndex=" + currentDriveOrderIndex + + ", currentRouteStepIndex=" + currentRouteStepIndex + + '}'; + } + + private ObjectHistory historyForNewState(State state) { + return state.isFinalState() + ? getHistory().withEntryAppended(new ObjectHistory.Entry(ORDER_REACHED_FINAL_STATE)) + : getHistory(); + } + + private ObjectHistory historyForNewDriveOrderState(DriveOrder.State state) { + return state == DriveOrder.State.FINISHED + ? getHistory().withEntryAppended(new ObjectHistory.Entry(ORDER_DRIVE_ORDER_FINISHED)) + : getHistory(); + } + + private ObjectHistory historyForNewProcessingVehicle(TCSObjectReference ref) { + return getHistory().withEntryAppended( + new ObjectHistory.Entry( + ORDER_PROCESSING_VEHICLE_CHANGED, + ref == null ? "" : ref.getName() + ) + ); + } + + /** + * This enumeration defines the various states a transport order may be in. + */ + public enum State { + + /** + * A transport order's initial state. + * A transport order remains in this state until its parameters have been + * set up completely. + */ + RAW, + /** + * Set (by a user/client) when a transport order's parameters have been set + * up completely and the kernel should dispatch it when possible. + */ + ACTIVE, + /** + * Marks a transport order as ready to be dispatched to a vehicle (i.e. all + * its dependencies have been finished). + */ + DISPATCHABLE, + /** + * Marks a transport order as being processed by a vehicle. + */ + BEING_PROCESSED, + /** + * Indicates the transport order is withdrawn from a processing vehicle but + * not yet in its final state (which will be FAILED), as the vehicle has not + * yet finished/cleaned up. + */ + WITHDRAWN, + /** + * Marks a transport order as successfully completed. + */ + FINISHED, + /** + * General failure state that marks a transport order as failed. + */ + FAILED, + /** + * Failure state that marks a transport order as unroutable, i.e. it is + * impossible to find a route that would allow a vehicle to process the + * transport order completely. + */ + UNROUTABLE; + + /** + * Checks if this state is a final state for a transport order. + * + * @return true if, and only if, this state is a final state + * for a transport order - i.e. FINISHED, FAILED or UNROUTABLE. + */ + public boolean isFinalState() { + return this.equals(FINISHED) + || this.equals(FAILED) + || this.equals(UNROUTABLE); + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/order/TransportOrderHistoryCodes.java b/opentcs-api-base/src/main/java/org/opentcs/data/order/TransportOrderHistoryCodes.java new file mode 100644 index 0000000..ea6ae91 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/order/TransportOrderHistoryCodes.java @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.order; + +/** + * Defines constants for basic history event codes related to transport orders and documents how the + * respective supplementary information is to be interpreted. + */ +public interface TransportOrderHistoryCodes { + + /** + * An event code indicating a transport order has been created. + *

+ * The history entry's supplement is empty. + *

+ */ + String ORDER_CREATED = "tcs:history:orderCreated"; + /** + * An event code indicating dispatching of a transport order to a vehicle has been deferred. + *

+ * The history entry's supplement contains a list of reasons for the deferral. + *

+ */ + String ORDER_DISPATCHING_DEFERRED = "tcs:history:orderDispatchingDeferred"; + /** + * An event code indicating dispatching of a transport order to a vehicle has been resumed. + *

+ * The history entry's supplement is empty. + *

+ */ + String ORDER_DISPATCHING_RESUMED = "tcs:history:orderDispatchingResumed"; + /** + * An event code indicating a transport order was assigned to a vehicle. + *

+ * The history entry's supplement contains the name of the vehicle the transport order was + * assigned to. + *

+ */ + String ORDER_ASSIGNED_TO_VEHICLE = "tcs:history:orderAssignedToVehicle"; + /** + * An event code indicating a transport order was reserved for a vehicle. + *

+ * The history entry's supplement contains the name of the vehicle the transport order was + * reserved for. + *

+ */ + String ORDER_RESERVED_FOR_VEHICLE = "tcs:history:orderReservedForVehicle"; + /** + * An event code indicating a transport order's processing vehicle changed. + *

+ * The history entry's supplement contains the name of the new processing vehicle, or the empty + * string, if the processing vehicle was unset. + *

+ */ + String ORDER_PROCESSING_VEHICLE_CHANGED = "tcs:history:orderProcVehicleChanged"; + /** + * An event code indicating a transport order was marked as being in a final state. + *

+ * The history entry's supplement is empty. + *

+ */ + String ORDER_REACHED_FINAL_STATE = "tcs:history:orderReachedFinalState"; + /** + * An event code indicating one of a transport order's drive orders has been finished. + *

+ * The history entry's supplement is empty. + *

+ */ + String ORDER_DRIVE_ORDER_FINISHED = "tcs:history:orderFinishedDriveOrder"; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/order/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/data/order/package-info.java new file mode 100644 index 0000000..8a96cb2 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/order/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Classes describing transport orders to be processed by vehicles. + */ +package org.opentcs.data.order; diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/data/package-info.java new file mode 100644 index 0000000..79b6411 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Basic openTCS data structures. + */ +package org.opentcs.data; diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/peripherals/PeripheralJob.java b/opentcs-api-base/src/main/java/org/opentcs/data/peripherals/PeripheralJob.java new file mode 100644 index 0000000..d108445 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/peripherals/PeripheralJob.java @@ -0,0 +1,448 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.peripherals; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.data.peripherals.PeripheralJobHistoryCodes.JOB_CREATED; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.io.Serializable; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; + +/** + * Represents a job that is to be processed by a peripheral device. + */ +public class PeripheralJob + extends + TCSObject + implements + Serializable { + + /** + * A token that may be used to reserve a peripheral device. + * A peripheral device that is reserved for a specific token can only process jobs which match + * that reservation token. + * This string may not be empty. + */ + @Nonnull + private final String reservationToken; + /** + * The vehicle for which this peripheral job was created. + * May be {@code null}, if this job wasn't created in the context of a transport order being + * processed by a vehicle. + */ + @Nullable + private final TCSObjectReference relatedVehicle; + /** + * The transport order for which this peripheral job was created. + * May be {@code null}, if this job wasn't created in the context of a transport order being + * processed by a vehicle. + */ + @Nullable + private final TCSObjectReference relatedTransportOrder; + /** + * The operation that is to be performed by the pripheral device. + */ + @Nonnull + private final PeripheralOperation peripheralOperation; + /** + * This peripheral job's current state. + */ + @Nonnull + private final State state; + /** + * The point of time at which this peripheral job was created. + */ + @Nonnull + private final Instant creationTime; + /** + * The point of time at which processing of this peripheral job was finished. + */ + @Nonnull + private final Instant finishedTime; + + /** + * Creates a new instance. + * + * @param name The peripheral job's name. + * @param reservationToken The reservation token to be used. + * @param peripheralOperation The operation to be performed. + */ + public PeripheralJob( + @Nonnull + String name, + @Nonnull + String reservationToken, + @Nonnull + PeripheralOperation peripheralOperation + ) { + this( + name, + new HashMap<>(), + new ObjectHistory().withEntryAppended(new ObjectHistory.Entry(JOB_CREATED)), + reservationToken, + null, + null, + peripheralOperation, + State.TO_BE_PROCESSED, + Instant.now(), + Instant.MAX + ); + } + + private PeripheralJob( + String objectName, + Map properties, + ObjectHistory history, + String reservationToken, + TCSObjectReference relatedVehicle, + TCSObjectReference transportOrder, + PeripheralOperation peripheralOperation, + State state, + Instant creationTime, + Instant finishedTime + ) { + super(objectName, properties, history); + this.reservationToken = requireNonNull(reservationToken, "reservationToken"); + checkArgument(!reservationToken.isEmpty(), "reservationToken may not be empty."); + this.relatedVehicle = relatedVehicle; + this.relatedTransportOrder = transportOrder; + this.peripheralOperation = requireNonNull(peripheralOperation, "peripheralOperation"); + this.state = requireNonNull(state, "state"); + this.creationTime = requireNonNull(creationTime, "creationTime"); + this.finishedTime = requireNonNull(finishedTime, "finishedTime"); + } + + @Override + public PeripheralJob withProperty(String key, String value) { + return new PeripheralJob( + getName(), + propertiesWith(key, value), + getHistory(), + reservationToken, + relatedVehicle, + relatedTransportOrder, + peripheralOperation, + state, + creationTime, + finishedTime + ); + } + + @Override + public PeripheralJob withProperties(Map properties) { + return new PeripheralJob( + getName(), + properties, + getHistory(), + reservationToken, + relatedVehicle, + relatedTransportOrder, + peripheralOperation, + state, + creationTime, + finishedTime + ); + } + + @Override + public PeripheralJob withHistoryEntry(ObjectHistory.Entry entry) { + return new PeripheralJob( + getName(), + getProperties(), + getHistory().withEntryAppended(entry), + reservationToken, + relatedVehicle, + relatedTransportOrder, + peripheralOperation, + state, + creationTime, + finishedTime + ); + } + + @Override + public PeripheralJob withHistory(ObjectHistory history) { + return new PeripheralJob( + getName(), + getProperties(), + history, + reservationToken, + relatedVehicle, + relatedTransportOrder, + peripheralOperation, + state, + creationTime, + finishedTime + ); + } + + /** + * Returns the token that may be used to reserve a peripheral device. + * + * @return The token that may be used to reserve a peripheral device. + */ + public String getReservationToken() { + return reservationToken; + } + + /** + * Creates a copy of this object, with the given reservation token. + * + * @param reservationToken The reservation token to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralJob withReservationToken(String reservationToken) { + return new PeripheralJob( + getName(), + getProperties(), + getHistory(), + reservationToken, + relatedVehicle, + relatedTransportOrder, + peripheralOperation, + state, + creationTime, + finishedTime + ); + } + + /** + * Returns the vehicle for which this peripheral job was created. + * + * @return The vehicle for which this peripheral job was created. + */ + public TCSObjectReference getRelatedVehicle() { + return relatedVehicle; + } + + /** + * Creates a copy of this object, with the given related vehicle. + * + * @param relatedVehicle The related vehicle to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralJob withRelatedVehicle(TCSObjectReference relatedVehicle) { + return new PeripheralJob( + getName(), + getProperties(), + getHistory(), + reservationToken, + relatedVehicle, + relatedTransportOrder, + peripheralOperation, + state, + creationTime, + finishedTime + ); + } + + /** + * Returns the transport order for which this peripheral job was created. + * + * @return The transport order for which this peripheral job was created. + */ + public TCSObjectReference getRelatedTransportOrder() { + return relatedTransportOrder; + } + + /** + * Creates a copy of this object, with the given related transport order. + * + * @param relatedTransportOrder The related transport order to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralJob withRelatedTransportOrder( + TCSObjectReference relatedTransportOrder + ) { + return new PeripheralJob( + getName(), + getProperties(), + getHistory(), + reservationToken, + relatedVehicle, + relatedTransportOrder, + peripheralOperation, + state, + creationTime, + finishedTime + ); + } + + /** + * Returns the operation that is to be performed by the peripheral device. + * + * @return The operation that is to be performed by the peripheral device. + */ + public PeripheralOperation getPeripheralOperation() { + return peripheralOperation; + } + + /** + * Creates a copy of this object, with the given peripheral operation. + * + * @param peripheralOperation The peripheral operation to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralJob withPeripheralOperation(PeripheralOperation peripheralOperation) { + return new PeripheralJob( + getName(), + getProperties(), + getHistory(), + reservationToken, + relatedVehicle, + relatedTransportOrder, + peripheralOperation, + state, + creationTime, + finishedTime + ); + } + + /** + * Returns this peripheral job's current state. + * + * @return this peripheral job's current state. + */ + public State getState() { + return state; + } + + /** + * Creates a copy of this object, with the given state. + * + * @param state The state to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralJob withState(State state) { + // XXX Finished time should probably not be set implicitly. + return new PeripheralJob( + getName(), + getProperties(), + getHistory(), + reservationToken, + relatedVehicle, + relatedTransportOrder, + peripheralOperation, + state, + creationTime, + state == State.FINISHED ? Instant.now() : finishedTime + ); + } + + /** + * Returns the point of time at which this peripheral job was created. + * + * @return The point of time at which this peripheral job was created. + */ + public Instant getCreationTime() { + return creationTime; + } + + /** + * Creates a copy of this object, with the given creation time. + * + * @param creationTime The creation time to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralJob withCreationTime(Instant creationTime) { + return new PeripheralJob( + getName(), + getProperties(), + getHistory(), + reservationToken, + relatedVehicle, + relatedTransportOrder, + peripheralOperation, + state, + creationTime, + finishedTime + ); + } + + /** + * Returns the point of time at which processing of this peripheral job was finished. + * + * @return The point of time at which processing of this peripheral job was finished. + */ + public Instant getFinishedTime() { + return finishedTime; + } + + /** + * Creates a copy of this object, with the given finished time. + * + * @param finishedTime The finished time to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralJob withFinishedTime(Instant finishedTime) { + return new PeripheralJob( + getName(), + getProperties(), + getHistory(), + reservationToken, + relatedVehicle, + relatedTransportOrder, + peripheralOperation, + state, + creationTime, + finishedTime + ); + } + + @Override + public String toString() { + return "PeripheralJob{" + + "name=" + getName() + + ", reservationToken=" + reservationToken + + ", relatedVehicle=" + relatedVehicle + + ", relatedTransportOrder=" + relatedTransportOrder + + ", peripheralOperation=" + peripheralOperation + + ", state=" + state + + ", creationTime=" + creationTime + + ", finishedTime=" + finishedTime + + '}'; + } + + /** + * Defines the various states a peripheral job may be in. + */ + public enum State { + /** + * Indicates a peripheral job is still waiting to be processed. + */ + TO_BE_PROCESSED, + /** + * Indicates a peripheral job is currently being processed by a peripheral. + */ + BEING_PROCESSED, + /** + * Indicates a peripheral job has been completed successfully. + */ + FINISHED, + /** + * Indicates execution of a peripheral job has failed / was aborted / was never started. + */ + FAILED; + + /** + * Checks if this state is a final state for a peripheral job. + * + * @return true if, and only if, this state is a final state for a peripheral job - + * i.e. FINISHED or FAILED. + */ + public boolean isFinalState() { + return this.equals(FINISHED) + || this.equals(FAILED); + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/peripherals/PeripheralJobHistoryCodes.java b/opentcs-api-base/src/main/java/org/opentcs/data/peripherals/PeripheralJobHistoryCodes.java new file mode 100644 index 0000000..ecef9ac --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/peripherals/PeripheralJobHistoryCodes.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.peripherals; + +/** + * Defines constants for basic history event codes related to peripheral jobs and documents how the + * respective supplementary information is to be interpreted. + */ +public interface PeripheralJobHistoryCodes { + + /** + * An event code indicating a peripheral job has been created. + *

+ * The history entry's supplement is empty. + *

+ */ + String JOB_CREATED = "tcs:history:jobCreated"; + /** + * An event code indicating a peripheral job was marked as being in a final state. + *

+ * The history entry's supplement is empty. + *

+ */ + String JOB_REACHED_FINAL_STATE = "tcs:history:jobReachedFinalState"; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/peripherals/PeripheralOperation.java b/opentcs-api-base/src/main/java/org/opentcs/data/peripherals/PeripheralOperation.java new file mode 100644 index 0000000..a866c2c --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/peripherals/PeripheralOperation.java @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.peripherals; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; + +/** + * Describes an operation that is to be executed by a peripheral device. + */ +public class PeripheralOperation + implements + Serializable { + + /** + * The location the peripheral device is associated with. + */ + @Nonnull + private final TCSResourceReference location; + /** + * The actual operation to be executed by the peripheral device. + */ + @Nonnull + private final String operation; + /** + * The moment at which this operation is to be executed. + */ + @Nonnull + private final ExecutionTrigger executionTrigger; + /** + * Whether the completion of this operation is required to allow a vehicle to continue driving. + */ + private final boolean completionRequired; + + /** + * Creates a new instance. + * + * @param location The location the peripheral device is associated with. + * @param operation The actual operation to be executed by the peripheral device. + * @param executionTrigger The moment at which this operation is to be executed. + * @param completionRequired Whether the completion of this operation is required to allow a + * vehicle to continue driving. + */ + public PeripheralOperation( + @Nonnull + TCSResourceReference location, + @Nonnull + String operation, + @Nonnull + ExecutionTrigger executionTrigger, + boolean completionRequired + ) { + this.location = requireNonNull(location, "location"); + this.operation = requireNonNull(operation, "operation"); + this.executionTrigger = requireNonNull(executionTrigger, "executionTrigger"); + this.completionRequired = completionRequired; + } + + /** + * Returns the location the peripheral device is associated with. + * + * @return The location the peripheral device is associated with. + */ + @Nonnull + public TCSResourceReference getLocation() { + return location; + } + + /** + * Returns the actual operation to be executed by the peripheral device. + * + * @return The actual operation to be executed by the peripheral device. + */ + @Nonnull + public String getOperation() { + return operation; + } + + /** + * Returns the moment at which this operation is to be executed. + * + * @return The moment at which this operation is to be executed. + */ + @Nonnull + public ExecutionTrigger getExecutionTrigger() { + return executionTrigger; + } + + /** + * Returns whether the completion of this operation is required to allow a vehicle to continue + * driving. + * + * @return Whether the completion of this operation is required to allow a vehicle to continue + * driving. + */ + public boolean isCompletionRequired() { + return completionRequired; + } + + /** + * Defines the various moments at which an operation may be executed. + */ + public enum ExecutionTrigger { + /** + * The operation is to be triggered immediately. + */ + IMMEDIATE, + /** + * The operation is to be triggered after the allocation of the path / before the movement. + */ + AFTER_ALLOCATION, + /** + * The operation is to be triggered after the movement. + */ + AFTER_MOVEMENT; + } + + @Override + public String toString() { + return "PeripheralOperation{" + + "location=" + location + ", " + + "operation=" + operation + ", " + + "executionTrigger=" + executionTrigger + ", " + + "completionRequired=" + completionRequired + '}'; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/data/peripherals/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/data/peripherals/package-info.java new file mode 100644 index 0000000..fa5030f --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/data/peripherals/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Classes describing peripheral jobs to be processed by peripheral devices. + */ +package org.opentcs.data.peripherals; diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/LowLevelCommunicationEvent.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/LowLevelCommunicationEvent.java new file mode 100644 index 0000000..e487a1a --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/LowLevelCommunicationEvent.java @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers; + +import java.io.Serializable; + +/** + * Marks low-level communication events. + * Can be used e.g. to distinguish low-level events that are meant for component-internal processing + * and that are irrelevant for high-level UI visualization. + */ +public interface LowLevelCommunicationEvent + extends + Serializable { + +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/BasicPeripheralCommAdapter.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/BasicPeripheralCommAdapter.java new file mode 100644 index 0000000..f463737 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/BasicPeripheralCommAdapter.java @@ -0,0 +1,171 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.peripherals; + +import static java.util.Objects.requireNonNull; + +import org.opentcs.data.model.PeripheralInformation; +import org.opentcs.drivers.peripherals.management.PeripheralProcessModelEvent; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A base class for peripheral communication adapters mainly providing command queue processing. + */ +public abstract class BasicPeripheralCommAdapter + implements + PeripheralCommAdapter { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(BasicPeripheralCommAdapter.class); + /** + * The handler used to send events to. + */ + private final EventHandler eventHandler; + /** + * A model of the peripheral device's and its communication adapter's attributes. + */ + private PeripheralProcessModel processModel; + /** + * Indicates whether this comm adapter is initialized. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param processModel A model of the peripheral device's and its communication adapter's + * attributes. + * @param eventHandler The handler used to send events to. + */ + public BasicPeripheralCommAdapter( + PeripheralProcessModel processModel, + EventHandler eventHandler + ) { + this.processModel = requireNonNull(processModel, "processModel"); + this.eventHandler = requireNonNull(eventHandler, "eventHandler"); + } + + /** + * {@inheritDoc} + *

+ * Overriding methods are expected to call this implementation, too. + *

+ */ + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + setProcessModel(getProcessModel().withState(PeripheralInformation.State.UNKNOWN)); + sendProcessModelChangedEvent(PeripheralProcessModel.Attribute.STATE); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + /** + * {@inheritDoc} + *

+ * Overriding methods are expected to call this implementation, too. + *

+ */ + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + initialized = false; + } + + /** + * {@inheritDoc} + *

+ * Overriding methods are expected to call this implementation, too. + *

+ */ + @Override + public void enable() { + if (isEnabled()) { + return; + } + + LOG.info("Peripheral comm adapter is being enabled: {}", processModel.getLocation().getName()); + connectPeripheral(); + setProcessModel(getProcessModel().withCommAdapterEnabled(true)); + sendProcessModelChangedEvent(PeripheralProcessModel.Attribute.COMM_ADAPTER_ENABLED); + } + + @Override + public boolean isEnabled() { + return processModel.isCommAdapterEnabled(); + } + + /** + * {@inheritDoc} + *

+ * Overriding methods are expected to call this implementation, too. + *

+ */ + @Override + public void disable() { + if (!isEnabled()) { + return; + } + + LOG.info("Peripheral comm adapter is being disabled: {}", processModel.getLocation().getName()); + disconnectPeripheral(); + setProcessModel( + getProcessModel().withCommAdapterEnabled(false) + .withState(PeripheralInformation.State.UNKNOWN) + ); + sendProcessModelChangedEvent(PeripheralProcessModel.Attribute.COMM_ADAPTER_ENABLED); + sendProcessModelChangedEvent(PeripheralProcessModel.Attribute.STATE); + } + + @Override + public PeripheralProcessModel getProcessModel() { + return processModel; + } + + protected void setProcessModel(PeripheralProcessModel processModel) { + this.processModel = processModel; + } + + protected EventHandler getEventHandler() { + return eventHandler; + } + + protected void sendProcessModelChangedEvent(PeripheralProcessModel.Attribute attributeChanged) { + eventHandler.onEvent( + new PeripheralProcessModelEvent( + processModel.getLocation(), + attributeChanged.name(), + processModel + ) + ); + } + + /** + * Initiates a communication channel to the peripheral device. + * This method should not block, i.e. it should not wait for the actual connection to be + * established, as the peripheral device could be temporarily absent or not responding at all. + * If that's the case, the communication adapter should continue trying to establish a connection + * until successful or until {@link #disconnectPeripheral()} is called. + */ + protected abstract void connectPeripheral(); + + /** + * Closes the communication channel to the peripheral device. + */ + protected abstract void disconnectPeripheral(); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralAdapterCommand.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralAdapterCommand.java new file mode 100644 index 0000000..40e9d17 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralAdapterCommand.java @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.peripherals; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; + +/** + * A command a peripheral communication adapter may execute. + */ +public interface PeripheralAdapterCommand + extends + Serializable { + + /** + * Executes the command. + * + * @param adapter The communication adapter to execute the command with. + */ + void execute( + @Nonnull + PeripheralCommAdapter adapter + ); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralCommAdapter.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralCommAdapter.java new file mode 100644 index 0000000..d4fbf18 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralCommAdapter.java @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.peripherals; + +import jakarta.annotation.Nonnull; +import org.opentcs.components.Lifecycle; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.util.ExplainedBoolean; + +/** + * This interface declares the methods that a driver communicating with and controlling a + * peripheral device must implement. + */ +public interface PeripheralCommAdapter + extends + Lifecycle { + + /** + * Enables this comm adapter, i.e. turns it on. + */ + void enable(); + + /** + * Disables this comm adapter, i.e. turns it off. + */ + void disable(); + + /** + * Checks whether this communication adapter is enabled. + * + * @return {@code true} if, and only if, this communication adapter is enabled. + */ + boolean isEnabled(); + + /** + * Returns a model of the peripheral device's and its communication adapter's attributes. + * + * @return A model of the peripheral device's and its communication adapter's attributes. + */ + @Nonnull + PeripheralProcessModel getProcessModel(); + + /** + * Checks if the peripheral device would be able to process the given job, taking into account + * its current state. + * + * @param job A job that might have to be processed. + * @return An {@link ExplainedBoolean} telling if the peripheral device would be able to process + * the job. + */ + @Nonnull + ExplainedBoolean canProcess( + @Nonnull + PeripheralJob job + ); + + /** + * Processes the given job by sending it or a representation that the peripheral device + * understands to the peripheral device itself. + * + * @param job The job to process. + * @param callback The callback to use to report back about the successful or failed completion of + * the job. + */ + void process( + @Nonnull + PeripheralJob job, + @Nonnull + PeripheralJobCallback callback + ); + + /** + * Aborts the current job, if any. + *

+ * Whether a job can actually be aborted depends on the actual peripheral/job semantics. + * The callback for the current job may still be called to indicate the job has failed, but it is + * not strictly expected to. + * The kernel will ignore calls to the callback after calling this method. + *

+ */ + void abortJob(); + + /** + * Executes the given {@link PeripheralAdapterCommand}. + * + * @param command The command to execute. + */ + void execute( + @Nonnull + PeripheralAdapterCommand command + ); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralCommAdapterDescription.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralCommAdapterDescription.java new file mode 100644 index 0000000..e10e2c5 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralCommAdapterDescription.java @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.peripherals; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; + +/** + * Provides the description for a peripheral communication adapter. + */ +public abstract class PeripheralCommAdapterDescription + implements + Serializable { + + /** + * Returns the description for a peripheral communication adapter. + * + * @return The description for a peripheral communication adapter. + */ + @Nonnull + public abstract String getDescription(); + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof PeripheralCommAdapterDescription)) { + return false; + } + + return getDescription().equals(((PeripheralCommAdapterDescription) obj).getDescription()); + } + + @Override + public int hashCode() { + return getDescription().hashCode(); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralCommAdapterFactory.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralCommAdapterFactory.java new file mode 100644 index 0000000..5858cb2 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralCommAdapterFactory.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.peripherals; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.opentcs.components.Lifecycle; +import org.opentcs.data.model.Location; + +/** + * Provides communication adapter instances for peripheral devices to be controlled. + */ +public interface PeripheralCommAdapterFactory + extends + Lifecycle { + + /** + * Returns a {@link PeripheralCommAdapterDescription} for the factory/the adapters provided. + * + * @return A {@link PeripheralCommAdapterDescription} for the factory/the adapters provided. + */ + @Nonnull + PeripheralCommAdapterDescription getDescription(); + + /** + * Checks whether this factory can provide a communication adapter for the given + * location/peripheral device. + * + * @param location The location to check for. + * @return {@code true} if, and only if, this factory can provide a communication adapter to + * control the given location/peripheral device. + */ + boolean providesAdapterFor( + @Nonnull + Location location + ); + + /** + * Returns a communication adapter for controlling the given location/peripheral device. + * + * @param location The location/peripheral device to be controlled. + * @return A communication adapter for controlling the given location/peripheral device, or + * {@code null}, if this factory cannot provide an adapter for it. + */ + @Nullable + PeripheralCommAdapter getAdapterFor( + @Nonnull + Location location + ); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralController.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralController.java new file mode 100644 index 0000000..ba9c60d --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralController.java @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.peripherals; + +import jakarta.annotation.Nonnull; +import org.opentcs.components.Lifecycle; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.util.ExplainedBoolean; + +/** + * Provides high-level methods for the system to control a peripheral device. + */ +public interface PeripheralController + extends + Lifecycle { + + /** + * Lets the peripheral device associated with this controller process the given job. + * The callback is used to inform about the successful or failed completion of the job. + * + * @param job The job to process. + * @param callback The callback to use. + * @throws IllegalStateException If this peripheral device associated with this controller + * cannot process the job. + */ + void process( + @Nonnull + PeripheralJob job, + @Nonnull + PeripheralJobCallback callback + ) + throws IllegalStateException; + + /** + * Aborts the current job, if any. + *

+ * Whether a job can actually be aborted depends on the actual peripheral/job semantics. + * The callback for the current job may still be called to indicate the job has failed, but it is + * not strictly expected to. + * The kernel will ignore calls to the callback after calling this method. + *

+ */ + void abortJob(); + + /** + * Checks if the peripheral device would be able to process the given job, taking into account + * its current state. + * + * @param job A job that might have to be processed. + * @return An {@link ExplainedBoolean} telling if the peripheral device would be able to process + * the job. + */ + @Nonnull + ExplainedBoolean canProcess( + @Nonnull + PeripheralJob job + ); + + /** + * Sends a {@link PeripheralAdapterCommand} to the communication adapter. + * + * @param command The adapter command to be sent. + */ + void sendCommAdapterCommand( + @Nonnull + PeripheralAdapterCommand command + ); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralControllerPool.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralControllerPool.java new file mode 100644 index 0000000..23fe24d --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralControllerPool.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.peripherals; + +import jakarta.annotation.Nonnull; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; + +/** + * Maintains associations between locations and peripheral controllers. + */ +public interface PeripheralControllerPool { + + /** + * Returns the peripheral controller associated with the given location. + * + * @param location The reference to the location. + * @return The peripheral controller associated with the given location. + * @throws IllegalArgumentException If no peripheral controller is associated with the given + * location or if the referenced location does not exist. + */ + @Nonnull + PeripheralController getPeripheralController(TCSResourceReference location) + throws IllegalArgumentException; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralJobCallback.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralJobCallback.java new file mode 100644 index 0000000..0a667da --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralJobCallback.java @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.peripherals; + +import jakarta.annotation.Nonnull; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.peripherals.PeripheralJob; + +/** + * A callback used to inform about the successful or failed completion of jobs. + */ +public interface PeripheralJobCallback { + + /** + * Called on successful completion of a job. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param ref A reference to the peripheral job that was successfully completed. + */ + void peripheralJobFinished( + @Nonnull + TCSObjectReference ref + ); + + /** + * Called on failed completion of a job. + *

+ * This method is supposed to be called only from the kernel executor thread. + *

+ * + * @param ref A reference to the peripheral job whose completion has failed. + */ + void peripheralJobFailed( + @Nonnull + TCSObjectReference ref + ); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralProcessModel.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralProcessModel.java new file mode 100644 index 0000000..882f5d4 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/PeripheralProcessModel.java @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.peripherals; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.PeripheralInformation; +import org.opentcs.data.model.TCSResourceReference; + +/** + * A model of a peripheral device's and its communication adapter's attributes. + */ +public class PeripheralProcessModel + implements + Serializable { + + /** + * The reference to the location that is attached to this model. + */ + private final TCSResourceReference location; + /** + * Whether the communication adapter is currently enabled. + */ + private final boolean commAdapterEnabled; + /** + * Whether the communication adapter is currently connected to the peripheral device. + */ + private final boolean commAdapterConnected; + /** + * The peripheral device's current state. + */ + private final PeripheralInformation.State state; + + /** + * Creates a new instance. + * + * @param location The reference to the location that is attached to this model. + */ + public PeripheralProcessModel(TCSResourceReference location) { + this(location, false, false, PeripheralInformation.State.NO_PERIPHERAL); + } + + protected PeripheralProcessModel( + @Nonnull + TCSResourceReference location, + boolean commAdapterEnabled, + boolean commAdapterConnected, + @Nonnull + PeripheralInformation.State state + ) { + this.location = requireNonNull(location, "location"); + this.commAdapterEnabled = commAdapterEnabled; + this.commAdapterConnected = commAdapterConnected; + this.state = requireNonNull(state, "state"); + } + + /** + * Returns the reference to the location that is attached to this model. + * + * @return The reference to the location that is attached to this model. + */ + @Nonnull + public TCSResourceReference getLocation() { + return location; + } + + /** + * Creates a copy of the object, with the given location reference. + * + * @param location The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralProcessModel withLocation( + @Nonnull + TCSResourceReference location + ) { + return new PeripheralProcessModel(location, commAdapterEnabled, commAdapterConnected, state); + } + + /** + * Returns whether the communication adapter is currently enabled. + * + * @return Whether the communication adapter is currently enabled. + */ + public boolean isCommAdapterEnabled() { + return commAdapterEnabled; + } + + /** + * Creates a copy of the object, with the given enabled state. + * + * @param commAdapterEnabled The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralProcessModel withCommAdapterEnabled(boolean commAdapterEnabled) { + return new PeripheralProcessModel(location, commAdapterEnabled, commAdapterConnected, state); + } + + /** + * Returns whether the communication adapter is currently connected to the peripheral device. + * + * @return Whether the communication adapter is currently connected to the peripheral device. + */ + public boolean isCommAdapterConnected() { + return commAdapterConnected; + } + + /** + * Creates a copy of the object, with the given connected state. + * + * @param commAdapterConnected The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralProcessModel withCommAdapterConnected(boolean commAdapterConnected) { + return new PeripheralProcessModel(location, commAdapterEnabled, commAdapterConnected, state); + } + + /** + * Returns the peripheral device's current state. + * + * @return The peripheral device's current state. + */ + public PeripheralInformation.State getState() { + return state; + } + + /** + * Creates a copy of the object, with the given state. + * + * @param state The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public PeripheralProcessModel withState(PeripheralInformation.State state) { + return new PeripheralProcessModel(location, commAdapterEnabled, commAdapterConnected, state); + } + + /** + * Used to describe what has changed in a process model. + */ + public enum Attribute { + /** + * Indicates a change of the location property. + */ + LOCATION, + /** + * Indicates a change of the comm adapter enabled property. + */ + COMM_ADAPTER_ENABLED, + /** + * Indicates a change of the comm adapter connected property. + */ + COMM_ADAPTER_CONNECTED, + /** + * Indicates a change of the state property. + */ + STATE; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/PeripheralAttachmentEvent.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/PeripheralAttachmentEvent.java new file mode 100644 index 0000000..8d1bb7f --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/PeripheralAttachmentEvent.java @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.peripherals.management; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; + +/** + * Instances of this class represent events emitted by/for attaching comm adapters. + */ +public class PeripheralAttachmentEvent + extends + PeripheralCommAdapterEvent + implements + Serializable { + + /** + * The location a peripheral comm adapter has been attached to. + */ + private final TCSResourceReference location; + /** + * The information to the actual attachment. + */ + private final PeripheralAttachmentInformation attachmentInformation; + + /** + * Creates a new instance. + * + * @param location The location a comm adapter has been attached to. + * @param attachmentInformation The information to the actual attachment. + */ + public PeripheralAttachmentEvent( + @Nonnull + TCSResourceReference location, + @Nonnull + PeripheralAttachmentInformation attachmentInformation + ) { + this.location = requireNonNull(location, "location"); + this.attachmentInformation = requireNonNull(attachmentInformation, "attachmentInformation"); + } + + /** + * Returns the location a comm adapter has been attached to. + * + * @return The location a comm adapter has been attached to. + */ + public TCSResourceReference getLocation() { + return location; + } + + /** + * Returns the information to the actual attachment. + * + * @return The information to the actual attachment. + */ + public PeripheralAttachmentInformation getAttachmentInformation() { + return attachmentInformation; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/PeripheralAttachmentInformation.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/PeripheralAttachmentInformation.java new file mode 100644 index 0000000..870affc --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/PeripheralAttachmentInformation.java @@ -0,0 +1,143 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.peripherals.management; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import java.util.List; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; + +/** + * Describes which communication adapter a location is currently associated with. + */ +public class PeripheralAttachmentInformation + implements + Serializable { + + /** + * The location this attachment information belongs to. + */ + private final TCSResourceReference locationReference; + /** + * The list of comm adapters available to be attached to the referenced location. + */ + private final List availableCommAdapters; + /** + * The comm adapter attached to the referenced location. + */ + private final PeripheralCommAdapterDescription attachedCommAdapter; + + /** + * Creates a new instance. + * + * @param locationReference The location this attachment information belongs to. + * @param availableCommAdapters The list of comm adapters available to be attached to the + * referenced location. + * @param attachedCommAdapter The comm adapter attached to the referenced location. + */ + public PeripheralAttachmentInformation( + @Nonnull + TCSResourceReference locationReference, + @Nonnull + List availableCommAdapters, + @Nonnull + PeripheralCommAdapterDescription attachedCommAdapter + ) { + this.locationReference = requireNonNull(locationReference, "locationReference"); + this.availableCommAdapters = requireNonNull(availableCommAdapters, "availableCommAdapters"); + this.attachedCommAdapter = requireNonNull(attachedCommAdapter, "attachedCommAdapter"); + } + + /** + * Returns the location this attachment information belongs to. + * + * @return The location this attachment information belongs to. + */ + @Nonnull + public TCSResourceReference getLocationReference() { + return locationReference; + } + + /** + * Creates a copy of this object with the given location reference. + * + * @param locationReference The new location reference. + * @return A copy of this object, differing in the given location reference. + */ + public PeripheralAttachmentInformation withLocationReference( + TCSResourceReference locationReference + ) { + return new PeripheralAttachmentInformation( + locationReference, + availableCommAdapters, + attachedCommAdapter + ); + } + + /** + * Returns the list of comm adapters available to be attached to the referenced location. + * + * @return The list of comm adapters available to be attached to the referenced location. + */ + @Nonnull + public List getAvailableCommAdapters() { + return availableCommAdapters; + } + + /** + * Creates a copy of this object with the given available comm adapters. + * + * @param availableCommAdapters The new available comm adapters. + * @return A copy of this object, differing in the given available comm adapters. + */ + public PeripheralAttachmentInformation withAvailableCommAdapters( + @Nonnull + List availableCommAdapters + ) { + return new PeripheralAttachmentInformation( + locationReference, + availableCommAdapters, + attachedCommAdapter + ); + } + + /** + * Returns the comm adapter attached to the referenced location. + * + * @return The comm adapter attached to the referenced location. + */ + @Nonnull + public PeripheralCommAdapterDescription getAttachedCommAdapter() { + return attachedCommAdapter; + } + + /** + * Creates a copy of this object with the given attached comm adapter. + * + * @param attachedCommAdapter The new attached comm adapter. + * @return A copy of this object, differing in the given attached comm adapter. + */ + public PeripheralAttachmentInformation withAttachedCommAdapter( + @Nonnull + PeripheralCommAdapterDescription attachedCommAdapter + ) { + return new PeripheralAttachmentInformation( + locationReference, + availableCommAdapters, + attachedCommAdapter + ); + } + + @Override + public String toString() { + return "PeripheralAttachmentInformation{" + + "locationReference=" + locationReference + + ", availableCommAdapters=" + availableCommAdapters + + ", attachedCommAdapter=" + attachedCommAdapter + + '}'; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/PeripheralCommAdapterEvent.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/PeripheralCommAdapterEvent.java new file mode 100644 index 0000000..cf743f2 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/PeripheralCommAdapterEvent.java @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.peripherals.management; + +import java.io.Serializable; +import org.opentcs.drivers.LowLevelCommunicationEvent; + +/** + * Instances of this class represent events emitted by/for peripheral comm adapter changes. + */ +public abstract class PeripheralCommAdapterEvent + implements + LowLevelCommunicationEvent, + Serializable { + + /** + * Creates an empty event. + */ + public PeripheralCommAdapterEvent() { + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/PeripheralCommAdapterPanel.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/PeripheralCommAdapterPanel.java new file mode 100644 index 0000000..b275789 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/PeripheralCommAdapterPanel.java @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.peripherals.management; + +import javax.swing.JPanel; +import org.opentcs.drivers.peripherals.PeripheralProcessModel; + +/** + * A base class for panels associated with peripheral comm adapters. + */ +public abstract class PeripheralCommAdapterPanel + extends + JPanel { + + /** + * Returns the title for this comm adapter panel. + * The default implementation returns the accessible name from the panel's accessible context. + * + * @return The title for this comm adapter panel. + */ + public String getTitle() { + return getAccessibleContext().getAccessibleName(); + } + + /** + * Notifies a comm adapter panel that the corresponding process model changed. + * The comm adapter panel may want to update the content its representing. + * + * @param processModel The new process model. + */ + public abstract void processModelChanged(PeripheralProcessModel processModel); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/PeripheralCommAdapterPanelFactory.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/PeripheralCommAdapterPanelFactory.java new file mode 100644 index 0000000..87ddb06 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/PeripheralCommAdapterPanelFactory.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.peripherals.management; + +import jakarta.annotation.Nonnull; +import java.util.List; +import org.opentcs.components.Lifecycle; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; +import org.opentcs.drivers.peripherals.PeripheralProcessModel; + +/** + * Provides peripheral comm adapter specific panels used for interaction and displaying information. + */ +public interface PeripheralCommAdapterPanelFactory + extends + Lifecycle { + + /** + * Returns a list of {@link PeripheralCommAdapterPanel}s. + * + * @param description The description to create panels for. + * @param location The location to create panels for. + * @param processModel The current state of the process model a panel may want to initialize its + * components with. + * @return A list of comm adapter panels, or an empty list, if this factory cannot provide panels + * for the given description. + */ + List getPanelsFor( + @Nonnull + PeripheralCommAdapterDescription description, + @Nonnull + TCSResourceReference location, + @Nonnull + PeripheralProcessModel processModel + ); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/PeripheralProcessModelEvent.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/PeripheralProcessModelEvent.java new file mode 100644 index 0000000..af6cfa3 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/PeripheralProcessModelEvent.java @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.peripherals.management; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.drivers.peripherals.PeripheralProcessModel; + +/** + * Instances of this class represent events emitted by/for changes on + * {@link PeripheralProcessModel}s. + */ +public class PeripheralProcessModelEvent + extends + PeripheralCommAdapterEvent + implements + Serializable { + + /** + * The location assiciated with the peripheral device. + */ + private final TCSResourceReference location; + /** + * The name of the attribute that has changed in the process model. + */ + private final String attributeChanged; + /** + * The process model with its current/changed state. + */ + private final PeripheralProcessModel processModel; + + /** + * Creates a new instance. + * + * @param location The location assiciated with the peripheral device. + * @param attributeChanged The name of the attribute that has changed in the process model. + * @param processModel The process model with its current/changed state. + */ + public PeripheralProcessModelEvent( + @Nonnull + TCSResourceReference location, + @Nonnull + String attributeChanged, + @Nonnull + PeripheralProcessModel processModel + ) { + this.location = requireNonNull(location, "location"); + this.attributeChanged = requireNonNull(attributeChanged, "attributeChanged"); + this.processModel = requireNonNull(processModel, "processModel"); + } + + /** + * Returns the location assiciated with the peripheral device. + * + * @return The location. + */ + public TCSResourceReference getLocation() { + return location; + } + + /** + * Returns the name of the attribute that has changed in the process model. + * + * @return The name of the attribute that has changed in the process model. + */ + public String getAttributeChanged() { + return attributeChanged; + } + + /** + * Returns the process model with its current/changed state. + * + * @return The process model with its current/changed state. + */ + public PeripheralProcessModel getProcessModel() { + return processModel; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/package-info.java new file mode 100644 index 0000000..727fb2e --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/management/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Components needed for processing information related to peripheral comm adapters. + */ +package org.opentcs.drivers.peripherals.management; diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/package-info.java new file mode 100644 index 0000000..72e91d3 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/peripherals/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Components needed for controlling peripheral devices and processing information sent by them. + */ +package org.opentcs.drivers.peripherals; diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/AdapterCommand.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/AdapterCommand.java new file mode 100644 index 0000000..0db7ed9 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/AdapterCommand.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle; + +import java.io.Serializable; + +/** + * A command a comm adapter may execute. + */ +public interface AdapterCommand + extends + Serializable { + + /** + * Executes the command. + * + * @param adapter The comm adapter to execute the command with. + */ + void execute(VehicleCommAdapter adapter); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/BasicVehicleCommAdapter.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/BasicVehicleCommAdapter.java new file mode 100644 index 0000000..f3d74ea --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/BasicVehicleCommAdapter.java @@ -0,0 +1,404 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.drivers.vehicle.VehicleProcessModel.Attribute.COMMAND_ENQUEUED; +import static org.opentcs.drivers.vehicle.VehicleProcessModel.Attribute.COMMAND_EXECUTED; +import static org.opentcs.util.Assertions.checkInRange; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.management.VehicleProcessModelTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A base class for communication adapters mainly providing command queue processing. + * + *

+ * Implementation notes: + *

+ *
    + *
  • Accessing the queues of {@link #getUnsentCommands() unsent} and + * {@link #getSentCommands() sent} commands from outside should always be protected by + * synchronization on the {@link BasicVehicleCommAdapter} instance.
  • + *
+ */ +public abstract class BasicVehicleCommAdapter + implements + VehicleCommAdapter, + PropertyChangeListener { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(BasicVehicleCommAdapter.class); + /** + * An observable model of the vehicle's and its comm adapter's attributes. + */ + private final VehicleProcessModel vehicleModel; + /** + * The number of commands this comm adapter accepts. Must be at least 1. + */ + private final int commandsCapacity; + /** + * The string to recognize as a recharge operation. + */ + private final String rechargeOperation; + /** + * The executor to run tasks on. + */ + private final ScheduledExecutorService executor; + /** + * Indicates whether this adapter is initialized. + */ + private boolean initialized; + /** + * This adapter's enabled flag. + */ + private boolean enabled; + /** + * This adapter's current command dispatcher task. + */ + private final Runnable commandDispatcherTask = new CommandDispatcherTask(); + /** + * This adapter's command queue. + */ + private final Queue commandQueue = new LinkedBlockingQueue<>(); + /** + * Contains the orders which have been sent to the vehicle but which haven't + * been executed by it, yet. + */ + private final Queue sentQueue = new LinkedBlockingQueue<>(); + + /** + * Creates a new instance. + * + * @param vehicleModel An observable model of the vehicle's and its comm adapter's attributes. + * @param commandsCapacity The number of commands this comm adapter accepts. Must be at least 1. + * @param rechargeOperation The string to recognize as a recharge operation. + * @param executor The executor to run tasks on. + */ + public BasicVehicleCommAdapter( + VehicleProcessModel vehicleModel, + int commandsCapacity, + String rechargeOperation, + ScheduledExecutorService executor + ) { + this.vehicleModel = requireNonNull(vehicleModel, "vehicleModel"); + this.commandsCapacity = checkInRange( + commandsCapacity, + 1, + Integer.MAX_VALUE, + "commandsCapacity" + ); + this.rechargeOperation = requireNonNull(rechargeOperation, "rechargeOperation"); + this.executor = requireNonNull(executor, "executor"); + } + + /** + * {@inheritDoc} + *

+ * Overriding methods are expected to call this implementation, too. + *

+ */ + @Override + public void initialize() { + if (initialized) { + LOG.debug("{}: Already initialized.", getName()); + return; + } + + getProcessModel().addPropertyChangeListener(this); + this.initialized = true; + } + + /** + * {@inheritDoc} + *

+ * Overriding methods are expected to call this implementation, too. + *

+ */ + @Override + public void terminate() { + if (!initialized) { + LOG.debug("{}: Not initialized.", getName()); + return; + } + + getProcessModel().removePropertyChangeListener(this); + this.initialized = false; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + /** + * {@inheritDoc} + *

+ * Overriding methods are expected to call this implementation, too. + *

+ */ + @Override + public synchronized void enable() { + if (isEnabled()) { + return; + } + LOG.info("Vehicle comm adapter is being enabled: {}", getName()); + connectVehicle(); + enabled = true; + getProcessModel().setCommAdapterEnabled(true); + } + + /** + * {@inheritDoc} + *

+ * Overriding methods are expected to call this implementation, too. + *

+ */ + @Override + public synchronized void disable() { + if (!isEnabled()) { + return; + } + LOG.info("Vehicle comm adapter is being disabled: {}", getName()); + disconnectVehicle(); + enabled = false; + // Update the vehicle's state for the rest of the system. + getProcessModel().setCommAdapterEnabled(false); + getProcessModel().setState(Vehicle.State.UNKNOWN); + } + + @Override + public synchronized boolean isEnabled() { + return enabled; + } + + @Override + public VehicleProcessModel getProcessModel() { + return vehicleModel; + } + + @Override + public VehicleProcessModelTO createTransferableProcessModel() { + return createCustomTransferableProcessModel() + .setName(getProcessModel().getName()) + .setCommAdapterConnected(getProcessModel().isCommAdapterConnected()) + .setCommAdapterEnabled(getProcessModel().isCommAdapterEnabled()) + .setEnergyLevel(getProcessModel().getEnergyLevel()) + .setLoadHandlingDevices(getProcessModel().getLoadHandlingDevices()) + .setNotifications(getProcessModel().getNotifications()) + .setPose(getProcessModel().getPose()) + .setPosition(getProcessModel().getPosition()) + .setState(getProcessModel().getState()) + .setBoundingBox(getProcessModel().getBoundingBox()); + } + + @Override + public synchronized Queue getUnsentCommands() { + return commandQueue; + } + + @Override + public synchronized Queue getSentCommands() { + return sentQueue; + } + + @Override + public int getCommandsCapacity() { + return commandsCapacity; + } + + @Override + public boolean canAcceptNextCommand() { + return (getUnsentCommands().size() + getSentCommands().size()) < getCommandsCapacity(); + } + + @Override + public String getRechargeOperation() { + return rechargeOperation; + } + + @Override + public synchronized boolean enqueueCommand(MovementCommand newCommand) { + requireNonNull(newCommand, "newCommand"); + + if (!canAcceptNextCommand()) { + return false; + } + + LOG.debug("{}: Adding command: {}", getName(), newCommand); + getUnsentCommands().add(newCommand); + getProcessModel().commandEnqueued(newCommand); + return true; + } + + @Override + public synchronized void clearCommandQueue() { + getUnsentCommands().clear(); + getSentCommands().clear(); + } + + @Override + public void execute(AdapterCommand command) { + command.execute(this); + } + + /** + * Processes updates of the {@link VehicleProcessModel}. + * + *

+ * Overriding methods should also call this. + *

+ * + * @param evt The property change event published by the model. + */ + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (Objects.equals(evt.getPropertyName(), COMMAND_ENQUEUED.name()) + || Objects.equals(evt.getPropertyName(), COMMAND_EXECUTED.name())) { + executor.execute(commandDispatcherTask); + } + } + + /** + * Returns this communication adapter's name. + * + * @return This communication adapter's name. + */ + public String getName() { + return getProcessModel().getName(); + } + + /** + * Returns the executor to run tasks on. + * + * @return The executor to run tasks on. + */ + public ScheduledExecutorService getExecutor() { + return executor; + } + + /** + * Converts the given command to something the vehicle can understand and sends the resulting data + * to the vehicle. + *

+ * Note that this method is called from the kernel executor and thus should not block. + *

+ * + * @param cmd The command to be sent. + * @throws IllegalArgumentException If there was a problem with interpreting the command or + * communicating it to the vehicle. + */ + public abstract void sendCommand(MovementCommand cmd) + throws IllegalArgumentException; + + /** + * Checks whether a new command can be sent to the vehicle. + *

+ * This method returns true only if there is at least one command in the + * {@link #getUnsentCommands() queue of unsent commands} waiting to be sent. + *

+ * + * @return true if, and only if, a new command can be sent to the vehicle. + */ + protected synchronized boolean canSendNextCommand() { + // Since canAcceptNextCommand() already ensures that the combined sizes of the queues of unsent + // and sent commands don't exceeed the number of commands the comm adapters can accept, the only + // thing to do here is to check if there are any commands to be sent. + return !getUnsentCommands().isEmpty(); + } + + // Abstract methods start here. + /** + * Initiates a communication channel to the vehicle. + * This method should not block, i.e. it should not wait for the actual + * connection to be established, as the vehicle could be temporarily absent + * or not responding at all. If that's the case, the communication adapter + * should continue trying to establish a connection until successful or until + * disconnectVehicle is called. + */ + protected abstract void connectVehicle(); + + /** + * Closes the communication channel to the vehicle. + */ + protected abstract void disconnectVehicle(); + + /** + * Checks whether the communication channel to the vehicle is open. + *

+ * Note that the return value of this method does not indicate + * whether communication with the vehicle is currently alive and/or if the + * vehicle is considered to be working/responding correctly. + *

+ * + * @return true if, and only if, the communication channel to the + * vehicle is open. + */ + protected abstract boolean isVehicleConnected(); + + /** + * Creates a transferable process model with the specific attributes of this comm adapter's + * process model set. + *

+ * This method should be overriden by implementing classes. + *

+ * + * @return A transferable process model. + */ + protected VehicleProcessModelTO createCustomTransferableProcessModel() { + return new VehicleProcessModelTO(); + } + + /** + * The task processing the command queue. + */ + private class CommandDispatcherTask + implements + Runnable { + + CommandDispatcherTask() { + } + + @Override + public void run() { + synchronized (BasicVehicleCommAdapter.this) { + if (!isEnabled()) { + LOG.debug("{}: Not enabled, skipping.", getName()); + return; + } + if (!canSendNextCommand()) { + LOG.debug("{}: Cannot send another command, skipping.", getName()); + return; + } + MovementCommand curCmd = getUnsentCommands().poll(); + if (curCmd == null) { + LOG.debug("{}: Nothing to send, skipping.", getName()); + return; + } + try { + LOG.debug("{}: Sending command: {}", getName(), curCmd); + sendCommand(curCmd); + // Remember that we sent this command to the vehicle. + getSentCommands().add(curCmd); + // Notify listeners that this command was sent. + getProcessModel().commandSent(curCmd); + } + catch (IllegalArgumentException exc) { + // Notify listeners that this command failed. + LOG.warn("{}: Failed sending command {}", getName(), curCmd, exc); + getProcessModel().commandFailed(curCmd); + } + } + } + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/DefaultVehicleCommAdapterDescription.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/DefaultVehicleCommAdapterDescription.java new file mode 100644 index 0000000..69b8775 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/DefaultVehicleCommAdapterDescription.java @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; + +/** + * The default implementation of {@link VehicleCommAdapterDescription}. + */ +public class DefaultVehicleCommAdapterDescription + extends + VehicleCommAdapterDescription { + + /** + * The description. + */ + private final String description; + /** + * Whether the comm adapter is a simulating one. + */ + private final boolean isSimVehicleCommAdapter; + + /** + * Creates a new instance. + * + * @param description The description. + * @param isSimVehicleCommAdapter Whether the comm adapter is a simulating one. + */ + public DefaultVehicleCommAdapterDescription( + @Nonnull + String description, + boolean isSimVehicleCommAdapter + ) { + this.description = requireNonNull(description, "description"); + this.isSimVehicleCommAdapter = isSimVehicleCommAdapter; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public boolean isSimVehicleCommAdapter() { + return isSimVehicleCommAdapter; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/IncomingPoseTransformer.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/IncomingPoseTransformer.java new file mode 100644 index 0000000..eda9636 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/IncomingPoseTransformer.java @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle; + +import java.util.function.UnaryOperator; +import org.opentcs.data.model.Pose; + +/** + * Transforms a {@link Pose} received by a vehicle to one in the plant model coordinate system. + */ +public interface IncomingPoseTransformer + extends + UnaryOperator { + + @Override + Pose apply(Pose pose); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/LoadHandlingDevice.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/LoadHandlingDevice.java new file mode 100644 index 0000000..f26efe6 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/LoadHandlingDevice.java @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle; + +import static java.util.Objects.requireNonNull; + +import java.io.Serializable; +import java.util.Objects; + +/** + * Describes a single load handling device on a vehicle. + */ +public class LoadHandlingDevice + implements + Serializable { + + /** + * A name/label for this device. + */ + private final String label; + /** + * A flag indicating whether this device is filled to its maximum capacity or + * not. + */ + private final boolean full; + + /** + * Creates a new LoadHandlingDevice. + * + * @param label The device's name/label. + * @param full A flag indicating whether this device is filled to its maximum + * capacity or not. + */ + public LoadHandlingDevice(String label, boolean full) { + this.label = requireNonNull(label, "label"); + this.full = full; + } + + /** + * Returns this load handling device's name/label. + * + * @return This load handling device's name/label. + */ + public String getLabel() { + return label; + } + + /** + * Returns a flag indicating whether this device is filled to its maximum + * capacity or not. + * + * @return A flag indicating whether this device is filled to its maximum + * capacity or not. + */ + public boolean isFull() { + return full; + } + + @Override + public String toString() { + return "LoadHandlingDevice{" + "label=" + label + ", full=" + full + '}'; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof LoadHandlingDevice)) { + return false; + } + final LoadHandlingDevice other = (LoadHandlingDevice) obj; + if (!Objects.equals(this.label, other.label)) { + return false; + } + if (this.full != other.full) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 23 * hash + Objects.hashCode(this.label); + hash = 23 * hash + (this.full ? 1 : 0); + return hash; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/MovementCommand.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/MovementCommand.java new file mode 100644 index 0000000..bab650e --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/MovementCommand.java @@ -0,0 +1,545 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.Map; +import java.util.Objects; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Point; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; + +/** + * A command for moving a step. + */ +public class MovementCommand { + + /** + * A constant indicating there is no operation to be executed after moving. + */ + public static final String NO_OPERATION = DriveOrder.Destination.OP_NOP; + /** + * A constant indicating the vehicle should basically just move to a point without a location + * associated to it. + */ + public static final String MOVE_OPERATION = DriveOrder.Destination.OP_MOVE; + /** + * A constant for parking the vehicle. (Again, basically doing nothing at the destination.) + */ + public static final String PARK_OPERATION = DriveOrder.Destination.OP_PARK; + /** + * The transport order this movement belongs to. + */ + private final TransportOrder transportOrder; + /** + * The drive order this movement belongs to. + */ + private final DriveOrder driveOrder; + /** + * The step describing the movement. + */ + private final Route.Step step; + /** + * The operation to be executed after moving. + */ + private final String operation; + /** + * The location at which the operation is to be executed. + * May be null if the movement command's operation is considred an empty + * operation (i.e. is {@link #NO_OPERATION}, {@link #MOVE_OPERATION} or {@link #PARK_OPERATION}). + */ + private final Location opLocation; + /** + * Indicates whether this movement is the final one for the drive order it belongs to. + */ + private final boolean finalMovement; + /** + * The destination position of the whole drive order. + */ + private final Point finalDestination; + /** + * The destination location of the whole drive order. + */ + private final Location finalDestinationLocation; + /** + * The operation to be executed at the destination position. + */ + private final String finalOperation; + /** + * Properties of the order this command is part of. + */ + private final Map properties; + + /** + * Creates a new instance. + * + * @param transportOrder The transport order this movement belongs to. + * @param driveOrder The drive order this movement belongs to. + * @param step The step describing the movement. + * @param operation The operation to be executed after moving. + * @param opLocation The location at which the operation is to be executed. + * May be null if the movement command's operation is considred an empty + * operation (i.e. is {@link #NO_OPERATION}, {@link #MOVE_OPERATION} or {@link #PARK_OPERATION}). + * @param finalMovement Indicates whether this movement is the final one in the drive order it + * belongs to. + * @param finalDestinationLocation The destination location of the whole drive order. + * @param finalDestination The destination position of the whole drive order. + * @param finalOperation The operation to be executed at the destination position. + * @param properties Properties of the order this command is part of. + */ + public MovementCommand( + @Nonnull + TransportOrder transportOrder, + @Nonnull + DriveOrder driveOrder, + @Nonnull + Route.Step step, + @Nonnull + String operation, + @Nullable + Location opLocation, + boolean finalMovement, + @Nullable + Location finalDestinationLocation, + @Nonnull + Point finalDestination, + @Nonnull + String finalOperation, + @Nonnull + Map properties + ) { + this.transportOrder = requireNonNull(transportOrder, "transportOrder"); + this.driveOrder = requireNonNull(driveOrder, "driveOrder"); + this.step = requireNonNull(step, "step"); + this.operation = requireNonNull(operation, "operation"); + this.finalMovement = finalMovement; + this.finalDestinationLocation = finalDestinationLocation; + this.finalDestination = requireNonNull(finalDestination, "finalDestination"); + this.finalOperation = requireNonNull(finalOperation, "finalOperation"); + this.properties = requireNonNull(properties, "properties"); + if (opLocation == null && !isEmptyOperation(operation)) { + throw new NullPointerException("opLocation is null while operation is not considered empty"); + } + this.opLocation = opLocation; + } + + /** + * Returns the transport order this movement belongs to. + * + * @return The transport order this movement belongs to. + */ + @Nonnull + public TransportOrder getTransportOrder() { + return transportOrder; + } + + /** + * Creates a copy of this object, with the given transport order. + * + * @param transportOrder The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public MovementCommand withTransportOrder( + @Nonnull + TransportOrder transportOrder + ) { + return new MovementCommand( + transportOrder, + driveOrder, + step, + operation, + opLocation, + finalMovement, + finalDestinationLocation, + finalDestination, + finalOperation, + properties + ); + } + + /** + * Returns the drive order this movement belongs to. + * + * @return The drive order this movement belongs to. + */ + @Nonnull + public DriveOrder getDriveOrder() { + return driveOrder; + } + + /** + * Creates a copy of this object, with the given drive order. + * + * @param driveOrder The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public MovementCommand withDriveOrder( + @Nonnull + DriveOrder driveOrder + ) { + return new MovementCommand( + transportOrder, + driveOrder, + step, + operation, + opLocation, + finalMovement, + finalDestinationLocation, + finalDestination, + finalOperation, + properties + ); + } + + /** + * Returns the step describing the movement. + * + * @return The step describing the movement. + */ + @Nonnull + public Route.Step getStep() { + return step; + } + + /** + * Creates a copy of this object, with the given step. + * + * @param step The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public MovementCommand withStep( + @Nonnull + Route.Step step + ) { + return new MovementCommand( + transportOrder, + driveOrder, + step, + operation, + opLocation, + finalMovement, + finalDestinationLocation, + finalDestination, + finalOperation, + properties + ); + } + + /** + * Returns the operation to be executed after moving. + * + * @return The operation to be executed after moving. + */ + @Nonnull + public String getOperation() { + return operation; + } + + /** + * Creates a copy of this object, with the given operation. + * + * @param operation The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public MovementCommand withOperation( + @Nonnull + String operation + ) { + return new MovementCommand( + transportOrder, + driveOrder, + step, + operation, + opLocation, + finalMovement, + finalDestinationLocation, + finalDestination, + finalOperation, + properties + ); + } + + /** + * Indicates whether an operation is to be executed in addition to moving or not. + * + * @return true if, and only if, no operation is to be executed. + */ + public boolean hasEmptyOperation() { + return isEmptyOperation(operation); + } + + /** + * Returns the location at which the operation is to be executed. + *

+ * May be null if the movement command's operation is considred an empty + * operation (i.e. is {@link #NO_OPERATION}, {@link #MOVE_OPERATION} or {@link #PARK_OPERATION}). + *

+ * + * @return The location at which the operation is to be executed. + */ + @Nullable + public Location getOpLocation() { + return opLocation; + } + + /** + * Creates a copy of this object, with the given location at which the operation is to be + * executed. + *

+ * May be null if the movement command's operation is considred an empty + * operation (i.e. is {@link #NO_OPERATION}, {@link #MOVE_OPERATION} or {@link #PARK_OPERATION}). + *

+ * + * @param opLocation The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public MovementCommand withOpLocation( + @Nullable + Location opLocation + ) { + return new MovementCommand( + transportOrder, + driveOrder, + step, + operation, + opLocation, + finalMovement, + finalDestinationLocation, + finalDestination, + finalOperation, + properties + ); + } + + /** + * Indicates whether this movement is the final one in the driver order it belongs to. + * + * @return true if, and only if, this movement is the final one. + */ + public boolean isFinalMovement() { + return finalMovement; + } + + /** + * Creates a copy of this object, with the given final movement flag. + * + * @param finalMovement The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public MovementCommand withFinalMovement(boolean finalMovement) { + return new MovementCommand( + transportOrder, + driveOrder, + step, + operation, + opLocation, + finalMovement, + finalDestinationLocation, + finalDestination, + finalOperation, + properties + ); + } + + /** + * Returns the final destination of the drive order this MovementCommand was created for. + * + * @return The final destination of the drive order this MovementCommand was created for. + */ + @Nonnull + public Point getFinalDestination() { + return finalDestination; + } + + /** + * Creates a copy of this object, with the given final destination. + * + * @param finalDestination The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public MovementCommand withFinalDestination( + @Nonnull + Point finalDestination + ) { + return new MovementCommand( + transportOrder, + driveOrder, + step, + operation, + opLocation, + finalMovement, + finalDestinationLocation, + finalDestination, + finalOperation, + properties + ); + } + + /** + * Returns the destination location of the whole drive order. + * + * @return The destination location of the whole drive order. + */ + @Nullable + public Location getFinalDestinationLocation() { + return finalDestinationLocation; + } + + /** + * Creates a copy of this object, with the given final destination location. + * + * @param finalDestinationLocation The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public MovementCommand withFinalDestinationLocation( + @Nullable + Location finalDestinationLocation + ) { + return new MovementCommand( + transportOrder, + driveOrder, + step, + operation, + opLocation, + finalMovement, + finalDestinationLocation, + finalDestination, + finalOperation, + properties + ); + } + + /** + * Returns the operation to be executed at the final destination position. + * + * @return The operation to be executed at the final destination position. + */ + @Nonnull + public String getFinalOperation() { + return finalOperation; + } + + /** + * Creates a copy of this object, with the given final operation. + * + * @param finalOperation The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public MovementCommand withFinalOperation( + @Nonnull + String finalOperation + ) { + return new MovementCommand( + transportOrder, + driveOrder, + step, + operation, + opLocation, + finalMovement, + finalDestinationLocation, + finalDestination, + finalOperation, + properties + ); + } + + /** + * Returns the properties of the order this command is part of. + * + * @return The properties of the order this command is part of. + */ + @Nonnull + public Map getProperties() { + return properties; + } + + /** + * Creates a copy of this object, with the given properties. + * + * @param properties The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public MovementCommand withProperties( + @Nonnull + Map properties + ) { + return new MovementCommand( + transportOrder, + driveOrder, + step, + operation, + opLocation, + finalMovement, + finalDestinationLocation, + finalDestination, + finalOperation, + properties + ); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof MovementCommand)) { + return false; + } + + MovementCommand other = (MovementCommand) o; + return step.equals(other.getStep()) && operation.equals(other.getOperation()); + } + + /** + * Compares the given movement command to this movement command, ignoring rerouting-related + * properties. + * + * @param command The movement command to compare to. + * @return {@code true}, if the given movement command is equal to this movement command + * (ignoring rerouting-related properties), otherwise {@code false}. + */ + public boolean equalsInMovement(MovementCommand command) { + if (command == null) { + return false; + } + + return this.getStep().equalsInMovement(command.getStep()) + && Objects.equals(this.getOperation(), command.getOperation()); + } + + @Override + public int hashCode() { + return step.hashCode() ^ operation.hashCode(); + } + + @Override + public String toString() { + return "MovementCommand{" + + "transportOrder=" + getTransportOrder() + + ", driveOrder=" + getDriveOrder() + + ", step=" + getStep() + + ", operation=" + getOperation() + + ", opLocation=" + getOpLocation() + + ", finalMovement=" + isFinalMovement() + + ", finalDestination=" + getFinalDestination() + + ", finalDestinationLocation=" + getFinalDestinationLocation() + + ", finalOperation=" + getFinalOperation() + + ", properties=" + getProperties() + + '}'; + } + + /** + * Checks whether an operation means something is to be done in addition to moving or not. + * + * @param operation The operation to be checked. + * @return true if, and only if, the vehicle should only move with the given + * operation. + */ + private boolean isEmptyOperation(String operation) { + return NO_OPERATION.equals(operation) + || MOVE_OPERATION.equals(operation) + || PARK_OPERATION.equals(operation); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/MovementCommandTransformer.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/MovementCommandTransformer.java new file mode 100644 index 0000000..3f9a98a --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/MovementCommandTransformer.java @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle; + +import java.util.function.UnaryOperator; + +/** + * Transforms contents of a {@link MovementCommand} before it is sent to a vehicle, thereby + * transforming coordinates in the plant model coordinate system to coordinates in the vehicle's + * coordinate system. + */ +public interface MovementCommandTransformer + extends + UnaryOperator { + + @Override + MovementCommand apply(MovementCommand command); + +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/SimVehicleCommAdapter.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/SimVehicleCommAdapter.java new file mode 100644 index 0000000..86e48a7 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/SimVehicleCommAdapter.java @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle; + +import jakarta.annotation.Nullable; + +/** + * This interface declares methods that a vehicle driver intended for simulation + * must implement. + */ +public interface SimVehicleCommAdapter + extends + VehicleCommAdapter { + + /** + * Sets an initial vehicle position. + * This method should not be called while the communication adapter is + * simulating order execution for the attached vehicle; the resulting + * behaviour is undefined. + * + * @param newPos The new position. + */ + void initVehiclePosition( + @Nullable + String newPos + ); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleCommAdapter.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleCommAdapter.java new file mode 100644 index 0000000..bf8223f --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleCommAdapter.java @@ -0,0 +1,203 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.Queue; +import org.opentcs.components.Lifecycle; +import org.opentcs.components.kernel.services.VehicleService; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.drivers.vehicle.management.VehicleProcessModelTO; +import org.opentcs.util.ExplainedBoolean; + +/** + * This interface declares the methods that a driver communicating with and + * controlling a physical vehicle must implement. + *

+ * A communication adapter is basically a driver that converts high-level + * commands sent by openTCS to a form that the controlled vehicles understand. + *

+ */ +public interface VehicleCommAdapter + extends + Lifecycle { + + /** + * Enables this comm adapter, i.e. turns it on. + */ + void enable(); + + /** + * Disables this comm adapter, i.e. turns it off. + */ + void disable(); + + /** + * Checks whether this communication adapter is enabled. + * + * @return true if, and only if, this communication adapter is + * enabled. + */ + boolean isEnabled(); + + /** + * Returns an observable model of the vehicle's and its comm adapter's attributes. + * + * @return An observable model of the vehicle's and its comm adapter's attributes. + */ + @Nonnull + VehicleProcessModel getProcessModel(); + + /** + * Returns a transferable/serializable model of the vehicle's and its comm adapter's attributes. + * + * @return A transferable/serializable model of the vehicle's and its comm adapter's attributes. + */ + @Nonnull + VehicleProcessModelTO createTransferableProcessModel(); + + /** + * Returns this adapter's queue of unsent commands. + *

+ * Unsent {@link MovementCommand}s are commands that the comm adapter received from the + * {@link VehicleController} it's associated with. When a command is sent to the vehicle, the + * command is removed from this queue and added to the {@link #getSentCommands() queue of sent + * commands}. + *

+ * + * @return This adapter's queue of unsent commands. + * @see #getCommandsCapacity() + */ + Queue getUnsentCommands(); + + /** + * Returns this adapter's queue of sent commands. + *

+ * Sent {@link MovementCommand}s are commands that the comm adapter has sent to the vehicle + * already but which have not yet been processed by it. + *

+ * + * @return This adapter's queue of sent commands. + * @see #getCommandsCapacity() + */ + Queue getSentCommands(); + + /** + * Indicates how many commands this comm adapter accepts. + *

+ * This capacity considers both the {@link #getUnsentCommands() queue of unsent commands} and the + * {@link #getSentCommands() queue of sent commands}. This means that: + *

+ *
    + *
  • The number of elements in both queues combined must not exceed this number.
  • + *
  • The vehicle will have at most this number of (not yet completed) commands at any given + * point of time.
  • + *
+ * + * @return The number of commands this comm adapter accepts. + */ + int getCommandsCapacity(); + + /** + * Checks whether this comm adapter can accept the next (i.e. one more) + * {@link MovementCommand command}. + * + * @return {@code true}, if this adapter can accept another command, otherwise {@code false}. + */ + boolean canAcceptNextCommand(); + + /** + * Returns the string the comm adapter recognizes as a recharge operation. + * + * @return The string the comm adapter recognizes as a recharge operation. + */ + String getRechargeOperation(); + + /** + * Appends a command to this communication adapter's queue of + * {@link #getUnsentCommands() unsent commands}. + *

+ * The return value of this method indicates whether the command was really added to the queue. + * The primary reason for a commmand not being added to the queue is that it would exceed the + * adapter's {@link #getCommandsCapacity() commands capacity}. + *

+ * + * @param newCommand The command to be added to this adapter's queue of + * {@link #getUnsentCommands() unsent commands}. + * @return true if, and only if, the new command was added to the queue. + */ + boolean enqueueCommand( + @Nonnull + MovementCommand newCommand + ); + + /** + * Clears this communication adapter's command queues (i.e. the queues of + * {@link #getUnsentCommands() unsent} and {@link #getSentCommands() sent} commands). + *

+ * All commands in the queue that have not been sent to this adapter's vehicle, yet, will be + * removed. Whether commands the vehicle has already received are still executed is up to the + * implementation and/or the vehicle. + *

+ */ + void clearCommandQueue(); + + /** + * Checks if the vehicle would be able to process the given transport order, taking into account + * its current state. + * + * @param order The transport order to be checked. + * @return An ExplainedBoolean indicating whether the vehicle would be able to + * process the given order. + */ + @Nonnull + ExplainedBoolean canProcess( + @Nonnull + TransportOrder order + ); + + /** + * Notifies the implementation that the vehicle's paused state in the kernel has changed. + * If pausing between points in the plant model is supported by the vehicle, the communication + * adapter may want to inform the vehicle about this change of state. + * + * @param paused The vehicle's new paused state. + */ + void onVehiclePaused(boolean paused); + + /** + * Processes a generic message to the communication adapter. + * This method provides a generic one-way communication channel to the comm adapter. The message + * can be anything, including null, and since + * {@link VehicleService#sendCommAdapterMessage(org.opentcs.data.TCSObjectReference, Object)} + * provides a way to send a message from outside the kernel, it can basically originate from any + * source. The message thus does not necessarily have to be meaningful to the concrete comm + * adapter implementation at all. + *

+ * + * Implementation notes: + * Meaningless messages should simply be ignored and not result in exceptions being thrown. + * If a comm adapter implementation does not support processing messages, it should simply provide + * an empty implementation. + * A call to this method should return quickly, i.e. this method should not execute long + * computations directly but start them in a separate thread. + *

+ * + * @param message The message to be processed. + */ + void processMessage( + @Nullable + Object message + ); + + /** + * Executes the given {@link AdapterCommand}. + * + * @param command The command to execute. + */ + void execute( + @Nonnull + AdapterCommand command + ); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleCommAdapterDescription.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleCommAdapterDescription.java new file mode 100644 index 0000000..33b7466 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleCommAdapterDescription.java @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle; + +import java.io.Serializable; + +/** + * Provides the description for a vehicle comm adapter. + */ +public abstract class VehicleCommAdapterDescription + implements + Serializable { + + /** + * Returns the description for a vehicle comm adapter. + * + * @return The description for a vehicle comm adapter. + */ + public abstract String getDescription(); + + /** + * Whether the comm adapter is a simulating one. + * + * @return true if, and only if, the vehicle is a simulating one. + */ + public abstract boolean isSimVehicleCommAdapter(); + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof VehicleCommAdapterDescription)) { + return false; + } + + VehicleCommAdapterDescription other = (VehicleCommAdapterDescription) obj; + return getDescription().equals(other.getDescription()) + && isSimVehicleCommAdapter() == other.isSimVehicleCommAdapter(); + } + + @Override + public int hashCode() { + return getDescription().hashCode(); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleCommAdapterEvent.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleCommAdapterEvent.java new file mode 100644 index 0000000..fc4a031 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleCommAdapterEvent.java @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle; + +import static java.util.Objects.requireNonNull; + +import java.io.Serializable; + +/** + * An event emitted by a communication adapter. + */ +public class VehicleCommAdapterEvent + implements + Serializable { + + /** + * The name of the adapter that emitted this event. + */ + private final String adapterName; + /** + * An optional appendix containing additional arbitrary information about the + * event. + */ + private final Serializable appendix; + + /** + * Creates a new instance. + * + * @param adapterName The name of the adapter that emitted this event. + * @param appendix An optional appendix containing additional arbitrary + * information about the event. + */ + public VehicleCommAdapterEvent(String adapterName, Serializable appendix) { + this.adapterName = requireNonNull(adapterName, "adapterName"); + this.appendix = appendix; + } + + /** + * Creates a new instance without an appendix. + * + * @param adapterName The name of the adapter that emitted this event. + */ + public VehicleCommAdapterEvent(String adapterName) { + this(adapterName, null); + } + + /** + * Returns the name of the adapter that emitted this event. + * + * @return The name of the adapter that emitted this event. + */ + public String getAdapterName() { + return adapterName; + } + + /** + * Returns the (optional) appendix containing additional arbitrary information + * about the event. + * + * @return The (optional) appendix containing additional arbitrary information + * about the event. + */ + public Serializable getAppendix() { + return appendix; + } + + @Override + public String toString() { + return "VehicleCommAdapterEvent{" + + "adapterName=" + adapterName + + ", appendix=" + appendix + + '}'; + } + +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleCommAdapterFactory.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleCommAdapterFactory.java new file mode 100644 index 0000000..2d7e84b --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleCommAdapterFactory.java @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.opentcs.components.Lifecycle; +import org.opentcs.data.model.Vehicle; + +/** + * Provides communication adapter instances for vehicles to be controlled. + */ +public interface VehicleCommAdapterFactory + extends + Lifecycle { + + /** + * Returns a {@link VehicleCommAdapterDescription} for the factory/the adapters provided. + * + * @return A {@link VehicleCommAdapterDescription} for the factory/the adapters provided. + */ + VehicleCommAdapterDescription getDescription(); + + /** + * Checks whether this factory can provide a communication adapter for the + * given vehicle. + * + * @param vehicle The vehicle to check for. + * @return true if, and only if, this factory can provide a + * communication adapter to control the given vehicle. + */ + boolean providesAdapterFor( + @Nonnull + Vehicle vehicle + ); + + /** + * Returns a communication adapter for controlling the given vehicle. + * + * @param vehicle The vehicle to be controlled. + * @return A communication adapter for controlling the given vehicle, or + * null, if this factory cannot provide an adapter for it. + */ + @Nullable + VehicleCommAdapter getAdapterFor( + @Nonnull + Vehicle vehicle + ); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleController.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleController.java new file mode 100644 index 0000000..3eb8326 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleController.java @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import org.opentcs.components.Lifecycle; +import org.opentcs.data.model.TCSResource; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.util.ExplainedBoolean; + +/** + * Provides high-level methods for the kernel to control a vehicle. + */ +public interface VehicleController + extends + Lifecycle { + + /** + * Sets/Updates the current transport order for the vehicle associated with this controller. + *

+ * The controller is expected to process the transport order's current drive order. + * Once processing of this drive order is finished, it sets the vehicle's processing state to + * {@link Vehicle.ProcState#AWAITING_ORDER} to signal this. + * This method will then be called for either the next drive order in the same transport order or + * a new transport order. + *

+ *

+ * This method may also be called again for the same/current drive order in case any + * future part of the route to be taken for the transport order has changed. + * In case of such an update, the continuity of the transport order's route is guaranteed, which + * means that the previously given route and the one given in {@code newOrder} match up to the + * last point already sent to the vehicle associated with this controller. + * Beyond that point the routes may diverge. + *

+ * + * @param newOrder The new or updated transport order. + * @throws IllegalArgumentException If {@code newOrder} cannot be processed for some reason, e.g. + * because it has already been partly processed and the route's continuity is not given, the + * vehicle's current position is unknown or the resources for the vehicle's current position may + * not be allocated (in case of forced rerouting). + */ + void setTransportOrder( + @Nonnull + TransportOrder newOrder + ) + throws IllegalArgumentException; + + /** + * Notifies the controller that the current transport order is to be aborted. + * After receiving this notification, the controller should not send any further movement commands + * to the vehicle. + * + * @param immediate If true, immediately reset the current transport order for the + * vehicle associated with this controller, clears the vehicle's command queue implicitly and + * frees all resources reserved for the removed commands/movements. + * (Note that this is unsafe, as the vehicle might be moving and clearing the command queue might + * overlap with the vehicle's movement/progress.) + */ + void abortTransportOrder(boolean immediate); + + /** + * Checks if the vehicle would be able to process the given transport order, taking into account + * its current state. + * + * @param order The transport order to be checked. + * @return An ExplainedBoolean indicating whether the vehicle would be able to + * process given order. + */ + @Nonnull + ExplainedBoolean canProcess( + @Nonnull + TransportOrder order + ); + + /** + * Notifies the implementation that the vehicle's paused state in the kernel has changed. + * If pausing between points in the plant model is supported by the vehicle, the communication + * adapter may want to inform the vehicle about this change of state. + * + * @param paused The vehicle's new paused state. + */ + void onVehiclePaused(boolean paused); + + /** + * Delivers a generic message to the communication adapter. + * + * @param message The message to be delivered. + */ + void sendCommAdapterMessage( + @Nullable + Object message + ); + + /** + * Sends a {@link AdapterCommand} to the communication adapter. + * + * @param command The adapter command to be sent. + */ + void sendCommAdapterCommand( + @Nonnull + AdapterCommand command + ); + + /** + * Returns a list of {@link MovementCommand}s that have been sent to the communication adapter. + * + * @return A list of {@link MovementCommand}s that have been sent to the communication adapter. + */ + @Nonnull + Queue getCommandsSent(); + + /** + * Returns the command for which the execution of peripheral operations must be completed before + * it can be sent to the communication adapter. + * For this command, allocated resources have already been accepted. + * + * @return The command for which the execution of peripheral operations is pending or + * {@link Optional#empty()} if there's no such command. + */ + @Nonnull + Optional getInteractionsPendingCommand(); + + /** + * Checks if the given set of resources are safe to be allocated immediately by this + * controller. + * + * @param resources The requested resources. + * @return {@code true} if the given resources are safe to be allocated by this controller, + * otherwise {@code false}. + */ + boolean mayAllocateNow( + @Nonnull + Set> resources + ); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleControllerPool.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleControllerPool.java new file mode 100644 index 0000000..42ce6cc --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleControllerPool.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle; + +import jakarta.annotation.Nonnull; + +/** + * Maintains associations between vehicles and vehicle controllers. + */ +public interface VehicleControllerPool { + + /** + * Returns the vehicle controller associated with the vehicle with the given name. + * If no vehicle controller is associated with it or if there is no vehicle with the given name, + * a null-object equivalent will be returned. + * + * @param vehicleName The name of the vehicle for which to return the vehicle controller. + * @return the vehicle controller associated with the vehicle with the given name, or a + * null-object equivalent. + */ + @Nonnull + VehicleController getVehicleController(String vehicleName); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleDataTransformerFactory.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleDataTransformerFactory.java new file mode 100644 index 0000000..3f99df6 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleDataTransformerFactory.java @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle; + +import jakarta.annotation.Nonnull; +import org.opentcs.data.model.Vehicle; + +/** + * Provides matching {@link MovementCommandTransformer} and {@link IncomingPoseTransformer} + * instances. + */ +public interface VehicleDataTransformerFactory { + + /** + * Returns the name of this factory. + * + * @return The name of this factory. + */ + @Nonnull + String getName(); + + /** + * Creates a {@link MovementCommandTransformer} for the given vehicle. + * + * @param vehicle The vehicle to create the transformer for. + * @return The newly created transformer. + * @throws IllegalArgumentException If a transformer cannot be created for the given vehicle. + */ + @Nonnull + MovementCommandTransformer createMovementCommandTransformer( + @Nonnull + Vehicle vehicle + ) + throws IllegalArgumentException; + + /** + * Creates a {@link IncomingPoseTransformer} for the given vehicle. + * + * @param vehicle The vehicle to create the transformer for. + * @return The newly created transformer. + * @throws IllegalArgumentException If a transformer cannot be created for the given vehicle. + */ + @Nonnull + IncomingPoseTransformer createIncomingPoseTransformer( + @Nonnull + Vehicle vehicle + ) + throws IllegalArgumentException; + + /** + * Checks if an {@link IncomingPoseTransformer} and {@link MovementCommandTransformer} can be + * created for the given vehicle. + * + * @param vehicle The vehicle to create the transformer for. + * @return {@code true} when both transformers can be created. + */ + boolean providesTransformersFor( + @Nonnull + Vehicle vehicle + ); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleProcessModel.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleProcessModel.java new file mode 100644 index 0000000..0c169df --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/VehicleProcessModel.java @@ -0,0 +1,863 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Queue; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.BoundingBox; +import org.opentcs.data.model.Pose; +import org.opentcs.data.model.Triple; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.notification.UserNotification; +import org.opentcs.util.annotations.ScheduledApiChange; + +/** + * An observable model of a vehicle's and its comm adapter's attributes. + */ +public class VehicleProcessModel { + + /** + * The maximum number of notifications we want to keep. + */ + private static final int MAX_NOTIFICATION_COUNT = 100; + /** + * A copy of the kernel's Vehicle instance. + */ + private final Vehicle vehicle; + /** + * A reference to the vehicle. + */ + private final TCSObjectReference vehicleReference; + /** + * Used for implementing property change events. + */ + @SuppressWarnings("this-escape") + private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); + /** + * The vehicle properties set by the driver. + * (I.e. this map does not contain properties/values set by any other components!) + */ + private final Map vehicleProperties = new HashMap<>(); + /** + * The transport order properties set by the driver. + * (I.e. this map does not contain properties/values set by any other components!) + */ + private final Map transportOrderProperties = new HashMap<>(); + /** + * Whether the comm adapter is currently enabled. + */ + private boolean commAdapterEnabled; + /** + * Whether the comm adapter is currently connected to the vehicle. + */ + private boolean commAdapterConnected; + /** + * The name of the vehicle's current position. + */ + private String position; + /** + * User notifications published by the comm adapter. + */ + private final Queue notifications = new ArrayDeque<>(); + /** + * The vehicle's pose. + */ + private Pose pose = new Pose(null, Double.NaN); + /** + * The vehicle's energy level. + */ + private int energyLevel = 100; + /** + * The vehicle's load handling devices (state). + */ + private List loadHandlingDevices = new ArrayList<>(); + /** + * The vehicle's current state. + */ + private Vehicle.State state = Vehicle.State.UNKNOWN; + /** + * The vehicle's current bounding box. + */ + private BoundingBox boundingBox; + + /** + * Creates a new instance. + * + * @param attachedVehicle The vehicle attached to the new instance. + */ + public VehicleProcessModel( + @Nonnull + Vehicle attachedVehicle + ) { + this.vehicle = requireNonNull(attachedVehicle, "attachedVehicle"); + this.vehicleReference = vehicle.getReference(); + this.boundingBox = vehicle.getBoundingBox(); + } + + /** + * Registers a new property change listener with this model. + * + * @param listener The listener to be registered. + */ + public void addPropertyChangeListener(PropertyChangeListener listener) { + pcs.addPropertyChangeListener(listener); + } + + /** + * Unregisters a property change listener from this model. + * + * @param listener The listener to be unregistered. + */ + public void removePropertyChangeListener(PropertyChangeListener listener) { + pcs.removePropertyChangeListener(listener); + } + + /** + * Returns a reference to the vehicle. + * + * @return A reference to the vehicle. + */ + @Nonnull + public TCSObjectReference getReference() { + return vehicleReference; + } + + /** + * Returns the vehicle's name. + * + * @return The vehicle's name. + */ + @Nonnull + public String getName() { + return vehicleReference.getName(); + } + + /** + * Returns user notifications published by the comm adapter. + * + * @return The notifications. + */ + @Nonnull + public Queue getNotifications() { + return notifications; + } + + /** + * Publishes an user notification. + * + * @param notification The notification to be published. + */ + public void publishUserNotification( + @Nonnull + UserNotification notification + ) { + requireNonNull(notification, "notification"); + + notifications.add(notification); + while (notifications.size() > MAX_NOTIFICATION_COUNT) { + notifications.remove(); + } + + getPropertyChangeSupport().firePropertyChange( + Attribute.USER_NOTIFICATION.name(), + null, + notification + ); + } + + /** + * Publishes an event via the kernel's event mechanism. + * + * @param event The event to be published. + */ + public void publishEvent( + @Nonnull + VehicleCommAdapterEvent event + ) { + requireNonNull(event, "event"); + + getPropertyChangeSupport().firePropertyChange( + Attribute.COMM_ADAPTER_EVENT.name(), + null, + event + ); + } + + /** + * Indicates whether the comm adapter is currently enabled or not. + * + * @return true if, and only if, the comm adapter is currently enabled. + */ + public boolean isCommAdapterEnabled() { + return commAdapterEnabled; + } + + /** + * Sets the comm adapter's enabled flag. + * + * @param commAdapterEnabled The new value. + */ + public void setCommAdapterEnabled(boolean commAdapterEnabled) { + boolean oldValue = this.commAdapterEnabled; + this.commAdapterEnabled = commAdapterEnabled; + + getPropertyChangeSupport().firePropertyChange( + Attribute.COMM_ADAPTER_ENABLED.name(), + oldValue, + commAdapterEnabled + ); + } + + /** + * Indicates whether the comm adapter is currently connected or not. + * + * @return true if, and only if, the comm adapter is currently connected. + */ + public boolean isCommAdapterConnected() { + return commAdapterConnected; + } + + /** + * Sets the comm adapter's connected flag. + * + * @param commAdapterConnected The new value. + */ + public void setCommAdapterConnected(boolean commAdapterConnected) { + boolean oldValue = this.commAdapterConnected; + this.commAdapterConnected = commAdapterConnected; + + getPropertyChangeSupport().firePropertyChange( + Attribute.COMM_ADAPTER_CONNECTED.name(), + oldValue, + commAdapterConnected + ); + } + + /** + * Returns the vehicle's current position. + * + * @return The position. + */ + @Nullable + public String getPosition() { + return position; + } + + /** + * Updates the vehicle's current position. + * + * @param position The new position + */ + public void setPosition( + @Nullable + String position + ) { + // Otherwise update the position, notify listeners and let the kernel know. + String oldValue = this.position; + this.position = position; + + getPropertyChangeSupport().firePropertyChange( + Attribute.POSITION.name(), + oldValue, + position + ); + } + + /** + * Returns the vehicle's precise position. + * + * @return The vehicle's precise position. + * @deprecated Use {@link #getPose()} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + @Nullable + public Triple getPrecisePosition() { + return pose.getPosition(); + } + + /** + * Sets the vehicle's precise position. + * + * @param position The new position. + * @deprecated Use {@link #setPose(Pose)}} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public void setPrecisePosition( + @Nullable + Triple position + ) { + setPose(pose.withPosition(position)); + } + + /** + * Returns the vehicle's current orientation angle. + * + * @return The vehicle's current orientation angle. + * @see Vehicle#getOrientationAngle() + * @deprecated Use {@link #getPose()} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public double getOrientationAngle() { + return pose.getOrientationAngle(); + } + + /** + * Sets the vehicle's current orientation angle. + * + * @param angle The new angle + * @deprecated Use {@link #setPose(Pose)} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public void setOrientationAngle(double angle) { + setPose(pose.withOrientationAngle(angle)); + } + + /** + * Returns the vehicle's pose. + * + * @return The vehicle's pose. + */ + @Nonnull + public Pose getPose() { + return pose; + } + + /** + * Sets the vehicle's pose. + * + * @param pose The new pose + */ + public void setPose( + @Nonnull + Pose pose + ) { + requireNonNull(pose, "pose"); + + Pose oldPose = this.pose; + this.pose = pose; + getPropertyChangeSupport().firePropertyChange( + Attribute.POSE.name(), + oldPose, + pose + ); + getPropertyChangeSupport().firePropertyChange( + Attribute.PRECISE_POSITION.name(), + oldPose.getPosition(), + pose.getPosition() + ); + getPropertyChangeSupport().firePropertyChange( + Attribute.ORIENTATION_ANGLE.name(), + oldPose.getOrientationAngle(), + pose.getOrientationAngle() + ); + } + + /** + * Returns the vehicle's current energy level. + * + * @return The vehicle's current energy level. + */ + public int getEnergyLevel() { + return energyLevel; + } + + /** + * Sets the vehicle's current energy level. + * + * @param newLevel The new level. + */ + public void setEnergyLevel(int newLevel) { + int oldValue = this.energyLevel; + this.energyLevel = newLevel; + + getPropertyChangeSupport().firePropertyChange( + Attribute.ENERGY_LEVEL.name(), + oldValue, + newLevel + ); + } + + /** + * Returns the vehicle's load handling devices. + * + * @return The vehicle's load handling devices. + */ + @Nonnull + public List getLoadHandlingDevices() { + return loadHandlingDevices; + } + + /** + * Sets the vehicle's load handling devices. + * + * @param devices The new devices + */ + public void setLoadHandlingDevices( + @Nonnull + List devices + ) { + List devs = new ArrayList<>(devices); + + List oldValue = this.loadHandlingDevices; + this.loadHandlingDevices = devs; + + getPropertyChangeSupport().firePropertyChange( + Attribute.LOAD_HANDLING_DEVICES.name(), + oldValue, + devs + ); + } + + /** + * Sets a property of the vehicle. + * + * @param key The property's key. + * @param value The property's new value. + */ + public void setProperty( + @Nonnull + String key, + @Nullable + String value + ) { + requireNonNull(key, "key"); + + // Check whether the new value is the same as the last one we set. If yes, ignore the update, + // as it would cause unnecessary churn in the kernel. + // Note that this assumes that other components do not modify properties set by this driver. + String oldValue = vehicleProperties.get(key); + if (Objects.equals(value, oldValue)) { + return; + } + vehicleProperties.put(key, value); + + getPropertyChangeSupport().firePropertyChange( + Attribute.VEHICLE_PROPERTY.name(), + null, + new VehiclePropertyUpdate(key, value) + ); + } + + /** + * Returns the vehicle's current state. + * + * @return The state + */ + @Nonnull + public Vehicle.State getState() { + return state; + } + + /** + * Sets the vehicle's current state. + * + * @param newState The new state + */ + public void setState( + @Nonnull + Vehicle.State newState + ) { + Vehicle.State oldState = this.state; + this.state = newState; + + getPropertyChangeSupport().firePropertyChange(Attribute.STATE.name(), oldState, newState); + + if (oldState != Vehicle.State.ERROR && newState == Vehicle.State.ERROR) { + publishUserNotification( + new UserNotification( + getName(), + "Vehicle state changed to ERROR", + UserNotification.Level.NOTEWORTHY + ) + ); + } + else if (oldState == Vehicle.State.ERROR && newState != Vehicle.State.ERROR) { + publishUserNotification( + new UserNotification( + getName(), + "Vehicle state is no longer ERROR", + UserNotification.Level.NOTEWORTHY + ) + ); + } + } + + /** + * Returns the vehicle's current length. + * + * @return The vehicle's current length. + * @deprecated Use {@link #getBoundingBox()} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public int getLength() { + return (int) boundingBox.getLength(); + } + + /** + * Sets the vehicle's current length. + * + * @param length The new length. + * @deprecated Use {@link #setBoundingBox(BoundingBox)} instead. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public void setLength(int length) { + setBoundingBox(getBoundingBox().withLength(length)); + } + + /** + * Returns the vehicle's current bounding box. + * + * @return The vehicle's current bounding box. + */ + @Nonnull + public BoundingBox getBoundingBox() { + return boundingBox; + } + + /** + * Sets the vehicle's current bounding box. + * + * @param boundingBox The new bounding box. + */ + public void setBoundingBox( + @Nonnull + BoundingBox boundingBox + ) { + requireNonNull(boundingBox, "boundingBox"); + + BoundingBox oldValue = this.boundingBox; + this.boundingBox = boundingBox; + + getPropertyChangeSupport().firePropertyChange( + Attribute.BOUNDING_BOX.name(), + oldValue, + boundingBox + ); + getPropertyChangeSupport().firePropertyChange( + Attribute.LENGTH.name(), + oldValue.getLength(), + boundingBox.getLength() + ); + } + + /** + * Sets a property of the transport order the vehicle is currently processing. + * + * @param key The property's key. + * @param value The property's new value. + */ + public void setTransportOrderProperty( + @Nonnull + String key, + @Nullable + String value + ) { + requireNonNull(key, "key"); + + // Check whether the new value is the same as the last one we set. If yes, ignore the update, + // as it would cause unnecessary churn in the kernel. + // Note that this assumes that other components do not modify properties set by this driver. + String oldValue = transportOrderProperties.get(key); + if (Objects.equals(value, oldValue)) { + return; + } + transportOrderProperties.put(key, value); + + getPropertyChangeSupport().firePropertyChange( + Attribute.TRANSPORT_ORDER_PROPERTY.name(), + null, + new TransportOrderPropertyUpdate(key, value) + ); + } + + /** + * Notifies observers that the given command has been added to the comm adapter's command queue. + * + * @param enqueuedCommand The command that has been added to the queue. + */ + public void commandEnqueued( + @Nonnull + MovementCommand enqueuedCommand + ) { + getPropertyChangeSupport().firePropertyChange( + Attribute.COMMAND_ENQUEUED.name(), + null, + enqueuedCommand + ); + } + + /** + * Notifies observers that the given command has been sent to the associated vehicle. + * + * @param sentCommand The command that has been sent to the vehicle. + */ + public void commandSent( + @Nonnull + MovementCommand sentCommand + ) { + getPropertyChangeSupport().firePropertyChange( + Attribute.COMMAND_SENT.name(), + null, + sentCommand + ); + } + + /** + * Notifies observers that the given command has been executed by the comm adapter/vehicle. + * + * @param executedCommand The command that has been executed. + */ + public void commandExecuted( + @Nonnull + MovementCommand executedCommand + ) { + getPropertyChangeSupport().firePropertyChange( + Attribute.COMMAND_EXECUTED.name(), + null, + executedCommand + ); + } + + /** + * Notifies observers that the given command could not be executed by the comm adapter/vehicle. + * + * @param failedCommand The command that could not be executed. + */ + public void commandFailed( + @Nonnull + MovementCommand failedCommand + ) { + getPropertyChangeSupport().firePropertyChange( + Attribute.COMMAND_FAILED.name(), + null, + failedCommand + ); + } + + /** + * Notifies observers that the vehicle would like to have its integration level changed. + * + * @param level The integration level to change to. + */ + public void integrationLevelChangeRequested( + @Nonnull + Vehicle.IntegrationLevel level + ) { + getPropertyChangeSupport().firePropertyChange( + Attribute.INTEGRATION_LEVEL_CHANGE_REQUESTED.name(), + null, + level + ); + } + + /** + * Notifies observers that the vehicle would like to have its current transport order withdrawn. + * + * @param forced Whether a forced withdrawal is requested. + */ + public void transportOrderWithdrawalRequested(boolean forced) { + getPropertyChangeSupport().firePropertyChange( + Attribute.TRANSPORT_ORDER_WITHDRAWAL_REQUESTED.name(), + null, + forced + ); + } + + protected PropertyChangeSupport getPropertyChangeSupport() { + return pcs; + } + + /** + * A notification object sent to observers to indicate a change of a property. + */ + public static class PropertyUpdate { + + /** + * The property's key. + */ + private final String key; + /** + * The property's new value. + */ + private final String value; + + /** + * Creates a new instance. + * + * @param key The key. + * @param value The new value. + */ + public PropertyUpdate(String key, String value) { + this.key = requireNonNull(key, "key"); + this.value = value; + } + + /** + * Returns the property's key. + * + * @return The property's key. + */ + public String getKey() { + return key; + } + + /** + * Returns the property's new value. + * + * @return The property's new value. + */ + public String getValue() { + return value; + } + } + + /** + * A notification object sent to observers to indicate a change of a vehicle's property. + */ + public static class VehiclePropertyUpdate + extends + PropertyUpdate { + + /** + * Creates a new instance. + * + * @param key The property's key. + * @param value The new value. + */ + public VehiclePropertyUpdate(String key, String value) { + super(key, value); + } + } + + /** + * A notification object sent to observers to indicate a change of a transport order's property. + */ + public static class TransportOrderPropertyUpdate + extends + PropertyUpdate { + + /** + * Creates a new instance. + * + * @param key The property's key. + * @param value The new value. + */ + public TransportOrderPropertyUpdate(String key, String value) { + super(key, value); + } + } + + /** + * Notification arguments to indicate some change. + */ + public enum Attribute { + /** + * Indicates a change of the comm adapter's enabled setting. + */ + COMM_ADAPTER_ENABLED, + /** + * Indicates a change of the comm adapter's connected setting. + */ + COMM_ADAPTER_CONNECTED, + /** + * Indicates a change of the vehicle's position. + */ + POSITION, + /** + * Indicates a change of the vehicle's precise position. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + PRECISE_POSITION, + /** + * Indicates a change of the vehicle's orientation angle. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + ORIENTATION_ANGLE, + /** + * Indicates a change of the vehicle's pose. + */ + POSE, + /** + * Indicates a change of the vehicle's energy level. + */ + ENERGY_LEVEL, + /** + * Indicates a change of the vehicle's load handling devices. + */ + LOAD_HANDLING_DEVICES, + /** + * Indicates a change of the vehicle's state. + */ + STATE, + /** + * Indicates a change of the vehicle's length. + */ + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + LENGTH, + /** + * Indicates a change of the vehicle's bounding box. + */ + BOUNDING_BOX, + /** + * Indicates a new user notification was published. + */ + USER_NOTIFICATION, + /** + * Indicates a new comm adapter event was published. + */ + COMM_ADAPTER_EVENT, + /** + * Indicates a command was enqueued. + */ + COMMAND_ENQUEUED, + /** + * Indicates a command was sent. + */ + COMMAND_SENT, + /** + * Indicates a command was executed successfully. + */ + COMMAND_EXECUTED, + /** + * Indicates a command failed. + */ + COMMAND_FAILED, + /** + * Indicates a change of a vehicle property. + */ + VEHICLE_PROPERTY, + /** + * Indicates a change of a transport order property. + */ + TRANSPORT_ORDER_PROPERTY, + /** + * Indicates a request to change the integration level of the vehicle. + */ + INTEGRATION_LEVEL_CHANGE_REQUESTED, + /** + * Indicates a request to withdraw the vehicles current transport order. + */ + TRANSPORT_ORDER_WITHDRAWAL_REQUESTED; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/commands/InitPositionCommand.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/commands/InitPositionCommand.java new file mode 100644 index 0000000..fd81601 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/commands/InitPositionCommand.java @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle.commands; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.SimVehicleCommAdapter; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A command for initializing the comm adapter's position. + */ +public class InitPositionCommand + implements + AdapterCommand { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(InitPositionCommand.class); + /** + * The position to set. + */ + private final String position; + + /** + * Creates a new instance. + * + * @param position The position to set. + */ + public InitPositionCommand( + @Nonnull + String position + ) { + this.position = requireNonNull(position, "position"); + } + + @Override + public void execute(VehicleCommAdapter adapter) { + if (!(adapter instanceof SimVehicleCommAdapter)) { + LOG.warn("Adapter is not a SimVehicleCommAdapter: {}", adapter); + return; + } + + ((SimVehicleCommAdapter) adapter).initVehiclePosition(position); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/commands/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/commands/package-info.java new file mode 100644 index 0000000..a45abe6 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/commands/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Commands a comm adapter may execute. + */ +package org.opentcs.drivers.vehicle.commands; diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/CommAdapterEvent.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/CommAdapterEvent.java new file mode 100644 index 0000000..dddfa47 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/CommAdapterEvent.java @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle.management; + +import java.io.Serializable; +import org.opentcs.drivers.LowLevelCommunicationEvent; + +/** + * Instances of this class represent events emitted by/for comm adapter changes. + */ +public abstract class CommAdapterEvent + implements + LowLevelCommunicationEvent, + Serializable { + + /** + * Creates an empty event. + */ + public CommAdapterEvent() { + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/ProcessModelEvent.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/ProcessModelEvent.java new file mode 100644 index 0000000..a39173c --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/ProcessModelEvent.java @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle.management; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import org.opentcs.drivers.vehicle.VehicleProcessModel; + +/** + * Instances of this class represent events emitted by/for changes on {@link VehicleProcessModel}s. + */ +public class ProcessModelEvent + extends + CommAdapterEvent + implements + Serializable { + + /** + * The attribute's name that changed in the process model. + */ + private final String attributeChanged; + /** + * A serializable representation of the corresponding process model. + */ + private final VehicleProcessModelTO updatedProcessModel; + + /** + * Creates a new instance. + * + * @param attributeChanged The attribute's name that changed. + * @param updatedProcessModel A serializable representation of the corresponding process model. + */ + public ProcessModelEvent( + @Nonnull + String attributeChanged, + @Nonnull + VehicleProcessModelTO updatedProcessModel + ) { + this.attributeChanged = requireNonNull(attributeChanged, "attributeChanged"); + this.updatedProcessModel = requireNonNull(updatedProcessModel, "updatedProcessModel"); + } + + /** + * Returns the attribute's name that changed in the process model. + * + * @return The attribute's name that changed in the process model. + */ + public String getAttributeChanged() { + return attributeChanged; + } + + /** + * Returns a serializable representation of the corresponding process model. + * + * @return A serializable representation of the corresponding process model. + */ + public VehicleProcessModelTO getUpdatedProcessModel() { + return updatedProcessModel; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/VehicleAttachmentEvent.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/VehicleAttachmentEvent.java new file mode 100644 index 0000000..a92937c --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/VehicleAttachmentEvent.java @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle.management; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; + +/** + * Instances of this class represent events emitted by/for attaching comm adapters. + */ +public class VehicleAttachmentEvent + extends + CommAdapterEvent + implements + Serializable { + + /** + * The vehicle's name a comm adapter has been attached to. + */ + private final String vehicleName; + /** + * The {@link VehicleAttachmentInformation} to the actual attachment. + */ + private final VehicleAttachmentInformation attachmentInformation; + + /** + * Creates a new instance. + * + * @param vehicleName The vehicle's name a comm adapter has been attached to. + * @param attachmentInformation The information to the actual attachment. + */ + public VehicleAttachmentEvent( + @Nonnull + String vehicleName, + @Nonnull + VehicleAttachmentInformation attachmentInformation + ) { + this.vehicleName = requireNonNull(vehicleName, "vehicleName"); + this.attachmentInformation = requireNonNull(attachmentInformation, "attachmentInformation"); + } + + /** + * Returns the vehicle's name a comm adapter has been attached to. + * + * @return The vehicle's name a comm adapter has been attached to. + */ + public String getVehicleName() { + return vehicleName; + } + + /** + * Returns the {@link VehicleAttachmentInformation} to the actual attachment. + * + * @return The {@link VehicleAttachmentInformation} to the actual attachment. + */ + public VehicleAttachmentInformation getAttachmentInformation() { + return attachmentInformation; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/VehicleAttachmentInformation.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/VehicleAttachmentInformation.java new file mode 100644 index 0000000..63f5c56 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/VehicleAttachmentInformation.java @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle.management; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.Serializable; +import java.util.List; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; + +/** + * Describes which communication adapter a vehicle is currently associated with and which adapters + * are available. + */ +public class VehicleAttachmentInformation + implements + Serializable { + + /** + * The vehicle this attachment information belongs to. + */ + private final TCSObjectReference vehicleReference; + /** + * The list of comm adapters available to be attached to the referenced vehicle. + */ + private final List availableCommAdapters; + /** + * The comm adapter attached to the referenced vehicle. + */ + private final VehicleCommAdapterDescription attachedCommAdapter; + + /** + * Creates a new instance. + * + * @param vehicleReference The vehicle this attachment information belongs to. + * @param availableCommAdapters The list of comm adapters available to be attached to the + * referenced vehicle. + * @param attachedCommAdapter The comm adapter attached to the referenced vehicle. + */ + public VehicleAttachmentInformation( + @Nonnull + TCSObjectReference vehicleReference, + @Nonnull + List availableCommAdapters, + @Nonnull + VehicleCommAdapterDescription attachedCommAdapter + ) { + this.vehicleReference = requireNonNull(vehicleReference, "vehicleReference"); + this.availableCommAdapters = requireNonNull(availableCommAdapters, "availableCommAdapters"); + this.attachedCommAdapter = requireNonNull(attachedCommAdapter, "attachedCommAdapter"); + } + + /** + * Returns the vehicle this attachment information belongs to. + * + * @return The vehicle this attachment information belongs to. + */ + @Nonnull + public TCSObjectReference getVehicleReference() { + return vehicleReference; + } + + /** + * Creates a copy of this object with the given vehicle reference. + * + * @param vehicleReference The new vehicle reference. + * @return A copy of this object, differing in the given vehicle reference. + */ + public VehicleAttachmentInformation withVehicleReference( + TCSObjectReference vehicleReference + ) { + return new VehicleAttachmentInformation( + vehicleReference, + getAvailableCommAdapters(), + getAttachedCommAdapter() + ); + } + + /** + * Returns the list of comm adapters available to be attached to the referenced vehicle. + * + * @return The list of comm adapters available to be attached to the referenced vehicle. + */ + @Nonnull + public List getAvailableCommAdapters() { + return availableCommAdapters; + } + + /** + * Creates a copy of this object with the given available comm adapters. + * + * @param availableCommAdapters The new available comm adapters. + * @return A copy of this object, differing in the given available comm adapters. + */ + public VehicleAttachmentInformation withAvailableCommAdapters( + @Nonnull + List availableCommAdapters + ) { + return new VehicleAttachmentInformation( + getVehicleReference(), + availableCommAdapters, + getAttachedCommAdapter() + ); + } + + /** + * Returns the comm adapter attached to the referenced vehicle. + * + * @return The comm adapter attached to the referenced vehicle. + */ + @Nonnull + public VehicleCommAdapterDescription getAttachedCommAdapter() { + return attachedCommAdapter; + } + + /** + * Creates a copy of this object with the given attached comm adapter. + * + * @param attachedCommAdapter The new attached comm adapter. + * @return A copy of this object, differing in the given attached comm adapter. + */ + public VehicleAttachmentInformation withAttachedCommAdapter( + @Nonnull + VehicleCommAdapterDescription attachedCommAdapter + ) { + return new VehicleAttachmentInformation( + getVehicleReference(), + getAvailableCommAdapters(), + attachedCommAdapter + ); + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/VehicleCommAdapterPanel.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/VehicleCommAdapterPanel.java new file mode 100644 index 0000000..0969226 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/VehicleCommAdapterPanel.java @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle.management; + +import javax.swing.JPanel; + +/** + * A base class for panels associated with comm adapters. + */ +public abstract class VehicleCommAdapterPanel + extends + JPanel { + + /** + * Returns the title for this comm adapter panel. + * The default implementation returns the accessible name from the panel's accessible context. + * + * @return The title for this comm adapter panel. + */ + public String getTitle() { + return getAccessibleContext().getAccessibleName(); + } + + /** + * Notifies a comm adapter panel that the corresponding process model changed. + * The comm adapter panel may want to update the content its representing. + * + * @param attributeChanged The attribute name that chagend. + * @param processModel The process model. + */ + public abstract void processModelChange( + String attributeChanged, + VehicleProcessModelTO processModel + ); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/VehicleCommAdapterPanelFactory.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/VehicleCommAdapterPanelFactory.java new file mode 100644 index 0000000..48470e4 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/VehicleCommAdapterPanelFactory.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle.management; + +import jakarta.annotation.Nonnull; +import java.util.List; +import org.opentcs.components.Lifecycle; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; + +/** + * Provides comm adapter specific panels used for interaction and displaying information. + */ +public interface VehicleCommAdapterPanelFactory + extends + Lifecycle { + + /** + * Returns a list of {@link VehicleCommAdapterPanel}s. + * + * @param description The description to create panels for. + * @param vehicle The vehicle to create panels for. + * @param processModel The current state of the process model a panel may want to initialize its + * components with. + * @return A list of comm adapter panels, or an empty list, if this factory cannot provide panels + * for the given description. + */ + List getPanelsFor( + @Nonnull + VehicleCommAdapterDescription description, + @Nonnull + TCSObjectReference vehicle, + @Nonnull + VehicleProcessModelTO processModel + ); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/VehicleProcessModelTO.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/VehicleProcessModelTO.java new file mode 100644 index 0000000..874612f --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/VehicleProcessModelTO.java @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle.management; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.io.Serializable; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import org.opentcs.data.model.BoundingBox; +import org.opentcs.data.model.Pose; +import org.opentcs.data.model.Triple; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.notification.UserNotification; +import org.opentcs.drivers.vehicle.LoadHandlingDevice; +import org.opentcs.drivers.vehicle.VehicleProcessModel; +import org.opentcs.util.annotations.ScheduledApiChange; + +/** + * A serializable representation of a {@link VehicleProcessModel}. + *

+ * For documentation of methods in this class, see the API documentation of their corresponding + * counterparts in {@link VehicleProcessModel}. + *

+ */ +public class VehicleProcessModelTO + implements + Serializable { + + private String name; + private boolean commAdapterEnabled; + private boolean commAdapterConnected; + private String position; + private Queue notifications = new ArrayDeque<>(); + private Pose pose = new Pose(null, Double.NaN); + private int energyLevel; + private List loadHandlingDevices = new ArrayList<>(); + private Vehicle.State state = Vehicle.State.UNKNOWN; + private BoundingBox boundingBox = new BoundingBox(1000, 1000, 1000); + + /** + * Creates a new instance. + */ + public VehicleProcessModelTO() { + } + + public String getName() { + return name; + } + + public VehicleProcessModelTO setName( + @Nonnull + String name + ) { + this.name = requireNonNull(name); + return this; + } + + public boolean isCommAdapterEnabled() { + return commAdapterEnabled; + } + + public VehicleProcessModelTO setCommAdapterEnabled(boolean commAdapterEnabled) { + this.commAdapterEnabled = commAdapterEnabled; + return this; + } + + public boolean isCommAdapterConnected() { + return commAdapterConnected; + } + + public VehicleProcessModelTO setCommAdapterConnected(boolean commAdapterConnected) { + this.commAdapterConnected = commAdapterConnected; + return this; + } + + @Nullable + public String getPosition() { + return position; + } + + public VehicleProcessModelTO setPosition( + @Nullable + String position + ) { + this.position = position; + return this; + } + + @Nonnull + public Queue getNotifications() { + return notifications; + } + + public VehicleProcessModelTO setNotifications( + @Nonnull + Queue notifications + ) { + this.notifications = requireNonNull(notifications, "notifications"); + return this; + } + + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + @Nullable + public Triple getPrecisePosition() { + return pose.getPosition(); + } + + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public VehicleProcessModelTO setPrecisePosition( + @Nullable + Triple precisePosition + ) { + this.pose = pose.withPosition(precisePosition); + return this; + } + + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public double getOrientationAngle() { + return pose.getOrientationAngle(); + } + + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public VehicleProcessModelTO setOrientationAngle(double orientationAngle) { + this.pose = pose.withOrientationAngle(orientationAngle); + return this; + } + + @Nonnull + public Pose getPose() { + return pose; + } + + public VehicleProcessModelTO setPose( + @Nonnull + Pose pose + ) { + this.pose = requireNonNull(pose, "pose"); + return this; + } + + public int getEnergyLevel() { + return energyLevel; + } + + public VehicleProcessModelTO setEnergyLevel(int energyLevel) { + this.energyLevel = energyLevel; + return this; + } + + @Nonnull + public List getLoadHandlingDevices() { + return loadHandlingDevices; + } + + public VehicleProcessModelTO setLoadHandlingDevices( + @Nonnull + List loadHandlingDevices + ) { + this.loadHandlingDevices = requireNonNull(loadHandlingDevices, "loadHandlingDevices"); + return this; + } + + @Nonnull + public Vehicle.State getState() { + return state; + } + + public VehicleProcessModelTO setState( + @Nonnull + Vehicle.State state + ) { + this.state = requireNonNull(state, "state"); + return this; + } + + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public int getLength() { + return (int) boundingBox.getLength(); + } + + @Deprecated + @ScheduledApiChange(when = "7.0", details = "Will be removed.") + public VehicleProcessModelTO setLength(int length) { + setBoundingBox(getBoundingBox().withLength(length)); + return this; + } + + public BoundingBox getBoundingBox() { + return boundingBox; + } + + public VehicleProcessModelTO setBoundingBox( + @Nonnull + BoundingBox boundingBox + ) { + this.boundingBox = requireNonNull(boundingBox, "boundingBox"); + return this; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/package-info.java new file mode 100644 index 0000000..cfd94dc --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/management/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Components needed for processing information related to comm adapters. + */ +package org.opentcs.drivers.vehicle.management; diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/messages/ClearError.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/messages/ClearError.java new file mode 100644 index 0000000..440b3c9 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/messages/ClearError.java @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle.messages; + +import java.io.Serializable; + +/** + * A message that informs a communication adapter that it/the vehicle should + * reset currently active errors if possible. + */ +public class ClearError + implements + Serializable { + + /** + * Creates a new instance. + */ + public ClearError() { + // Do nada. + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/messages/SetSpeedMultiplier.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/messages/SetSpeedMultiplier.java new file mode 100644 index 0000000..2e61d8e --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/messages/SetSpeedMultiplier.java @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle.messages; + +import static org.opentcs.util.Assertions.checkInRange; + +import java.io.Serializable; + +/** + * A message that informs a communication adapter about a speed multiplier it/the vehicle should + * apply. + */ +public class SetSpeedMultiplier + implements + Serializable { + + /** + * The speed multiplier in percent. + */ + private final int multiplier; + + /** + * Creates a new instance. + * + * @param multiplier The speed multiplier in percent. + */ + public SetSpeedMultiplier(final int multiplier) { + this.multiplier = checkInRange(multiplier, 0, 100, "multiplier"); + } + + /** + * Returns the speed multiplier in percent. + * + * @return The speed multiplier in percent. + */ + public int getMultiplier() { + return multiplier; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/messages/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/messages/package-info.java new file mode 100644 index 0000000..bb9573e --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/messages/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Reference implementations of commonly used messages to be processed by vehicle drivers. + */ +package org.opentcs.drivers.vehicle.messages; diff --git a/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/package-info.java new file mode 100644 index 0000000..0f458f0 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/drivers/vehicle/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Components needed for controlling physical vehicles and processing information sent by them. + */ +package org.opentcs.drivers.vehicle; diff --git a/opentcs-api-base/src/main/java/org/opentcs/util/Assertions.java b/opentcs-api-base/src/main/java/org/opentcs/util/Assertions.java new file mode 100644 index 0000000..026bcb6 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/util/Assertions.java @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util; + +import jakarta.annotation.Nullable; + +/** + * Utility methods for checking preconditions, postconditions etc.. + */ +public class Assertions { + + /** + * Prevents instatiation. + */ + private Assertions() { + } + + /** + * Ensures the given expression is {@code true}. + * + * @param expression The expression to be checked. + * @param errorMessage An optional error message. + * @throws IllegalArgumentException If the given expression is not true. + */ + public static void checkArgument(boolean expression, String errorMessage) + throws IllegalArgumentException { + checkArgument(expression, errorMessage, (Object) null); + } + + /** + * Ensures the given expression is {@code true}. + * + * @param expression The expression to be checked. + * @param messageTemplate A formatting template for the error message. + * @param messageArgs The arguments to be formatted into the message template. + * @throws IllegalArgumentException If the given expression is not true. + */ + public static void checkArgument( + boolean expression, + String messageTemplate, + @Nullable + Object... messageArgs + ) + throws IllegalArgumentException { + if (!expression) { + throw new IllegalArgumentException( + String.format( + String.valueOf(messageTemplate), + messageArgs + ) + ); + } + } + + /** + * Ensures the given expression is {@code true}. + * + * @param expression The expression to be checked. + * @param errorMessage An optional error message. + * @throws IllegalStateException If the given expression is not true. + */ + public static void checkState(boolean expression, String errorMessage) + throws IllegalStateException { + checkState(expression, errorMessage, (Object) null); + } + + /** + * Ensures the given expression is {@code true}. + * + * @param expression The expression to be checked. + * @param messageTemplate A formatting template for the error message. + * @param messageArgs The arguments to be formatted into the message template. + * @throws IllegalStateException If the given expression is not true. + */ + public static void checkState( + boolean expression, + String messageTemplate, + @Nullable + Object... messageArgs + ) + throws IllegalStateException { + if (!expression) { + throw new IllegalStateException( + String.format( + String.valueOf(messageTemplate), + messageArgs + ) + ); + } + } + + /** + * Ensures that {@code value} is not smaller than {@code minimum} and not greater than + * {@code maximum}. + * + * @param value The value to be checked. + * @param minimum The minimum value. + * @param maximum The maximum value. + * @return The given value. + * @throws IllegalArgumentException If value is not within the given range. + */ + public static int checkInRange(int value, int minimum, int maximum) + throws IllegalArgumentException { + return checkInRange(value, minimum, maximum, "value"); + } + + /** + * Ensures that {@code value} is not smaller than {@code minimum} and not greater than + * {@code maximum}. + * + * @param value The value to be checked. + * @param minimum The minimum value. + * @param maximum The maximum value. + * @param valueName An optional name for the value to be used for the exception message. + * @return The given value. + * @throws IllegalArgumentException If value is not within the given range. + */ + public static int checkInRange(int value, int minimum, int maximum, String valueName) + throws IllegalArgumentException { + if (value < minimum || value > maximum) { + throw new IllegalArgumentException( + String.format( + "%s is not in [%d..%d]: %d", + String.valueOf(valueName), + minimum, + maximum, + value + ) + ); + } + return value; + } + + /** + * Ensures that {@code value} is not smaller than {@code minimum} and not greater than + * {@code maximum}. + * + * @param value The value to be checked. + * @param minimum The minimum value. + * @param maximum The maximum value. + * @return The given value. + * @throws IllegalArgumentException If value is not within the given range. + */ + public static long checkInRange(long value, long minimum, long maximum) + throws IllegalArgumentException { + return checkInRange(value, minimum, maximum, "value"); + } + + /** + * Ensures that {@code value} is not smaller than {@code minimum} and not greater than + * {@code maximum}. + * + * @param value The value to be checked. + * @param minimum The minimum value. + * @param maximum The maximum value. + * @param valueName An optional name for the value to be used for the exception message. + * @return The given value. + * @throws IllegalArgumentException If value is not within the given range. + */ + public static long checkInRange(long value, long minimum, long maximum, String valueName) + throws IllegalArgumentException { + if (value < minimum || value > maximum) { + throw new IllegalArgumentException( + String.format( + "%s is not in [%d..%d]: %d", + String.valueOf(valueName), + minimum, + maximum, + value + ) + ); + } + return value; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/util/CallWrapper.java b/opentcs-api-base/src/main/java/org/opentcs/util/CallWrapper.java new file mode 100644 index 0000000..af9be1e --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/util/CallWrapper.java @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util; + +import java.util.concurrent.Callable; + +/** + * Provides methods for wrapping other method calls. + * This can be useful to ensure preparations have been done for a larger set of various method + * calls, for example. + */ +public interface CallWrapper { + + /** + * Calls a method that has a return value. + * + * @param The return value's type. + * @param callable The method wrapped in a {@link Callable} instance. + * @return The result of the method's call. + * @throws Exception If there was an exception calling method. + */ + R call(Callable callable) + throws Exception; + + /** + * Calls a mehtod that has no return value. + * + * @param runnable The method wrapped in a {@link Runnable} instance. + * @throws Exception If there was an exception calling method. + */ + void call(Runnable runnable) + throws Exception; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/util/ClassMatcher.java b/opentcs-api-base/src/main/java/org/opentcs/util/ClassMatcher.java new file mode 100644 index 0000000..5be61fa --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/util/ClassMatcher.java @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; + +/** + * Checks whether the input argument is assignable to any of a given set of classes. + */ +public class ClassMatcher + implements + Predicate, + Serializable { + + /** + * The set of classes to evaluate incoming events to. + */ + private final Set> clazzes = new HashSet<>(); + + /** + * Creates a new instance. + * + * @param events The set of classes to evaluate incoming events to. + */ + public ClassMatcher(Class... events) { + clazzes.addAll(Arrays.asList(events)); + } + + @Override + public boolean test(Object object) { + return clazzes.stream().anyMatch(clazz -> clazz.isAssignableFrom(object.getClass())); + } + + @Override + public Predicate negate() { + return new ClassMatcher() { + @Override + public boolean test(Object t) { + return !ClassMatcher.this.test(t); + } + }; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/util/CyclicTask.java b/opentcs-api-base/src/main/java/org/opentcs/util/CyclicTask.java new file mode 100644 index 0000000..98d269f --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/util/CyclicTask.java @@ -0,0 +1,174 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util; + +import static org.opentcs.util.Assertions.checkInRange; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A template for cyclic tasks. + * Subclasses only need to provide an implementation of + * runActualTask(), which will be called until the task is + * terminated by calling terminate(); after each call of + * runActualTask(), a configurable delay may be inserted. + */ +public abstract class CyclicTask + implements + Runnable { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(CyclicTask.class); + /** + * The time (in ms) that this task sleeps after each execution of + * runActualTask(). + */ + private final long sleepTime; + /** + * A private object to safely synchronize on. + */ + private final Object syncObject = new Object(); + /** + * The thread executing this task. + */ + private volatile Thread taskThread; + /** + * This task's terminated flag. + */ + private volatile boolean terminated; + /** + * Whether this task ignores interrupts occurring while sleeping between + * executions of the actual task. + */ + private volatile boolean ignoringInterrupts; + + /** + * Creates a new CyclicTask. + * + * @param tSleep The time to sleep between two executions of the actual + * task (in milliseconds). + */ + public CyclicTask(final long tSleep) { + this.sleepTime = checkInRange(tSleep, 0, Long.MAX_VALUE, "tSleep"); + } + + /** + * Indicates whether this task has been terminated. + * + * @return true if, and only if, this task's + * terminated flag has been set. + */ + public boolean isTerminated() { + return terminated; + } + + /** + * Terminates this task before its next execution cycle. + * This method merely flags the task for termination and returns immediately. + * If the actual task is currently being executed, its execution will not be + * interrupted, but it will not be run again after finishing. + */ + public void terminate() { + synchronized (syncObject) { + if (isTerminated()) { + LOG.warn("Already terminated"); + } + + terminated = true; + syncObject.notify(); + } + } + + /** + * Terminates this task before its next execution cycle and waits for it to + * finish before returning. + * (This method waits for termination unless the calling thread is the thread + * that is executing this task. In that case, this method merely flags this + * task for termination and returns immediately.) + */ + public void terminateAndWait() { + Thread joinThread; + + synchronized (syncObject) { + if (isTerminated()) { + LOG.warn("Already terminated"); + return; + } + else { + joinThread = taskThread; + terminated = true; + syncObject.notify(); + } + } + // Wait for the executing thread to finish - unless the end of + // execution had already been reached or the executing thread is terminating + // this task itself. (In the latter case, we would wait forever for the + // join() to return.) + if (joinThread != null && joinThread != Thread.currentThread()) { + try { + joinThread.join(); + } + catch (InterruptedException exc) { + throw new IllegalStateException("Unexpectedly interrupted", exc); + } + } + } + + /** + * Indicates whether this task is ignoring interrupts while it's sleeping. + * + * @return true if, and only if, this task is ignoring interrupts + * while it's sleeping. + */ + public boolean isIgnoringInterrupts() { + return ignoringInterrupts; + } + + /** + * Sets/unsets this task's flag for ignoring interrupts during sleep phases. + * + * @param ignoreInterrupts If true, this task will ignore + * interrupts during sleep phases; if false, the + * run() method will throw an exception when interrupted. + */ + public void setIgnoringInterrupts(boolean ignoreInterrupts) { + ignoringInterrupts = ignoreInterrupts; + } + + @Override + public void run() { + // Save the executing thread for use in terminateAndWait(). + taskThread = Thread.currentThread(); + // Execute the actual task until terminated. + while (!isTerminated()) { + LOG.debug("Running actual task..."); + runActualTask(); + // Only sleep if this task is not terminated and the sleep time is not 0. + if (!isTerminated() && sleepTime > 0) { + synchronized (syncObject) { + try { + syncObject.wait(sleepTime); + } + catch (InterruptedException exc) { + if (!isIgnoringInterrupts() || isTerminated()) { + LOG.error("Unexpectedly interrupted", exc); + throw new IllegalStateException("Unexpectedly interrupted", exc); + } + } + } + } + } + // Unset taskThread again - this should prevent problems with join()ing + // threads (in terminateAndWait()) that execute more than one task + // subsequently. + taskThread = null; + } + + /** + * Defines the actual work this task should do in every cycle. + */ + protected abstract void runActualTask(); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/util/ExplainedBoolean.java b/opentcs-api-base/src/main/java/org/opentcs/util/ExplainedBoolean.java new file mode 100644 index 0000000..8bd8952 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/util/ExplainedBoolean.java @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; + +/** + * A boolean with an explanation/reason for its value. + */ +public class ExplainedBoolean { + + /** + * The actual value. + */ + private final boolean value; + /** + * A reason/explanation for the value. + */ + private final String reason; + + /** + * Creates a new instance. + * + * @param value The actual value. + * @param reason A reason/explanation for the value. + */ + public ExplainedBoolean( + boolean value, + @Nonnull + String reason + ) { + this.value = value; + this.reason = requireNonNull(reason, "reason"); + } + + /** + * Returns the actual value. + * + * @return The actual value. + */ + public boolean getValue() { + return value; + } + + /** + * A reason/explanation for the value. + * + * @return The reason + */ + @Nonnull + public String getReason() { + return reason; + } +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/util/annotations/ScheduledApiChange.java b/opentcs-api-base/src/main/java/org/opentcs/util/annotations/ScheduledApiChange.java new file mode 100644 index 0000000..662657d --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/util/annotations/ScheduledApiChange.java @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.annotations; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; + +import java.lang.annotation.Documented; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks an API detail (class, method, field) that is scheduled for an incompatible change. + *

+ * This annotation should not be used outside the source code of the openTCS project itself. It + * should not be considered part of the public API. + *

+ *

+ * This annotation is intended to be used as a supplement for @Deprecated for two purposes: + *

+ *
    + *
  1. For users of the openTCS API to gain knowledge about upcoming changes.
  2. + *
  3. For openTCS developers to easily find pending changes during pre-release cleanups.
  4. + *
+ */ +@Target({CONSTRUCTOR, FIELD, METHOD, TYPE}) +@Retention(RetentionPolicy.SOURCE) +@Documented +@Repeatable(ScheduledApiChanges.class) +public @interface ScheduledApiChange { + + /** + * Returns the date or version at which the API is scheduled to be changed. + * + * @return The date or version at which the API is scheduled to be changed. + */ + String when(); + + /** + * Returns optional details about the scheduled change. + * + * @return Optional details about the scheduled change. + */ + String details() default ""; +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/util/annotations/ScheduledApiChanges.java b/opentcs-api-base/src/main/java/org/opentcs/util/annotations/ScheduledApiChanges.java new file mode 100644 index 0000000..4aaaa5e --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/util/annotations/ScheduledApiChanges.java @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.annotations; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A container annotation for {@link ScheduledApiChange}. + */ +@Target({CONSTRUCTOR, FIELD, METHOD, TYPE}) +@Retention(RetentionPolicy.SOURCE) +@Documented +public @interface ScheduledApiChanges { + + /** + * Returns the contained schedules. + * + * @return The contained schedules. + */ + ScheduledApiChange[] value(); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/util/annotations/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/util/annotations/package-info.java new file mode 100644 index 0000000..8585990 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/util/annotations/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Useful annotations. + */ +package org.opentcs.util.annotations; diff --git a/opentcs-api-base/src/main/java/org/opentcs/util/event/EventBus.java b/opentcs-api-base/src/main/java/org/opentcs/util/event/EventBus.java new file mode 100644 index 0000000..23863b6 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/util/event/EventBus.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.event; + +/** + * A distributor of events. + * Forwards events received via {@link #onEvent(java.lang.Object)} to all subscribed handlers. + */ +public interface EventBus + extends + EventHandler, + EventSource { + +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/util/event/EventHandler.java b/opentcs-api-base/src/main/java/org/opentcs/util/event/EventHandler.java new file mode 100644 index 0000000..4e8a250 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/util/event/EventHandler.java @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.event; + +/** + * A handler for events emitted by an {@link EventSource}. + */ +public interface EventHandler { + + /** + * Processes the event object. + * + * @param event The event object. + */ + void onEvent(Object event); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/util/event/EventSource.java b/opentcs-api-base/src/main/java/org/opentcs/util/event/EventSource.java new file mode 100644 index 0000000..ec4c535 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/util/event/EventSource.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.event; + +/** + * A source of events that can be subscribed to. + */ +public interface EventSource { + + /** + * Subscribes the given listener to events emitted by this source. + * + * @param listener The listener to be subscribed. + */ + void subscribe(EventHandler listener); + + /** + * Unsubscribes the given listener. + * + * @param listener The listener to be unsubscribed. + */ + void unsubscribe(EventHandler listener); +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/util/event/SimpleEventBus.java b/opentcs-api-base/src/main/java/org/opentcs/util/event/SimpleEventBus.java new file mode 100644 index 0000000..fd48372 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/util/event/SimpleEventBus.java @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.event; + +import static java.util.Objects.requireNonNull; + +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A plain implementation of {@link EventBus}. + */ +public class SimpleEventBus + implements + EventBus { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(SimpleEventBus.class); + /** + * The listeners. + */ + private final Set listeners = new CopyOnWriteArraySet<>(); + + /** + * Creates a new instance. + */ + public SimpleEventBus() { + } + + @Override + public void onEvent(Object event) { + try { + for (EventHandler listener : listeners) { + listener.onEvent(event); + } + } + catch (Exception exc) { + LOG.warn("Exception thrown by event handler", exc); + } + } + + @Override + public void subscribe(EventHandler listener) { + requireNonNull(listener, "listener"); + + listeners.add(listener); + } + + @Override + public void unsubscribe(EventHandler listener) { + requireNonNull(listener, "listener"); + + listeners.remove(listener); + } + +} diff --git a/opentcs-api-base/src/main/java/org/opentcs/util/event/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/util/event/package-info.java new file mode 100644 index 0000000..2017b1c --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/util/event/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Interfaces and classes for event handling. + */ +package org.opentcs.util.event; diff --git a/opentcs-api-base/src/main/java/org/opentcs/util/package-info.java b/opentcs-api-base/src/main/java/org/opentcs/util/package-info.java new file mode 100644 index 0000000..0193dd6 --- /dev/null +++ b/opentcs-api-base/src/main/java/org/opentcs/util/package-info.java @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * A collection of classes providing miscellaneous functions that are used + * throughout openTCS. + */ +package org.opentcs.util; diff --git a/opentcs-api-base/src/main/java/overview.html b/opentcs-api-base/src/main/java/overview.html new file mode 100644 index 0000000..c241dd3 --- /dev/null +++ b/opentcs-api-base/src/main/java/overview.html @@ -0,0 +1,17 @@ + + + + + + + This is the description of the openTCS base API. + +

+ Tutorials/code examples can be found in the developer's guide that is also part of the + openTCS distribution. +

+ + diff --git a/opentcs-api-base/src/test/java/org/opentcs/access/rmi/services/RemoteServicesTest.java b/opentcs-api-base/src/test/java/org/opentcs/access/rmi/services/RemoteServicesTest.java new file mode 100644 index 0000000..dd221e8 --- /dev/null +++ b/opentcs-api-base/src/test/java/org/opentcs/access/rmi/services/RemoteServicesTest.java @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.rmi.services; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.fail; + +import java.lang.reflect.Method; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.components.kernel.services.NotificationService; +import org.opentcs.components.kernel.services.PeripheralDispatcherService; +import org.opentcs.components.kernel.services.PeripheralJobService; +import org.opentcs.components.kernel.services.PeripheralService; +import org.opentcs.components.kernel.services.PlantModelService; +import org.opentcs.components.kernel.services.QueryService; +import org.opentcs.components.kernel.services.RouterService; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.components.kernel.services.TransportOrderService; +import org.opentcs.components.kernel.services.VehicleService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tests for the remote service interfaces. + */ +class RemoteServicesTest { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(RemoteServicesTest.class); + + @Test + void shouldMapAllMethodsInServiceInterfaces() { + checkMapping(DispatcherService.class, RemoteDispatcherService.class); + checkMapping(NotificationService.class, RemoteNotificationService.class); + checkMapping(PeripheralDispatcherService.class, RemotePeripheralDispatcherService.class); + checkMapping(PeripheralJobService.class, RemotePeripheralJobService.class); + checkMapping(PeripheralService.class, RemotePeripheralService.class); + checkMapping(PlantModelService.class, RemotePlantModelService.class); + checkMapping(QueryService.class, RemoteQueryService.class); + checkMapping(RouterService.class, RemoteRouterService.class); + checkMapping(TCSObjectService.class, RemoteTCSObjectService.class); + checkMapping(TransportOrderService.class, RemoteTransportOrderService.class); + checkMapping(VehicleService.class, RemoteVehicleService.class); + } + + private void checkMapping(Class serviceInterface, Class remoteServiceInterface) { + List.of(serviceInterface.getDeclaredMethods()).stream() + // Only check methods whose names do not contain a dollar sign, as a dollar sign is a good + // indicator for generated methods. (One example for such occurrences would be methods + // generated by tools like JaCoCo, e.g. with interfaces that contain default implementations + // for some methods.) + .filter(method -> !method.getName().contains("$")) + .forEach(method -> { + try { + Method remoteMethod = getRemoteServiceMethod(remoteServiceInterface, method); + LOG.debug("Found {} corresponding to {}", remoteMethod, method); + } + catch (NoSuchMethodException exc) { + fail("Did not find corresponding method for: " + method); + } + }); + } + + private static Method getRemoteServiceMethod(Class remoteServiceInterface, Method method) + throws NoSuchMethodException { + requireNonNull(method, "method"); + + Class[] paramTypes = method.getParameterTypes(); + Class[] extParamTypes = new Class[paramTypes.length + 1]; + // We're looking for a method with the same parameter types as the called one, but with an + // additional client ID as the first parameter. + extParamTypes[0] = ClientID.class; + System.arraycopy(paramTypes, 0, extParamTypes, 1, paramTypes.length); + return remoteServiceInterface.getMethod(method.getName(), extParamTypes); + } +} diff --git a/opentcs-api-base/src/test/java/org/opentcs/access/to/model/PlantModelCreationTOTest.java b/opentcs-api-base/src/test/java/org/opentcs/access/to/model/PlantModelCreationTOTest.java new file mode 100644 index 0000000..d438c6c --- /dev/null +++ b/opentcs-api-base/src/test/java/org/opentcs/access/to/model/PlantModelCreationTOTest.java @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.access.to.model; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; +import static org.hamcrest.Matchers.samePropertyValuesAs; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.ModelConstants; +import org.opentcs.data.model.visualization.Layer; +import org.opentcs.data.model.visualization.LayerGroup; + +/** + * Tests for {@link PlantModelCreationTO}. + */ +class PlantModelCreationTOTest { + + private Layer defaultLayer; + private LayerGroup defaultLayerGroup; + + @BeforeEach + void setUp() { + defaultLayer = new Layer( + ModelConstants.DEFAULT_LAYER_ID, + ModelConstants.DEFAULT_LAYER_ORDINAL, + true, + ModelConstants.DEFAULT_LAYER_NAME, + ModelConstants.DEFAULT_LAYER_GROUP_ID + ); + + defaultLayerGroup = new LayerGroup( + ModelConstants.DEFAULT_LAYER_GROUP_ID, + ModelConstants.DEFAULT_LAYER_GROUP_NAME, + true + ); + } + + @Test + void addMissingLayerAndLayerGroupWhenAddingVisualLayout() { + VisualLayoutCreationTO visualLayout = new VisualLayoutCreationTO("V1"); + PlantModelCreationTO plantModel = new PlantModelCreationTO("name") + .withVisualLayout(visualLayout); + + assertThat(plantModel.getVisualLayout().getLayers(), hasSize(1)); + assertThat(plantModel.getVisualLayout().getLayerGroups(), hasSize(1)); + assertThat( + plantModel.getVisualLayout().getLayers().get(0), + samePropertyValuesAs(defaultLayer) + ); + assertThat( + plantModel.getVisualLayout().getLayerGroups().get(0), + samePropertyValuesAs(defaultLayerGroup) + ); + } + + @Test + void addMissingLayerWhenAddingVisualLayout() { + VisualLayoutCreationTO visualLayout = new VisualLayoutCreationTO("V1") + .withLayerGroup(defaultLayerGroup); + PlantModelCreationTO plantModel = new PlantModelCreationTO("name") + .withVisualLayout(visualLayout); + + assertThat(plantModel.getVisualLayout().getLayers(), hasSize(1)); + assertThat(plantModel.getVisualLayout().getLayerGroups(), hasSize(1)); + assertThat( + plantModel.getVisualLayout().getLayers().get(0), + samePropertyValuesAs(defaultLayer) + ); + assertThat( + plantModel.getVisualLayout().getLayerGroups().get(0), + is(sameInstance(defaultLayerGroup)) + ); + } + + @Test + void addMissingLayerGroupWhenAddingVisualLayout() { + VisualLayoutCreationTO visualLayout = new VisualLayoutCreationTO("V1") + .withLayer(defaultLayer); + PlantModelCreationTO plantModel = new PlantModelCreationTO("name") + .withVisualLayout(visualLayout); + + assertThat(plantModel.getVisualLayout().getLayers(), hasSize(1)); + assertThat(plantModel.getVisualLayout().getLayerGroups(), hasSize(1)); + assertThat( + plantModel.getVisualLayout().getLayers().get(0), + is(sameInstance(defaultLayer)) + ); + assertThat( + plantModel.getVisualLayout().getLayerGroups().get(0), + samePropertyValuesAs(defaultLayerGroup) + ); + } +} diff --git a/opentcs-api-base/src/test/java/org/opentcs/data/ObjectHistoryTest.java b/opentcs-api-base/src/test/java/org/opentcs/data/ObjectHistoryTest.java new file mode 100644 index 0000000..2c078a1 --- /dev/null +++ b/opentcs-api-base/src/test/java/org/opentcs/data/ObjectHistoryTest.java @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentcs.data.ObjectHistory.Entry; + +/** + * Unit tests for {@link ObjectHistory}. + */ +class ObjectHistoryTest { + + @Test + void checkIfObjectHistoryInitiallyIsEmpty() { + ObjectHistory history = new ObjectHistory(); + assertThat(history.getEntries(), hasSize(0)); + } + + @Test + void checkIfEntryListIsUnmodifiable() { + Entry entry1 = new Entry(Instant.now(), "eventCode1"); + Entry entry2 = new Entry(Instant.now(), "eventCode2"); + + ObjectHistory history = new ObjectHistory().withEntries(List.of(entry1)); + + assertThrows( + UnsupportedOperationException.class, + () -> history.getEntries().add(entry2) + ); + } + + @Test + void testObjectHistoryCreationWithEntries() { + Entry entry1 = new Entry(Instant.now(), "eventCode1"); + Entry entry2 = new Entry(Instant.now(), "eventCode2"); + + List entries = Arrays.asList(entry1, entry2); + + ObjectHistory history = new ObjectHistory().withEntries(entries); + + assertThat(history.getEntries(), hasSize(2)); + assertThat(history.getEntries(), contains(entry1, entry2)); + } + + @Test + void checkIfObjectHistoryWithEntryAppendedCreateAndAppendTheGivenEntriesCorrect() { + Entry entry1 = new Entry(Instant.now(), "eventCode1"); + Entry entry2 = new Entry(Instant.now(), "eventCode2"); + + ObjectHistory history = new ObjectHistory(); + + history = history.withEntryAppended(entry1); + history = history.withEntryAppended(entry2); + + assertThat(history.getEntries(), hasSize(2)); + assertThat(history.getEntries(), contains(entry1, entry2)); + } + + @Test + void checkIfEntryHasTimestampEventCodeAndSupplement() { + Instant timestamp = Instant.now(); + Entry entry = new Entry(timestamp, "eventCode1", "supplement"); + + assertEquals(entry.getTimestamp(), timestamp); + assertEquals(entry.getEventCode(), "eventCode1"); + assertEquals(entry.getSupplement(), "supplement"); + } + + @Test + void throwIfSupplementIsNotSerializable() { + assertThrows( + IllegalArgumentException.class, + () -> new Entry(Instant.now(), "eventCode", new Object()) + ); + } + + @Test + void checkIfEntryHasNoSupplement() { + Instant timestamp = Instant.now(); + Entry entry = new Entry(timestamp, "eventCode1"); + + assertEquals(entry.getTimestamp(), timestamp); + assertEquals(entry.getEventCode(), "eventCode1"); + assertEquals(entry.getSupplement(), ""); + } + + @Test + void checkIfEntryHasTimestamp() { + Entry entry = new Entry("eventCode1", "supplement"); + + assertTrue(entry.getTimestamp() != null); + assertEquals(entry.getEventCode(), "eventCode1"); + assertEquals(entry.getSupplement(), "supplement"); + } + + @Test + void checkIfEntryHasTimestampAnNoSupplement() { + Entry entry = new Entry("eventCode1"); + + assertTrue(entry.getTimestamp() != null); + assertEquals(entry.getEventCode(), "eventCode1"); + assertEquals(entry.getSupplement(), ""); + } + +} diff --git a/opentcs-api-base/src/test/java/org/opentcs/data/TCSObjectReferenceTest.java b/opentcs-api-base/src/test/java/org/opentcs/data/TCSObjectReferenceTest.java new file mode 100644 index 0000000..36c7996 --- /dev/null +++ b/opentcs-api-base/src/test/java/org/opentcs/data/TCSObjectReferenceTest.java @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link TCSObjectReference}. + */ +class TCSObjectReferenceTest { + + @Test + void considerReferenceForEquality() { + TestType1 object = new TestType1("some-name"); + + assertThat(object.getReference()) + .isEqualTo(object.getReference()) + .isNotEqualTo(null); + } + + @Test + void considerNameForEquality() { + assertThat(new TestType1("some-name").getReference()) + .isEqualTo(new TestType1("some-name").getReference()) + .isNotEqualTo(new TestType1("some-other-name").getReference()); + } + + @Test + void considerClassForEquality() { + assertThat(new TestType1("some-name").getReference()) + .isEqualTo(new TestType1("some-name").getReference()) + .isNotEqualTo(new TestType2("some-name").getReference()) + .isNotEqualTo(new Object()); + } + + @Test + void considerNameForHashCode() { + assertThat(new TestType1("some-name").getReference()) + .hasSameHashCodeAs(new TestType1("some-name").getReference()) + .doesNotHaveSameHashCodeAs(new TestType1("some-other-name").getReference()); + } + + private static class TestType1 + extends + TCSObject { + + TestType1(String objectName) { + super(objectName); + } + + TestType1(String objectName, Map properties, ObjectHistory history) { + super(objectName, properties, history); + } + + @Override + public TestType1 withProperty(String key, String value) { + return new TestType1(getName(), propertiesWith(key, value), getHistory()); + } + + @Override + public TestType1 withProperties(Map properties) { + return new TestType1(getName(), properties, getHistory()); + } + + @Override + public TestType1 withHistoryEntry(ObjectHistory.Entry entry) { + return new TestType1(getName(), getProperties(), getHistory().withEntryAppended(entry)); + } + + @Override + public TestType1 withHistory(ObjectHistory history) { + return new TestType1(getName(), getProperties(), history); + } + } + + private static class TestType2 + extends + TCSObject { + + TestType2(String objectName) { + super(objectName); + } + + TestType2(String objectName, Map properties, ObjectHistory history) { + super(objectName, properties, history); + } + + @Override + public TestType2 withProperty(String key, String value) { + return new TestType2(getName(), propertiesWith(key, value), getHistory()); + } + + @Override + public TestType2 withProperties(Map properties) { + return new TestType2(getName(), properties, getHistory()); + } + + @Override + public TestType2 withHistoryEntry(ObjectHistory.Entry entry) { + return new TestType2(getName(), getProperties(), getHistory().withEntryAppended(entry)); + } + + @Override + public TestType2 withHistory(ObjectHistory history) { + return new TestType2(getName(), getProperties(), history); + } + } +} diff --git a/opentcs-api-base/src/test/java/org/opentcs/data/TCSObjectTest.java b/opentcs-api-base/src/test/java/org/opentcs/data/TCSObjectTest.java new file mode 100644 index 0000000..c0f6f54 --- /dev/null +++ b/opentcs-api-base/src/test/java/org/opentcs/data/TCSObjectTest.java @@ -0,0 +1,201 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link TCSObject}. + */ +class TCSObjectTest { + + @Test + void considerReferenceForEquality() { + TestType1 object = new TestType1("some-name"); + + assertThat(object) + .isEqualTo(object) + .isNotEqualTo(null); + } + + @Test + void considerNameForEquality() { + assertThat(new TestType1("some-name")) + .isEqualTo(new TestType1("some-name")) + .isNotEqualTo(new TestType1("some-other-name")); + } + + @Test + void considerClassForEquality() { + assertThat(new TestType1("some-name")) + .isEqualTo(new TestType1("some-name")) + .isNotEqualTo(new TestType2("some-name")); + } + + @Test + void considerNameForHashCode() { + assertThat(new TestType1("some-name")) + .hasSameHashCodeAs(new TestType1("some-name")) + .doesNotHaveSameHashCodeAs(new TestType1("some-other-name")); + } + + @Test + void considerClassForHashCode() { + assertThat(new TestType1("some-name")) + .hasSameHashCodeAs(new TestType1("some-name")) + .doesNotHaveSameHashCodeAs(new TestType2("some-name")); + } + + @Test + void addProperty() { + TestType1 object = new TestType1("some-object") + .withProperty("some-key", "some-value"); + + assertThat(object.getProperty("some-key")) + .isEqualTo("some-value"); + assertThat(object.getProperties()) + .containsKey("some-key"); + } + + @Test + void removePropertyViaNullValue() { + TestType1 original = new TestType1("some-object") + .withProperty("some-key", "some-value"); + + TestType1 modified = original.withProperty("some-key", null); + + assertThat(modified.getProperty("some-key")) + .isNull(); + assertThat(modified.getProperties()) + .doesNotContainKey("some-key"); + } + + @Test + void filterNullValuesFromMap() { + Map input = new HashMap<>(); + input.put("one", "one"); + input.put("two", "two"); + input.put("null-1", null); + input.put("null-2", null); + input.put("three", "three"); + input.put("null-3", null); + + Map result = TCSObject.mapWithoutNullValues(input); + + assertThat(result) + .hasSize(3) + .containsOnlyKeys("one", "two", "three"); + } + + @Test + void filterNullValuesFromList() { + List result = TCSObject.listWithoutNullValues( + Arrays.asList( + "one", + "two", + null, + null, + "three", + null + ) + ); + + assertThat(result) + .hasSize(3) + .containsExactly("one", "two", "three"); + } + + @Test + void filterNullValuesFromSet() { + Set result = TCSObject.setWithoutNullValues( + new HashSet<>( + Arrays.asList( + "one", + "two", + null, + null, + "three", + null + ) + ) + ); + + assertThat(result) + .hasSize(3) + .containsExactlyInAnyOrder("one", "two", "three"); + } + + private static class TestType1 + extends + TCSObject { + + TestType1(String objectName) { + super(objectName); + } + + TestType1(String objectName, Map properties, ObjectHistory history) { + super(objectName, properties, history); + } + + @Override + public TestType1 withProperty(String key, String value) { + return new TestType1(getName(), propertiesWith(key, value), getHistory()); + } + + @Override + public TestType1 withProperties(Map properties) { + return new TestType1(getName(), properties, getHistory()); + } + + @Override + public TestType1 withHistoryEntry(ObjectHistory.Entry entry) { + return new TestType1(getName(), getProperties(), getHistory().withEntryAppended(entry)); + } + + @Override + public TestType1 withHistory(ObjectHistory history) { + return new TestType1(getName(), getProperties(), history); + } + } + + private static class TestType2 + extends + TCSObject { + + TestType2(String objectName) { + super(objectName); + } + + TestType2(String objectName, Map properties, ObjectHistory history) { + super(objectName, properties, history); + } + + @Override + public TestType2 withProperty(String key, String value) { + return new TestType2(getName(), propertiesWith(key, value), getHistory()); + } + + @Override + public TestType2 withProperties(Map properties) { + return new TestType2(getName(), properties, getHistory()); + } + + @Override + public TestType2 withHistoryEntry(ObjectHistory.Entry entry) { + return new TestType2(getName(), getProperties(), getHistory().withEntryAppended(entry)); + } + + @Override + public TestType2 withHistory(ObjectHistory history) { + return new TestType2(getName(), getProperties(), history); + } + } +} diff --git a/opentcs-api-base/src/test/java/org/opentcs/data/model/ModelSerializationTest.java b/opentcs-api-base/src/test/java/org/opentcs/data/model/ModelSerializationTest.java new file mode 100644 index 0000000..6becf08 --- /dev/null +++ b/opentcs-api-base/src/test/java/org/opentcs/data/model/ModelSerializationTest.java @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import org.junit.jupiter.api.Test; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectReference; + +/** + * Tests for proper serialization and deserialization of classes derived by TCSObject. + */ +class ModelSerializationTest { + + @Test + void shouldSerializeAndDeserializeBlock() + throws Exception { + Block originalObject = new Block("Block1"); + Block deserializedObject = (Block) deserializeTCSObject(serializeTCSObject(originalObject)); + + assertEquals(originalObject, deserializedObject); + } + + @Test + void shouldSerializeAndDeserializeLocation() + throws Exception { + @SuppressWarnings("unchecked") + Location originalObject = new Location("Location1", mock(TCSObjectReference.class)); + Location deserializedObject + = (Location) deserializeTCSObject(serializeTCSObject(originalObject)); + + assertEquals(originalObject, deserializedObject); + } + + @Test + void shouldSerializeAndDeserializeLocationType() + throws Exception { + LocationType originalObject = new LocationType("LocationType1"); + LocationType deserializedObject + = (LocationType) deserializeTCSObject(serializeTCSObject(originalObject)); + + assertEquals(originalObject, deserializedObject); + } + + @Test + void shouldSerializeAndDeserializePath() + throws Exception { + @SuppressWarnings("unchecked") + Path originalObject = new Path( + "Path1", + mock(TCSObjectReference.class), + mock(TCSObjectReference.class) + ); + Path deserializedObject = (Path) deserializeTCSObject(serializeTCSObject(originalObject)); + + assertEquals(originalObject, deserializedObject); + } + + @Test + void shouldSerializeAndDeserializePoint() + throws Exception { + Point originalObject = new Point("Point1"); + Point deserializedObject = (Point) deserializeTCSObject(serializeTCSObject(originalObject)); + + assertEquals(originalObject, deserializedObject); + } + + @Test + void shouldSerializeAndDeserializeVehicle() + throws Exception { + Vehicle originalObject = new Vehicle("Vehicle1"); + Vehicle deserializedObject = (Vehicle) deserializeTCSObject(serializeTCSObject(originalObject)); + + assertEquals(originalObject, deserializedObject); + } + + private byte[] serializeTCSObject(TCSObject tcsObject) + throws IOException { + byte[] serializedObject; + try (ByteArrayOutputStream os = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(os)) { + oos.writeObject(tcsObject); + serializedObject = os.toByteArray(); + } + return serializedObject; + } + + private TCSObject deserializeTCSObject(byte[] serializedObject) + throws IOException, + ClassNotFoundException { + TCSObject deserializedObject; + try (ByteArrayInputStream is = new ByteArrayInputStream(serializedObject); + ObjectInputStream ois = new ObjectInputStream(is)) { + deserializedObject = (TCSObject) ois.readObject(); + } + return deserializedObject; + } +} diff --git a/opentcs-api-base/src/test/java/org/opentcs/data/order/OrderSerializationTest.java b/opentcs-api-base/src/test/java/org/opentcs/data/order/OrderSerializationTest.java new file mode 100644 index 0000000..55f07b8 --- /dev/null +++ b/opentcs-api-base/src/test/java/org/opentcs/data/order/OrderSerializationTest.java @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.data.order; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; + +/** + * Tests for proper serialization and deserialization for TransportOrder and OrderSequence. + */ +class OrderSerializationTest { + + OrderSerializationTest() { + } + + @Test + void shouldSerializeAndDeserializeTransportOrder() + throws Exception { + TransportOrder originalObject = createTransportOrder(); + TransportOrder deserializedObject + = (TransportOrder) deserializeTCSObject(serializeTCSObject(originalObject)); + + assertEquals(originalObject, deserializedObject); + assertEquals(originalObject.getProperties(), deserializedObject.getProperties()); + assertTrue( + originalObject.getAllDriveOrders().get(0).getDestination() + .equals(deserializedObject.getAllDriveOrders().get(0).getDestination()) + ); + } + + @Test + void shouldSerializeAndDeserializeOrderSequence() + throws Exception { + OrderSequence originalObject = createOrderSequence(); + OrderSequence deserializedObject + = (OrderSequence) deserializeTCSObject(serializeTCSObject(originalObject)); + + assertEquals(originalObject, deserializedObject); + assertTrue( + originalObject.getOrders().get(0) + .equals(deserializedObject.getOrders().get(0)) + ); + } + + private TransportOrder createTransportOrder() { + List driveOrders = new ArrayList<>(); + @SuppressWarnings("unchecked") + Location location1 = new Location("Location1", mock(TCSObjectReference.class)); + @SuppressWarnings("unchecked") + Location location2 = new Location("Location2", mock(TCSObjectReference.class)); + driveOrders.add( + new DriveOrder( + new DriveOrder.Destination(location1.getReference()) + .withOperation("someOperation1") + ) + ); + driveOrders.add( + new DriveOrder( + new DriveOrder.Destination(location2.getReference()) + .withOperation("someOperation2") + ) + ); + TransportOrder transportOrder = new TransportOrder("TransportOrder", driveOrders) + .withProperty("someKey", "someValue"); + return transportOrder; + } + + private OrderSequence createOrderSequence() { + return new OrderSequence("OrderSequence") + .withOrder(createTransportOrder().getReference()); + } + + private byte[] serializeTCSObject(TCSObject tcsObject) + throws IOException { + byte[] serializedObject; + try (ByteArrayOutputStream os = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(os)) { + oos.writeObject(tcsObject); + serializedObject = os.toByteArray(); + } + + return serializedObject; + } + + private TCSObject deserializeTCSObject(byte[] serializedObject) + throws IOException, + ClassNotFoundException { + TCSObject deserializedObject; + try (ByteArrayInputStream is = new ByteArrayInputStream(serializedObject); + ObjectInputStream ois = new ObjectInputStream(is)) { + deserializedObject = (TCSObject) ois.readObject(); + } + return deserializedObject; + } +} diff --git a/opentcs-api-base/src/test/java/org/opentcs/drivers/vehicle/MovementCommandTest.java b/opentcs-api-base/src/test/java/org/opentcs/drivers/vehicle/MovementCommandTest.java new file mode 100644 index 0000000..2262fe4 --- /dev/null +++ b/opentcs-api-base/src/test/java/org/opentcs/drivers/vehicle/MovementCommandTest.java @@ -0,0 +1,201 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.drivers.vehicle; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; + +/** + * Test for {@link MovementCommand}. + */ +public class MovementCommandTest { + + private Point destinationPoint; + private Location location; + + @BeforeEach + public void setUp() { + destinationPoint = new Point("p1"); + location = new Location("L1", new LocationType("LT1").getReference()); + } + + @Test + void considerIdenticalMovementCommandsEqual() { + Route.Step stepAB = createStep( + "A", "B", + Vehicle.Orientation.FORWARD, + 0, true, null + ); + Route route = new Route(List.of(stepAB), 22); + + DriveOrder driveOrder + = new DriveOrder(new DriveOrder.Destination(destinationPoint.getReference())) + .withRoute(route); + TransportOrder transportOrder = new TransportOrder("some-order", List.of(driveOrder)); + + MovementCommand command = new MovementCommand( + transportOrder, + driveOrder, + stepAB, + "some-operation", + location, + false, + location, + destinationPoint, + "final-operation", + Map.of() + ); + + assertTrue(command.equalsInMovement(command)); + } + + @Test + void considerMovementCommandsWithDifferentStepsNotEqual() { + Route.Step stepAB = createStep( + "A", "B", + Vehicle.Orientation.FORWARD, + 0, true, null + ); + Route route = new Route(List.of(stepAB), 22); + + DriveOrder driveOrder + = new DriveOrder(new DriveOrder.Destination(destinationPoint.getReference())) + .withRoute(route); + TransportOrder transportOrder = new TransportOrder("some-order", List.of(driveOrder)); + + MovementCommand commandA = new MovementCommand( + transportOrder, + driveOrder, + stepAB, + "some-operation", + location, + false, + location, + destinationPoint, + "final-operation", + Map.of("a", "b") + ); + + Route.Step stepBC = createStep( + "B", "C", + Vehicle.Orientation.FORWARD, + 0, true, null + ); + Route route2 = new Route(List.of(stepBC), 22); + + driveOrder + = new DriveOrder(new DriveOrder.Destination(destinationPoint.getReference())) + .withRoute(route2); + transportOrder = new TransportOrder("some-order", List.of(driveOrder)); + + MovementCommand commandB = new MovementCommand( + transportOrder, + driveOrder, + stepBC, + "some-operation", + location, + false, + location, + destinationPoint, + "final-operation", + Map.of("a", "b") + ); + + assertFalse(commandA.equalsInMovement(commandB)); + } + + @Test + void considerMovementCommandsWithOperationNotEqual() { + Route.Step stepAB = createStep( + "A", "B", + Vehicle.Orientation.FORWARD, + 0, true, null + ); + Route route = new Route(List.of(stepAB), 22); + + DriveOrder driveOrder + = new DriveOrder(new DriveOrder.Destination(destinationPoint.getReference())) + .withRoute(route); + TransportOrder transportOrder = new TransportOrder("some-order", List.of(driveOrder)); + + MovementCommand commandA = new MovementCommand( + transportOrder, + driveOrder, + stepAB, + "operation-a", + location, + false, + location, + destinationPoint, + "final-operation", + Map.of("a", "b") + ); + + MovementCommand commandB = new MovementCommand( + transportOrder, + driveOrder, + stepAB, + "operation-b", + location, + false, + location, + destinationPoint, + "final-operation", + Map.of("a", "b") + ); + + assertFalse(commandA.equalsInMovement(commandB)); + } + + private Route.Step createStep( + @Nonnull + String srcPointName, + @Nonnull + String destPointName, + @Nonnull + Vehicle.Orientation orientation, + int routeIndex, + boolean executionAllowed, + @Nullable + ReroutingType reroutingType + ) { + requireNonNull(srcPointName, "srcPointName"); + requireNonNull(destPointName, "destPointName"); + requireNonNull(orientation, "orientation"); + + Point srcPoint = new Point(srcPointName); + Point destPoint = new Point(destPointName); + Path path = new Path( + srcPointName + "-" + destPointName, + srcPoint.getReference(), + destPoint.getReference() + ); + + return new Route.Step( + path, + srcPoint, + destPoint, + orientation, + routeIndex, + executionAllowed, + reroutingType + ); + } +} diff --git a/opentcs-api-base/src/test/java/org/opentcs/util/AssertionsTest.java b/opentcs-api-base/src/test/java/org/opentcs/util/AssertionsTest.java new file mode 100644 index 0000000..434c1ee --- /dev/null +++ b/opentcs-api-base/src/test/java/org/opentcs/util/AssertionsTest.java @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link Assertions}. + */ +class AssertionsTest { + + AssertionsTest() { + } + + @Test + void checkArgumentShouldFormatIntegerMessageTemplateArgument() { + try { + Assertions.checkArgument(false, "%s", 123); + } + catch (IllegalArgumentException exc) { + assertEquals("123", exc.getMessage()); + } + } + + @Test + void checkArgumentShouldThrowIfExpressionIsFalse() { + assertThrows( + IllegalArgumentException.class, + () -> Assertions.checkArgument(false, "The given expression is not true!") + ); + } + + @Test + void checkStateShouldFormatIntegerMessageTemplateArgument() { + try { + Assertions.checkState(false, "%s", 456); + } + catch (IllegalStateException exc) { + assertEquals("456", exc.getMessage()); + } + } + + @Test + void checkStateShouldThrowIfExpressionIsFalse() { + assertThrows( + IllegalStateException.class, + () -> Assertions.checkState(false, "The given expression is not true") + ); + } + + @Test + void checkInRangeShouldSucceedWithinBoundaries() { + assertEquals(22, Assertions.checkInRange(22, 22, 24)); + assertEquals(23, Assertions.checkInRange(23, 22, 24)); + assertEquals(24, Assertions.checkInRange(24, 22, 24)); + assertEquals( + Integer.MAX_VALUE, + Assertions.checkInRange(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE) + ); + assertEquals( + Integer.MIN_VALUE, + Assertions.checkInRange(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE) + ); + } + + @Test + void checkInRangeShouldFailOnLessThanMinimum() { + assertThrows( + IllegalArgumentException.class, + () -> Assertions.checkInRange(21, 22, 24) + ); + } + + @Test + void checkInRangeShouldFailOnMoreThanMaximum() { + assertThrows( + IllegalArgumentException.class, + () -> Assertions.checkInRange(25, 22, 24) + ); + } + + @Test + void checkInRangeLongShouldSucceedWithinBoundaries() { + assertEquals(23, Assertions.checkInRange(23, 23, 25)); + assertEquals(24, Assertions.checkInRange(24, 23, 25)); + assertEquals(25, Assertions.checkInRange(25, 23, 25)); + assertEquals( + Long.MIN_VALUE, + Assertions.checkInRange(Long.MIN_VALUE, Long.MIN_VALUE, Long.MIN_VALUE) + ); + assertEquals( + Long.MAX_VALUE, + Assertions.checkInRange(Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE) + ); + } + + @Test + void checkInRangeLongShouldFailOnLessThanMinimum() { + assertThrows( + IllegalArgumentException.class, + () -> Assertions.checkInRange(22, 23, 25) + ); + } + + @Test + void checkInRangeLongShouldFailOnMoreThanMaximum() { + assertThrows( + IllegalArgumentException.class, + () -> Assertions.checkInRange(26, 23, 25) + ); + } +} diff --git a/opentcs-api-base/src/test/java/org/opentcs/util/ClassMatcherTest.java b/opentcs-api-base/src/test/java/org/opentcs/util/ClassMatcherTest.java new file mode 100644 index 0000000..cb38b1c --- /dev/null +++ b/opentcs-api-base/src/test/java/org/opentcs/util/ClassMatcherTest.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; + +/** + * Unit tests for {@link ClassMatcher}. + */ +class ClassMatcherTest { + + @Test + void confirmGivenClasses() { + ClassMatcher classMatcher = new ClassMatcher(Point.class, Path.class); + + assertTrue(classMatcher.test(new Point("some-point"))); + assertTrue( + classMatcher.test( + new Path( + "some-path", + new Point("src-point").getReference(), + new Point("dst-point").getReference() + ) + ) + ); + } +} diff --git a/opentcs-api-base/src/test/java/org/opentcs/util/event/SimpleEventBusTest.java b/opentcs-api-base/src/test/java/org/opentcs/util/event/SimpleEventBusTest.java new file mode 100644 index 0000000..c0bc14e --- /dev/null +++ b/opentcs-api-base/src/test/java/org/opentcs/util/event/SimpleEventBusTest.java @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.event; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link SimpleEventBus}. + */ +class SimpleEventBusTest { + + private SimpleEventBus eventBus; + + @BeforeEach + void setUp() { + eventBus = new SimpleEventBus(); + } + + @Test + void forwardEventToSubscribers() { + List receivedObjects = new ArrayList<>(); + EventHandler eventHandler = (object) -> receivedObjects.add(object); + + eventBus.subscribe(eventHandler); + + eventBus.onEvent(new Object()); + + assertThat(receivedObjects, hasSize(1)); + + eventBus.unsubscribe(eventHandler); + receivedObjects.clear(); + + eventBus.onEvent(new Object()); + + assertThat(receivedObjects, is(empty())); + } +} diff --git a/opentcs-api-injection/build.gradle b/opentcs-api-injection/build.gradle new file mode 100644 index 0000000..36061f4 --- /dev/null +++ b/opentcs-api-injection/build.gradle @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply from: "${rootDir}/gradle/java-project.gradle" +apply from: "${rootDir}/gradle/java-codequality.gradle" +apply from: "${rootDir}/gradle/publishing-java.gradle" + +dependencies { + api project(':opentcs-api-base') + api group: 'com.google.inject', name: 'guice', version: '7.0.0' + api group: 'com.google.inject.extensions', name: 'guice-assistedinject', version: '7.0.0' +} + +task release { + dependsOn build +} diff --git a/opentcs-api-injection/gradle.properties b/opentcs-api-injection/gradle.properties new file mode 100644 index 0000000..14431ac --- /dev/null +++ b/opentcs-api-injection/gradle.properties @@ -0,0 +1,40 @@ +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineMethodParams=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAfterDotInChainedMethodCalls=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineDisjunctiveCatchTypes=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineFor=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineImplements=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.sortMembersByVisibility=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.visibilityOrder=PUBLIC;PROTECTED;DEFAULT;PRIVATE +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.enable-indent=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineArrayInit=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineCallArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.keepGettersAndSettersTogether=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classMembersOrder=STATIC FIELD;FIELD;STATIC_INIT;CONSTRUCTOR;INSTANCE_INIT;STATIC METHOD;METHOD;STATIC CLASS;CLASS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapCommentText=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.importGroupsOrder=* +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize=4 +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineAnnotationArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineTryResources=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.preserveNewLinesInComments=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineParenthesized=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineThrows=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=100 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project diff --git a/opentcs-api-injection/src/main/java/org/opentcs/customizations/ConfigurableInjectionModule.java b/opentcs-api-injection/src/main/java/org/opentcs/customizations/ConfigurableInjectionModule.java new file mode 100644 index 0000000..a50db23 --- /dev/null +++ b/opentcs-api-injection/src/main/java/org/opentcs/customizations/ConfigurableInjectionModule.java @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.customizations; + +import com.google.inject.AbstractModule; + +/** + * A base class for Guice modules adding or customizing bindings for the kernel application and the + * plant overview application. + */ +public abstract class ConfigurableInjectionModule + extends + AbstractModule { + + /** + * A provider for configuration bindings. + */ + private org.opentcs.configuration.ConfigurationBindingProvider configBindingProvider; + + /** + * Returns the configuration bindung provider. + * + * @return The configuration binding provider. + */ + public org.opentcs.configuration.ConfigurationBindingProvider getConfigBindingProvider() { + return configBindingProvider; + } + + /** + * Sets the configuration binding provider. + * + * @param configBindingProvider The new configuration binding provider. + */ + public void setConfigBindingProvider( + org.opentcs.configuration.ConfigurationBindingProvider configBindingProvider + ) { + this.configBindingProvider = configBindingProvider; + } +} diff --git a/opentcs-api-injection/src/main/java/org/opentcs/customizations/controlcenter/ControlCenterInjectionModule.java b/opentcs-api-injection/src/main/java/org/opentcs/customizations/controlcenter/ControlCenterInjectionModule.java new file mode 100644 index 0000000..8720fc4 --- /dev/null +++ b/opentcs-api-injection/src/main/java/org/opentcs/customizations/controlcenter/ControlCenterInjectionModule.java @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.customizations.controlcenter; + +import com.google.inject.multibindings.Multibinder; +import org.opentcs.components.kernelcontrolcenter.ControlCenterPanel; +import org.opentcs.customizations.ConfigurableInjectionModule; +import org.opentcs.drivers.peripherals.management.PeripheralCommAdapterPanelFactory; +import org.opentcs.drivers.vehicle.management.VehicleCommAdapterPanelFactory; + +/** + * A base class for Guice modules adding or customizing bindings for the kernel control center + * application. + */ +public abstract class ControlCenterInjectionModule + extends + ConfigurableInjectionModule { + + /** + * Returns a multibinder that can be used to register {@link ControlCenterPanel} implementations + * for the kernel's modelling mode. + * + * @return The multibinder. + */ + protected Multibinder controlCenterPanelBinderModelling() { + return Multibinder.newSetBinder( + binder(), + ControlCenterPanel.class, + ActiveInModellingMode.class + ); + } + + /** + * Returns a multibinder that can be used to register {@link ControlCenterPanel} implementations + * for the kernel's operating mode. + * + * @return The multibinder. + */ + protected Multibinder controlCenterPanelBinderOperating() { + return Multibinder.newSetBinder( + binder(), + ControlCenterPanel.class, + ActiveInOperatingMode.class + ); + } + + /** + * Returns a multibinder that can be used to register {@link VehicleCommAdapterPanelFactory} + * implementations. + * + * @return The multibinder. + */ + protected Multibinder commAdapterPanelFactoryBinder() { + return Multibinder.newSetBinder(binder(), VehicleCommAdapterPanelFactory.class); + } + + /** + * Returns a multibinder that can be used to register {@link PeripheralCommAdapterPanelFactory} + * implementations. + * + * @return The multibinder. + */ + protected Multibinder + peripheralCommAdapterPanelFactoryBinder() { + return Multibinder.newSetBinder(binder(), PeripheralCommAdapterPanelFactory.class); + } +} diff --git a/opentcs-api-injection/src/main/java/org/opentcs/customizations/controlcenter/package-info.java b/opentcs-api-injection/src/main/java/org/opentcs/customizations/controlcenter/package-info.java new file mode 100644 index 0000000..fafad86 --- /dev/null +++ b/opentcs-api-injection/src/main/java/org/opentcs/customizations/controlcenter/package-info.java @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Components supporting extension and customization of the openTCS kernel control center + * application. + */ +package org.opentcs.customizations.controlcenter; diff --git a/opentcs-api-injection/src/main/java/org/opentcs/customizations/kernel/KernelInjectionModule.java b/opentcs-api-injection/src/main/java/org/opentcs/customizations/kernel/KernelInjectionModule.java new file mode 100644 index 0000000..2677887 --- /dev/null +++ b/opentcs-api-injection/src/main/java/org/opentcs/customizations/kernel/KernelInjectionModule.java @@ -0,0 +1,170 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.customizations.kernel; + +import com.google.inject.Singleton; +import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.MapBinder; +import com.google.inject.multibindings.Multibinder; +import org.opentcs.components.kernel.Dispatcher; +import org.opentcs.components.kernel.KernelExtension; +import org.opentcs.components.kernel.OrderSequenceCleanupApproval; +import org.opentcs.components.kernel.PeripheralJobCleanupApproval; +import org.opentcs.components.kernel.PeripheralJobDispatcher; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.components.kernel.TransportOrderCleanupApproval; +import org.opentcs.components.kernel.routing.EdgeEvaluator; +import org.opentcs.customizations.ConfigurableInjectionModule; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterFactory; +import org.opentcs.drivers.vehicle.VehicleCommAdapterFactory; +import org.opentcs.drivers.vehicle.VehicleDataTransformerFactory; + +/** + * A base class for Guice modules adding or customizing bindings for the kernel application. + */ +public abstract class KernelInjectionModule + extends + ConfigurableInjectionModule { + + /** + * Sets the scheduler implementation to be used. + * + * @param clazz The implementation. + */ + protected void bindScheduler(Class clazz) { + bind(Scheduler.class).to(clazz).in(Singleton.class); + } + + /** + * Sets the router implementation to be used. + * + * @param clazz The implementation. + */ + protected void bindRouter(Class clazz) { + bind(Router.class).to(clazz).in(Singleton.class); + } + + /** + * Sets the dispatcher implementation to be used. + * + * @param clazz The implementation. + */ + protected void bindDispatcher(Class clazz) { + bind(Dispatcher.class).to(clazz).in(Singleton.class); + } + + /** + * Sets the peripheral job dispatcher implementation to be used. + * + * @param clazz The implementation. + */ + protected void bindPeripheralJobDispatcher(Class clazz) { + bind(PeripheralJobDispatcher.class).to(clazz).in(Singleton.class); + } + + /** + * Returns a multibinder that can be used to register kernel extensions for all kernel states. + * + * @return The multibinder. + */ + protected Multibinder extensionsBinderAllModes() { + return Multibinder.newSetBinder(binder(), KernelExtension.class, ActiveInAllModes.class); + } + + /** + * Returns a multibinder that can be used to register kernel extensions for the kernel's modelling + * state. + * + * @return The multibinder. + */ + protected Multibinder extensionsBinderModelling() { + return Multibinder.newSetBinder(binder(), KernelExtension.class, ActiveInModellingMode.class); + } + + /** + * Returns a multibinder that can be used to register kernel extensions for the kernel's operating + * state. + * + * @return The multibinder. + */ + protected Multibinder extensionsBinderOperating() { + return Multibinder.newSetBinder(binder(), KernelExtension.class, ActiveInOperatingMode.class); + } + + /** + * Returns a multibinder that can be used to register vehicle communication adapter factories. + * + * @return The multibinder. + */ + protected Multibinder vehicleCommAdaptersBinder() { + return Multibinder.newSetBinder(binder(), VehicleCommAdapterFactory.class); + } + + /** + * Returns a multibinder that can be used to register vehicle data transformer factories. + * + * @return The multibinder. + */ + protected Multibinder vehicleDataTransformersBinder() { + return Multibinder.newSetBinder(binder(), VehicleDataTransformerFactory.class); + } + + /** + * Returns a multibinder that can be used to register peripheral communication adapter factories. + * + * @return The multibinder. + */ + protected Multibinder peripheralCommAdaptersBinder() { + return Multibinder.newSetBinder(binder(), PeripheralCommAdapterFactory.class); + } + + /** + * Returns a multibinder that can be used to register transport order cleanup approvals. + * + * @return The multibinder. + */ + protected Multibinder transportOrderCleanupApprovalBinder() { + return Multibinder.newSetBinder(binder(), TransportOrderCleanupApproval.class); + } + + /** + * Returns a multibinder that can be used to register order sequence cleanup approvals. + * + * @return The multibinder. + */ + protected Multibinder orderSequenceCleanupApprovalBinder() { + return Multibinder.newSetBinder(binder(), OrderSequenceCleanupApproval.class); + } + + /** + * Returns a multibinder that can be used to register peripheral job cleanup approvals. + * + * @return The multibinder. + */ + protected Multibinder peripheralJobCleanupApprovalBinder() { + return Multibinder.newSetBinder(binder(), PeripheralJobCleanupApproval.class); + } + + /** + * Returns a multibinder that can be used to register scheduler modules. + * + * @return The multibinder. + */ + protected Multibinder schedulerModuleBinder() { + return Multibinder.newSetBinder(binder(), Scheduler.Module.class); + } + + /** + * Returns a mapbinder that can be used to register edge evaluators. + * + * @return The mapbinder. + */ + protected MapBinder edgeEvaluatorBinder() { + return MapBinder.newMapBinder( + binder(), + TypeLiteral.get(String.class), + TypeLiteral.get(EdgeEvaluator.class) + ); + } +} diff --git a/opentcs-api-injection/src/main/java/org/opentcs/customizations/kernel/package-info.java b/opentcs-api-injection/src/main/java/org/opentcs/customizations/kernel/package-info.java new file mode 100644 index 0000000..4b8bac2 --- /dev/null +++ b/opentcs-api-injection/src/main/java/org/opentcs/customizations/kernel/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Components supporting extension and customization of the openTCS kernel application. + */ +package org.opentcs.customizations.kernel; diff --git a/opentcs-api-injection/src/main/java/org/opentcs/customizations/package-info.java b/opentcs-api-injection/src/main/java/org/opentcs/customizations/package-info.java new file mode 100644 index 0000000..269a5cc --- /dev/null +++ b/opentcs-api-injection/src/main/java/org/opentcs/customizations/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Classes utilized for extending and customizing openTCS applications. + */ +package org.opentcs.customizations; diff --git a/opentcs-api-injection/src/main/java/org/opentcs/customizations/plantoverview/PlantOverviewInjectionModule.java b/opentcs-api-injection/src/main/java/org/opentcs/customizations/plantoverview/PlantOverviewInjectionModule.java new file mode 100644 index 0000000..b4cff7d --- /dev/null +++ b/opentcs-api-injection/src/main/java/org/opentcs/customizations/plantoverview/PlantOverviewInjectionModule.java @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.customizations.plantoverview; + +import com.google.inject.multibindings.Multibinder; +import org.opentcs.components.plantoverview.ObjectHistoryEntryFormatter; +import org.opentcs.components.plantoverview.OrderTypeSuggestions; +import org.opentcs.components.plantoverview.PlantModelExporter; +import org.opentcs.components.plantoverview.PlantModelImporter; +import org.opentcs.components.plantoverview.PluggablePanelFactory; +import org.opentcs.components.plantoverview.PropertySuggestions; +import org.opentcs.customizations.ConfigurableInjectionModule; + +/** + * A base class for Guice modules adding or customizing bindings for the plant overview application. + */ +public abstract class PlantOverviewInjectionModule + extends + ConfigurableInjectionModule { + + /** + * Returns a multibinder that can be used to register plant model importers. + * + * @return The multibinder. + */ + protected Multibinder plantModelImporterBinder() { + return Multibinder.newSetBinder(binder(), PlantModelImporter.class); + } + + /** + * Returns a multibinder that can be used to register plant model exporters. + * + * @return The multibinder. + */ + protected Multibinder plantModelExporterBinder() { + return Multibinder.newSetBinder(binder(), PlantModelExporter.class); + } + + /** + * Returns a multibinder that can be used to register factories for pluggable panels. + * + * @return The multibinder. + */ + protected Multibinder pluggablePanelFactoryBinder() { + return Multibinder.newSetBinder(binder(), PluggablePanelFactory.class); + } + + /** + * Returns a multibinder that can be used to register classes that provide suggested properties. + * + * @return The multibinder. + */ + protected Multibinder propertySuggestionsBinder() { + return Multibinder.newSetBinder(binder(), PropertySuggestions.class); + } + + /** + * Returns a multibinder that can be used to register classes that provide suggested order types. + * + * @return The multibinder. + */ + protected Multibinder orderTypeSuggestionsBinder() { + return Multibinder.newSetBinder(binder(), OrderTypeSuggestions.class); + } + + /** + * Returns a multibinder that can be used to register {@link ObjectHistoryEntryFormatter}s. + * + * @return The multibinder. + */ + protected Multibinder objectHistoryEntryFormatterBinder() { + return Multibinder.newSetBinder(binder(), ObjectHistoryEntryFormatter.class); + } +} diff --git a/opentcs-api-injection/src/main/java/org/opentcs/customizations/plantoverview/package-info.java b/opentcs-api-injection/src/main/java/org/opentcs/customizations/plantoverview/package-info.java new file mode 100644 index 0000000..cec88ef --- /dev/null +++ b/opentcs-api-injection/src/main/java/org/opentcs/customizations/plantoverview/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Components supporting extension and customization of the openTCS plant overview application. + */ +package org.opentcs.customizations.plantoverview; diff --git a/opentcs-api-injection/src/main/java/overview.html b/opentcs-api-injection/src/main/java/overview.html new file mode 100644 index 0000000..25acc07 --- /dev/null +++ b/opentcs-api-injection/src/main/java/overview.html @@ -0,0 +1,17 @@ + + + + + + + This is the description of the openTCS injection API. + +

+ Tutorials/code examples can be found in the developer's guide that is also part of the + openTCS distribution. +

+ + diff --git a/opentcs-commadapter-loopback/build.gradle b/opentcs-commadapter-loopback/build.gradle new file mode 100644 index 0000000..f6741e7 --- /dev/null +++ b/opentcs-commadapter-loopback/build.gradle @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply from: "${rootDir}/gradle/java-project.gradle" +apply from: "${rootDir}/gradle/java-codequality.gradle" +apply from: "${rootDir}/gradle/guice-project.gradle" +apply from: "${rootDir}/gradle/publishing-java.gradle" + +dependencies { + api project(':opentcs-api-injection') + api project(':opentcs-common') +} + +task release { + dependsOn build +} diff --git a/opentcs-commadapter-loopback/gradle.properties b/opentcs-commadapter-loopback/gradle.properties new file mode 100644 index 0000000..14431ac --- /dev/null +++ b/opentcs-commadapter-loopback/gradle.properties @@ -0,0 +1,40 @@ +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineMethodParams=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAfterDotInChainedMethodCalls=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineDisjunctiveCatchTypes=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineFor=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineImplements=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.sortMembersByVisibility=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.visibilityOrder=PUBLIC;PROTECTED;DEFAULT;PRIVATE +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.enable-indent=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineArrayInit=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineCallArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.keepGettersAndSettersTogether=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classMembersOrder=STATIC FIELD;FIELD;STATIC_INIT;CONSTRUCTOR;INSTANCE_INIT;STATIC METHOD;METHOD;STATIC CLASS;CLASS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapCommentText=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.importGroupsOrder=* +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize=4 +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineAnnotationArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineTryResources=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.preserveNewLinesInComments=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineParenthesized=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineThrows=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=100 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project diff --git a/opentcs-commadapter-loopback/src/guiceConfig/java/org/opentcs/virtualvehicle/LoopbackCommAdapterModule.java b/opentcs-commadapter-loopback/src/guiceConfig/java/org/opentcs/virtualvehicle/LoopbackCommAdapterModule.java new file mode 100644 index 0000000..b68ccb0 --- /dev/null +++ b/opentcs-commadapter-loopback/src/guiceConfig/java/org/opentcs/virtualvehicle/LoopbackCommAdapterModule.java @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle; + +import com.google.inject.assistedinject.FactoryModuleBuilder; +import org.opentcs.customizations.kernel.KernelInjectionModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Configures/binds the loopback communication adapters of the openTCS kernel. + */ +public class LoopbackCommAdapterModule + extends + KernelInjectionModule { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(LoopbackCommAdapterModule.class); + + /** + * Creates a new instance. + */ + public LoopbackCommAdapterModule() { + } + + @Override + protected void configure() { + VirtualVehicleConfiguration configuration + = getConfigBindingProvider().get( + VirtualVehicleConfiguration.PREFIX, + VirtualVehicleConfiguration.class + ); + + if (!configuration.enable()) { + LOG.info("Loopback driver disabled by configuration."); + return; + } + + bind(VirtualVehicleConfiguration.class) + .toInstance(configuration); + + install(new FactoryModuleBuilder().build(LoopbackAdapterComponentsFactory.class)); + + // tag::documentation_createCommAdapterModule[] + vehicleCommAdaptersBinder().addBinding().to(LoopbackCommunicationAdapterFactory.class); + // end::documentation_createCommAdapterModule[] + } + +} diff --git a/opentcs-commadapter-loopback/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.kernel.KernelInjectionModule b/opentcs-commadapter-loopback/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.kernel.KernelInjectionModule new file mode 100644 index 0000000..bf0777b --- /dev/null +++ b/opentcs-commadapter-loopback/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.kernel.KernelInjectionModule @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT + +org.opentcs.virtualvehicle.LoopbackCommAdapterModule diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/AdapterPanelComponentsFactory.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/AdapterPanelComponentsFactory.java new file mode 100644 index 0000000..e884701 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/AdapterPanelComponentsFactory.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle; + +import org.opentcs.components.kernel.services.VehicleService; + +/** + * A factory for creating various comm adapter panel specific instances. + */ +public interface AdapterPanelComponentsFactory { + + /** + * Creates a {@link LoopbackCommAdapterPanel} representing the given process model's content. + * + * @param processModel The process model to represent. + * @param vehicleService The vehicle service used for interaction with the comm adapter. + * @return The comm adapter panel. + */ + LoopbackCommAdapterPanel createPanel( + LoopbackVehicleModelTO processModel, + VehicleService vehicleService + ); +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/I18nLoopbackCommAdapter.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/I18nLoopbackCommAdapter.java new file mode 100644 index 0000000..0fad87d --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/I18nLoopbackCommAdapter.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle; + +/** + * Defines constants regarding internationalization. + */ +public interface I18nLoopbackCommAdapter { + + /** + * The path to the project's resource bundle. + */ + String BUNDLE_PATH = "i18n/org/opentcs/commadapter/loopback/Bundle"; +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackAdapterComponentsFactory.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackAdapterComponentsFactory.java new file mode 100644 index 0000000..92585fb --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackAdapterComponentsFactory.java @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle; + +import org.opentcs.data.model.Vehicle; + +/** + * A factory for various loopback specific instances. + */ +public interface LoopbackAdapterComponentsFactory { + + /** + * Creates a new LoopbackCommunicationAdapter for the given vehicle. + * + * @param vehicle The vehicle. + * @return A new LoopbackCommunicationAdapter for the given vehicle. + */ + LoopbackCommunicationAdapter createLoopbackCommAdapter(Vehicle vehicle); +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackCommAdapterPanel.form b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackCommAdapterPanel.form new file mode 100644 index 0000000..10fff84 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackCommAdapterPanel.form @@ -0,0 +1,961 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackCommAdapterPanel.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackCommAdapterPanel.java new file mode 100644 index 0000000..16d207f --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackCommAdapterPanel.java @@ -0,0 +1,1424 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.virtualvehicle.I18nLoopbackCommAdapter.BUNDLE_PATH; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.ResourceBundle; +import java.util.Set; +import javax.swing.SwingUtilities; +import org.opentcs.components.kernel.services.VehicleService; +import org.opentcs.customizations.ServiceCallWrapper; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Triple; +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.LoadHandlingDevice; +import org.opentcs.drivers.vehicle.VehicleCommAdapterEvent; +import org.opentcs.drivers.vehicle.VehicleProcessModel; +import org.opentcs.drivers.vehicle.management.VehicleCommAdapterPanel; +import org.opentcs.drivers.vehicle.management.VehicleProcessModelTO; +import org.opentcs.util.CallWrapper; +import org.opentcs.util.Comparators; +import org.opentcs.util.gui.StringListCellRenderer; +import org.opentcs.virtualvehicle.commands.CurrentMovementCommandFailedCommand; +import org.opentcs.virtualvehicle.commands.PublishEventCommand; +import org.opentcs.virtualvehicle.commands.SetEnergyLevelCommand; +import org.opentcs.virtualvehicle.commands.SetLoadHandlingDevicesCommand; +import org.opentcs.virtualvehicle.commands.SetOrientationAngleCommand; +import org.opentcs.virtualvehicle.commands.SetPositionCommand; +import org.opentcs.virtualvehicle.commands.SetPrecisePositionCommand; +import org.opentcs.virtualvehicle.commands.SetSingleStepModeEnabledCommand; +import org.opentcs.virtualvehicle.commands.SetStateCommand; +import org.opentcs.virtualvehicle.commands.SetVehiclePausedCommand; +import org.opentcs.virtualvehicle.commands.SetVehiclePropertyCommand; +import org.opentcs.virtualvehicle.commands.TriggerCommand; +import org.opentcs.virtualvehicle.inputcomponents.DropdownListInputPanel; +import org.opentcs.virtualvehicle.inputcomponents.InputDialog; +import org.opentcs.virtualvehicle.inputcomponents.InputPanel; +import org.opentcs.virtualvehicle.inputcomponents.SingleTextInputPanel; +import org.opentcs.virtualvehicle.inputcomponents.TextInputPanel; +import org.opentcs.virtualvehicle.inputcomponents.TripleTextInputPanel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The panel corresponding to the LoopbackCommunicationAdapter. + */ +public class LoopbackCommAdapterPanel + extends + VehicleCommAdapterPanel { + + /** + * The resource bundle. + */ + private static final ResourceBundle BUNDLE = ResourceBundle.getBundle(BUNDLE_PATH); + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(LoopbackCommAdapterPanel.class); + /** + * The vehicle service used for interaction with the comm adapter. + */ + private final VehicleService vehicleService; + /** + * The comm adapter's process model. + */ + private LoopbackVehicleModelTO processModel; + /** + * The call wrapper to use for service calls. + */ + private final CallWrapper callWrapper; + + /** + * Creates new LoopbackCommunicationAdapterPanel. + * + * @param processModel The comm adapter's process model. + * @param vehicleService The vehicle service. + * @param callWrapper The call wrapper to use for service calls. + */ + @Inject + @SuppressWarnings("this-escape") + public LoopbackCommAdapterPanel( + @Assisted + LoopbackVehicleModelTO processModel, + @Assisted + VehicleService vehicleService, + @ServiceCallWrapper + CallWrapper callWrapper + ) { + + this.processModel = requireNonNull(processModel, "processModel"); + this.vehicleService = requireNonNull(vehicleService, "vehicleService"); + this.callWrapper = requireNonNull(callWrapper, "callWrapper"); + + initComponents(); + initGuiContent(); + } + + @Override + public void processModelChange(String attributeChanged, VehicleProcessModelTO newProcessModel) { + if (!(newProcessModel instanceof LoopbackVehicleModelTO)) { + return; + } + + processModel = (LoopbackVehicleModelTO) newProcessModel; + updateLoopbackVehicleModelData(attributeChanged, processModel); + updateVehicleProcessModelData(attributeChanged, processModel); + } + + private void initGuiContent() { + for (VehicleProcessModel.Attribute attribute : VehicleProcessModel.Attribute.values()) { + processModelChange(attribute.name(), processModel); + } + for (LoopbackVehicleModel.Attribute attribute : LoopbackVehicleModel.Attribute.values()) { + processModelChange(attribute.name(), processModel); + } + } + + private void updateLoopbackVehicleModelData( + String attributeChanged, + LoopbackVehicleModelTO processModel + ) { + if (Objects.equals( + attributeChanged, + LoopbackVehicleModel.Attribute.OPERATING_TIME.name() + )) { + updateOperatingTime(processModel.getOperatingTime()); + } + else if (Objects.equals( + attributeChanged, + LoopbackVehicleModel.Attribute.ACCELERATION.name() + )) { + updateMaxAcceleration(processModel.getMaxAcceleration()); + } + else if (Objects.equals( + attributeChanged, + LoopbackVehicleModel.Attribute.DECELERATION.name() + )) { + updateMaxDeceleration(processModel.getMaxDeceleration()); + } + else if (Objects.equals( + attributeChanged, + LoopbackVehicleModel.Attribute.MAX_FORWARD_VELOCITY.name() + )) { + updateMaxForwardVelocity(processModel.getMaxFwdVelocity()); + } + else if (Objects.equals( + attributeChanged, + LoopbackVehicleModel.Attribute.MAX_REVERSE_VELOCITY.name() + )) { + updateMaxReverseVelocity(processModel.getMaxRevVelocity()); + } + else if (Objects.equals( + attributeChanged, + LoopbackVehicleModel.Attribute.SINGLE_STEP_MODE.name() + )) { + updateSingleStepMode(processModel.isSingleStepModeEnabled()); + } + else if (Objects.equals( + attributeChanged, + LoopbackVehicleModel.Attribute.VEHICLE_PAUSED.name() + )) { + updateVehiclePaused(processModel.isVehiclePaused()); + } + } + + private void updateVehicleProcessModelData( + String attributeChanged, + VehicleProcessModelTO processModel + ) { + if (Objects.equals( + attributeChanged, + VehicleProcessModel.Attribute.COMM_ADAPTER_ENABLED.name() + )) { + updateCommAdapterEnabled(processModel.isCommAdapterEnabled()); + } + else if (Objects.equals( + attributeChanged, + VehicleProcessModel.Attribute.POSITION.name() + )) { + updatePosition(processModel.getPosition()); + } + else if (Objects.equals( + attributeChanged, + VehicleProcessModel.Attribute.STATE.name() + )) { + updateVehicleState(processModel.getState()); + } + else if (Objects.equals( + attributeChanged, + VehicleProcessModel.Attribute.POSE.name() + )) { + updatePrecisePosition(processModel.getPose().getPosition()); + updateOrientationAngle(processModel.getPose().getOrientationAngle()); + } + else if (Objects.equals( + attributeChanged, + VehicleProcessModel.Attribute.ENERGY_LEVEL.name() + )) { + updateEnergyLevel(processModel.getEnergyLevel()); + } + else if (Objects.equals( + attributeChanged, + VehicleProcessModel.Attribute.LOAD_HANDLING_DEVICES.name() + )) { + updateVehicleLoadHandlingDevice(processModel.getLoadHandlingDevices()); + } + } + + private void updateVehicleLoadHandlingDevice(List devices) { + if (devices.size() > 1) { + LOG.warn("size of load handling devices greater than 1 ({})", devices.size()); + } + boolean loaded = devices.stream() + .findFirst() + .map(lhd -> lhd.isFull()) + .orElse(false); + SwingUtilities.invokeLater(() -> lHDCheckbox.setSelected(loaded)); + } + + private void updateEnergyLevel(int energy) { + SwingUtilities.invokeLater(() -> energyLevelTxt.setText(Integer.toString(energy))); + } + + private void updateCommAdapterEnabled(boolean isEnabled) { + SwingUtilities.invokeLater(() -> { + setStatePanelEnabled(isEnabled); + chkBoxEnable.setSelected(isEnabled); + }); + } + + private void updatePosition(String vehiclePosition) { + SwingUtilities.invokeLater(() -> { + if (vehiclePosition == null) { + positionTxt.setText(""); + return; + } + + try { + for (Point curPoint : callWrapper.call(() -> vehicleService.fetchObjects(Point.class))) { + if (curPoint.getName().equals(vehiclePosition)) { + positionTxt.setText(curPoint.getName()); + break; + } + } + } + catch (Exception ex) { + LOG.warn("Error fetching points", ex); + } + }); + } + + private void updateVehicleState(Vehicle.State vehicleState) { + SwingUtilities.invokeLater(() -> stateTxt.setText(vehicleState.toString())); + } + + private void updatePrecisePosition(Triple precisePos) { + SwingUtilities.invokeLater(() -> setPrecisePosText(precisePos)); + } + + private void updateOrientationAngle(double orientation) { + SwingUtilities.invokeLater(() -> { + if (Double.isNaN(orientation)) { + orientationAngleTxt.setText( + BUNDLE.getString( + "loopbackCommAdapterPanel.textField_orientationAngle.angleNotSetPlaceholder" + ) + ); + } + else { + orientationAngleTxt.setText(Double.toString(orientation)); + } + }); + } + + private void updateOperatingTime(int defaultOperatingTime) { + SwingUtilities.invokeLater(() -> opTimeTxt.setText(Integer.toString(defaultOperatingTime))); + } + + private void updateMaxAcceleration(int maxAcceleration) { + SwingUtilities.invokeLater(() -> maxAccelTxt.setText(Integer.toString(maxAcceleration))); + } + + private void updateMaxDeceleration(int maxDeceleration) { + SwingUtilities.invokeLater(() -> maxDecelTxt.setText(Integer.toString(maxDeceleration))); + } + + private void updateMaxForwardVelocity(int maxFwdVelocity) { + SwingUtilities.invokeLater(() -> maxFwdVeloTxt.setText(Integer.toString(maxFwdVelocity))); + } + + private void updateMaxReverseVelocity(int maxRevVelocity) { + SwingUtilities.invokeLater(() -> maxRevVeloTxt.setText(Integer.toString(maxRevVelocity))); + } + + private void updateSingleStepMode(boolean singleStepMode) { + SwingUtilities.invokeLater(() -> { + triggerButton.setEnabled(singleStepMode); + singleModeRadioButton.setSelected(singleStepMode); + flowModeRadioButton.setSelected(!singleStepMode); + }); + } + + private void updateVehiclePaused(boolean isVehiclePaused) { + SwingUtilities.invokeLater(() -> pauseVehicleCheckBox.setSelected(isVehiclePaused)); + } + + /** + * Enable/disable the input fields and buttons in the "Current position/state" panel. + * If disabled the user can not change any values or modify the vehicles state. + * + * @param enabled boolean indicating if the panel should be enabled + */ + private void setStatePanelEnabled(boolean enabled) { + SwingUtilities.invokeLater(() -> positionTxt.setEnabled(enabled)); + SwingUtilities.invokeLater(() -> stateTxt.setEnabled(enabled)); + SwingUtilities.invokeLater(() -> energyLevelTxt.setEnabled(enabled)); + SwingUtilities.invokeLater(() -> precisePosTextArea.setEnabled(enabled)); + SwingUtilities.invokeLater(() -> orientationAngleTxt.setEnabled(enabled)); + SwingUtilities.invokeLater(() -> pauseVehicleCheckBox.setEnabled(enabled)); + } + + private TCSObjectReference getVehicleReference() + throws Exception { + return callWrapper.call(() -> vehicleService.fetchObject(Vehicle.class, processModel.getName())) + .getReference(); + } + + private void sendCommAdapterCommand(AdapterCommand command) { + try { + TCSObjectReference vehicleRef = getVehicleReference(); + callWrapper.call(() -> vehicleService.sendCommAdapterCommand(vehicleRef, command)); + } + catch (Exception ex) { + LOG.warn("Error sending comm adapter command '{}'", command, ex); + } + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + modeButtonGroup = new javax.swing.ButtonGroup(); + propertyEditorGroup = new javax.swing.ButtonGroup(); + vehicleBahaviourPanel = new javax.swing.JPanel(); + PropsPowerOuterContainerPanel = new javax.swing.JPanel(); + PropsPowerInnerContainerPanel = new javax.swing.JPanel(); + vehiclePropsPanel = new javax.swing.JPanel(); + maxFwdVeloLbl = new javax.swing.JLabel(); + maxFwdVeloTxt = new javax.swing.JTextField(); + maxFwdVeloUnitLbl = new javax.swing.JLabel(); + maxRevVeloLbl = new javax.swing.JLabel(); + maxRevVeloTxt = new javax.swing.JTextField(); + maxRevVeloUnitLbl = new javax.swing.JLabel(); + maxAccelLbl = new javax.swing.JLabel(); + maxAccelTxt = new javax.swing.JTextField(); + maxAccelUnitLbl = new javax.swing.JLabel(); + maxDecelTxt = new javax.swing.JTextField(); + maxDecelLbl = new javax.swing.JLabel(); + maxDecelUnitLbl = new javax.swing.JLabel(); + defaultOpTimeLbl = new javax.swing.JLabel(); + defaultOpTimeUntiLbl = new javax.swing.JLabel(); + opTimeTxt = new javax.swing.JTextField(); + profilesContainerPanel = new javax.swing.JPanel(); + filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 0)); + vehicleStatePanel = new javax.swing.JPanel(); + stateContainerPanel = new javax.swing.JPanel(); + connectionPanel = new javax.swing.JPanel(); + chkBoxEnable = new javax.swing.JCheckBox(); + curPosPanel = new javax.swing.JPanel(); + energyLevelTxt = new javax.swing.JTextField(); + energyLevelLbl = new javax.swing.JLabel(); + pauseVehicleCheckBox = new javax.swing.JCheckBox(); + orientationAngleLbl = new javax.swing.JLabel(); + precisePosUnitLabel = new javax.swing.JLabel(); + orientationAngleTxt = new javax.swing.JTextField(); + energyLevelLabel = new javax.swing.JLabel(); + orientationLabel = new javax.swing.JLabel(); + positionTxt = new javax.swing.JTextField(); + positionLabel = new javax.swing.JLabel(); + pauseVehicleLabel = new javax.swing.JLabel(); + jLabel2 = new javax.swing.JLabel(); + stateTxt = new javax.swing.JTextField(); + jLabel3 = new javax.swing.JLabel(); + precisePosTextArea = new javax.swing.JTextArea(); + propertySetterPanel = new javax.swing.JPanel(); + keyLabel = new javax.swing.JLabel(); + valueTextField = new javax.swing.JTextField(); + propSetButton = new javax.swing.JButton(); + removePropRadioBtn = new javax.swing.JRadioButton(); + setPropValueRadioBtn = new javax.swing.JRadioButton(); + jPanel3 = new javax.swing.JPanel(); + keyTextField = new javax.swing.JTextField(); + eventPanel = new javax.swing.JPanel(); + includeAppendixCheckBox = new javax.swing.JCheckBox(); + appendixTxt = new javax.swing.JTextField(); + dispatchEventButton = new javax.swing.JButton(); + dispatchCommandFailedButton = new javax.swing.JButton(); + controlTabPanel = new javax.swing.JPanel(); + singleModeRadioButton = new javax.swing.JRadioButton(); + flowModeRadioButton = new javax.swing.JRadioButton(); + triggerButton = new javax.swing.JButton(); + loadDevicePanel = new javax.swing.JPanel(); + jPanel1 = new javax.swing.JPanel(); + jPanel2 = new javax.swing.JPanel(); + lHDCheckbox = new javax.swing.JCheckBox(); + + setName("LoopbackCommunicationAdapterPanel"); // NOI18N + setLayout(new java.awt.BorderLayout()); + + vehicleBahaviourPanel.setLayout(new java.awt.BorderLayout()); + + PropsPowerOuterContainerPanel.setLayout(new java.awt.BorderLayout()); + + PropsPowerInnerContainerPanel.setLayout(new javax.swing.BoxLayout(PropsPowerInnerContainerPanel, javax.swing.BoxLayout.X_AXIS)); + + vehiclePropsPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(BUNDLE.getString("loopbackCommAdapterPanel.panel_vehicleProperties.border.title"))); // NOI18N + vehiclePropsPanel.setLayout(new java.awt.GridBagLayout()); + + maxFwdVeloLbl.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + maxFwdVeloLbl.setText(BUNDLE.getString("loopbackCommAdapterPanel.label_maximumForwardVelocity.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 3); + vehiclePropsPanel.add(maxFwdVeloLbl, gridBagConstraints); + + maxFwdVeloTxt.setEditable(false); + maxFwdVeloTxt.setColumns(5); + maxFwdVeloTxt.setHorizontalAlignment(javax.swing.JTextField.RIGHT); + maxFwdVeloTxt.setText("0"); + maxFwdVeloTxt.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + maxFwdVeloTxt.setEnabled(false); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 3); + vehiclePropsPanel.add(maxFwdVeloTxt, gridBagConstraints); + + maxFwdVeloUnitLbl.setText("mm/s"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 3); + vehiclePropsPanel.add(maxFwdVeloUnitLbl, gridBagConstraints); + + maxRevVeloLbl.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + maxRevVeloLbl.setText(BUNDLE.getString("loopbackCommAdapterPanel.label_maximumReverseVelocity.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 3); + vehiclePropsPanel.add(maxRevVeloLbl, gridBagConstraints); + + maxRevVeloTxt.setEditable(false); + maxRevVeloTxt.setColumns(5); + maxRevVeloTxt.setHorizontalAlignment(javax.swing.JTextField.RIGHT); + maxRevVeloTxt.setText("0"); + maxRevVeloTxt.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + maxRevVeloTxt.setEnabled(false); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 3); + vehiclePropsPanel.add(maxRevVeloTxt, gridBagConstraints); + + maxRevVeloUnitLbl.setText("mm/s"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 3); + vehiclePropsPanel.add(maxRevVeloUnitLbl, gridBagConstraints); + + maxAccelLbl.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + maxAccelLbl.setText(BUNDLE.getString("loopbackCommAdapterPanel.label_maximumAcceleration.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 3); + vehiclePropsPanel.add(maxAccelLbl, gridBagConstraints); + + maxAccelTxt.setEditable(false); + maxAccelTxt.setColumns(5); + maxAccelTxt.setHorizontalAlignment(javax.swing.JTextField.RIGHT); + maxAccelTxt.setText("1000"); + maxAccelTxt.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + maxAccelTxt.setEnabled(false); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 3); + vehiclePropsPanel.add(maxAccelTxt, gridBagConstraints); + + maxAccelUnitLbl.setText("mm/s2"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 3); + vehiclePropsPanel.add(maxAccelUnitLbl, gridBagConstraints); + + maxDecelTxt.setEditable(false); + maxDecelTxt.setColumns(5); + maxDecelTxt.setHorizontalAlignment(javax.swing.JTextField.RIGHT); + maxDecelTxt.setText("1000"); + maxDecelTxt.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + maxDecelTxt.setEnabled(false); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 3); + vehiclePropsPanel.add(maxDecelTxt, gridBagConstraints); + + maxDecelLbl.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + maxDecelLbl.setText(BUNDLE.getString("loopbackCommAdapterPanel.label_maximumDeceleration.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 3); + vehiclePropsPanel.add(maxDecelLbl, gridBagConstraints); + + maxDecelUnitLbl.setText("mm/s2"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 3); + vehiclePropsPanel.add(maxDecelUnitLbl, gridBagConstraints); + + defaultOpTimeLbl.setText(BUNDLE.getString("loopbackCommAdapterPanel.label_operatingTime.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 3); + vehiclePropsPanel.add(defaultOpTimeLbl, gridBagConstraints); + + defaultOpTimeUntiLbl.setText("ms"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 4; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 3); + vehiclePropsPanel.add(defaultOpTimeUntiLbl, gridBagConstraints); + + opTimeTxt.setEditable(false); + opTimeTxt.setColumns(5); + opTimeTxt.setHorizontalAlignment(javax.swing.JTextField.RIGHT); + opTimeTxt.setText("1000"); + opTimeTxt.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + opTimeTxt.setEnabled(false); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 3); + vehiclePropsPanel.add(opTimeTxt, gridBagConstraints); + + PropsPowerInnerContainerPanel.add(vehiclePropsPanel); + + PropsPowerOuterContainerPanel.add(PropsPowerInnerContainerPanel, java.awt.BorderLayout.WEST); + + vehicleBahaviourPanel.add(PropsPowerOuterContainerPanel, java.awt.BorderLayout.NORTH); + + profilesContainerPanel.setLayout(new java.awt.BorderLayout()); + profilesContainerPanel.add(filler1, java.awt.BorderLayout.CENTER); + + vehicleBahaviourPanel.add(profilesContainerPanel, java.awt.BorderLayout.SOUTH); + + add(vehicleBahaviourPanel, java.awt.BorderLayout.CENTER); + + vehicleStatePanel.setLayout(new java.awt.BorderLayout()); + + stateContainerPanel.setLayout(new javax.swing.BoxLayout(stateContainerPanel, javax.swing.BoxLayout.Y_AXIS)); + + connectionPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(BUNDLE.getString("loopbackCommAdapterPanel.panel_adapterStatus.border.title"))); // NOI18N + connectionPanel.setName("connectionPanel"); // NOI18N + connectionPanel.setLayout(new java.awt.GridBagLayout()); + + chkBoxEnable.setText(BUNDLE.getString("loopbackCommAdapterPanel.checkBox_enableAdapter.text")); // NOI18N + chkBoxEnable.setName("chkBoxEnable"); // NOI18N + chkBoxEnable.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkBoxEnableActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + connectionPanel.add(chkBoxEnable, gridBagConstraints); + + stateContainerPanel.add(connectionPanel); + + curPosPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(BUNDLE.getString("loopbackCommAdapterPanel.panel_vehicleStatus.border.title"))); // NOI18N + curPosPanel.setName("curPosPanel"); // NOI18N + curPosPanel.setLayout(new java.awt.GridBagLayout()); + + energyLevelTxt.setEditable(false); + energyLevelTxt.setBackground(new java.awt.Color(255, 255, 255)); + energyLevelTxt.setText("100"); + energyLevelTxt.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + energyLevelTxt.setName("energyLevelTxt"); // NOI18N + energyLevelTxt.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + energyLevelTxtMouseClicked(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(3, 0, 0, 0); + curPosPanel.add(energyLevelTxt, gridBagConstraints); + + energyLevelLbl.setText("%"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + curPosPanel.add(energyLevelLbl, gridBagConstraints); + + pauseVehicleCheckBox.setEnabled(false); + pauseVehicleCheckBox.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); + pauseVehicleCheckBox.setHorizontalTextPosition(javax.swing.SwingConstants.LEADING); + pauseVehicleCheckBox.setName("pauseVehicleCheckBox"); // NOI18N + pauseVehicleCheckBox.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + pauseVehicleCheckBoxItemStateChanged(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 5; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + curPosPanel.add(pauseVehicleCheckBox, gridBagConstraints); + + orientationAngleLbl.setText("º"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 4; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + curPosPanel.add(orientationAngleLbl, gridBagConstraints); + + precisePosUnitLabel.setText("mm"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + curPosPanel.add(precisePosUnitLabel, gridBagConstraints); + + orientationAngleTxt.setEditable(false); + orientationAngleTxt.setBackground(new java.awt.Color(255, 255, 255)); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/commadapter/loopback/Bundle"); // NOI18N + orientationAngleTxt.setText(bundle.getString("loopbackCommAdapterPanel.textField_orientationAngle.angleNotSetPlaceholder")); // NOI18N + orientationAngleTxt.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + orientationAngleTxt.setName("orientationAngleTxt"); // NOI18N + orientationAngleTxt.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + orientationAngleTxtMouseClicked(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(3, 0, 0, 0); + curPosPanel.add(orientationAngleTxt, gridBagConstraints); + + energyLevelLabel.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + energyLevelLabel.setText(bundle.getString("loopbackCommAdapterPanel.label_energyLevel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 3); + curPosPanel.add(energyLevelLabel, gridBagConstraints); + + orientationLabel.setHorizontalAlignment(javax.swing.SwingConstants.RIGHT); + orientationLabel.setText(bundle.getString("loopbackCommAdapterPanel.label_orientationAngle.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 3); + curPosPanel.add(orientationLabel, gridBagConstraints); + + positionTxt.setEditable(false); + positionTxt.setBackground(new java.awt.Color(255, 255, 255)); + positionTxt.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + positionTxt.setName("positionTxt"); // NOI18N + positionTxt.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + positionTxtMouseClicked(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + curPosPanel.add(positionTxt, gridBagConstraints); + + positionLabel.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + positionLabel.setText(bundle.getString("loopbackCommAdapterPanel.label_position.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 3); + curPosPanel.add(positionLabel, gridBagConstraints); + + pauseVehicleLabel.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + pauseVehicleLabel.setText(bundle.getString("loopbackCommAdapterPanel.label_pauseVehicle.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 5; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 3); + curPosPanel.add(pauseVehicleLabel, gridBagConstraints); + + jLabel2.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + jLabel2.setText(bundle.getString("loopbackCommAdapterPanel.label_state.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 3); + curPosPanel.add(jLabel2, gridBagConstraints); + + stateTxt.setEditable(false); + stateTxt.setBackground(new java.awt.Color(255, 255, 255)); + stateTxt.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + stateTxt.setName("stateTxt"); // NOI18N + stateTxt.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + stateTxtMouseClicked(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(3, 0, 0, 0); + curPosPanel.add(stateTxt, gridBagConstraints); + + jLabel3.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + jLabel3.setText(bundle.getString("loopbackCommAdapterPanel.label_precisePosition.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 3); + curPosPanel.add(jLabel3, gridBagConstraints); + + precisePosTextArea.setEditable(false); + precisePosTextArea.setFont(positionTxt.getFont()); + precisePosTextArea.setRows(3); + precisePosTextArea.setText("X:\nY:\nZ:"); + precisePosTextArea.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + precisePosTextArea.setName("precisePosTextArea"); // NOI18N + precisePosTextArea.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + precisePosTextAreaMouseClicked(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(3, 0, 0, 0); + curPosPanel.add(precisePosTextArea, gridBagConstraints); + + stateContainerPanel.add(curPosPanel); + curPosPanel.getAccessibleContext().setAccessibleName("Change"); + + propertySetterPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("loopbackCommAdapterPanel.panel_vehicleProperty.border.title"))); // NOI18N + propertySetterPanel.setLayout(new java.awt.GridBagLayout()); + + keyLabel.setText(bundle.getString("loopbackCommAdapterPanel.label_propertyKey.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 3); + propertySetterPanel.add(keyLabel, gridBagConstraints); + + valueTextField.setMaximumSize(new java.awt.Dimension(4, 18)); + valueTextField.setMinimumSize(new java.awt.Dimension(4, 18)); + valueTextField.setPreferredSize(new java.awt.Dimension(100, 20)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + propertySetterPanel.add(valueTextField, gridBagConstraints); + + propSetButton.setText(bundle.getString("loopbackCommAdapterPanel.button_setProperty.text")); // NOI18N + propSetButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + propSetButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.gridwidth = 2; + propertySetterPanel.add(propSetButton, gridBagConstraints); + + propertyEditorGroup.add(removePropRadioBtn); + removePropRadioBtn.setText(bundle.getString("loopbackCommAdapterPanel.radioButton_removeProperty.text")); // NOI18N + removePropRadioBtn.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + removePropRadioBtnActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + propertySetterPanel.add(removePropRadioBtn, gridBagConstraints); + + propertyEditorGroup.add(setPropValueRadioBtn); + setPropValueRadioBtn.setSelected(true); + setPropValueRadioBtn.setText(bundle.getString("loopbackCommAdapterPanel.radioButton_setProperty.text")); // NOI18N + setPropValueRadioBtn.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + setPropValueRadioBtnActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + propertySetterPanel.add(setPropValueRadioBtn, gridBagConstraints); + + jPanel3.setLayout(new java.awt.GridBagLayout()); + + keyTextField.setPreferredSize(new java.awt.Dimension(100, 20)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + jPanel3.add(keyTextField, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + propertySetterPanel.add(jPanel3, gridBagConstraints); + + stateContainerPanel.add(propertySetterPanel); + + eventPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("loopbackCommAdapterPanel.panel_eventDispatching.title"))); // NOI18N + eventPanel.setLayout(new java.awt.GridBagLayout()); + + includeAppendixCheckBox.setText(bundle.getString("loopbackCommAdapterPanel.checkBox_includeAppendix.text")); // NOI18N + includeAppendixCheckBox.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + includeAppendixCheckBoxItemStateChanged(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.weightx = 1.0; + eventPanel.add(includeAppendixCheckBox, gridBagConstraints); + + appendixTxt.setEditable(false); + appendixTxt.setColumns(10); + appendixTxt.setText("XYZ"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + eventPanel.add(appendixTxt, gridBagConstraints); + + dispatchEventButton.setText(bundle.getString("loopbackCommAdapterPanel.button_dispatchEvent.text")); // NOI18N + dispatchEventButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + dispatchEventButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + eventPanel.add(dispatchEventButton, gridBagConstraints); + + dispatchCommandFailedButton.setText(bundle.getString("loopbackCommAdapterPanel.button_failCurrentCommand.text")); // NOI18N + dispatchCommandFailedButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + dispatchCommandFailedButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + eventPanel.add(dispatchCommandFailedButton, gridBagConstraints); + + stateContainerPanel.add(eventPanel); + + controlTabPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(BUNDLE.getString("loopbackCommAdapterPanel.panel_commandProcessing.border.title"))); // NOI18N + controlTabPanel.setLayout(new java.awt.GridBagLayout()); + + modeButtonGroup.add(singleModeRadioButton); + singleModeRadioButton.setText(BUNDLE.getString("loopbackCommAdapterPanel.checkBox_commandProcessingManual.text")); // NOI18N + singleModeRadioButton.setBorder(javax.swing.BorderFactory.createEmptyBorder(0, 0, 0, 0)); + singleModeRadioButton.setMargin(new java.awt.Insets(0, 0, 0, 0)); + singleModeRadioButton.setName("singleModeRadioButton"); // NOI18N + singleModeRadioButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + singleModeRadioButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + controlTabPanel.add(singleModeRadioButton, gridBagConstraints); + + modeButtonGroup.add(flowModeRadioButton); + flowModeRadioButton.setSelected(true); + flowModeRadioButton.setText(BUNDLE.getString("loopbackCommAdapterPanel.checkBox_commandProcessingAutomatic.text")); // NOI18N + flowModeRadioButton.setBorder(javax.swing.BorderFactory.createEmptyBorder(0, 0, 0, 0)); + flowModeRadioButton.setMargin(new java.awt.Insets(0, 0, 0, 0)); + flowModeRadioButton.setName("flowModeRadioButton"); // NOI18N + flowModeRadioButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + flowModeRadioButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + controlTabPanel.add(flowModeRadioButton, gridBagConstraints); + + triggerButton.setText(BUNDLE.getString("loopbackCommAdapterPanel.button_nextStep.text")); // NOI18N + triggerButton.setEnabled(false); + triggerButton.setName("triggerButton"); // NOI18N + triggerButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + triggerButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 3); + controlTabPanel.add(triggerButton, gridBagConstraints); + + stateContainerPanel.add(controlTabPanel); + + vehicleStatePanel.add(stateContainerPanel, java.awt.BorderLayout.NORTH); + + loadDevicePanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("loopbackCommAdapterPanel.panel_loadHandlingDevice.border.title"))); // NOI18N + loadDevicePanel.setLayout(new java.awt.BorderLayout()); + + jPanel1.setLayout(new java.awt.GridBagLayout()); + loadDevicePanel.add(jPanel1, java.awt.BorderLayout.SOUTH); + + lHDCheckbox.setText("Device loaded"); + lHDCheckbox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + lHDCheckboxClicked(evt); + } + }); + jPanel2.add(lHDCheckbox); + + loadDevicePanel.add(jPanel2, java.awt.BorderLayout.WEST); + + vehicleStatePanel.add(loadDevicePanel, java.awt.BorderLayout.CENTER); + + add(vehicleStatePanel, java.awt.BorderLayout.WEST); + + getAccessibleContext().setAccessibleName(bundle.getString("loopbackCommAdapterPanel.accessibleName")); // NOI18N + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void singleModeRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_singleModeRadioButtonActionPerformed + if (singleModeRadioButton.isSelected()) { + triggerButton.setEnabled(true); + + sendCommAdapterCommand(new SetSingleStepModeEnabledCommand(true)); + } + }//GEN-LAST:event_singleModeRadioButtonActionPerformed + + private void flowModeRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_flowModeRadioButtonActionPerformed + if (flowModeRadioButton.isSelected()) { + triggerButton.setEnabled(false); + + sendCommAdapterCommand(new SetSingleStepModeEnabledCommand(false)); + } + }//GEN-LAST:event_flowModeRadioButtonActionPerformed + + private void triggerButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_triggerButtonActionPerformed + sendCommAdapterCommand(new TriggerCommand()); + }//GEN-LAST:event_triggerButtonActionPerformed + + private void chkBoxEnableActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chkBoxEnableActionPerformed + try { + Vehicle vehicle = callWrapper.call( + () -> vehicleService.fetchObject(Vehicle.class, processModel.getName()) + ); + + if (chkBoxEnable.isSelected()) { + callWrapper.call(() -> vehicleService.enableCommAdapter(vehicle.getReference())); + } + else { + callWrapper.call(() -> vehicleService.disableCommAdapter(vehicle.getReference())); + } + + setStatePanelEnabled(chkBoxEnable.isSelected()); + } + catch (Exception ex) { + LOG.warn("Error enabling/disabling comm adapter", ex); + } + }//GEN-LAST:event_chkBoxEnableActionPerformed + + + private void precisePosTextAreaMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_precisePosTextAreaMouseClicked + if (precisePosTextArea.isEnabled()) { + Triple pos = processModel.getPose().getPosition(); + // Create panel and dialog + TripleTextInputPanel.Builder builder + = new TripleTextInputPanel.Builder( + BUNDLE.getString("loopbackCommAdapterPanel.dialog_setPrecisePosition.title") + ); + builder.setUnitLabels("mm"); + builder.setLabels("X:", "Y:", "Z:"); + builder.enableResetButton(null); + builder.enableValidation(TextInputPanel.TextInputValidator.REGEX_INT); + if (pos != null) { + builder.setInitialValues( + Long.toString(pos.getX()), + Long.toString(pos.getY()), + Long.toString(pos.getZ()) + ); + } + InputPanel panel = builder.build(); + InputDialog dialog = new InputDialog(panel); + dialog.setVisible(true); + // Get dialog result and set vehicle precise position + if (dialog.getReturnStatus() == InputDialog.ReturnStatus.ACCEPTED) { + if (dialog.getInput() == null) { + // Clear precise position + sendCommAdapterCommand(new SetPrecisePositionCommand(null)); + } + else { + // Set new precise position + long x; + long y; + long z; + String[] newPos = (String[]) dialog.getInput(); + try { + x = Long.parseLong(newPos[0]); + y = Long.parseLong(newPos[1]); + z = Long.parseLong(newPos[2]); + } + catch (NumberFormatException | NullPointerException e) { + return; + } + + sendCommAdapterCommand(new SetPrecisePositionCommand(new Triple(x, y, z))); + } + } + } + }//GEN-LAST:event_precisePosTextAreaMouseClicked + + private void stateTxtMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_stateTxtMouseClicked + if (!stateTxt.isEnabled()) { + return; + } + + Vehicle.State currentState = processModel.getState(); + // Create panel and dialog + InputPanel panel = new DropdownListInputPanel.Builder<>( + BUNDLE.getString("loopbackCommAdapterPanel.dialog_setState.title"), + Arrays.asList(Vehicle.State.values()) + ) + .setSelectionRepresenter(x -> x == null ? "" : x.name()) + .setLabel(BUNDLE.getString("loopbackCommAdapterPanel.label_state.text")) + .setInitialSelection(currentState) + .build(); + InputDialog dialog = new InputDialog(panel); + dialog.setVisible(true); + // Get dialog results and set vahicle stare + if (dialog.getReturnStatus() == InputDialog.ReturnStatus.ACCEPTED) { + Vehicle.State newState = (Vehicle.State) dialog.getInput(); + if (newState != currentState) { + sendCommAdapterCommand(new SetStateCommand(newState)); + } + } + }//GEN-LAST:event_stateTxtMouseClicked + + private void positionTxtMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_positionTxtMouseClicked + if (!positionTxt.isEnabled()) { + return; + } + + // Prepare list of model points + Set pointSet; + try { + pointSet = callWrapper.call(() -> vehicleService.fetchObjects(Point.class)); + } + catch (Exception ex) { + LOG.warn("Error fetching points", ex); + return; + } + + List pointList = new ArrayList<>(pointSet); + Collections.sort(pointList, Comparators.objectsByName()); + pointList.add(0, null); + // Get currently selected point + // TODO is there a better way to do this? + Point currentPoint = null; + String currentPointName = processModel.getPosition(); + for (Point p : pointList) { + if (p != null && p.getName().equals(currentPointName)) { + currentPoint = p; + break; + } + } + // Create panel and dialog + InputPanel panel = new DropdownListInputPanel.Builder<>( + BUNDLE.getString("loopbackCommAdapterPanel.dialog_setPosition.title"), pointList + ) + .setSelectionRepresenter(x -> x == null ? "" : x.getName()) + .setLabel(BUNDLE.getString("loopbackCommAdapterPanel.label_position.text")) + .setEditable(true) + .setInitialSelection(currentPoint) + .setRenderer(new StringListCellRenderer<>(x -> x == null ? "" : x.getName())) + .build(); + InputDialog dialog = new InputDialog(panel); + dialog.setVisible(true); + // Get result from dialog and set vehicle position + if (dialog.getReturnStatus() == InputDialog.ReturnStatus.ACCEPTED) { + Object item = dialog.getInput(); + if (item == null) { + sendCommAdapterCommand(new SetPositionCommand(null)); + } + else { + sendCommAdapterCommand(new SetPositionCommand(((Point) item).getName())); + } + } + }//GEN-LAST:event_positionTxtMouseClicked + + private void orientationAngleTxtMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_orientationAngleTxtMouseClicked + if (!orientationAngleTxt.isEnabled()) { + return; + } + + double currentAngle = processModel.getPose().getOrientationAngle(); + String initialValue = (Double.isNaN(currentAngle) ? "" : Double.toString(currentAngle)); + // Create dialog and panel + InputPanel panel = new SingleTextInputPanel.Builder( + BUNDLE.getString("loopbackCommAdapterPanel.dialog_setOrientationAngle.title") + ) + .setLabel(BUNDLE.getString("loopbackCommAdapterPanel.label_orientationAngle.text")) + .setUnitLabel("º") + .setInitialValue(initialValue) + .enableResetButton(null) + .enableValidation(TextInputPanel.TextInputValidator.REGEX_FLOAT) + .build(); + InputDialog dialog = new InputDialog(panel); + dialog.setVisible(true); + // Get input from dialog + InputDialog.ReturnStatus returnStatus = dialog.getReturnStatus(); + if (returnStatus == InputDialog.ReturnStatus.ACCEPTED) { + String input = (String) dialog.getInput(); + if (input == null) { + // The reset button was pressed + if (!Double.isNaN(processModel.getPose().getOrientationAngle())) { + sendCommAdapterCommand(new SetOrientationAngleCommand(Double.NaN)); + } + } + else { + // Set orientation provided by the user + double angle; + try { + angle = Double.parseDouble(input); + } + catch (NumberFormatException e) { + LOG.warn("Exception parsing orientation angle value '{}'", input, e); + return; + } + + sendCommAdapterCommand(new SetOrientationAngleCommand(angle)); + } + } + }//GEN-LAST:event_orientationAngleTxtMouseClicked + + private void pauseVehicleCheckBoxItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_pauseVehicleCheckBoxItemStateChanged + if (evt.getStateChange() == java.awt.event.ItemEvent.SELECTED) { + sendCommAdapterCommand(new SetVehiclePausedCommand(true)); + } + else if (evt.getStateChange() == java.awt.event.ItemEvent.DESELECTED) { + sendCommAdapterCommand(new SetVehiclePausedCommand(false)); + } + }//GEN-LAST:event_pauseVehicleCheckBoxItemStateChanged + + private void energyLevelTxtMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_energyLevelTxtMouseClicked + if (!energyLevelTxt.isEnabled()) { + return; + } + + // Create panel and dialog + InputPanel panel = new SingleTextInputPanel.Builder( + BUNDLE.getString("loopbackCommAdapterPanel.dialog_setEnergyLevel.title") + ) + .setLabel(BUNDLE.getString("loopbackCommAdapterPanel.label_energyLevel.text")) + .setUnitLabel("%") + .setInitialValue(energyLevelTxt.getText()) + .enableValidation(TextInputPanel.TextInputValidator.REGEX_INT_RANGE_0_100) + .build(); + InputDialog dialog = new InputDialog(panel); + dialog.setVisible(true); + // Get result from dialog and set energy level + if (dialog.getReturnStatus() == InputDialog.ReturnStatus.ACCEPTED) { + String input = (String) dialog.getInput(); + int energy; + try { + energy = Integer.parseInt(input); + } + catch (NumberFormatException e) { + return; + } + + sendCommAdapterCommand(new SetEnergyLevelCommand(energy)); + } + }//GEN-LAST:event_energyLevelTxtMouseClicked + + private void includeAppendixCheckBoxItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_includeAppendixCheckBoxItemStateChanged + appendixTxt.setEditable(includeAppendixCheckBox.isSelected()); + }//GEN-LAST:event_includeAppendixCheckBoxItemStateChanged + + private void dispatchEventButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_dispatchEventButtonActionPerformed + String appendix = includeAppendixCheckBox.isSelected() ? appendixTxt.getText() : null; + VehicleCommAdapterEvent event = new VehicleCommAdapterEvent( + processModel.getName(), + appendix + ); + sendCommAdapterCommand(new PublishEventCommand(event)); + }//GEN-LAST:event_dispatchEventButtonActionPerformed + + private void dispatchCommandFailedButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_dispatchCommandFailedButtonActionPerformed + sendCommAdapterCommand(new CurrentMovementCommandFailedCommand()); + }//GEN-LAST:event_dispatchCommandFailedButtonActionPerformed + + private void propSetButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_propSetButtonActionPerformed + sendCommAdapterCommand( + new SetVehiclePropertyCommand( + keyTextField.getText(), + setPropValueRadioBtn.isSelected() + ? valueTextField.getText() : null + ) + ); + }//GEN-LAST:event_propSetButtonActionPerformed + + private void removePropRadioBtnActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removePropRadioBtnActionPerformed + valueTextField.setEnabled(false); + }//GEN-LAST:event_removePropRadioBtnActionPerformed + + private void setPropValueRadioBtnActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_setPropValueRadioBtnActionPerformed + valueTextField.setEnabled(true); + }//GEN-LAST:event_setPropValueRadioBtnActionPerformed + + private void lHDCheckboxClicked(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_lHDCheckboxClicked + List devices = Arrays.asList( + new LoadHandlingDevice(LoopbackCommunicationAdapter.LHD_NAME, lHDCheckbox.isSelected()) + ); + sendCommAdapterCommand(new SetLoadHandlingDevicesCommand(devices)); + }//GEN-LAST:event_lHDCheckboxClicked + + /** + * Set the specified precise position to the text area. The method takes care + * of the formatting. If any of the parameters is null all values will be set + * to the "clear"-value. + * + * @param x x-position + * @param y y-position + * @param z z-poition + */ + private void setPrecisePosText(Triple precisePos) { + // Convert values to strings + String xS = BUNDLE.getString( + "loopbackCommAdapterPanel.textArea_precisePosition.positionNotSetPlaceholder" + ); + String yS = BUNDLE.getString( + "loopbackCommAdapterPanel.textArea_precisePosition.positionNotSetPlaceholder" + ); + String zS = BUNDLE.getString( + "loopbackCommAdapterPanel.textArea_precisePosition.positionNotSetPlaceholder" + ); + + if (precisePos != null) { + xS = String.valueOf(precisePos.getX()); + yS = String.valueOf(precisePos.getY()); + zS = String.valueOf(precisePos.getZ()); + } + + // Clip extremely long string values + xS = (xS.length() > 20) ? (xS.substring(0, 20) + "...") : xS; + yS = (yS.length() > 20) ? (yS.substring(0, 20) + "...") : yS; + zS = (zS.length() > 20) ? (zS.substring(0, 20) + "...") : zS; + + // Build formatted text + StringBuilder text = new StringBuilder(""); + text.append("X: ").append(xS).append("\n") + .append("Y: ").append(yS).append("\n") + .append("Z: ").append(zS); + precisePosTextArea.setText(text.toString()); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel PropsPowerInnerContainerPanel; + private javax.swing.JPanel PropsPowerOuterContainerPanel; + private javax.swing.JTextField appendixTxt; + private javax.swing.JCheckBox chkBoxEnable; + private javax.swing.JPanel connectionPanel; + private javax.swing.JPanel controlTabPanel; + private javax.swing.JPanel curPosPanel; + private javax.swing.JLabel defaultOpTimeLbl; + private javax.swing.JLabel defaultOpTimeUntiLbl; + private javax.swing.JButton dispatchCommandFailedButton; + private javax.swing.JButton dispatchEventButton; + private javax.swing.JLabel energyLevelLabel; + private javax.swing.JLabel energyLevelLbl; + private javax.swing.JTextField energyLevelTxt; + private javax.swing.JPanel eventPanel; + private javax.swing.Box.Filler filler1; + private javax.swing.JRadioButton flowModeRadioButton; + private javax.swing.JCheckBox includeAppendixCheckBox; + private javax.swing.JLabel jLabel2; + private javax.swing.JLabel jLabel3; + private javax.swing.JPanel jPanel1; + private javax.swing.JPanel jPanel2; + private javax.swing.JPanel jPanel3; + private javax.swing.JLabel keyLabel; + private javax.swing.JTextField keyTextField; + private javax.swing.JCheckBox lHDCheckbox; + private javax.swing.JPanel loadDevicePanel; + private javax.swing.JLabel maxAccelLbl; + private javax.swing.JTextField maxAccelTxt; + private javax.swing.JLabel maxAccelUnitLbl; + private javax.swing.JLabel maxDecelLbl; + private javax.swing.JTextField maxDecelTxt; + private javax.swing.JLabel maxDecelUnitLbl; + private javax.swing.JLabel maxFwdVeloLbl; + private javax.swing.JTextField maxFwdVeloTxt; + private javax.swing.JLabel maxFwdVeloUnitLbl; + private javax.swing.JLabel maxRevVeloLbl; + private javax.swing.JTextField maxRevVeloTxt; + private javax.swing.JLabel maxRevVeloUnitLbl; + private javax.swing.ButtonGroup modeButtonGroup; + private javax.swing.JTextField opTimeTxt; + private javax.swing.JLabel orientationAngleLbl; + private javax.swing.JTextField orientationAngleTxt; + private javax.swing.JLabel orientationLabel; + private javax.swing.JCheckBox pauseVehicleCheckBox; + private javax.swing.JLabel pauseVehicleLabel; + private javax.swing.JLabel positionLabel; + private javax.swing.JTextField positionTxt; + private javax.swing.JTextArea precisePosTextArea; + private javax.swing.JLabel precisePosUnitLabel; + private javax.swing.JPanel profilesContainerPanel; + private javax.swing.JButton propSetButton; + private javax.swing.ButtonGroup propertyEditorGroup; + private javax.swing.JPanel propertySetterPanel; + private javax.swing.JRadioButton removePropRadioBtn; + private javax.swing.JRadioButton setPropValueRadioBtn; + private javax.swing.JRadioButton singleModeRadioButton; + private javax.swing.JPanel stateContainerPanel; + private javax.swing.JTextField stateTxt; + private javax.swing.JButton triggerButton; + private javax.swing.JTextField valueTextField; + private javax.swing.JPanel vehicleBahaviourPanel; + private javax.swing.JPanel vehiclePropsPanel; + private javax.swing.JPanel vehicleStatePanel; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackCommAdapterPanelFactory.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackCommAdapterPanelFactory.java new file mode 100644 index 0000000..5584827 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackCommAdapterPanelFactory.java @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.List; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; +import org.opentcs.drivers.vehicle.management.VehicleCommAdapterPanel; +import org.opentcs.drivers.vehicle.management.VehicleCommAdapterPanelFactory; +import org.opentcs.drivers.vehicle.management.VehicleProcessModelTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A factory for creating {@link LoopbackCommAdapterPanel} instances. + */ +public class LoopbackCommAdapterPanelFactory + implements + VehicleCommAdapterPanelFactory { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(LoopbackCommAdapterPanelFactory.class); + /** + * The service portal. + */ + private final KernelServicePortal servicePortal; + /** + * The components factory. + */ + private final AdapterPanelComponentsFactory componentsFactory; + /** + * Whether this factory is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param servicePortal The service portal. + * @param componentsFactory The components factory. + */ + @Inject + public LoopbackCommAdapterPanelFactory( + KernelServicePortal servicePortal, + AdapterPanelComponentsFactory componentsFactory + ) { + this.servicePortal = requireNonNull(servicePortal, "servicePortal"); + this.componentsFactory = requireNonNull(componentsFactory, "componentsFactory"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + initialized = false; + } + + @Override + public List getPanelsFor( + @Nonnull + VehicleCommAdapterDescription description, + @Nonnull + TCSObjectReference vehicle, + @Nonnull + VehicleProcessModelTO processModel + ) { + requireNonNull(description, "description"); + requireNonNull(vehicle, "vehicle"); + requireNonNull(processModel, "processModel"); + + if (!providesPanelsFor(description, processModel)) { + return new ArrayList<>(); + } + + List panels = new ArrayList<>(); + panels.add( + componentsFactory.createPanel( + ((LoopbackVehicleModelTO) processModel), + servicePortal.getVehicleService() + ) + ); + return panels; + } + + /** + * Checks whether this factory can provide comm adapter panels for the given description and the + * given type of process model. + * + * @param description The description to check for. + * @param processModel The process model. + * @return {@code true} if, and only if, this factory can provide comm adapter panels for the + * given description and the given type of process model. + */ + private boolean providesPanelsFor( + VehicleCommAdapterDescription description, + VehicleProcessModelTO processModel + ) { + return (description instanceof LoopbackCommunicationAdapterDescription) + && (processModel instanceof LoopbackVehicleModelTO); + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackCommunicationAdapter.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackCommunicationAdapter.java new file mode 100644 index 0000000..0215ba2 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackCommunicationAdapter.java @@ -0,0 +1,559 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.beans.PropertyChangeEvent; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import org.opentcs.common.LoopbackAdapterConstants; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.Route.Step; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.drivers.vehicle.BasicVehicleCommAdapter; +import org.opentcs.drivers.vehicle.LoadHandlingDevice; +import org.opentcs.drivers.vehicle.MovementCommand; +import org.opentcs.drivers.vehicle.SimVehicleCommAdapter; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; +import org.opentcs.drivers.vehicle.VehicleProcessModel; +import org.opentcs.drivers.vehicle.management.VehicleProcessModelTO; +import org.opentcs.util.ExplainedBoolean; +import org.opentcs.virtualvehicle.VelocityController.WayEntry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@link VehicleCommAdapter} that does not really communicate with a physical vehicle but roughly + * simulates one. + */ +public class LoopbackCommunicationAdapter + extends + BasicVehicleCommAdapter + implements + SimVehicleCommAdapter { + + /** + * The name of the load handling device set by this adapter. + */ + public static final String LHD_NAME = "default"; + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(LoopbackCommunicationAdapter.class); + /** + * An error code indicating that there's a conflict between a load operation and the vehicle's + * current load state. + */ + private static final String LOAD_OPERATION_CONFLICT = "cannotLoadWhenLoaded"; + /** + * An error code indicating that there's a conflict between an unload operation and the vehicle's + * current load state. + */ + private static final String UNLOAD_OPERATION_CONFLICT = "cannotUnloadWhenNotLoaded"; + /** + * The time (in ms) of a single simulation step. + */ + private static final int SIMULATION_PERIOD = 100; + /** + * This instance's configuration. + */ + private final VirtualVehicleConfiguration configuration; + /** + * Indicates whether the vehicle simulation is running or not. + */ + private volatile boolean isSimulationRunning; + /** + * The vehicle to this comm adapter instance. + */ + private final Vehicle vehicle; + /** + * The vehicle's load state. + */ + private LoadState loadState = LoadState.EMPTY; + /** + * Whether the loopback adapter is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param configuration This class's configuration. + * @param vehicle The vehicle this adapter is associated with. + * @param kernelExecutor The kernel's executor. + */ + @Inject + public LoopbackCommunicationAdapter( + VirtualVehicleConfiguration configuration, + @Assisted + Vehicle vehicle, + @KernelExecutor + ScheduledExecutorService kernelExecutor + ) { + super( + new LoopbackVehicleModel(vehicle), + configuration.commandQueueCapacity(), + configuration.rechargeOperation(), + kernelExecutor + ); + this.vehicle = requireNonNull(vehicle, "vehicle"); + this.configuration = requireNonNull(configuration, "configuration"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + super.initialize(); + + String initialPos + = vehicle.getProperties().get(LoopbackAdapterConstants.PROPKEY_INITIAL_POSITION); + if (initialPos != null) { + initVehiclePosition(initialPos); + } + getProcessModel().setState(Vehicle.State.IDLE); + getProcessModel().setLoadHandlingDevices( + Arrays.asList(new LoadHandlingDevice(LHD_NAME, false)) + ); + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + super.terminate(); + initialized = false; + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + super.propertyChange(evt); + + if (!((evt.getSource()) instanceof LoopbackVehicleModel)) { + return; + } + if (Objects.equals( + evt.getPropertyName(), + VehicleProcessModel.Attribute.LOAD_HANDLING_DEVICES.name() + )) { + if (!getProcessModel().getLoadHandlingDevices().isEmpty() + && getProcessModel().getLoadHandlingDevices().get(0).isFull()) { + loadState = LoadState.FULL; + getProcessModel().setBoundingBox( + getProcessModel().getBoundingBox().withLength(configuration.vehicleLengthLoaded()) + ); + } + else { + loadState = LoadState.EMPTY; + getProcessModel().setBoundingBox( + getProcessModel().getBoundingBox().withLength(configuration.vehicleLengthUnloaded()) + ); + } + } + if (Objects.equals( + evt.getPropertyName(), + LoopbackVehicleModel.Attribute.SINGLE_STEP_MODE.name() + )) { + // When switching from single step mode to automatic mode and there are commands to be + // processed, ensure that we start/continue processing them. + if (!getProcessModel().isSingleStepModeEnabled() + && !getSentCommands().isEmpty() + && !isSimulationRunning) { + isSimulationRunning = true; + ((ExecutorService) getExecutor()).submit( + () -> startVehicleSimulation(getSentCommands().peek()) + ); + } + } + } + + @Override + public synchronized void enable() { + if (isEnabled()) { + return; + } + super.enable(); + } + + @Override + public synchronized void disable() { + if (!isEnabled()) { + return; + } + super.disable(); + } + + @Override + public LoopbackVehicleModel getProcessModel() { + return (LoopbackVehicleModel) super.getProcessModel(); + } + + @Override + public synchronized void sendCommand(MovementCommand cmd) { + requireNonNull(cmd, "cmd"); + + // Start the simulation task if we're not in single step mode and not simulating already. + if (!getProcessModel().isSingleStepModeEnabled() + && !isSimulationRunning) { + isSimulationRunning = true; + // The command is added to the sent queue after this method returns. Therefore + // we have to explicitly start the simulation like this. + if (getSentCommands().isEmpty()) { + ((ExecutorService) getExecutor()).submit(() -> startVehicleSimulation(cmd)); + } + else { + ((ExecutorService) getExecutor()).submit( + () -> startVehicleSimulation(getSentCommands().peek()) + ); + } + } + } + + @Override + public void onVehiclePaused(boolean paused) { + getProcessModel().setVehiclePaused(paused); + } + + @Override + public void processMessage(Object message) { + } + + @Override + public synchronized void initVehiclePosition(String newPos) { + ((ExecutorService) getExecutor()).submit(() -> getProcessModel().setPosition(newPos)); + } + + @Override + public synchronized ExplainedBoolean canProcess(TransportOrder order) { + requireNonNull(order, "order"); + + return canProcess( + order.getFutureDriveOrders().stream() + .map(driveOrder -> driveOrder.getDestination().getOperation()) + .collect(Collectors.toList()) + ); + } + + private ExplainedBoolean canProcess(List operations) { + requireNonNull(operations, "operations"); + + LOG.debug("{}: Checking processability of {}...", getName(), operations); + boolean canProcess = true; + String reason = ""; + + // Do NOT require the vehicle to be IDLE or CHARGING here! + // That would mean a vehicle moving to a parking position or recharging location would always + // have to finish that order first, which would render a transport order's dispensable flag + // useless. + boolean loaded = loadState == LoadState.FULL; + Iterator opIter = operations.iterator(); + while (canProcess && opIter.hasNext()) { + final String nextOp = opIter.next(); + // If we're loaded, we cannot load another piece, but could unload. + if (loaded) { + if (nextOp.startsWith(getProcessModel().getLoadOperation())) { + canProcess = false; + reason = LOAD_OPERATION_CONFLICT; + } + else if (nextOp.startsWith(getProcessModel().getUnloadOperation())) { + loaded = false; + } + } // If we're not loaded, we could load, but not unload. + else if (nextOp.startsWith(getProcessModel().getLoadOperation())) { + loaded = true; + } + else if (nextOp.startsWith(getProcessModel().getUnloadOperation())) { + canProcess = false; + reason = UNLOAD_OPERATION_CONFLICT; + } + } + if (!canProcess) { + LOG.debug("{}: Cannot process {}, reason: '{}'", getName(), operations, reason); + } + return new ExplainedBoolean(canProcess, reason); + } + + @Override + protected synchronized void connectVehicle() { + } + + @Override + protected synchronized void disconnectVehicle() { + } + + @Override + protected synchronized boolean isVehicleConnected() { + return true; + } + + @Override + protected VehicleProcessModelTO createCustomTransferableProcessModel() { + return new LoopbackVehicleModelTO() + .setLoadOperation(getProcessModel().getLoadOperation()) + .setMaxAcceleration(getProcessModel().getMaxAcceleration()) + .setMaxDeceleration(getProcessModel().getMaxDecceleration()) + .setMaxFwdVelocity(getProcessModel().getMaxFwdVelocity()) + .setMaxRevVelocity(getProcessModel().getMaxRevVelocity()) + .setOperatingTime(getProcessModel().getOperatingTime()) + .setSingleStepModeEnabled(getProcessModel().isSingleStepModeEnabled()) + .setUnloadOperation(getProcessModel().getUnloadOperation()) + .setVehiclePaused(getProcessModel().isVehiclePaused()); + } + + /** + * Triggers a step in single step mode. + */ + public synchronized void trigger() { + if (getProcessModel().isSingleStepModeEnabled() + && !getSentCommands().isEmpty() + && !isSimulationRunning) { + isSimulationRunning = true; + ((ExecutorService) getExecutor()).submit( + () -> startVehicleSimulation(getSentCommands().peek()) + ); + } + } + + private void startVehicleSimulation(MovementCommand command) { + LOG.debug("Starting vehicle simulation for command: {}", command); + Step step = command.getStep(); + getProcessModel().setState(Vehicle.State.EXECUTING); + + if (step.getPath() == null) { + LOG.debug("Starting operation simulation..."); + getExecutor().schedule( + () -> operationSimulation(command, 0), + SIMULATION_PERIOD, + TimeUnit.MILLISECONDS + ); + } + else { + getProcessModel().getVelocityController().addWayEntry( + new WayEntry( + step.getPath().getLength(), + maxVelocity(step), + step.getDestinationPoint().getName(), + step.getVehicleOrientation() + ) + ); + + LOG.debug("Starting movement simulation..."); + getExecutor().schedule( + () -> movementSimulation(command), + SIMULATION_PERIOD, + TimeUnit.MILLISECONDS + ); + } + } + + private int maxVelocity(Step step) { + return (step.getVehicleOrientation() == Vehicle.Orientation.BACKWARD) + ? step.getPath().getMaxReverseVelocity() + : step.getPath().getMaxVelocity(); + } + + /** + * Simulate the movement part of a MovementCommand. + * + * @param command The command to simulate. + */ + private void movementSimulation(MovementCommand command) { + if (!getProcessModel().getVelocityController().hasWayEntries()) { + return; + } + + WayEntry prevWayEntry = getProcessModel().getVelocityController().getCurrentWayEntry(); + getProcessModel().getVelocityController().advanceTime(getSimulationTimeStep()); + WayEntry currentWayEntry = getProcessModel().getVelocityController().getCurrentWayEntry(); + //if we are still on the same way entry then reschedule to do it again + if (prevWayEntry == currentWayEntry) { + getExecutor().schedule( + () -> movementSimulation(command), + SIMULATION_PERIOD, + TimeUnit.MILLISECONDS + ); + } + else { + //if the way enties are different then we have finished this step + //and we can move on. + getProcessModel().setPosition(prevWayEntry.getDestPointName()); + LOG.debug("Movement simulation finished."); + if (!command.hasEmptyOperation()) { + LOG.debug("Starting operation simulation..."); + getExecutor().schedule( + () -> operationSimulation(command, 0), + SIMULATION_PERIOD, + TimeUnit.MILLISECONDS + ); + } + else { + finishMovementCommand(command); + simulateNextCommand(); + } + } + } + + /** + * Simulate the operation part of a movement command. + * + * @param command The command to simulate. + * @param timePassed The amount of time passed since starting the simulation. + */ + private void operationSimulation( + MovementCommand command, + int timePassed + ) { + if (timePassed < getProcessModel().getOperatingTime()) { + getProcessModel().getVelocityController().advanceTime(getSimulationTimeStep()); + getExecutor().schedule( + () -> operationSimulation(command, timePassed + getSimulationTimeStep()), + SIMULATION_PERIOD, + TimeUnit.MILLISECONDS + ); + } + else { + LOG.debug("Operation simulation finished."); + finishMovementCommand(command); + String operation = command.getOperation(); + if (operation.equals(getProcessModel().getLoadOperation())) { + // Update load handling devices as defined by this operation + getProcessModel().setLoadHandlingDevices( + Arrays.asList(new LoadHandlingDevice(LHD_NAME, true)) + ); + simulateNextCommand(); + } + else if (operation.equals(getProcessModel().getUnloadOperation())) { + getProcessModel().setLoadHandlingDevices( + Arrays.asList(new LoadHandlingDevice(LHD_NAME, false)) + ); + simulateNextCommand(); + } + else if (operation.equals(this.getRechargeOperation())) { + LOG.debug("Starting recharge simulation..."); + finishMovementCommand(command); + getProcessModel().setState(Vehicle.State.CHARGING); + getExecutor().schedule( + () -> chargingSimulation( + getProcessModel().getPosition(), + getProcessModel().getEnergyLevel() + ), + SIMULATION_PERIOD, + TimeUnit.MILLISECONDS + ); + } + else { + simulateNextCommand(); + } + } + } + + /** + * Simulate recharging the vehicle. + * + * @param rechargePosition The vehicle position where the recharge simulation was started. + * @param rechargePercentage The recharge percentage of the vehicle while it is charging. + */ + private void chargingSimulation( + String rechargePosition, + float rechargePercentage + ) { + if (!getSentCommands().isEmpty()) { + LOG.debug("Aborting recharge operation, vehicle has an order..."); + simulateNextCommand(); + return; + } + + if (getProcessModel().getState() != Vehicle.State.CHARGING) { + LOG.debug("Aborting recharge operation, vehicle no longer charging state..."); + simulateNextCommand(); + return; + } + + if (!Objects.equals(getProcessModel().getPosition(), rechargePosition)) { + LOG.debug("Aborting recharge operation, vehicle position changed..."); + simulateNextCommand(); + return; + } + if (nextChargePercentage(rechargePercentage) < 100.0) { + getProcessModel().setEnergyLevel((int) rechargePercentage); + getExecutor().schedule( + () -> chargingSimulation(rechargePosition, nextChargePercentage(rechargePercentage)), + SIMULATION_PERIOD, + TimeUnit.MILLISECONDS + ); + } + else { + LOG.debug("Finishing recharge operation, vehicle at 100%..."); + getProcessModel().setEnergyLevel(100); + simulateNextCommand(); + } + } + + private float nextChargePercentage(float basePercentage) { + return basePercentage + + (float) (configuration.rechargePercentagePerSecond() / 1000.0) * SIMULATION_PERIOD; + } + + private void finishMovementCommand(MovementCommand command) { + //Set the vehicle state to idle + if (getSentCommands().size() <= 1 && getUnsentCommands().isEmpty()) { + getProcessModel().setState(Vehicle.State.IDLE); + } + if (Objects.equals(getSentCommands().peek(), command)) { + // Let the comm adapter know we have finished this command. + getProcessModel().commandExecuted(getSentCommands().poll()); + } + else { + LOG.warn( + "{}: Simulated command not oldest in sent queue: {} != {}", + getName(), + command, + getSentCommands().peek() + ); + } + } + + void simulateNextCommand() { + if (getSentCommands().isEmpty() || getProcessModel().isSingleStepModeEnabled()) { + LOG.debug("Vehicle simulation is done."); + getProcessModel().setState(Vehicle.State.IDLE); + isSimulationRunning = false; + } + else { + LOG.debug("Triggering simulation for next command: {}", getSentCommands().peek()); + ((ExecutorService) getExecutor()).submit( + () -> startVehicleSimulation(getSentCommands().peek()) + ); + } + } + + private int getSimulationTimeStep() { + return (int) (SIMULATION_PERIOD * configuration.simulationTimeFactor()); + } + + /** + * The vehicle's possible load states. + */ + private enum LoadState { + EMPTY, + FULL; + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackCommunicationAdapterDescription.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackCommunicationAdapterDescription.java new file mode 100644 index 0000000..82c751f --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackCommunicationAdapterDescription.java @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle; + +import static org.opentcs.virtualvehicle.I18nLoopbackCommAdapter.BUNDLE_PATH; + +import java.util.ResourceBundle; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; + +/** + * The loopback adapter's {@link VehicleCommAdapterDescription}. + */ +public class LoopbackCommunicationAdapterDescription + extends + VehicleCommAdapterDescription { + + /** + * Creates a new instance. + */ + public LoopbackCommunicationAdapterDescription() { + } + + @Override + public String getDescription() { + return ResourceBundle.getBundle(BUNDLE_PATH) + .getString("loopbackCommunicationAdapterDescription.description"); + } + + @Override + public boolean isSimVehicleCommAdapter() { + return true; + } + +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackCommunicationAdapterFactory.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackCommunicationAdapterFactory.java new file mode 100644 index 0000000..b4c30eb --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackCommunicationAdapterFactory.java @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; +import org.opentcs.drivers.vehicle.VehicleCommAdapterFactory; + +/** + * A factory for loopback communication adapters (virtual vehicles). + */ +public class LoopbackCommunicationAdapterFactory + implements + VehicleCommAdapterFactory { + + /** + * The adapter components factory. + */ + private final LoopbackAdapterComponentsFactory adapterFactory; + /** + * Indicates whether this component is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new factory. + * + * @param componentsFactory The adapter components factory. + */ + @Inject + public LoopbackCommunicationAdapterFactory(LoopbackAdapterComponentsFactory componentsFactory) { + this.adapterFactory = requireNonNull(componentsFactory, "componentsFactory"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + initialized = false; + } + + @Override + public VehicleCommAdapterDescription getDescription() { + return new LoopbackCommunicationAdapterDescription(); + } + + @Override + public boolean providesAdapterFor(Vehicle vehicle) { + requireNonNull(vehicle, "vehicle"); + return true; + } + + @Override + public LoopbackCommunicationAdapter getAdapterFor(Vehicle vehicle) { + requireNonNull(vehicle, "vehicle"); + return adapterFactory.createLoopbackCommAdapter(vehicle); + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackVehicleModel.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackVehicleModel.java new file mode 100644 index 0000000..513eef3 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackVehicleModel.java @@ -0,0 +1,326 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle; + +import jakarta.annotation.Nonnull; +import org.opentcs.common.LoopbackAdapterConstants; +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.VehicleProcessModel; + +/** + * An observable model of a virtual vehicle's and its comm adapter's attributes. + */ +public class LoopbackVehicleModel + extends + VehicleProcessModel { + + /** + * Indicates whether this communication adapter is in single step mode or not (i.e. in automatic + * mode). + */ + private boolean singleStepModeEnabled; + /** + * Indicates which operation is a loading operation. + */ + private final String loadOperation; + /** + * Indicates which operation is an unloading operation. + */ + private final String unloadOperation; + /** + * The time needed for executing operations. + */ + private int operatingTime; + /** + * The velocity controller for calculating the simulated vehicle's velocity and current position. + */ + private final VelocityController velocityController; + + public LoopbackVehicleModel(Vehicle attachedVehicle) { + super(attachedVehicle); + this.velocityController = new VelocityController( + parseDeceleration(attachedVehicle), + parseAcceleration(attachedVehicle), + attachedVehicle.getMaxReverseVelocity(), + attachedVehicle.getMaxVelocity() + ); + this.operatingTime = parseOperatingTime(attachedVehicle); + this.loadOperation = extractLoadOperation(attachedVehicle); + this.unloadOperation = extractUnloadOperation(attachedVehicle); + } + + public String getLoadOperation() { + return this.loadOperation; + } + + public String getUnloadOperation() { + return this.unloadOperation; + } + + /** + * Sets this communication adapter's single step mode flag. + * + * @param mode If true, sets this adapter to single step mode, + * otherwise sets this adapter to flow mode. + */ + public synchronized void setSingleStepModeEnabled(final boolean mode) { + boolean oldValue = singleStepModeEnabled; + singleStepModeEnabled = mode; + + getPropertyChangeSupport().firePropertyChange( + Attribute.SINGLE_STEP_MODE.name(), + oldValue, + mode + ); + } + + /** + * Returns this communication adapter's single step mode flag. + * + * @return true if, and only if, this adapter is currently in + * single step mode. + */ + public synchronized boolean isSingleStepModeEnabled() { + return singleStepModeEnabled; + } + + /** + * Returns the default operating time. + * + * @return The default operating time + */ + public synchronized int getOperatingTime() { + return operatingTime; + } + + /** + * Sets the default operating time. + * + * @param defaultOperatingTime The new default operating time + */ + public synchronized void setOperatingTime(int defaultOperatingTime) { + int oldValue = this.operatingTime; + this.operatingTime = defaultOperatingTime; + + getPropertyChangeSupport().firePropertyChange( + Attribute.OPERATING_TIME.name(), + oldValue, + defaultOperatingTime + ); + } + + /** + * Returns the maximum deceleration. + * + * @return The maximum deceleration + */ + public synchronized int getMaxDecceleration() { + return velocityController.getMaxDeceleration(); + } + + /** + * Sets the maximum deceleration. + * + * @param maxDeceleration The new maximum deceleration + */ + public synchronized void setMaxDeceleration(int maxDeceleration) { + int oldValue = velocityController.getMaxDeceleration(); + velocityController.setMaxDeceleration(maxDeceleration); + + getPropertyChangeSupport().firePropertyChange( + Attribute.DECELERATION.name(), + oldValue, + maxDeceleration + ); + } + + /** + * Returns the maximum acceleration. + * + * @return The maximum acceleration + */ + public synchronized int getMaxAcceleration() { + return velocityController.getMaxAcceleration(); + } + + /** + * Sets the maximum acceleration. + * + * @param maxAcceleration The new maximum acceleration + */ + public synchronized void setMaxAcceleration(int maxAcceleration) { + int oldValue = velocityController.getMaxAcceleration(); + velocityController.setMaxAcceleration(maxAcceleration); + + getPropertyChangeSupport().firePropertyChange( + Attribute.ACCELERATION.name(), + oldValue, + maxAcceleration + ); + } + + /** + * Returns the maximum reverse velocity. + * + * @return The maximum reverse velocity. + */ + public synchronized int getMaxRevVelocity() { + return velocityController.getMaxRevVelocity(); + } + + /** + * Sets the maximum reverse velocity. + * + * @param maxRevVelocity The new maximum reverse velocity + */ + public synchronized void setMaxRevVelocity(int maxRevVelocity) { + int oldValue = velocityController.getMaxRevVelocity(); + velocityController.setMaxRevVelocity(maxRevVelocity); + + getPropertyChangeSupport().firePropertyChange( + Attribute.MAX_REVERSE_VELOCITY.name(), + oldValue, + maxRevVelocity + ); + } + + /** + * Returns the maximum forward velocity. + * + * @return The maximum forward velocity. + */ + public synchronized int getMaxFwdVelocity() { + return velocityController.getMaxFwdVelocity(); + } + + /** + * Sets the maximum forward velocity. + * + * @param maxFwdVelocity The new maximum forward velocity. + */ + public synchronized void setMaxFwdVelocity(int maxFwdVelocity) { + int oldValue = velocityController.getMaxFwdVelocity(); + velocityController.setMaxFwdVelocity(maxFwdVelocity); + + getPropertyChangeSupport().firePropertyChange( + Attribute.MAX_FORWARD_VELOCITY.name(), + oldValue, + maxFwdVelocity + ); + } + + /** + * Returns whether the vehicle is paused. + * + * @return paused + */ + public synchronized boolean isVehiclePaused() { + return velocityController.isVehiclePaused(); + } + + /** + * Pause the vehicle (i.e. set it's velocity to zero). + * + * @param pause True, if vehicle shall be paused. False, otherwise. + */ + public synchronized void setVehiclePaused(boolean pause) { + boolean oldValue = velocityController.isVehiclePaused(); + velocityController.setVehiclePaused(pause); + + getPropertyChangeSupport().firePropertyChange( + Attribute.VEHICLE_PAUSED.name(), + oldValue, + pause + ); + } + + /** + * Returns the virtual vehicle's velocity controller. + * + * @return The virtual vehicle's velocity controller. + */ + @Nonnull + public VelocityController getVelocityController() { + return velocityController; + } + + private int parseOperatingTime(Vehicle vehicle) { + String opTime = vehicle.getProperty(LoopbackAdapterConstants.PROPKEY_OPERATING_TIME); + // Ensure it's a positive value. + return Math.max(Parsers.tryParseString(opTime, 5000), 1); + } + + /** + * Gets the maximum acceleration. If the user did not specify any, 1000(m/s²) is returned. + * + * @param vehicle the vehicle + * @return the maximum acceleration. + */ + private int parseAcceleration(Vehicle vehicle) { + String acceleration = vehicle.getProperty(LoopbackAdapterConstants.PROPKEY_ACCELERATION); + // Ensure it's a positive value. + return Math.max(Parsers.tryParseString(acceleration, 500), 1); + } + + /** + * Gets the maximum decceleration. If the user did not specify any, 1000(m/s²) is returned. + * + * @param vehicle the vehicle + * @return the maximum decceleration. + */ + private int parseDeceleration(Vehicle vehicle) { + String deceleration = vehicle.getProperty(LoopbackAdapterConstants.PROPKEY_DECELERATION); + // Ensure it's a negative value. + return Math.min(Parsers.tryParseString(deceleration, -500), -1); + } + + private static String extractLoadOperation(Vehicle attachedVehicle) { + String result = attachedVehicle.getProperty(LoopbackAdapterConstants.PROPKEY_LOAD_OPERATION); + if (result == null) { + result = LoopbackAdapterConstants.PROPVAL_LOAD_OPERATION_DEFAULT; + } + return result; + } + + private static String extractUnloadOperation(Vehicle attachedVehicle) { + String result = attachedVehicle.getProperty(LoopbackAdapterConstants.PROPKEY_UNLOAD_OPERATION); + if (result == null) { + result = LoopbackAdapterConstants.PROPVAL_UNLOAD_OPERATION_DEFAULT; + } + return result; + } + + /** + * Notification arguments to indicate some change. + */ + public enum Attribute { + /** + * Indicates a change of the virtual vehicle's single step mode setting. + */ + SINGLE_STEP_MODE, + /** + * Indicates a change of the virtual vehicle's default operating time. + */ + OPERATING_TIME, + /** + * Indicates a change of the virtual vehicle's maximum acceleration. + */ + ACCELERATION, + /** + * Indicates a change of the virtual vehicle's maximum deceleration. + */ + DECELERATION, + /** + * Indicates a change of the virtual vehicle's maximum forward velocity. + */ + MAX_FORWARD_VELOCITY, + /** + * Indicates a change of the virtual vehicle's maximum reverse velocity. + */ + MAX_REVERSE_VELOCITY, + /** + * Indicates a change of the virtual vehicle's paused setting. + */ + VEHICLE_PAUSED, + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackVehicleModelTO.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackVehicleModelTO.java new file mode 100644 index 0000000..3ab04a9 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/LoopbackVehicleModelTO.java @@ -0,0 +1,137 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle; + +import org.opentcs.drivers.vehicle.management.VehicleProcessModelTO; + +/** + * A serializable representation of a {@link LoopbackVehicleModel}. + */ +public class LoopbackVehicleModelTO + extends + VehicleProcessModelTO { + + /** + * Whether this communication adapter is in single step mode or not (i.e. in automatic mode). + */ + private boolean singleStepModeEnabled; + /** + * Indicates which operation is a loading operation. + */ + private String loadOperation; + /** + * Indicates which operation is an unloading operation. + */ + private String unloadOperation; + /** + * The time needed for executing operations. + */ + private int operatingTime; + /** + * The maximum acceleration. + */ + private int maxAcceleration; + /** + * The maximum deceleration. + */ + private int maxDeceleration; + /** + * The maximum forward velocity. + */ + private int maxFwdVelocity; + /** + * The maximum reverse velocity. + */ + private int maxRevVelocity; + /** + * Whether the vehicle is paused or not. + */ + private boolean vehiclePaused; + + /** + * Creates a new instance. + */ + public LoopbackVehicleModelTO() { + } + + public boolean isSingleStepModeEnabled() { + return singleStepModeEnabled; + } + + public LoopbackVehicleModelTO setSingleStepModeEnabled(boolean singleStepModeEnabled) { + this.singleStepModeEnabled = singleStepModeEnabled; + return this; + } + + public String getLoadOperation() { + return loadOperation; + } + + public LoopbackVehicleModelTO setLoadOperation(String loadOperation) { + this.loadOperation = loadOperation; + return this; + } + + public String getUnloadOperation() { + return unloadOperation; + } + + public LoopbackVehicleModelTO setUnloadOperation(String unloadOperation) { + this.unloadOperation = unloadOperation; + return this; + } + + public int getOperatingTime() { + return operatingTime; + } + + public LoopbackVehicleModelTO setOperatingTime(int operatingTime) { + this.operatingTime = operatingTime; + return this; + } + + public int getMaxAcceleration() { + return maxAcceleration; + } + + public LoopbackVehicleModelTO setMaxAcceleration(int maxAcceleration) { + this.maxAcceleration = maxAcceleration; + return this; + } + + public int getMaxDeceleration() { + return maxDeceleration; + } + + public LoopbackVehicleModelTO setMaxDeceleration(int maxDeceleration) { + this.maxDeceleration = maxDeceleration; + return this; + } + + public int getMaxFwdVelocity() { + return maxFwdVelocity; + } + + public LoopbackVehicleModelTO setMaxFwdVelocity(int maxFwdVelocity) { + this.maxFwdVelocity = maxFwdVelocity; + return this; + } + + public int getMaxRevVelocity() { + return maxRevVelocity; + } + + public LoopbackVehicleModelTO setMaxRevVelocity(int maxRevVelocity) { + this.maxRevVelocity = maxRevVelocity; + return this; + } + + public boolean isVehiclePaused() { + return vehiclePaused; + } + + public LoopbackVehicleModelTO setVehiclePaused(boolean vehiclePaused) { + this.vehiclePaused = vehiclePaused; + return this; + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/Parsers.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/Parsers.java new file mode 100644 index 0000000..9a96786 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/Parsers.java @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle; + +import com.google.common.primitives.Ints; +import jakarta.annotation.Nullable; + +/** + * This class provides methods for parsing. + */ +public class Parsers { + + /** + * Prevents instantiation of this utility class. + */ + private Parsers() { + } + + /** + * Parses a String to an int. If toParse could not be parsed successfully then the + * default value retOnFail is returned. + * + * @param toParse the String to be parsed. + * @param retOnFail default value that is returned if the String could not be parsed. + * @return if the String could be parsed then the int value of the + * String is returned, else retOnFail + */ + public static int tryParseString( + @Nullable + String toParse, + int retOnFail + ) { + + if (toParse == null) { + return retOnFail; + } + Integer parseTry = Ints.tryParse(toParse); + if (parseTry == null) { + return retOnFail; + } + return parseTry; + + } + +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/VelocityController.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/VelocityController.java new file mode 100644 index 0000000..2d41f34 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/VelocityController.java @@ -0,0 +1,418 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +import java.io.Serializable; +import java.util.ArrayDeque; +import java.util.Queue; +import org.opentcs.data.model.Vehicle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Simulates the velocity of a vehicle depending on the length of the way and + * the time it has moved already. + */ +public class VelocityController { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(VelocityController.class); + /** + * The maximum deceleration of the vehicle (in mm/s2). + */ + private int maxDeceleration; + /** + * The maximum acceleration of the vehicle (in mm/s2). + */ + private int maxAcceleration; + /** + * The maximum reverse velocity of the vehicle (in mm/s). + */ + private int maxRevVelocity; + /** + * The maximum forward velocity of the vehicle (in mm/s). + */ + private int maxFwdVelocity; + /** + * The current acceleration (in mm/s2). + */ + private int currentAcceleration; + /** + * The current velocity (in mm/s). + */ + private int currentVelocity; + /** + * The current position (in mm from the beginning of the current way entry). + */ + private long currentPosition; + /** + * The current time, relative to the point of time at which this velocity + * controller was created. + */ + private long currentTime; + /** + * This controller's processing queue. + */ + private final Queue wayEntries = new ArrayDeque<>(); + /** + * True, if the vehicle has been paused, e.g. via the kernel gui + * or a by a client message. + */ + private boolean paused; + + /** + * Creates a new VelocityController. + * + * @param maxDecel The maximum deceleration of the vehicle (in + * mm/s2). + * @param maxAccel The maximum acceleration of the vehicle (in + * mm/s2). + * @param maxRevVelo The maximum reverse velocity of the vehicle (in mm/s). + * @param maxFwdVelo The maximum forward velocity of the vehicle (in mm/s). + */ + public VelocityController( + int maxDecel, + int maxAccel, + int maxRevVelo, + int maxFwdVelo + ) { + maxDeceleration = maxDecel; + maxAcceleration = maxAccel; + maxRevVelocity = maxRevVelo; + maxFwdVelocity = maxFwdVelo; + paused = false; + } + + /** + * Returns the maximum deceleration. + * + * @return The maximum deceleration + */ + public int getMaxDeceleration() { + return maxDeceleration; + } + + /** + * Sets the maximum deceleration. + * + * @param maxDeceleration The new maximum deceleration + */ + public void setMaxDeceleration(int maxDeceleration) { + this.maxDeceleration = maxDeceleration; + } + + /** + * Returns the maximum acceleration. + * + * @return The maximum acceleration + */ + public int getMaxAcceleration() { + return maxAcceleration; + } + + /** + * Sets the maximum acceleration. + * + * @param maxAcceleration The new maximum acceleration + */ + public void setMaxAcceleration(int maxAcceleration) { + this.maxAcceleration = maxAcceleration; + } + + /** + * Returns the maximum reverse velocity. + * + * @return The maximum reverse velocity + */ + public int getMaxRevVelocity() { + return maxRevVelocity; + } + + /** + * Sets the maximum reverse velocity. + * + * @param maxRevVelocity The new maximum reverse velocity + */ + public void setMaxRevVelocity(int maxRevVelocity) { + this.maxRevVelocity = maxRevVelocity; + } + + /** + * Returns the maximum forward velocity. + * + * @return The maximum forward velocity + */ + public int getMaxFwdVelocity() { + return maxFwdVelocity; + } + + /** + * Sets the maximum forward velocity. + * + * @param maxFwdVelocity The new maximum forward velocity + */ + public void setMaxFwdVelocity(int maxFwdVelocity) { + this.maxFwdVelocity = maxFwdVelocity; + } + + /** + * Returns whether the vehicle is paused. + * + * @return paused + */ + public boolean isVehiclePaused() { + return paused; + } + + /** + * Pause the vehicle (i.e. set it's velocity to zero). + * + * @param pause True, if vehicle shall be paused. False, otherwise. + */ + public void setVehiclePaused(boolean pause) { + paused = pause; + } + + /** + * Returns this controller's current velocity. + * + * @return This controller's current velocity. + */ + public int getCurrentVelocity() { + return currentVelocity; + } + + /** + * Returns the vehicle's current position (in mm from the beginning of the + * current way entry. + * + * @return The vehicle's current position (in mm from the beginning of the + * current way entry. + */ + public long getCurrentPosition() { + return currentPosition; + } + + /** + * Returns the current time, relative to to the point of time at which this + * controller was started. + * + * @return The current time, relative to to the point of time at which this + * controller was started. + */ + public long getCurrentTime() { + return currentTime; + } + + /** + * Adds a way entry to this vehicle controller's processing queue. + * + * @param newEntry The way entry to add. + */ + public void addWayEntry(WayEntry newEntry) { + requireNonNull(newEntry, "newEntry"); + + wayEntries.add(newEntry); + } + + /** + * Returns the way entry this velocity controller is currently processing. + * + * @return The way entry this velocity controller is currently processing. If + * the processing queue is currently empty, null is returned. + */ + public WayEntry getCurrentWayEntry() { + return wayEntries.peek(); + } + + /** + * Returns true if, and only if, there are way entries to be + * processed in this velocity controller's queue. + * + * @return true if, and only if, there are way entries to be + * processed in this velocity controller's queue. + */ + public boolean hasWayEntries() { + return !wayEntries.isEmpty(); + } + + /** + * Increase this controller's current time by the given value and simulate + * the events that would happen in this time frame. + * + * @param dt The time by which to advance this controller (in milliseconds). + * Must be at least 1. + */ + public void advanceTime(int dt) { + checkArgument(dt >= 1, "dt is less than 1: %d", dt); + + final long oldPosition = currentPosition; + final int oldVelocity = currentVelocity; + final WayEntry curWayEntry = wayEntries.peek(); + if (curWayEntry == null || paused) { + currentAcceleration = 0; + currentVelocity = 0; + } + else { + final int maxVelocity; + final Vehicle.Orientation orientation = curWayEntry.vehicleOrientation; + switch (orientation) { + case FORWARD: + maxVelocity = maxFwdVelocity; + break; + case BACKWARD: + maxVelocity = maxRevVelocity; + break; + default: + LOG.warn("Unhandled orientation: {}, assuming forward.", orientation); + maxVelocity = maxFwdVelocity; + } + final int targetVelocity = Math.min(curWayEntry.targetVelocity, maxVelocity); + // Accelerate as quickly as possible. + final long accelerationDistance = 10; + // Recompute the acceleration to reach/keep the desired velocity. + currentAcceleration + = (currentVelocity == targetVelocity) ? 0 + : suitableAcceleration(targetVelocity, accelerationDistance); + // Recompute current velocity. + currentVelocity = oldVelocity + currentAcceleration * dt / 1000; + // Recompute current position. + currentPosition = oldPosition + oldVelocity * dt / 1000 + + currentAcceleration * dt * dt / 1000000 / 2; + // Check if we have left the way entry and entered the next. + if (currentPosition >= curWayEntry.length) { + currentPosition -= curWayEntry.length; + wayEntries.poll(); + } + } + // The given time has now passed. + currentTime += dt; + } + + /** + * Returns the acceleration (in mm/s2) needed for reaching a given + * velocity exactly after travelling a given distance (respecting the current + * velocity). + * + * @param targetVelocity The desired velocity (in mm/s). + * @param travelDistance The distance after which the desired velocity is + * supposed to be reached (in mm). Must be a positive value. + * @return The acceleration needed for reaching the given velocity after + * travelling the given distance. + */ + int suitableAcceleration(final int targetVelocity, final long travelDistance) { + if (travelDistance < 1) { + throw new IllegalArgumentException("travelDistance is less than 1"); + } + final double v_current = currentVelocity; + final double v_target = targetVelocity; + final double s = travelDistance; + // Compute travelling time. + // XXX Divide by zero if (v_current == -v_target), especially if both are 0! + final double t = s / (v_current + (v_target - v_current) / 2); + LOG.debug( + "t = " + t + + "; s = " + s + + "; v_current = " + v_current + + "; v_target = " + v_target + ); + // Compute acceleration. + int result = (int) ((v_target - v_current) / t); + LOG.debug("result = " + result); + if (result > maxAcceleration) { + result = maxAcceleration; + } + else if (result < maxDeceleration) { + result = maxDeceleration; + } + return result; + } + + /** + * An entry in a vehicle controller's processing queue. + */ + public static class WayEntry + implements + Serializable { + + /** + * The length of the way to drive (in mm). + */ + private final long length; + /** + * The target velocity on this way (in mm/s). + */ + private final int targetVelocity; + /** + * The name of the destination point. + */ + private final String destPointName; + /** + * The vehicle's orientation on this way. + */ + private final Vehicle.Orientation vehicleOrientation; + + /** + * Creates a new WayEntry. + * + * @param length The length of the way to drive (in mm). + * @param maxVelocity The maximum velocity on this way (in mm/s). + * @param destPointName The name of the destination point. + * @param orientation The vehicle's orientation on this way. + */ + public WayEntry( + long length, + int maxVelocity, + String destPointName, + Vehicle.Orientation orientation + ) { + checkArgument(length > 0, "length is not > 0 but %s", length); + this.length = length; + if (maxVelocity < 1) { + LOG.warn("maxVelocity is zero or negative, setting to 100"); + this.targetVelocity = 100; + } + else { + this.targetVelocity = maxVelocity; + } + this.destPointName = requireNonNull(destPointName, "destPointName"); + this.vehicleOrientation = requireNonNull(orientation, "vehicleOrientation"); + } + + /** + * Returns the name of the destination point. + * + * @return The name of the destination point. + */ + public String getDestPointName() { + return destPointName; + } + + @Override + public boolean equals(Object o) { + if (o instanceof WayEntry) { + WayEntry other = (WayEntry) o; + return other.length == length + && other.targetVelocity == targetVelocity + && destPointName.equals(other.destPointName) + && vehicleOrientation.equals(other.vehicleOrientation); + } + else { + return false; + } + } + + @Override + public int hashCode() { + return (int) (length ^ (length >>> 32)) + ^ targetVelocity + ^ destPointName.hashCode() + ^ vehicleOrientation.hashCode(); + } + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/VirtualVehicleConfiguration.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/VirtualVehicleConfiguration.java new file mode 100644 index 0000000..d4d6f1f --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/VirtualVehicleConfiguration.java @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle; + +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; + +/** + * Provides methods to configure to {@link LoopbackCommunicationAdapter}. + */ +@ConfigurationPrefix(VirtualVehicleConfiguration.PREFIX) +public interface VirtualVehicleConfiguration { + + /** + * This configuration's prefix. + */ + String PREFIX = "virtualvehicle"; + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to enable to register/enable the loopback driver.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_enable" + ) + boolean enable(); + + @ConfigurationEntry( + type = "Integer", + description = "The adapter's command queue capacity.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_NEW_PLANT_MODEL, + orderKey = "1_attributes_1" + ) + int commandQueueCapacity(); + + @ConfigurationEntry( + type = "String", + description = "The string to be treated as a recharge operation.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_NEW_PLANT_MODEL, + orderKey = "1_attributes_2" + ) + String rechargeOperation(); + + @ConfigurationEntry( + type = "Double", + description = "The rate at which the vehicle recharges in percent per second.", + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "1_attributes_3" + ) + double rechargePercentagePerSecond(); + + @ConfigurationEntry( + type = "Double", + description = { + "The simulation time factor.", + "1.0 is real time, greater values speed up simulation." + }, + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "2_behaviour_1" + ) + double simulationTimeFactor(); + + @ConfigurationEntry( + type = "Integer", + description = {"The virtual vehicle's length in mm when it's loaded."}, + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "2_behaviour_2" + ) + int vehicleLengthLoaded(); + + @ConfigurationEntry( + type = "Integer", + description = {"The virtual vehicle's length in mm when it's unloaded."}, + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "2_behaviour_3" + ) + int vehicleLengthUnloaded(); +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/CurrentMovementCommandFailedCommand.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/CurrentMovementCommandFailedCommand.java new file mode 100644 index 0000000..3185f03 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/CurrentMovementCommandFailedCommand.java @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.commands; + +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.MovementCommand; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; + +/** + * A command to notify the loopback adapter the last/current movement command failed. + */ +public class CurrentMovementCommandFailedCommand + implements + AdapterCommand { + + /** + * Creates a new instance. + */ + public CurrentMovementCommandFailedCommand() { + } + + @Override + public void execute(VehicleCommAdapter adapter) { + MovementCommand failedCommand = adapter.getSentCommands().peek(); + if (failedCommand != null) { + adapter.getProcessModel().commandFailed(adapter.getSentCommands().peek()); + } + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/PublishEventCommand.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/PublishEventCommand.java new file mode 100644 index 0000000..52689be --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/PublishEventCommand.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.commands; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; +import org.opentcs.drivers.vehicle.VehicleCommAdapterEvent; + +/** + * A command to publish {@link VehicleCommAdapterEvent}s. + */ +public class PublishEventCommand + implements + AdapterCommand { + + /** + * The event to publish. + */ + private final VehicleCommAdapterEvent event; + + /** + * Creates a new instance. + * + * @param event The event to publish. + */ + public PublishEventCommand( + @Nonnull + VehicleCommAdapterEvent event + ) { + this.event = requireNonNull(event, "event"); + } + + @Override + public void execute(VehicleCommAdapter adapter) { + adapter.getProcessModel().publishEvent(event); + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetEnergyLevelCommand.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetEnergyLevelCommand.java new file mode 100644 index 0000000..6350248 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetEnergyLevelCommand.java @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.commands; + +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; + +/** + * A command to set a vehicle's energy level. + */ +public class SetEnergyLevelCommand + implements + AdapterCommand { + + /** + * The energy level to set. + */ + private final int level; + + /** + * Creates a new instance. + * + * @param level The energy level to set. + */ + public SetEnergyLevelCommand(int level) { + this.level = level; + } + + @Override + public void execute(VehicleCommAdapter adapter) { + adapter.getProcessModel().setEnergyLevel(level); + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetLoadHandlingDevicesCommand.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetLoadHandlingDevicesCommand.java new file mode 100644 index 0000000..7bfaabd --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetLoadHandlingDevicesCommand.java @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.commands; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.List; +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.LoadHandlingDevice; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; + +/** + * A command to set the {@link LoadHandlingDevice}s attached to a vehicle. + */ +public class SetLoadHandlingDevicesCommand + implements + AdapterCommand { + + /** + * The list of load handling devices. + */ + private final List devices; + + /** + * Creates a new instance. + * + * @param devices The list of load handling devices. + */ + public SetLoadHandlingDevicesCommand( + @Nonnull + List devices + ) { + this.devices = requireNonNull(devices, "devices"); + } + + @Override + public void execute(VehicleCommAdapter adapter) { + adapter.getProcessModel().setLoadHandlingDevices(devices); + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetOrientationAngleCommand.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetOrientationAngleCommand.java new file mode 100644 index 0000000..085b03f --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetOrientationAngleCommand.java @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.commands; + +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; + +/** + * A command to set a vehicle's orientation angle. + */ +public class SetOrientationAngleCommand + implements + AdapterCommand { + + /** + * The orientation angle to set. + */ + private final double angle; + + /** + * Creates a new instance. + * + * @param angle The orientation angle to set. + */ + public SetOrientationAngleCommand(double angle) { + this.angle = angle; + } + + @Override + public void execute(VehicleCommAdapter adapter) { + adapter.getProcessModel().setPose( + adapter.getProcessModel().getPose().withOrientationAngle(angle) + ); + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetPositionCommand.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetPositionCommand.java new file mode 100644 index 0000000..162c1aa --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetPositionCommand.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.commands; + +import jakarta.annotation.Nullable; +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; + +/** + * A command to set a vehicle's position. + */ +public class SetPositionCommand + implements + AdapterCommand { + + /** + * The position to set. + */ + private final String position; + + /** + * Creates a new instance. + * + * @param position The position to set. + */ + public SetPositionCommand( + @Nullable + String position + ) { + this.position = position; + } + + @Override + public void execute(VehicleCommAdapter adapter) { + adapter.getProcessModel().setPosition(position); + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetPrecisePositionCommand.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetPrecisePositionCommand.java new file mode 100644 index 0000000..781a104 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetPrecisePositionCommand.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.commands; + +import jakarta.annotation.Nullable; +import org.opentcs.data.model.Triple; +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; + +/** + * A command to set a vehicle's precise position. + */ +public class SetPrecisePositionCommand + implements + AdapterCommand { + + /** + * The percise position to set. + */ + private final Triple position; + + /** + * Creates a new instance. + * + * @param position The precise position to set. + */ + public SetPrecisePositionCommand( + @Nullable + Triple position + ) { + this.position = position; + } + + @Override + public void execute(VehicleCommAdapter adapter) { + adapter.getProcessModel().setPose( + adapter.getProcessModel().getPose().withPosition(position) + ); + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetSingleStepModeEnabledCommand.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetSingleStepModeEnabledCommand.java new file mode 100644 index 0000000..5f4d683 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetSingleStepModeEnabledCommand.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.commands; + +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; +import org.opentcs.virtualvehicle.LoopbackCommunicationAdapter; + +/** + * A command to enable/disable the comm adapter's single step mode. + */ +public class SetSingleStepModeEnabledCommand + implements + AdapterCommand { + + /** + * Whether to enable/disable single step mode. + */ + private final boolean enabled; + + /** + * Creates a new instance. + * + * @param enabled Whether to enable/disable single step mode. + */ + public SetSingleStepModeEnabledCommand(boolean enabled) { + this.enabled = enabled; + } + + @Override + public void execute(VehicleCommAdapter adapter) { + if (!(adapter instanceof LoopbackCommunicationAdapter)) { + return; + } + + LoopbackCommunicationAdapter loopbackAdapter = (LoopbackCommunicationAdapter) adapter; + loopbackAdapter.getProcessModel().setSingleStepModeEnabled(enabled); + } + +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetStateCommand.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetStateCommand.java new file mode 100644 index 0000000..62434ec --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetStateCommand.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.commands; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; + +/** + * A command to set a vehicle's state. + */ +public class SetStateCommand + implements + AdapterCommand { + + /** + * The vehicle state to set. + */ + private final Vehicle.State state; + + /** + * Creates a new instance. + * + * @param state The vehicle state to set. + */ + public SetStateCommand( + @Nonnull + Vehicle.State state + ) { + this.state = requireNonNull(state, "state"); + } + + @Override + public void execute(VehicleCommAdapter adapter) { + adapter.getProcessModel().setState(state); + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetVehiclePausedCommand.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetVehiclePausedCommand.java new file mode 100644 index 0000000..5a597a2 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetVehiclePausedCommand.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.commands; + +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; +import org.opentcs.virtualvehicle.LoopbackCommunicationAdapter; + +/** + * A command to pause/unpause the vehicle. + */ +public class SetVehiclePausedCommand + implements + AdapterCommand { + + /** + * Whether to pause/unpause the vehicle. + */ + private final boolean paused; + + /** + * Creates a new instance. + * + * @param paused Whether to pause/unpause the vehicle. + */ + public SetVehiclePausedCommand(boolean paused) { + this.paused = paused; + } + + @Override + public void execute(VehicleCommAdapter adapter) { + if (!(adapter instanceof LoopbackCommunicationAdapter)) { + return; + } + + LoopbackCommunicationAdapter loopbackAdapter = (LoopbackCommunicationAdapter) adapter; + loopbackAdapter.getProcessModel().setVehiclePaused(paused); + } + +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetVehiclePropertyCommand.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetVehiclePropertyCommand.java new file mode 100644 index 0000000..2ebff87 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/SetVehiclePropertyCommand.java @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.commands; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; + +/** + * A command to set a vehicle's property. + */ +public class SetVehiclePropertyCommand + implements + AdapterCommand { + + /** + * The property key to set. + */ + private final String key; + /** + * The property value to set. + */ + private final String value; + + /** + * Creates a new instance. + * + * @param key The property key to set. + * @param value The property value to set. + */ + public SetVehiclePropertyCommand( + @Nonnull + String key, + @Nullable + String value + ) { + this.key = requireNonNull(key, "key"); + this.value = value; + } + + @Override + public void execute(VehicleCommAdapter adapter) { + adapter.getProcessModel().setProperty(key, value); + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/TriggerCommand.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/TriggerCommand.java new file mode 100644 index 0000000..6bdf810 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/commands/TriggerCommand.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.commands; + +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; +import org.opentcs.virtualvehicle.LoopbackCommunicationAdapter; + +/** + * A command to trigger the comm adapter in single step mode. + */ +public class TriggerCommand + implements + AdapterCommand { + + /** + * Creates a new instance. + */ + public TriggerCommand() { + } + + @Override + public void execute(VehicleCommAdapter adapter) { + if (!(adapter instanceof LoopbackCommunicationAdapter)) { + return; + } + + LoopbackCommunicationAdapter loopbackAdapter = (LoopbackCommunicationAdapter) adapter; + loopbackAdapter.trigger(); + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/DropdownListInputPanel.form b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/DropdownListInputPanel.form new file mode 100644 index 0000000..d6cf791 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/DropdownListInputPanel.form @@ -0,0 +1,64 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/DropdownListInputPanel.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/DropdownListInputPanel.java new file mode 100644 index 0000000..beaeb95 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/DropdownListInputPanel.java @@ -0,0 +1,277 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.inputcomponents; + +import static java.util.Objects.requireNonNull; + +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import javax.swing.DefaultComboBoxModel; +import javax.swing.ListCellRenderer; + +/** + * An input panel providing a dropdown list and optionally a message and a + * label. + * + * The Object that is returned by {@link InputPanel#getInput} is + * an object from the provided content list. + * + * @param Type of the elements in the dropdown list + */ +public final class DropdownListInputPanel + extends + InputPanel { + + /** + * Create a new panel. + * + * @param title Title of the panel. + */ + private DropdownListInputPanel(String title) { + super(title); + initComponents(); + } + + @Override + protected void captureInput() { + // If the combobox is editable and the input was entered using the jTextField and confirmed + // using the enter button, then the textField's input is not yet saved as the comboBox + // selection. + // That's why it is safer to get the input from the textfield, if the combobox is editable. + input = comboBox.isEditable() ? comboBox.getEditor().getItem() : comboBox.getSelectedItem(); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + messageLabel = new javax.swing.JLabel(); + label = new javax.swing.JLabel(); + comboBox = new javax.swing.JComboBox<>(); + + setLayout(new java.awt.GridBagLayout()); + + messageLabel.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + messageLabel.setText("Message"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(messageLabel, gridBagConstraints); + + label.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + label.setText("Label"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(label, gridBagConstraints); + + comboBox.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(comboBox, gridBagConstraints); + }// //GEN-END:initComponents + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JComboBox comboBox; + private javax.swing.JLabel label; + private javax.swing.JLabel messageLabel; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON + + /** + * See {@link InputPanel.Builder}. + * + * @param Type of the elements in the dropdown list + */ + public static class Builder + implements + InputPanel.Builder { + + /** + * The panel's title. + */ + private final String title; + /** + * The optional message. + */ + private String message; + /** + * The label for the dropdown list. + */ + private String label; + /** + * Content for the dropdown list. + */ + private final List content; + /** + * Initially selected index of the dropdown list. + * Default is 0. + */ + private int initialIndex; + /** + * Whether the combo box should be editable. + */ + private boolean editable; + /** + * Strategy for presenting the E-Objects in View. + */ + private ListCellRenderer renderer; + /** + * Function for representing a selected element in the combo box. + * This is basically a renderer for the combo box editor's content. + */ + private Function selectionRepresenter = o -> o == null ? "" : o.toString(); + + /** + * Create a new Builder. + * + * @param title The title of the panel. + * @param content List of items. + */ + public Builder(String title, List content) { + this.title = requireNonNull(title, "title"); + this.content = requireNonNull(content, "content"); + } + + @Override + public InputPanel build() { + DropdownListInputPanel panel = new DropdownListInputPanel<>(title); + panel.messageLabel.setText(message); + panel.label.setText(label); + panel.comboBox.setEditable(editable); + DefaultComboBoxModel model = new DefaultComboBoxModel<>(); + panel.comboBox.setModel(model); + + if (editable) { + EditableComboBoxEditor editor = new EditableComboBoxEditor<>( + Collections.unmodifiableList(panel.getValidationListeners()), + panel.comboBox, selectionRepresenter + ); + panel.comboBox.setEditor(editor); + model.addListDataListener(editor); + } + + //to notify the editor about new input + for (E c : content) { + model.addElement(c); + } + + if (this.renderer != null) { + panel.comboBox.setRenderer(this.renderer); + } + if (!content.isEmpty()) { + panel.comboBox.setSelectedIndex(initialIndex); + } + return panel; + } + + /** + * Set the message of the panel. + * The user of this method must take care for the line breaks in the message, as it is not + * wrapped automatically! + * + * @param message The message. + * @return The builder instance. + */ + public Builder setMessage(String message) { + this.message = message; + return this; + } + + /** + * Sets the editable flag for the combo box. + * + * @param editable The editable flag. + * @return The builder instance. + */ + public Builder setEditable(boolean editable) { + this.editable = editable; + return this; + } + + /** + * Set the CellRenderer for the dropdown list. + * if none is set, then the default renderer will be used, which calls toString(). + * + * @param renderer The renderer. + * @return The builder instance. + */ + public Builder setRenderer(ListCellRenderer renderer) { + this.renderer = renderer; + return this; + } + + /** + * Set the label of the panel. + * + * @param label The label. + * @return The builder instance. + */ + public Builder setLabel(String label) { + this.label = label; + return this; + } + + /** + * Set the initial selected list entry. + * + * @param index Must be > 0, will have no effect otherwise. + * @return The builder instance. + */ + public Builder setInitialSelection(int index) { + if (index >= 0) { + initialIndex = index; + } + return this; + } + + /** + * Set the initial selected list entry. + * + * @param element Element to select. Selection remains unchanged if element is not in drop down + * list or element is null and the content list does not allow null values. + * @return The builder instance. + */ + public Builder setInitialSelection(Object element) { + int index; + try { + index = content.indexOf(element); + } + catch (NullPointerException e) { + index = -1; + } + return setInitialSelection(index); + } + + /** + * Sets the representer for selected elements in the combo box. + * This is basically a renderer for the combo box editor's content. + * If none is set, a default representer will be used, which calls toString(). + * + * @param selectionRepresenter Function for representing a selected element in the combo box. + * @return The builder instance. + */ + public Builder setSelectionRepresenter(Function selectionRepresenter) { + this.selectionRepresenter = selectionRepresenter; + return this; + } + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/EditableComboBoxEditor.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/EditableComboBoxEditor.java new file mode 100644 index 0000000..ea94aea --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/EditableComboBoxEditor.java @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.inputcomponents; + +import static java.util.Objects.requireNonNull; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import javax.swing.ComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; +import javax.swing.plaf.basic.BasicComboBoxEditor; + +/** + * An editor for editable combo boxes. + */ +public class EditableComboBoxEditor + extends + BasicComboBoxEditor + implements + ListDataListener { + + /** + * Represents the model of the comboBox as a set. + */ + private final Set content = new HashSet<>(); + /** + * The relevant combobox. + */ + private final JComboBox comboBox; + /** + * Returns the string representation for the combo box's selected item. + */ + private final Function representer; + + /** + * Creates and instance and configures an {@link EditableComboBoxListener} for the editor. + * + * @param validationListeners validation listeners. + * @param comboBox the comboBox that is edited. + * @param representer Returns the string representation for the combo box's selected item. + */ + public EditableComboBoxEditor( + List validationListeners, + JComboBox comboBox, + Function representer + ) { + this.comboBox = requireNonNull(comboBox, "comboBox"); + this.representer = requireNonNull(representer, "representer"); + editor.getDocument().addDocumentListener( + new EditableComboBoxListener<>( + content, + validationListeners, + editor, + representer + ) + ); + } + + @Override + public void intervalAdded(ListDataEvent e) { + loadContent(); + } + + @Override + public void intervalRemoved(ListDataEvent e) { + loadContent(); + } + + @Override + public void contentsChanged(ListDataEvent e) { + loadContent(); + } + + private void loadContent() { + //get the current comboBoxModel and add the modelelements to content + ComboBoxModel model = comboBox.getModel(); + for (int i = 0; i < model.getSize(); i++) { + + content.add(model.getElementAt(i)); + + } + } + + @Override + public Object getItem() { + for (E p : content) { + if (representer.apply(p).equals(editor.getText())) { + return p; + } + } + return null; + } + + @Override + @SuppressWarnings("unchecked") + public void setItem(Object anObject) { + editor.setText(representer.apply((E) anObject)); + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/EditableComboBoxListener.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/EditableComboBoxListener.java new file mode 100644 index 0000000..0ca5a19 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/EditableComboBoxListener.java @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.inputcomponents; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import javax.swing.JTextField; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +/** + * A validator for the input of the textfield. + */ +public class EditableComboBoxListener + implements + DocumentListener { + + private final JTextField textField; + private final List validationListeners; + private final Set content; + + /** + * Returns the string representation for the combo box's selected item. + */ + private final Function representer; + + /** + * Creates an instance. + * + * @param content the elements of the comboBox' dropdownlist. + * @param validationListeners the listeners to be notified about the validity of the user input. + * @param textField the textfield with the input to be validated. + * @param representer Returns the string representation for the combo box's selected item. + */ + public EditableComboBoxListener( + Set content, + List validationListeners, + JTextField textField, + Function representer + ) { + + this.content = requireNonNull(content, "content"); + this.validationListeners = requireNonNull(validationListeners, "validationListeners"); + this.textField = requireNonNull(textField, "textField"); + this.representer = requireNonNull(representer, "representer"); + + } + + @Override + public void insertUpdate(DocumentEvent e) { + validate(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + validate(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + validate(); + } + + private void notifyValidationListeners(boolean isValid) { + for (ValidationListener valListener : validationListeners) { + valListener.validityChanged(new ValidationEvent(this, isValid)); + } + } + + private void validate() { + if (textField.getText().equals("")) { + notifyValidationListeners(true); + return; + } + + for (E element : content) { + if (representer.apply(element).equals(textField.getText())) { + notifyValidationListeners(true); + return; + } + } + notifyValidationListeners(false); + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/InputDialog.form b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/InputDialog.form new file mode 100644 index 0000000..4e83a6e --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/InputDialog.form @@ -0,0 +1,91 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/InputDialog.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/InputDialog.java new file mode 100644 index 0000000..ea0861a --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/InputDialog.java @@ -0,0 +1,214 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.inputcomponents; + +import java.awt.BorderLayout; +import javax.swing.border.EmptyBorder; + +/** + * A generic dialog for user input. It has two or three buttons + * (Ok, Cancel, reset(optional)) and acts as a frame for an + * {@link InputPanel}, that must be provided when + * creating the dialog. InputDialog listens to + * InputPanel's {@link ValidationEvent} to dynamically + * enable/disable the ok-button if the panel validates it's input. + */ +public class InputDialog + extends + javax.swing.JDialog + implements + ValidationListener { + + /** + * The panel that contains the content of the dialog. + */ + private final InputPanel panel; + /** + * Return status of the dialog. Will be set when the dialog is beeing closed. + */ + private ReturnStatus returnStatus; + + /** + * Enum values to indicate if the dialog accepted input from the user or + * if the input was canceled. + * TODO: no enum needed here? + */ + public enum ReturnStatus { + + /** + * The Dialog was cancelled. No input available. + */ + CANCELED, + /** + * The input was accepted and is available. + */ + ACCEPTED + } + + /** + * Create a new instance of InputDialog. + * + * @param panel the panel to be displayed in the dialog. + */ + @SuppressWarnings("this-escape") + public InputDialog(InputPanel panel) { + super(); + initComponents(); + setLocationRelativeTo(null); + // Set up embedded panel + this.panel = panel; + setTitle(panel.getTitle()); + panel.setBorder(new EmptyBorder(6, 6, 10, 6)); + // Setup dialog + getContentPane().add(panel, BorderLayout.CENTER); + getRootPane().setDefaultButton(okButton); + if (!panel.isResetable()) { + resetButton.setVisible(false); + } + pack(); + // Init validation value manually + panel.addValidationListener(this); + } + + @Override + public void validityChanged(ValidationEvent e) { + okButton.setEnabled(e.valid()); + } + + /** + * Get the return status of the dialog that indicates if there is input + * available via {@link #getInput()}. + * If the return status is {@link ReturnStatus#ACCEPTED ACCEPTED}, + * the panels input was captured + * or reset and is available through {@link #getInput()}. + * If the return status is {@link ReturnStatus#CANCELED CANCELLED}, + * {@link #getInput()} should not + * be called as the dialog was canceled and there is no valid input available. + * If the dialog wasn't closed yet, null will be returned. + * + * @return the return status + */ + public ReturnStatus getReturnStatus() { + return returnStatus; + } + + /** + * Get the input from the embedded panel. This is the same as calling + * {@link InputPanel#getInput()} directly. + * Prior to calling this method you should check if there even is any input + * (see {@link #getReturnStatus()}). + * + * @return The input from the panel + */ + public Object getInput() { + return panel.getInput(); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + + buttonPanel = new javax.swing.JPanel(); + okButton = new javax.swing.JButton(); + cancelButton = new javax.swing.JButton(); + resetButton = new javax.swing.JButton(); + + setModal(true); + setResizable(false); + addWindowListener(new java.awt.event.WindowAdapter() { + public void windowClosing(java.awt.event.WindowEvent evt) { + dialogClosing(evt); + } + }); + + buttonPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(1, 1, 1, 1)); + + okButton.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/commadapter/loopback/Bundle"); // NOI18N + okButton.setText(bundle.getString("inputDialog.button_ok.text")); // NOI18N + okButton.setName("inputDialogOkButton"); // NOI18N + okButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + okButtonActionPerformed(evt); + } + }); + buttonPanel.add(okButton); + + cancelButton.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + cancelButton.setText(bundle.getString("inputDialog.button_cancel.text")); // NOI18N + cancelButton.setName("inputDialogCancelButton"); // NOI18N + cancelButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cancelButtonActionPerformed(evt); + } + }); + buttonPanel.add(cancelButton); + + resetButton.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + resetButton.setText(bundle.getString("inputDialog.button_reset.text")); // NOI18N + resetButton.setName("inputDialogResetButton"); // NOI18N + resetButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + resetButtonActionPerformed(evt); + } + }); + buttonPanel.add(resetButton); + + getContentPane().add(buttonPanel, java.awt.BorderLayout.PAGE_END); + + pack(); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed + doClose(ReturnStatus.CANCELED); + }//GEN-LAST:event_cancelButtonActionPerformed + + private void okButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_okButtonActionPerformed + panel.captureInput(); + doClose(ReturnStatus.ACCEPTED); + }//GEN-LAST:event_okButtonActionPerformed + + private void resetButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_resetButtonActionPerformed + panel.doReset(); + doClose(ReturnStatus.ACCEPTED); + }//GEN-LAST:event_resetButtonActionPerformed + + /** + * Handler for the WindowClosing event. + * Called when the user closes the dialog via the X-Button or F4. + * + * @param evt WindowEvent + */ + private void dialogClosing(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_dialogClosing + doClose(ReturnStatus.CANCELED); + }//GEN-LAST:event_dialogClosing + + /** + * Close the dialog properly and set the return status to indicate + * how/why it was closed. + */ + private void doClose(ReturnStatus retStatus) { + returnStatus = retStatus; + setVisible(false); + dispose(); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel buttonPanel; + private javax.swing.JButton cancelButton; + private javax.swing.JButton okButton; + private javax.swing.JButton resetButton; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/InputPanel.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/InputPanel.java new file mode 100644 index 0000000..0a40ac8 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/InputPanel.java @@ -0,0 +1,197 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.inputcomponents; + +import java.util.ArrayList; +import java.util.List; + +/** + * Base class for panels that provide input methods for the user and can be + * embedded in an {@link InputDialog}. + */ +public abstract class InputPanel + extends + javax.swing.JPanel { + + /** + * Object to store the user's input. Must be explicitly set via captureInput() + * or doReset(). What exactly this input is depends on the actual + * implementation of InputPanel's subclass. + */ + protected Object input; + /** + * If true, the panel's state/input can be reset via doReset() to a default + * value. + * By default InputPanels are not resetable. + */ + protected boolean resetable; + /** + * List of ValidationListeners that will receive ValidationEvents. + */ + private final List validationListeners = new ArrayList<>(); + /** + * Title of the panel. Might be used by the surrounding dialog. + */ + private final String title; + /** + * Indicates whether the current user input is valid. + * If the panel validates the input this can be changed via setInputValid(). + */ + private boolean inputValid = true; + + /** + * Create a new instance of InputPanel. + * + * @param title The title of this panel. + */ + public InputPanel(String title) { + this.title = title; + } + + /** + * Get the title of this panel. + * + * @return The title + */ + public String getTitle() { + return title; + } + + /** + * Add a {@link ValidationListener} that will receive + * {@link ValidationEvent ValidationEvents} + * whenever the validity of the input in this panel changes. The + * {@link ValidationListener} will receive a {@link ValidationEvent} with the + * current validity state immediately after beeing added. + * If the panel does not validate it's input the validity will never change. + * + * @param listener The {@link ValidationListener} + */ + public void addValidationListener(ValidationListener listener) { + validationListeners.add(listener); + // Fire initial validation event for this listener + listener.validityChanged(new ValidationEvent(this, inputValid)); + } + + /** + * Mark the input of the panel as valid/invalid and send + * {@link ValidationEvent ValidationEvents} + * to the attached {@link ValidationListener ValidationListeners}. + * The Validity should only be changed via this method! + * + * @param valid true, if input is valid. false otherwise. + */ + protected void setInputValid(boolean valid) { + boolean changed = valid != inputValid; + inputValid = valid; + if (changed) { + ValidationEvent e = new ValidationEvent(this, valid); + for (ValidationListener l : validationListeners) { + l.validityChanged(e); + } + } + } + + /** + * + * @return The validation Listeners. + */ + public List getValidationListeners() { + return validationListeners; + } + + /** + * Determine if the current input in the panel is valid. + * If the input isn't validated this will always return true. + * + * @see #addValidationListener + * @return true if input is valid, false otherwise. + */ + public boolean isInputValid() { + return inputValid; + } + + /** + * Get the user input from the panel. If the input wasn't captured before + * (see {@link #captureInput()}) null is returned. Otherwise it depends on the + * concrete implementing panel what the input can look like. + * + * @return The input + */ + public Object getInput() { + return input; + } + + /** + * Tells the panel to get and store the user input which will be available + * via {@link #getInput()} afterwards. + * Usually this method should be called from the enclosing dialog when the + * ok button is pressed. It is not intended to be used by the user of the + * panel! + */ + protected abstract void captureInput(); + + /** + * Returns whether the content of this panel can be reset to a default value. + * If the panel is resetable the enclosing dialog might want to show a reset + * button. + * + * @see #doReset() + * @return panel is resetable? + */ + public boolean isResetable() { + return resetable; + } + + /** + * Inform the panel, that it should reset it's input values (probably because + * the reset button in the enclosing dialog was pressed). + * It's up to the specific panel itself to decide what is resetted.´ + * The default implementation does nothing. + * It should be overwritten in subclasses if reset functionality is needed. + */ + public void doReset() { + // Do nothing here. + } + + /** + *

+ * An interface that can be used to implement the builder-pattern + * (see Joshua Bloch's Effective Java). + *
+ * As an InputPanel might have many required and/or optional + * parameters it can be more convinient to use a Builder class instead of + * public constructors. + * A builder should implement a public constructor with required parameters + * for the panel and public setters for optional parameters. + * The InputPanel is created by the {@link #build} method. + *
+ * For an example implementation see {@link SingleTextInputPanel.Builder}. + *

+ *

+ * Usage: + *

+ *
    + *
  1. Instanciate the builder, passing required parameters to the + * constructor.
  2. + *
  3. Set optional parameters via the other public methods.
  4. + *
  5. Actually build the panel according to the previously specified + * parameters using the build() method.
  6. + *
+ *

+ * Note: + * The parameter methods should always return the builder itself, so the + * creation of a panel can be done in a single statement (see the + * Builder-Pattern in Joshua Bloch's Effective Java). + *

+ */ + public interface Builder { + + /** + * Finally build the {@link InputPanel} as described by this Builder. + * + * @return The created InputPanel. + */ + InputPanel build(); + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/SingleTextInputPanel.form b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/SingleTextInputPanel.form new file mode 100644 index 0000000..1f837d7 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/SingleTextInputPanel.form @@ -0,0 +1,80 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/SingleTextInputPanel.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/SingleTextInputPanel.java new file mode 100644 index 0000000..3e26d7a --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/SingleTextInputPanel.java @@ -0,0 +1,278 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.inputcomponents; + +/** + * A general input panel with a text field and optionally: + *
    + *
  • a message/text
  • + *
  • a label
  • + *
  • a unit label for the input
  • + *
+ * The input of the text field can be validated + * (see {@link Builder#enableValidation enableValidation}). + *
+ * For instanciation the contained + * {@link SingleTextInputPanel.Builder Builder}-class must be used. + *
+ * The Object that is returned by {@link InputPanel#getInput} is + * a String (the text in the text field). + */ +public final class SingleTextInputPanel + extends + TextInputPanel { + + /** + * If the panel is resetable this is the value the input is set to when + * doReset() is called. + */ + private Object resetValue; + + /** + * Creates new instance of SingleTextInputPanel. + * The given title is not used in the panel itselft but can be used by + * the enclosing component. + * + * @param title The title of the panel. + */ + private SingleTextInputPanel(String title) { + super(title); + initComponents(); + } + + /** + * Enable input validation against the given regular expression. + * + * @see InputPanel#addValidationListener + * @param format A regular expression. + */ + private void enableInputValidation(String format) { + if (format != null) { + inputField.getDocument().addDocumentListener(new TextInputValidator(format)); + } + } + + @Override + protected void captureInput() { + input = inputField.getText(); + } + + @Override + public void doReset() { + input = resetValue; + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + unitLabel = new javax.swing.JLabel(); + label = new javax.swing.JLabel(); + inputField = new javax.swing.JTextField(); + messageLabel = new javax.swing.JLabel(); + + setLayout(new java.awt.GridBagLayout()); + + unitLabel.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + unitLabel.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); + unitLabel.setText("Unit-Label"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(unitLabel, gridBagConstraints); + + label.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + label.setText("Label"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(label, gridBagConstraints); + + inputField.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + inputField.setHorizontalAlignment(javax.swing.JTextField.RIGHT); + inputField.setText("initial Value"); + inputField.setPreferredSize(new java.awt.Dimension(70, 20)); + inputField.addFocusListener(new java.awt.event.FocusAdapter() { + public void focusGained(java.awt.event.FocusEvent evt) { + inputFieldFocusGained(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(inputField, gridBagConstraints); + + messageLabel.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + messageLabel.setText("Message"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(messageLabel, gridBagConstraints); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void inputFieldFocusGained(java.awt.event.FocusEvent evt) {//GEN-FIRST:event_inputFieldFocusGained + inputField.selectAll(); + }//GEN-LAST:event_inputFieldFocusGained + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JTextField inputField; + private javax.swing.JLabel label; + private javax.swing.JLabel messageLabel; + private javax.swing.JLabel unitLabel; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON + + /** + * See {@link InputPanel.Builder}. + */ + public static class Builder + implements + InputPanel.Builder { + + /** + * The panel's title. + */ + private final String title; + /** + * Label of for the text field. + */ + private String label; + /** + * Unit label of the text field. + */ + private String unitLabel; + /** + * Initial value for the text field. + */ + private String initialValue; + /** + * Message to be displayed in the panel. + */ + private String message; + /** + * Regex for validation of the text field's content. + */ + private String format; + /** + * Show a reset button in the panel. + * Default is false. + */ + private boolean resetButton; + /** + * Value the input is reset to when the reset button is used. + */ + private Object resetValue; + + /** + * Create a new builder. + * + * @param title Title of the panel. + */ + public Builder(String title) { + this.title = title; + } + + @Override + public InputPanel build() { + SingleTextInputPanel panel = new SingleTextInputPanel(title); + panel.enableInputValidation(format); + panel.label.setText(label); + panel.unitLabel.setText(unitLabel); + panel.inputField.setText(initialValue); + panel.messageLabel.setText(message); + panel.resetable = resetButton; + if (panel.resetable) { + panel.resetValue = resetValue; + } + return panel; + } + + /** + * Set the label of the panel. + * + * @param label The Label + * @return the instance of this Builder + */ + public Builder setLabel(String label) { + this.label = label; + return this; + } + + /** + * Set the initial value for the text field of the panel. + * + * @param initialValue the initial value + * @return the instance of this Builder + */ + public Builder setInitialValue(String initialValue) { + this.initialValue = initialValue; + return this; + } + + /** + * Set the text for the unit label of the panel. + * + * @param unitLabel the unit + * @return the instance of this Builder + */ + public Builder setUnitLabel(String unitLabel) { + this.unitLabel = unitLabel; + return this; + } + + /** + * Set the message of the panel. + * The user of this method must take care for the line breaks in the message, + * as it is not wrapped automatically! + * + * @param message the message + * @return the instance of this Builder + */ + public Builder setMessage(String message) { + this.message = message; + return this; + } + + /** + * Make the panel validate it's input. + * + * @param format The regular expression that will be used for validation. + * @return the instance of this Builder + */ + public Builder enableValidation(String format) { + this.format = format; + return this; + } + + /** + * Set a value the panel's input can be reset to. + * + * @param resetValue the reset value + * @return the instance of this Builder + */ + public Builder enableResetButton(Object resetValue) { + this.resetButton = true; + this.resetValue = resetValue; + return this; + } + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/TextInputPanel.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/TextInputPanel.java new file mode 100644 index 0000000..7e8b245 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/TextInputPanel.java @@ -0,0 +1,176 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.inputcomponents; + +import java.util.Objects; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Abstract base class for InputPanels that use text fields for input. + * The main purpose of this class is to provide an easy to use way to validate + * text inputs using it's nested class {@link TextInputPanel.TextInputValidator + * TextInputValidator}. + */ +public abstract class TextInputPanel + extends + InputPanel { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(TextInputPanel.class); + + /** + * Create a new instance of TextInputPanel. + * + * @param title The title of this panel. + */ + public TextInputPanel(String title) { + super(title); + } + + /** + *

+ * Mark the input of the specified Document as valid/invalid + * and send {@link ValidationEvent ValidationEvents} to the attached + * {@link ValidationListener ValidationListeners}. + * The Document should be related to an input component in this + * panel. + *

+ *

+ * Note:
+ * The default implementation just forwards the call to + * {@link InputPanel#setInputValid(boolean)} without respect to the + * Document. Therefore subclasses with multiple Documents + * should overwrite this method to for example check if all input + * fields are valid and then decide if + * {@link InputPanel#setInputValid(boolean)} should be called or not. + *

+ * + * @param valid true, if the content of the Document is valid + * @param doc the Document + */ + protected void setInputValid(boolean valid, Document doc) { + setInputValid(valid); + } + + /** + *

+ * A {@link javax.swing.event.DocumentListener DocumentListener} that can be + * used by subclasses of {@link TextInputPanel} to validate input in + * {@link javax.swing.JTextField JTextFields} and other Components that + * use {@link javax.swing.text.Document Documents}. + * It listens to the DocumentEvents of a + * Document and validates the Document's + * content against a specified regular expression. Some convenient regular + * expressions are provided as public static variables. + * After validation {@link TextInputPanel#setInputValid(boolean, + * javax.swing.text.Document)} is called. + *

+ *

+ * Note:
+ * The, for convenience, provided regular expressions do + * NOT check whether the given number really fits into the range of the + * corresponding data type (e.g. int for REGEX_INT). + *

+ * + * @see TextInputPanel#setInputValid(boolean, javax.swing.text.Document) + */ + public class TextInputValidator + implements + DocumentListener { + + /** + * Regular expression that accepts a floating point number of arbitary length. + * The decimal point and positions after it can be omitted. + *

+ * Examples: + *

+ *
    + *
  • 3.0 is valid
  • + *
  • -3 is valid
  • + *
  • 3. is invalid
  • + *
  • .3 is invalid
  • + *
+ */ + public static final String REGEX_FLOAT = "[-+]?[0-9]+(\\.[0-9]+)?"; + /** + * Regular expression that accepts a positive floating point number of arbitrary length and 0. + */ + public static final String REGEX_FLOAT_POS = "\\+?[0-9]+(\\.[0-9]+)?"; + /** + * Regular expression that accepts a negative floating point number of arbitrary length and 0. + */ + public static final String REGEX_FLOAT_NEG = "-[0-9]+(\\.[0-9]+)?|0+(\\.0+)?"; + /** + * Regular expression that accepts any integer of arbitrary length. + */ + public static final String REGEX_INT = "[-+]?[0-9]+"; + /** + * Regular expression that accepts any positive integer of arbitrary length and 0. + */ + public static final String REGEX_INT_POS = "\\+?[0-9]+"; + /** + * Regular expression that accepts any negative integer of arbitrary length and 0. + */ + public static final String REGEX_INT_NEG = "-[0-9]+|0+"; + /** + * Regular expression that accepts an integer in the interval [0,100]. + */ + public static final String REGEX_INT_RANGE_0_100 = "[0-9]|[1-9][0-9]|100"; + /** + * Regular expression that accepts anything except an empty (or whitespace-only) string. + */ + public static final String REGEX_NOT_EMPTY = ".*\\S.*"; + /** + * Regular expression to validate the documents text against. + */ + private final String format; + + /** + * Create an instance of TextInputValidator. + * + * @param format The regular expression to use for validation. + */ + protected TextInputValidator(String format) { + this.format = Objects.requireNonNull(format); + } + + @Override + public void insertUpdate(DocumentEvent e) { + validate(e.getDocument()); + } + + @Override + public void removeUpdate(DocumentEvent e) { + validate(e.getDocument()); + } + + @Override + public void changedUpdate(DocumentEvent e) { + } + + /** + * Validate the specified Document and set the validation + * state in the {@link InputPanel} accordingly. + * + * @param doc The Document to validate. + */ + private void validate(Document doc) { + String text; + try { + text = doc.getText(0, doc.getLength()); + } + catch (BadLocationException e) { + LOG.warn("Exception retrieving document text", e); + return; + } + setInputValid(text.matches(format), doc); + } + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/TextListInputPanel.form b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/TextListInputPanel.form new file mode 100644 index 0000000..7d11b29 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/TextListInputPanel.form @@ -0,0 +1,76 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/TextListInputPanel.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/TextListInputPanel.java new file mode 100644 index 0000000..61a197e --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/TextListInputPanel.java @@ -0,0 +1,231 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.inputcomponents; + +import java.util.List; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +/** + * An input panel with a text field for user input as well as a list of predefined + * inputs to select from. + */ +public final class TextListInputPanel + extends + TextInputPanel { + + /** + * Creates a new instance TextListInputPanel. + * + * @param title the title of the panel + */ + private TextListInputPanel(String title) { + super(title); + resetable = false; + initComponents(); + list.addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + if (!e.getValueIsAdjusting()) { + Object selection = list.getSelectedValue(); + if (selection != null) { + inputField.setText((String) selection); + } + } + } + }); + } + + @Override + protected void captureInput() { + input = inputField.getText(); + } + + /** + * Enable input validation against the given regular expression. + * + * @see InputPanel#addValidationListener + * @param format A regular expression. + */ + private void enableInputValidation(String format) { + if (format != null) { + inputField.getDocument().addDocumentListener(new TextInputValidator(format)); + } + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + messageLabel = new javax.swing.JLabel(); + inputField = new javax.swing.JTextField(); + jScrollPane1 = new javax.swing.JScrollPane(); + list = new javax.swing.JList(); + + setLayout(new java.awt.GridBagLayout()); + + messageLabel.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + messageLabel.setText("Message"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(messageLabel, gridBagConstraints); + + inputField.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + inputField.setPreferredSize(new java.awt.Dimension(70, 20)); + inputField.addFocusListener(new java.awt.event.FocusAdapter() { + public void focusGained(java.awt.event.FocusEvent evt) { + inputFieldFocusGained(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(inputField, gridBagConstraints); + + list.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + list.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + jScrollPane1.setViewportView(list); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + add(jScrollPane1, gridBagConstraints); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void inputFieldFocusGained(java.awt.event.FocusEvent evt) {//GEN-FIRST:event_inputFieldFocusGained + inputField.selectAll(); + }//GEN-LAST:event_inputFieldFocusGained + + // CHECKSTYLE:OFF + // FORMATTER:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JTextField inputField; + private javax.swing.JScrollPane jScrollPane1; + private javax.swing.JList list; + private javax.swing.JLabel messageLabel; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON + + /** + * See {@link InputPanel.Builder}. + */ + public static class Builder + implements + InputPanel.Builder { + + /** + * The panel's title. + */ + private final String title; + /** + * The optional message. + */ + private String message; + /** + * Content for the dropdown list. + */ + private final List content; + /** + * Regex for validation of the text field's content. + */ + private String format; + /** + * Initially selected index of the list. + * Default is 0. + */ + private int initialIndex; + + /** + * Create a new Builder. + * + * @param title the title of the panel + * @param content Predefined items to display in the panel's list + */ + public Builder(String title, List content) { + this.title = title; + this.content = content; + } + + /** + * Set the message of the panel. + * The user of this method must take care for the line breaks in the message, + * as it is not wrapped automatically! + * + * @param message the message + * @return the instance of this Builder + */ + public Builder setMessage(String message) { + this.message = message; + return this; + } + + /** + * Make the panel validate it's input. + * + * @param format The regular expression that will be used for validation. + * @return the instance of this Builder + */ + public Builder enableValidation(String format) { + this.format = format; + return this; + } + + /** + * Set the initial selected list entry. + * + * @param index must be > 0, will have no effect otherwise + * @return the instance of this Builder + */ + public Builder setInitialSelection(int index) { + if (index >= 0) { + initialIndex = index; + } + return this; + } + + /** + * Set the initial selected list entry. + * + * @param element Element to select. Selection remains unchanged if + * element ist not in the list or element is + * null. + * @return the instance fo this Builder + */ + public Builder setInitialSelection(String element) { + int index; + try { + index = content.indexOf(element); + } + catch (NullPointerException e) { + index = -1; + } + return setInitialSelection(index); + } + + @Override + public TextListInputPanel build() { + TextListInputPanel panel = new TextListInputPanel(title); + panel.enableInputValidation(format); + panel.messageLabel.setText(message); + panel.list.setListData(content.toArray(new String[content.size()])); + panel.list.setSelectedIndex(initialIndex); + return panel; + } + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/TripleTextInputPanel.form b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/TripleTextInputPanel.form new file mode 100644 index 0000000..11e37d5 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/TripleTextInputPanel.form @@ -0,0 +1,186 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/TripleTextInputPanel.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/TripleTextInputPanel.java new file mode 100644 index 0000000..d2a2934 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/TripleTextInputPanel.java @@ -0,0 +1,498 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.inputcomponents; + +import javax.swing.text.Document; + +/** + * General input panel with three text fields and optionally: + *
    + *
  • a message/text
  • + *
  • labels for the text fields
  • + *
  • unit labels for the text fields
  • + *
+ * The input of the text fields can be validated + * (see {@link Builder#enableValidation enableValidation}). + * For instanciation the contained + * {@link TripleTextInputPanel.Builder Builder}-class must be used. + * The Object that is returned by {@link InputPanel#getInput} is + * an Array of three Strings. + */ +public final class TripleTextInputPanel + extends + TextInputPanel { + + /** + * If the panel is resetable this is the value the input is set to when + * doReset() is called. + */ + private Object resetValue; + /** + * Flag indicating if the content of the first text field is a valid input. + */ + private boolean inputField1Valid = true; + /** + * Flag indicating if the content of the second text field is a valid input. + */ + private boolean inputField2Valid = true; + /** + * Flag indicating if the content of the third text field is a valid input. + */ + private boolean inputField3Valid = true; + + /** + * Create a new TripleTextInputPanel. + * + * @param title Title of the panel + */ + private TripleTextInputPanel(String title) { + super(title); + initComponents(); + } + + /** + * Enable input validation against the given regular expressions. + * If a format string is null the related text field is not + * validated. + * + * @see InputPanel#addValidationListener + * @param format1 A regular expression for the first text field. + * @param format2 A regular expression for the second text field. + * @param format3 A regular expression for the third text field. + */ + private void enableInputValidation( + String format1, + String format2, + String format3 + ) { + if (format1 != null) { + inputField1.getDocument().addDocumentListener(new TextInputValidator(format1)); + } + if (format2 != null) { + inputField2.getDocument().addDocumentListener(new TextInputValidator(format2)); + } + if (format3 != null) { + inputField3.getDocument().addDocumentListener(new TextInputValidator(format3)); + } + } + + @Override + protected void captureInput() { + input = new String[]{ + inputField1.getText(), + inputField2.getText(), + inputField3.getText() + }; + } + + @Override + public void doReset() { + input = resetValue; + } + + /** + * Mark the input of the specified Document as valid, if this + * Document belongs to one of the three text fields in this panel. + * If valid is true the validity of the other + * two documents is checked, too. Only if all three text fields contain valid + * input, the whole input of the panel is marked as valid. + * + * @see TextInputPanel#setInputValid(boolean, javax.swing.text.Document) + * @param valid true, if the content of the Document is valid + * @param doc the Document + */ + @Override + protected void setInputValid(boolean valid, Document doc) { + // Find out to which input field the document belongs and check the others + boolean allValid = valid; + if (doc == inputField1.getDocument()) { + inputField1Valid = valid; + if (!(inputField2Valid && inputField3Valid)) { + allValid = false; + } + } + else if (doc == inputField2.getDocument()) { + inputField2Valid = valid; + if (!(inputField1Valid && inputField3Valid)) { + allValid = false; + } + } + else if (doc == inputField3.getDocument()) { + inputField3Valid = valid; + if (!(inputField1Valid && inputField2Valid)) { + allValid = false; + } + } + else { + allValid = false; + } + setInputValid(allValid); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + unitLabel3 = new javax.swing.JLabel(); + label3 = new javax.swing.JLabel(); + inputField3 = new javax.swing.JTextField(); + messageLabel = new javax.swing.JLabel(); + label1 = new javax.swing.JLabel(); + inputField1 = new javax.swing.JTextField(); + unitLabel1 = new javax.swing.JLabel(); + label2 = new javax.swing.JLabel(); + inputField2 = new javax.swing.JTextField(); + unitLabel2 = new javax.swing.JLabel(); + + setLayout(new java.awt.GridBagLayout()); + + unitLabel3.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + unitLabel3.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); + unitLabel3.setText("Unit-Label"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(unitLabel3, gridBagConstraints); + + label3.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + label3.setText("Label"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(label3, gridBagConstraints); + + inputField3.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + inputField3.setHorizontalAlignment(javax.swing.JTextField.RIGHT); + inputField3.setText("initial Value"); + inputField3.setName("inputField3"); // NOI18N + inputField3.setPreferredSize(new java.awt.Dimension(70, 20)); + inputField3.addFocusListener(new java.awt.event.FocusAdapter() { + public void focusGained(java.awt.event.FocusEvent evt) { + inputField3FocusGained(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 3; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(inputField3, gridBagConstraints); + inputField3.getAccessibleContext().setAccessibleName(""); + + messageLabel.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + messageLabel.setText("Message"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(messageLabel, gridBagConstraints); + + label1.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + label1.setText("Label"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(label1, gridBagConstraints); + + inputField1.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + inputField1.setHorizontalAlignment(javax.swing.JTextField.RIGHT); + inputField1.setText("initial Value"); + inputField1.setName("inputField1"); // NOI18N + inputField1.setPreferredSize(new java.awt.Dimension(70, 20)); + inputField1.addFocusListener(new java.awt.event.FocusAdapter() { + public void focusGained(java.awt.event.FocusEvent evt) { + inputField1FocusGained(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(inputField1, gridBagConstraints); + inputField1.getAccessibleContext().setAccessibleName(""); + + unitLabel1.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + unitLabel1.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); + unitLabel1.setText("Unit-Label"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(unitLabel1, gridBagConstraints); + + label2.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + label2.setText("Label"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(label2, gridBagConstraints); + + inputField2.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + inputField2.setHorizontalAlignment(javax.swing.JTextField.RIGHT); + inputField2.setText("initial Value"); + inputField2.setName("inputField2"); // NOI18N + inputField2.setPreferredSize(new java.awt.Dimension(70, 20)); + inputField2.addFocusListener(new java.awt.event.FocusAdapter() { + public void focusGained(java.awt.event.FocusEvent evt) { + inputField2FocusGained(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(inputField2, gridBagConstraints); + inputField2.getAccessibleContext().setAccessibleName(""); + + unitLabel2.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + unitLabel2.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); + unitLabel2.setText("Unit-Label"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(unitLabel2, gridBagConstraints); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void inputField1FocusGained(java.awt.event.FocusEvent evt) {//GEN-FIRST:event_inputField1FocusGained + inputField1.selectAll(); + }//GEN-LAST:event_inputField1FocusGained + + private void inputField2FocusGained(java.awt.event.FocusEvent evt) {//GEN-FIRST:event_inputField2FocusGained + inputField2.selectAll(); + }//GEN-LAST:event_inputField2FocusGained + + private void inputField3FocusGained(java.awt.event.FocusEvent evt) {//GEN-FIRST:event_inputField3FocusGained + inputField3.selectAll(); + }//GEN-LAST:event_inputField3FocusGained + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JTextField inputField1; + private javax.swing.JTextField inputField2; + private javax.swing.JTextField inputField3; + private javax.swing.JLabel label1; + private javax.swing.JLabel label2; + private javax.swing.JLabel label3; + private javax.swing.JLabel messageLabel; + private javax.swing.JLabel unitLabel1; + private javax.swing.JLabel unitLabel2; + private javax.swing.JLabel unitLabel3; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON + + /** + * See {@link InputPanel.Builder}. + */ + public static class Builder + implements + InputPanel.Builder { + + /** + * The panel's title. + */ + private final String title; + /** + * Message to be displayed in the panel. + */ + private String message; + /** + * Labels of for the text fields. + */ + private final String[] labels = new String[3]; + /** + * Unit labels of the text fields. + */ + private final String[] unitLabels = new String[3]; + /** + * Initial values for the text fields. + */ + private final String[] initialValues = new String[3]; + /** + * Regular expressions for validation of the text field contents. + */ + private final String[] formats = new String[3]; + /** + * Show a reset button in the panel. + * Default is false. + */ + private boolean resetButton; + /** + * Value the input is reset to when the reset button is used. + */ + private Object resetValue; + + /** + * Create a new builder. + * + * @param title Title of the panel. + */ + public Builder(String title) { + this.title = title; + } + + @Override + public InputPanel build() { + TripleTextInputPanel panel = new TripleTextInputPanel(title); + panel.enableInputValidation(formats[0], formats[1], formats[2]); + panel.label1.setText(labels[0]); + panel.label2.setText(labels[1]); + panel.label3.setText(labels[2]); + panel.unitLabel1.setText(unitLabels[0]); + panel.unitLabel2.setText(unitLabels[1]); + panel.unitLabel3.setText(unitLabels[2]); + panel.inputField1.setText(initialValues[0]); + panel.inputField2.setText(initialValues[1]); + panel.inputField3.setText(initialValues[2]); + panel.messageLabel.setText(message); + panel.resetable = resetButton; + if (panel.resetable) { + panel.resetValue = resetValue; + } + return panel; + } + + /** + * Set the labels for the text fields. + * Passing null means there should not be such a label. + * + * @param label1 The label of the first text field + * @param label2 The label of the second text field + * @param label3 The label of the third text field + * @return the instance of this Builder + */ + public Builder setLabels(String label1, String label2, String label3) { + labels[0] = label1; + labels[1] = label2; + labels[2] = label3; + return this; + } + + /** + * Set the same initial value for all three text fields. + * + * @param value The initial value. + * @return the isntance of this Builder + */ + public Builder setInitialValues(String value) { + return setInitialValues(value, value, value); + } + + /** + * Set the initial values for the text fields of the panel + * Passing null means there should not be an initial value + * for this text field. + * + * @param val1 The initial value of the first text field + * @param val2 The initial value of the second text field + * @param val3 The initial value of the third text field + * @return the instance of this Builder + */ + public Builder setInitialValues(String val1, String val2, String val3) { + initialValues[0] = val1; + initialValues[1] = val2; + initialValues[2] = val3; + return this; + } + + /** + * Set the same text for the unit label for all three text fields. + * + * @param unit The unit + * @return the isntance of this Builder + */ + public Builder setUnitLabels(String unit) { + return setUnitLabels(unit, unit, unit); + } + + /** + * Set the text for the unit labels of the panel. + * Passing null means there should not be a unit label for + * the corresponding text field. + * + * @param unit1 The unit of the first text field + * @param unit2 The unit of the second text field + * @param unit3 The unit of the third text field + * @return the instance of this Builder + */ + public Builder setUnitLabels(String unit1, String unit2, String unit3) { + unitLabels[0] = unit1; + unitLabels[1] = unit2; + unitLabels[2] = unit3; + return this; + } + + /** + * Set the message of the panel. + * The user of this method must take care for the line breaks in the message, + * as it is not wrapped automatically! + * + * @param message the message + * @return the instance of this Builder + */ + public Builder setMessage(String message) { + this.message = message; + return this; + } + + public Builder enableValidation(String format) { + return enableValidation(format, format, format); + } + + /** + * Make the panel validate it's input by using the specified regular + * expressions for the text fields. + * Passing null as an argument means that the corresponding text field + * should not be validated. + * + * @param format1 The regular expression that will be used for validation in + * the first text field. + * @param format2 The regular expression that will be used for validation in + * the second text field. + * @param format3 The regular expression that will be used for validation in + * the third text field. + * @return the instance of this Builder + */ + public Builder enableValidation( + String format1, + String format2, + String format3 + ) { + formats[0] = format1; + formats[1] = format2; + formats[2] = format3; + return this; + } + + /** + * Set a value the panel's input can be reset to. + * + * @param resetValue the reset value + * @return the instance of this Builder + */ + public Builder enableResetButton(Object resetValue) { + this.resetButton = true; + this.resetValue = resetValue; + return this; + } + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/ValidationEvent.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/ValidationEvent.java new file mode 100644 index 0000000..3617962 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/ValidationEvent.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.inputcomponents; + +import java.util.EventObject; + +/** + * An event holding a single boolean variable indicating if something is valid. + */ +public class ValidationEvent + extends + EventObject { + + /** + * Validation state. + */ + private final boolean valid; + + /** + * Create a new ValidationEvent. + * + * @param source The source of the event. + * @param valid The state of validation that should be reported by this event. + */ + public ValidationEvent(Object source, boolean valid) { + super(source); + this.valid = valid; + } + + /** + * Return the state of validation. + * + * @return valid + */ + public boolean valid() { + return valid; + } +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/ValidationListener.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/ValidationListener.java new file mode 100644 index 0000000..9f87b3e --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/ValidationListener.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle.inputcomponents; + +import java.util.EventListener; + +/** + * A listener interface for {@link ValidationEvent ValidationEvents}. + */ +public interface ValidationListener + extends + EventListener { + + /** + * Should be called when the state of validation changed. + * + * @param e The ValidationEvent containing validation information. + */ + void validityChanged(ValidationEvent e); +} diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/package-info.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/package-info.java new file mode 100644 index 0000000..f81c3c9 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/inputcomponents/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Generic GUI components for input dialogs. + */ +package org.opentcs.virtualvehicle.inputcomponents; diff --git a/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/package-info.java b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/package-info.java new file mode 100644 index 0000000..24b9a8f --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/java/org/opentcs/virtualvehicle/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Classes for emulating the behaviour of a physical vehicle. + */ +package org.opentcs.virtualvehicle; diff --git a/opentcs-commadapter-loopback/src/main/resources/REUSE.toml b/opentcs-commadapter-loopback/src/main/resources/REUSE.toml new file mode 100644 index 0000000..42aab58 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/resources/REUSE.toml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC0-1.0 + +version = 1 + +[[annotations]] +path = ["**/*.gif", "**/*.jpg", "**/*.png", "**/*.svg"] +precedence = "closest" +SPDX-FileCopyrightText = "The openTCS Authors" +SPDX-License-Identifier = "CC-BY-4.0" diff --git a/opentcs-commadapter-loopback/src/main/resources/i18n/org/opentcs/commadapter/loopback/Bundle.properties b/opentcs-commadapter-loopback/src/main/resources/i18n/org/opentcs/commadapter/loopback/Bundle.properties new file mode 100644 index 0000000..1a20eee --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/resources/i18n/org/opentcs/commadapter/loopback/Bundle.properties @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +inputDialog.button_cancel.text=Cancel +inputDialog.button_ok.text=Ok +inputDialog.button_reset.text=Reset +loopbackCommAdapterPanel.accessibleName=Loopback options +loopbackCommAdapterPanel.button_dispatchEvent.text=Dispatch event +loopbackCommAdapterPanel.button_failCurrentCommand.text=Fail current command +loopbackCommAdapterPanel.button_nextStep.text=Next step +loopbackCommAdapterPanel.button_setProperty.text = Apply +loopbackCommAdapterPanel.checkBox_commandProcessingAutomatic.text=Automatic mode +loopbackCommAdapterPanel.checkBox_commandProcessingManual.text=Single step/manual mode +loopbackCommAdapterPanel.checkBox_enableAdapter.text=Enable loopback adapter +loopbackCommAdapterPanel.checkBox_includeAppendix.text=Include message as appendix: +loopbackCommAdapterPanel.dialog_setEnergyLevel.title=Set energy level +loopbackCommAdapterPanel.dialog_setOrientationAngle.title=Set orientation angle +loopbackCommAdapterPanel.dialog_setPosition.title=Set vehicle position +loopbackCommAdapterPanel.dialog_setPrecisePosition.title=Set precise position +loopbackCommAdapterPanel.dialog_setState.title=Set vehicle state +loopbackCommAdapterPanel.label_energyLevel.text=Energy level: +loopbackCommAdapterPanel.label_maximumAcceleration.text=Acceleration: +loopbackCommAdapterPanel.label_maximumDeceleration.text=Deceleration: +loopbackCommAdapterPanel.label_maximumForwardVelocity.text=Max. forward velocity: +loopbackCommAdapterPanel.label_maximumReverseVelocity.text=Max. reverse velocity: +loopbackCommAdapterPanel.label_operatingTime.text=Operating time: +loopbackCommAdapterPanel.label_orientationAngle.text=Orientation: +loopbackCommAdapterPanel.label_pauseVehicle.text=Pause vehicle: +loopbackCommAdapterPanel.label_position.text=Position: +loopbackCommAdapterPanel.label_precisePosition.text=Precise position: +loopbackCommAdapterPanel.label_propertyKey.text=Key: +loopbackCommAdapterPanel.label_state.text=State: +loopbackCommAdapterPanel.panel_adapterStatus.border.title=Adapter status +loopbackCommAdapterPanel.panel_commandProcessing.border.title=Command processing +loopbackCommAdapterPanel.panel_eventDispatching.title=Event dispatching +loopbackCommAdapterPanel.panel_loadHandlingDevice.border.title=Load handling device +loopbackCommAdapterPanel.panel_vehicleProperties.border.title=Vehicle properties +loopbackCommAdapterPanel.panel_vehicleProperty.border.title = Vehicle property +loopbackCommAdapterPanel.panel_vehicleStatus.border.title=Current position/state +loopbackCommAdapterPanel.radioButton_removeProperty.text=Remove property +loopbackCommAdapterPanel.radioButton_setProperty.text=Set this value: +loopbackCommAdapterPanel.textArea_precisePosition.positionNotSetPlaceholder=- +loopbackCommAdapterPanel.textField_orientationAngle.angleNotSetPlaceholder=- +loopbackCommunicationAdapterDescription.description=Loopback adapter (virtual vehicle) diff --git a/opentcs-commadapter-loopback/src/main/resources/i18n/org/opentcs/commadapter/loopback/Bundle_de.properties b/opentcs-commadapter-loopback/src/main/resources/i18n/org/opentcs/commadapter/loopback/Bundle_de.properties new file mode 100644 index 0000000..c201892 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/resources/i18n/org/opentcs/commadapter/loopback/Bundle_de.properties @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +inputDialog.button_cancel.text=Abbrechen +inputDialog.button_ok.text=Ok +inputDialog.button_reset.text=Zur\u00fccksetzen +loopbackCommAdapterPanel.accessibleName=Loopback-Optionen +loopbackCommAdapterPanel.button_dispatchEvent.text=Event versenden +loopbackCommAdapterPanel.button_nextStep.text=N\u00e4chster Schritt +loopbackCommAdapterPanel.button_setProperty.text = Anwenden +loopbackCommAdapterPanel.checkBox_commandProcessingAutomatic.text=Automatik +loopbackCommAdapterPanel.checkBox_commandProcessingManual.text=Manuell +loopbackCommAdapterPanel.checkBox_enableAdapter.text=Loopback-Treiber einschalten +loopbackCommAdapterPanel.checkBox_includeAppendix.text=Nachricht als Anhang hinzuf\u00fcgen: +loopbackCommAdapterPanel.dialog_setEnergyLevel.title=Energie-Level setzen +loopbackCommAdapterPanel.dialog_setOrientationAngle.title=Orientierung setzen +loopbackCommAdapterPanel.dialog_setPosition.title=Fahrzeug-Position setzen +loopbackCommAdapterPanel.dialog_setPrecisePosition.title=Exakte Position setzen +loopbackCommAdapterPanel.dialog_setState.title=Fahrzeug-Zustand setzen +loopbackCommAdapterPanel.label_energyLevel.text=Energie-Level: +loopbackCommAdapterPanel.label_maximumAcceleration.text=Beschleunigung: +loopbackCommAdapterPanel.label_maximumDeceleration.text=Abbremsung: +loopbackCommAdapterPanel.label_maximumForwardVelocity.text=Max. Vorw\u00e4rts-Geschwindigkeit: +loopbackCommAdapterPanel.label_maximumReverseVelocity.text=Max. R\u00fcckw\u00e4rts-Geschwindigkeit: +loopbackCommAdapterPanel.label_operatingTime.text=Operationsdauer +loopbackCommAdapterPanel.label_orientationAngle.text=Orientierung: +loopbackCommAdapterPanel.label_pauseVehicle.text=Fahrzeug anhalten: +loopbackCommAdapterPanel.label_position.text=Position: +loopbackCommAdapterPanel.label_precisePosition.text=Exakte Position: +loopbackCommAdapterPanel.label_propertyKey.text = Schl\u00fcssel: +loopbackCommAdapterPanel.label_state.text=Zustand: +loopbackCommAdapterPanel.panel_adapterStatus.border.title=Adapterzustand +loopbackCommAdapterPanel.panel_commandProcessing.border.title=Kommandoverarbeitung +loopbackCommAdapterPanel.panel_eventDispatching.title=Event-Versand +loopbackCommAdapterPanel.panel_loadHandlingDevice.border.title=Lastaufnahmemittel +loopbackCommAdapterPanel.panel_vehicleProperties.border.title=Fahrzeugeigenschaften +loopbackCommAdapterPanel.panel_vehicleProperty.border.title = Fahrzeug-Property +loopbackCommAdapterPanel.panel_vehicleStatus.border.title=Aktuelle Position +loopbackCommAdapterPanel.radioButton_removeProperty.text=Property l\u00f6schen +loopbackCommAdapterPanel.radioButton_setProperty.text=Diesen Wert setzen: +loopbackCommAdapterPanel.textArea_precisePosition.positionNotSetPlaceholder=- +loopbackCommAdapterPanel.textField_orientationAngle.angleNotSetPlaceholder=- +loopbackCommunicationAdapterDescription.description=Loopback-Treiber (virtuelles Fahrzeug) diff --git a/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/add_icon.png b/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/add_icon.png new file mode 100644 index 0000000..5b353c5 Binary files /dev/null and b/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/add_icon.png differ diff --git a/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/delete_icon.png b/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/delete_icon.png new file mode 100644 index 0000000..6ced4c3 Binary files /dev/null and b/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/delete_icon.png differ diff --git a/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/delete_icon.svg b/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/delete_icon.svg new file mode 100644 index 0000000..87975c7 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/delete_icon.svg @@ -0,0 +1,1839 @@ + + + + + + + + + + + + hdd + drive + fixed + media + hard + + + + + Open Clip Art Library, Source: GNOME Icon Theme, Source: GNOME Icon Theme, Source: GNOME Icon Theme, Source: GNOME Icon Theme + + + + + Jakub Steiner, Lapo Calamandrei + + + + + Jakub Steiner, Lapo Calamandrei + + + + image/svg+xml + + + en + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/edit_icon.png b/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/edit_icon.png new file mode 100644 index 0000000..92cbf30 Binary files /dev/null and b/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/edit_icon.png differ diff --git a/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/load_icon.png b/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/load_icon.png new file mode 100644 index 0000000..fc0b729 Binary files /dev/null and b/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/load_icon.png differ diff --git a/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/load_icon.svg b/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/load_icon.svg new file mode 100644 index 0000000..f8b91e9 --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/load_icon.svg @@ -0,0 +1,1054 @@ + + + + + + + + + + + + hdd + drive + fixed + media + hard + + + + + Open Clip Art Library, Source: GNOME Icon Theme, Source: GNOME Icon Theme, Source: GNOME Icon Theme, Source: GNOME Icon Theme + + + + + Jakub Steiner, Lapo Calamandrei + + + + + Jakub Steiner, Lapo Calamandrei + + + + image/svg+xml + + + en + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/remove_icon.png b/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/remove_icon.png new file mode 100644 index 0000000..3b4da1c Binary files /dev/null and b/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/remove_icon.png differ diff --git a/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/save_icon.png b/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/save_icon.png new file mode 100644 index 0000000..7fbea9b Binary files /dev/null and b/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/save_icon.png differ diff --git a/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/save_icon.svg b/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/save_icon.svg new file mode 100644 index 0000000..cd4689c --- /dev/null +++ b/opentcs-commadapter-loopback/src/main/resources/org/opentcs/virtualvehicle/images/save_icon.svg @@ -0,0 +1,229 @@ + + + + + + Jakub Steiner, Lapo Calamandrei + + + + hdd + drive + fixed + media + hard + + + + + Open Clip Art Library, Source: GNOME Icon Theme, Source: GNOME Icon Theme, Source: GNOME Icon Theme, Source: GNOME Icon Theme + + + + + Jakub Steiner, Lapo Calamandrei + + + + + Jakub Steiner, Lapo Calamandrei + + + + image/svg+xml + + + en + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-commadapter-loopback/src/test/java/org/opentcs/virtualvehicle/VelocityControllerTest.java b/opentcs-commadapter-loopback/src/test/java/org/opentcs/virtualvehicle/VelocityControllerTest.java new file mode 100644 index 0000000..b216872 --- /dev/null +++ b/opentcs-commadapter-loopback/src/test/java/org/opentcs/virtualvehicle/VelocityControllerTest.java @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.virtualvehicle; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Vehicle; + +/** + * Unit tests for {@link VelocityController}. + */ +class VelocityControllerTest { + + private static final int MAX_DECEL = -1000; + private static final int MAX_ACCEL = 1000; + private static final int MAX_REV_VELO = -500; + private static final int MAX_FWD_VELO = 500; + + private static final long WAY_LENGTH = 5000; + private static final int MAX_VELO = 500; + private static final String POINT_NAME = "Destination_Point"; + + private VelocityController controller; + + @BeforeEach + void setUp() { + controller = new VelocityController(MAX_DECEL, MAX_ACCEL, MAX_REV_VELO, MAX_FWD_VELO); + } + + @Test + void initialControllerHasNoWayEntries() { + assertFalse(controller.hasWayEntries()); + assertNull(controller.getCurrentWayEntry()); + } + + @Test + void throwOnAddingNullWayEntry() { + assertThrows(NullPointerException.class, () -> controller.addWayEntry(null)); + } + + @Test + void throwOnAdvancingWithNegativeTime() { + assertThrows(IllegalArgumentException.class, () -> controller.advanceTime(-1)); + } + + @Test + void advanceTimeAdvancesTime() { + long timeBefore = controller.getCurrentTime(); + + controller.advanceTime(5); + + assertThat(controller.getCurrentTime(), is(timeBefore + 5)); + } + + @Test + void vehicleDoesNotChangePositionWhilePaused() { + VelocityController.WayEntry wayEntry + = new VelocityController.WayEntry( + WAY_LENGTH, + MAX_VELO, + POINT_NAME, + Vehicle.Orientation.FORWARD + ); + controller.addWayEntry(wayEntry); + controller.setVehiclePaused(true); + + long posBefore = controller.getCurrentPosition(); + + controller.advanceTime(5); + + assertThat(controller.getCurrentPosition(), is(posBefore)); + } + + @Test + void processWayEntriesInGivenOrder() { + controller = new VelocityController(MAX_DECEL, 1000, MAX_REV_VELO, 500); + + VelocityController.WayEntry firstEntry + = new VelocityController.WayEntry(1, MAX_VELO, POINT_NAME, Vehicle.Orientation.FORWARD); + VelocityController.WayEntry secondEntry + = new VelocityController.WayEntry(10000, MAX_VELO, POINT_NAME, Vehicle.Orientation.FORWARD); + controller.addWayEntry(firstEntry); + controller.addWayEntry(secondEntry); + + assertThat(controller.getCurrentWayEntry(), is(sameInstance(firstEntry))); + assertSame(firstEntry, controller.getCurrentWayEntry()); + + controller.advanceTime(100); + + assertThat(controller.getCurrentWayEntry(), is(sameInstance(secondEntry))); + assertSame(secondEntry, controller.getCurrentWayEntry()); + } + + @Test + void accelerateToMaxVelocityLimitedByController() { + final int maxFwdVelocity = 500; // mm/s + final int maxAcceleration = 250; // mm/s^2 + controller = new VelocityController(MAX_DECEL, maxAcceleration, MAX_REV_VELO, maxFwdVelocity); + + // Way point with enough length to reach the maximum velocity and with a higher velocity limit + // than the vehicle's own maximum velocity. + VelocityController.WayEntry wayEntry + = new VelocityController.WayEntry( + 100000, + 2 * maxFwdVelocity, + POINT_NAME, + Vehicle.Orientation.FORWARD + ); + controller.addWayEntry(wayEntry); + + controller.advanceTime(1000); + + // Reach 250 mm/s in 1s: + assertThat(controller.getCurrentVelocity(), is(250)); + + controller.advanceTime(1000); + + // Reach max. velocity of 500 mm/s in 2s: + assertThat(controller.getCurrentVelocity(), is(500)); + + controller.advanceTime(1000); + + // Stay at max velocity: + assertThat(controller.getCurrentVelocity(), is(500)); + } + + @Test + void accelerateToMaxVelocityLimitedByWayEntry() { + final int maxFwdVelocity = 500; // mm/s + final int maxAcceleration = 500; // mm/s^2 + controller = new VelocityController(MAX_DECEL, maxAcceleration, MAX_REV_VELO, maxFwdVelocity); + + // Way point with a velocity limit less than the vehicle's own maximum velocity. + VelocityController.WayEntry wayEntry + = new VelocityController.WayEntry( + 10000, + 250, + POINT_NAME, + Vehicle.Orientation.FORWARD + ); + controller.addWayEntry(wayEntry); + + for (int i = 0; i < 10; i++) { + controller.advanceTime(100); + } + + // Velocity could be 500 mm/s after one second, but should be limited to 250 mm/s. + assertThat(controller.getCurrentVelocity(), is(250)); + } +} diff --git a/opentcs-common/build.gradle b/opentcs-common/build.gradle new file mode 100644 index 0000000..8c174b8 --- /dev/null +++ b/opentcs-common/build.gradle @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply from: "${rootDir}/gradle/java-project.gradle" +apply from: "${rootDir}/gradle/java-codequality.gradle" +apply from: "${rootDir}/gradle/publishing-java.gradle" + +dependencies { + api project(':opentcs-api-base') + + api group: 'jakarta.xml.bind', name: 'jakarta.xml.bind-api', version: '2.3.3' + api group: 'org.glassfish.jaxb', name: 'jaxb-runtime', version: '2.3.9' + + implementation group: 'org.semver4j', name: 'semver4j', version: '5.4.0' +} + +processResources.doLast { + // Write a properties file with the build version and date. + def props = new Properties() + props.setProperty('opentcs.version', version) + props.setProperty('opentcs.builddate', project.ext.buildDate) + + new File(sourceSets.main.output.resourcesDir, 'opentcs.properties').withWriter() { + props.store(it, null) + } +} + +task release { + dependsOn build +} diff --git a/opentcs-common/gradle.properties b/opentcs-common/gradle.properties new file mode 100644 index 0000000..14431ac --- /dev/null +++ b/opentcs-common/gradle.properties @@ -0,0 +1,40 @@ +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineMethodParams=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAfterDotInChainedMethodCalls=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineDisjunctiveCatchTypes=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineFor=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineImplements=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.sortMembersByVisibility=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.visibilityOrder=PUBLIC;PROTECTED;DEFAULT;PRIVATE +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.enable-indent=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineArrayInit=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineCallArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.keepGettersAndSettersTogether=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classMembersOrder=STATIC FIELD;FIELD;STATIC_INIT;CONSTRUCTOR;INSTANCE_INIT;STATIC METHOD;METHOD;STATIC CLASS;CLASS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapCommentText=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.importGroupsOrder=* +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize=4 +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineAnnotationArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineTryResources=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.preserveNewLinesInComments=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineParenthesized=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineThrows=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=100 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project diff --git a/opentcs-common/src/main/java/org/opentcs/common/ClientConnectionMode.java b/opentcs-common/src/main/java/org/opentcs/common/ClientConnectionMode.java new file mode 100644 index 0000000..c9d75d8 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/common/ClientConnectionMode.java @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.common; + +/** + * Defines the modes in which an application may be in. + */ +public enum ClientConnectionMode { + /** + * The application SHOULD be connected/online. + */ + ONLINE, + /** + * The application SHOULD be disconnected/offline. + */ + OFFLINE +} diff --git a/opentcs-common/src/main/java/org/opentcs/common/DefaultPortalManager.java b/opentcs-common/src/main/java/org/opentcs/common/DefaultPortalManager.java new file mode 100644 index 0000000..929bd34 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/common/DefaultPortalManager.java @@ -0,0 +1,197 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.common; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.List; +import java.util.ResourceBundle; +import javax.swing.JOptionPane; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.util.I18nCommon; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.gui.dialog.ConnectToServerDialog; +import org.opentcs.util.gui.dialog.ConnectionParamSet; +import org.opentcs.util.gui.dialog.NullConnectionParamSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The default implementation of {@link PortalManager}, providing a single + * {@link KernelServicePortal}. + */ +public class DefaultPortalManager + implements + PortalManager { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(DefaultPortalManager.class); + /** + * This class's resource bundle. + */ + private static final ResourceBundle BUNDLE + = ResourceBundle.getBundle(I18nCommon.BUNDLE_PATH); + /** + * The handler to send events to. + */ + private final EventHandler eventHandler; + /** + * The connection bookmarks to use. + */ + private final List connectionBookmarks; + /** + * The service portal instance we are working with. + */ + private final KernelServicePortal servicePortal; + /** + * The last successfully established connection. + */ + private ConnectionParamSet lastConnection = new NullConnectionParamSet(); + /** + * The current connection. {@link NullConnectionParamSet}, if no connection is currently + * established. + */ + private ConnectionParamSet currentConnection = new NullConnectionParamSet(); + + /** + * Creates a new instance. + * + * @param servicePortal The service portal instance we a working with. + * @param eventHandler The handler to send events to. + * @param connectionBookmarks The connection bookmarks to use. + */ + @Inject + public DefaultPortalManager( + KernelServicePortal servicePortal, + @ApplicationEventBus + EventHandler eventHandler, + List connectionBookmarks + ) { + this.eventHandler = requireNonNull(eventHandler, "eventHandler"); + this.servicePortal = requireNonNull(servicePortal, "servicePortal"); + this.connectionBookmarks = requireNonNull(connectionBookmarks, "connectionBookmarks"); + } + + @Override + public boolean connect(ConnectionMode mode) { + if (isConnected()) { + return true; + } + + switch (mode) { + case AUTO: + if (connectionBookmarks.isEmpty()) { + LOG.info("Cannot connect automatically. No connection bookmarks available."); + return false; + } + ConnectionParamSet paramSet = connectionBookmarks.get(0); + return connect(paramSet.getDescription(), paramSet.getHost(), paramSet.getPort()); + case MANUAL: + return connectWithDialog(); + case RECONNECT: + if (lastConnection instanceof NullConnectionParamSet) { + LOG.info("Cannot reconnect. No portal we were previously connected to."); + return false; + } + return connect( + lastConnection.getDescription(), + lastConnection.getHost(), + lastConnection.getPort() + ); + default: + LOG.warn("Unhandled connection mode '{}'. Not connecting.", mode.name()); + return false; + } + } + + @Override + public void disconnect() { + if (!isConnected()) { + return; + } + + eventHandler.onEvent(ConnectionState.DISCONNECTING); + + try { + servicePortal.logout(); + } + catch (KernelRuntimeException e) { + LOG.warn("Exception trying to disconnect from remote portal", e); + } + + lastConnection = currentConnection; + currentConnection = new NullConnectionParamSet(); + eventHandler.onEvent(ConnectionState.DISCONNECTED); + } + + @Override + public boolean isConnected() { + return !(currentConnection instanceof NullConnectionParamSet); + } + + @Override + public KernelServicePortal getPortal() { + return servicePortal; + } + + @Override + public String getDescription() { + return currentConnection.getDescription(); + } + + @Override + public String getHost() { + return currentConnection.getHost(); + } + + @Override + public int getPort() { + return currentConnection.getPort(); + } + + /** + * Tries to establish a connection to the portal. + * + * @param host The name of the host running the kernel/portal. + * @param port The port to connect to. + * @return {@code true} if, and only if, the connection was established successfully. + */ + private boolean connect(String description, String host, int port) { + try { + eventHandler.onEvent(ConnectionState.CONNECTING); + servicePortal.login(host, port); + } + catch (KernelRuntimeException e) { + LOG.warn("Failed to connect to remote portal", e); + eventHandler.onEvent(ConnectionState.DISCONNECTED); + JOptionPane.showMessageDialog( + null, + BUNDLE.getString("connectToServerDialog.optionPane_noConnection.message"), + BUNDLE.getString("connectToServerDialog.optionPane_noConnection.message"), + JOptionPane.ERROR_MESSAGE + ); + + // Retry connection attempt + return connectWithDialog(); + } + + currentConnection = new ConnectionParamSet(description, host, port); + eventHandler.onEvent(ConnectionState.CONNECTED); + return true; + } + + private boolean connectWithDialog() { + ConnectToServerDialog dialog = new ConnectToServerDialog(connectionBookmarks); + dialog.setVisible(true); + if (dialog.getReturnStatus() == ConnectToServerDialog.RET_OK) { + return connect(dialog.getDescription(), dialog.getHost(), dialog.getPort()); + } + + return false; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/common/GuestUserCredentials.java b/opentcs-common/src/main/java/org/opentcs/common/GuestUserCredentials.java new file mode 100644 index 0000000..26463cb --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/common/GuestUserCredentials.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.common; + +/** + * Defines the credentials for a guest user account. + */ +public interface GuestUserCredentials { + + /** + * The default/guest user name. + */ + String USER = "Alice"; + /** + * The default/guest password. + */ + String PASSWORD = "xyz"; +} diff --git a/opentcs-common/src/main/java/org/opentcs/common/KernelClientApplication.java b/opentcs-common/src/main/java/org/opentcs/common/KernelClientApplication.java new file mode 100644 index 0000000..97f5370 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/common/KernelClientApplication.java @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.common; + +import org.opentcs.components.Lifecycle; + +/** + * Provides methods used in a kernel client application's context. + */ +public interface KernelClientApplication + extends + Lifecycle { + + /** + * Tells the application to switch its state to online. + * + * @param autoConnect Whether to connect automatically to the kernel or to show a connect dialog + * when going online. + */ + void online(boolean autoConnect); + + /** + * Tells the application to switch its state to offline. + */ + void offline(); + + /** + * Checks whether the application's state is online. + * + * @return Whether the application's state is online. + */ + boolean isOnline(); +} diff --git a/opentcs-common/src/main/java/org/opentcs/common/LoggingScheduledThreadPoolExecutor.java b/opentcs-common/src/main/java/org/opentcs/common/LoggingScheduledThreadPoolExecutor.java new file mode 100644 index 0000000..1a3e850 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/common/LoggingScheduledThreadPoolExecutor.java @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.common; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.RunnableScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Extends the {@link ScheduledThreadPoolExecutor} by logging exceptions thrown by scheduled tasks. + */ +public class LoggingScheduledThreadPoolExecutor + extends + ScheduledThreadPoolExecutor { + + /** + * This class's logger. + */ + private static final Logger LOG + = LoggerFactory.getLogger(LoggingScheduledThreadPoolExecutor.class); + + /** + * Creates a new instance. + * + * @param corePoolSize The number of threads to keep in the pool. + * @param threadFactory The factory to use when the executor creates a new thread. + * @throws IllegalArgumentException If {@code corePoolSize < 0} + * @throws NullPointerException If {@code threadFactory} is null + */ + public LoggingScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) { + super(corePoolSize, threadFactory); + } + + @Override + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + if (t == null && r instanceof Future) { + try { + Future future = (Future) r; + if (future.isDone()) { + future.get(); + } + else if (isPeriodic(future)) { + // Periodic futures will never be done + return; + } + else { + LOG.debug("Future was not done: {}", future); + } + } + catch (ExecutionException ee) { + LOG.warn("Unhandled exception in executed task", ee.getCause()); + } + catch (CancellationException ce) { + LOG.debug("Task was cancelled", ce); + } + catch (InterruptedException ie) { + LOG.debug("Interrupted during Future.get()", ie); + // Ignore/Reset + Thread.currentThread().interrupt(); + } + } + if (t != null) { + LOG.error("Abrupt termination", t); + } + } + + private boolean isPeriodic(Future future) { + if (future instanceof RunnableScheduledFuture) { + RunnableScheduledFuture runnableFuture = (RunnableScheduledFuture) future; + if (runnableFuture.isPeriodic()) { + return true; + } + } + return false; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/common/LoopbackAdapterConstants.java b/opentcs-common/src/main/java/org/opentcs/common/LoopbackAdapterConstants.java new file mode 100644 index 0000000..3ce61e5 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/common/LoopbackAdapterConstants.java @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.common; + +/** + * This interface provides access to vehicle-property keys that are used in both the + * plant overview as well as in the kernel. + */ +public interface LoopbackAdapterConstants { + + /** + * The key of the vehicle property that specifies the vehicle's initial position. + */ + String PROPKEY_INITIAL_POSITION = "loopback:initialPosition"; + /** + * The key of the vehicle property that specifies the default operating time. + */ + String PROPKEY_OPERATING_TIME = "loopback:operatingTime"; + /** + * The key of the vehicle property that specifies which operation loads the load handling device. + */ + String PROPKEY_LOAD_OPERATION = "loopback:loadOperation"; + /** + * The default value of the load operation property. + */ + String PROPVAL_LOAD_OPERATION_DEFAULT = "Load cargo"; + /** + * The key of the vehicle property that specifies which operation unloads the load handling + * device. + */ + String PROPKEY_UNLOAD_OPERATION = "loopback:unloadOperation"; + /** + * The default value of the unload operation property. + */ + String PROPVAL_UNLOAD_OPERATION_DEFAULT = "Unload cargo"; + /** + * The key of the vehicle property that specifies the maximum acceleration of a vehicle. + */ + String PROPKEY_ACCELERATION = "loopback:acceleration"; + /** + * The key of the vehicle property that specifies the maximum decceleration of a vehicle. + */ + String PROPKEY_DECELERATION = "loopback:deceleration"; + +} diff --git a/opentcs-common/src/main/java/org/opentcs/common/PortalManager.java b/opentcs-common/src/main/java/org/opentcs/common/PortalManager.java new file mode 100644 index 0000000..1db5761 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/common/PortalManager.java @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.common; + +import org.opentcs.access.KernelServicePortal; +import org.opentcs.components.kernel.services.ServiceUnavailableException; + +/** + * Declares methods for managing a connection to a remote portal. + */ +public interface PortalManager { + + /** + * Tries to establish a connection to the portal. + * + * @param mode The mode to use for the connection attempt. + * @return {@code true} if, and only if, the connection was established successfully. + */ + boolean connect(ConnectionMode mode); + + /** + * Tells the portal manager the connection to the portal was lost. + */ + void disconnect(); + + /** + * Checks whether a connection to the portal is established. + * + * @return {@code true} if, and only if, a connection to the portal is established. + */ + boolean isConnected(); + + /** + * Returns the remote kernel client portal the manager is working with. + * + * @return The remote kernel client portal. + */ + KernelServicePortal getPortal(); + + /** + * Returns a description for the current connection. + * + * @return A description for the current connection. + */ + String getDescription(); + + /** + * Returns the host currently connected to. + * + * @return The host currently connected to, or {@code null}, if not connected. + */ + String getHost(); + + /** + * Returns the port currently connected to. + * + * @return The port currently connected to, or {@code -1}, if not connected. + */ + int getPort(); + + /** + * Defines the states in which a portal manager instance may be in. + */ + enum ConnectionState { + + /** + * Indicates the portal manager is trying to connect to the remote portal. + */ + CONNECTING, + /** + * Indicates the portal is connected and logged in to a remote portal, thus in a usable state. + */ + CONNECTED, + /** + * Indicates the portal is disconnecting from the remote portal. + */ + DISCONNECTING, + /** + * Indicates the portal is not connected to a remote portal. + * While in this state, calls to the portal's service methods will result in a + * {@link ServiceUnavailableException}. + */ + DISCONNECTED; + } + + /** + * Defines the modes a portal manager uses to establish a connection to a portal. + */ + enum ConnectionMode { + + /** + * Connect automatically by using a predefined set of connection parameters. + */ + AUTO, + /** + * Connect manually by showing a dialog allowing to enter connection parameters. + */ + MANUAL, + /** + * Connect to the portal we were previously connected to. + */ + RECONNECT; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/common/SameThreadExecutorService.java b/opentcs-common/src/main/java/org/opentcs/common/SameThreadExecutorService.java new file mode 100644 index 0000000..3554d40 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/common/SameThreadExecutorService.java @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.common; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; +import static org.opentcs.util.Assertions.checkState; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +/** + * An executor service that executes all task directly on the same thread. + */ +public class SameThreadExecutorService + implements + ExecutorService { + + private boolean shutdown; + + public SameThreadExecutorService() { + shutdown = false; + } + + @Override + public void shutdown() { + shutdown = true; + } + + @Override + public List shutdownNow() { + shutdown = true; + return List.of(); + } + + @Override + public boolean isShutdown() { + return shutdown; + } + + @Override + public boolean isTerminated() { + return shutdown; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) + throws InterruptedException { + // Calling `awaitTermination` before calling shutdown is not a valid use for an executor + // service and therefore should throw an exeception. + checkState(shutdown, "Awaiting termination before shutdown was called"); + return true; + } + + @Override + public Future submit(Callable task) { + CompletableFuture future = new CompletableFuture<>(); + try { + future.complete(task.call()); + } + catch (Exception e) { + future.completeExceptionally(e); + } + return future; + } + + @Override + public Future submit(Runnable task, T result) { + return this.submit(() -> { + task.run(); + return result; + }); + } + + @Override + public Future submit(Runnable task) { + return this.submit(task, null); + } + + @Override + public List> invokeAll(Collection> tasks) + throws InterruptedException { + return tasks.stream() + .map(task -> submit(task)) + .collect(Collectors.toList()); + } + + @Override + public List> invokeAll( + Collection> tasks, + long timeout, + TimeUnit unit + ) + throws InterruptedException { + return invokeAll(tasks); + } + + @Override + public T invokeAny(Collection> tasks) + throws InterruptedException, + ExecutionException { + requireNonNull(tasks, "tasks"); + checkArgument(tasks.isEmpty(), "tasks is empty"); + if (tasks.stream().anyMatch(task -> task == null)) { + throw new NullPointerException("At least one task given is null"); + } + + // This implementation interprets the method documentation so that all tasks are started + // before the result of the first successful one is returned. + // Since all task are executed directly on the same thread, all task will finish before this + // method returns the value of any successful task. + List> futures = invokeAll(tasks); + + for (Future future : futures) { + try { + return future.get(); + } + catch (InterruptedException e) { + throw e; + } + catch (Exception e) { + // any other exception thrown by the future is ignored + } + } + + throw new ExecutionException(new Exception("None of the provided task sucessfully terminated")); + } + + @Override + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException, + ExecutionException, + TimeoutException { + return invokeAny(tasks); + } + + @Override + public void execute(Runnable command) { + command.run(); + } + +} diff --git a/opentcs-common/src/main/java/org/opentcs/common/peripherals/NullPeripheralCommAdapterDescription.java b/opentcs-common/src/main/java/org/opentcs/common/peripherals/NullPeripheralCommAdapterDescription.java new file mode 100644 index 0000000..da27f17 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/common/peripherals/NullPeripheralCommAdapterDescription.java @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.common.peripherals; + +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; + +/** + * A {@link PeripheralCommAdapterDescription} for no comm adapter. + */ +public class NullPeripheralCommAdapterDescription + extends + PeripheralCommAdapterDescription { + + /** + * Creates a new instance. + */ + public NullPeripheralCommAdapterDescription() { + } + + @Override + public String getDescription() { + return "-"; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/Colors.java b/opentcs-common/src/main/java/org/opentcs/util/Colors.java new file mode 100644 index 0000000..507ee88 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/Colors.java @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.awt.Color; + +/** + * Provides utilty methods for working with colors. + */ +public class Colors { + + /** + * Prevents instantiation of this utility class. + */ + private Colors() { + } + + /** + * Returns a hexadecimal representation of the given color in the RGB color space. + *

+ * The pattern of the strings returned by this method is {@code "#RRGGBB"}. + *

+ * + * @param color The color to be encoded. + * @return The representation of the given color. + */ + @Nonnull + public static String encodeToHexRGB( + @Nonnull + Color color + ) { + requireNonNull(color, "color"); + + return String.format("#%06X", color.getRGB() & 0x00FFFFFF); + } + + /** + * Returns a {@code Color} instance described by the given hexadecimal representation. + * + * @param rgbHex The hexadecimal representation of the color to be returned in the RGB color + * space. + * + * @return A {@code Color} instance described by the given value. + * @throws NumberFormatException If the given string cannot be parsed. + */ + @Nonnull + public static Color decodeFromHexRGB( + @Nonnull + String rgbHex + ) + throws NumberFormatException { + requireNonNull(rgbHex, "rgbHex"); + + return Color.decode(rgbHex); + } + +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/Comparators.java b/opentcs-common/src/main/java/org/opentcs/util/Comparators.java new file mode 100644 index 0000000..27651d8 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/Comparators.java @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util; + +import java.util.Comparator; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.peripherals.PeripheralJob; + +/** + * Some commonly used comparator implementations. + */ +public final class Comparators { + + /** + * Prevents undesired instantiation. + */ + private Comparators() { + } + + /** + * Returns a comparator for sorting TCSObjects lexicographically by their names. + * + * @return A comparator for sorting TCSObjects lexicographically by their names. + */ + public static Comparator> objectsByName() { + return Comparator.comparing(TCSObject::getName); + } + + /** + * Returns a comparator for sorting TCSObjectReferences lexicographically by their + * names. + * + * @return A comparator for sorting TCSObjectReferences lexicographically by their + * names. + */ + public static Comparator> referencesByName() { + return Comparator.comparing(TCSObjectReference::getName); + } + + /** + * A comparator for sorting transport orders by their deadlines, with the most urgent ones coming + * first. + * Transport orders are sorted by their deadlines first; if two orders have exactly the same + * deadline, they are sorted by their (unique) creation times, with the older one coming first. + * + * @return A comparator for sorting transport orders by their deadlines. + */ + public static Comparator ordersByDeadline() { + return Comparator.comparing(TransportOrder::getDeadline).thenComparing(ordersByAge()); + } + + /** + * A comparator for sorting transport orders by their age, with the oldest ones coming first. + * + * @return A comparator for sorting transport orders by their age. + */ + public static Comparator ordersByAge() { + return Comparator.comparing(TransportOrder::getCreationTime).thenComparing(objectsByName()); + } + + /** + * A comparator for sorting peripheral jobs by their age, with the oldest ones coming first. + * + * @return A comparator for sorting peripheral jobs by their age. + */ + public static Comparator jobsByAge() { + return Comparator.comparing(PeripheralJob::getCreationTime).thenComparing(objectsByName()); + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/Environment.java b/opentcs-common/src/main/java/org/opentcs/util/Environment.java new file mode 100644 index 0000000..c43649c --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/Environment.java @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides generic information about the openTCS environment. + */ +public final class Environment { + + /** + * The baseline properties file. + */ + private static final String BASELINE_PROPS_FILE = "/opentcs.properties"; + /** + * The customization properties file. + */ + private static final String CUSTOMIZATION_PROPS_FILE = "/opentcs-customization.properties"; + /** + * The openTCS baseline version as a string. + */ + private static final String BASELINE_VERSION; + /** + * The build date of the openTCS baseline as a string. + */ + private static final String BASELINE_BUILD_DATE; + /** + * The name of the customized distribution. + */ + private static final String CUSTOMIZATION_NAME; + /** + * The version of the customized distribution as a string. + */ + private static final String CUSTOMIZATION_VERSION; + /** + * The build date of the customized distribution as a string. + */ + private static final String CUSTOMIZATION_BUILD_DATE; + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(Environment.class); + + static { + Properties props = new Properties(); + try { + InputStream inStream = Environment.class.getResourceAsStream(BASELINE_PROPS_FILE); + if (inStream != null) { + props.load(inStream); + } + inStream = Environment.class.getResourceAsStream(CUSTOMIZATION_PROPS_FILE); + if (inStream != null) { + props.load(inStream); + } + } + catch (IOException exc) { + throw new IllegalStateException("Could not load environment properties", exc); + } + BASELINE_VERSION = props.getProperty("opentcs.version", "unknown version"); + BASELINE_BUILD_DATE = props.getProperty("opentcs.builddate", "unknown build date"); + CUSTOMIZATION_NAME = props.getProperty("opentcs.customization.name", "-"); + CUSTOMIZATION_VERSION = props.getProperty("opentcs.customization.version", "-"); + CUSTOMIZATION_BUILD_DATE = props.getProperty("opentcs.customization.builddate", "-"); + } + + /** + * Prevents undesired instantiation. + */ + private Environment() { + } + + /** + * Returns the version of openTCS (i.e. the base library) as a string. + * + * @return The version of openTCS (i.e. the base library) as a string. + */ + public static String getBaselineVersion() { + return BASELINE_VERSION; + } + + /** + * Returns the build date of openTCS (i.e. the base library) as a string. + * + * @return The build date of openTCS (i.e. the base library) as a string. + */ + public static String getBaselineBuildDate() { + return BASELINE_BUILD_DATE; + } + + /** + * Returns the name of the customized distribution. + * + * @return The name of the customized distribution. + */ + public static String getCustomizationName() { + return CUSTOMIZATION_NAME; + } + + /** + * Returns the version of the customized distribution as a string. + * + * @return The version of the customized distribution as a string. + */ + public static String getCustomizationVersion() { + return CUSTOMIZATION_VERSION; + } + + /** + * Returns the build date of the customized distribution as a string. + * + * @return The build date of the customized distribution as a string. + */ + public static String getCustomizationBuildDate() { + return CUSTOMIZATION_BUILD_DATE; + } + + /** + * Write information about the OpenTCS version, the operating system and + * the running Java VM to the log. + */ + public static void logSystemInfo() { + LOG.info( + "openTCS baseline version: {} (build date: {}), " + + "customization '{}' version {} (build date: {}), " + + "Java: {}, {}; JVM: {}, {}; OS: {}, {}", + BASELINE_VERSION, + BASELINE_BUILD_DATE, + CUSTOMIZATION_NAME, + CUSTOMIZATION_VERSION, + CUSTOMIZATION_BUILD_DATE, + System.getProperty("java.version"), + System.getProperty("java.vendor"), + System.getProperty("java.vm.version"), + System.getProperty("java.vm.vendor"), + System.getProperty("os.name"), + System.getProperty("os.arch") + ); + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/FileSystems.java b/opentcs-common/src/main/java/org/opentcs/util/FileSystems.java new file mode 100644 index 0000000..880dfe6 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/FileSystems.java @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util; + +import static java.util.Objects.requireNonNull; + +import java.io.File; + +/** + * This class provides helper methods for working with file systems. + */ +public final class FileSystems { + + /** + * Prevents creation of instances. + */ + private FileSystems() { + } + + /** + * Recursively deletes a given file/directory. + * + * @param target The file/directory to be deleted recursively. + * @return true if deleting the target was successful, else + * false. + */ + public static boolean deleteRecursively(File target) { + requireNonNull(target, "target"); + + // If the target is a directory, remove its contents first. + if (target.isDirectory()) { + File[] entries = target.listFiles(); + for (File curEntry : entries) { + boolean successful; + + if (curEntry.isDirectory()) { + successful = deleteRecursively(curEntry); + } + else { + successful = curEntry.delete(); + } + + if (!successful) { + return false; + } + } + } + + return target.delete(); + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/I18nCommon.java b/opentcs-common/src/main/java/org/opentcs/util/I18nCommon.java new file mode 100644 index 0000000..755d668 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/I18nCommon.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util; + +/** + * Defines constants regarding internationalization. + */ +public interface I18nCommon { + + /** + * The path to the project's resource bundle. + */ + String BUNDLE_PATH = "i18n/org/opentcs/common/Bundle"; +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/NumberParsers.java b/opentcs-common/src/main/java/org/opentcs/util/NumberParsers.java new file mode 100644 index 0000000..12943b2 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/NumberParsers.java @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkInRange; + +/** + * Provides methods for optimized parsing of numbers from character sequences. + */ +public final class NumberParsers { + + /** + * The number of characters used for Long.MAX_VALUE. + */ + private static final int MAX_POSITIVE_LONG_CHARS = Long.toString(Long.MAX_VALUE).length(); + /** + * The number of characters used for Long.MIN_VALUE. + */ + private static final int MAX_NEGATIVE_LONG_CHARS = Long.toString(Long.MIN_VALUE).length(); + + /** + * Prevents creation of instances of this class. + */ + private NumberParsers() { + } + + /** + * Parses a sequence of characters as a decimal number and returns the latter. + * + * @param source The character sequence to be parsed. + * @return The decimal number represented by the given character sequence. + * @throws NumberFormatException If the parsed sequence of characters does not + * represent a decimal number. + */ + public static long parsePureDecimalLong(CharSequence source) + throws NumberFormatException { + return parsePureDecimalLong(source, 0, source.length()); + } + + /** + * Parses a (sub)sequence of characters as a decimal number and returns the + * latter. + * + * @param source The character sequence to be parsed. + * @param startIndex The position at which to start parsing. + * @param length The number of characters to parse. + * @return The decimal number represented by the given character sequence. + * @throws NumberFormatException If the parsed sequence of characters does not + * represent a decimal number. + */ + public static long parsePureDecimalLong( + CharSequence source, + int startIndex, + int length + ) + throws NumberFormatException { + requireNonNull(source, "source"); + checkInRange(startIndex, 0, source.length() - 1, "startIndex"); + checkInRange(length, 1, Integer.MAX_VALUE, "length"); + + long result = 0; + int index = 0; + boolean negative; + long limit; + // Check if we have a negative number and initialize accordingly. + if (source.charAt(startIndex) == '-') { + if (length > MAX_NEGATIVE_LONG_CHARS) { + throw new NumberFormatException("too long to be parsed"); + } + negative = true; + limit = Long.MIN_VALUE; + index++; + } + else { + if (length > MAX_POSITIVE_LONG_CHARS) { + throw new NumberFormatException("too long to be parsed"); + } + negative = false; + limit = -Long.MAX_VALUE; + } + while (index < length) { + int digit = source.charAt(startIndex + index) - '0'; + // If we've just read something other than a digit, throw an exception. + if (digit < 0 || digit > 9) { + throw new NumberFormatException( + "not a decimal digit: " + source.charAt(startIndex + index) + ); + } + result *= 10; + // Check if the next operation would overflow the result. + if (result < limit + digit) { + throw new NumberFormatException( + "parsed number exceeds value boundaries" + ); + } + result -= digit; + index++; + } + if (negative) { + // If we did not parse at least one digit, throw an exception. + if (index < 2) { + throw new NumberFormatException("minus sign without succeeding digits"); + } + return result; + } + else { + return -result; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/UniqueStringGenerator.java b/opentcs-common/src/main/java/org/opentcs/util/UniqueStringGenerator.java new file mode 100644 index 0000000..3798967 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/UniqueStringGenerator.java @@ -0,0 +1,197 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import java.text.DecimalFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * Provides a way to acquire unique strings. + * + * @param The type of the selectors/keys to be used for mapping to name + * patterns. + */ +public class UniqueStringGenerator { + + /** + * Configured name patterns. + */ + private final Map namePatterns = new HashMap<>(); + /** + * All strings known to this generator, sorted lexicographically. + */ + private final SortedSet existingStrings = new TreeSet<>(); + + /** + * Creates a new instance without any name patterns. + */ + public UniqueStringGenerator() { + // Do nada. + } + + /** + * Registers a name pattern for the given selector. + * + * @param selector The selector. + * @param prefix The prefix of names to be used for the given selector. + * @param suffixPattern The suffix pattern to be used for the given selector. + */ + public void registerNamePattern( + S selector, + String prefix, + String suffixPattern + ) { + namePatterns.put(selector, new NamePattern(prefix, suffixPattern)); + } + + /** + * Adds a String to those known by this generator. As a result, this generator + * will never return a String that is equal to the given one. + * + * @param newString The string to be added. + */ + public void addString(final String newString) { + requireNonNull(newString, "newString is null"); + existingStrings.add(newString); + } + + /** + * Makes this generator forget a known String. As a result, this generator + * might return a String that is equal to the given one in the future. + * + * @param rmString The string to be forgotten. + */ + public void removeString(final String rmString) { + requireNonNull(rmString, "rmString is null"); + existingStrings.remove(rmString); + } + + /** + * Returns true if this generator has this string. + * + * @param str The string to test. + * @return true if this generator already has this string. + */ + public boolean hasString(String str) { + return existingStrings.contains(str); + } + + /** + * Removes all known Strings. + */ + public void clear() { + existingStrings.clear(); + } + + /** + * Returns a string that is unique among all strings registered with this + * generator. + * + * @param selector A selector for the name pattern to be used. + * @return A string that is unique among all strings registered with this + * generator. + */ + public String getUniqueString(S selector) { + requireNonNull(selector, "selector"); + + NamePattern namePattern = namePatterns.get(selector); + checkArgument(namePattern != null, "Unknown selector: %s", selector); + + return getUniqueString(namePattern.prefix, namePattern.suffixPattern); + } + + /** + * Returns a String that is unique among all known Strings in this generator. + * The returned String will consist of the given prefix followed by an integer + * formatted according to the given pattern. The pattern has to be of the form + * understood by java.text.DecimalFormat. + * + * @param prefix The prefix of the String to be generated. + * @param suffixPattern A pattern describing the suffix of the generated + * String. Must be of the form understood by + * java.text.DecimalFormat. + * @return A String that is unique among all known Strings. + */ + public String getUniqueString( + final String prefix, + final String suffixPattern + ) { + requireNonNull(suffixPattern, "suffixPattern is null"); + + final String actualPrefix = prefix == null ? "" : prefix; + final DecimalFormat format = new DecimalFormat(suffixPattern); + final String lBound = actualPrefix + "0"; + final String uBound = actualPrefix + ":"; + final int prefixLength = actualPrefix.length(); + long maxSuffixValue = 0; + // Get all existing strings with the same prefix and at least one digit + // following it. + for (String curName : existingStrings.subSet(lBound, uBound)) { + // Check if the suffix contains only digits. + boolean allDigits = containsOnlyDigits(curName.substring(prefixLength)); + // If the suffix contains only digits, parse it and remember the maximum + // suffix value we found so far. (If the suffix contains other characters, + // ignore this string - we generate suffixes with digits only, so there + // can't be a collision. + if (allDigits) { + final long curSuffixValue = NumberParsers.parsePureDecimalLong( + curName, prefixLength, curName.length() - prefixLength + ); + maxSuffixValue + = maxSuffixValue > curSuffixValue ? maxSuffixValue : curSuffixValue; + } + } + // Increment the highest value found and use that as the suffix + return actualPrefix + format.format(maxSuffixValue + 1); + } + + /** + * Checks if the given string contains only (decimal) digits. + * + * @param input The string to be checked. + * @return true if, and only if, the given string contains only + * (decimal) digits. + */ + private boolean containsOnlyDigits(String input) { + assert input != null; + for (int i = 0; i < input.length(); i++) { + int digit = input.charAt(i) - '0'; + if (digit < 0 || digit > 9) { + return false; + } + } + return true; + } + + /** + * A name pattern. + */ + private static class NamePattern { + + /** + * The prefix to be used. + */ + private final String prefix; + /** + * The suffix pattern to be used. + */ + private final String suffixPattern; + + /** + * Creates a new instance. + * + * @param prefix The prefix to be used. + * @param suffixPattern The suffix pattern to be used. + */ + private NamePattern(String prefix, String suffixPattern) { + this.prefix = requireNonNull(prefix, "prefix"); + this.suffixPattern = requireNonNull(suffixPattern, "suffixPattern"); + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/gui/BoundsPopupMenuListener.java b/opentcs-common/src/main/java/org/opentcs/util/gui/BoundsPopupMenuListener.java new file mode 100644 index 0000000..82d2ec0 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/gui/BoundsPopupMenuListener.java @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.gui; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Point; +import javax.swing.JComboBox; +import javax.swing.JList; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; +import javax.swing.plaf.basic.BasicComboPopup; + +/** + * Changes the bounds of a JComboBox's popup menu to allow the popup to be wieder than the combo + * box. + * Register it with a combo box using + * {@link JComboBox#addPopupMenuListener(javax.swing.event.PopupMenuListener)}. + * + *

+ * This class will only work for a JComboBox that uses a BasicComboPop. + *

+ */ +public class BoundsPopupMenuListener + implements + PopupMenuListener { + + /** + * The scrollpane of the combobox. + */ + private JScrollPane scrollPane; + + /** + * General purpose constructor to set all popup properties at once. + * + */ + public BoundsPopupMenuListener() { + } + + /** + * Alter the bounds of the popup just before it is made visible. + * + * @param e The event. + */ + @Override + public void popupMenuWillBecomeVisible(PopupMenuEvent e) { + JComboBox comboBox = (JComboBox) e.getSource(); + if (comboBox.getItemCount() == 0) { + return; + } + final Object child = comboBox.getAccessibleContext().getAccessibleChild(0); + + if (child instanceof BasicComboPopup) { + SwingUtilities.invokeLater(() -> customizePopup((BasicComboPopup) child)); + } + } + + @Override + public void popupMenuCanceled(PopupMenuEvent e) { + } + + @Override + public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { + // In its normal state the scrollpane does not have a scrollbar + if (scrollPane != null) { + scrollPane.setHorizontalScrollBar(null); + } + } + + protected void customizePopup(BasicComboPopup popup) { + scrollPane = getScrollPane(popup); + popupWider(popup); + + // For some reason in JDK7 the popup will not display at its preferred + // width unless its location has been changed from its default + // (ie. for normal "pop down" shift the popup and reset) + Component comboBox = popup.getInvoker(); + Point location = comboBox.getLocationOnScreen(); + + int height = comboBox.getSize().height; + popup.setLocation(location.x, location.y + height - 1); + popup.setLocation(location.x, location.y + height); + } + + /** + * Adjusts the width of the scrollpane used by the popup. + * + * @param popup The popup. + */ + protected void popupWider(BasicComboPopup popup) { + JList list = popup.getList(); + + // Determine the maximimum width to use: + // a) determine the popup preferred width + // b) ensure width is not less than the scroll pane width + int popupWidth = list.getPreferredSize().width + + 5 // make sure horizontal scrollbar doesn't appear + + getScrollBarWidth(popup, scrollPane); + + Dimension scrollPaneSize = scrollPane.getPreferredSize(); + popupWidth = Math.max(popupWidth, scrollPaneSize.width); + + // Adjust the width + scrollPaneSize.width = popupWidth; + scrollPane.setPreferredSize(scrollPaneSize); + scrollPane.setMaximumSize(scrollPaneSize); + } + + /** + * Returns the scroll pane used by the popup so its bounds can be adjusted. + * + * @param popup The popup. + * @return The scroll pane used by the popup . + */ + protected JScrollPane getScrollPane(BasicComboPopup popup) { + JList list = popup.getList(); + Container c = SwingUtilities.getAncestorOfClass(JScrollPane.class, list); + + return (JScrollPane) c; + } + + protected int getScrollBarWidth(BasicComboPopup popup, JScrollPane scrollPane) { + // I can't find any property on the scrollBar to determine if it will be + // displayed or not so use brute force to determine this. + JComboBox comboBox = (JComboBox) popup.getInvoker(); + + if (comboBox.getItemCount() > comboBox.getMaximumRowCount()) { + return scrollPane.getVerticalScrollBar().getPreferredSize().width; + } + else { + return 0; + } + } + +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/gui/Icons.java b/opentcs-common/src/main/java/org/opentcs/util/gui/Icons.java new file mode 100644 index 0000000..12256ff --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/gui/Icons.java @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.gui; + +import java.awt.Image; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import javax.imageio.ImageIO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Static methods related to window icons. + */ +public final class Icons { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(Icons.class); + /** + * Path to the openTCS window icons. + */ + private static final String ICON_PATH = "/org/opentcs/util/gui/res/icons/"; + /** + * File names of the openTCS window icons. + */ + private static final String[] ICON_FILES = {"opentcs_icon_016.png", + "opentcs_icon_032.png", + "opentcs_icon_064.png", + "opentcs_icon_128.png", + "opentcs_icon_256.png"}; + + /** + * Prevents instantiation. + */ + private Icons() { + // Do nada. + } + + /** + * Get the icon for OpenTCS windows in different resolutions. + * + * @return List of icons + */ + public static List getOpenTCSIcons() { + try { + List icons = new ArrayList<>(); + for (String iconFile : ICON_FILES) { + String iconURL = ICON_PATH + iconFile; + final Image icon = ImageIO.read(Icons.class.getResource(iconURL)); + icons.add(icon); + } + return icons; + } + catch (IOException | IllegalArgumentException exc) { + LOG.warn("Couldn't load icon images from path {}", ICON_PATH, exc); + return new ArrayList<>(); + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/gui/StringListCellRenderer.java b/opentcs-common/src/main/java/org/opentcs/util/gui/StringListCellRenderer.java new file mode 100644 index 0000000..ece1336 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/gui/StringListCellRenderer.java @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.gui; + +import static java.util.Objects.requireNonNull; + +import java.awt.Component; +import java.util.function.Function; +import javax.swing.DefaultListCellRenderer; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.ListCellRenderer; + +/** + * Renders values to JLabels. + * + * @param The type of the values to be rendered. + */ +public class StringListCellRenderer + implements + ListCellRenderer { + + /** + * A default renderer for creating the label. + */ + private final DefaultListCellRenderer defaultRenderer = new DefaultListCellRenderer(); + /** + * Returns a String representation of E. + */ + private final Function representer; + + /** + * Creates an instance. + * + * @param representer a string representation provider for the values of the list. + * Null value as parameter for the representer is possible. + * The result is set as text of the JLabel. + */ + public StringListCellRenderer(Function representer) { + this.representer = requireNonNull(representer, "representer"); + } + + @Override + public Component getListCellRendererComponent( + JList list, + E value, + int index, + boolean isSelected, + boolean cellHasFocus + ) { + JLabel label = (JLabel) defaultRenderer.getListCellRendererComponent( + list, + value, + index, + isSelected, + cellHasFocus + ); + label.setText(representer.apply(value)); + return label; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/gui/StringTableCellRenderer.java b/opentcs-common/src/main/java/org/opentcs/util/gui/StringTableCellRenderer.java new file mode 100644 index 0000000..7102c2d --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/gui/StringTableCellRenderer.java @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.gui; + +import static java.util.Objects.requireNonNull; + +import java.awt.Component; +import java.util.function.Function; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableCellRenderer; + +/** + * Renders values to JLabels. + * + * @param The type of the table cell values that the representer can represent. + */ +public class StringTableCellRenderer + implements + TableCellRenderer { + + /** + * The default renderer we delegate to. + */ + private final DefaultTableCellRenderer delegate = new DefaultTableCellRenderer(); + /** + * Returns a String representation of E. + */ + private final Function representer; + + /** + * Creates an instance. + * + * @param representer a string representation provider for the values of the list. + * Null value as parameter for the representer is possible. + * The result is set as text of the JLabel. + */ + public StringTableCellRenderer(Function representer) { + this.representer = requireNonNull(representer, "representer"); + } + + @Override + public Component getTableCellRendererComponent( + JTable table, Object value, boolean isSelected, + boolean hasFocus, int row, int column + ) { + JLabel label = (JLabel) delegate.getTableCellRendererComponent( + table, + value, + isSelected, + hasFocus, + row, + column + ); + @SuppressWarnings("unchecked") + E val = (E) value; + label.setText(representer.apply(val)); + return label; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/gui/dialog/CancelButton.java b/opentcs-common/src/main/java/org/opentcs/util/gui/dialog/CancelButton.java new file mode 100644 index 0000000..350dab1 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/gui/dialog/CancelButton.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.gui.dialog; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import javax.swing.JButton; +import javax.swing.KeyStroke; + +/** + * Cancel Button which closes a dialog by pressing ESC. + */ +public class CancelButton + extends + JButton { + + /** + * Creates a new instance. + */ + public CancelButton() { + this(null); + } + + /** + * Creates a new instance. + * + * @param text Label of this button. + */ + @SuppressWarnings("this-escape") + public CancelButton(String text) { + super(text); + + ActionListener al = new ActionListener() { + + @Override + public void actionPerformed(ActionEvent event) { + String cmd = event.getActionCommand(); + + if (cmd.equals("PressedESCAPE")) { + doClick(); + } + } + }; + + registerKeyboardAction( + al, "PressedESCAPE", + KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), + JButton.WHEN_IN_FOCUSED_WINDOW + ); + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/gui/dialog/ConnectToServerDialog.form b/opentcs-common/src/main/java/org/opentcs/util/gui/dialog/ConnectToServerDialog.form new file mode 100644 index 0000000..1cd0778 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/gui/dialog/ConnectToServerDialog.form @@ -0,0 +1,204 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-common/src/main/java/org/opentcs/util/gui/dialog/ConnectToServerDialog.java b/opentcs-common/src/main/java/org/opentcs/util/gui/dialog/ConnectToServerDialog.java new file mode 100644 index 0000000..9981578 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/gui/dialog/ConnectToServerDialog.java @@ -0,0 +1,355 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.gui.dialog; + +import static java.util.Objects.requireNonNull; + +import java.rmi.registry.Registry; +import java.util.List; +import java.util.ResourceBundle; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JOptionPane; +import org.opentcs.util.I18nCommon; +import org.opentcs.util.gui.Icons; + +/** + * A dialog that lets the user enter parameters for a connection to the portal. + */ +public class ConnectToServerDialog + extends + JDialog { + + /** + * A return status code - returned if Cancel button has been pressed. + */ + public static final int RET_CANCEL = 0; + /** + * A return status code - returned if OK button has been pressed. + */ + public static final int RET_OK = 1; + /** + * This class's resource bundle. + */ + private static final ResourceBundle BUNDLE + = ResourceBundle.getBundle(I18nCommon.BUNDLE_PATH); + /** + * The connection's description. + */ + private String description; + /** + * The host to connect to. + */ + private String host; + /** + * The port to connect to. + */ + private int port; + /** + * The list of connection param sets. + */ + private final List paramSets; + /** + * This dialog's return status code. + */ + private int returnStatus = RET_CANCEL; + + /** + * Creates a new instance. + * + * @param paramSets The list of connection param sets. + */ + @SuppressWarnings("this-escape") + public ConnectToServerDialog(List paramSets) { + super((JFrame) null, true); + this.paramSets = requireNonNull(paramSets, "paramSets"); + + initComponents(); + initConnectionBookmarks(); + + if (paramSets.isEmpty()) { + description = "Localhost"; + host = "localhost"; + port = Registry.REGISTRY_PORT; + } + else { + description = paramSets.get(0).getDescription(); + host = paramSets.get(0).getHost(); + port = paramSets.get(0).getPort(); + } + + textFieldDescription.setText(description); + textFieldServer.setText(host); + textFieldPort.setText(String.valueOf(port)); + getRootPane().setDefaultButton(okButton); + + setIconImages(Icons.getOpenTCSIcons()); + setLocationRelativeTo(null); + pack(); + } + + public String getDescription() { + return description; + } + + public int getPort() { + return port; + } + + public String getHost() { + return host; + } + + /** + * Returns the return status of this dialog. + * + * @return the return status of this dialog - one of {@link #RET_OK} or {@link #RET_CANCEL}. + */ + public int getReturnStatus() { + return returnStatus; + } + + private void doClose(int retStatus) { + returnStatus = retStatus; + setVisible(false); + dispose(); + } + + /** + * Initializes connection bookmarks from the config file. + */ + private void initConnectionBookmarks() { + DefaultComboBoxModel model + = (DefaultComboBoxModel) cbComboBox.getModel(); + + for (ConnectionParamSet bookmark : paramSets) { + model.addElement(bookmark); + } + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + labelDescription = new javax.swing.JLabel(); + textFieldDescription = new javax.swing.JTextField(); + labelServer = new javax.swing.JLabel(); + textFieldServer = new javax.swing.JTextField(); + labelPort = new javax.swing.JLabel(); + textFieldPort = new javax.swing.JTextField(); + panelButtons = new javax.swing.JPanel(); + okButton = new javax.swing.JButton(); + cancelButton = new CancelButton(); + cbComboBox = new javax.swing.JComboBox<>(); + + setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/common/Bundle"); // NOI18N + setTitle(bundle.getString("connectToServerDialog.title")); // NOI18N + getContentPane().setLayout(new java.awt.GridBagLayout()); + + labelDescription.setFont(labelDescription.getFont()); + labelDescription.setText(bundle.getString("connectToServerDialog.label_description.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 8, 0, 4); + getContentPane().add(labelDescription, gridBagConstraints); + + textFieldDescription.setColumns(15); + textFieldDescription.setFont(textFieldDescription.getFont()); + textFieldDescription.setText("Localhost"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(4, 0, 0, 8); + getContentPane().add(textFieldDescription, gridBagConstraints); + + labelServer.setFont(labelServer.getFont()); + labelServer.setText(bundle.getString("connectToServerDialog.label_host.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 8, 0, 4); + getContentPane().add(labelServer, gridBagConstraints); + + textFieldServer.setColumns(15); + textFieldServer.setFont(textFieldServer.getFont()); + textFieldServer.setText("localhost"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(4, 0, 0, 8); + getContentPane().add(textFieldServer, gridBagConstraints); + + labelPort.setFont(labelPort.getFont()); + labelPort.setText(bundle.getString("connectToServerDialog.label_port.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 8, 0, 4); + getContentPane().add(labelPort, gridBagConstraints); + + textFieldPort.setColumns(15); + textFieldPort.setFont(textFieldPort.getFont()); + textFieldPort.setText("1099"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(4, 0, 0, 8); + getContentPane().add(textFieldPort, gridBagConstraints); + + panelButtons.setLayout(new java.awt.GridBagLayout()); + + okButton.setFont(okButton.getFont().deriveFont(okButton.getFont().getStyle() | java.awt.Font.BOLD)); + okButton.setText(bundle.getString("connectToServerDialog.button_ok.text")); // NOI18N + okButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + okButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 4); + panelButtons.add(okButton, gridBagConstraints); + + cancelButton.setFont(cancelButton.getFont()); + cancelButton.setText(bundle.getString("connectToServerDialog.button_cancle.text")); // NOI18N + cancelButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cancelButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 4); + panelButtons.add(cancelButton, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.PAGE_END; + gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 4); + getContentPane().add(panelButtons, gridBagConstraints); + + cbComboBox.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + cbComboBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cbComboBoxActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTH; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(4, 0, 0, 8); + getContentPane().add(cbComboBox, gridBagConstraints); + + pack(); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void okButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_okButtonActionPerformed + description = textFieldDescription.getText(); + if (description.isEmpty()) { + JOptionPane.showMessageDialog( + this, + BUNDLE.getString("connectToServerDialog.optionPane_invalidDescription.message"), + BUNDLE.getString("connectToServerDialog.optionPane_invalidDescription.title"), + JOptionPane.ERROR_MESSAGE + ); + return; + } + + host = textFieldServer.getText(); + if (host.isEmpty()) { + JOptionPane.showMessageDialog( + this, + BUNDLE.getString("connectToServerDialog.optionPane_invalidHost.message"), + BUNDLE.getString("connectToServerDialog.optionPane_invalidHost.title"), + JOptionPane.ERROR_MESSAGE + ); + return; + } + + try { + port = Integer.parseInt(textFieldPort.getText()); + if (port < 0 || port > 65535) { + throw new NumberFormatException(); + } + } + catch (NumberFormatException e) { + JOptionPane.showMessageDialog( + this, + BUNDLE.getString("connectToServerDialog.optionPane_invalidPort.message"), + BUNDLE.getString("connectToServerDialog.optionPane_invalidPort.title"), + JOptionPane.ERROR_MESSAGE + ); + return; + } + + doClose(RET_OK); + }//GEN-LAST:event_okButtonActionPerformed + + private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed + doClose(RET_CANCEL); + }//GEN-LAST:event_cancelButtonActionPerformed + + private void cbComboBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cbComboBoxActionPerformed + DefaultComboBoxModel model + = (DefaultComboBoxModel) cbComboBox.getModel(); + ConnectionParamSet cb = (ConnectionParamSet) model.getSelectedItem(); + + if (cb != null) { + textFieldDescription.setText(cb.getDescription()); + textFieldServer.setText(cb.getHost()); + textFieldPort.setText(String.valueOf(cb.getPort())); + } + }//GEN-LAST:event_cbComboBoxActionPerformed + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton cancelButton; + private javax.swing.JComboBox cbComboBox; + private javax.swing.JLabel labelDescription; + private javax.swing.JLabel labelPort; + private javax.swing.JLabel labelServer; + private javax.swing.JButton okButton; + private javax.swing.JPanel panelButtons; + private javax.swing.JTextField textFieldDescription; + private javax.swing.JTextField textFieldPort; + private javax.swing.JTextField textFieldServer; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:OFF +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/gui/dialog/ConnectionParamSet.java b/opentcs-common/src/main/java/org/opentcs/util/gui/dialog/ConnectionParamSet.java new file mode 100644 index 0000000..6afbf94 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/gui/dialog/ConnectionParamSet.java @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.gui.dialog; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; +import static org.opentcs.util.Assertions.checkInRange; + +import java.rmi.registry.Registry; +import java.util.Objects; + +/** + * A set of parameters for a connection to the portal. + */ +public class ConnectionParamSet { + + /** + * The description. + */ + private final String description; + /** + * The host name. + */ + private final String host; + /** + * The port number. + */ + private final int port; + + /** + * Creates a new instance. + * + * @param description The description for this connection. + * @param host The host to be used. + * @param port The port number to be used. + * @throws IllegalArgumentException If the port number is out of the range of valid port numbers. + */ + public ConnectionParamSet(String description, String host, int port) { + this.description = requireNonNull(description, "description"); + this.host = requireNonNull(host); + this.port = checkInRange(port, 0, 65535, "port"); + } + + /** + * Creates a new instance. + * + * @param description The description for this connection. + * @param host The host to be used. + * @param port The port number to be used. + * @throws NumberFormatException If the port string does not contain a parseable port number. + * @throws IllegalArgumentException If the port number is out of the range of valid port numbers. + */ + public ConnectionParamSet(String description, String host, String port) + throws NumberFormatException, + IllegalArgumentException { + this(description, host, Integer.parseInt(port)); + } + + /** + * Creates a new instance with the description, host and port parsed from the given string, which + * must follow a pattern of "description:host:port". + * + * @param paramString The string containing the description, host and port. + */ + public ConnectionParamSet(String paramString) { + requireNonNull(paramString, "paramString"); + String[] split = paramString.split("\\|", 3); + checkArgument( + split.length == 3, + "Could not parse input as 'description:host:port': %s", + paramString + ); + this.description = split[0]; + this.host = split[1]; + this.port = checkInRange(Integer.parseInt(split[2]), 0, 65535, "port"); + } + + /** + * Creates a new instance for host "localhost" and port 1099. + */ + public ConnectionParamSet() { + this("Localhost", "localhost", Registry.REGISTRY_PORT); + } + + /** + * Returns the description. + * + * @return The description. + */ + public String getDescription() { + return description; + } + + /** + * Returns the host parameter. + * + * @return The host parameter. + */ + public String getHost() { + return host; + } + + /** + * Returns the port parameter. + * + * @return The port parameter. + */ + public int getPort() { + return port; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ConnectionParamSet)) { + return false; + } + ConnectionParamSet other = (ConnectionParamSet) o; + return description.equals(other.description) && host.equals(other.host) && port == other.port; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 23 * hash + Objects.hashCode(this.description); + hash = 23 * hash + Objects.hashCode(this.host); + hash = 23 * hash + this.port; + return hash; + } + + @Override + public String toString() { + return getDescription() + " - " + getHost() + ":" + getPort(); + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/gui/dialog/NullConnectionParamSet.java b/opentcs-common/src/main/java/org/opentcs/util/gui/dialog/NullConnectionParamSet.java new file mode 100644 index 0000000..ad3366a --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/gui/dialog/NullConnectionParamSet.java @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.gui.dialog; + +/** + * A connection param set used for not established connections. + */ +public class NullConnectionParamSet + extends + ConnectionParamSet { + + public NullConnectionParamSet() { + super("-", "", 0); + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/gui/package-info.java b/opentcs-common/src/main/java/org/opentcs/util/gui/package-info.java new file mode 100644 index 0000000..20dd85e --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/gui/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Supportive classes for graphical frontends. + */ +package org.opentcs.util.gui; diff --git a/opentcs-common/src/main/java/org/opentcs/util/logging/SingleLineFormatter.java b/opentcs-common/src/main/java/org/opentcs/util/logging/SingleLineFormatter.java new file mode 100644 index 0000000..2afdc42 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/logging/SingleLineFormatter.java @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.logging; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.logging.Formatter; +import java.util.logging.LogRecord; + +/** + * A Formatter for LogRecords that formats messages + * for output on a single line (except when a Throwable is + * associated with the LogRecord. + */ +public class SingleLineFormatter + extends + Formatter { + + /** + * A DateFormat instance for formatting timestamps. + */ + private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd-HH:mm:ss-SSS"); + /** + * A Date instance for formatting timestamps. + */ + private final Date date = new Date(); + /** + * Line separator for wrapping lines at the end of log messages. + */ + private final String lineSeparator = System.getProperty("line.separator"); + + /** + * Creates a new SingleLineFormatter. + */ + public SingleLineFormatter() { + } + + @Override + public synchronized String format(LogRecord record) { + date.setTime(record.getMillis()); + StringBuilder result = new StringBuilder(); + + result.append('[') + .append(DATE_FORMAT.format(date)) + .append("] ") + .append(String.format("%1$-7.7s", record.getLevel().getName())) + .append(' ') + .append(String.format("%1$-20s", Thread.currentThread().getName())) + .append(' ') + .append(String.format("%1$-55s", source(record))) + .append(": ") + .append(formatMessage(record)) + .append(lineSeparator); + + if (record.getThrown() != null) { + result.append(stackTrace(record.getThrown())); + result.append(lineSeparator); + } + + return result.toString(); + } + + private String source(LogRecord record) { + return record.getSourceClassName() != null + ? record.getSourceClassName() + .replaceAll("\\B\\w+(\\.[a-z])", "$1") + "." + record.getSourceMethodName() + "()" + : record.getLoggerName(); + } + + private String stackTrace(Throwable thrown) { + try (StringWriter sWriter = new StringWriter(); + PrintWriter pWriter = new PrintWriter(sWriter)) { + thrown.printStackTrace(pWriter); + pWriter.flush(); + return sWriter.toString(); + } + catch (IOException exc) { + throw new IllegalStateException("Could not print stack trace for log output", exc); + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/logging/UncaughtExceptionLogger.java b/opentcs-common/src/main/java/org/opentcs/util/logging/UncaughtExceptionLogger.java new file mode 100644 index 0000000..4d1bfab --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/logging/UncaughtExceptionLogger.java @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.logging; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An UncaughtExceptionHandler that logs everything not caught and then exits. + */ +public class UncaughtExceptionLogger + implements + Thread.UncaughtExceptionHandler { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(UncaughtExceptionLogger.class); + /** + * A flag indicating whether to exit on uncaught exceptions or not. + */ + private final boolean doExit; + + /** + * Creates a new UncaughtExceptionLogger. + * + * @param exitOnException A flag indicating whether to exit on uncaught + * exceptions or not. + */ + public UncaughtExceptionLogger(boolean exitOnException) { + super(); + doExit = exitOnException; + } + + @Override + public void uncaughtException(Thread t, Throwable e) { + // Log the exception, and then get out of here. + LOG.error("Unhandled exception", e); + if (doExit) { + System.exit(1); + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/logging/package-info.java b/opentcs-common/src/main/java/org/opentcs/util/logging/package-info.java new file mode 100644 index 0000000..54988d9 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/logging/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Utility classes extending basic logging features. + */ +package org.opentcs.util.logging; diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/BasePlantModelTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/BasePlantModelTO.java new file mode 100644 index 0000000..c795e05 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/BasePlantModelTO.java @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlTransient; + +/** + * The base class for a plant model transfer object. + */ +@XmlTransient +@XmlAccessorType(XmlAccessType.PROPERTY) +public class BasePlantModelTO { + + private String version = ""; + + /** + * Creates a new instance. + */ + public BasePlantModelTO() { + } + + @XmlAttribute(required = true) + public String getVersion() { + return version; + } + + public BasePlantModelTO setVersion( + @Nonnull + String version + ) { + requireNonNull(version, "version"); + this.version = version; + return this; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/ModelParser.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/ModelParser.java new file mode 100644 index 0000000..60a4102 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/ModelParser.java @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.nio.charset.Charset; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.util.persistence.v6.V6ModelParser; +import org.opentcs.util.persistence.v6.V6PlantModelTO; +import org.opentcs.util.persistence.v6.V6TOMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides methods for parsing {@link PlantModelCreationTO}s from/to a file. + */ +public class ModelParser { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ModelParser.class); + /** + * The charset to use for the reader/writer. + */ + private static final Charset CHARSET = Charset.forName("UTF-8"); + + /** + * Creates a new instance. + */ + public ModelParser() { + } + + /** + * Reads a model from the given file and parses it to a {@link PlantModelCreationTO} instance. + * + * @param file The model file to read. + * @return The parsed {@link PlantModelCreationTO}. + * @throws IOException If there was an error reading the model file. + * @throws IllegalArgumentException If there is no parser for the version of the model file. + */ + public PlantModelCreationTO readModel(File file) + throws IOException { + String modelVersion = peekModelVersion(file); + + LOG.debug("File '{}' contains a model version '{}'.", file.getAbsolutePath(), modelVersion); + + try (Reader reader = new BufferedReader( + new InputStreamReader( + new FileInputStream(file), + CHARSET + ) + )) { + return new V6ModelParser().read(reader, modelVersion); + } + } + + /** + * Writes the given {@link PlantModelCreationTO} to the given file. + * + * @param model The model. + * @param file The file to write the model to. + * @throws IOException If there was an error writing to the model file. + */ + public void writeModel(PlantModelCreationTO model, File file) + throws IOException { + try (Writer writer = new BufferedWriter( + new OutputStreamWriter( + new FileOutputStream(file), + CHARSET + ) + )) { + V6TOMapper mapper = new V6TOMapper(); + V6PlantModelTO mappedModel = mapper.map(model); + mappedModel.toXml(writer); + } + } + + private String peekModelVersion(File file) + throws IOException { + try (Reader reader = new BufferedReader( + new InputStreamReader( + new FileInputStream(file), + CHARSET + ) + )) { + return ProbePlantModelTO.fromXml(reader).getVersion(); + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/ProbePlantModelTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/ProbePlantModelTO.java new file mode 100644 index 0000000..ddd929e --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/ProbePlantModelTO.java @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.IOException; +import java.io.Reader; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; +import org.xml.sax.SAXException; + +/** + * Allows reading a model file to access basic information (such as the model version) for + * validation purposes. + */ +@XmlRootElement(name = "model") +@XmlAccessorType(XmlAccessType.PROPERTY) +public class ProbePlantModelTO + extends + BasePlantModelTO { + + /** + * Creates a new instance. + */ + public ProbePlantModelTO() { + } + + /** + * Unmarshals an instance of this class from the given XML representation. + * + * @param reader Provides the XML representation to parse to an instance. + * @return The instance unmarshalled from the given reader. + * @throws IOException If there was a problem unmarshalling the given string. + */ + public static ProbePlantModelTO fromXml( + @Nonnull + Reader reader + ) + throws IOException { + requireNonNull(reader, "reader"); + + try { + return (ProbePlantModelTO) createUnmarshaller().unmarshal(reader); + } + catch (JAXBException | SAXException exc) { + throw new IOException("Exception unmarshalling data", exc); + } + } + + private static Unmarshaller createUnmarshaller() + throws JAXBException, + SAXException { + return createContext().createUnmarshaller(); + } + + private static JAXBContext createContext() + throws JAXBException { + return JAXBContext.newInstance(ProbePlantModelTO.class); + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/AllowedOperationTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/AllowedOperationTO.java new file mode 100644 index 0000000..b9fff6e --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/AllowedOperationTO.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v004; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +public class AllowedOperationTO + extends + PlantModelElementTO { + + /** + * Creates a new instance. + */ + public AllowedOperationTO() { + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/AllowedPeripheralOperationTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/AllowedPeripheralOperationTO.java new file mode 100644 index 0000000..10e0fc6 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/AllowedPeripheralOperationTO.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v004; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +public class AllowedPeripheralOperationTO + extends + PlantModelElementTO { + + /** + * Creates a new instance. + */ + public AllowedPeripheralOperationTO() { + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/BlockTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/BlockTO.java new file mode 100644 index 0000000..b30c009 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/BlockTO.java @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v004; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(propOrder = {"name", "type", "members", "properties", "blockLayout"}) +public class BlockTO + extends + PlantModelElementTO { + + private String type = "SINGLE_VEHICLE_ONLY"; + private List members = new ArrayList<>(); + private BlockLayout blockLayout = new BlockLayout(); + + /** + * Creates a new instance. + */ + public BlockTO() { + } + + @XmlAttribute(required = true) + public String getType() { + return type; + } + + public BlockTO setType( + @Nonnull + String type + ) { + requireNonNull(type, "type"); + this.type = type; + return this; + } + + @XmlElement(name = "member") + public List getMembers() { + return members; + } + + public BlockTO setMembers( + @Nonnull + List members + ) { + requireNonNull(members, "members"); + this.members = members; + return this; + } + + @XmlElement(required = true) + public BlockLayout getBlockLayout() { + return blockLayout; + } + + public BlockTO setBlockLayout( + @Nonnull + BlockLayout blockLayout + ) { + this.blockLayout = requireNonNull(blockLayout, "blockLayout"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + public static class BlockLayout { + + private String color = ""; + + /** + * Creates a new instance. + */ + public BlockLayout() { + } + + @XmlAttribute(required = true) + public String getColor() { + return color; + } + + public BlockLayout setColor( + @Nonnull + String color + ) { + this.color = requireNonNull(color, "color"); + return this; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/Comparators.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/Comparators.java new file mode 100644 index 0000000..7bd4efc --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/Comparators.java @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v004; + +import java.util.Comparator; + +/** + * Some comparator implementations for JAXB classes. + */ +public final class Comparators { + + /** + * Prevents instantiation. + */ + private Comparators() { + } + + /** + * Returns a comparator for ordering PlantModelElementTOs ascendingly by their names. + * + * @return A comparator for ordering PlantModelElementTOs ascendingly by their names. + */ + public static Comparator elementsByName() { + return Comparator.comparing(PlantModelElementTO::getName); + } + + /** + * Returns a comparator for ordering OutgoingPaths ascendingly by their names. + * + * @return A comparator for ordering OutgoingPaths ascendingly by their names. + */ + public static Comparator outgoingPathsByName() { + return Comparator.comparing(PointTO.OutgoingPath::getName); + } + + /** + * Returns a comparator for ordering Links ascendingly by their point names. + * + * @return A comparator for ordering Links ascendingly by their point names. + */ + public static Comparator linksByPointName() { + return Comparator.comparing(LocationTO.Link::getPoint); + } + + /** + * Returns a comparator for ordering Propertiess ascendingly by their names. + * + * @return A comparator for ordering Properties ascendingly by their names. + */ + public static Comparator propertiesByName() { + return Comparator.comparing(PropertyTO::getName); + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/CoupleTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/CoupleTO.java new file mode 100644 index 0000000..6b7ba1f --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/CoupleTO.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v004; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(propOrder = {"x", "y"}) +public class CoupleTO { + + private Long x; + private Long y; + + public CoupleTO() { + } + + @XmlAttribute(required = true) + public Long getX() { + return x; + } + + public CoupleTO setX( + @Nonnull + Long x + ) { + this.x = requireNonNull(x, "x"); + return this; + } + + @XmlAttribute(required = true) + public Long getY() { + return y; + } + + public CoupleTO setY( + @Nonnull + Long y + ) { + this.y = requireNonNull(y, "y"); + return this; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/LocationTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/LocationTO.java new file mode 100644 index 0000000..7a3066a --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/LocationTO.java @@ -0,0 +1,259 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v004; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType( + propOrder = {"name", "xPosition", "yPosition", "zPosition", "links", "locked", + "properties", "locationLayout"} +) +public class LocationTO + extends + PlantModelElementTO { + + private Long xPosition = 0L; + private Long yPosition = 0L; + private Long zPosition = 0L; + private String type = ""; + private List links = new ArrayList<>(); + private Boolean locked = false; + private LocationLayout locationLayout = new LocationLayout(); + + /** + * Creates a new instance. + */ + public LocationTO() { + } + + @XmlAttribute + public Long getxPosition() { + return xPosition; + } + + public LocationTO setxPosition( + @Nonnull + Long xPosition + ) { + requireNonNull(xPosition, "xPosition"); + this.xPosition = xPosition; + return this; + } + + @XmlAttribute + public Long getyPosition() { + return yPosition; + } + + public LocationTO setyPosition( + @Nonnull + Long yPosition + ) { + requireNonNull(yPosition, "yPosition"); + this.yPosition = yPosition; + return this; + } + + @XmlAttribute + public Long getzPosition() { + return zPosition; + } + + public LocationTO setzPosition( + @Nonnull + Long zPosition + ) { + requireNonNull(zPosition, "zPosition"); + this.zPosition = zPosition; + return this; + } + + @XmlAttribute + public String getType() { + return type; + } + + public LocationTO setType( + @Nonnull + String type + ) { + requireNonNull(type, "type"); + this.type = type; + return this; + } + + @XmlElement(name = "link", required = true) + public List getLinks() { + return links; + } + + public LocationTO setLinks( + @Nonnull + List links + ) { + requireNonNull(links, "links"); + this.links = links; + return this; + } + + @XmlAttribute(required = true) + public Boolean isLocked() { + return locked; + } + + public LocationTO setLocked(Boolean locked) { + this.locked = locked; + return this; + } + + @XmlElement(required = true) + public LocationLayout getLocationLayout() { + return locationLayout; + } + + public LocationTO setLocationLayout( + @Nonnull + LocationLayout locationLayout + ) { + this.locationLayout = requireNonNull(locationLayout, "locationLayout"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType(propOrder = {"point", "allowedOperations"}) + public static class Link { + + private String point = ""; + private List allowedOperations = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public Link() { + } + + @XmlAttribute(required = true) + public String getPoint() { + return point; + } + + public Link setPoint( + @Nonnull + String point + ) { + requireNonNull(point, "point"); + this.point = point; + return this; + } + + @XmlElement(name = "allowedOperation") + public List getAllowedOperations() { + return allowedOperations; + } + + public Link setAllowedOperations( + @Nonnull + List allowedOperations + ) { + requireNonNull(allowedOperations, "allowedOperations"); + this.allowedOperations = allowedOperations; + return this; + } + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType( + propOrder = {"xPosition", "yPosition", "xLabelOffset", "yLabelOffset", + "locationRepresentation", "layerId"} + ) + public static class LocationLayout { + + private Long xPosition = 0L; + private Long yPosition = 0L; + private Long xLabelOffset = 0L; + private Long yLabelOffset = 0L; + private String locationRepresentation = ""; + private Integer layerId = 0; + + /** + * Creates a new instance. + */ + public LocationLayout() { + } + + @XmlAttribute(required = true) + public Long getxPosition() { + return xPosition; + } + + public LocationLayout setxPosition(Long xPosition) { + this.xPosition = requireNonNull(xPosition, "xPosition"); + return this; + } + + @XmlAttribute(required = true) + public Long getyPosition() { + return yPosition; + } + + public LocationLayout setyPosition(Long yPosition) { + this.yPosition = requireNonNull(yPosition, "yPosition"); + return this; + } + + @XmlAttribute(required = true) + public Long getxLabelOffset() { + return xLabelOffset; + } + + public LocationLayout setxLabelOffset(Long xLabelOffset) { + this.xLabelOffset = requireNonNull(xLabelOffset, "xLabelOffset"); + return this; + } + + @XmlAttribute(required = true) + public Long getyLabelOffset() { + return yLabelOffset; + } + + public LocationLayout setyLabelOffset(Long yLabelOffset) { + this.yLabelOffset = requireNonNull(yLabelOffset, "yLabelOffset"); + return this; + } + + @XmlAttribute(required = true) + public String getLocationRepresentation() { + return locationRepresentation; + } + + public LocationLayout setLocationRepresentation(String locationRepresentation) { + this.locationRepresentation = requireNonNull( + locationRepresentation, + "locationRepresentation" + ); + return this; + } + + @XmlAttribute(required = true) + public Integer getLayerId() { + return layerId; + } + + public LocationLayout setLayerId(Integer layerId) { + this.layerId = requireNonNull(layerId, "layerId"); + return this; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/LocationTypeTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/LocationTypeTO.java new file mode 100644 index 0000000..c36a518 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/LocationTypeTO.java @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v004; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType( + propOrder = {"name", + "allowedOperations", + "allowedPeripheralOperations", + "properties", + "locationTypeLayout"} +) +public class LocationTypeTO + extends + PlantModelElementTO { + + private List allowedOperations = new ArrayList<>(); + private List allowedPeripheralOperations = new ArrayList<>(); + private LocationTypeLayout locationTypeLayout = new LocationTypeLayout(); + + /** + * Creates a new instance. + */ + public LocationTypeTO() { + } + + @XmlElement(name = "allowedOperation") + public List getAllowedOperations() { + return allowedOperations; + } + + public LocationTypeTO setAllowedOperations( + @Nonnull + List allowedOperations + ) { + this.allowedOperations = requireNonNull(allowedOperations, "allowedOperations"); + return this; + } + + @XmlElement(name = "allowedPeripheralOperation") + public List getAllowedPeripheralOperations() { + return allowedPeripheralOperations; + } + + public LocationTypeTO setAllowedPeripheralOperations( + List allowedPeripheralOperations + ) { + this.allowedPeripheralOperations = requireNonNull( + allowedPeripheralOperations, + "allowedPeripheralOperations" + ); + return this; + } + + @XmlElement(required = true) + public LocationTypeLayout getLocationTypeLayout() { + return locationTypeLayout; + } + + public LocationTypeTO setLocationTypeLayout( + @Nonnull + LocationTypeLayout locationTypeLayout + ) { + this.locationTypeLayout = requireNonNull(locationTypeLayout, "locationTypeLayout"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + public static class LocationTypeLayout { + + private String locationRepresentation = ""; + + /** + * Creates a new instance. + */ + public LocationTypeLayout() { + } + + @XmlAttribute(required = true) + public String getLocationRepresentation() { + return locationRepresentation; + } + + public LocationTypeLayout setLocationRepresentation( + @Nonnull + String locationRepresentation + ) { + this.locationRepresentation = requireNonNull( + locationRepresentation, + "locationRepresentation" + ); + return this; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/MemberTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/MemberTO.java new file mode 100644 index 0000000..0230844 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/MemberTO.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v004; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +public class MemberTO + extends + PlantModelElementTO { + + /** + * Creates a new instance. + */ + public MemberTO() { + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/PathTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/PathTO.java new file mode 100644 index 0000000..239c262 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/PathTO.java @@ -0,0 +1,265 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v004; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlSchemaType; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType( + propOrder = {"name", + "sourcePoint", + "destinationPoint", + "length", + "maxVelocity", + "maxReverseVelocity", + "peripheralOperations", + "locked", + "vehicleEnvelopes", + "properties", + "pathLayout"} +) +public class PathTO + extends + PlantModelElementTO { + + private String sourcePoint = ""; + private String destinationPoint = ""; + private Long length = 0L; + private Long maxVelocity = 0L; + private Long maxReverseVelocity = 0L; + private List peripheralOperations = new ArrayList<>(); + private Boolean locked = false; + private List vehicleEnvelopes = new ArrayList<>(); + private PathLayout pathLayout = new PathLayout(); + + /** + * Creates a new instance. + */ + public PathTO() { + } + + @XmlAttribute(required = true) + public String getSourcePoint() { + return sourcePoint; + } + + public PathTO setSourcePoint( + @Nonnull + String sourcePoint + ) { + requireNonNull(sourcePoint, "sourcePoint"); + this.sourcePoint = sourcePoint; + return this; + } + + @XmlAttribute(required = true) + public String getDestinationPoint() { + return destinationPoint; + } + + public PathTO setDestinationPoint( + @Nonnull + String destinationPoint + ) { + requireNonNull(destinationPoint, "destinationPoint"); + this.destinationPoint = destinationPoint; + return this; + } + + @XmlAttribute + @XmlSchemaType(name = "unsignedInt") + public Long getLength() { + return length; + } + + public PathTO setLength( + @Nonnull + Long length + ) { + requireNonNull(length, "length"); + this.length = length; + return this; + } + + @XmlAttribute(required = true) + @XmlSchemaType(name = "unsignedInt") + public Long getMaxVelocity() { + return maxVelocity; + } + + public PathTO setMaxVelocity( + @Nonnull + Long maxVelocity + ) { + requireNonNull(maxVelocity, "maxVelocity"); + this.maxVelocity = maxVelocity; + return this; + } + + @XmlAttribute(required = true) + @XmlSchemaType(name = "unsignedInt") + public Long getMaxReverseVelocity() { + return maxReverseVelocity; + } + + public PathTO setMaxReverseVelocity( + @Nonnull + Long maxReverseVelocity + ) { + requireNonNull(maxReverseVelocity, "maxReverseVelocity"); + this.maxReverseVelocity = maxReverseVelocity; + return this; + } + + @XmlElement(name = "peripheralOperation") + public List getPeripheralOperations() { + return peripheralOperations; + } + + public PathTO setPeripheralOperations(List peripheralOperations) { + this.peripheralOperations = requireNonNull(peripheralOperations, "peripheralOperations"); + return this; + } + + @XmlAttribute(required = true) + public Boolean isLocked() { + return locked; + } + + public PathTO setLocked(Boolean locked) { + this.locked = locked; + return this; + } + + @XmlElement(name = "vehicleEnvelope") + public List getVehicleEnvelopes() { + return vehicleEnvelopes; + } + + public PathTO setVehicleEnvelopes( + @Nonnull + List vehicleEnvelopes + ) { + this.vehicleEnvelopes = requireNonNull(vehicleEnvelopes, "vehicleEnvelopes"); + return this; + } + + @XmlElement(required = true) + public PathLayout getPathLayout() { + return pathLayout; + } + + public PathTO setPathLayout( + @Nonnull + PathLayout pathLayout + ) { + this.pathLayout = requireNonNull(pathLayout, "pathLayout"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType(propOrder = {"connectionType", "layerId", "controlPoints"}) + public static class PathLayout { + + private String connectionType = ""; + private Integer layerId = 0; + private List controlPoints = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public PathLayout() { + } + + @XmlAttribute(required = true) + public String getConnectionType() { + return connectionType; + } + + public PathLayout setConnectionType( + @Nonnull + String connectionType + ) { + this.connectionType = requireNonNull(connectionType, "connectionType"); + return this; + } + + @XmlAttribute(required = true) + public Integer getLayerId() { + return layerId; + } + + public PathLayout setLayerId( + @Nonnull + Integer layerId + ) { + this.layerId = requireNonNull(layerId, "layerId"); + return this; + } + + @XmlElement(name = "controlPoint") + public List getControlPoints() { + return controlPoints; + } + + public PathLayout setControlPoints( + @Nonnull + List controlPoints + ) { + this.controlPoints = requireNonNull(controlPoints, "controlPoints"); + return this; + } + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType(propOrder = {"x", "y"}) + public static class ControlPoint { + + private Long x = 0L; + private Long y = 0L; + + /** + * Creates a new instance. + */ + public ControlPoint() { + } + + @XmlAttribute(required = true) + public Long getX() { + return x; + } + + public ControlPoint setX( + @Nonnull + Long x + ) { + this.x = requireNonNull(x, "x"); + return this; + } + + @XmlAttribute(required = true) + public Long getY() { + return y; + } + + public ControlPoint setY( + @Nonnull + Long y + ) { + this.y = requireNonNull(y, "y"); + return this; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/PeripheralOperationTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/PeripheralOperationTO.java new file mode 100644 index 0000000..e5e46d6 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/PeripheralOperationTO.java @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v004; + +import static java.util.Objects.requireNonNull; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +public class PeripheralOperationTO + extends + PlantModelElementTO { + + private String locationName = ""; + private String executionTrigger = ""; + private boolean completionRequired; + + /** + * Creates a new instance. + */ + public PeripheralOperationTO() { + } + + @XmlAttribute(required = true) + public String getLocationName() { + return locationName; + } + + public PeripheralOperationTO setLocationName(String locationName) { + this.locationName = requireNonNull(locationName, "locationName"); + return this; + } + + @XmlAttribute(required = true) + public String getExecutionTrigger() { + return executionTrigger; + } + + public PeripheralOperationTO setExecutionTrigger(String executionTrigger) { + this.executionTrigger = requireNonNull(executionTrigger, "executionTrigger"); + return this; + } + + @XmlAttribute(required = true) + public boolean isCompletionRequired() { + return completionRequired; + } + + public PeripheralOperationTO setCompletionRequired(boolean completionRequired) { + this.completionRequired = completionRequired; + return this; + } + +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/PlantModelElementTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/PlantModelElementTO.java new file mode 100644 index 0000000..da45be2 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/PlantModelElementTO.java @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v004; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlTransient; + +/** + */ +@XmlTransient +@XmlAccessorType(XmlAccessType.PROPERTY) +public class PlantModelElementTO { + + private String name = ""; + private List properties = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public PlantModelElementTO() { + } + + @XmlAttribute(required = true) + public String getName() { + return name; + } + + public PlantModelElementTO setName( + @Nonnull + String name + ) { + requireNonNull(name, "name"); + this.name = name; + return this; + } + + @XmlElement(name = "property") + public List getProperties() { + return properties; + } + + public PlantModelElementTO setProperties( + @Nonnull + List properties + ) { + requireNonNull(properties, "properties"); + this.properties = properties; + return this; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/PointTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/PointTO.java new file mode 100644 index 0000000..68bae9b --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/PointTO.java @@ -0,0 +1,259 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v004; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType( + propOrder = {"name", "xPosition", "yPosition", "zPosition", "vehicleOrientationAngle", + "type", "vehicleEnvelopes", "outgoingPaths", "properties", "pointLayout"} +) +public class PointTO + extends + PlantModelElementTO { + + private Long xPosition = 0L; + private Long yPosition = 0L; + private Long zPosition = 0L; + private Float vehicleOrientationAngle = 0.0F; + private String type = "HALT_POSITION"; + private List vehicleEnvelopes = new ArrayList<>(); + private List outgoingPaths = new ArrayList<>(); + private PointLayout pointLayout = new PointLayout(); + + /** + * Creates a new instance. + */ + public PointTO() { + } + + @XmlAttribute(required = true) + public Long getxPosition() { + return xPosition; + } + + public PointTO setxPosition( + @Nonnull + Long xPosition + ) { + requireNonNull(xPosition, "xPosition"); + this.xPosition = xPosition; + return this; + } + + @XmlAttribute(required = true) + public Long getyPosition() { + return yPosition; + } + + public PointTO setyPosition( + @Nonnull + Long yPosition + ) { + requireNonNull(yPosition, "yPosition"); + this.yPosition = yPosition; + return this; + } + + @XmlAttribute + public Long getzPosition() { + return zPosition; + } + + public PointTO setzPosition( + @Nonnull + Long zPosition + ) { + requireNonNull(zPosition, "zPosition"); + this.zPosition = zPosition; + return this; + } + + @XmlAttribute + public Float getVehicleOrientationAngle() { + return vehicleOrientationAngle; + } + + public PointTO setVehicleOrientationAngle( + @Nonnull + Float vehicleOrientationAngle + ) { + requireNonNull(vehicleOrientationAngle, "vehicleOrientationAngle"); + this.vehicleOrientationAngle = vehicleOrientationAngle; + return this; + } + + @XmlAttribute(required = true) + public String getType() { + return type; + } + + public PointTO setType( + @Nonnull + String type + ) { + requireNonNull(type, "type"); + this.type = type; + return this; + } + + @XmlElement(name = "outgoingPath") + public List getOutgoingPaths() { + return outgoingPaths; + } + + public PointTO setOutgoingPaths( + @Nonnull + List outgoingPath + ) { + requireNonNull(outgoingPath, "outgoingPath"); + this.outgoingPaths = outgoingPath; + return this; + } + + @XmlElement(name = "vehicleEnvelope") + public List getVehicleEnvelopes() { + return vehicleEnvelopes; + } + + public PointTO setVehicleEnvelopes( + @Nonnull + List vehicleEnvelopes + ) { + this.vehicleEnvelopes = requireNonNull(vehicleEnvelopes, "vehicleEnvelopes"); + return this; + } + + @XmlElement(required = true) + public PointLayout getPointLayout() { + return pointLayout; + } + + public PointTO setPointLayout( + @Nonnull + PointLayout pointLayout + ) { + this.pointLayout = requireNonNull(pointLayout, "pointLayout"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + public static class OutgoingPath { + + private String name = ""; + + /** + * Creates a new instance. + */ + public OutgoingPath() { + } + + @XmlAttribute(required = true) + public String getName() { + return name; + } + + public OutgoingPath setName( + @Nonnull + String name + ) { + requireNonNull(name, "name"); + this.name = name; + return this; + } + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType(propOrder = {"xPosition", "yPosition", "xLabelOffset", "yLabelOffset", "layerId"}) + public static class PointLayout { + + private Long xPosition = 0L; + private Long yPosition = 0L; + private Long xLabelOffset = 0L; + private Long yLabelOffset = 0L; + private Integer layerId = 0; + + /** + * Creates a new instance. + */ + public PointLayout() { + } + + @XmlAttribute(required = true) + public Long getxPosition() { + return xPosition; + } + + public PointLayout setxPosition( + @Nonnull + Long xPosition + ) { + this.xPosition = requireNonNull(xPosition, "xPosition"); + return this; + } + + @XmlAttribute(required = true) + public Long getyPosition() { + return yPosition; + } + + public PointLayout setyPosition( + @Nonnull + Long yPosition + ) { + this.yPosition = requireNonNull(yPosition, "yPosition"); + return this; + } + + @XmlAttribute(required = true) + public Long getxLabelOffset() { + return xLabelOffset; + } + + public PointLayout setxLabelOffset( + @Nonnull + Long xLabelOffset + ) { + this.xLabelOffset = requireNonNull(xLabelOffset, "xLabelOffset"); + return this; + } + + @XmlAttribute(required = true) + public Long getyLabelOffset() { + return yLabelOffset; + } + + public PointLayout setyLabelOffset( + @Nonnull + Long yLabelOffset + ) { + this.yLabelOffset = requireNonNull(yLabelOffset, "yLabelOffset"); + return this; + } + + @XmlAttribute(required = true) + public Integer getLayerId() { + return layerId; + } + + public PointLayout setLayerId( + @Nonnull + Integer layerId + ) { + this.layerId = requireNonNull(layerId, "layerId"); + return this; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/PropertyTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/PropertyTO.java new file mode 100644 index 0000000..39aa595 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/PropertyTO.java @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v004; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(propOrder = {"name", "value"}) +public class PropertyTO { + + private String name = ""; + private String value = ""; + + /** + * Creates a new instance. + */ + public PropertyTO() { + } + + @XmlAttribute(required = true) + public String getName() { + return name; + } + + public PropertyTO setName( + @Nonnull + String name + ) { + this.name = requireNonNull(name, "name"); + return this; + } + + @XmlAttribute(required = true) + public String getValue() { + return value; + } + + public PropertyTO setValue( + @Nonnull + String value + ) { + requireNonNull(value, "value"); + this.value = value; + return this; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/V004ModelParser.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/V004ModelParser.java new file mode 100644 index 0000000..3ba191c --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/V004ModelParser.java @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v004; + +import java.io.IOException; +import java.io.Reader; +import java.util.Objects; + +/** + * The parser for V004 models. + */ +public class V004ModelParser { + + /** + * Creates a new instance. + */ + public V004ModelParser() { + } + + /** + * Reads a model with the given reader and parses it to a {@link V004PlantModelTO} instance. + * + * @param reader The reader to use. + * @param modelVersion The model version. + * @return The parsed {@link V004PlantModelTO}. + * @throws IOException If there was an error reading the model. + */ + public V004PlantModelTO readRaw(Reader reader, String modelVersion) + throws IOException { + if (Objects.equals(modelVersion, V004PlantModelTO.VERSION_STRING)) { + return V004PlantModelTO.fromXml(reader); + } + else { + throw new IllegalArgumentException( + String.format("There is no parser for a model file with version: %s.", modelVersion) + ); + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/V004PlantModelTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/V004PlantModelTO.java new file mode 100644 index 0000000..d0a7d57 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/V004PlantModelTO.java @@ -0,0 +1,260 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v004; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import javax.xml.XMLConstants; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import org.opentcs.util.persistence.BasePlantModelTO; +import org.xml.sax.SAXException; + +/** + */ +@XmlRootElement(name = "model") +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType( + propOrder = {"version", "name", "points", "paths", "vehicles", "locationTypes", + "locations", "blocks", "visualLayout", "properties"} +) +public class V004PlantModelTO + extends + BasePlantModelTO { + + /** + * This plant model implementation's version string. + */ + public static final String VERSION_STRING = "0.0.4"; + + private String name = ""; + private List points = new ArrayList<>(); + private List paths = new ArrayList<>(); + private List vehicles = new ArrayList<>(); + private List locationTypes = new ArrayList<>(); + private List locations = new ArrayList<>(); + private List blocks = new ArrayList<>(); + private VisualLayoutTO visualLayout = new VisualLayoutTO(); + private List properties = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public V004PlantModelTO() { + } + + @XmlAttribute(required = true) + public String getName() { + return name; + } + + public V004PlantModelTO setName( + @Nonnull + String name + ) { + requireNonNull(name, "name"); + this.name = name; + return this; + } + + @XmlElement(name = "point") + public List getPoints() { + return points; + } + + public V004PlantModelTO setPoints( + @Nonnull + List points + ) { + requireNonNull(points, "points"); + this.points = points; + return this; + } + + @XmlElement(name = "path") + public List getPaths() { + return paths; + } + + public V004PlantModelTO setPaths( + @Nonnull + List paths + ) { + requireNonNull(paths, "paths"); + this.paths = paths; + return this; + } + + @XmlElement(name = "vehicle") + public List getVehicles() { + return vehicles; + } + + public V004PlantModelTO setVehicles( + @Nonnull + List vehicles + ) { + requireNonNull(vehicles, "vehicles"); + this.vehicles = vehicles; + return this; + } + + @XmlElement(name = "locationType") + public List getLocationTypes() { + return locationTypes; + } + + public V004PlantModelTO setLocationTypes( + @Nonnull + List locationTypes + ) { + requireNonNull(locationTypes, "locationTypes"); + this.locationTypes = locationTypes; + return this; + } + + @XmlElement(name = "location") + public List getLocations() { + return locations; + } + + public V004PlantModelTO setLocations( + @Nonnull + List locations + ) { + requireNonNull(locations, "locations"); + this.locations = locations; + return this; + } + + @XmlElement(name = "block") + public List getBlocks() { + return blocks; + } + + public V004PlantModelTO setBlocks( + @Nonnull + List blocks + ) { + requireNonNull(blocks, "blocks"); + this.blocks = blocks; + return this; + } + + @XmlElement + public VisualLayoutTO getVisualLayout() { + return visualLayout; + } + + public V004PlantModelTO setVisualLayout( + @Nonnull + VisualLayoutTO visualLayout + ) { + this.visualLayout = requireNonNull(visualLayout, "visualLayout"); + return this; + } + + @XmlElement(name = "property") + public List getProperties() { + return properties; + } + + public V004PlantModelTO setProperties( + @Nonnull + List properties + ) { + requireNonNull(properties, "properties"); + this.properties = properties; + return this; + } + + /** + * Marshals this instance to its XML representation and writes it to the given writer. + * + * @param writer The writer to write this instance's XML representation to. + * @throws IOException If there was a problem marshalling this instance. + */ + public void toXml( + @Nonnull + Writer writer + ) + throws IOException { + requireNonNull(writer, "writer"); + + try { + createMarshaller().marshal(this, writer); + } + catch (JAXBException | SAXException exc) { + throw new IOException("Exception marshalling data", exc); + } + } + + /** + * Unmarshals an instance of this class from the given XML representation. + * + * @param reader Provides the XML representation to parse to an instance. + * @return The instance unmarshalled from the given reader. + * @throws IOException If there was a problem unmarshalling the given string. + */ + public static V004PlantModelTO fromXml( + @Nonnull + Reader reader + ) + throws IOException { + requireNonNull(reader, "reader"); + + try { + return (V004PlantModelTO) createUnmarshaller().unmarshal(reader); + } + catch (JAXBException | SAXException exc) { + throw new IOException("Exception unmarshalling data", exc); + } + } + + private static Marshaller createMarshaller() + throws JAXBException, + SAXException { + Marshaller marshaller = createContext().createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + marshaller.setSchema(createSchema()); + return marshaller; + } + + private static Unmarshaller createUnmarshaller() + throws JAXBException, + SAXException { + Unmarshaller unmarshaller = createContext().createUnmarshaller(); + unmarshaller.setSchema(createSchema()); + return unmarshaller; + } + + private static JAXBContext createContext() + throws JAXBException { + return JAXBContext.newInstance(V004PlantModelTO.class); + } + + private static Schema createSchema() + throws SAXException { + URL schemaUrl + = V004PlantModelTO.class.getResource("/org/opentcs/util/persistence/model-0.0.4.xsd"); + SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + return schemaFactory.newSchema(schemaUrl); + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/VehicleEnvelopeTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/VehicleEnvelopeTO.java new file mode 100644 index 0000000..ff68f6a --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/VehicleEnvelopeTO.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v004; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(propOrder = {"key", "vertices"}) +public class VehicleEnvelopeTO { + + private String key; + private List vertices; + + public VehicleEnvelopeTO() { + } + + public String getKey() { + return key; + } + + @XmlAttribute + public VehicleEnvelopeTO setKey( + @Nonnull + String key + ) { + this.key = requireNonNull(key, "key"); + return this; + } + + @XmlElement(name = "vertex") + public List getVertices() { + return vertices; + } + + public VehicleEnvelopeTO setVertices( + @Nonnull + List vertices + ) { + this.vertices = requireNonNull(vertices, "vertices"); + return this; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/VehicleTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/VehicleTO.java new file mode 100644 index 0000000..21a4512 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/VehicleTO.java @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v004; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlSchemaType; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType( + propOrder = {"name", "length", "energyLevelCritical", "energyLevelGood", + "energyLevelFullyRecharged", "energyLevelSufficientlyRecharged", + "maxVelocity", "maxReverseVelocity", "properties", "vehicleLayout"} +) +public class VehicleTO + extends + PlantModelElementTO { + + //max velocity in mm/s. + private int maxVelocity; + //max rev velocity in mm/s. + private int maxReverseVelocity; + private Long length = 0L; + private Long energyLevelCritical = 0L; + private Long energyLevelGood = 0L; + private Long energyLevelFullyRecharged = 0L; + private Long energyLevelSufficientlyRecharged = 0L; + private String envelopeKey; + private VehicleLayout vehicleLayout = new VehicleLayout(); + + /** + * Creates a new instance. + */ + public VehicleTO() { + } + + @XmlAttribute + @XmlSchemaType(name = "unsignedInt") + public Long getLength() { + return length; + } + + public VehicleTO setLength( + @Nonnull + Long length + ) { + requireNonNull(length, "length"); + this.length = length; + return this; + } + + @XmlAttribute + @XmlSchemaType(name = "unsignedInt") + public Long getEnergyLevelCritical() { + return energyLevelCritical; + } + + public VehicleTO setEnergyLevelCritical( + @Nonnull + Long energyLevelCritical + ) { + requireNonNull(energyLevelCritical, "energyLevelCritical"); + this.energyLevelCritical = energyLevelCritical; + return this; + } + + @XmlAttribute + @XmlSchemaType(name = "unsignedInt") + public Long getEnergyLevelGood() { + return energyLevelGood; + } + + public VehicleTO setEnergyLevelGood( + @Nonnull + Long energyLevelGood + ) { + requireNonNull(energyLevelGood, "energyLevelGood"); + this.energyLevelGood = energyLevelGood; + return this; + } + + @XmlAttribute + @XmlSchemaType(name = "unsignedInt") + public Long getEnergyLevelFullyRecharged() { + return energyLevelFullyRecharged; + } + + public VehicleTO setEnergyLevelFullyRecharged( + @Nonnull + Long energyLevelFullyRecharged + ) { + requireNonNull(energyLevelFullyRecharged, "energyLevelFullyRecharged"); + this.energyLevelFullyRecharged = energyLevelFullyRecharged; + return this; + } + + @XmlAttribute + @XmlSchemaType(name = "unsignedInt") + public Long getEnergyLevelSufficientlyRecharged() { + return energyLevelSufficientlyRecharged; + } + + public VehicleTO setEnergyLevelSufficientlyRecharged( + @Nonnull + Long energyLevelSufficientlyRecharged + ) { + requireNonNull(energyLevelSufficientlyRecharged, "energyLevelSufficientlyRecharged"); + this.energyLevelSufficientlyRecharged = energyLevelSufficientlyRecharged; + return this; + } + + @XmlAttribute + @XmlSchemaType(name = "unsignedInt") + public int getMaxVelocity() { + return maxVelocity; + } + + public VehicleTO setMaxVelocity( + @Nonnull + int maxVelocity + ) { + this.maxVelocity = maxVelocity; + return this; + } + + @XmlAttribute + @XmlSchemaType(name = "unsignedInt") + public int getMaxReverseVelocity() { + return maxReverseVelocity; + } + + public VehicleTO setMaxReverseVelocity( + @Nonnull + int maxReverseVelocity + ) { + this.maxReverseVelocity = maxReverseVelocity; + return this; + } + + @XmlAttribute + @Nullable + public String getEnvelopeKey() { + return envelopeKey; + } + + public VehicleTO setEnvelopeKey( + @Nullable + String envelopeKey + ) { + this.envelopeKey = envelopeKey; + return this; + } + + @XmlElement(required = true) + public VehicleLayout getVehicleLayout() { + return vehicleLayout; + } + + public VehicleTO setVehicleLayout( + @Nonnull + VehicleLayout vehicleLayout + ) { + this.vehicleLayout = requireNonNull(vehicleLayout, "vehicleLayout"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + public static class VehicleLayout { + + private String color = ""; + + /** + * Creates a new instance. + */ + public VehicleLayout() { + } + + @XmlAttribute(required = true) + public String getColor() { + return color; + } + + public VehicleLayout setColor( + @Nonnull + String color + ) { + this.color = requireNonNull(color, "color"); + return this; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/VisualLayoutTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/VisualLayoutTO.java new file mode 100644 index 0000000..ee42864 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v004/VisualLayoutTO.java @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v004; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(propOrder = {"name", "scaleX", "scaleY", "layers", "layerGroups", "properties"}) +public class VisualLayoutTO + extends + PlantModelElementTO { + + private Float scaleX = 0.0F; + private Float scaleY = 0.0F; + private List layers = new ArrayList<>(); + private List layerGroups = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public VisualLayoutTO() { + } + + @XmlAttribute(required = true) + public Float getScaleX() { + return scaleX; + } + + public VisualLayoutTO setScaleX( + @Nonnull + Float scaleX + ) { + requireNonNull(scaleX, "scaleX"); + this.scaleX = scaleX; + return this; + } + + @XmlAttribute(required = true) + public Float getScaleY() { + return scaleY; + } + + public VisualLayoutTO setScaleY( + @Nonnull + Float scaleY + ) { + requireNonNull(scaleY, "scaleY"); + this.scaleY = scaleY; + return this; + } + + @XmlElement(name = "layer") + public List getLayers() { + return layers; + } + + public VisualLayoutTO setLayers( + @Nonnull + List layers + ) { + this.layers = requireNonNull(layers, "layers"); + return this; + } + + @XmlElement(name = "layerGroup") + public List getLayerGroups() { + return layerGroups; + } + + public VisualLayoutTO setLayerGroups( + @Nonnull + List layerGroups + ) { + this.layerGroups = requireNonNull(layerGroups, "layerGroups"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType(propOrder = {"id", "ordinal", "visible", "name", "groupId"}) + public static class Layer { + + private Integer id = 0; + private Integer ordinal = 0; + private Boolean visible = true; + private String name = ""; + private Integer groupId = 0; + + /** + * Creates a new instance. + */ + public Layer() { + } + + @XmlAttribute(required = true) + public Integer getId() { + return id; + } + + public Layer setId(Integer id) { + this.id = requireNonNull(id, "id"); + return this; + } + + @XmlAttribute(required = true) + public Integer getOrdinal() { + return ordinal; + } + + public Layer setOrdinal(Integer ordinal) { + this.ordinal = requireNonNull(ordinal, "ordinal"); + return this; + } + + @XmlAttribute(required = true) + public Boolean isVisible() { + return visible; + } + + public Layer setVisible(Boolean visible) { + this.visible = requireNonNull(visible, "visible"); + return this; + } + + @XmlAttribute(required = true) + public String getName() { + return name; + } + + public Layer setName(String name) { + this.name = requireNonNull(name, "name"); + return this; + } + + @XmlAttribute(required = true) + public Integer getGroupId() { + return groupId; + } + + public Layer setGroupId(Integer groupId) { + this.groupId = requireNonNull(groupId, "groupId"); + return this; + } + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType(propOrder = {"id", "name", "visible"}) + public static class LayerGroup { + + private Integer id = 0; + private String name = ""; + private Boolean visible = true; + + /** + * Creates a new instance. + */ + public LayerGroup() { + } + + @XmlAttribute(required = true) + public Integer getId() { + return id; + } + + public LayerGroup setId(Integer id) { + this.id = requireNonNull(id, "id"); + return this; + } + + @XmlAttribute(required = true) + public String getName() { + return name; + } + + public LayerGroup setName(String name) { + this.name = requireNonNull(name, "name"); + return this; + } + + @XmlAttribute(required = true) + public Boolean isVisible() { + return visible; + } + + public LayerGroup setVisible(Boolean visible) { + this.visible = requireNonNull(visible, "visible"); + return this; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/AllowedOperationTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/AllowedOperationTO.java new file mode 100644 index 0000000..78cd6f7 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/AllowedOperationTO.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v005; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +public class AllowedOperationTO + extends + PlantModelElementTO { + + /** + * Creates a new instance. + */ + public AllowedOperationTO() { + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/AllowedPeripheralOperationTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/AllowedPeripheralOperationTO.java new file mode 100644 index 0000000..c57bf3d --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/AllowedPeripheralOperationTO.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v005; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +public class AllowedPeripheralOperationTO + extends + PlantModelElementTO { + + /** + * Creates a new instance. + */ + public AllowedPeripheralOperationTO() { + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/BlockTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/BlockTO.java new file mode 100644 index 0000000..db5fcaa --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/BlockTO.java @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v005; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(propOrder = {"name", "type", "members", "properties", "blockLayout"}) +public class BlockTO + extends + PlantModelElementTO { + + private String type = "SINGLE_VEHICLE_ONLY"; + private List members = new ArrayList<>(); + private BlockLayout blockLayout = new BlockLayout(); + + /** + * Creates a new instance. + */ + public BlockTO() { + } + + @XmlAttribute(required = true) + public String getType() { + return type; + } + + public BlockTO setType( + @Nonnull + String type + ) { + requireNonNull(type, "type"); + this.type = type; + return this; + } + + @XmlElement(name = "member") + public List getMembers() { + return members; + } + + public BlockTO setMembers( + @Nonnull + List members + ) { + requireNonNull(members, "members"); + this.members = members; + return this; + } + + @XmlElement(required = true) + public BlockLayout getBlockLayout() { + return blockLayout; + } + + public BlockTO setBlockLayout( + @Nonnull + BlockLayout blockLayout + ) { + this.blockLayout = requireNonNull(blockLayout, "blockLayout"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + public static class BlockLayout { + + private String color = ""; + + /** + * Creates a new instance. + */ + public BlockLayout() { + } + + @XmlAttribute(required = true) + public String getColor() { + return color; + } + + public BlockLayout setColor( + @Nonnull + String color + ) { + this.color = requireNonNull(color, "color"); + return this; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/Comparators.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/Comparators.java new file mode 100644 index 0000000..8bc088d --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/Comparators.java @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v005; + +import java.util.Comparator; + +/** + * Some comparator implementations for JAXB classes. + */ +public final class Comparators { + + /** + * Prevents instantiation. + */ + private Comparators() { + } + + /** + * Returns a comparator for ordering PlantModelElementTOs ascendingly by their names. + * + * @return A comparator for ordering PlantModelElementTOs ascendingly by their names. + */ + public static Comparator elementsByName() { + return Comparator.comparing(PlantModelElementTO::getName); + } + + /** + * Returns a comparator for ordering OutgoingPaths ascendingly by their names. + * + * @return A comparator for ordering OutgoingPaths ascendingly by their names. + */ + public static Comparator outgoingPathsByName() { + return Comparator.comparing(PointTO.OutgoingPath::getName); + } + + /** + * Returns a comparator for ordering Links ascendingly by their point names. + * + * @return A comparator for ordering Links ascendingly by their point names. + */ + public static Comparator linksByPointName() { + return Comparator.comparing(LocationTO.Link::getPoint); + } + + /** + * Returns a comparator for ordering Propertiess ascendingly by their names. + * + * @return A comparator for ordering Properties ascendingly by their names. + */ + public static Comparator propertiesByName() { + return Comparator.comparing(PropertyTO::getName); + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/CoupleTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/CoupleTO.java new file mode 100644 index 0000000..73395b4 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/CoupleTO.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v005; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(propOrder = {"x", "y"}) +public class CoupleTO { + + private Long x; + private Long y; + + public CoupleTO() { + } + + @XmlAttribute(required = true) + public Long getX() { + return x; + } + + public CoupleTO setX( + @Nonnull + Long x + ) { + this.x = requireNonNull(x, "x"); + return this; + } + + @XmlAttribute(required = true) + public Long getY() { + return y; + } + + public CoupleTO setY( + @Nonnull + Long y + ) { + this.y = requireNonNull(y, "y"); + return this; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/LocationTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/LocationTO.java new file mode 100644 index 0000000..01f89c4 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/LocationTO.java @@ -0,0 +1,259 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v005; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType( + propOrder = {"name", "xPosition", "yPosition", "zPosition", "links", "locked", + "properties", "locationLayout"} +) +public class LocationTO + extends + PlantModelElementTO { + + private Long xPosition = 0L; + private Long yPosition = 0L; + private Long zPosition = 0L; + private String type = ""; + private List links = new ArrayList<>(); + private Boolean locked = false; + private LocationLayout locationLayout = new LocationLayout(); + + /** + * Creates a new instance. + */ + public LocationTO() { + } + + @XmlAttribute + public Long getxPosition() { + return xPosition; + } + + public LocationTO setxPosition( + @Nonnull + Long xPosition + ) { + requireNonNull(xPosition, "xPosition"); + this.xPosition = xPosition; + return this; + } + + @XmlAttribute + public Long getyPosition() { + return yPosition; + } + + public LocationTO setyPosition( + @Nonnull + Long yPosition + ) { + requireNonNull(yPosition, "yPosition"); + this.yPosition = yPosition; + return this; + } + + @XmlAttribute + public Long getzPosition() { + return zPosition; + } + + public LocationTO setzPosition( + @Nonnull + Long zPosition + ) { + requireNonNull(zPosition, "zPosition"); + this.zPosition = zPosition; + return this; + } + + @XmlAttribute + public String getType() { + return type; + } + + public LocationTO setType( + @Nonnull + String type + ) { + requireNonNull(type, "type"); + this.type = type; + return this; + } + + @XmlElement(name = "link", required = true) + public List getLinks() { + return links; + } + + public LocationTO setLinks( + @Nonnull + List links + ) { + requireNonNull(links, "links"); + this.links = links; + return this; + } + + @XmlAttribute(required = true) + public Boolean isLocked() { + return locked; + } + + public LocationTO setLocked(Boolean locked) { + this.locked = locked; + return this; + } + + @XmlElement(required = true) + public LocationLayout getLocationLayout() { + return locationLayout; + } + + public LocationTO setLocationLayout( + @Nonnull + LocationLayout locationLayout + ) { + this.locationLayout = requireNonNull(locationLayout, "locationLayout"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType(propOrder = {"point", "allowedOperations"}) + public static class Link { + + private String point = ""; + private List allowedOperations = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public Link() { + } + + @XmlAttribute(required = true) + public String getPoint() { + return point; + } + + public Link setPoint( + @Nonnull + String point + ) { + requireNonNull(point, "point"); + this.point = point; + return this; + } + + @XmlElement(name = "allowedOperation") + public List getAllowedOperations() { + return allowedOperations; + } + + public Link setAllowedOperations( + @Nonnull + List allowedOperations + ) { + requireNonNull(allowedOperations, "allowedOperations"); + this.allowedOperations = allowedOperations; + return this; + } + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType( + propOrder = {"xPosition", "yPosition", "xLabelOffset", "yLabelOffset", + "locationRepresentation", "layerId"} + ) + public static class LocationLayout { + + private Long xPosition = 0L; + private Long yPosition = 0L; + private Long xLabelOffset = 0L; + private Long yLabelOffset = 0L; + private String locationRepresentation = ""; + private Integer layerId = 0; + + /** + * Creates a new instance. + */ + public LocationLayout() { + } + + @XmlAttribute(required = true) + public Long getxPosition() { + return xPosition; + } + + public LocationLayout setxPosition(Long xPosition) { + this.xPosition = requireNonNull(xPosition, "xPosition"); + return this; + } + + @XmlAttribute(required = true) + public Long getyPosition() { + return yPosition; + } + + public LocationLayout setyPosition(Long yPosition) { + this.yPosition = requireNonNull(yPosition, "yPosition"); + return this; + } + + @XmlAttribute(required = true) + public Long getxLabelOffset() { + return xLabelOffset; + } + + public LocationLayout setxLabelOffset(Long xLabelOffset) { + this.xLabelOffset = requireNonNull(xLabelOffset, "xLabelOffset"); + return this; + } + + @XmlAttribute(required = true) + public Long getyLabelOffset() { + return yLabelOffset; + } + + public LocationLayout setyLabelOffset(Long yLabelOffset) { + this.yLabelOffset = requireNonNull(yLabelOffset, "yLabelOffset"); + return this; + } + + @XmlAttribute(required = true) + public String getLocationRepresentation() { + return locationRepresentation; + } + + public LocationLayout setLocationRepresentation(String locationRepresentation) { + this.locationRepresentation = requireNonNull( + locationRepresentation, + "locationRepresentation" + ); + return this; + } + + @XmlAttribute(required = true) + public Integer getLayerId() { + return layerId; + } + + public LocationLayout setLayerId(Integer layerId) { + this.layerId = requireNonNull(layerId, "layerId"); + return this; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/LocationTypeTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/LocationTypeTO.java new file mode 100644 index 0000000..6af71ae --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/LocationTypeTO.java @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v005; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType( + propOrder = {"name", + "allowedOperations", + "allowedPeripheralOperations", + "properties", + "locationTypeLayout"} +) +public class LocationTypeTO + extends + PlantModelElementTO { + + private List allowedOperations = new ArrayList<>(); + private List allowedPeripheralOperations = new ArrayList<>(); + private LocationTypeLayout locationTypeLayout = new LocationTypeLayout(); + + /** + * Creates a new instance. + */ + public LocationTypeTO() { + } + + @XmlElement(name = "allowedOperation") + public List getAllowedOperations() { + return allowedOperations; + } + + public LocationTypeTO setAllowedOperations( + @Nonnull + List allowedOperations + ) { + this.allowedOperations = requireNonNull(allowedOperations, "allowedOperations"); + return this; + } + + @XmlElement(name = "allowedPeripheralOperation") + public List getAllowedPeripheralOperations() { + return allowedPeripheralOperations; + } + + public LocationTypeTO setAllowedPeripheralOperations( + List allowedPeripheralOperations + ) { + this.allowedPeripheralOperations = requireNonNull( + allowedPeripheralOperations, + "allowedPeripheralOperations" + ); + return this; + } + + @XmlElement(required = true) + public LocationTypeLayout getLocationTypeLayout() { + return locationTypeLayout; + } + + public LocationTypeTO setLocationTypeLayout( + @Nonnull + LocationTypeLayout locationTypeLayout + ) { + this.locationTypeLayout = requireNonNull(locationTypeLayout, "locationTypeLayout"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + public static class LocationTypeLayout { + + private String locationRepresentation = ""; + + /** + * Creates a new instance. + */ + public LocationTypeLayout() { + } + + @XmlAttribute(required = true) + public String getLocationRepresentation() { + return locationRepresentation; + } + + public LocationTypeLayout setLocationRepresentation( + @Nonnull + String locationRepresentation + ) { + this.locationRepresentation = requireNonNull( + locationRepresentation, + "locationRepresentation" + ); + return this; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/MemberTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/MemberTO.java new file mode 100644 index 0000000..594dd14 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/MemberTO.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v005; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +public class MemberTO + extends + PlantModelElementTO { + + /** + * Creates a new instance. + */ + public MemberTO() { + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/PathTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/PathTO.java new file mode 100644 index 0000000..e837cee --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/PathTO.java @@ -0,0 +1,265 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v005; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlSchemaType; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType( + propOrder = {"name", + "sourcePoint", + "destinationPoint", + "length", + "maxVelocity", + "maxReverseVelocity", + "peripheralOperations", + "locked", + "vehicleEnvelopes", + "properties", + "pathLayout"} +) +public class PathTO + extends + PlantModelElementTO { + + private String sourcePoint = ""; + private String destinationPoint = ""; + private Long length = 0L; + private Long maxVelocity = 0L; + private Long maxReverseVelocity = 0L; + private List peripheralOperations = new ArrayList<>(); + private Boolean locked = false; + private List vehicleEnvelopes = new ArrayList<>(); + private PathLayout pathLayout = new PathLayout(); + + /** + * Creates a new instance. + */ + public PathTO() { + } + + @XmlAttribute(required = true) + public String getSourcePoint() { + return sourcePoint; + } + + public PathTO setSourcePoint( + @Nonnull + String sourcePoint + ) { + requireNonNull(sourcePoint, "sourcePoint"); + this.sourcePoint = sourcePoint; + return this; + } + + @XmlAttribute(required = true) + public String getDestinationPoint() { + return destinationPoint; + } + + public PathTO setDestinationPoint( + @Nonnull + String destinationPoint + ) { + requireNonNull(destinationPoint, "destinationPoint"); + this.destinationPoint = destinationPoint; + return this; + } + + @XmlAttribute + @XmlSchemaType(name = "unsignedInt") + public Long getLength() { + return length; + } + + public PathTO setLength( + @Nonnull + Long length + ) { + requireNonNull(length, "length"); + this.length = length; + return this; + } + + @XmlAttribute(required = true) + @XmlSchemaType(name = "unsignedInt") + public Long getMaxVelocity() { + return maxVelocity; + } + + public PathTO setMaxVelocity( + @Nonnull + Long maxVelocity + ) { + requireNonNull(maxVelocity, "maxVelocity"); + this.maxVelocity = maxVelocity; + return this; + } + + @XmlAttribute(required = true) + @XmlSchemaType(name = "unsignedInt") + public Long getMaxReverseVelocity() { + return maxReverseVelocity; + } + + public PathTO setMaxReverseVelocity( + @Nonnull + Long maxReverseVelocity + ) { + requireNonNull(maxReverseVelocity, "maxReverseVelocity"); + this.maxReverseVelocity = maxReverseVelocity; + return this; + } + + @XmlElement(name = "peripheralOperation") + public List getPeripheralOperations() { + return peripheralOperations; + } + + public PathTO setPeripheralOperations(List peripheralOperations) { + this.peripheralOperations = requireNonNull(peripheralOperations, "peripheralOperations"); + return this; + } + + @XmlAttribute(required = true) + public Boolean isLocked() { + return locked; + } + + public PathTO setLocked(Boolean locked) { + this.locked = locked; + return this; + } + + @XmlElement(name = "vehicleEnvelope") + public List getVehicleEnvelopes() { + return vehicleEnvelopes; + } + + public PathTO setVehicleEnvelopes( + @Nonnull + List vehicleEnvelopes + ) { + this.vehicleEnvelopes = requireNonNull(vehicleEnvelopes, "vehicleEnvelopes"); + return this; + } + + @XmlElement(required = true) + public PathLayout getPathLayout() { + return pathLayout; + } + + public PathTO setPathLayout( + @Nonnull + PathLayout pathLayout + ) { + this.pathLayout = requireNonNull(pathLayout, "pathLayout"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType(propOrder = {"connectionType", "layerId", "controlPoints"}) + public static class PathLayout { + + private String connectionType = ""; + private Integer layerId = 0; + private List controlPoints = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public PathLayout() { + } + + @XmlAttribute(required = true) + public String getConnectionType() { + return connectionType; + } + + public PathLayout setConnectionType( + @Nonnull + String connectionType + ) { + this.connectionType = requireNonNull(connectionType, "connectionType"); + return this; + } + + @XmlAttribute(required = true) + public Integer getLayerId() { + return layerId; + } + + public PathLayout setLayerId( + @Nonnull + Integer layerId + ) { + this.layerId = requireNonNull(layerId, "layerId"); + return this; + } + + @XmlElement(name = "controlPoint") + public List getControlPoints() { + return controlPoints; + } + + public PathLayout setControlPoints( + @Nonnull + List controlPoints + ) { + this.controlPoints = requireNonNull(controlPoints, "controlPoints"); + return this; + } + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType(propOrder = {"x", "y"}) + public static class ControlPoint { + + private Long x = 0L; + private Long y = 0L; + + /** + * Creates a new instance. + */ + public ControlPoint() { + } + + @XmlAttribute(required = true) + public Long getX() { + return x; + } + + public ControlPoint setX( + @Nonnull + Long x + ) { + this.x = requireNonNull(x, "x"); + return this; + } + + @XmlAttribute(required = true) + public Long getY() { + return y; + } + + public ControlPoint setY( + @Nonnull + Long y + ) { + this.y = requireNonNull(y, "y"); + return this; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/PeripheralOperationTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/PeripheralOperationTO.java new file mode 100644 index 0000000..b1489ad --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/PeripheralOperationTO.java @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v005; + +import static java.util.Objects.requireNonNull; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +public class PeripheralOperationTO + extends + PlantModelElementTO { + + private String locationName = ""; + private String executionTrigger = ""; + private boolean completionRequired; + + /** + * Creates a new instance. + */ + public PeripheralOperationTO() { + } + + @XmlAttribute(required = true) + public String getLocationName() { + return locationName; + } + + public PeripheralOperationTO setLocationName(String locationName) { + this.locationName = requireNonNull(locationName, "locationName"); + return this; + } + + @XmlAttribute(required = true) + public String getExecutionTrigger() { + return executionTrigger; + } + + public PeripheralOperationTO setExecutionTrigger(String executionTrigger) { + this.executionTrigger = requireNonNull(executionTrigger, "executionTrigger"); + return this; + } + + @XmlAttribute(required = true) + public boolean isCompletionRequired() { + return completionRequired; + } + + public PeripheralOperationTO setCompletionRequired(boolean completionRequired) { + this.completionRequired = completionRequired; + return this; + } + +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/PlantModelElementTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/PlantModelElementTO.java new file mode 100644 index 0000000..811dee3 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/PlantModelElementTO.java @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v005; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlTransient; + +/** + */ +@XmlTransient +@XmlAccessorType(XmlAccessType.PROPERTY) +public class PlantModelElementTO { + + private String name = ""; + private List properties = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public PlantModelElementTO() { + } + + @XmlAttribute(required = true) + public String getName() { + return name; + } + + public PlantModelElementTO setName( + @Nonnull + String name + ) { + requireNonNull(name, "name"); + this.name = name; + return this; + } + + @XmlElement(name = "property") + public List getProperties() { + return properties; + } + + public PlantModelElementTO setProperties( + @Nonnull + List properties + ) { + requireNonNull(properties, "properties"); + this.properties = properties; + return this; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/PointTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/PointTO.java new file mode 100644 index 0000000..e52fe1f --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/PointTO.java @@ -0,0 +1,259 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v005; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType( + propOrder = {"name", "xPosition", "yPosition", "zPosition", "vehicleOrientationAngle", + "type", "vehicleEnvelopes", "outgoingPaths", "properties", "pointLayout"} +) +public class PointTO + extends + PlantModelElementTO { + + private Long xPosition = 0L; + private Long yPosition = 0L; + private Long zPosition = 0L; + private Float vehicleOrientationAngle = 0.0F; + private String type = "HALT_POSITION"; + private List vehicleEnvelopes = new ArrayList<>(); + private List outgoingPaths = new ArrayList<>(); + private PointLayout pointLayout = new PointLayout(); + + /** + * Creates a new instance. + */ + public PointTO() { + } + + @XmlAttribute(required = true) + public Long getxPosition() { + return xPosition; + } + + public PointTO setxPosition( + @Nonnull + Long xPosition + ) { + requireNonNull(xPosition, "xPosition"); + this.xPosition = xPosition; + return this; + } + + @XmlAttribute(required = true) + public Long getyPosition() { + return yPosition; + } + + public PointTO setyPosition( + @Nonnull + Long yPosition + ) { + requireNonNull(yPosition, "yPosition"); + this.yPosition = yPosition; + return this; + } + + @XmlAttribute + public Long getzPosition() { + return zPosition; + } + + public PointTO setzPosition( + @Nonnull + Long zPosition + ) { + requireNonNull(zPosition, "zPosition"); + this.zPosition = zPosition; + return this; + } + + @XmlAttribute + public Float getVehicleOrientationAngle() { + return vehicleOrientationAngle; + } + + public PointTO setVehicleOrientationAngle( + @Nonnull + Float vehicleOrientationAngle + ) { + requireNonNull(vehicleOrientationAngle, "vehicleOrientationAngle"); + this.vehicleOrientationAngle = vehicleOrientationAngle; + return this; + } + + @XmlAttribute(required = true) + public String getType() { + return type; + } + + public PointTO setType( + @Nonnull + String type + ) { + requireNonNull(type, "type"); + this.type = type; + return this; + } + + @XmlElement(name = "outgoingPath") + public List getOutgoingPaths() { + return outgoingPaths; + } + + public PointTO setOutgoingPaths( + @Nonnull + List outgoingPath + ) { + requireNonNull(outgoingPath, "outgoingPath"); + this.outgoingPaths = outgoingPath; + return this; + } + + @XmlElement(name = "vehicleEnvelope") + public List getVehicleEnvelopes() { + return vehicleEnvelopes; + } + + public PointTO setVehicleEnvelopes( + @Nonnull + List vehicleEnvelopes + ) { + this.vehicleEnvelopes = requireNonNull(vehicleEnvelopes, "vehicleEnvelopes"); + return this; + } + + @XmlElement(required = true) + public PointLayout getPointLayout() { + return pointLayout; + } + + public PointTO setPointLayout( + @Nonnull + PointLayout pointLayout + ) { + this.pointLayout = requireNonNull(pointLayout, "pointLayout"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + public static class OutgoingPath { + + private String name = ""; + + /** + * Creates a new instance. + */ + public OutgoingPath() { + } + + @XmlAttribute(required = true) + public String getName() { + return name; + } + + public OutgoingPath setName( + @Nonnull + String name + ) { + requireNonNull(name, "name"); + this.name = name; + return this; + } + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType(propOrder = {"xPosition", "yPosition", "xLabelOffset", "yLabelOffset", "layerId"}) + public static class PointLayout { + + private Long xPosition = 0L; + private Long yPosition = 0L; + private Long xLabelOffset = 0L; + private Long yLabelOffset = 0L; + private Integer layerId = 0; + + /** + * Creates a new instance. + */ + public PointLayout() { + } + + @XmlAttribute(required = true) + public Long getxPosition() { + return xPosition; + } + + public PointLayout setxPosition( + @Nonnull + Long xPosition + ) { + this.xPosition = requireNonNull(xPosition, "xPosition"); + return this; + } + + @XmlAttribute(required = true) + public Long getyPosition() { + return yPosition; + } + + public PointLayout setyPosition( + @Nonnull + Long yPosition + ) { + this.yPosition = requireNonNull(yPosition, "yPosition"); + return this; + } + + @XmlAttribute(required = true) + public Long getxLabelOffset() { + return xLabelOffset; + } + + public PointLayout setxLabelOffset( + @Nonnull + Long xLabelOffset + ) { + this.xLabelOffset = requireNonNull(xLabelOffset, "xLabelOffset"); + return this; + } + + @XmlAttribute(required = true) + public Long getyLabelOffset() { + return yLabelOffset; + } + + public PointLayout setyLabelOffset( + @Nonnull + Long yLabelOffset + ) { + this.yLabelOffset = requireNonNull(yLabelOffset, "yLabelOffset"); + return this; + } + + @XmlAttribute(required = true) + public Integer getLayerId() { + return layerId; + } + + public PointLayout setLayerId( + @Nonnull + Integer layerId + ) { + this.layerId = requireNonNull(layerId, "layerId"); + return this; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/PropertyTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/PropertyTO.java new file mode 100644 index 0000000..e4425d9 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/PropertyTO.java @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v005; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(propOrder = {"name", "value"}) +public class PropertyTO { + + private String name = ""; + private String value = ""; + + /** + * Creates a new instance. + */ + public PropertyTO() { + } + + @XmlAttribute(required = true) + public String getName() { + return name; + } + + public PropertyTO setName( + @Nonnull + String name + ) { + this.name = requireNonNull(name, "name"); + return this; + } + + @XmlAttribute(required = true) + public String getValue() { + return value; + } + + public PropertyTO setValue( + @Nonnull + String value + ) { + requireNonNull(value, "value"); + this.value = value; + return this; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/V005ModelParser.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/V005ModelParser.java new file mode 100644 index 0000000..78509c3 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/V005ModelParser.java @@ -0,0 +1,355 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v005; + +import static org.opentcs.data.ObjectPropConstants.LOCTYPE_DEFAULT_REPRESENTATION; +import static org.opentcs.data.ObjectPropConstants.LOC_DEFAULT_REPRESENTATION; + +import java.io.IOException; +import java.io.Reader; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.opentcs.data.model.visualization.LocationRepresentation; +import org.opentcs.util.persistence.v004.V004ModelParser; +import org.opentcs.util.persistence.v004.V004PlantModelTO; + +/** + * The parser for V005 models. + */ +public class V005ModelParser { + + /** + * Creates a new instance. + */ + public V005ModelParser() { + } + + /** + * Reads a model with the given reader and parses it to a {@link V005PlantModelTO} instance. + * + * @param reader The reader to use. + * @param modelVersion The model version. + * @return The parsed {@link V005PlantModelTO}. + * @throws IOException If there was an error reading the model. + */ + public V005PlantModelTO readRaw(Reader reader, String modelVersion) + throws IOException { + if (Objects.equals(modelVersion, V005PlantModelTO.VERSION_STRING)) { + return V005PlantModelTO.fromXml(reader); + } + else { + return convert(new V004ModelParser().readRaw(reader, modelVersion)); + } + } + + private V005PlantModelTO convert(V004PlantModelTO to) { + return new V005PlantModelTO() + .setName(to.getName()) + .setPoints(convertPoints(to)) + .setPaths(convertPaths(to)) + .setVehicles(convertVehicles(to)) + .setLocationTypes(convertLocationTypes(to)) + .setLocations(convertLocations(to)) + .setBlocks(convertBlocks(to)) + .setVisualLayout(convertVisualLayout(to)) + .setProperties(convertProperties(to.getProperties())); + } + + private List convertProperties( + List tos + ) { + return tos.stream() + .map(property -> new PropertyTO().setName(property.getName()).setValue(property.getValue())) + .toList(); + } + + private List convertPoints(V004PlantModelTO to) { + return to.getPoints().stream() + .map(point -> { + PointTO result = new PointTO(); + result.setName(point.getName()) + .setProperties(convertProperties(point.getProperties())); + result.setxPosition(point.getxPosition()) + .setyPosition(point.getyPosition()) + .setzPosition(point.getzPosition()) + .setVehicleOrientationAngle(point.getVehicleOrientationAngle()) + .setType(point.getType()) + .setVehicleEnvelopes(convertVehicleEnvelopes(point.getVehicleEnvelopes())) + .setOutgoingPaths(convertOutgoingPaths(point)) + .setPointLayout( + new PointTO.PointLayout() + .setxPosition(point.getPointLayout().getxPosition()) + .setyPosition(point.getPointLayout().getyPosition()) + .setxLabelOffset(point.getPointLayout().getxLabelOffset()) + .setyLabelOffset(point.getPointLayout().getyLabelOffset()) + .setLayerId(point.getPointLayout().getLayerId()) + ); + return result; + }) + .toList(); + } + + private List convertVehicleEnvelopes( + List tos + ) { + return tos.stream() + .map( + vehicleEnvelope -> new VehicleEnvelopeTO() + .setKey(vehicleEnvelope.getKey()) + .setVertices( + vehicleEnvelope.getVertices().stream() + .map( + couple -> new CoupleTO() + .setX(couple.getX()) + .setY(couple.getY()) + ) + .toList() + ) + ) + .toList(); + } + + private Map toPropertiesMap( + List properties + ) { + Map result = new HashMap<>(); + for (org.opentcs.util.persistence.v004.PropertyTO property : properties) { + result.put(property.getName(), property.getValue()); + } + return result; + } + + private List convertOutgoingPaths( + org.opentcs.util.persistence.v004.PointTO to + ) { + return to.getOutgoingPaths().stream() + .map(path -> new PointTO.OutgoingPath().setName(path.getName())) + .toList(); + } + + private List convertPaths(V004PlantModelTO to) { + return to.getPaths().stream() + .map(path -> { + PathTO result = new PathTO(); + result.setName(path.getName()) + .setProperties(convertProperties(path.getProperties())); + result.setSourcePoint(path.getSourcePoint()) + .setDestinationPoint(path.getDestinationPoint()) + .setLength(path.getLength()) + .setMaxVelocity(path.getMaxVelocity()) + .setMaxReverseVelocity(path.getMaxReverseVelocity()) + .setPeripheralOperations(convertPeripheralOperations(path.getPeripheralOperations())) + .setLocked(path.isLocked()) + .setVehicleEnvelopes(convertVehicleEnvelopes(path.getVehicleEnvelopes())) + .setPathLayout( + new PathTO.PathLayout() + .setConnectionType(path.getPathLayout().getConnectionType()) + .setControlPoints( + path.getPathLayout().getControlPoints().stream() + .map( + controlPoint -> new PathTO.ControlPoint() + .setX(controlPoint.getX()) + .setY(controlPoint.getY()) + ) + .toList() + ) + .setLayerId(path.getPathLayout().getLayerId()) + ); + return result; + }) + .toList(); + } + + private List convertPeripheralOperations( + List tos + ) { + return tos.stream() + .map( + peripheralOperation -> { + PeripheralOperationTO result = new PeripheralOperationTO(); + result.setName(peripheralOperation.getName()) + .setProperties(convertProperties(peripheralOperation.getProperties())); + result.setLocationName(peripheralOperation.getLocationName()) + .setExecutionTrigger(peripheralOperation.getExecutionTrigger()) + .setCompletionRequired(peripheralOperation.isCompletionRequired()); + return result; + } + ) + .toList(); + } + + private List convertVehicles(V004PlantModelTO to) { + return to.getVehicles().stream() + .map(vehicle -> { + VehicleTO result = new VehicleTO(); + result.setName(vehicle.getName()) + .setProperties(convertProperties(vehicle.getProperties())); + result.setLength(vehicle.getLength()) + .setEnergyLevelCritical(vehicle.getEnergyLevelCritical()) + .setEnergyLevelGood(vehicle.getEnergyLevelGood()) + .setEnergyLevelFullyRecharged(vehicle.getEnergyLevelFullyRecharged()) + .setEnergyLevelSufficientlyRecharged(vehicle.getEnergyLevelSufficientlyRecharged()) + .setMaxVelocity(vehicle.getMaxVelocity()) + .setMaxReverseVelocity(vehicle.getMaxReverseVelocity()) + .setEnvelopeKey(vehicle.getEnvelopeKey()) + .setVehicleLayout( + new VehicleTO.VehicleLayout() + .setColor(vehicle.getVehicleLayout().getColor()) + ); + return result; + }) + .toList(); + } + + private List convertLocationTypes(V004PlantModelTO to) { + return to.getLocationTypes().stream() + .map(locationType -> { + String locationRepresentation = toPropertiesMap(locationType.getProperties()) + .getOrDefault(LOCTYPE_DEFAULT_REPRESENTATION, LocationRepresentation.NONE.name()); + + LocationTypeTO result = new LocationTypeTO(); + result.setName(locationType.getName()) + .setProperties(convertProperties(locationType.getProperties())); + result.setAllowedOperations(convertAllowedOperations(locationType.getAllowedOperations())) + .setAllowedPeripheralOperations( + convertAllowedPeripheralOperations(locationType.getAllowedPeripheralOperations()) + ) + .setLocationTypeLayout( + new LocationTypeTO.LocationTypeLayout() + .setLocationRepresentation(locationRepresentation) + ); + return result; + }) + .toList(); + } + + private List convertAllowedOperations( + List tos + ) { + return tos.stream() + .map(allowedOperation -> { + AllowedOperationTO result = new AllowedOperationTO(); + result.setName(allowedOperation.getName()); + result.setProperties(convertProperties(allowedOperation.getProperties())); + return result; + }) + .toList(); + } + + private List convertAllowedPeripheralOperations( + List tos + ) { + return tos.stream() + .map( + allowedPeripheralOperation -> { + AllowedPeripheralOperationTO result = new AllowedPeripheralOperationTO(); + result.setName(allowedPeripheralOperation.getName()); + result.setProperties(convertProperties(allowedPeripheralOperation.getProperties())); + return result; + } + ) + .toList(); + } + + private List convertLocations(V004PlantModelTO to) { + return to.getLocations().stream() + .map(location -> { + String locationRepresentation = toPropertiesMap(location.getProperties()) + .getOrDefault(LOC_DEFAULT_REPRESENTATION, LocationRepresentation.DEFAULT.name()); + + LocationTO result = new LocationTO(); + result.setName(location.getName()) + .setProperties(convertProperties(location.getProperties())); + result.setxPosition(location.getxPosition()) + .setyPosition(location.getyPosition()) + .setzPosition(location.getzPosition()) + .setType(location.getType()) + .setLinks(convertLinks(location)) + .setLocked(location.isLocked()) + .setLocationLayout( + new LocationTO.LocationLayout() + .setxPosition(location.getLocationLayout().getxPosition()) + .setyPosition(location.getLocationLayout().getyPosition()) + .setxLabelOffset(location.getLocationLayout().getxLabelOffset()) + .setyLabelOffset(location.getLocationLayout().getyLabelOffset()) + .setLocationRepresentation(locationRepresentation) + .setLayerId(location.getLocationLayout().getLayerId()) + ); + return result; + }) + .toList(); + } + + private List convertLinks(org.opentcs.util.persistence.v004.LocationTO to) { + return to.getLinks().stream() + .map(link -> { + return new LocationTO.Link() + .setPoint(link.getPoint()) + .setAllowedOperations(convertAllowedOperations(link.getAllowedOperations())); + }) + .toList(); + } + + private List convertBlocks(V004PlantModelTO to) { + return to.getBlocks().stream() + .map(block -> { + BlockTO result = new BlockTO(); + result.setName(block.getName()) + .setProperties(convertProperties(block.getProperties())); + result.setType(block.getType()) + .setMembers(convertMembers(block.getMembers())) + .setBlockLayout( + new BlockTO.BlockLayout() + .setColor(block.getBlockLayout().getColor()) + ); + return result; + }) + .toList(); + } + + private List convertMembers(List tos) { + return tos.stream() + .map(member -> { + MemberTO result = new MemberTO(); + result.setName(member.getName()) + .setProperties(convertProperties(member.getProperties())); + return result; + }) + .toList(); + } + + private VisualLayoutTO convertVisualLayout(V004PlantModelTO to) { + VisualLayoutTO result = new VisualLayoutTO() + .setScaleX(to.getVisualLayout().getScaleX()) + .setScaleY(to.getVisualLayout().getScaleY()) + .setLayers( + to.getVisualLayout().getLayers().stream() + .map( + layer -> new VisualLayoutTO.Layer() + .setId(layer.getId()) + .setOrdinal(layer.getOrdinal()) + .setVisible(layer.isVisible()) + .setName(layer.getName()) + .setGroupId(layer.getGroupId()) + ) + .toList() + ) + .setLayerGroups( + to.getVisualLayout().getLayerGroups().stream() + .map( + layerGroup -> new VisualLayoutTO.LayerGroup() + .setId(layerGroup.getId()) + .setName(layerGroup.getName()) + .setVisible(layerGroup.isVisible()) + ) + .toList() + ); + result + .setProperties(convertProperties(to.getVisualLayout().getProperties())) + .setName(to.getVisualLayout().getName()); + + return result; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/V005PlantModelTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/V005PlantModelTO.java new file mode 100644 index 0000000..8cb1651 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/V005PlantModelTO.java @@ -0,0 +1,260 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v005; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import javax.xml.XMLConstants; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import org.opentcs.util.persistence.BasePlantModelTO; +import org.xml.sax.SAXException; + +/** + */ +@XmlRootElement(name = "model") +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType( + propOrder = {"version", "name", "points", "paths", "vehicles", "locationTypes", + "locations", "blocks", "visualLayout", "properties"} +) +public class V005PlantModelTO + extends + BasePlantModelTO { + + /** + * This plant model implementation's version string. + */ + public static final String VERSION_STRING = "0.0.5"; + + private String name = ""; + private List points = new ArrayList<>(); + private List paths = new ArrayList<>(); + private List vehicles = new ArrayList<>(); + private List locationTypes = new ArrayList<>(); + private List locations = new ArrayList<>(); + private List blocks = new ArrayList<>(); + private VisualLayoutTO visualLayout = new VisualLayoutTO(); + private List properties = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public V005PlantModelTO() { + } + + @XmlAttribute(required = true) + public String getName() { + return name; + } + + public V005PlantModelTO setName( + @Nonnull + String name + ) { + requireNonNull(name, "name"); + this.name = name; + return this; + } + + @XmlElement(name = "point") + public List getPoints() { + return points; + } + + public V005PlantModelTO setPoints( + @Nonnull + List points + ) { + requireNonNull(points, "points"); + this.points = points; + return this; + } + + @XmlElement(name = "path") + public List getPaths() { + return paths; + } + + public V005PlantModelTO setPaths( + @Nonnull + List paths + ) { + requireNonNull(paths, "paths"); + this.paths = paths; + return this; + } + + @XmlElement(name = "vehicle") + public List getVehicles() { + return vehicles; + } + + public V005PlantModelTO setVehicles( + @Nonnull + List vehicles + ) { + requireNonNull(vehicles, "vehicles"); + this.vehicles = vehicles; + return this; + } + + @XmlElement(name = "locationType") + public List getLocationTypes() { + return locationTypes; + } + + public V005PlantModelTO setLocationTypes( + @Nonnull + List locationTypes + ) { + requireNonNull(locationTypes, "locationTypes"); + this.locationTypes = locationTypes; + return this; + } + + @XmlElement(name = "location") + public List getLocations() { + return locations; + } + + public V005PlantModelTO setLocations( + @Nonnull + List locations + ) { + requireNonNull(locations, "locations"); + this.locations = locations; + return this; + } + + @XmlElement(name = "block") + public List getBlocks() { + return blocks; + } + + public V005PlantModelTO setBlocks( + @Nonnull + List blocks + ) { + requireNonNull(blocks, "blocks"); + this.blocks = blocks; + return this; + } + + @XmlElement + public VisualLayoutTO getVisualLayout() { + return visualLayout; + } + + public V005PlantModelTO setVisualLayout( + @Nonnull + VisualLayoutTO visualLayout + ) { + this.visualLayout = requireNonNull(visualLayout, "visualLayout"); + return this; + } + + @XmlElement(name = "property") + public List getProperties() { + return properties; + } + + public V005PlantModelTO setProperties( + @Nonnull + List properties + ) { + requireNonNull(properties, "properties"); + this.properties = properties; + return this; + } + + /** + * Marshals this instance to its XML representation and writes it to the given writer. + * + * @param writer The writer to write this instance's XML representation to. + * @throws IOException If there was a problem marshalling this instance. + */ + public void toXml( + @Nonnull + Writer writer + ) + throws IOException { + requireNonNull(writer, "writer"); + + try { + createMarshaller().marshal(this, writer); + } + catch (JAXBException | SAXException exc) { + throw new IOException("Exception marshalling data", exc); + } + } + + /** + * Unmarshals an instance of this class from the given XML representation. + * + * @param reader Provides the XML representation to parse to an instance. + * @return The instance unmarshalled from the given reader. + * @throws IOException If there was a problem unmarshalling the given string. + */ + public static V005PlantModelTO fromXml( + @Nonnull + Reader reader + ) + throws IOException { + requireNonNull(reader, "reader"); + + try { + return (V005PlantModelTO) createUnmarshaller().unmarshal(reader); + } + catch (JAXBException | SAXException exc) { + throw new IOException("Exception unmarshalling data", exc); + } + } + + private static Marshaller createMarshaller() + throws JAXBException, + SAXException { + Marshaller marshaller = createContext().createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + marshaller.setSchema(createSchema()); + return marshaller; + } + + private static Unmarshaller createUnmarshaller() + throws JAXBException, + SAXException { + Unmarshaller unmarshaller = createContext().createUnmarshaller(); + unmarshaller.setSchema(createSchema()); + return unmarshaller; + } + + private static JAXBContext createContext() + throws JAXBException { + return JAXBContext.newInstance(V005PlantModelTO.class); + } + + private static Schema createSchema() + throws SAXException { + URL schemaUrl + = V005PlantModelTO.class.getResource("/org/opentcs/util/persistence/model-0.0.5.xsd"); + SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + return schemaFactory.newSchema(schemaUrl); + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/VehicleEnvelopeTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/VehicleEnvelopeTO.java new file mode 100644 index 0000000..0d11a22 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/VehicleEnvelopeTO.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v005; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(propOrder = {"key", "vertices"}) +public class VehicleEnvelopeTO { + + private String key; + private List vertices; + + public VehicleEnvelopeTO() { + } + + public String getKey() { + return key; + } + + @XmlAttribute + public VehicleEnvelopeTO setKey( + @Nonnull + String key + ) { + this.key = requireNonNull(key, "key"); + return this; + } + + @XmlElement(name = "vertex") + public List getVertices() { + return vertices; + } + + public VehicleEnvelopeTO setVertices( + @Nonnull + List vertices + ) { + this.vertices = requireNonNull(vertices, "vertices"); + return this; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/VehicleTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/VehicleTO.java new file mode 100644 index 0000000..1782304 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/VehicleTO.java @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v005; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlSchemaType; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType( + propOrder = {"name", "length", "energyLevelCritical", "energyLevelGood", + "energyLevelFullyRecharged", "energyLevelSufficientlyRecharged", + "maxVelocity", "maxReverseVelocity", "properties", "vehicleLayout"} +) +public class VehicleTO + extends + PlantModelElementTO { + + //max velocity in mm/s. + private int maxVelocity; + //max rev velocity in mm/s. + private int maxReverseVelocity; + private Long length = 0L; + private Long energyLevelCritical = 0L; + private Long energyLevelGood = 0L; + private Long energyLevelFullyRecharged = 0L; + private Long energyLevelSufficientlyRecharged = 0L; + private String envelopeKey; + private VehicleLayout vehicleLayout = new VehicleLayout(); + + /** + * Creates a new instance. + */ + public VehicleTO() { + } + + @XmlAttribute + @XmlSchemaType(name = "unsignedInt") + public Long getLength() { + return length; + } + + public VehicleTO setLength( + @Nonnull + Long length + ) { + requireNonNull(length, "length"); + this.length = length; + return this; + } + + @XmlAttribute + @XmlSchemaType(name = "unsignedInt") + public Long getEnergyLevelCritical() { + return energyLevelCritical; + } + + public VehicleTO setEnergyLevelCritical( + @Nonnull + Long energyLevelCritical + ) { + requireNonNull(energyLevelCritical, "energyLevelCritical"); + this.energyLevelCritical = energyLevelCritical; + return this; + } + + @XmlAttribute + @XmlSchemaType(name = "unsignedInt") + public Long getEnergyLevelGood() { + return energyLevelGood; + } + + public VehicleTO setEnergyLevelGood( + @Nonnull + Long energyLevelGood + ) { + requireNonNull(energyLevelGood, "energyLevelGood"); + this.energyLevelGood = energyLevelGood; + return this; + } + + @XmlAttribute + @XmlSchemaType(name = "unsignedInt") + public Long getEnergyLevelFullyRecharged() { + return energyLevelFullyRecharged; + } + + public VehicleTO setEnergyLevelFullyRecharged( + @Nonnull + Long energyLevelFullyRecharged + ) { + requireNonNull(energyLevelFullyRecharged, "energyLevelFullyRecharged"); + this.energyLevelFullyRecharged = energyLevelFullyRecharged; + return this; + } + + @XmlAttribute + @XmlSchemaType(name = "unsignedInt") + public Long getEnergyLevelSufficientlyRecharged() { + return energyLevelSufficientlyRecharged; + } + + public VehicleTO setEnergyLevelSufficientlyRecharged( + @Nonnull + Long energyLevelSufficientlyRecharged + ) { + requireNonNull(energyLevelSufficientlyRecharged, "energyLevelSufficientlyRecharged"); + this.energyLevelSufficientlyRecharged = energyLevelSufficientlyRecharged; + return this; + } + + @XmlAttribute + @XmlSchemaType(name = "unsignedInt") + public int getMaxVelocity() { + return maxVelocity; + } + + public VehicleTO setMaxVelocity( + @Nonnull + int maxVelocity + ) { + this.maxVelocity = maxVelocity; + return this; + } + + @XmlAttribute + @XmlSchemaType(name = "unsignedInt") + public int getMaxReverseVelocity() { + return maxReverseVelocity; + } + + public VehicleTO setMaxReverseVelocity( + @Nonnull + int maxReverseVelocity + ) { + this.maxReverseVelocity = maxReverseVelocity; + return this; + } + + @XmlAttribute + @Nullable + public String getEnvelopeKey() { + return envelopeKey; + } + + public VehicleTO setEnvelopeKey( + @Nullable + String envelopeKey + ) { + this.envelopeKey = envelopeKey; + return this; + } + + @XmlElement(required = true) + public VehicleLayout getVehicleLayout() { + return vehicleLayout; + } + + public VehicleTO setVehicleLayout( + @Nonnull + VehicleLayout vehicleLayout + ) { + this.vehicleLayout = requireNonNull(vehicleLayout, "vehicleLayout"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + public static class VehicleLayout { + + private String color = ""; + + /** + * Creates a new instance. + */ + public VehicleLayout() { + } + + @XmlAttribute(required = true) + public String getColor() { + return color; + } + + public VehicleLayout setColor( + @Nonnull + String color + ) { + this.color = requireNonNull(color, "color"); + return this; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/VisualLayoutTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/VisualLayoutTO.java new file mode 100644 index 0000000..fc41fd7 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v005/VisualLayoutTO.java @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v005; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(propOrder = {"name", "scaleX", "scaleY", "layers", "layerGroups", "properties"}) +public class VisualLayoutTO + extends + PlantModelElementTO { + + private Float scaleX = 0.0F; + private Float scaleY = 0.0F; + private List layers = new ArrayList<>(); + private List layerGroups = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public VisualLayoutTO() { + } + + @XmlAttribute(required = true) + public Float getScaleX() { + return scaleX; + } + + public VisualLayoutTO setScaleX( + @Nonnull + Float scaleX + ) { + requireNonNull(scaleX, "scaleX"); + this.scaleX = scaleX; + return this; + } + + @XmlAttribute(required = true) + public Float getScaleY() { + return scaleY; + } + + public VisualLayoutTO setScaleY( + @Nonnull + Float scaleY + ) { + requireNonNull(scaleY, "scaleY"); + this.scaleY = scaleY; + return this; + } + + @XmlElement(name = "layer") + public List getLayers() { + return layers; + } + + public VisualLayoutTO setLayers( + @Nonnull + List layers + ) { + this.layers = requireNonNull(layers, "layers"); + return this; + } + + @XmlElement(name = "layerGroup") + public List getLayerGroups() { + return layerGroups; + } + + public VisualLayoutTO setLayerGroups( + @Nonnull + List layerGroups + ) { + this.layerGroups = requireNonNull(layerGroups, "layerGroups"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType(propOrder = {"id", "ordinal", "visible", "name", "groupId"}) + public static class Layer { + + private Integer id = 0; + private Integer ordinal = 0; + private Boolean visible = true; + private String name = ""; + private Integer groupId = 0; + + /** + * Creates a new instance. + */ + public Layer() { + } + + @XmlAttribute(required = true) + public Integer getId() { + return id; + } + + public Layer setId(Integer id) { + this.id = requireNonNull(id, "id"); + return this; + } + + @XmlAttribute(required = true) + public Integer getOrdinal() { + return ordinal; + } + + public Layer setOrdinal(Integer ordinal) { + this.ordinal = requireNonNull(ordinal, "ordinal"); + return this; + } + + @XmlAttribute(required = true) + public Boolean isVisible() { + return visible; + } + + public Layer setVisible(Boolean visible) { + this.visible = requireNonNull(visible, "visible"); + return this; + } + + @XmlAttribute(required = true) + public String getName() { + return name; + } + + public Layer setName(String name) { + this.name = requireNonNull(name, "name"); + return this; + } + + @XmlAttribute(required = true) + public Integer getGroupId() { + return groupId; + } + + public Layer setGroupId(Integer groupId) { + this.groupId = requireNonNull(groupId, "groupId"); + return this; + } + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType(propOrder = {"id", "name", "visible"}) + public static class LayerGroup { + + private Integer id = 0; + private String name = ""; + private Boolean visible = true; + + /** + * Creates a new instance. + */ + public LayerGroup() { + } + + @XmlAttribute(required = true) + public Integer getId() { + return id; + } + + public LayerGroup setId(Integer id) { + this.id = requireNonNull(id, "id"); + return this; + } + + @XmlAttribute(required = true) + public String getName() { + return name; + } + + public LayerGroup setName(String name) { + this.name = requireNonNull(name, "name"); + return this; + } + + @XmlAttribute(required = true) + public Boolean isVisible() { + return visible; + } + + public LayerGroup setVisible(Boolean visible) { + this.visible = requireNonNull(visible, "visible"); + return this; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/AllowedOperationTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/AllowedOperationTO.java new file mode 100644 index 0000000..529c419 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/AllowedOperationTO.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +public class AllowedOperationTO + extends + PlantModelElementTO { + + /** + * Creates a new instance. + */ + public AllowedOperationTO() { + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/AllowedPeripheralOperationTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/AllowedPeripheralOperationTO.java new file mode 100644 index 0000000..b0698fa --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/AllowedPeripheralOperationTO.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +public class AllowedPeripheralOperationTO + extends + PlantModelElementTO { + + /** + * Creates a new instance. + */ + public AllowedPeripheralOperationTO() { + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/BlockTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/BlockTO.java new file mode 100644 index 0000000..19ae332 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/BlockTO.java @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(propOrder = {"name", "type", "members", "properties", "blockLayout"}) +public class BlockTO + extends + PlantModelElementTO { + + private String type = "SINGLE_VEHICLE_ONLY"; + private List members = new ArrayList<>(); + private BlockLayout blockLayout = new BlockLayout(); + + /** + * Creates a new instance. + */ + public BlockTO() { + } + + @XmlAttribute(required = true) + public String getType() { + return type; + } + + public BlockTO setType( + @Nonnull + String type + ) { + requireNonNull(type, "type"); + this.type = type; + return this; + } + + @XmlElement(name = "member") + public List getMembers() { + return members; + } + + public BlockTO setMembers( + @Nonnull + List members + ) { + requireNonNull(members, "members"); + this.members = members; + return this; + } + + @XmlElement(required = true) + public BlockLayout getBlockLayout() { + return blockLayout; + } + + public BlockTO setBlockLayout( + @Nonnull + BlockLayout blockLayout + ) { + this.blockLayout = requireNonNull(blockLayout, "blockLayout"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + public static class BlockLayout { + + private String color = ""; + + /** + * Creates a new instance. + */ + public BlockLayout() { + } + + @XmlAttribute(required = true) + public String getColor() { + return color; + } + + public BlockLayout setColor( + @Nonnull + String color + ) { + this.color = requireNonNull(color, "color"); + return this; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/BoundingBoxTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/BoundingBoxTO.java new file mode 100644 index 0000000..8068605 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/BoundingBoxTO.java @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(propOrder = {"length", "width", "height", "referenceOffsetX", "referenceOffsetY"}) +public class BoundingBoxTO { + + private long length; + private long width; + private long height; + private long referenceOffsetX; + private long referenceOffsetY; + + /** + * Creates a new instance. + */ + public BoundingBoxTO() { + } + + @XmlAttribute(required = true) + public long getLength() { + return length; + } + + public BoundingBoxTO setLength(long length) { + this.length = length; + return this; + } + + @XmlAttribute(required = true) + public long getWidth() { + return width; + } + + public BoundingBoxTO setWidth(long width) { + this.width = width; + return this; + } + + @XmlAttribute(required = true) + public long getHeight() { + return height; + } + + public BoundingBoxTO setHeight(long height) { + this.height = height; + return this; + } + + @XmlAttribute(required = true) + public long getReferenceOffsetX() { + return referenceOffsetX; + } + + public BoundingBoxTO setReferenceOffsetX(long referenceOffsetX) { + this.referenceOffsetX = referenceOffsetX; + return this; + } + + @XmlAttribute(required = true) + public long getReferenceOffsetY() { + return referenceOffsetY; + } + + public BoundingBoxTO setReferenceOffsetY(long referenceOffsetY) { + this.referenceOffsetY = referenceOffsetY; + return this; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/Comparators.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/Comparators.java new file mode 100644 index 0000000..5cfdb26 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/Comparators.java @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import java.util.Comparator; + +/** + * Some comparator implementations for JAXB classes. + */ +public final class Comparators { + + /** + * Prevents instantiation. + */ + private Comparators() { + } + + /** + * Returns a comparator for ordering PlantModelElementTOs ascendingly by their names. + * + * @return A comparator for ordering PlantModelElementTOs ascendingly by their names. + */ + public static Comparator elementsByName() { + return Comparator.comparing(PlantModelElementTO::getName); + } + + /** + * Returns a comparator for ordering OutgoingPaths ascendingly by their names. + * + * @return A comparator for ordering OutgoingPaths ascendingly by their names. + */ + public static Comparator outgoingPathsByName() { + return Comparator.comparing(PointTO.OutgoingPath::getName); + } + + /** + * Returns a comparator for ordering Links ascendingly by their point names. + * + * @return A comparator for ordering Links ascendingly by their point names. + */ + public static Comparator linksByPointName() { + return Comparator.comparing(LocationTO.Link::getPoint); + } + + /** + * Returns a comparator for ordering Propertiess ascendingly by their names. + * + * @return A comparator for ordering Properties ascendingly by their names. + */ + public static Comparator propertiesByName() { + return Comparator.comparing(PropertyTO::getName); + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/CoupleTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/CoupleTO.java new file mode 100644 index 0000000..124b08a --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/CoupleTO.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(propOrder = {"x", "y"}) +public class CoupleTO { + + private Long x; + private Long y; + + public CoupleTO() { + } + + @XmlAttribute(required = true) + public Long getX() { + return x; + } + + public CoupleTO setX( + @Nonnull + Long x + ) { + this.x = requireNonNull(x, "x"); + return this; + } + + @XmlAttribute(required = true) + public Long getY() { + return y; + } + + public CoupleTO setY( + @Nonnull + Long y + ) { + this.y = requireNonNull(y, "y"); + return this; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/LocationTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/LocationTO.java new file mode 100644 index 0000000..cba0866 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/LocationTO.java @@ -0,0 +1,259 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType( + propOrder = {"name", "positionX", "positionY", "positionZ", "links", "locked", + "properties", "locationLayout"} +) +public class LocationTO + extends + PlantModelElementTO { + + private Long positionX = 0L; + private Long positionY = 0L; + private Long positionZ = 0L; + private String type = ""; + private List links = new ArrayList<>(); + private Boolean locked = false; + private LocationLayout locationLayout = new LocationLayout(); + + /** + * Creates a new instance. + */ + public LocationTO() { + } + + @XmlAttribute(required = true) + public Long getPositionX() { + return positionX; + } + + public LocationTO setPositionX( + @Nonnull + Long positionX + ) { + requireNonNull(positionX, "positionX"); + this.positionX = positionX; + return this; + } + + @XmlAttribute(required = true) + public Long getPositionY() { + return positionY; + } + + public LocationTO setPositionY( + @Nonnull + Long positionY + ) { + requireNonNull(positionY, "positionY"); + this.positionY = positionY; + return this; + } + + @XmlAttribute(required = true) + public Long getPositionZ() { + return positionZ; + } + + public LocationTO setPositionZ( + @Nonnull + Long positionZ + ) { + requireNonNull(positionZ, "positionZ"); + this.positionZ = positionZ; + return this; + } + + @XmlAttribute + public String getType() { + return type; + } + + public LocationTO setType( + @Nonnull + String type + ) { + requireNonNull(type, "type"); + this.type = type; + return this; + } + + @XmlElement(name = "link", required = true) + public List getLinks() { + return links; + } + + public LocationTO setLinks( + @Nonnull + List links + ) { + requireNonNull(links, "links"); + this.links = links; + return this; + } + + @XmlAttribute(required = true) + public Boolean isLocked() { + return locked; + } + + public LocationTO setLocked(Boolean locked) { + this.locked = locked; + return this; + } + + @XmlElement(required = true) + public LocationLayout getLocationLayout() { + return locationLayout; + } + + public LocationTO setLocationLayout( + @Nonnull + LocationLayout locationLayout + ) { + this.locationLayout = requireNonNull(locationLayout, "locationLayout"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType(propOrder = {"point", "allowedOperations"}) + public static class Link { + + private String point = ""; + private List allowedOperations = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public Link() { + } + + @XmlAttribute(required = true) + public String getPoint() { + return point; + } + + public Link setPoint( + @Nonnull + String point + ) { + requireNonNull(point, "point"); + this.point = point; + return this; + } + + @XmlElement(name = "allowedOperation") + public List getAllowedOperations() { + return allowedOperations; + } + + public Link setAllowedOperations( + @Nonnull + List allowedOperations + ) { + requireNonNull(allowedOperations, "allowedOperations"); + this.allowedOperations = allowedOperations; + return this; + } + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType( + propOrder = {"positionX", "positionY", "labelOffsetX", "labelOffsetY", + "locationRepresentation", "layerId"} + ) + public static class LocationLayout { + + private Long positionX = 0L; + private Long positionY = 0L; + private Long labelOffsetX = 0L; + private Long labelOffsetY = 0L; + private String locationRepresentation = ""; + private Integer layerId = 0; + + /** + * Creates a new instance. + */ + public LocationLayout() { + } + + @XmlAttribute(required = true) + public Long getPositionX() { + return positionX; + } + + public LocationLayout setPositionX(Long positionX) { + this.positionX = requireNonNull(positionX, "positionX"); + return this; + } + + @XmlAttribute(required = true) + public Long getPositionY() { + return positionY; + } + + public LocationLayout setPositionY(Long positionY) { + this.positionY = requireNonNull(positionY, "positionY"); + return this; + } + + @XmlAttribute(required = true) + public Long getLabelOffsetX() { + return labelOffsetX; + } + + public LocationLayout setLabelOffsetX(Long labelOffsetX) { + this.labelOffsetX = requireNonNull(labelOffsetX, "labelOffsetX"); + return this; + } + + @XmlAttribute(required = true) + public Long getLabelOffsetY() { + return labelOffsetY; + } + + public LocationLayout setLabelOffsetY(Long labelOffsetY) { + this.labelOffsetY = requireNonNull(labelOffsetY, "labelOffsetY"); + return this; + } + + @XmlAttribute(required = true) + public String getLocationRepresentation() { + return locationRepresentation; + } + + public LocationLayout setLocationRepresentation(String locationRepresentation) { + this.locationRepresentation = requireNonNull( + locationRepresentation, + "locationRepresentation" + ); + return this; + } + + @XmlAttribute(required = true) + public Integer getLayerId() { + return layerId; + } + + public LocationLayout setLayerId(Integer layerId) { + this.layerId = requireNonNull(layerId, "layerId"); + return this; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/LocationTypeTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/LocationTypeTO.java new file mode 100644 index 0000000..ed9942d --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/LocationTypeTO.java @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType( + propOrder = {"name", + "allowedOperations", + "allowedPeripheralOperations", + "properties", + "locationTypeLayout"} +) +public class LocationTypeTO + extends + PlantModelElementTO { + + private List allowedOperations = new ArrayList<>(); + private List allowedPeripheralOperations = new ArrayList<>(); + private LocationTypeLayout locationTypeLayout = new LocationTypeLayout(); + + /** + * Creates a new instance. + */ + public LocationTypeTO() { + } + + @XmlElement(name = "allowedOperation") + public List getAllowedOperations() { + return allowedOperations; + } + + public LocationTypeTO setAllowedOperations( + @Nonnull + List allowedOperations + ) { + this.allowedOperations = requireNonNull(allowedOperations, "allowedOperations"); + return this; + } + + @XmlElement(name = "allowedPeripheralOperation") + public List getAllowedPeripheralOperations() { + return allowedPeripheralOperations; + } + + public LocationTypeTO setAllowedPeripheralOperations( + List allowedPeripheralOperations + ) { + this.allowedPeripheralOperations = requireNonNull( + allowedPeripheralOperations, + "allowedPeripheralOperations" + ); + return this; + } + + @XmlElement(required = true) + public LocationTypeLayout getLocationTypeLayout() { + return locationTypeLayout; + } + + public LocationTypeTO setLocationTypeLayout( + @Nonnull + LocationTypeLayout locationTypeLayout + ) { + this.locationTypeLayout = requireNonNull(locationTypeLayout, "locationTypeLayout"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + public static class LocationTypeLayout { + + private String locationRepresentation = ""; + + /** + * Creates a new instance. + */ + public LocationTypeLayout() { + } + + @XmlAttribute(required = true) + public String getLocationRepresentation() { + return locationRepresentation; + } + + public LocationTypeLayout setLocationRepresentation( + @Nonnull + String locationRepresentation + ) { + this.locationRepresentation = requireNonNull( + locationRepresentation, + "locationRepresentation" + ); + return this; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/MemberTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/MemberTO.java new file mode 100644 index 0000000..b5eeadb --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/MemberTO.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +public class MemberTO + extends + PlantModelElementTO { + + /** + * Creates a new instance. + */ + public MemberTO() { + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/PathTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/PathTO.java new file mode 100644 index 0000000..be42347 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/PathTO.java @@ -0,0 +1,265 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlSchemaType; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType( + propOrder = {"name", + "sourcePoint", + "destinationPoint", + "length", + "maxVelocity", + "maxReverseVelocity", + "peripheralOperations", + "locked", + "vehicleEnvelopes", + "properties", + "pathLayout"} +) +public class PathTO + extends + PlantModelElementTO { + + private String sourcePoint = ""; + private String destinationPoint = ""; + private Long length = 0L; + private Long maxVelocity = 0L; + private Long maxReverseVelocity = 0L; + private List peripheralOperations = new ArrayList<>(); + private Boolean locked = false; + private List vehicleEnvelopes = new ArrayList<>(); + private PathLayout pathLayout = new PathLayout(); + + /** + * Creates a new instance. + */ + public PathTO() { + } + + @XmlAttribute(required = true) + public String getSourcePoint() { + return sourcePoint; + } + + public PathTO setSourcePoint( + @Nonnull + String sourcePoint + ) { + requireNonNull(sourcePoint, "sourcePoint"); + this.sourcePoint = sourcePoint; + return this; + } + + @XmlAttribute(required = true) + public String getDestinationPoint() { + return destinationPoint; + } + + public PathTO setDestinationPoint( + @Nonnull + String destinationPoint + ) { + requireNonNull(destinationPoint, "destinationPoint"); + this.destinationPoint = destinationPoint; + return this; + } + + @XmlAttribute(required = true) + @XmlSchemaType(name = "unsignedInt") + public Long getLength() { + return length; + } + + public PathTO setLength( + @Nonnull + Long length + ) { + requireNonNull(length, "length"); + this.length = length; + return this; + } + + @XmlAttribute(required = true) + @XmlSchemaType(name = "unsignedInt") + public Long getMaxVelocity() { + return maxVelocity; + } + + public PathTO setMaxVelocity( + @Nonnull + Long maxVelocity + ) { + requireNonNull(maxVelocity, "maxVelocity"); + this.maxVelocity = maxVelocity; + return this; + } + + @XmlAttribute(required = true) + @XmlSchemaType(name = "unsignedInt") + public Long getMaxReverseVelocity() { + return maxReverseVelocity; + } + + public PathTO setMaxReverseVelocity( + @Nonnull + Long maxReverseVelocity + ) { + requireNonNull(maxReverseVelocity, "maxReverseVelocity"); + this.maxReverseVelocity = maxReverseVelocity; + return this; + } + + @XmlElement(name = "peripheralOperation") + public List getPeripheralOperations() { + return peripheralOperations; + } + + public PathTO setPeripheralOperations(List peripheralOperations) { + this.peripheralOperations = requireNonNull(peripheralOperations, "peripheralOperations"); + return this; + } + + @XmlAttribute(required = true) + public Boolean isLocked() { + return locked; + } + + public PathTO setLocked(Boolean locked) { + this.locked = locked; + return this; + } + + @XmlElement(name = "vehicleEnvelope") + public List getVehicleEnvelopes() { + return vehicleEnvelopes; + } + + public PathTO setVehicleEnvelopes( + @Nonnull + List vehicleEnvelopes + ) { + this.vehicleEnvelopes = requireNonNull(vehicleEnvelopes, "vehicleEnvelopes"); + return this; + } + + @XmlElement(required = true) + public PathLayout getPathLayout() { + return pathLayout; + } + + public PathTO setPathLayout( + @Nonnull + PathLayout pathLayout + ) { + this.pathLayout = requireNonNull(pathLayout, "pathLayout"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType(propOrder = {"connectionType", "layerId", "controlPoints"}) + public static class PathLayout { + + private String connectionType = ""; + private Integer layerId = 0; + private List controlPoints = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public PathLayout() { + } + + @XmlAttribute(required = true) + public String getConnectionType() { + return connectionType; + } + + public PathLayout setConnectionType( + @Nonnull + String connectionType + ) { + this.connectionType = requireNonNull(connectionType, "connectionType"); + return this; + } + + @XmlAttribute(required = true) + public Integer getLayerId() { + return layerId; + } + + public PathLayout setLayerId( + @Nonnull + Integer layerId + ) { + this.layerId = requireNonNull(layerId, "layerId"); + return this; + } + + @XmlElement(name = "controlPoint") + public List getControlPoints() { + return controlPoints; + } + + public PathLayout setControlPoints( + @Nonnull + List controlPoints + ) { + this.controlPoints = requireNonNull(controlPoints, "controlPoints"); + return this; + } + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType(propOrder = {"x", "y"}) + public static class ControlPoint { + + private Long x = 0L; + private Long y = 0L; + + /** + * Creates a new instance. + */ + public ControlPoint() { + } + + @XmlAttribute(required = true) + public Long getX() { + return x; + } + + public ControlPoint setX( + @Nonnull + Long x + ) { + this.x = requireNonNull(x, "x"); + return this; + } + + @XmlAttribute(required = true) + public Long getY() { + return y; + } + + public ControlPoint setY( + @Nonnull + Long y + ) { + this.y = requireNonNull(y, "y"); + return this; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/PeripheralOperationTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/PeripheralOperationTO.java new file mode 100644 index 0000000..cd8cb5e --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/PeripheralOperationTO.java @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import static java.util.Objects.requireNonNull; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +public class PeripheralOperationTO + extends + PlantModelElementTO { + + private String locationName = ""; + private String executionTrigger = ""; + private boolean completionRequired; + + /** + * Creates a new instance. + */ + public PeripheralOperationTO() { + } + + @XmlAttribute(required = true) + public String getLocationName() { + return locationName; + } + + public PeripheralOperationTO setLocationName(String locationName) { + this.locationName = requireNonNull(locationName, "locationName"); + return this; + } + + @XmlAttribute(required = true) + public String getExecutionTrigger() { + return executionTrigger; + } + + public PeripheralOperationTO setExecutionTrigger(String executionTrigger) { + this.executionTrigger = requireNonNull(executionTrigger, "executionTrigger"); + return this; + } + + @XmlAttribute(required = true) + public boolean isCompletionRequired() { + return completionRequired; + } + + public PeripheralOperationTO setCompletionRequired(boolean completionRequired) { + this.completionRequired = completionRequired; + return this; + } + +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/PlantModelElementTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/PlantModelElementTO.java new file mode 100644 index 0000000..726b4b1 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/PlantModelElementTO.java @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlTransient; + +/** + */ +@XmlTransient +@XmlAccessorType(XmlAccessType.PROPERTY) +public class PlantModelElementTO { + + private String name = ""; + private List properties = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public PlantModelElementTO() { + } + + @XmlAttribute(required = true) + public String getName() { + return name; + } + + public PlantModelElementTO setName( + @Nonnull + String name + ) { + requireNonNull(name, "name"); + this.name = name; + return this; + } + + @XmlElement(name = "property") + public List getProperties() { + return properties; + } + + public PlantModelElementTO setProperties( + @Nonnull + List properties + ) { + requireNonNull(properties, "properties"); + this.properties = properties; + return this; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/PointTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/PointTO.java new file mode 100644 index 0000000..4327f08 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/PointTO.java @@ -0,0 +1,275 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType( + propOrder = {"name", "positionX", "positionY", "positionZ", "vehicleOrientationAngle", + "type", "maxVehicleBoundingBox", "vehicleEnvelopes", "outgoingPaths", "properties", + "pointLayout"} +) +public class PointTO + extends + PlantModelElementTO { + + private Long positionX = 0L; + private Long positionY = 0L; + private Long positionZ = 0L; + private Float vehicleOrientationAngle = 0.0F; + private String type = "HALT_POSITION"; + private BoundingBoxTO maxVehicleBoundingBox = new BoundingBoxTO(); + private List vehicleEnvelopes = new ArrayList<>(); + private List outgoingPaths = new ArrayList<>(); + private PointLayout pointLayout = new PointLayout(); + + /** + * Creates a new instance. + */ + public PointTO() { + } + + @XmlAttribute(required = true) + public Long getPositionX() { + return positionX; + } + + public PointTO setPositionX( + @Nonnull + Long positionX + ) { + requireNonNull(positionX, "positionX"); + this.positionX = positionX; + return this; + } + + @XmlAttribute(required = true) + public Long getPositionY() { + return positionY; + } + + public PointTO setPositionY( + @Nonnull + Long positionY + ) { + requireNonNull(positionY, "positionY"); + this.positionY = positionY; + return this; + } + + @XmlAttribute(required = true) + public Long getPositionZ() { + return positionZ; + } + + public PointTO setPositionZ( + @Nonnull + Long positionZ + ) { + requireNonNull(positionZ, "positionZ"); + this.positionZ = positionZ; + return this; + } + + @XmlAttribute + public Float getVehicleOrientationAngle() { + return vehicleOrientationAngle; + } + + public PointTO setVehicleOrientationAngle( + @Nonnull + Float vehicleOrientationAngle + ) { + requireNonNull(vehicleOrientationAngle, "vehicleOrientationAngle"); + this.vehicleOrientationAngle = vehicleOrientationAngle; + return this; + } + + @XmlAttribute(required = true) + public String getType() { + return type; + } + + public PointTO setType( + @Nonnull + String type + ) { + requireNonNull(type, "type"); + this.type = type; + return this; + } + + @XmlElement + @Nonnull + public BoundingBoxTO getMaxVehicleBoundingBox() { + return maxVehicleBoundingBox; + } + + public PointTO setMaxVehicleBoundingBox( + @Nonnull + BoundingBoxTO maxVehicleBoundingBox + ) { + this.maxVehicleBoundingBox = requireNonNull(maxVehicleBoundingBox, "maxVehicleBoundingBox"); + return this; + } + + @XmlElement(name = "outgoingPath") + public List getOutgoingPaths() { + return outgoingPaths; + } + + public PointTO setOutgoingPaths( + @Nonnull + List outgoingPath + ) { + requireNonNull(outgoingPath, "outgoingPath"); + this.outgoingPaths = outgoingPath; + return this; + } + + @XmlElement(name = "vehicleEnvelope") + public List getVehicleEnvelopes() { + return vehicleEnvelopes; + } + + public PointTO setVehicleEnvelopes( + @Nonnull + List vehicleEnvelopes + ) { + this.vehicleEnvelopes = requireNonNull(vehicleEnvelopes, "vehicleEnvelopes"); + return this; + } + + @XmlElement(required = true) + public PointLayout getPointLayout() { + return pointLayout; + } + + public PointTO setPointLayout( + @Nonnull + PointLayout pointLayout + ) { + this.pointLayout = requireNonNull(pointLayout, "pointLayout"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + public static class OutgoingPath { + + private String name = ""; + + /** + * Creates a new instance. + */ + public OutgoingPath() { + } + + @XmlAttribute(required = true) + public String getName() { + return name; + } + + public OutgoingPath setName( + @Nonnull + String name + ) { + requireNonNull(name, "name"); + this.name = name; + return this; + } + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType(propOrder = {"positionX", "positionY", "labelOffsetX", "labelOffsetY", "layerId"}) + public static class PointLayout { + + private Long positionX = 0L; + private Long positionY = 0L; + private Long labelOffsetX = 0L; + private Long labelOffsetY = 0L; + private Integer layerId = 0; + + /** + * Creates a new instance. + */ + public PointLayout() { + } + + @XmlAttribute(required = true) + public Long getPositionX() { + return positionX; + } + + public PointLayout setPositionX( + @Nonnull + Long positionX + ) { + this.positionX = requireNonNull(positionX, "positionX"); + return this; + } + + @XmlAttribute(required = true) + public Long getPositionY() { + return positionY; + } + + public PointLayout setPositionY( + @Nonnull + Long positionY + ) { + this.positionY = requireNonNull(positionY, "positionY"); + return this; + } + + @XmlAttribute(required = true) + public Long getLabelOffsetX() { + return labelOffsetX; + } + + public PointLayout setLabelOffsetX( + @Nonnull + Long labelOffsetX + ) { + this.labelOffsetX = requireNonNull(labelOffsetX, "labelOffsetX"); + return this; + } + + @XmlAttribute(required = true) + public Long getLabelOffsetY() { + return labelOffsetY; + } + + public PointLayout setLabelOffsetY( + @Nonnull + Long labelOffsetY + ) { + this.labelOffsetY = requireNonNull(labelOffsetY, "labelOffsetY"); + return this; + } + + @XmlAttribute(required = true) + public Integer getLayerId() { + return layerId; + } + + public PointLayout setLayerId( + @Nonnull + Integer layerId + ) { + this.layerId = requireNonNull(layerId, "layerId"); + return this; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/PropertyTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/PropertyTO.java new file mode 100644 index 0000000..3472c5e --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/PropertyTO.java @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(propOrder = {"name", "value"}) +public class PropertyTO { + + private String name = ""; + private String value = ""; + + /** + * Creates a new instance. + */ + public PropertyTO() { + } + + @XmlAttribute(required = true) + public String getName() { + return name; + } + + public PropertyTO setName( + @Nonnull + String name + ) { + this.name = requireNonNull(name, "name"); + return this; + } + + @XmlAttribute(required = true) + public String getValue() { + return value; + } + + public PropertyTO setValue( + @Nonnull + String value + ) { + requireNonNull(value, "value"); + this.value = value; + return this; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/V6ModelParser.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/V6ModelParser.java new file mode 100644 index 0000000..d5a8674 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/V6ModelParser.java @@ -0,0 +1,407 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.data.ObjectPropConstants.LOCTYPE_DEFAULT_REPRESENTATION; +import static org.opentcs.data.ObjectPropConstants.LOC_DEFAULT_REPRESENTATION; + +import jakarta.annotation.Nonnull; +import java.io.IOException; +import java.io.Reader; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.data.model.visualization.LocationRepresentation; +import org.opentcs.util.persistence.v005.V005ModelParser; +import org.opentcs.util.persistence.v005.V005PlantModelTO; +import org.semver4j.Semver; +import org.semver4j.SemverException; + +/** + * The parser for V6 models. + */ +public class V6ModelParser { + + /** + * The maximum supported schema version for model files. + */ + private static final Semver V6_SUPPORTED_VERSION = new Semver(V6PlantModelTO.VERSION_STRING); + + /** + * Creates a new instance. + */ + public V6ModelParser() { + } + + /** + * Reads a model with the given reader and parses it to a {@link PlantModelCreationTO} instance. + * + * @param reader The reader to use. + * @param modelVersion The model version. + * @return The parsed {@link PlantModelCreationTO}. + * @throws IOException If there was an error reading the model. + */ + public PlantModelCreationTO read(Reader reader, String modelVersion) + throws IOException { + return new V6TOMapper().map(readRaw(reader, modelVersion)); + } + + /** + * Reads a model with the given reader and parses it to a {@link V6PlantModelTO} instance. + * + * @param reader The reader to use. + * @param modelVersion The model version. + * @return The parsed {@link V6PlantModelTO}. + * @throws IOException If there was an error reading the model. + */ + public V6PlantModelTO readRaw( + @Nonnull + Reader reader, + @Nonnull + String modelVersion + ) + throws IOException { + requireNonNull(reader, "reader"); + requireNonNull(modelVersion, "modelVersion"); + + Semver fileVersionNumber; + try { + fileVersionNumber = new Semver(modelVersion); + } + catch (SemverException e) { + throw new IOException(e); + } + + if (fileVersionNumber.getMajor() == V6_SUPPORTED_VERSION.getMajor() + && fileVersionNumber.isLowerThanOrEqualTo(V6_SUPPORTED_VERSION)) { + return V6PlantModelTO.fromXml(reader); + } + else { + return convert(new V005ModelParser().readRaw(reader, modelVersion)); + } + } + + private V6PlantModelTO convert(V005PlantModelTO to) { + return new V6PlantModelTO() + .setName(to.getName()) + .setPoints(convertPoints(to)) + .setPaths(convertPaths(to)) + .setVehicles(convertVehicles(to)) + .setLocationTypes(convertLocationTypes(to)) + .setLocations(convertLocations(to)) + .setBlocks(convertBlocks(to)) + .setVisualLayout(convertVisualLayout(to)) + .setProperties(convertProperties(to.getProperties())); + } + + private List convertProperties( + List tos + ) { + return tos.stream() + .map(property -> new PropertyTO().setName(property.getName()).setValue(property.getValue())) + .toList(); + } + + private List convertPoints(V005PlantModelTO to) { + return to.getPoints().stream() + .map(point -> { + PointTO result = new PointTO(); + result.setName(point.getName()) + .setProperties(convertProperties(point.getProperties())); + result.setPositionX(point.getxPosition()) + .setPositionY(point.getyPosition()) + .setPositionZ(point.getzPosition()) + .setVehicleOrientationAngle(point.getVehicleOrientationAngle()) + .setType(point.getType()) + .setVehicleEnvelopes(convertVehicleEnvelopes(point.getVehicleEnvelopes())) + .setOutgoingPaths(convertOutgoingPaths(point)) + .setMaxVehicleBoundingBox( + new BoundingBoxTO() + .setLength(1000) + .setWidth(1000) + .setHeight(1000) + .setReferenceOffsetX(0) + .setReferenceOffsetY(0) + ) + .setPointLayout( + new PointTO.PointLayout() + .setPositionX(point.getPointLayout().getxPosition()) + .setPositionY(point.getPointLayout().getyPosition()) + .setLabelOffsetX(point.getPointLayout().getxLabelOffset()) + .setLabelOffsetY(point.getPointLayout().getyLabelOffset()) + .setLayerId(point.getPointLayout().getLayerId()) + ); + return result; + }) + .toList(); + } + + private List convertVehicleEnvelopes( + List tos + ) { + return tos.stream() + .map( + vehicleEnvelope -> new VehicleEnvelopeTO() + .setKey(vehicleEnvelope.getKey()) + .setVertices( + vehicleEnvelope.getVertices().stream() + .map( + couple -> new CoupleTO() + .setX(couple.getX()) + .setY(couple.getY()) + ) + .toList() + ) + ) + .toList(); + } + + private Map toPropertiesMap( + List properties + ) { + Map result = new HashMap<>(); + for (org.opentcs.util.persistence.v005.PropertyTO property : properties) { + result.put(property.getName(), property.getValue()); + } + return result; + } + + private List convertOutgoingPaths( + org.opentcs.util.persistence.v005.PointTO to + ) { + return to.getOutgoingPaths().stream() + .map(path -> new PointTO.OutgoingPath().setName(path.getName())) + .toList(); + } + + private List convertPaths(V005PlantModelTO to) { + return to.getPaths().stream() + .map(path -> { + PathTO result = new PathTO(); + result.setName(path.getName()) + .setProperties(convertProperties(path.getProperties())); + result.setSourcePoint(path.getSourcePoint()) + .setDestinationPoint(path.getDestinationPoint()) + .setLength(path.getLength()) + .setMaxVelocity(path.getMaxVelocity()) + .setMaxReverseVelocity(path.getMaxReverseVelocity()) + .setPeripheralOperations(convertPeripheralOperations(path.getPeripheralOperations())) + .setLocked(path.isLocked()) + .setVehicleEnvelopes(convertVehicleEnvelopes(path.getVehicleEnvelopes())) + .setPathLayout( + new PathTO.PathLayout() + .setConnectionType(path.getPathLayout().getConnectionType()) + .setControlPoints( + path.getPathLayout().getControlPoints().stream() + .map( + controlPoint -> new PathTO.ControlPoint() + .setX(controlPoint.getX()) + .setY(controlPoint.getY()) + ) + .toList() + ) + .setLayerId(path.getPathLayout().getLayerId()) + ); + return result; + }) + .toList(); + } + + private List convertPeripheralOperations( + List tos + ) { + return tos.stream() + .map( + peripheralOperation -> { + PeripheralOperationTO result = new PeripheralOperationTO(); + result.setName(peripheralOperation.getName()) + .setProperties(convertProperties(peripheralOperation.getProperties())); + result.setLocationName(peripheralOperation.getLocationName()) + .setExecutionTrigger(peripheralOperation.getExecutionTrigger()) + .setCompletionRequired(peripheralOperation.isCompletionRequired()); + return result; + } + ) + .toList(); + } + + private List convertVehicles(V005PlantModelTO to) { + return to.getVehicles().stream() + .map(vehicle -> { + VehicleTO result = new VehicleTO(); + result.setName(vehicle.getName()) + .setProperties(convertProperties(vehicle.getProperties())); + result.setEnergyLevelCritical(vehicle.getEnergyLevelCritical()) + .setEnergyLevelGood(vehicle.getEnergyLevelGood()) + .setEnergyLevelFullyRecharged(vehicle.getEnergyLevelFullyRecharged()) + .setEnergyLevelSufficientlyRecharged(vehicle.getEnergyLevelSufficientlyRecharged()) + .setMaxVelocity(vehicle.getMaxVelocity()) + .setMaxReverseVelocity(vehicle.getMaxReverseVelocity()) + .setEnvelopeKey(vehicle.getEnvelopeKey()) + .setBoundingBox( + new BoundingBoxTO() + .setLength(vehicle.getLength()) + .setWidth(1000) + .setHeight(1000) + ) + .setVehicleLayout( + new VehicleTO.VehicleLayout() + .setColor(vehicle.getVehicleLayout().getColor()) + ); + return result; + }) + .toList(); + } + + private List convertLocationTypes(V005PlantModelTO to) { + return to.getLocationTypes().stream() + .map(locationType -> { + String locationRepresentation = toPropertiesMap(locationType.getProperties()) + .getOrDefault(LOCTYPE_DEFAULT_REPRESENTATION, LocationRepresentation.NONE.name()); + + LocationTypeTO result = new LocationTypeTO(); + result.setName(locationType.getName()) + .setProperties(convertProperties(locationType.getProperties())); + result.setAllowedOperations(convertAllowedOperations(locationType.getAllowedOperations())) + .setAllowedPeripheralOperations( + convertAllowedPeripheralOperations(locationType.getAllowedPeripheralOperations()) + ) + .setLocationTypeLayout( + new LocationTypeTO.LocationTypeLayout() + .setLocationRepresentation(locationRepresentation) + ); + return result; + }) + .toList(); + } + + private List convertAllowedOperations( + List tos + ) { + return tos.stream() + .map(allowedOperation -> { + AllowedOperationTO result = new AllowedOperationTO(); + result.setName(allowedOperation.getName()); + result.setProperties(convertProperties(allowedOperation.getProperties())); + return result; + }) + .toList(); + } + + private List convertAllowedPeripheralOperations( + List tos + ) { + return tos.stream() + .map( + allowedPeripheralOperation -> { + AllowedPeripheralOperationTO result = new AllowedPeripheralOperationTO(); + result.setName(allowedPeripheralOperation.getName()); + result.setProperties(convertProperties(allowedPeripheralOperation.getProperties())); + return result; + } + ) + .toList(); + } + + private List convertLocations(V005PlantModelTO to) { + return to.getLocations().stream() + .map(location -> { + String locationRepresentation = toPropertiesMap(location.getProperties()) + .getOrDefault(LOC_DEFAULT_REPRESENTATION, LocationRepresentation.DEFAULT.name()); + + LocationTO result = new LocationTO(); + result.setName(location.getName()) + .setProperties(convertProperties(location.getProperties())); + result.setPositionX(location.getxPosition()) + .setPositionY(location.getyPosition()) + .setPositionZ(location.getzPosition()) + .setType(location.getType()) + .setLinks(convertLinks(location)) + .setLocked(location.isLocked()) + .setLocationLayout( + new LocationTO.LocationLayout() + .setPositionX(location.getLocationLayout().getxPosition()) + .setPositionY(location.getLocationLayout().getyPosition()) + .setLabelOffsetX(location.getLocationLayout().getxLabelOffset()) + .setLabelOffsetY(location.getLocationLayout().getyLabelOffset()) + .setLocationRepresentation(locationRepresentation) + .setLayerId(location.getLocationLayout().getLayerId()) + ); + return result; + }) + .toList(); + } + + private List convertLinks(org.opentcs.util.persistence.v005.LocationTO to) { + return to.getLinks().stream() + .map(link -> { + return new LocationTO.Link() + .setPoint(link.getPoint()) + .setAllowedOperations(convertAllowedOperations(link.getAllowedOperations())); + }) + .toList(); + } + + private List convertBlocks(V005PlantModelTO to) { + return to.getBlocks().stream() + .map(block -> { + BlockTO result = new BlockTO(); + result.setName(block.getName()) + .setProperties(convertProperties(block.getProperties())); + result.setType(block.getType()) + .setMembers(convertMembers(block.getMembers())) + .setBlockLayout( + new BlockTO.BlockLayout() + .setColor(block.getBlockLayout().getColor()) + ); + return result; + }) + .toList(); + } + + private List convertMembers(List tos) { + return tos.stream() + .map(member -> { + MemberTO result = new MemberTO(); + result.setName(member.getName()) + .setProperties(convertProperties(member.getProperties())); + return result; + }) + .toList(); + } + + private VisualLayoutTO convertVisualLayout(V005PlantModelTO to) { + VisualLayoutTO result = new VisualLayoutTO() + .setScaleX(to.getVisualLayout().getScaleX()) + .setScaleY(to.getVisualLayout().getScaleY()) + .setLayers( + to.getVisualLayout().getLayers().stream() + .map( + layer -> new VisualLayoutTO.Layer() + .setId(layer.getId()) + .setOrdinal(layer.getOrdinal()) + .setVisible(layer.isVisible()) + .setName(layer.getName()) + .setGroupId(layer.getGroupId()) + ) + .toList() + ) + .setLayerGroups( + to.getVisualLayout().getLayerGroups().stream() + .map( + layerGroup -> new VisualLayoutTO.LayerGroup() + .setId(layerGroup.getId()) + .setName(layerGroup.getName()) + .setVisible(layerGroup.isVisible()) + ) + .toList() + ); + result + .setProperties(convertProperties(to.getVisualLayout().getProperties())) + .setName(to.getVisualLayout().getName()); + + return result; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/V6PlantModelTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/V6PlantModelTO.java new file mode 100644 index 0000000..24a84fb --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/V6PlantModelTO.java @@ -0,0 +1,260 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import javax.xml.XMLConstants; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import org.opentcs.util.persistence.BasePlantModelTO; +import org.xml.sax.SAXException; + +/** + */ +@XmlRootElement(name = "model") +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType( + propOrder = {"version", "name", "points", "paths", "vehicles", "locationTypes", + "locations", "blocks", "visualLayout", "properties"} +) +public class V6PlantModelTO + extends + BasePlantModelTO { + + /** + * This plant model implementation's version string. + */ + public static final String VERSION_STRING = "6.0.0"; + + private String name = ""; + private List points = new ArrayList<>(); + private List paths = new ArrayList<>(); + private List vehicles = new ArrayList<>(); + private List locationTypes = new ArrayList<>(); + private List locations = new ArrayList<>(); + private List blocks = new ArrayList<>(); + private VisualLayoutTO visualLayout = new VisualLayoutTO(); + private List properties = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public V6PlantModelTO() { + } + + @XmlAttribute(required = true) + public String getName() { + return name; + } + + public V6PlantModelTO setName( + @Nonnull + String name + ) { + requireNonNull(name, "name"); + this.name = name; + return this; + } + + @XmlElement(name = "point") + public List getPoints() { + return points; + } + + public V6PlantModelTO setPoints( + @Nonnull + List points + ) { + requireNonNull(points, "points"); + this.points = points; + return this; + } + + @XmlElement(name = "path") + public List getPaths() { + return paths; + } + + public V6PlantModelTO setPaths( + @Nonnull + List paths + ) { + requireNonNull(paths, "paths"); + this.paths = paths; + return this; + } + + @XmlElement(name = "vehicle") + public List getVehicles() { + return vehicles; + } + + public V6PlantModelTO setVehicles( + @Nonnull + List vehicles + ) { + requireNonNull(vehicles, "vehicles"); + this.vehicles = vehicles; + return this; + } + + @XmlElement(name = "locationType") + public List getLocationTypes() { + return locationTypes; + } + + public V6PlantModelTO setLocationTypes( + @Nonnull + List locationTypes + ) { + requireNonNull(locationTypes, "locationTypes"); + this.locationTypes = locationTypes; + return this; + } + + @XmlElement(name = "location") + public List getLocations() { + return locations; + } + + public V6PlantModelTO setLocations( + @Nonnull + List locations + ) { + requireNonNull(locations, "locations"); + this.locations = locations; + return this; + } + + @XmlElement(name = "block") + public List getBlocks() { + return blocks; + } + + public V6PlantModelTO setBlocks( + @Nonnull + List blocks + ) { + requireNonNull(blocks, "blocks"); + this.blocks = blocks; + return this; + } + + @XmlElement + public VisualLayoutTO getVisualLayout() { + return visualLayout; + } + + public V6PlantModelTO setVisualLayout( + @Nonnull + VisualLayoutTO visualLayout + ) { + this.visualLayout = requireNonNull(visualLayout, "visualLayout"); + return this; + } + + @XmlElement(name = "property") + public List getProperties() { + return properties; + } + + public V6PlantModelTO setProperties( + @Nonnull + List properties + ) { + requireNonNull(properties, "properties"); + this.properties = properties; + return this; + } + + /** + * Marshals this instance to its XML representation and writes it to the given writer. + * + * @param writer The writer to write this instance's XML representation to. + * @throws IOException If there was a problem marshalling this instance. + */ + public void toXml( + @Nonnull + Writer writer + ) + throws IOException { + requireNonNull(writer, "writer"); + + try { + createMarshaller().marshal(this, writer); + } + catch (JAXBException | SAXException exc) { + throw new IOException("Exception marshalling data", exc); + } + } + + /** + * Unmarshals an instance of this class from the given XML representation. + * + * @param reader Provides the XML representation to parse to an instance. + * @return The instance unmarshalled from the given reader. + * @throws IOException If there was a problem unmarshalling the given string. + */ + public static V6PlantModelTO fromXml( + @Nonnull + Reader reader + ) + throws IOException { + requireNonNull(reader, "reader"); + + try { + return (V6PlantModelTO) createUnmarshaller().unmarshal(reader); + } + catch (JAXBException | SAXException exc) { + throw new IOException("Exception unmarshalling data", exc); + } + } + + private static Marshaller createMarshaller() + throws JAXBException, + SAXException { + Marshaller marshaller = createContext().createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + marshaller.setSchema(createSchema()); + return marshaller; + } + + private static Unmarshaller createUnmarshaller() + throws JAXBException, + SAXException { + Unmarshaller unmarshaller = createContext().createUnmarshaller(); + unmarshaller.setSchema(createSchema()); + return unmarshaller; + } + + private static JAXBContext createContext() + throws JAXBException { + return JAXBContext.newInstance(V6PlantModelTO.class); + } + + private static Schema createSchema() + throws SAXException { + URL schemaUrl + = V6PlantModelTO.class.getResource("/org/opentcs/util/persistence/model-6.0.0.xsd"); + SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + return schemaFactory.newSchema(schemaUrl); + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/V6TOMapper.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/V6TOMapper.java new file mode 100644 index 0000000..8eb3a5b --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/V6TOMapper.java @@ -0,0 +1,772 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.access.to.model.BlockCreationTO; +import org.opentcs.access.to.model.BoundingBoxCreationTO; +import org.opentcs.access.to.model.CoupleCreationTO; +import org.opentcs.access.to.model.LocationCreationTO; +import org.opentcs.access.to.model.LocationTypeCreationTO; +import org.opentcs.access.to.model.PathCreationTO; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.access.to.model.PointCreationTO; +import org.opentcs.access.to.model.VehicleCreationTO; +import org.opentcs.access.to.model.VisualLayoutCreationTO; +import org.opentcs.access.to.peripherals.PeripheralOperationCreationTO; +import org.opentcs.data.model.Block; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Envelope; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Pose; +import org.opentcs.data.model.Triple; +import org.opentcs.data.model.visualization.Layer; +import org.opentcs.data.model.visualization.LayerGroup; +import org.opentcs.data.model.visualization.LocationRepresentation; +import org.opentcs.data.peripherals.PeripheralOperation; +import org.opentcs.util.Colors; + +/** + * Provides methods for mapping {@link PlantModelCreationTO} to {@link V6PlantModelTO} and + * vice versa. + */ +public class V6TOMapper { + + /** + * Creates a new instance. + */ + public V6TOMapper() { + } + + /** + * Maps the given model to a {@link PlantModelCreationTO} instance. + * + * @param model The model to map. + * @return The mapped {@link PlantModelCreationTO} instance. + */ + public PlantModelCreationTO map(V6PlantModelTO model) { + return new PlantModelCreationTO(model.getName()) + .withPoints(toPointCreationTO(model.getPoints())) + .withVehicles(toVehicleCreationTO(model.getVehicles())) + .withPaths(toPathCreationTO(model.getPaths())) + .withLocationTypes(toLocationTypeCreationTO(model.getLocationTypes())) + .withLocations(toLocationCreationTO(model.getLocations())) + .withBlocks(toBlockCreationTO(model.getBlocks())) + .withVisualLayout(toVisualLayoutCreationTO(model.getVisualLayout())) + .withProperties(convertProperties(model.getProperties())); + } + + /** + * Maps the given model to a {@link V6PlantModelTO} instance. + * + * @param model The model to map. + * @return The mapped {@link V6PlantModelTO} instance. + */ + public V6PlantModelTO map(PlantModelCreationTO model) { + V6PlantModelTO result = new V6PlantModelTO(); + + result.setName(model.getName()); + result.setVersion(V6PlantModelTO.VERSION_STRING); + result.getPoints().addAll(toPointTO(model.getPoints(), model.getPaths())); + result.getVehicles().addAll(toVehicleTO(model.getVehicles())); + result.getPaths().addAll(toPathTO(model.getPaths())); + result.getLocationTypes().addAll(toLocationTypeTO(model.getLocationTypes())); + result.getLocations().addAll(toLocationTO(model.getLocations())); + result.getBlocks().addAll(toBlockTO(model.getBlocks())); + result.setVisualLayout(toVisualLayoutTO(model.getVisualLayout())); + result.getProperties().addAll(convertProperties(model.getProperties())); + + return result; + } + + //Methods for mapping from PlantModelElementTO to CreationTO start here. + private List toPointCreationTO(List points) { + List result = new ArrayList<>(); + + for (PointTO point : points) { + result.add( + new PointCreationTO(point.getName()) + .withPose( + new Pose( + new Triple(point.getPositionX(), point.getPositionY(), point.getPositionZ()), + point.getVehicleOrientationAngle().doubleValue() + ) + ) + .withType(Point.Type.valueOf(point.getType())) + .withVehicleEnvelopes(toEnvelopeMap(point.getVehicleEnvelopes())) + .withMaxVehicleBoundingBox(toBoundingBoxCreationTO(point.getMaxVehicleBoundingBox())) + .withProperties(convertProperties(point.getProperties())) + .withLayout( + new PointCreationTO.Layout( + new Couple( + point.getPointLayout().getPositionX(), + point.getPointLayout().getPositionY() + ), + new Couple( + point.getPointLayout().getLabelOffsetX(), + point.getPointLayout().getLabelOffsetY() + ), + point.getPointLayout().getLayerId() + ) + ) + ); + } + + return result; + } + + private List toVehicleCreationTO(List vehicles) { + List result = new ArrayList<>(); + + for (VehicleTO vehicle : vehicles) { + result.add( + new VehicleCreationTO(vehicle.getName()) + .withBoundingBox(toBoundingBoxCreationTO(vehicle.getBoundingBox())) + .withEnergyLevelThresholdSet(toEnergyLevelThresholdSetCreationTO(vehicle)) + .withMaxReverseVelocity(vehicle.getMaxReverseVelocity()) + .withMaxVelocity(vehicle.getMaxVelocity()) + .withEnvelopeKey(vehicle.getEnvelopeKey()) + .withProperties(convertProperties(vehicle.getProperties())) + .withLayout( + new VehicleCreationTO.Layout( + Colors.decodeFromHexRGB(vehicle.getVehicleLayout().getColor()) + ) + ) + ); + } + + return result; + } + + private List toPathCreationTO(List paths) { + List result = new ArrayList<>(); + + for (PathTO path : paths) { + result.add( + new PathCreationTO( + path.getName(), + path.getSourcePoint(), + path.getDestinationPoint() + ) + .withLength(path.getLength()) + .withLocked(path.isLocked()) + .withMaxVelocity(path.getMaxVelocity().intValue()) + .withMaxReverseVelocity(path.getMaxReverseVelocity().intValue()) + .withPeripheralOperations( + toPeripheralOperationCreationTOs(path.getPeripheralOperations()) + ) + .withVehicleEnvelopes(toEnvelopeMap(path.getVehicleEnvelopes())) + .withProperties(convertProperties(path.getProperties())) + .withLayout( + new PathCreationTO.Layout( + Path.Layout.ConnectionType.valueOf(path.getPathLayout().getConnectionType()), + path.getPathLayout().getControlPoints().stream() + .map(controlPoint -> new Couple(controlPoint.getX(), controlPoint.getY())) + .toList(), + path.getPathLayout().getLayerId() + ) + ) + ); + } + + return result; + } + + private List toPeripheralOperationCreationTOs( + List tos + ) { + return tos.stream() + .map( + to -> new PeripheralOperationCreationTO(to.getName(), to.getLocationName()) + .withExecutionTrigger( + PeripheralOperation.ExecutionTrigger.valueOf(to.getExecutionTrigger()) + ) + .withCompletionRequired(to.isCompletionRequired()) + ) + .toList(); + } + + private List toLocationTypeCreationTO( + List locationTypes + ) { + List result = new ArrayList<>(); + + for (LocationTypeTO locationType : locationTypes) { + result.add( + new LocationTypeCreationTO(locationType.getName()) + .withAllowedOperations(getOperationNames(locationType.getAllowedOperations())) + .withAllowedPeripheralOperations( + getPeripheralOperationNames( + locationType.getAllowedPeripheralOperations() + ) + ) + .withProperties(convertProperties(locationType.getProperties())) + .withLayout( + new LocationTypeCreationTO.Layout( + LocationRepresentation.valueOf( + locationType.getLocationTypeLayout().getLocationRepresentation() + ) + ) + ) + ); + } + + return result; + } + + private List toLocationCreationTO(List locations) { + List result = new ArrayList<>(); + + for (LocationTO location : locations) { + result.add( + new LocationCreationTO( + location.getName(), + location.getType(), + new Triple( + location.getPositionX(), + location.getPositionY(), + location.getPositionZ() + ) + ) + .withLinks(getLinks(location)) + .withLocked(location.isLocked()) + .withProperties(convertProperties(location.getProperties())) + .withLayout( + new LocationCreationTO.Layout( + new Couple( + location.getLocationLayout().getPositionX(), + location.getLocationLayout().getPositionY() + ), + new Couple( + location.getLocationLayout().getLabelOffsetX(), + location.getLocationLayout().getLabelOffsetY() + ), + LocationRepresentation.valueOf( + location.getLocationLayout().getLocationRepresentation() + ), + location.getLocationLayout().getLayerId() + ) + ) + ); + } + + return result; + } + + private List toBlockCreationTO(List blocks) { + List result = new ArrayList<>(); + + for (BlockTO block : blocks) { + result.add( + new BlockCreationTO(block.getName()) + .withType(Block.Type.valueOf(block.getType())) + .withMemberNames( + block.getMembers().stream() + .map(member -> member.getName()) + .collect(Collectors.toSet()) + ) + .withProperties(convertProperties(block.getProperties())) + .withLayout( + new BlockCreationTO.Layout( + Colors.decodeFromHexRGB(block.getBlockLayout().getColor()) + ) + ) + ); + } + + return result; + } + + private VisualLayoutCreationTO toVisualLayoutCreationTO(VisualLayoutTO visualLayout) { + return new VisualLayoutCreationTO(visualLayout.getName()) + .withScaleX(visualLayout.getScaleX()) + .withScaleY(visualLayout.getScaleY()) + .withLayers(convertLayers(visualLayout.getLayers())) + .withLayerGroups(convertLayerGroups(visualLayout.getLayerGroups())) + .withProperties(convertProperties(visualLayout.getProperties())); + } + + private List convertLayers(List layers) { + List result = new ArrayList<>(); + + for (VisualLayoutTO.Layer layer : layers) { + result.add( + new Layer( + layer.getId(), + layer.getOrdinal(), + layer.isVisible(), + layer.getName(), + layer.getGroupId() + ) + ); + } + + return result; + } + + private List convertLayerGroups(List layerGroups) { + List result = new ArrayList<>(); + + for (VisualLayoutTO.LayerGroup layerGroup : layerGroups) { + result.add( + new LayerGroup( + layerGroup.getId(), + layerGroup.getName(), + layerGroup.isVisible() + ) + ); + } + + return result; + } + + private Map convertProperties(List propsList) { + Map result = new HashMap<>(); + for (PropertyTO property : propsList) { + String propName + = isNullOrEmpty(property.getName()) ? "Property unknown" : property.getName(); + String propValue + = isNullOrEmpty(property.getValue()) ? "Value unknown" : property.getValue(); + + result.put(propName, propValue); + } + + return result; + } + + private List getOperationNames(List ops) { + List result = new ArrayList<>(ops.size()); + for (AllowedOperationTO operation : ops) { + result.add(operation.getName()); + } + return result; + } + + private List getPeripheralOperationNames(List ops) { + List result = new ArrayList<>(ops.size()); + for (AllowedPeripheralOperationTO operation : ops) { + result.add(operation.getName()); + } + return result; + } + + private Map> getLinks(LocationTO to) { + Map> result = new HashMap<>(); + for (LocationTO.Link linkTO : to.getLinks()) { + result.put( + linkTO.getPoint(), + new HashSet<>(getOperationNames(linkTO.getAllowedOperations())) + ); + } + + return result; + } + + //Methods for mapping from CreationTO to PlantModelElementTO start here. + private List toPointTO(List points, List paths) { + List result = new ArrayList<>(); + + for (PointCreationTO point : points) { + PointTO pointTO = new PointTO(); + pointTO.setName(point.getName()); + pointTO.setPositionX(point.getPose().getPosition().getX()) + .setPositionY(point.getPose().getPosition().getY()) + .setVehicleOrientationAngle((float) point.getPose().getOrientationAngle()) + .setType(point.getType().name()) + .setOutgoingPaths(getOutgoingPaths(point, paths)) + .setVehicleEnvelopes(toVehicleEnvelopeTOs(point.getVehicleEnvelopes())) + .setMaxVehicleBoundingBox(toBoundingBoxTO(point.getMaxVehicleBoundingBox())) + .setPointLayout( + new PointTO.PointLayout() + .setPositionX(point.getLayout().getPosition().getX()) + .setPositionY(point.getLayout().getPosition().getY()) + .setLabelOffsetX(point.getLayout().getLabelOffset().getX()) + .setLabelOffsetY(point.getLayout().getLabelOffset().getY()) + .setLayerId(point.getLayout().getLayerId()) + ) + .setProperties(convertProperties(point.getProperties())); + + result.add(pointTO); + } + + Collections.sort(result, Comparators.elementsByName()); + + return result; + } + + private List toVehicleTO(List vehicles) { + List result = new ArrayList<>(); + + for (VehicleCreationTO vehicle : vehicles) { + VehicleTO vehicleTO = new VehicleTO(); + vehicleTO.setName(vehicle.getName()); + vehicleTO.setBoundingBox(toBoundingBoxTO(vehicle.getBoundingBox())) + .setMaxVelocity(vehicle.getMaxVelocity()) + .setMaxReverseVelocity(vehicle.getMaxReverseVelocity()) + .setEnergyLevelGood((long) vehicle.getEnergyLevelThresholdSet().getEnergyLevelGood()) + .setEnergyLevelCritical( + (long) vehicle.getEnergyLevelThresholdSet().getEnergyLevelCritical() + ) + .setEnergyLevelFullyRecharged( + (long) vehicle.getEnergyLevelThresholdSet().getEnergyLevelFullyRecharged() + ) + .setEnergyLevelSufficientlyRecharged( + (long) vehicle.getEnergyLevelThresholdSet().getEnergyLevelSufficientlyRecharged() + ) + .setEnvelopeKey(vehicle.getEnvelopeKey()) + .setVehicleLayout( + new VehicleTO.VehicleLayout() + .setColor(Colors.encodeToHexRGB(vehicle.getLayout().getRouteColor())) + ) + .setProperties(convertProperties(vehicle.getProperties())); + + result.add(vehicleTO); + } + + Collections.sort(result, Comparators.elementsByName()); + + return result; + } + + private List toPathTO(List paths) { + List result = new ArrayList<>(); + + for (PathCreationTO path : paths) { + PathTO pathTO = new PathTO(); + pathTO.setName(path.getName()); + pathTO.setSourcePoint(path.getSrcPointName()) + .setDestinationPoint(path.getDestPointName()) + .setLength(path.getLength()) + .setMaxVelocity((long) path.getMaxVelocity()) + .setMaxReverseVelocity((long) path.getMaxReverseVelocity()) + .setPeripheralOperations(toPeripheralOperationTOs(path.getPeripheralOperations())) + .setLocked(path.isLocked()) + .setVehicleEnvelopes(toVehicleEnvelopeTOs(path.getVehicleEnvelopes())) + .setPathLayout( + new PathTO.PathLayout() + .setConnectionType(path.getLayout().getConnectionType().name()) + .setControlPoints( + path.getLayout().getControlPoints().stream() + .map(controlPoint -> { + return new PathTO.ControlPoint() + .setX(controlPoint.getX()) + .setY(controlPoint.getY()); + }) + .toList() + ) + .setLayerId(path.getLayout().getLayerId()) + ) + .setProperties(convertProperties(path.getProperties())); + + result.add(pathTO); + } + + Collections.sort(result, Comparators.elementsByName()); + + return result; + } + + private List toPeripheralOperationTOs( + List tos + ) { + return tos.stream() + .map( + to -> (PeripheralOperationTO) new PeripheralOperationTO() + .setLocationName(to.getLocationName()) + .setExecutionTrigger(to.getExecutionTrigger().name()) + .setCompletionRequired(to.isCompletionRequired()) + .setName(to.getOperation()) + ) + .toList(); + } + + private List toLocationTypeTO( + List locationTypes + ) { + List result = new ArrayList<>(); + + for (LocationTypeCreationTO locationType : locationTypes) { + LocationTypeTO locationTypeTO = new LocationTypeTO(); + locationTypeTO.setName(locationType.getName()); + locationTypeTO.setAllowedOperations( + toAllowedOperationTOs(locationType.getAllowedOperations()) + ) + .setAllowedPeripheralOperations( + toAllowedPeripheralOperationTOs(locationType.getAllowedPeripheralOperations()) + ) + .setLocationTypeLayout( + new LocationTypeTO.LocationTypeLayout() + .setLocationRepresentation( + locationType.getLayout().getLocationRepresentation().name() + ) + ) + .setProperties(convertProperties(locationType.getProperties())); + + result.add(locationTypeTO); + } + + Collections.sort(result, Comparators.elementsByName()); + + return result; + } + + private List toLocationTO(List locations) { + List result = new ArrayList<>(); + + for (LocationCreationTO location : locations) { + LocationTO locationTO = new LocationTO(); + locationTO.setName(location.getName()); + locationTO.setPositionX(location.getPosition().getX()) + .setPositionY(location.getPosition().getY()) + .setType(location.getTypeName()) + .setLinks(toLocationTOLinks(location.getLinks())) + .setLocked(location.isLocked()) + .setLocationLayout( + new LocationTO.LocationLayout() + .setPositionX(location.getLayout().getPosition().getX()) + .setPositionY(location.getLayout().getPosition().getY()) + .setLabelOffsetX(location.getLayout().getLabelOffset().getX()) + .setLabelOffsetY(location.getLayout().getLabelOffset().getY()) + .setLocationRepresentation( + location.getLayout().getLocationRepresentation().name() + ) + .setLayerId(location.getLayout().getLayerId()) + ) + .setProperties(convertProperties(location.getProperties())); + + result.add(locationTO); + } + + Collections.sort(result, Comparators.elementsByName()); + + return result; + } + + private List toBlockTO(List blocks) { + List result = new ArrayList<>(); + + for (BlockCreationTO block : blocks) { + BlockTO blockTO = new BlockTO(); + blockTO.setName(block.getName()); + blockTO.setType(block.getType().name()) + .setMembers(toMemberTOs(block.getMemberNames())) + .setBlockLayout( + new BlockTO.BlockLayout() + .setColor(Colors.encodeToHexRGB(block.getLayout().getColor())) + ) + .setProperties(convertProperties(block.getProperties())); + + result.add(blockTO); + } + + Collections.sort(result, Comparators.elementsByName()); + + return result; + } + + private VisualLayoutTO toVisualLayoutTO(VisualLayoutCreationTO layout) { + VisualLayoutTO result = new VisualLayoutTO(); + + result.setName(layout.getName()) + .setProperties(convertProperties(layout.getProperties())); + result.setScaleX((float) layout.getScaleX()) + .setScaleY((float) layout.getScaleY()) + .setLayers(toLayerTOs(layout.getLayers())) + .setLayerGroups(toLayerGroupTOs(layout.getLayerGroups())); + + return result; + } + + private List toLayerTOs(List layers) { + List result = new ArrayList<>(); + + for (Layer layer : layers) { + result.add( + new VisualLayoutTO.Layer() + .setId(layer.getId()) + .setOrdinal(layer.getOrdinal()) + .setVisible(layer.isVisible()) + .setName(layer.getName()) + .setGroupId(layer.getGroupId()) + ); + } + + return result; + } + + private List toLayerGroupTOs(List layerGroups) { + List result = new ArrayList<>(); + + for (LayerGroup layerGroup : layerGroups) { + result.add( + new VisualLayoutTO.LayerGroup() + .setId(layerGroup.getId()) + .setName(layerGroup.getName()) + .setVisible(layerGroup.isVisible()) + ); + } + + return result; + } + + private List getOutgoingPaths( + PointCreationTO point, + List paths + ) { + List result = new ArrayList<>(); + + for (PathCreationTO path : paths) { + if (Objects.equals(path.getSrcPointName(), point.getName())) { + result.add(new PointTO.OutgoingPath().setName(path.getName())); + } + } + + Collections.sort(result, Comparators.outgoingPathsByName()); + + return result; + } + + private List toAllowedOperationTOs(Collection allowedOperations) { + return allowedOperations.stream() + .sorted() + .map(allowedOperation -> { + return (AllowedOperationTO) new AllowedOperationTO().setName(allowedOperation); + }) + .toList(); + } + + private List toAllowedPeripheralOperationTOs( + Collection allowedOperations + ) { + return allowedOperations.stream() + .sorted() + .map(allowedOperation -> { + return (AllowedPeripheralOperationTO) new AllowedPeripheralOperationTO() + .setName(allowedOperation); + }) + .toList(); + } + + private List toLocationTOLinks(Map> links) { + List result = new ArrayList<>(); + + links.forEach((key, value) -> { + result.add( + new LocationTO.Link() + .setPoint(key) + .setAllowedOperations(toAllowedOperationTOs(value)) + ); + }); + + Collections.sort(result, Comparators.linksByPointName()); + + return result; + } + + private List toMemberTOs(Collection members) { + return members.stream() + .map(member -> (MemberTO) new MemberTO().setName(member)) + .sorted(Comparators.elementsByName()) + .toList(); + } + + private List convertProperties(Map properties) { + List result = new ArrayList<>(); + + properties.forEach((key, value) -> { + result.add(new PropertyTO().setName(key).setValue(value)); + }); + + Collections.sort(result, Comparators.propertiesByName()); + + return result; + } + + private boolean isNullOrEmpty(String s) { + return s == null || s.isEmpty(); + } + + private Map toEnvelopeMap(List envelopeTOs) { + return envelopeTOs.stream() + .collect( + Collectors.toMap( + VehicleEnvelopeTO::getKey, + vehicleEnvelopeTO -> toEnvelope(vehicleEnvelopeTO) + ) + ); + } + + private Envelope toEnvelope(VehicleEnvelopeTO vehicleEnvelopeTO) { + return new Envelope( + vehicleEnvelopeTO.getVertices().stream() + .map(coupleTO -> new Couple(coupleTO.getX(), coupleTO.getY())) + .toList() + ); + } + + private List toVehicleEnvelopeTOs(Map envelopeMap) { + return envelopeMap.entrySet() + .stream() + .sorted(Map.Entry.comparingByKey()) + .map( + entry -> new VehicleEnvelopeTO() + .setKey(entry.getKey()) + .setVertices(toCoupleTOs(entry.getValue().getVertices())) + ) + .toList(); + } + + private List toCoupleTOs(List couples) { + return couples.stream() + .map( + couple -> new CoupleTO() + .setX(couple.getX()) + .setY(couple.getY()) + ) + .toList(); + } + + private BoundingBoxCreationTO toBoundingBoxCreationTO(BoundingBoxTO boundingBox) { + return new BoundingBoxCreationTO( + boundingBox.getLength(), + boundingBox.getWidth(), + boundingBox.getHeight() + ) + .withReferenceOffset( + new CoupleCreationTO( + boundingBox.getReferenceOffsetX(), + boundingBox.getReferenceOffsetY() + ) + ); + } + + private VehicleCreationTO.EnergyLevelThresholdSet toEnergyLevelThresholdSetCreationTO( + VehicleTO vehicle + ) { + return new VehicleCreationTO.EnergyLevelThresholdSet( + vehicle.getEnergyLevelCritical().intValue(), + vehicle.getEnergyLevelGood().intValue(), + vehicle.getEnergyLevelSufficientlyRecharged().intValue(), + vehicle.getEnergyLevelFullyRecharged().intValue() + ); + } + + private BoundingBoxTO toBoundingBoxTO(BoundingBoxCreationTO boundingBox) { + return new BoundingBoxTO() + .setLength(boundingBox.getLength()) + .setWidth(boundingBox.getWidth()) + .setHeight(boundingBox.getHeight()) + .setReferenceOffsetX(boundingBox.getReferenceOffset().getX()) + .setReferenceOffsetY(boundingBox.getReferenceOffset().getY()); + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/VehicleEnvelopeTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/VehicleEnvelopeTO.java new file mode 100644 index 0000000..c8b0cc0 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/VehicleEnvelopeTO.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(propOrder = {"key", "vertices"}) +public class VehicleEnvelopeTO { + + private String key; + private List vertices; + + public VehicleEnvelopeTO() { + } + + public String getKey() { + return key; + } + + @XmlAttribute + public VehicleEnvelopeTO setKey( + @Nonnull + String key + ) { + this.key = requireNonNull(key, "key"); + return this; + } + + @XmlElement(name = "vertex") + public List getVertices() { + return vertices; + } + + public VehicleEnvelopeTO setVertices( + @Nonnull + List vertices + ) { + this.vertices = requireNonNull(vertices, "vertices"); + return this; + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/VehicleTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/VehicleTO.java new file mode 100644 index 0000000..5f99caa --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/VehicleTO.java @@ -0,0 +1,202 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlSchemaType; +import javax.xml.bind.annotation.XmlType; +import org.opentcs.util.annotations.ScheduledApiChange; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType( + propOrder = {"name", "energyLevelCritical", "energyLevelGood", "energyLevelFullyRecharged", + "energyLevelSufficientlyRecharged", "maxVelocity", "maxReverseVelocity", "boundingBox", + "properties", "vehicleLayout"} +) +public class VehicleTO + extends + PlantModelElementTO { + + //max velocity in mm/s. + private int maxVelocity; + //max rev velocity in mm/s. + private int maxReverseVelocity; + private Long energyLevelCritical = 0L; + private Long energyLevelGood = 0L; + private Long energyLevelFullyRecharged = 0L; + private Long energyLevelSufficientlyRecharged = 0L; + private String envelopeKey; + private BoundingBoxTO boundingBox = new BoundingBoxTO(); + private VehicleLayout vehicleLayout = new VehicleLayout(); + + /** + * Creates a new instance. + */ + public VehicleTO() { + } + + @XmlAttribute(required = true) + @XmlSchemaType(name = "unsignedInt") + public Long getEnergyLevelCritical() { + return energyLevelCritical; + } + + public VehicleTO setEnergyLevelCritical( + @Nonnull + Long energyLevelCritical + ) { + requireNonNull(energyLevelCritical, "energyLevelCritical"); + this.energyLevelCritical = energyLevelCritical; + return this; + } + + @XmlAttribute(required = true) + @XmlSchemaType(name = "unsignedInt") + public Long getEnergyLevelGood() { + return energyLevelGood; + } + + public VehicleTO setEnergyLevelGood( + @Nonnull + Long energyLevelGood + ) { + requireNonNull(energyLevelGood, "energyLevelGood"); + this.energyLevelGood = energyLevelGood; + return this; + } + + @XmlAttribute(required = true) + @XmlSchemaType(name = "unsignedInt") + public Long getEnergyLevelFullyRecharged() { + return energyLevelFullyRecharged; + } + + public VehicleTO setEnergyLevelFullyRecharged( + @Nonnull + Long energyLevelFullyRecharged + ) { + requireNonNull(energyLevelFullyRecharged, "energyLevelFullyRecharged"); + this.energyLevelFullyRecharged = energyLevelFullyRecharged; + return this; + } + + @XmlAttribute(required = true) + @XmlSchemaType(name = "unsignedInt") + public Long getEnergyLevelSufficientlyRecharged() { + return energyLevelSufficientlyRecharged; + } + + public VehicleTO setEnergyLevelSufficientlyRecharged( + @Nonnull + Long energyLevelSufficientlyRecharged + ) { + requireNonNull(energyLevelSufficientlyRecharged, "energyLevelSufficientlyRecharged"); + this.energyLevelSufficientlyRecharged = energyLevelSufficientlyRecharged; + return this; + } + + @XmlAttribute(required = true) + @XmlSchemaType(name = "unsignedInt") + public int getMaxVelocity() { + return maxVelocity; + } + + public VehicleTO setMaxVelocity( + @Nonnull + int maxVelocity + ) { + this.maxVelocity = maxVelocity; + return this; + } + + @XmlAttribute(required = true) + @XmlSchemaType(name = "unsignedInt") + public int getMaxReverseVelocity() { + return maxReverseVelocity; + } + + public VehicleTO setMaxReverseVelocity( + @Nonnull + int maxReverseVelocity + ) { + this.maxReverseVelocity = maxReverseVelocity; + return this; + } + + @ScheduledApiChange(when = "7.0", details = "Envelope key will become non-null.") + @XmlAttribute + @Nullable + public String getEnvelopeKey() { + return envelopeKey; + } + + @ScheduledApiChange(when = "7.0", details = "Envelope key will become non-null.") + public VehicleTO setEnvelopeKey( + @Nullable + String envelopeKey + ) { + this.envelopeKey = envelopeKey; + return this; + } + + @XmlElement + @Nonnull + public BoundingBoxTO getBoundingBox() { + return boundingBox; + } + + public VehicleTO setBoundingBox( + @Nonnull + BoundingBoxTO boundingBox + ) { + this.boundingBox = requireNonNull(boundingBox, "boundingBox"); + return this; + } + + @XmlElement(required = true) + public VehicleLayout getVehicleLayout() { + return vehicleLayout; + } + + public VehicleTO setVehicleLayout( + @Nonnull + VehicleLayout vehicleLayout + ) { + this.vehicleLayout = requireNonNull(vehicleLayout, "vehicleLayout"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + public static class VehicleLayout { + + private String color = ""; + + /** + * Creates a new instance. + */ + public VehicleLayout() { + } + + @XmlAttribute(required = true) + public String getColor() { + return color; + } + + public VehicleLayout setColor( + @Nonnull + String color + ) { + this.color = requireNonNull(color, "color"); + return this; + } + } +} diff --git a/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/VisualLayoutTO.java b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/VisualLayoutTO.java new file mode 100644 index 0000000..07b1621 --- /dev/null +++ b/opentcs-common/src/main/java/org/opentcs/util/persistence/v6/VisualLayoutTO.java @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + */ +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(propOrder = {"name", "scaleX", "scaleY", "layers", "layerGroups", "properties"}) +public class VisualLayoutTO + extends + PlantModelElementTO { + + private Float scaleX = 0.0F; + private Float scaleY = 0.0F; + private List layers = new ArrayList<>(); + private List layerGroups = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public VisualLayoutTO() { + } + + @XmlAttribute(required = true) + public Float getScaleX() { + return scaleX; + } + + public VisualLayoutTO setScaleX( + @Nonnull + Float scaleX + ) { + requireNonNull(scaleX, "scaleX"); + this.scaleX = scaleX; + return this; + } + + @XmlAttribute(required = true) + public Float getScaleY() { + return scaleY; + } + + public VisualLayoutTO setScaleY( + @Nonnull + Float scaleY + ) { + requireNonNull(scaleY, "scaleY"); + this.scaleY = scaleY; + return this; + } + + @XmlElement(name = "layer") + public List getLayers() { + return layers; + } + + public VisualLayoutTO setLayers( + @Nonnull + List layers + ) { + this.layers = requireNonNull(layers, "layers"); + return this; + } + + @XmlElement(name = "layerGroup") + public List getLayerGroups() { + return layerGroups; + } + + public VisualLayoutTO setLayerGroups( + @Nonnull + List layerGroups + ) { + this.layerGroups = requireNonNull(layerGroups, "layerGroups"); + return this; + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType(propOrder = {"id", "ordinal", "visible", "name", "groupId"}) + public static class Layer { + + private Integer id = 0; + private Integer ordinal = 0; + private Boolean visible = true; + private String name = ""; + private Integer groupId = 0; + + /** + * Creates a new instance. + */ + public Layer() { + } + + @XmlAttribute(required = true) + public Integer getId() { + return id; + } + + public Layer setId(Integer id) { + this.id = requireNonNull(id, "id"); + return this; + } + + @XmlAttribute(required = true) + public Integer getOrdinal() { + return ordinal; + } + + public Layer setOrdinal(Integer ordinal) { + this.ordinal = requireNonNull(ordinal, "ordinal"); + return this; + } + + @XmlAttribute(required = true) + public Boolean isVisible() { + return visible; + } + + public Layer setVisible(Boolean visible) { + this.visible = requireNonNull(visible, "visible"); + return this; + } + + @XmlAttribute(required = true) + public String getName() { + return name; + } + + public Layer setName(String name) { + this.name = requireNonNull(name, "name"); + return this; + } + + @XmlAttribute(required = true) + public Integer getGroupId() { + return groupId; + } + + public Layer setGroupId(Integer groupId) { + this.groupId = requireNonNull(groupId, "groupId"); + return this; + } + } + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType(propOrder = {"id", "name", "visible"}) + public static class LayerGroup { + + private Integer id = 0; + private String name = ""; + private Boolean visible = true; + + /** + * Creates a new instance. + */ + public LayerGroup() { + } + + @XmlAttribute(required = true) + public Integer getId() { + return id; + } + + public LayerGroup setId(Integer id) { + this.id = requireNonNull(id, "id"); + return this; + } + + @XmlAttribute(required = true) + public String getName() { + return name; + } + + public LayerGroup setName(String name) { + this.name = requireNonNull(name, "name"); + return this; + } + + @XmlAttribute(required = true) + public Boolean isVisible() { + return visible; + } + + public LayerGroup setVisible(Boolean visible) { + this.visible = requireNonNull(visible, "visible"); + return this; + } + } +} diff --git a/opentcs-common/src/main/resources/REUSE.toml b/opentcs-common/src/main/resources/REUSE.toml new file mode 100644 index 0000000..42aab58 --- /dev/null +++ b/opentcs-common/src/main/resources/REUSE.toml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC0-1.0 + +version = 1 + +[[annotations]] +path = ["**/*.gif", "**/*.jpg", "**/*.png", "**/*.svg"] +precedence = "closest" +SPDX-FileCopyrightText = "The openTCS Authors" +SPDX-License-Identifier = "CC-BY-4.0" diff --git a/opentcs-common/src/main/resources/i18n/org/opentcs/common/Bundle.properties b/opentcs-common/src/main/resources/i18n/org/opentcs/common/Bundle.properties new file mode 100644 index 0000000..998f785 --- /dev/null +++ b/opentcs-common/src/main/resources/i18n/org/opentcs/common/Bundle.properties @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +connectToServerDialog.button_cancle.text=Cancel +connectToServerDialog.button_ok.text=OK +connectToServerDialog.label_description.text=Description: +connectToServerDialog.label_host.text=Host: +connectToServerDialog.label_port.text=RMI Port: +connectToServerDialog.optionPane_invalidDescription.message=Please enter a description. +connectToServerDialog.optionPane_invalidDescription.title=Invalid description +connectToServerDialog.optionPane_invalidHost.message=Please enter a hostname. +connectToServerDialog.optionPane_invalidHost.title=Invalid hostname +connectToServerDialog.optionPane_invalidPort.message=Please choose a port between 0 and 65535. +connectToServerDialog.optionPane_invalidPort.title=Invalid port +connectToServerDialog.optionPane_noConnection.message=Could not establish connection to kernel. +connectToServerDialog.title=Connect to kernel diff --git a/opentcs-common/src/main/resources/i18n/org/opentcs/common/Bundle_de.properties b/opentcs-common/src/main/resources/i18n/org/opentcs/common/Bundle_de.properties new file mode 100644 index 0000000..05e3a55 --- /dev/null +++ b/opentcs-common/src/main/resources/i18n/org/opentcs/common/Bundle_de.properties @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +connectToServerDialog.button_cancle.text=Abbrechen +connectToServerDialog.label_description.text=Beschreibung: +connectToServerDialog.label_host.text=Rechnername: +connectToServerDialog.optionPane_invalidDescription.message=Bitte tragen Sie eine Beschreibung ein. +connectToServerDialog.optionPane_invalidDescription.title=Ung\u00fcltige Beschreibung +connectToServerDialog.optionPane_invalidHost.message=Bitte tragen Sie einen Rechnernamen ein. +connectToServerDialog.optionPane_invalidHost.title=Ung\u00fcltiger Rechnername +connectToServerDialog.optionPane_invalidPort.message=Bitte w\u00e4hlen Sie einen Port zwischen 0 und 65535. +connectToServerDialog.optionPane_invalidPort.title=Ung\u00fcltiger Port +connectToServerDialog.optionPane_noConnection.message=Verbindung zum Kernel konnte nicht aufgebaut werden. +connectToServerDialog.title=Mit Kernel verbinden diff --git a/opentcs-common/src/main/resources/org/opentcs/util/gui/res/icons/opentcs_icon_016.png b/opentcs-common/src/main/resources/org/opentcs/util/gui/res/icons/opentcs_icon_016.png new file mode 100644 index 0000000..2e6199f Binary files /dev/null and b/opentcs-common/src/main/resources/org/opentcs/util/gui/res/icons/opentcs_icon_016.png differ diff --git a/opentcs-common/src/main/resources/org/opentcs/util/gui/res/icons/opentcs_icon_032.png b/opentcs-common/src/main/resources/org/opentcs/util/gui/res/icons/opentcs_icon_032.png new file mode 100644 index 0000000..9913335 Binary files /dev/null and b/opentcs-common/src/main/resources/org/opentcs/util/gui/res/icons/opentcs_icon_032.png differ diff --git a/opentcs-common/src/main/resources/org/opentcs/util/gui/res/icons/opentcs_icon_064.png b/opentcs-common/src/main/resources/org/opentcs/util/gui/res/icons/opentcs_icon_064.png new file mode 100644 index 0000000..5993f3b Binary files /dev/null and b/opentcs-common/src/main/resources/org/opentcs/util/gui/res/icons/opentcs_icon_064.png differ diff --git a/opentcs-common/src/main/resources/org/opentcs/util/gui/res/icons/opentcs_icon_128.png b/opentcs-common/src/main/resources/org/opentcs/util/gui/res/icons/opentcs_icon_128.png new file mode 100644 index 0000000..2a2f313 Binary files /dev/null and b/opentcs-common/src/main/resources/org/opentcs/util/gui/res/icons/opentcs_icon_128.png differ diff --git a/opentcs-common/src/main/resources/org/opentcs/util/gui/res/icons/opentcs_icon_256.png b/opentcs-common/src/main/resources/org/opentcs/util/gui/res/icons/opentcs_icon_256.png new file mode 100644 index 0000000..affbdd4 Binary files /dev/null and b/opentcs-common/src/main/resources/org/opentcs/util/gui/res/icons/opentcs_icon_256.png differ diff --git a/opentcs-common/src/main/resources/org/opentcs/util/gui/res/icons/template.svg b/opentcs-common/src/main/resources/org/opentcs/util/gui/res/icons/template.svg new file mode 100644 index 0000000..27162d6 --- /dev/null +++ b/opentcs-common/src/main/resources/org/opentcs/util/gui/res/icons/template.svg @@ -0,0 +1,93 @@ + + + + + + + + + + image/svg+xml + + + + + + + + open + TCS + + diff --git a/opentcs-common/src/main/resources/org/opentcs/util/persistence/README.adoc b/opentcs-common/src/main/resources/org/opentcs/util/persistence/README.adoc new file mode 100644 index 0000000..0f29063 --- /dev/null +++ b/opentcs-common/src/main/resources/org/opentcs/util/persistence/README.adoc @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + += Information on openTCS plant model XML schemas + +== General information + +The openTCS plant model XML schemas adhere to link:https://semver.org/spec/v2.0.0.html[Semantic Versioning]. + +This means: + +* If something is removed from the schema (e.g. a plant model element property that is no longer supported), the MAJOR version is incremented. +* If something is added to the schema in a backward compatible manner (e.g. a new plant model element property), the MINOR version is incremented. +* If something is fixed in the schema (e.g. a typo), the PATCH version is incremented. + +== Implementation remarks + +* For every MAJOR version, only _a single_ implementation is maintained here -- the one for the most recent MINOR/PATCH schema version. +* If the MINOR or PATCH version changes, the version string in the corresponding implementation must be updated. +* When reading a plant model file, an implementation must check whether the version that is read is compatible with the maximum version that the implementation supports. + If the version is not compatible (e.g. because it is a more recent MINOR/PATCH version than the one supported by the implementation), reading the respective plant model file must fail. +* When writing a plant model file, an implementation must always apply the most recent MINOR/PATCH version for the MAJOR version it supports. +* When a new MAJOR version is introduced, the previous MAJOR version's code for mapping to/from base API data structures can and should be removed, as it will no longer be used then. + (With the design applied at the time of this writing, this would be the `V6ModelParser` class's `read()` method and the class `V6TOMapper`.) diff --git a/opentcs-common/src/main/resources/org/opentcs/util/persistence/model-0.0.4.xsd b/opentcs-common/src/main/resources/org/opentcs/util/persistence/model-0.0.4.xsd new file mode 100644 index 0000000..6052b1b --- /dev/null +++ b/opentcs-common/src/main/resources/org/opentcs/util/persistence/model-0.0.4.xsd @@ -0,0 +1,544 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-common/src/main/resources/org/opentcs/util/persistence/model-0.0.5.xsd b/opentcs-common/src/main/resources/org/opentcs/util/persistence/model-0.0.5.xsd new file mode 100644 index 0000000..281b15f --- /dev/null +++ b/opentcs-common/src/main/resources/org/opentcs/util/persistence/model-0.0.5.xsd @@ -0,0 +1,542 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-common/src/main/resources/org/opentcs/util/persistence/model-6.0.0.xsd b/opentcs-common/src/main/resources/org/opentcs/util/persistence/model-6.0.0.xsd new file mode 100644 index 0000000..281254a --- /dev/null +++ b/opentcs-common/src/main/resources/org/opentcs/util/persistence/model-6.0.0.xsd @@ -0,0 +1,579 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-common/src/main/resources/org/opentcs/util/persistence/useraccounts.xsd b/opentcs-common/src/main/resources/org/opentcs/util/persistence/useraccounts.xsd new file mode 100644 index 0000000..4575882 --- /dev/null +++ b/opentcs-common/src/main/resources/org/opentcs/util/persistence/useraccounts.xsd @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-common/src/test/java/org/opentcs/common/SameThreadExecutorServiceTest.java b/opentcs-common/src/test/java/org/opentcs/common/SameThreadExecutorServiceTest.java new file mode 100644 index 0000000..0e3ebc2 --- /dev/null +++ b/opentcs-common/src/test/java/org/opentcs/common/SameThreadExecutorServiceTest.java @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.common; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link SameThreadExecutorService}. + */ +class SameThreadExecutorServiceTest { + + private ExecutorService executor; + + @BeforeEach + void setup() { + executor = new SameThreadExecutorService(); + } + + @Test + void shouldShutdown() { + assertThat(executor.isShutdown(), is(false)); + executor.shutdown(); + assertThat(executor.isShutdown(), is(true)); + } + + @Test + void shouldShutdownNow() { + // shutdownNow should shutdown the executor and always return an empty list of tasks + // since all task are executed to completion when submited. + executor.submit(() -> { + }); + + assertThat(executor.shutdownNow(), empty()); + assertThat(executor.isShutdown(), is(true)); + } + + @Test + void awaitTerminationShouldReturnIfShutdown() + throws InterruptedException { + executor.shutdown(); + assertThat(executor.awaitTermination(10, TimeUnit.SECONDS), is(true)); + } + + @Test + void awaitTerminationShouldThrowIfNotShutdown() + throws InterruptedException { + assertThrows( + IllegalStateException.class, + () -> { + executor.awaitTermination(10, TimeUnit.SECONDS); + } + ); + } + + @Test + void shouldRunSubmittedRunnable() { + Runnable task = mock(Runnable.class); + executor.submit(task); + verify(task).run(); + } + + @Test + void shouldRunSubmittedCallable() + throws Exception { + @SuppressWarnings("unchecked") + Callable task = (Callable) mock(Callable.class); + executor.submit(task); + verify(task).call(); + } + +} diff --git a/opentcs-common/src/test/java/org/opentcs/util/ColorsTest.java b/opentcs-common/src/test/java/org/opentcs/util/ColorsTest.java new file mode 100644 index 0000000..efd90f9 --- /dev/null +++ b/opentcs-common/src/test/java/org/opentcs/util/ColorsTest.java @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.awt.Color; +import org.junit.jupiter.api.Test; + +/** + */ +class ColorsTest { + + @Test + void testEncodeToHexRGB() { + assertEquals("#000000", Colors.encodeToHexRGB(new Color(0, 0, 0))); + assertEquals("#FFFFFF", Colors.encodeToHexRGB(new Color(255, 255, 255))); + assertEquals("#FF0000", Colors.encodeToHexRGB(new Color(255, 0, 0))); + assertEquals("#00FF00", Colors.encodeToHexRGB(new Color(0, 255, 0))); + assertEquals("#0000FF", Colors.encodeToHexRGB(new Color(0, 0, 255))); + assertEquals("#000001", Colors.encodeToHexRGB(new Color(0, 0, 1))); + assertEquals("#000100", Colors.encodeToHexRGB(new Color(0, 1, 0))); + assertEquals("#010000", Colors.encodeToHexRGB(new Color(1, 0, 0))); + } + + @Test + void testDecodeFromHexRGB() { + Color color; + + color = Colors.decodeFromHexRGB("#000000"); + assertEquals(0, color.getRed()); + assertEquals(0, color.getGreen()); + assertEquals(0, color.getBlue()); + + color = Colors.decodeFromHexRGB("#FFFFFF"); + assertEquals(255, color.getRed()); + assertEquals(255, color.getGreen()); + assertEquals(255, color.getBlue()); + + color = Colors.decodeFromHexRGB("#010000"); + assertEquals(1, color.getRed()); + assertEquals(0, color.getGreen()); + assertEquals(0, color.getBlue()); + + color = Colors.decodeFromHexRGB("#000100"); + assertEquals(0, color.getRed()); + assertEquals(1, color.getGreen()); + assertEquals(0, color.getBlue()); + + color = Colors.decodeFromHexRGB("#000001"); + assertEquals(0, color.getRed()); + assertEquals(0, color.getGreen()); + assertEquals(1, color.getBlue()); + } + +} diff --git a/opentcs-common/src/test/java/org/opentcs/util/ComparatorsTest.java b/opentcs-common/src/test/java/org/opentcs/util/ComparatorsTest.java new file mode 100644 index 0000000..dd5214f --- /dev/null +++ b/opentcs-common/src/test/java/org/opentcs/util/ComparatorsTest.java @@ -0,0 +1,291 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import java.time.Instant; +import java.util.Comparator; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Point; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.data.peripherals.PeripheralOperation; + +/** + * Unit tests for {@link Comparators}. + */ +class ComparatorsTest { + + @Test + void compareObjectsByName() { + Comparator> comparator = Comparators.objectsByName(); + + assertThat( + comparator.compare(new Point("1"), new Point("1")), + is(0) + ); + + assertThat( + comparator.compare(new Point("1"), new Point("2")), + is(-1) + ); + assertThat( + comparator.compare(new Point("2"), new Point("1")), + is(1) + ); + + assertThat( + comparator.compare(new Point("01"), new Point("02")), + is(-1) + ); + assertThat( + comparator.compare(new Point("02"), new Point("01")), + is(1) + ); + + assertThat( + comparator.compare(new Point("point01"), new Point("point02")), + is(-1) + ); + assertThat( + comparator.compare(new Point("point02"), new Point("point01")), + is(1) + ); + + assertThat( + comparator.compare(new Point("point-01"), new Point("point-02")), + is(-1) + ); + assertThat( + comparator.compare(new Point("point-02"), new Point("point-01")), + is(1) + ); + } + + @Test + void compareReferencesByName() { + Comparator> comparator = Comparators.referencesByName(); + + assertThat( + comparator.compare( + new Point("1").getReference(), + new Point("1").getReference() + ), + is(0) + ); + + assertThat( + comparator.compare( + new Point("1").getReference(), + new Point("2").getReference() + ), + is(-1) + ); + assertThat( + comparator.compare( + new Point("2").getReference(), + new Point("1").getReference() + ), + is(1) + ); + + assertThat( + comparator.compare( + new Point("01").getReference(), + new Point("02").getReference() + ), + is(-1) + ); + assertThat( + comparator.compare( + new Point("02").getReference(), + new Point("01").getReference() + ), + is(1) + ); + + assertThat( + comparator.compare( + new Point("point01").getReference(), + new Point("point02").getReference() + ), + is(-1) + ); + assertThat( + comparator.compare( + new Point("point02").getReference(), + new Point("point01").getReference() + ), + is(1) + ); + + assertThat( + comparator.compare( + new Point("point-01").getReference(), + new Point("point-02").getReference() + ), + is(-1) + ); + assertThat( + comparator.compare( + new Point("point-02").getReference(), + new Point("point-01").getReference() + ), + is(1) + ); + } + + @Test + void compareOrdersByAge() { + Comparator comparator = Comparators.ordersByAge(); + + TransportOrder order1 = new TransportOrder("order-1", List.of()); + TransportOrder order2 = new TransportOrder("order-2", List.of()); + + assertThat( + comparator.compare( + order1.withCreationTime(Instant.ofEpochMilli(1000)), + order2.withCreationTime(Instant.ofEpochMilli(2000)) + ), + is(-1) + ); + + assertThat( + comparator.compare( + order1.withCreationTime(Instant.ofEpochMilli(2000)), + order2.withCreationTime(Instant.ofEpochMilli(1000)) + ), + is(1) + ); + + // Compares by name if age is same: + assertThat( + comparator.compare( + order1.withCreationTime(Instant.ofEpochMilli(2000)), + order2.withCreationTime(Instant.ofEpochMilli(2000)) + ), + is(-1) + ); + + assertThat( + comparator.compare( + order2.withCreationTime(Instant.ofEpochMilli(2000)), + order1.withCreationTime(Instant.ofEpochMilli(2000)) + ), + is(1) + ); + } + + @Test + void compareOrdersByDeadline() { + Comparator comparator = Comparators.ordersByDeadline(); + + TransportOrder order1 = new TransportOrder("order-1", List.of()) + .withCreationTime(Instant.ofEpochMilli(1000)); + TransportOrder order2 = new TransportOrder("order-2", List.of()) + .withCreationTime(Instant.ofEpochMilli(2000)); + + assertThat( + comparator.compare( + order1.withDeadline(Instant.ofEpochMilli(5000)), + order2.withDeadline(Instant.ofEpochMilli(7000)) + ), + is(-1) + ); + + assertThat( + comparator.compare( + order1.withDeadline(Instant.ofEpochMilli(7000)), + order2.withDeadline(Instant.ofEpochMilli(5000)) + ), + is(1) + ); + + // Compares by age if deadline is same: + assertThat( + comparator.compare( + order1.withDeadline(Instant.ofEpochMilli(5000)), + order2.withDeadline(Instant.ofEpochMilli(5000)) + ), + is(-1) + ); + + assertThat( + comparator.compare( + order2.withDeadline(Instant.ofEpochMilli(5000)), + order1.withDeadline(Instant.ofEpochMilli(5000)) + ), + is(1) + ); + } + + @Test + void comparePeripheralJobsByAge() { + Comparator comparator = Comparators.jobsByAge(); + + LocationType locType = new LocationType("some-loc-type"); + Location location = new Location("some-location", locType.getReference()); + + PeripheralJob job1 = new PeripheralJob( + "job-1", + "some-token", + new PeripheralOperation( + location.getReference(), + "some-operation", + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + true + ) + ); + PeripheralJob job2 = new PeripheralJob( + "job-2", + "some-token", + new PeripheralOperation( + location.getReference(), + "some-operation", + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + true + ) + ); + + assertThat( + comparator.compare( + job1.withCreationTime(Instant.ofEpochMilli(1000)), + job2.withCreationTime(Instant.ofEpochMilli(2000)) + ), + is(-1) + ); + + assertThat( + comparator.compare( + job1.withCreationTime(Instant.ofEpochMilli(2000)), + job2.withCreationTime(Instant.ofEpochMilli(1000)) + ), + is(1) + ); + + // Compare by name if age is same: + assertThat( + comparator.compare( + job1.withCreationTime(Instant.ofEpochMilli(2000)), + job2.withCreationTime(Instant.ofEpochMilli(2000)) + ), + is(-1) + ); + + assertThat( + comparator.compare( + job2.withCreationTime(Instant.ofEpochMilli(2000)), + job1.withCreationTime(Instant.ofEpochMilli(2000)) + ), + is(1) + ); + + } +} diff --git a/opentcs-common/src/test/java/org/opentcs/util/NumberParsersTest.java b/opentcs-common/src/test/java/org/opentcs/util/NumberParsersTest.java new file mode 100644 index 0000000..e6c48db --- /dev/null +++ b/opentcs-common/src/test/java/org/opentcs/util/NumberParsersTest.java @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.stream.LongStream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + */ +class NumberParsersTest { + + @ParameterizedTest + @MethodSource("paramsFactory") + void parsesNumbers(long number) { + assertEquals(number, NumberParsers.parsePureDecimalLong(Long.toString(number))); + } + + static LongStream paramsFactory() { + return LongStream.concat( + LongStream.of(Long.MIN_VALUE, Long.MIN_VALUE + 1, Long.MAX_VALUE - 1, Long.MAX_VALUE), + LongStream.rangeClosed(-100, 100) + ); + } +} diff --git a/opentcs-common/src/test/java/org/opentcs/util/UniqueStringGeneratorTest.java b/opentcs-common/src/test/java/org/opentcs/util/UniqueStringGeneratorTest.java new file mode 100644 index 0000000..50a4df1 --- /dev/null +++ b/opentcs-common/src/test/java/org/opentcs/util/UniqueStringGeneratorTest.java @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * A test case for class UniqueStringGenerator. + */ +class UniqueStringGeneratorTest { + + private static final String PREFIX = "TestPrefix"; + + private static final String PATTERN_ONE_DIGIT = "0"; + + private static final String PATTERN_TWO_DIGITS = "00"; + + private UniqueStringGenerator generator; + + @BeforeEach + void setUp() { + generator = new UniqueStringGenerator<>(); + } + + @Test + void testRepeatedGenerationWithoutModification() { + String generatedString = generator.getUniqueString( + PREFIX, + PATTERN_TWO_DIGITS + ); + assertEquals(PREFIX + "01", generatedString); + generatedString = generator.getUniqueString(PREFIX, PATTERN_TWO_DIGITS); + assertEquals(PREFIX + "01", generatedString); + } + + @Test + void shouldProvideConfiguredPatterns() { + final String namePatternPrefix = "SomePrefix"; + final String namePatterPrefix2 = "AnotherPrefix"; + final Object selector = new Object(); + final Object selector2 = new Object(); + + generator.registerNamePattern(selector, namePatternPrefix, "0000"); + generator.registerNamePattern(selector2, namePatterPrefix2, "0000"); + + assertEquals( + namePatternPrefix + "0001", + generator.getUniqueString(selector) + ); + assertEquals( + namePatterPrefix2 + "0001", + generator.getUniqueString(selector2) + ); + } + + @Test + void testRepeatedGenerationWithAddition() { + String generatedString = generator.getUniqueString( + PREFIX, + PATTERN_TWO_DIGITS + ); + assertEquals(PREFIX + "01", generatedString); + generator.addString(generatedString); + generatedString = generator.getUniqueString(PREFIX, PATTERN_TWO_DIGITS); + assertEquals(PREFIX + "02", generatedString); + } + + @Test + void testNullPrefix() { + String generatedString = generator.getUniqueString( + null, + PATTERN_ONE_DIGIT + ); + assertEquals("1", generatedString); + } + + @Test + void shouldHaveString() { + generator.addString("some string"); + assertTrue(generator.hasString("some string")); + } + + @Test + void shouldNotHaveString() { + generator.addString("some string"); + assertFalse(generator.hasString("some other string")); + } + +} diff --git a/opentcs-common/src/test/java/org/opentcs/util/gui/StringListCellRendererTest.java b/opentcs-common/src/test/java/org/opentcs/util/gui/StringListCellRendererTest.java new file mode 100644 index 0000000..76a32fc --- /dev/null +++ b/opentcs-common/src/test/java/org/opentcs/util/gui/StringListCellRendererTest.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.gui; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; + +import java.awt.Component; +import javax.swing.JLabel; +import javax.swing.JList; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; + +/** + */ +class StringListCellRendererTest { + + private JList> list; + private StringListCellRenderer> renderer; + + @BeforeEach + void setUp() { + list = new JList<>(); + renderer = new StringListCellRenderer<>(x -> x == null ? "" : x.getName()); + } + + /** + * Test of getListCellRendererComponent method, of class FunctionalListCellRenderer. + */ + @Test + void returnsNullForNullValue() { + Component result = renderer.getListCellRendererComponent(list, null, 0, true, true); + assertThat(result, is(instanceOf(JLabel.class))); + JLabel labelResult = (JLabel) result; + assertThat(labelResult.getText(), is(equalTo(""))); + } + + @Test + void returnsLabelWithNameAsText() { + Vehicle vehicle = new Vehicle("VehicleName"); + TCSObjectReference vehicleReference = vehicle.getReference(); + Component result = renderer.getListCellRendererComponent(list, vehicleReference, 0, true, true); + assertThat(result, is(instanceOf(JLabel.class))); + JLabel labelResult = (JLabel) result; + assertThat(labelResult.getText(), is(equalTo("VehicleName"))); + } + +} diff --git a/opentcs-common/src/test/java/org/opentcs/util/gui/StringTableCellRendererTest.java b/opentcs-common/src/test/java/org/opentcs/util/gui/StringTableCellRendererTest.java new file mode 100644 index 0000000..8a1ed80 --- /dev/null +++ b/opentcs-common/src/test/java/org/opentcs/util/gui/StringTableCellRendererTest.java @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.gui; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; + +import java.awt.Component; +import javax.swing.JLabel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; + +/** + */ +class StringTableCellRendererTest { + + private StringTableCellRenderer> renderer; + + @BeforeEach + void setUp() { + renderer = new StringTableCellRenderer<>(x -> x == null ? "" : x.getName()); + } + + @Test + void returnsNullForNullValue() { + Component result = renderer.getTableCellRendererComponent(null, null, false, false, 0, 0); + assertThat(result, is(instanceOf(JLabel.class))); + JLabel labelResult = (JLabel) result; + assertThat(labelResult.getText(), is(equalTo(""))); + } + + @Test + void returnsLabelWithNameAsText() { + Vehicle vehicle = new Vehicle("VehicleName"); + TCSObjectReference vehicleReference = vehicle.getReference(); + Component result + = renderer.getTableCellRendererComponent(null, vehicleReference, false, false, 0, 0); + assertThat(result, is(instanceOf(JLabel.class))); + JLabel labelResult = (JLabel) result; + assertThat(labelResult.getText(), is(equalTo("VehicleName"))); + } + +} diff --git a/opentcs-common/src/test/java/org/opentcs/util/persistence/v004/V004DrivingCoursePersistenceTest.java b/opentcs-common/src/test/java/org/opentcs/util/persistence/v004/V004DrivingCoursePersistenceTest.java new file mode 100644 index 0000000..f4079f5 --- /dev/null +++ b/opentcs-common/src/test/java/org/opentcs/util/persistence/v004/V004DrivingCoursePersistenceTest.java @@ -0,0 +1,392 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v004; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + */ +class V004DrivingCoursePersistenceTest { + + private V004PlantModelTO plantModel; + + @BeforeEach + void setUp() { + plantModel = createPlantModel(); + } + + @Test + void persistAndMaterializeModelAttributes() + throws IOException { + plantModel.setVersion("0.0.4"); + plantModel.getProperties().add(new PropertyTO().setName("some-prop").setValue("some-prop-val")); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V004PlantModelTO parsedModel = V004PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getVersion(), is(equalTo("0.0.4"))); + assertThat(parsedModel.getProperties(), hasSize(1)); + assertThat(parsedModel.getProperties().get(0).getName(), is(equalTo("some-prop"))); + assertThat(parsedModel.getProperties().get(0).getValue(), is(equalTo("some-prop-val"))); + } + + @Test + void persistAndMaterializePoints() + throws IOException { + plantModel.getPoints().get(0).setName("my-point"); + plantModel.getPoints().get(0).setxPosition(1L); + plantModel.getPoints().get(0).setyPosition(2L); + plantModel.getPoints().get(0).setzPosition(3L); + plantModel.getPoints().get(0).setVehicleOrientationAngle(12.34f); + plantModel.getPoints().get(0).setType("REPORT_POSITION"); + plantModel.getPoints().get(0).getOutgoingPaths() + .add(new PointTO.OutgoingPath().setName("some-path")); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V004PlantModelTO parsedModel = V004PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getPoints(), hasSize(2)); + assertThat(parsedModel.getPoints().get(0).getName(), is(equalTo("my-point"))); + assertThat(parsedModel.getPoints().get(0).getxPosition(), is(1L)); + assertThat(parsedModel.getPoints().get(0).getyPosition(), is(2L)); + assertThat(parsedModel.getPoints().get(0).getzPosition(), is(3L)); + assertThat(parsedModel.getPoints().get(0).getVehicleOrientationAngle(), is(12.34f)); + assertThat(parsedModel.getPoints().get(0).getType(), is(equalTo("REPORT_POSITION"))); + assertThat(parsedModel.getPoints().get(0).getOutgoingPaths(), hasSize(1)); + assertThat( + parsedModel.getPoints().get(0).getOutgoingPaths().get(0).getName(), + is(equalTo("some-path")) + ); + } + + @Test + void persistAndMaterializePaths() + throws IOException { + plantModel.getPaths().get(0).setName("my-path"); + plantModel.getPaths().get(0).setSourcePoint("some-source-point"); + plantModel.getPaths().get(0).setDestinationPoint("some-dest-point"); + plantModel.getPaths().get(0).setLength(1234L); + plantModel.getPaths().get(0).setLocked(true); + plantModel.getPaths().get(0).setMaxVelocity(9876L); + plantModel.getPaths().get(0).setMaxReverseVelocity(5432L); + plantModel.getPaths().get(0).getPeripheralOperations().add( + new PeripheralOperationTO() + .setLocationName("some-loc-name") + .setExecutionTrigger("AFTER_ALLOCATION") + .setCompletionRequired(true) + ); + plantModel.getPaths().get(0).getVehicleEnvelopes().add( + new VehicleEnvelopeTO() + .setKey("some-key") + .setVertices( + List.of( + new CoupleTO() + .setX(1234L) + .setY(5678L) + ) + ) + ); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V004PlantModelTO parsedModel = V004PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getPaths(), hasSize(1)); + assertThat(parsedModel.getPaths().get(0).getName(), is(equalTo("my-path"))); + assertThat(parsedModel.getPaths().get(0).getLength(), is(1234L)); + assertThat(parsedModel.getPaths().get(0).isLocked(), is(true)); + assertThat(parsedModel.getPaths().get(0).getMaxVelocity(), is(9876L)); + assertThat(parsedModel.getPaths().get(0).getMaxReverseVelocity(), is(5432L)); + assertThat(parsedModel.getPaths().get(0).getSourcePoint(), is(equalTo("some-source-point"))); + assertThat(parsedModel.getPaths().get(0).getDestinationPoint(), is(equalTo("some-dest-point"))); + assertThat(parsedModel.getPaths().get(0).getPeripheralOperations(), hasSize(1)); + assertThat( + parsedModel.getPaths().get(0).getPeripheralOperations().get(0).getLocationName(), + is(equalTo("some-loc-name")) + ); + assertThat( + parsedModel.getPaths().get(0).getPeripheralOperations().get(0).getExecutionTrigger(), + is(equalTo("AFTER_ALLOCATION")) + ); + assertThat( + parsedModel.getPaths().get(0).getPeripheralOperations().get(0).isCompletionRequired(), + is(true) + ); + assertThat(parsedModel.getPaths().get(0).getVehicleEnvelopes(), hasSize(1)); + assertThat(parsedModel.getPaths().get(0).getVehicleEnvelopes().get(0).getKey(), is("some-key")); + assertThat( + parsedModel.getPaths().get(0).getVehicleEnvelopes().get(0).getVertices(), + hasSize(1) + ); + assertThat( + parsedModel.getPaths().get(0).getVehicleEnvelopes().get(0).getVertices().get(0).getX(), + is(1234L) + ); + assertThat( + parsedModel.getPaths().get(0).getVehicleEnvelopes().get(0).getVertices().get(0).getY(), + is(5678L) + ); + } + + @Test + void persistAndMaterializeLocationTypes() + throws IOException { + plantModel.getLocationTypes().get(0).setName("my-location-type"); + plantModel.getLocationTypes().get(0).getAllowedPeripheralOperations().add( + (AllowedPeripheralOperationTO) new AllowedPeripheralOperationTO().setName("some-op") + ); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V004PlantModelTO parsedModel = V004PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getLocationTypes(), hasSize(1)); + assertThat(parsedModel.getLocationTypes().get(0).getName(), is(equalTo("my-location-type"))); + assertThat(parsedModel.getLocationTypes().get(0).getAllowedPeripheralOperations(), hasSize(1)); + assertThat( + parsedModel.getLocationTypes().get(0).getAllowedPeripheralOperations().get(0).getName(), + is(equalTo("some-op")) + ); + } + + @Test + void persistAndMaterializeLocations() + throws IOException { + plantModel.getLocations().get(0).setName("my-location"); + plantModel.getLocations().get(0).setType("some-loc-type"); + plantModel.getLocations().get(0).setLocked(true); + plantModel.getLocations().get(0).setxPosition(1L); + plantModel.getLocations().get(0).setyPosition(2L); + plantModel.getLocations().get(0).setzPosition(3L); + plantModel.getLocations().get(0).getLinks().add( + new LocationTO.Link() + .setPoint("some-point") + .setAllowedOperations( + new ArrayList<>( + List.of( + (AllowedOperationTO) new AllowedOperationTO().setName("some-op") + ) + ) + ) + ); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V004PlantModelTO parsedModel = V004PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getLocations(), hasSize(1)); + assertThat(parsedModel.getLocations().get(0).getName(), is(equalTo("my-location"))); + assertThat(parsedModel.getLocations().get(0).getType(), is(equalTo("some-loc-type"))); + assertThat(parsedModel.getLocations().get(0).isLocked(), is(true)); + assertThat(parsedModel.getLocations().get(0).getxPosition(), is(1L)); + assertThat(parsedModel.getLocations().get(0).getyPosition(), is(2L)); + assertThat(parsedModel.getLocations().get(0).getzPosition(), is(3L)); + assertThat(parsedModel.getLocations().get(0).getLinks(), hasSize(1)); + assertThat( + parsedModel.getLocations().get(0).getLinks().get(0).getPoint(), + is(equalTo("some-point")) + ); + assertThat( + parsedModel.getLocations().get(0).getLinks().get(0).getAllowedOperations(), + hasSize(1) + ); + assertThat( + parsedModel.getLocations().get(0).getLinks().get(0).getAllowedOperations().get(0).getName(), + is(equalTo("some-op")) + ); + } + + @Test + void persistAndMaterializeBlocks() + throws IOException { + plantModel.getBlocks().get(0).setName("my-block"); + plantModel.getBlocks().get(0).setType("SAME_DIRECTION_ONLY"); + plantModel.getBlocks().get(0).getMembers().add( + (MemberTO) new MemberTO().setName("some-member") + ); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V004PlantModelTO parsedModel = V004PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getBlocks(), hasSize(1)); + assertThat(parsedModel.getBlocks().get(0).getName(), is(equalTo("my-block"))); + assertThat(parsedModel.getBlocks().get(0).getType(), is(equalTo("SAME_DIRECTION_ONLY"))); + assertThat(parsedModel.getBlocks().get(0).getMembers(), hasSize(1)); + assertThat( + parsedModel.getBlocks().get(0).getMembers().get(0).getName(), + is(equalTo("some-member")) + ); + } + + @Test + void persistAndMaterializeVehicles() + throws IOException { + plantModel.getVehicles().get(0).setName("my-vehicle"); + plantModel.getVehicles().get(0).setLength(1234L); + plantModel.getVehicles().get(0).setMaxVelocity(333); + plantModel.getVehicles().get(0).setMaxReverseVelocity(444); + plantModel.getVehicles().get(0).setEnergyLevelCritical(33L); + plantModel.getVehicles().get(0).setEnergyLevelGood(88L); + plantModel.getVehicles().get(0).setEnergyLevelSufficientlyRecharged(66L); + plantModel.getVehicles().get(0).setEnergyLevelFullyRecharged(99L); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V004PlantModelTO parsedModel = V004PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getVehicles(), hasSize(1)); + assertThat(parsedModel.getVehicles().get(0).getName(), is(equalTo("my-vehicle"))); + assertThat(parsedModel.getVehicles().get(0).getLength(), is(1234L)); + assertThat(parsedModel.getVehicles().get(0).getMaxVelocity(), is(333)); + assertThat(parsedModel.getVehicles().get(0).getMaxReverseVelocity(), is(444)); + assertThat(parsedModel.getVehicles().get(0).getEnergyLevelCritical(), is(33L)); + assertThat(parsedModel.getVehicles().get(0).getEnergyLevelGood(), is(88L)); + assertThat(parsedModel.getVehicles().get(0).getEnergyLevelSufficientlyRecharged(), is(66L)); + assertThat(parsedModel.getVehicles().get(0).getEnergyLevelFullyRecharged(), is(99L)); + } + + private String toXml(V004PlantModelTO plantModel) + throws IOException { + StringWriter writer = new StringWriter(); + plantModel.toXml(writer); + return writer.toString(); + } + + private V004PlantModelTO createPlantModel() { + return (V004PlantModelTO) new V004PlantModelTO() + .setName(UUID.randomUUID().toString()) + .setPoints( + new ArrayList<>( + List.of( + (PointTO) new PointTO() + .setPointLayout( + new PointTO.PointLayout() + .setxPosition(1L) + .setyPosition(2L) + .setxLabelOffset(20L) + .setyLabelOffset(20L) + .setLayerId(0) + ) + .setName(UUID.randomUUID().toString()), + (PointTO) new PointTO() + .setPointLayout( + new PointTO.PointLayout() + .setxPosition(4L) + .setyPosition(5L) + .setxLabelOffset(20L) + .setyLabelOffset(20L) + .setLayerId(0) + ) + .setName(UUID.randomUUID().toString()) + ) + ) + ) + .setPaths( + new ArrayList<>( + List.of( + (PathTO) new PathTO() + .setPathLayout( + new PathTO.PathLayout() + .setConnectionType("DIRECT") + .setLayerId(0) + ) + .setName(UUID.randomUUID().toString()) + ) + ) + ) + .setLocationTypes( + new ArrayList<>( + List.of( + (LocationTypeTO) new LocationTypeTO() + .setLocationTypeLayout( + new LocationTypeTO.LocationTypeLayout() + .setLocationRepresentation("LOAD_TRANSFER_GENERIC") + ) + .setName(UUID.randomUUID().toString()) + ) + ) + ) + .setLocations( + List.of( + (LocationTO) new LocationTO() + .setLocationLayout( + new LocationTO.LocationLayout() + .setxPosition(100L) + .setyPosition(200L) + .setxLabelOffset(20L) + .setyLabelOffset(20L) + .setLocationRepresentation("LOAD_TRANSFER_GENERIC") + .setLayerId(0) + ) + .setName(UUID.randomUUID().toString()) + ) + ) + .setBlocks( + List.of( + (BlockTO) new BlockTO() + .setBlockLayout( + new BlockTO.BlockLayout() + .setColor("#FF0000") + ) + .setName(UUID.randomUUID().toString()) + ) + ) + .setVehicles( + List.of( + (VehicleTO) new VehicleTO() + .setVehicleLayout( + new VehicleTO.VehicleLayout() + .setColor("#FF0000") + ) + .setName(UUID.randomUUID().toString()) + ) + ) + .setVisualLayout( + (VisualLayoutTO) new VisualLayoutTO() + .setScaleX(50.0f) + .setScaleY(50.0f) + .setLayers( + List.of( + new VisualLayoutTO.Layer() + .setId(0) + .setOrdinal(0) + .setVisible(Boolean.TRUE) + .setName(UUID.randomUUID().toString()) + .setGroupId(0) + ) + ) + .setLayerGroups( + List.of( + new VisualLayoutTO.LayerGroup() + .setId(0) + .setName(UUID.randomUUID().toString()) + .setVisible(Boolean.TRUE) + ) + ) + .setName(UUID.randomUUID().toString()) + ) + .setVersion("0.0.4"); + } +} diff --git a/opentcs-common/src/test/java/org/opentcs/util/persistence/v005/V005DrivingCoursePersistenceTest.java b/opentcs-common/src/test/java/org/opentcs/util/persistence/v005/V005DrivingCoursePersistenceTest.java new file mode 100644 index 0000000..dc0871f --- /dev/null +++ b/opentcs-common/src/test/java/org/opentcs/util/persistence/v005/V005DrivingCoursePersistenceTest.java @@ -0,0 +1,392 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v005; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + */ +class V005DrivingCoursePersistenceTest { + + private V005PlantModelTO plantModel; + + @BeforeEach + void setUp() { + plantModel = createPlantModel(); + } + + @Test + void persistAndMaterializeModelAttributes() + throws IOException { + plantModel.setVersion("0.0.5"); + plantModel.getProperties().add(new PropertyTO().setName("some-prop").setValue("some-prop-val")); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V005PlantModelTO parsedModel = V005PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getVersion(), is(equalTo("0.0.5"))); + assertThat(parsedModel.getProperties(), hasSize(1)); + assertThat(parsedModel.getProperties().get(0).getName(), is(equalTo("some-prop"))); + assertThat(parsedModel.getProperties().get(0).getValue(), is(equalTo("some-prop-val"))); + } + + @Test + void persistAndMaterializePoints() + throws IOException { + plantModel.getPoints().get(0).setName("my-point"); + plantModel.getPoints().get(0).setxPosition(1L); + plantModel.getPoints().get(0).setyPosition(2L); + plantModel.getPoints().get(0).setzPosition(3L); + plantModel.getPoints().get(0).setVehicleOrientationAngle(12.34f); + plantModel.getPoints().get(0).setType("PARK_POSITION"); + plantModel.getPoints().get(0).getOutgoingPaths() + .add(new PointTO.OutgoingPath().setName("some-path")); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V005PlantModelTO parsedModel = V005PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getPoints(), hasSize(2)); + assertThat(parsedModel.getPoints().get(0).getName(), is(equalTo("my-point"))); + assertThat(parsedModel.getPoints().get(0).getxPosition(), is(1L)); + assertThat(parsedModel.getPoints().get(0).getyPosition(), is(2L)); + assertThat(parsedModel.getPoints().get(0).getzPosition(), is(3L)); + assertThat(parsedModel.getPoints().get(0).getVehicleOrientationAngle(), is(12.34f)); + assertThat(parsedModel.getPoints().get(0).getType(), is(equalTo("PARK_POSITION"))); + assertThat(parsedModel.getPoints().get(0).getOutgoingPaths(), hasSize(1)); + assertThat( + parsedModel.getPoints().get(0).getOutgoingPaths().get(0).getName(), + is(equalTo("some-path")) + ); + } + + @Test + void persistAndMaterializePaths() + throws IOException { + plantModel.getPaths().get(0).setName("my-path"); + plantModel.getPaths().get(0).setSourcePoint("some-source-point"); + plantModel.getPaths().get(0).setDestinationPoint("some-dest-point"); + plantModel.getPaths().get(0).setLength(1234L); + plantModel.getPaths().get(0).setLocked(true); + plantModel.getPaths().get(0).setMaxVelocity(9876L); + plantModel.getPaths().get(0).setMaxReverseVelocity(5432L); + plantModel.getPaths().get(0).getPeripheralOperations().add( + new PeripheralOperationTO() + .setLocationName("some-loc-name") + .setExecutionTrigger("AFTER_ALLOCATION") + .setCompletionRequired(true) + ); + plantModel.getPaths().get(0).getVehicleEnvelopes().add( + new VehicleEnvelopeTO() + .setKey("some-key") + .setVertices( + List.of( + new CoupleTO() + .setX(1234L) + .setY(5678L) + ) + ) + ); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V005PlantModelTO parsedModel = V005PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getPaths(), hasSize(1)); + assertThat(parsedModel.getPaths().get(0).getName(), is(equalTo("my-path"))); + assertThat(parsedModel.getPaths().get(0).getLength(), is(1234L)); + assertThat(parsedModel.getPaths().get(0).isLocked(), is(true)); + assertThat(parsedModel.getPaths().get(0).getMaxVelocity(), is(9876L)); + assertThat(parsedModel.getPaths().get(0).getMaxReverseVelocity(), is(5432L)); + assertThat(parsedModel.getPaths().get(0).getSourcePoint(), is(equalTo("some-source-point"))); + assertThat(parsedModel.getPaths().get(0).getDestinationPoint(), is(equalTo("some-dest-point"))); + assertThat(parsedModel.getPaths().get(0).getPeripheralOperations(), hasSize(1)); + assertThat( + parsedModel.getPaths().get(0).getPeripheralOperations().get(0).getLocationName(), + is(equalTo("some-loc-name")) + ); + assertThat( + parsedModel.getPaths().get(0).getPeripheralOperations().get(0).getExecutionTrigger(), + is(equalTo("AFTER_ALLOCATION")) + ); + assertThat( + parsedModel.getPaths().get(0).getPeripheralOperations().get(0).isCompletionRequired(), + is(true) + ); + assertThat(parsedModel.getPaths().get(0).getVehicleEnvelopes(), hasSize(1)); + assertThat(parsedModel.getPaths().get(0).getVehicleEnvelopes().get(0).getKey(), is("some-key")); + assertThat( + parsedModel.getPaths().get(0).getVehicleEnvelopes().get(0).getVertices(), + hasSize(1) + ); + assertThat( + parsedModel.getPaths().get(0).getVehicleEnvelopes().get(0).getVertices().get(0).getX(), + is(1234L) + ); + assertThat( + parsedModel.getPaths().get(0).getVehicleEnvelopes().get(0).getVertices().get(0).getY(), + is(5678L) + ); + } + + @Test + void persistAndMaterializeLocationTypes() + throws IOException { + plantModel.getLocationTypes().get(0).setName("my-location-type"); + plantModel.getLocationTypes().get(0).getAllowedPeripheralOperations().add( + (AllowedPeripheralOperationTO) new AllowedPeripheralOperationTO().setName("some-op") + ); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V005PlantModelTO parsedModel = V005PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getLocationTypes(), hasSize(1)); + assertThat(parsedModel.getLocationTypes().get(0).getName(), is(equalTo("my-location-type"))); + assertThat(parsedModel.getLocationTypes().get(0).getAllowedPeripheralOperations(), hasSize(1)); + assertThat( + parsedModel.getLocationTypes().get(0).getAllowedPeripheralOperations().get(0).getName(), + is(equalTo("some-op")) + ); + } + + @Test + void persistAndMaterializeLocations() + throws IOException { + plantModel.getLocations().get(0).setName("my-location"); + plantModel.getLocations().get(0).setType("some-loc-type"); + plantModel.getLocations().get(0).setLocked(true); + plantModel.getLocations().get(0).setxPosition(1L); + plantModel.getLocations().get(0).setyPosition(2L); + plantModel.getLocations().get(0).setzPosition(3L); + plantModel.getLocations().get(0).getLinks().add( + new LocationTO.Link() + .setPoint("some-point") + .setAllowedOperations( + new ArrayList<>( + List.of( + (AllowedOperationTO) new AllowedOperationTO().setName("some-op") + ) + ) + ) + ); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V005PlantModelTO parsedModel = V005PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getLocations(), hasSize(1)); + assertThat(parsedModel.getLocations().get(0).getName(), is(equalTo("my-location"))); + assertThat(parsedModel.getLocations().get(0).getType(), is(equalTo("some-loc-type"))); + assertThat(parsedModel.getLocations().get(0).isLocked(), is(true)); + assertThat(parsedModel.getLocations().get(0).getxPosition(), is(1L)); + assertThat(parsedModel.getLocations().get(0).getyPosition(), is(2L)); + assertThat(parsedModel.getLocations().get(0).getzPosition(), is(3L)); + assertThat(parsedModel.getLocations().get(0).getLinks(), hasSize(1)); + assertThat( + parsedModel.getLocations().get(0).getLinks().get(0).getPoint(), + is(equalTo("some-point")) + ); + assertThat( + parsedModel.getLocations().get(0).getLinks().get(0).getAllowedOperations(), + hasSize(1) + ); + assertThat( + parsedModel.getLocations().get(0).getLinks().get(0).getAllowedOperations().get(0).getName(), + is(equalTo("some-op")) + ); + } + + @Test + void persistAndMaterializeBlocks() + throws IOException { + plantModel.getBlocks().get(0).setName("my-block"); + plantModel.getBlocks().get(0).setType("SAME_DIRECTION_ONLY"); + plantModel.getBlocks().get(0).getMembers().add( + (MemberTO) new MemberTO().setName("some-member") + ); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V005PlantModelTO parsedModel = V005PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getBlocks(), hasSize(1)); + assertThat(parsedModel.getBlocks().get(0).getName(), is(equalTo("my-block"))); + assertThat(parsedModel.getBlocks().get(0).getType(), is(equalTo("SAME_DIRECTION_ONLY"))); + assertThat(parsedModel.getBlocks().get(0).getMembers(), hasSize(1)); + assertThat( + parsedModel.getBlocks().get(0).getMembers().get(0).getName(), + is(equalTo("some-member")) + ); + } + + @Test + void persistAndMaterializeVehicles() + throws IOException { + plantModel.getVehicles().get(0).setName("my-vehicle"); + plantModel.getVehicles().get(0).setLength(1234L); + plantModel.getVehicles().get(0).setMaxVelocity(333); + plantModel.getVehicles().get(0).setMaxReverseVelocity(444); + plantModel.getVehicles().get(0).setEnergyLevelCritical(33L); + plantModel.getVehicles().get(0).setEnergyLevelGood(88L); + plantModel.getVehicles().get(0).setEnergyLevelSufficientlyRecharged(66L); + plantModel.getVehicles().get(0).setEnergyLevelFullyRecharged(99L); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V005PlantModelTO parsedModel = V005PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getVehicles(), hasSize(1)); + assertThat(parsedModel.getVehicles().get(0).getName(), is(equalTo("my-vehicle"))); + assertThat(parsedModel.getVehicles().get(0).getLength(), is(1234L)); + assertThat(parsedModel.getVehicles().get(0).getMaxVelocity(), is(333)); + assertThat(parsedModel.getVehicles().get(0).getMaxReverseVelocity(), is(444)); + assertThat(parsedModel.getVehicles().get(0).getEnergyLevelCritical(), is(33L)); + assertThat(parsedModel.getVehicles().get(0).getEnergyLevelGood(), is(88L)); + assertThat(parsedModel.getVehicles().get(0).getEnergyLevelSufficientlyRecharged(), is(66L)); + assertThat(parsedModel.getVehicles().get(0).getEnergyLevelFullyRecharged(), is(99L)); + } + + private String toXml(V005PlantModelTO plantModel) + throws IOException { + StringWriter writer = new StringWriter(); + plantModel.toXml(writer); + return writer.toString(); + } + + private V005PlantModelTO createPlantModel() { + return (V005PlantModelTO) new V005PlantModelTO() + .setName(UUID.randomUUID().toString()) + .setPoints( + new ArrayList<>( + List.of( + (PointTO) new PointTO() + .setPointLayout( + new PointTO.PointLayout() + .setxPosition(1L) + .setyPosition(2L) + .setxLabelOffset(20L) + .setyLabelOffset(20L) + .setLayerId(0) + ) + .setName(UUID.randomUUID().toString()), + (PointTO) new PointTO() + .setPointLayout( + new PointTO.PointLayout() + .setxPosition(4L) + .setyPosition(5L) + .setxLabelOffset(20L) + .setyLabelOffset(20L) + .setLayerId(0) + ) + .setName(UUID.randomUUID().toString()) + ) + ) + ) + .setPaths( + new ArrayList<>( + List.of( + (PathTO) new PathTO() + .setPathLayout( + new PathTO.PathLayout() + .setConnectionType("DIRECT") + .setLayerId(0) + ) + .setName(UUID.randomUUID().toString()) + ) + ) + ) + .setLocationTypes( + new ArrayList<>( + List.of( + (LocationTypeTO) new LocationTypeTO() + .setLocationTypeLayout( + new LocationTypeTO.LocationTypeLayout() + .setLocationRepresentation("LOAD_TRANSFER_GENERIC") + ) + .setName(UUID.randomUUID().toString()) + ) + ) + ) + .setLocations( + List.of( + (LocationTO) new LocationTO() + .setLocationLayout( + new LocationTO.LocationLayout() + .setxPosition(100L) + .setyPosition(200L) + .setxLabelOffset(20L) + .setyLabelOffset(20L) + .setLocationRepresentation("LOAD_TRANSFER_GENERIC") + .setLayerId(0) + ) + .setName(UUID.randomUUID().toString()) + ) + ) + .setBlocks( + List.of( + (BlockTO) new BlockTO() + .setBlockLayout( + new BlockTO.BlockLayout() + .setColor("#FF0000") + ) + .setName(UUID.randomUUID().toString()) + ) + ) + .setVehicles( + List.of( + (VehicleTO) new VehicleTO() + .setVehicleLayout( + new VehicleTO.VehicleLayout() + .setColor("#FF0000") + ) + .setName(UUID.randomUUID().toString()) + ) + ) + .setVisualLayout( + (VisualLayoutTO) new VisualLayoutTO() + .setScaleX(50.0f) + .setScaleY(50.0f) + .setLayers( + List.of( + new VisualLayoutTO.Layer() + .setId(0) + .setOrdinal(0) + .setVisible(Boolean.TRUE) + .setName(UUID.randomUUID().toString()) + .setGroupId(0) + ) + ) + .setLayerGroups( + List.of( + new VisualLayoutTO.LayerGroup() + .setId(0) + .setName(UUID.randomUUID().toString()) + .setVisible(Boolean.TRUE) + ) + ) + .setName(UUID.randomUUID().toString()) + ) + .setVersion("0.0.5"); + } +} diff --git a/opentcs-common/src/test/java/org/opentcs/util/persistence/v6/V6DrivingCoursePersistenceTest.java b/opentcs-common/src/test/java/org/opentcs/util/persistence/v6/V6DrivingCoursePersistenceTest.java new file mode 100644 index 0000000..f1da45c --- /dev/null +++ b/opentcs-common/src/test/java/org/opentcs/util/persistence/v6/V6DrivingCoursePersistenceTest.java @@ -0,0 +1,583 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.util.persistence.v6; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.util.persistence.v6.BlockTO.BlockLayout; +import org.opentcs.util.persistence.v6.LocationTO.LocationLayout; +import org.opentcs.util.persistence.v6.LocationTypeTO.LocationTypeLayout; +import org.opentcs.util.persistence.v6.PathTO.ControlPoint; +import org.opentcs.util.persistence.v6.PathTO.PathLayout; +import org.opentcs.util.persistence.v6.PointTO.PointLayout; +import org.opentcs.util.persistence.v6.VehicleTO.VehicleLayout; + +/** + */ +public class V6DrivingCoursePersistenceTest { + + private V6PlantModelTO plantModel; + + @BeforeEach + void setUp() { + plantModel = createPlantModel(); + } + + @Test + void persistAndMaterializeModelAttributes() + throws IOException { + plantModel.setVersion("6.0.0"); + plantModel.getProperties().add(new PropertyTO().setName("some-prop").setValue("some-prop-val")); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V6PlantModelTO parsedModel = V6PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getVersion(), is(equalTo("6.0.0"))); + assertThat(parsedModel.getProperties(), hasSize(1)); + assertThat(parsedModel.getProperties().get(0).getName(), is(equalTo("some-prop"))); + assertThat(parsedModel.getProperties().get(0).getValue(), is(equalTo("some-prop-val"))); + } + + @Test + void persistAndMaterializePoints() + throws IOException { + plantModel.getPoints().get(0).setName("my-point"); + plantModel.getPoints().get(0).setPositionX(1L); + plantModel.getPoints().get(0).setPositionY(2L); + plantModel.getPoints().get(0).setPositionZ(3L); + plantModel.getPoints().get(0).setVehicleOrientationAngle(12.34f); + plantModel.getPoints().get(0).setType("PARK_POSITION"); + plantModel.getPoints().get(0).setMaxVehicleBoundingBox( + new BoundingBoxTO() + .setLength(100) + .setWidth(200) + .setHeight(150) + .setReferenceOffsetX(0) + .setReferenceOffsetY(10) + ); + plantModel.getPoints().get(0).getVehicleEnvelopes().add( + new VehicleEnvelopeTO() + .setKey("some-key") + .setVertices( + List.of( + new CoupleTO() + .setX(10L) + .setY(20L) + ) + ) + ); + plantModel.getPoints().get(0).getOutgoingPaths() + .add(new PointTO.OutgoingPath().setName("some-path")); + plantModel.getPoints().get(0).getProperties().add( + new PropertyTO() + .setName("some-name") + .setValue("some-value") + ); + plantModel.getPoints().get(0).setPointLayout( + new PointLayout() + .setPositionX(20L) + .setPositionY(30L) + .setLabelOffsetX(10L) + .setLabelOffsetY(20L) + .setLayerId(11) + ); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V6PlantModelTO parsedModel = V6PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getPoints(), hasSize(2)); + assertThat(parsedModel.getPoints().get(0).getName(), is(equalTo("my-point"))); + assertThat(parsedModel.getPoints().get(0).getPositionX(), is(1L)); + assertThat(parsedModel.getPoints().get(0).getPositionY(), is(2L)); + assertThat(parsedModel.getPoints().get(0).getPositionZ(), is(3L)); + assertThat(parsedModel.getPoints().get(0).getVehicleOrientationAngle(), is(12.34f)); + assertThat(parsedModel.getPoints().get(0).getType(), is(equalTo("PARK_POSITION"))); + assertThat(parsedModel.getPoints().get(0).getMaxVehicleBoundingBox().getLength(), is(100L)); + assertThat(parsedModel.getPoints().get(0).getMaxVehicleBoundingBox().getWidth(), is(200L)); + assertThat(parsedModel.getPoints().get(0).getMaxVehicleBoundingBox().getHeight(), is(150L)); + assertThat( + parsedModel.getPoints().get(0).getMaxVehicleBoundingBox().getReferenceOffsetX(), is(0L) + ); + assertThat( + parsedModel.getPoints().get(0).getMaxVehicleBoundingBox().getReferenceOffsetY(), is(10L) + ); + assertThat(parsedModel.getPoints().get(0).getVehicleEnvelopes(), hasSize(1)); + assertThat( + parsedModel.getPoints().get(0).getVehicleEnvelopes().get(0).getKey(), is("some-key") + ); + assertThat( + parsedModel.getPoints().get(0).getVehicleEnvelopes().get(0).getVertices(), hasSize(1) + ); + assertThat( + parsedModel.getPoints().get(0).getVehicleEnvelopes().get(0).getVertices().get(0).getX(), + is(10L) + ); + assertThat( + parsedModel.getPoints().get(0).getVehicleEnvelopes().get(0).getVertices().get(0).getY(), + is(20L) + ); + assertThat(parsedModel.getPoints().get(0).getOutgoingPaths(), hasSize(1)); + assertThat( + parsedModel.getPoints().get(0).getOutgoingPaths().get(0).getName(), + is(equalTo("some-path")) + ); + assertThat(parsedModel.getPoints().get(0).getProperties(), hasSize(1)); + assertThat(parsedModel.getPoints().get(0).getProperties().get(0).getName(), is("some-name")); + assertThat(parsedModel.getPoints().get(0).getProperties().get(0).getValue(), is("some-value")); + assertThat(parsedModel.getPoints().get(0).getPointLayout().getPositionX(), is(20L)); + assertThat(parsedModel.getPoints().get(0).getPointLayout().getPositionY(), is(30L)); + assertThat(parsedModel.getPoints().get(0).getPointLayout().getLabelOffsetX(), is(10L)); + assertThat(parsedModel.getPoints().get(0).getPointLayout().getLabelOffsetY(), is(20L)); + assertThat(parsedModel.getPoints().get(0).getPointLayout().getLayerId(), is(11)); + } + + @Test + void persistAndMaterializePaths() + throws IOException { + plantModel.getPaths().get(0).setName("my-path"); + plantModel.getPaths().get(0).setSourcePoint("some-source-point"); + plantModel.getPaths().get(0).setDestinationPoint("some-dest-point"); + plantModel.getPaths().get(0).setLength(1234L); + plantModel.getPaths().get(0).setLocked(true); + plantModel.getPaths().get(0).setMaxVelocity(9876L); + plantModel.getPaths().get(0).setMaxReverseVelocity(5432L); + plantModel.getPaths().get(0).getPeripheralOperations().add( + new PeripheralOperationTO() + .setLocationName("some-loc-name") + .setExecutionTrigger("AFTER_ALLOCATION") + .setCompletionRequired(true) + ); + plantModel.getPaths().get(0).getVehicleEnvelopes().add( + new VehicleEnvelopeTO() + .setKey("some-key") + .setVertices( + List.of( + new CoupleTO() + .setX(1234L) + .setY(5678L) + ) + ) + ); + plantModel.getPaths().get(0).getProperties().add( + new PropertyTO() + .setName("some-name") + .setValue("some-value") + ); + plantModel.getPaths().get(0).setPathLayout( + new PathLayout() + .setConnectionType("POLYPATH") + .setLayerId(1) + .setControlPoints( + List.of( + new ControlPoint() + .setX(10L) + .setY(20L) + ) + ) + ); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V6PlantModelTO parsedModel = V6PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getPaths(), hasSize(1)); + assertThat(parsedModel.getPaths().get(0).getName(), is(equalTo("my-path"))); + assertThat(parsedModel.getPaths().get(0).getLength(), is(1234L)); + assertThat(parsedModel.getPaths().get(0).isLocked(), is(true)); + assertThat(parsedModel.getPaths().get(0).getMaxVelocity(), is(9876L)); + assertThat(parsedModel.getPaths().get(0).getMaxReverseVelocity(), is(5432L)); + assertThat(parsedModel.getPaths().get(0).getSourcePoint(), is(equalTo("some-source-point"))); + assertThat(parsedModel.getPaths().get(0).getDestinationPoint(), is(equalTo("some-dest-point"))); + assertThat(parsedModel.getPaths().get(0).getPeripheralOperations(), hasSize(1)); + assertThat( + parsedModel.getPaths().get(0).getPeripheralOperations().get(0).getLocationName(), + is(equalTo("some-loc-name")) + ); + assertThat( + parsedModel.getPaths().get(0).getPeripheralOperations().get(0).getExecutionTrigger(), + is(equalTo("AFTER_ALLOCATION")) + ); + assertThat( + parsedModel.getPaths().get(0).getPeripheralOperations().get(0).isCompletionRequired(), + is(true) + ); + assertThat(parsedModel.getPaths().get(0).getVehicleEnvelopes(), hasSize(1)); + assertThat(parsedModel.getPaths().get(0).getVehicleEnvelopes().get(0).getKey(), is("some-key")); + assertThat( + parsedModel.getPaths().get(0).getVehicleEnvelopes().get(0).getVertices(), + hasSize(1) + ); + assertThat( + parsedModel.getPaths().get(0).getVehicleEnvelopes().get(0).getVertices().get(0).getX(), + is(1234L) + ); + assertThat( + parsedModel.getPaths().get(0).getVehicleEnvelopes().get(0).getVertices().get(0).getY(), + is(5678L) + ); + assertThat(parsedModel.getPaths().get(0).getProperties(), hasSize(1)); + assertThat(parsedModel.getPaths().get(0).getProperties().get(0).getName(), is("some-name")); + assertThat(parsedModel.getPaths().get(0).getProperties().get(0).getValue(), is("some-value")); + assertThat(parsedModel.getPaths().get(0).getPathLayout().getConnectionType(), is("POLYPATH")); + assertThat(parsedModel.getPaths().get(0).getPathLayout().getLayerId(), is(1)); + assertThat(parsedModel.getPaths().get(0).getPathLayout().getControlPoints(), hasSize(1)); + assertThat( + parsedModel.getPaths().get(0).getPathLayout().getControlPoints().get(0).getX(), is(10L) + ); + assertThat( + parsedModel.getPaths().get(0).getPathLayout().getControlPoints().get(0).getY(), is(20L) + ); + } + + @Test + void persistAndMaterializeLocationTypes() + throws IOException { + plantModel.getLocationTypes().get(0).setName("my-location-type"); + plantModel.getLocationTypes().get(0).getAllowedOperations().add( + (AllowedOperationTO) new AllowedOperationTO().setName("some-op") + ); + plantModel.getLocationTypes().get(0).getAllowedPeripheralOperations().add( + (AllowedPeripheralOperationTO) new AllowedPeripheralOperationTO().setName("some-op") + ); + plantModel.getLocationTypes().get(0).getProperties().add( + new PropertyTO() + .setName("some-name") + .setValue("some-value") + ); + plantModel.getLocationTypes().get(0).setLocationTypeLayout( + new LocationTypeLayout() + .setLocationRepresentation("LOAD_TRANSFER_GENERIC") + ); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V6PlantModelTO parsedModel = V6PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getLocationTypes(), hasSize(1)); + assertThat(parsedModel.getLocationTypes().get(0).getName(), is(equalTo("my-location-type"))); + assertThat(parsedModel.getLocationTypes().get(0).getAllowedOperations(), hasSize(1)); + assertThat( + parsedModel.getLocationTypes().get(0).getAllowedOperations().get(0).getName(), + is(equalTo("some-op")) + ); + assertThat(parsedModel.getLocationTypes().get(0).getAllowedPeripheralOperations(), hasSize(1)); + assertThat( + parsedModel.getLocationTypes().get(0).getAllowedPeripheralOperations().get(0).getName(), + is(equalTo("some-op")) + ); + assertThat(parsedModel.getLocationTypes().get(0).getProperties(), hasSize(1)); + assertThat( + parsedModel.getLocationTypes().get(0).getProperties().get(0).getName(), is("some-name") + ); + assertThat( + parsedModel.getLocationTypes().get(0).getProperties().get(0).getValue(), is("some-value") + ); + assertThat( + parsedModel.getLocationTypes().get(0).getLocationTypeLayout().getLocationRepresentation(), + is("LOAD_TRANSFER_GENERIC") + ); + } + + @Test + void persistAndMaterializeLocations() + throws IOException { + plantModel.getLocations().get(0).setName("my-location"); + plantModel.getLocations().get(0).setType("some-loc-type"); + plantModel.getLocations().get(0).setLocked(true); + plantModel.getLocations().get(0).setPositionX(1L); + plantModel.getLocations().get(0).setPositionY(2L); + plantModel.getLocations().get(0).setPositionZ(3L); + plantModel.getLocations().get(0).getLinks().add( + new LocationTO.Link() + .setPoint("some-point") + .setAllowedOperations( + new ArrayList<>( + List.of( + (AllowedOperationTO) new AllowedOperationTO().setName("some-op") + ) + ) + ) + ); + plantModel.getLocations().get(0).getProperties().add( + new PropertyTO() + .setName("some-name") + .setValue("some-value") + ); + plantModel.getLocations().get(0).setLocationLayout( + new LocationLayout() + .setPositionX(20L) + .setPositionY(30L) + .setLabelOffsetX(10L) + .setLabelOffsetY(20L) + .setLocationRepresentation("LOAD_TRANSFER_GENERIC") + .setLayerId(11) + ); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V6PlantModelTO parsedModel = V6PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getLocations(), hasSize(1)); + assertThat(parsedModel.getLocations().get(0).getName(), is(equalTo("my-location"))); + assertThat(parsedModel.getLocations().get(0).getType(), is(equalTo("some-loc-type"))); + assertThat(parsedModel.getLocations().get(0).isLocked(), is(true)); + assertThat(parsedModel.getLocations().get(0).getPositionX(), is(1L)); + assertThat(parsedModel.getLocations().get(0).getPositionY(), is(2L)); + assertThat(parsedModel.getLocations().get(0).getPositionZ(), is(3L)); + assertThat(parsedModel.getLocations().get(0).getLinks(), hasSize(1)); + assertThat( + parsedModel.getLocations().get(0).getLinks().get(0).getPoint(), + is(equalTo("some-point")) + ); + assertThat( + parsedModel.getLocations().get(0).getLinks().get(0).getAllowedOperations(), + hasSize(1) + ); + assertThat( + parsedModel.getLocations().get(0).getLinks().get(0).getAllowedOperations().get(0).getName(), + is(equalTo("some-op")) + ); + assertThat(parsedModel.getLocations().get(0).getProperties(), hasSize(1)); + assertThat(parsedModel.getLocations().get(0).getProperties().get(0).getName(), is("some-name")); + assertThat( + parsedModel.getLocations().get(0).getProperties().get(0).getValue(), is("some-value") + ); + assertThat(parsedModel.getLocations().get(0).getLocationLayout().getPositionX(), is(20L)); + assertThat(parsedModel.getLocations().get(0).getLocationLayout().getPositionY(), is(30L)); + assertThat(parsedModel.getLocations().get(0).getLocationLayout().getLabelOffsetX(), is(10L)); + assertThat(parsedModel.getLocations().get(0).getLocationLayout().getLabelOffsetY(), is(20L)); + assertThat( + parsedModel.getLocations().get(0).getLocationLayout().getLocationRepresentation(), + is("LOAD_TRANSFER_GENERIC") + ); + assertThat(parsedModel.getLocations().get(0).getLocationLayout().getLayerId(), is(11)); + } + + @Test + void persistAndMaterializeBlocks() + throws IOException { + plantModel.getBlocks().get(0).setName("my-block"); + plantModel.getBlocks().get(0).setType("SAME_DIRECTION_ONLY"); + plantModel.getBlocks().get(0).getMembers().add( + (MemberTO) new MemberTO().setName("some-member") + ); + plantModel.getBlocks().get(0).getProperties().add( + new PropertyTO() + .setName("some-name") + .setValue("some-value") + ); + plantModel.getBlocks().get(0).setBlockLayout(new BlockLayout().setColor("#FFFFFF")); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V6PlantModelTO parsedModel = V6PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getBlocks(), hasSize(1)); + assertThat(parsedModel.getBlocks().get(0).getName(), is(equalTo("my-block"))); + assertThat(parsedModel.getBlocks().get(0).getType(), is(equalTo("SAME_DIRECTION_ONLY"))); + assertThat(parsedModel.getBlocks().get(0).getMembers(), hasSize(1)); + assertThat( + parsedModel.getBlocks().get(0).getMembers().get(0).getName(), + is(equalTo("some-member")) + ); + assertThat(parsedModel.getBlocks().get(0).getProperties(), hasSize(1)); + assertThat(parsedModel.getBlocks().get(0).getProperties().get(0).getName(), is("some-name")); + assertThat(parsedModel.getBlocks().get(0).getProperties().get(0).getValue(), is("some-value")); + assertThat(parsedModel.getBlocks().get(0).getBlockLayout().getColor(), is("#FFFFFF")); + } + + @Test + void persistAndMaterializeVehicles() + throws IOException { + plantModel.getVehicles().get(0).setName("my-vehicle"); + plantModel.getVehicles().get(0).setBoundingBox( + new BoundingBoxTO() + .setLength(100) + .setWidth(200) + .setHeight(150) + .setReferenceOffsetX(0) + .setReferenceOffsetY(10) + ); + plantModel.getVehicles().get(0).setMaxVelocity(333); + plantModel.getVehicles().get(0).setMaxReverseVelocity(444); + plantModel.getVehicles().get(0).setEnergyLevelCritical(33L); + plantModel.getVehicles().get(0).setEnergyLevelGood(88L); + plantModel.getVehicles().get(0).setEnergyLevelSufficientlyRecharged(66L); + plantModel.getVehicles().get(0).setEnergyLevelFullyRecharged(99L); + plantModel.getVehicles().get(0).setEnvelopeKey("some-key"); + plantModel.getVehicles().get(0).getProperties().add( + new PropertyTO() + .setName("some-name") + .setValue("some-value") + ); + plantModel.getVehicles().get(0).setVehicleLayout(new VehicleLayout().setColor("#FFFFFF")); + + // Write to XML... + String xmlOutput = toXml(plantModel); + // ...then parse it back and verify it contains the same elements. + V6PlantModelTO parsedModel = V6PlantModelTO.fromXml(new StringReader(xmlOutput)); + + assertThat(parsedModel.getVehicles(), hasSize(1)); + assertThat(parsedModel.getVehicles().get(0).getName(), is(equalTo("my-vehicle"))); + assertThat(parsedModel.getVehicles().get(0).getBoundingBox().getLength(), is(100L)); + assertThat(parsedModel.getVehicles().get(0).getBoundingBox().getWidth(), is(200L)); + assertThat(parsedModel.getVehicles().get(0).getBoundingBox().getHeight(), is(150L)); + assertThat(parsedModel.getVehicles().get(0).getBoundingBox().getReferenceOffsetX(), is(0L)); + assertThat(parsedModel.getVehicles().get(0).getBoundingBox().getReferenceOffsetY(), is(10L)); + assertThat(parsedModel.getVehicles().get(0).getMaxVelocity(), is(333)); + assertThat(parsedModel.getVehicles().get(0).getMaxReverseVelocity(), is(444)); + assertThat(parsedModel.getVehicles().get(0).getEnergyLevelCritical(), is(33L)); + assertThat(parsedModel.getVehicles().get(0).getEnergyLevelGood(), is(88L)); + assertThat(parsedModel.getVehicles().get(0).getEnergyLevelSufficientlyRecharged(), is(66L)); + assertThat(parsedModel.getVehicles().get(0).getEnergyLevelFullyRecharged(), is(99L)); + assertThat(parsedModel.getVehicles().get(0).getEnvelopeKey(), is("some-key")); + assertThat(parsedModel.getVehicles().get(0).getProperties(), hasSize(1)); + assertThat(parsedModel.getVehicles().get(0).getProperties().get(0).getName(), is("some-name")); + assertThat( + parsedModel.getVehicles().get(0).getProperties().get(0).getValue(), is("some-value") + ); + assertThat(parsedModel.getVehicles().get(0).getVehicleLayout().getColor(), is("#FFFFFF")); + } + + private String toXml(V6PlantModelTO plantModel) + throws IOException { + StringWriter writer = new StringWriter(); + plantModel.toXml(writer); + return writer.toString(); + } + + private V6PlantModelTO createPlantModel() { + return (V6PlantModelTO) new V6PlantModelTO() + .setName(UUID.randomUUID().toString()) + .setPoints( + new ArrayList<>( + List.of( + (PointTO) new PointTO() + .setPointLayout( + new PointTO.PointLayout() + .setPositionX(1L) + .setPositionY(2L) + .setLabelOffsetX(20L) + .setLabelOffsetY(20L) + .setLayerId(0) + ) + .setName(UUID.randomUUID().toString()), + (PointTO) new PointTO() + .setPointLayout( + new PointTO.PointLayout() + .setPositionX(4L) + .setPositionY(5L) + .setLabelOffsetX(20L) + .setLabelOffsetY(20L) + .setLayerId(0) + ) + .setName(UUID.randomUUID().toString()) + ) + ) + ) + .setPaths( + new ArrayList<>( + List.of( + (PathTO) new PathTO() + .setPathLayout( + new PathTO.PathLayout() + .setConnectionType("DIRECT") + .setLayerId(0) + ) + .setName(UUID.randomUUID().toString()) + ) + ) + ) + .setLocationTypes( + new ArrayList<>( + List.of( + (LocationTypeTO) new LocationTypeTO() + .setLocationTypeLayout( + new LocationTypeTO.LocationTypeLayout() + .setLocationRepresentation("LOAD_TRANSFER_GENERIC") + ) + .setName(UUID.randomUUID().toString()) + ) + ) + ) + .setLocations( + List.of( + (LocationTO) new LocationTO() + .setLocationLayout( + new LocationTO.LocationLayout() + .setPositionX(100L) + .setPositionY(200L) + .setLabelOffsetX(20L) + .setLabelOffsetY(20L) + .setLocationRepresentation("LOAD_TRANSFER_GENERIC") + .setLayerId(0) + ) + .setName(UUID.randomUUID().toString()) + ) + ) + .setBlocks( + List.of( + (BlockTO) new BlockTO() + .setBlockLayout( + new BlockTO.BlockLayout() + .setColor("#FF0000") + ) + .setName(UUID.randomUUID().toString()) + ) + ) + .setVehicles( + List.of( + (VehicleTO) new VehicleTO() + .setVehicleLayout( + new VehicleTO.VehicleLayout() + .setColor("#FF0000") + ) + .setName(UUID.randomUUID().toString()) + ) + ) + .setVisualLayout( + (VisualLayoutTO) new VisualLayoutTO() + .setScaleX(50.0f) + .setScaleY(50.0f) + .setLayers( + List.of( + new VisualLayoutTO.Layer() + .setId(0) + .setOrdinal(0) + .setVisible(Boolean.TRUE) + .setName(UUID.randomUUID().toString()) + .setGroupId(0) + ) + ) + .setLayerGroups( + List.of( + new VisualLayoutTO.LayerGroup() + .setId(0) + .setName(UUID.randomUUID().toString()) + .setVisible(Boolean.TRUE) + ) + ) + .setName(UUID.randomUUID().toString()) + ) + .setVersion("6.0.0"); + } +} diff --git a/opentcs-documentation/build.gradle b/opentcs-documentation/build.gradle new file mode 100644 index 0000000..8365bb7 --- /dev/null +++ b/opentcs-documentation/build.gradle @@ -0,0 +1,306 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +plugins { + id 'org.hidetake.swagger.generator' version '2.19.2' + // To use AsciiDoctor for documentation + id 'org.asciidoctor.jvm.convert' version '3.3.2' + id 'org.asciidoctor.jvm.pdf' version '3.3.2' +} + +evaluationDependsOn(':opentcs-api-base') +evaluationDependsOn(':opentcs-api-injection') +evaluationDependsOn(':opentcs-common') +evaluationDependsOn(':opentcs-kernel') +evaluationDependsOn(':opentcs-kernelcontrolcenter') +evaluationDependsOn(':opentcs-modeleditor') +evaluationDependsOn(':opentcs-operationsdesk') +evaluationDependsOn(':opentcs-peripheralcommadapter-loopback') +evaluationDependsOn(':opentcs-plantoverview-panel-loadgenerator') + +apply from: "${rootDir}/gradle/java-project.gradle" +apply from: "${rootDir}/gradle/java-codequality.gradle" +apply from: "${rootDir}/gradle/publishing-java.gradle" + +apply plugin: 'distribution' + +def baseApiDir = file("$buildDir/api-base") +def injectionApiDir = file("$buildDir/api-injection") +def webApiDir = file("$buildDir/swagger-ui-servicewebapiv1") +def configDocDir = file("$buildDir/configdoc") +def userManualDir = file("$buildDir/users-guide") +def devManualDir = file("$buildDir/developers-guide") +def devManualImagesDir = file("$devManualDir/images") +def releaseNotesDir = file("$buildDir/release-notes") +def assetsDir = file("src/docs/_assets") + +ext.collectableDistDir = file("$buildDir/install") + +configurations { + schemagen + configdocgen +} + +swaggerSources { + servicewebapiv1 { + inputFile = file("src/docs/service-web-api-v1/openapi.yaml") + } +} + +dependencies { + api project(':opentcs-common') + api project(':opentcs-kernel') + + schemagen project(':opentcs-kernel') + schemagen project(':opentcs-modeleditor') + schemagen project(':opentcs-operationsdesk') + schemagen jar.outputs.files + + configdocgen project(':opentcs-kernel') + configdocgen project(':opentcs-kernelcontrolcenter') + configdocgen project(':opentcs-modeleditor') + configdocgen project(':opentcs-operationsdesk') + configdocgen jar.outputs.files + + swaggerUI group: 'org.webjars', name: 'swagger-ui', version: '3.52.5' +} + +distributions { + main { + contents.from(project(':opentcs-api-base').javadoc.destinationDir) { + into('developer/api-base') + } + contents.from(project(':opentcs-api-injection').javadoc.destinationDir) { + into('developer/api-injection') + } + contents.from(webApiDir) { + into('developer/service-web-api-v1') + } + contents.from(devManualDir) { + into('developer/developers-guide') + } + contents.from(userManualDir) { + into('user') + } + contents.from(releaseNotesDir) + contents.from(assetsDir) { + into('_assets') + } + } +} + +task renderDocs { + dependsOn project(':opentcs-api-base').javadoc + dependsOn project(':opentcs-api-injection').javadoc + dependsOn 'asciidoctor' + dependsOn 'generateSwaggerUI' +} + +installDist.dependsOn renderDocs + +distTar { + enabled = false + dependsOn renderDocs + archiveBaseName = archiveBaseName.get().toLowerCase() +} + +distZip { + dependsOn renderDocs + archiveBaseName = archiveBaseName.get().toLowerCase() +} + +task release { + dependsOn build + dependsOn installDist +} + +asciidoctor { + dependsOn 'asciidoctorUsersGuide' + dependsOn 'asciidoctorDevelopersGuide' + dependsOn 'asciidoctorReleaseNotes' + enabled = false +} + +task asciidoctorReleaseNotes(type: org.asciidoctor.gradle.jvm.AsciidoctorTask) { + // Document type: article (default), book, inline, manpage) + options doctype: 'article' + // Where to look for AsciiDoc files. Default: src/docs/asciidoc + sourceDir = file("src/docs/release-notes") + baseDirFollowsSourceDir() + // Where to put the rendered documents. Default: $buildDir/asciidoc. + outputDir = releaseNotesDir + sources { + include 'index.adoc' + include 'changelog.adoc' + include 'contributors.adoc' + include 'faq.adoc' + } + outputOptions { + // Whether to put backends' outputs into separate subdirectories + separateOutputDirs = false + // Set the backends the processor should use: html5 (default), docbook, manpage, pdf, deckjs + backends = ['html5'] + } + // Attributes specific to the HTML output + attributes 'webfonts': false, // Disable webfonts + 'iconfont-remote': false, // Disable remote icon fonts + 'docinfo': "${file('src/docs/release-notes/docinfo.html')}, shared" // The docinfo file references the stylesheets for fonts to use + +} + +task asciidoctorUsersGuide(type: org.asciidoctor.gradle.jvm.AsciidoctorTask) { + dependsOn 'configdocgen' + // Document type: article (default), book, inline, manpage) + options doctype: 'book' + // Where to look for AsciiDoc files. Default: src/docs/asciidoc + sourceDir = file("src/docs/users-guide") + baseDirFollowsSourceDir() + // Where to put the rendered documents. Default: $buildDir/asciidoc. + outputDir = userManualDir + sources { + include 'opentcs-users-guide.adoc' + } + outputOptions{ + // Whether to put backends' outputs into separate subdirectories + separateOutputDirs = false + // Set the backends the processor should use: html5 (default), docbook, manpage, pdf, deckjs + backends = ['html5', 'pdf'] + } + attributes 'configdoc': configDocDir, + // Attributes specific to the HTML output + 'webfonts': false, // Disable webfonts + 'iconfont-remote': false, // Disable remote icon fonts + 'docinfo': "${file('src/docs/users-guide/docinfo.html')}, shared" // The docinfo file references the stylesheets for fonts to use + + resources { + from(sourceDir) { + include '**/*.jpg' + include '**/*.png' + include '**/*.svg' + exclude 'themes' + } + } +} + +task asciidoctorDevelopersGuide(type: org.asciidoctor.gradle.jvm.AsciidoctorTask) { + // Document type: article (default), book, inline, manpage) + options doctype: 'book' + // Where to look for AsciiDoc files. Default: src/docs/asciidoc + sourceDir = file("src/docs/developers-guide") + baseDirFollowsSourceDir() + // Where to put the rendered documents. Default: $buildDir/asciidoc. + outputDir = devManualDir + sources { + include 'opentcs-developers-guide.adoc' + } + outputOptions{ + // Whether to put backends' outputs into separate subdirectories + separateOutputDirs = false + // Set the backends the processor should use: html5 (default), docbook, manpage, pdf, deckjs + backends = ['html5', 'pdf'] + } + attributes 'documentation-testSrc': project.testSrcDir, + 'loopback-guiceSrc': project(':opentcs-commadapter-loopback').guiceSrcDir, + 'peripheral-loopback-guiceSrc': project(':opentcs-peripheralcommadapter-loopback').guiceSrcDir, + 'controlCenter-guiceSrc': project(':opentcs-kernelcontrolcenter').guiceSrcDir, + 'kernel-guiceSrc': project(':opentcs-kernel').guiceSrcDir, + 'loadGeneratorPanel-guiceSrc': project(':opentcs-plantoverview-panel-loadgenerator').guiceSrcDir, + 'imagesoutdir': devManualImagesDir, // Set the images directory for the output of asciidoctor-diagram + // Attributes specific to the HTML output + 'webfonts': false, // Disable webfonts + 'iconfont-remote': false, // Disable remote icon fonts + 'docinfo': "${file('src/docs/developers-guide/docinfo.html')}, shared" // The docinfo file references the stylesheets for fonts to use + + // 'docinfo': "${file('src/docs/docinfo.html')}, shared", // doesn't seem to work + //'docinfodir': file('src/docs'), + resources { + from(sourceDir) { + include '**/*.png' + } + } + doLast{ + delete "$devManualDir/.asciidoctor" + } +} + +task configdocgen { + dependsOn 'jar' + dependsOn ':opentcs-kernel:jar' + dependsOn ':opentcs-kernelcontrolcenter:jar' + dependsOn ':opentcs-modeleditor:jar' + dependsOn ':opentcs-operationsdesk:jar' + + doLast { + mkdir(configDocDir) + + javaexec { + classpath configurations.configdocgen + mainClass = "org.opentcs.documentation.ConfigDocGenerator" + args = [ + "org.opentcs.kernel.KernelApplicationConfiguration", + "${configDocDir}/KernelApplicationConfigurationEntries.adoc", + + "org.opentcs.kernel.OrderPoolConfiguration", + "${configDocDir}/OrderPoolConfigurationEntries.adoc", + + "org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration", + "${configDocDir}/DefaultDispatcherConfigurationEntries.adoc", + + "org.opentcs.strategies.basic.routing.DefaultRouterConfiguration", + "${configDocDir}/DefaultRouterConfigurationEntries.adoc", + + "org.opentcs.strategies.basic.routing.jgrapht.ShortestPathConfiguration", + "${configDocDir}/ShortestPathConfigurationEntries.adoc", + + "org.opentcs.strategies.basic.routing.edgeevaluator.ExplicitPropertiesConfiguration", + "${configDocDir}/ExplicitPropertiesConfigurationEntries.adoc", + + "org.opentcs.strategies.basic.peripherals.dispatching.DefaultPeripheralJobDispatcherConfiguration", + "${configDocDir}/DefaultPeripheralJobDispatcherConfigurationEntries.adoc", + + "org.opentcs.kernel.extensions.adminwebapi.AdminWebApiConfiguration", + "${configDocDir}/AdminWebApiConfigurationEntries.adoc", + + "org.opentcs.kernel.extensions.servicewebapi.ServiceWebApiConfiguration", + "${configDocDir}/ServiceWebApiConfigurationEntries.adoc", + + "org.opentcs.kernel.extensions.rmi.RmiKernelInterfaceConfiguration", + "${configDocDir}/RmiKernelInterfaceConfigurationEntries.adoc", + + "org.opentcs.kernel.SslConfiguration", + "${configDocDir}/KernelSslConfigurationEntries.adoc", + + "org.opentcs.virtualvehicle.VirtualVehicleConfiguration", + "${configDocDir}/VirtualVehicleConfigurationEntries.adoc", + + "org.opentcs.commadapter.peripheral.loopback.VirtualPeripheralConfiguration", + "${configDocDir}/VirtualPeripheralConfigurationEntries.adoc", + + "org.opentcs.kernel.extensions.watchdog.WatchdogConfiguration", + "${configDocDir}/WatchdogConfigurationEntries.adoc", + + "org.opentcs.kernelcontrolcenter.util.KernelControlCenterConfiguration", + "${configDocDir}/KernelControlCenterApplicationConfigurationEntries.adoc", + + "org.opentcs.kernelcontrolcenter.exchange.SslConfiguration", + "${configDocDir}/KccSslConfigurationEntries.adoc", + + "org.opentcs.guing.common.exchange.SslConfiguration", + "${configDocDir}/PoSslConfigurationEntries.adoc", + + "org.opentcs.modeleditor.util.ModelEditorConfiguration", + "${configDocDir}/ModelEditorConfigurationEntries.adoc", + + "org.opentcs.modeleditor.util.ElementNamingSchemeConfiguration", + "${configDocDir}/PO_ElementNamingSchemeConfigurationEntries.adoc", + + "org.opentcs.operationsdesk.util.OperationsDeskConfiguration", + "${configDocDir}/OperationsDeskConfigurationEntries.adoc" + ] + } + } +} + +publishing { + ((MavenPublication) publications.getByName(project.name + '_mavenJava')).artifact(distZip) +} diff --git a/opentcs-documentation/gradle.properties b/opentcs-documentation/gradle.properties new file mode 100644 index 0000000..14431ac --- /dev/null +++ b/opentcs-documentation/gradle.properties @@ -0,0 +1,40 @@ +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineMethodParams=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAfterDotInChainedMethodCalls=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineDisjunctiveCatchTypes=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineFor=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineImplements=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.sortMembersByVisibility=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.visibilityOrder=PUBLIC;PROTECTED;DEFAULT;PRIVATE +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.enable-indent=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineArrayInit=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineCallArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.keepGettersAndSettersTogether=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classMembersOrder=STATIC FIELD;FIELD;STATIC_INIT;CONSTRUCTOR;INSTANCE_INIT;STATIC METHOD;METHOD;STATIC CLASS;CLASS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapCommentText=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.importGroupsOrder=* +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize=4 +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineAnnotationArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineTryResources=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.preserveNewLinesInComments=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineParenthesized=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineThrows=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=100 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project diff --git a/opentcs-documentation/src/docs/_assets/css/font-awesome.css b/opentcs-documentation/src/docs/_assets/css/font-awesome.css new file mode 100644 index 0000000..7243cf1 --- /dev/null +++ b/opentcs-documentation/src/docs/_assets/css/font-awesome.css @@ -0,0 +1,2336 @@ +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ +/* FONT PATH + * -------------------------- */ +@font-face { + font-family: 'FontAwesome'; + src: url('../fonts/fontawesome/fontawesome-webfont.woff2?v=4.7.0') format('woff2'); + font-weight: normal; + font-style: normal; +} +.fa { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +/* makes the font 33% larger relative to the icon container */ +.fa-lg { + font-size: 1.33333333em; + line-height: 0.75em; + vertical-align: -15%; +} +.fa-2x { + font-size: 2em; +} +.fa-3x { + font-size: 3em; +} +.fa-4x { + font-size: 4em; +} +.fa-5x { + font-size: 5em; +} +.fa-fw { + width: 1.28571429em; + text-align: center; +} +.fa-ul { + padding-left: 0; + margin-left: 2.14285714em; + list-style-type: none; +} +.fa-ul > li { + position: relative; +} +.fa-li { + position: absolute; + left: -2.14285714em; + width: 2.14285714em; + top: 0.14285714em; + text-align: center; +} +.fa-li.fa-lg { + left: -1.85714286em; +} +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eeeeee; + border-radius: .1em; +} +.fa-pull-left { + float: left; +} +.fa-pull-right { + float: right; +} +.fa.fa-pull-left { + margin-right: .3em; +} +.fa.fa-pull-right { + margin-left: .3em; +} +/* Deprecated as of 4.4.0 */ +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.fa.pull-left { + margin-right: .3em; +} +.fa.pull-right { + margin-left: .3em; +} +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); +} +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +.fa-rotate-90 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} +.fa-rotate-180 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} +.fa-rotate-270 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); +} +.fa-flip-horizontal { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.fa-flip-vertical { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); +} +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + filter: none; +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.fa-stack-1x { + line-height: inherit; +} +.fa-stack-2x { + font-size: 2em; +} +.fa-inverse { + color: #ffffff; +} +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: "\f000"; +} +.fa-music:before { + content: "\f001"; +} +.fa-search:before { + content: "\f002"; +} +.fa-envelope-o:before { + content: "\f003"; +} +.fa-heart:before { + content: "\f004"; +} +.fa-star:before { + content: "\f005"; +} +.fa-star-o:before { + content: "\f006"; +} +.fa-user:before { + content: "\f007"; +} +.fa-film:before { + content: "\f008"; +} +.fa-th-large:before { + content: "\f009"; +} +.fa-th:before { + content: "\f00a"; +} +.fa-th-list:before { + content: "\f00b"; +} +.fa-check:before { + content: "\f00c"; +} +.fa-remove:before, +.fa-close:before, +.fa-times:before { + content: "\f00d"; +} +.fa-search-plus:before { + content: "\f00e"; +} +.fa-search-minus:before { + content: "\f010"; +} +.fa-power-off:before { + content: "\f011"; +} +.fa-signal:before { + content: "\f012"; +} +.fa-gear:before, +.fa-cog:before { + content: "\f013"; +} +.fa-trash-o:before { + content: "\f014"; +} +.fa-home:before { + content: "\f015"; +} +.fa-file-o:before { + content: "\f016"; +} +.fa-clock-o:before { + content: "\f017"; +} +.fa-road:before { + content: "\f018"; +} +.fa-download:before { + content: "\f019"; +} +.fa-arrow-circle-o-down:before { + content: "\f01a"; +} +.fa-arrow-circle-o-up:before { + content: "\f01b"; +} +.fa-inbox:before { + content: "\f01c"; +} +.fa-play-circle-o:before { + content: "\f01d"; +} +.fa-rotate-right:before, +.fa-repeat:before { + content: "\f01e"; +} +.fa-refresh:before { + content: "\f021"; +} +.fa-list-alt:before { + content: "\f022"; +} +.fa-lock:before { + content: "\f023"; +} +.fa-flag:before { + content: "\f024"; +} +.fa-headphones:before { + content: "\f025"; +} +.fa-volume-off:before { + content: "\f026"; +} +.fa-volume-down:before { + content: "\f027"; +} +.fa-volume-up:before { + content: "\f028"; +} +.fa-qrcode:before { + content: "\f029"; +} +.fa-barcode:before { + content: "\f02a"; +} +.fa-tag:before { + content: "\f02b"; +} +.fa-tags:before { + content: "\f02c"; +} +.fa-book:before { + content: "\f02d"; +} +.fa-bookmark:before { + content: "\f02e"; +} +.fa-print:before { + content: "\f02f"; +} +.fa-camera:before { + content: "\f030"; +} +.fa-font:before { + content: "\f031"; +} +.fa-bold:before { + content: "\f032"; +} +.fa-italic:before { + content: "\f033"; +} +.fa-text-height:before { + content: "\f034"; +} +.fa-text-width:before { + content: "\f035"; +} +.fa-align-left:before { + content: "\f036"; +} +.fa-align-center:before { + content: "\f037"; +} +.fa-align-right:before { + content: "\f038"; +} +.fa-align-justify:before { + content: "\f039"; +} +.fa-list:before { + content: "\f03a"; +} +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} +.fa-indent:before { + content: "\f03c"; +} +.fa-video-camera:before { + content: "\f03d"; +} +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: "\f03e"; +} +.fa-pencil:before { + content: "\f040"; +} +.fa-map-marker:before { + content: "\f041"; +} +.fa-adjust:before { + content: "\f042"; +} +.fa-tint:before { + content: "\f043"; +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: "\f044"; +} +.fa-share-square-o:before { + content: "\f045"; +} +.fa-check-square-o:before { + content: "\f046"; +} +.fa-arrows:before { + content: "\f047"; +} +.fa-step-backward:before { + content: "\f048"; +} +.fa-fast-backward:before { + content: "\f049"; +} +.fa-backward:before { + content: "\f04a"; +} +.fa-play:before { + content: "\f04b"; +} +.fa-pause:before { + content: "\f04c"; +} +.fa-stop:before { + content: "\f04d"; +} +.fa-forward:before { + content: "\f04e"; +} +.fa-fast-forward:before { + content: "\f050"; +} +.fa-step-forward:before { + content: "\f051"; +} +.fa-eject:before { + content: "\f052"; +} +.fa-chevron-left:before { + content: "\f053"; +} +.fa-chevron-right:before { + content: "\f054"; +} +.fa-plus-circle:before { + content: "\f055"; +} +.fa-minus-circle:before { + content: "\f056"; +} +.fa-times-circle:before { + content: "\f057"; +} +.fa-check-circle:before { + content: "\f058"; +} +.fa-question-circle:before { + content: "\f059"; +} +.fa-info-circle:before { + content: "\f05a"; +} +.fa-crosshairs:before { + content: "\f05b"; +} +.fa-times-circle-o:before { + content: "\f05c"; +} +.fa-check-circle-o:before { + content: "\f05d"; +} +.fa-ban:before { + content: "\f05e"; +} +.fa-arrow-left:before { + content: "\f060"; +} +.fa-arrow-right:before { + content: "\f061"; +} +.fa-arrow-up:before { + content: "\f062"; +} +.fa-arrow-down:before { + content: "\f063"; +} +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} +.fa-expand:before { + content: "\f065"; +} +.fa-compress:before { + content: "\f066"; +} +.fa-plus:before { + content: "\f067"; +} +.fa-minus:before { + content: "\f068"; +} +.fa-asterisk:before { + content: "\f069"; +} +.fa-exclamation-circle:before { + content: "\f06a"; +} +.fa-gift:before { + content: "\f06b"; +} +.fa-leaf:before { + content: "\f06c"; +} +.fa-fire:before { + content: "\f06d"; +} +.fa-eye:before { + content: "\f06e"; +} +.fa-eye-slash:before { + content: "\f070"; +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "\f071"; +} +.fa-plane:before { + content: "\f072"; +} +.fa-calendar:before { + content: "\f073"; +} +.fa-random:before { + content: "\f074"; +} +.fa-comment:before { + content: "\f075"; +} +.fa-magnet:before { + content: "\f076"; +} +.fa-chevron-up:before { + content: "\f077"; +} +.fa-chevron-down:before { + content: "\f078"; +} +.fa-retweet:before { + content: "\f079"; +} +.fa-shopping-cart:before { + content: "\f07a"; +} +.fa-folder:before { + content: "\f07b"; +} +.fa-folder-open:before { + content: "\f07c"; +} +.fa-arrows-v:before { + content: "\f07d"; +} +.fa-arrows-h:before { + content: "\f07e"; +} +.fa-bar-chart-o:before, +.fa-bar-chart:before { + content: "\f080"; +} +.fa-twitter-square:before { + content: "\f081"; +} +.fa-facebook-square:before { + content: "\f082"; +} +.fa-camera-retro:before { + content: "\f083"; +} +.fa-key:before { + content: "\f084"; +} +.fa-gears:before, +.fa-cogs:before { + content: "\f085"; +} +.fa-comments:before { + content: "\f086"; +} +.fa-thumbs-o-up:before { + content: "\f087"; +} +.fa-thumbs-o-down:before { + content: "\f088"; +} +.fa-star-half:before { + content: "\f089"; +} +.fa-heart-o:before { + content: "\f08a"; +} +.fa-sign-out:before { + content: "\f08b"; +} +.fa-linkedin-square:before { + content: "\f08c"; +} +.fa-thumb-tack:before { + content: "\f08d"; +} +.fa-external-link:before { + content: "\f08e"; +} +.fa-sign-in:before { + content: "\f090"; +} +.fa-trophy:before { + content: "\f091"; +} +.fa-github-square:before { + content: "\f092"; +} +.fa-upload:before { + content: "\f093"; +} +.fa-lemon-o:before { + content: "\f094"; +} +.fa-phone:before { + content: "\f095"; +} +.fa-square-o:before { + content: "\f096"; +} +.fa-bookmark-o:before { + content: "\f097"; +} +.fa-phone-square:before { + content: "\f098"; +} +.fa-twitter:before { + content: "\f099"; +} +.fa-facebook-f:before, +.fa-facebook:before { + content: "\f09a"; +} +.fa-github:before { + content: "\f09b"; +} +.fa-unlock:before { + content: "\f09c"; +} +.fa-credit-card:before { + content: "\f09d"; +} +.fa-feed:before, +.fa-rss:before { + content: "\f09e"; +} +.fa-hdd-o:before { + content: "\f0a0"; +} +.fa-bullhorn:before { + content: "\f0a1"; +} +.fa-bell:before { + content: "\f0f3"; +} +.fa-certificate:before { + content: "\f0a3"; +} +.fa-hand-o-right:before { + content: "\f0a4"; +} +.fa-hand-o-left:before { + content: "\f0a5"; +} +.fa-hand-o-up:before { + content: "\f0a6"; +} +.fa-hand-o-down:before { + content: "\f0a7"; +} +.fa-arrow-circle-left:before { + content: "\f0a8"; +} +.fa-arrow-circle-right:before { + content: "\f0a9"; +} +.fa-arrow-circle-up:before { + content: "\f0aa"; +} +.fa-arrow-circle-down:before { + content: "\f0ab"; +} +.fa-globe:before { + content: "\f0ac"; +} +.fa-wrench:before { + content: "\f0ad"; +} +.fa-tasks:before { + content: "\f0ae"; +} +.fa-filter:before { + content: "\f0b0"; +} +.fa-briefcase:before { + content: "\f0b1"; +} +.fa-arrows-alt:before { + content: "\f0b2"; +} +.fa-group:before, +.fa-users:before { + content: "\f0c0"; +} +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} +.fa-cloud:before { + content: "\f0c2"; +} +.fa-flask:before { + content: "\f0c3"; +} +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} +.fa-copy:before, +.fa-files-o:before { + content: "\f0c5"; +} +.fa-paperclip:before { + content: "\f0c6"; +} +.fa-save:before, +.fa-floppy-o:before { + content: "\f0c7"; +} +.fa-square:before { + content: "\f0c8"; +} +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: "\f0c9"; +} +.fa-list-ul:before { + content: "\f0ca"; +} +.fa-list-ol:before { + content: "\f0cb"; +} +.fa-strikethrough:before { + content: "\f0cc"; +} +.fa-underline:before { + content: "\f0cd"; +} +.fa-table:before { + content: "\f0ce"; +} +.fa-magic:before { + content: "\f0d0"; +} +.fa-truck:before { + content: "\f0d1"; +} +.fa-pinterest:before { + content: "\f0d2"; +} +.fa-pinterest-square:before { + content: "\f0d3"; +} +.fa-google-plus-square:before { + content: "\f0d4"; +} +.fa-google-plus:before { + content: "\f0d5"; +} +.fa-money:before { + content: "\f0d6"; +} +.fa-caret-down:before { + content: "\f0d7"; +} +.fa-caret-up:before { + content: "\f0d8"; +} +.fa-caret-left:before { + content: "\f0d9"; +} +.fa-caret-right:before { + content: "\f0da"; +} +.fa-columns:before { + content: "\f0db"; +} +.fa-unsorted:before, +.fa-sort:before { + content: "\f0dc"; +} +.fa-sort-down:before, +.fa-sort-desc:before { + content: "\f0dd"; +} +.fa-sort-up:before, +.fa-sort-asc:before { + content: "\f0de"; +} +.fa-envelope:before { + content: "\f0e0"; +} +.fa-linkedin:before { + content: "\f0e1"; +} +.fa-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} +.fa-legal:before, +.fa-gavel:before { + content: "\f0e3"; +} +.fa-dashboard:before, +.fa-tachometer:before { + content: "\f0e4"; +} +.fa-comment-o:before { + content: "\f0e5"; +} +.fa-comments-o:before { + content: "\f0e6"; +} +.fa-flash:before, +.fa-bolt:before { + content: "\f0e7"; +} +.fa-sitemap:before { + content: "\f0e8"; +} +.fa-umbrella:before { + content: "\f0e9"; +} +.fa-paste:before, +.fa-clipboard:before { + content: "\f0ea"; +} +.fa-lightbulb-o:before { + content: "\f0eb"; +} +.fa-exchange:before { + content: "\f0ec"; +} +.fa-cloud-download:before { + content: "\f0ed"; +} +.fa-cloud-upload:before { + content: "\f0ee"; +} +.fa-user-md:before { + content: "\f0f0"; +} +.fa-stethoscope:before { + content: "\f0f1"; +} +.fa-suitcase:before { + content: "\f0f2"; +} +.fa-bell-o:before { + content: "\f0a2"; +} +.fa-coffee:before { + content: "\f0f4"; +} +.fa-cutlery:before { + content: "\f0f5"; +} +.fa-file-text-o:before { + content: "\f0f6"; +} +.fa-building-o:before { + content: "\f0f7"; +} +.fa-hospital-o:before { + content: "\f0f8"; +} +.fa-ambulance:before { + content: "\f0f9"; +} +.fa-medkit:before { + content: "\f0fa"; +} +.fa-fighter-jet:before { + content: "\f0fb"; +} +.fa-beer:before { + content: "\f0fc"; +} +.fa-h-square:before { + content: "\f0fd"; +} +.fa-plus-square:before { + content: "\f0fe"; +} +.fa-angle-double-left:before { + content: "\f100"; +} +.fa-angle-double-right:before { + content: "\f101"; +} +.fa-angle-double-up:before { + content: "\f102"; +} +.fa-angle-double-down:before { + content: "\f103"; +} +.fa-angle-left:before { + content: "\f104"; +} +.fa-angle-right:before { + content: "\f105"; +} +.fa-angle-up:before { + content: "\f106"; +} +.fa-angle-down:before { + content: "\f107"; +} +.fa-desktop:before { + content: "\f108"; +} +.fa-laptop:before { + content: "\f109"; +} +.fa-tablet:before { + content: "\f10a"; +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f10b"; +} +.fa-circle-o:before { + content: "\f10c"; +} +.fa-quote-left:before { + content: "\f10d"; +} +.fa-quote-right:before { + content: "\f10e"; +} +.fa-spinner:before { + content: "\f110"; +} +.fa-circle:before { + content: "\f111"; +} +.fa-mail-reply:before, +.fa-reply:before { + content: "\f112"; +} +.fa-github-alt:before { + content: "\f113"; +} +.fa-folder-o:before { + content: "\f114"; +} +.fa-folder-open-o:before { + content: "\f115"; +} +.fa-smile-o:before { + content: "\f118"; +} +.fa-frown-o:before { + content: "\f119"; +} +.fa-meh-o:before { + content: "\f11a"; +} +.fa-gamepad:before { + content: "\f11b"; +} +.fa-keyboard-o:before { + content: "\f11c"; +} +.fa-flag-o:before { + content: "\f11d"; +} +.fa-flag-checkered:before { + content: "\f11e"; +} +.fa-terminal:before { + content: "\f120"; +} +.fa-code:before { + content: "\f121"; +} +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "\f122"; +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "\f123"; +} +.fa-location-arrow:before { + content: "\f124"; +} +.fa-crop:before { + content: "\f125"; +} +.fa-code-fork:before { + content: "\f126"; +} +.fa-unlink:before, +.fa-chain-broken:before { + content: "\f127"; +} +.fa-question:before { + content: "\f128"; +} +.fa-info:before { + content: "\f129"; +} +.fa-exclamation:before { + content: "\f12a"; +} +.fa-superscript:before { + content: "\f12b"; +} +.fa-subscript:before { + content: "\f12c"; +} +.fa-eraser:before { + content: "\f12d"; +} +.fa-puzzle-piece:before { + content: "\f12e"; +} +.fa-microphone:before { + content: "\f130"; +} +.fa-microphone-slash:before { + content: "\f131"; +} +.fa-shield:before { + content: "\f132"; +} +.fa-calendar-o:before { + content: "\f133"; +} +.fa-fire-extinguisher:before { + content: "\f134"; +} +.fa-rocket:before { + content: "\f135"; +} +.fa-maxcdn:before { + content: "\f136"; +} +.fa-chevron-circle-left:before { + content: "\f137"; +} +.fa-chevron-circle-right:before { + content: "\f138"; +} +.fa-chevron-circle-up:before { + content: "\f139"; +} +.fa-chevron-circle-down:before { + content: "\f13a"; +} +.fa-html5:before { + content: "\f13b"; +} +.fa-css3:before { + content: "\f13c"; +} +.fa-anchor:before { + content: "\f13d"; +} +.fa-unlock-alt:before { + content: "\f13e"; +} +.fa-bullseye:before { + content: "\f140"; +} +.fa-ellipsis-h:before { + content: "\f141"; +} +.fa-ellipsis-v:before { + content: "\f142"; +} +.fa-rss-square:before { + content: "\f143"; +} +.fa-play-circle:before { + content: "\f144"; +} +.fa-ticket:before { + content: "\f145"; +} +.fa-minus-square:before { + content: "\f146"; +} +.fa-minus-square-o:before { + content: "\f147"; +} +.fa-level-up:before { + content: "\f148"; +} +.fa-level-down:before { + content: "\f149"; +} +.fa-check-square:before { + content: "\f14a"; +} +.fa-pencil-square:before { + content: "\f14b"; +} +.fa-external-link-square:before { + content: "\f14c"; +} +.fa-share-square:before { + content: "\f14d"; +} +.fa-compass:before { + content: "\f14e"; +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "\f150"; +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "\f151"; +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "\f152"; +} +.fa-euro:before, +.fa-eur:before { + content: "\f153"; +} +.fa-gbp:before { + content: "\f154"; +} +.fa-dollar:before, +.fa-usd:before { + content: "\f155"; +} +.fa-rupee:before, +.fa-inr:before { + content: "\f156"; +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "\f157"; +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "\f158"; +} +.fa-won:before, +.fa-krw:before { + content: "\f159"; +} +.fa-bitcoin:before, +.fa-btc:before { + content: "\f15a"; +} +.fa-file:before { + content: "\f15b"; +} +.fa-file-text:before { + content: "\f15c"; +} +.fa-sort-alpha-asc:before { + content: "\f15d"; +} +.fa-sort-alpha-desc:before { + content: "\f15e"; +} +.fa-sort-amount-asc:before { + content: "\f160"; +} +.fa-sort-amount-desc:before { + content: "\f161"; +} +.fa-sort-numeric-asc:before { + content: "\f162"; +} +.fa-sort-numeric-desc:before { + content: "\f163"; +} +.fa-thumbs-up:before { + content: "\f164"; +} +.fa-thumbs-down:before { + content: "\f165"; +} +.fa-youtube-square:before { + content: "\f166"; +} +.fa-youtube:before { + content: "\f167"; +} +.fa-xing:before { + content: "\f168"; +} +.fa-xing-square:before { + content: "\f169"; +} +.fa-youtube-play:before { + content: "\f16a"; +} +.fa-dropbox:before { + content: "\f16b"; +} +.fa-stack-overflow:before { + content: "\f16c"; +} +.fa-instagram:before { + content: "\f16d"; +} +.fa-flickr:before { + content: "\f16e"; +} +.fa-adn:before { + content: "\f170"; +} +.fa-bitbucket:before { + content: "\f171"; +} +.fa-bitbucket-square:before { + content: "\f172"; +} +.fa-tumblr:before { + content: "\f173"; +} +.fa-tumblr-square:before { + content: "\f174"; +} +.fa-long-arrow-down:before { + content: "\f175"; +} +.fa-long-arrow-up:before { + content: "\f176"; +} +.fa-long-arrow-left:before { + content: "\f177"; +} +.fa-long-arrow-right:before { + content: "\f178"; +} +.fa-apple:before { + content: "\f179"; +} +.fa-windows:before { + content: "\f17a"; +} +.fa-android:before { + content: "\f17b"; +} +.fa-linux:before { + content: "\f17c"; +} +.fa-dribbble:before { + content: "\f17d"; +} +.fa-skype:before { + content: "\f17e"; +} +.fa-foursquare:before { + content: "\f180"; +} +.fa-trello:before { + content: "\f181"; +} +.fa-female:before { + content: "\f182"; +} +.fa-male:before { + content: "\f183"; +} +.fa-gittip:before, +.fa-gratipay:before { + content: "\f184"; +} +.fa-sun-o:before { + content: "\f185"; +} +.fa-moon-o:before { + content: "\f186"; +} +.fa-archive:before { + content: "\f187"; +} +.fa-bug:before { + content: "\f188"; +} +.fa-vk:before { + content: "\f189"; +} +.fa-weibo:before { + content: "\f18a"; +} +.fa-renren:before { + content: "\f18b"; +} +.fa-pagelines:before { + content: "\f18c"; +} +.fa-stack-exchange:before { + content: "\f18d"; +} +.fa-arrow-circle-o-right:before { + content: "\f18e"; +} +.fa-arrow-circle-o-left:before { + content: "\f190"; +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "\f191"; +} +.fa-dot-circle-o:before { + content: "\f192"; +} +.fa-wheelchair:before { + content: "\f193"; +} +.fa-vimeo-square:before { + content: "\f194"; +} +.fa-turkish-lira:before, +.fa-try:before { + content: "\f195"; +} +.fa-plus-square-o:before { + content: "\f196"; +} +.fa-space-shuttle:before { + content: "\f197"; +} +.fa-slack:before { + content: "\f198"; +} +.fa-envelope-square:before { + content: "\f199"; +} +.fa-wordpress:before { + content: "\f19a"; +} +.fa-openid:before { + content: "\f19b"; +} +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: "\f19c"; +} +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: "\f19d"; +} +.fa-yahoo:before { + content: "\f19e"; +} +.fa-google:before { + content: "\f1a0"; +} +.fa-reddit:before { + content: "\f1a1"; +} +.fa-reddit-square:before { + content: "\f1a2"; +} +.fa-stumbleupon-circle:before { + content: "\f1a3"; +} +.fa-stumbleupon:before { + content: "\f1a4"; +} +.fa-delicious:before { + content: "\f1a5"; +} +.fa-digg:before { + content: "\f1a6"; +} +.fa-pied-piper-pp:before { + content: "\f1a7"; +} +.fa-pied-piper-alt:before { + content: "\f1a8"; +} +.fa-drupal:before { + content: "\f1a9"; +} +.fa-joomla:before { + content: "\f1aa"; +} +.fa-language:before { + content: "\f1ab"; +} +.fa-fax:before { + content: "\f1ac"; +} +.fa-building:before { + content: "\f1ad"; +} +.fa-child:before { + content: "\f1ae"; +} +.fa-paw:before { + content: "\f1b0"; +} +.fa-spoon:before { + content: "\f1b1"; +} +.fa-cube:before { + content: "\f1b2"; +} +.fa-cubes:before { + content: "\f1b3"; +} +.fa-behance:before { + content: "\f1b4"; +} +.fa-behance-square:before { + content: "\f1b5"; +} +.fa-steam:before { + content: "\f1b6"; +} +.fa-steam-square:before { + content: "\f1b7"; +} +.fa-recycle:before { + content: "\f1b8"; +} +.fa-automobile:before, +.fa-car:before { + content: "\f1b9"; +} +.fa-cab:before, +.fa-taxi:before { + content: "\f1ba"; +} +.fa-tree:before { + content: "\f1bb"; +} +.fa-spotify:before { + content: "\f1bc"; +} +.fa-deviantart:before { + content: "\f1bd"; +} +.fa-soundcloud:before { + content: "\f1be"; +} +.fa-database:before { + content: "\f1c0"; +} +.fa-file-pdf-o:before { + content: "\f1c1"; +} +.fa-file-word-o:before { + content: "\f1c2"; +} +.fa-file-excel-o:before { + content: "\f1c3"; +} +.fa-file-powerpoint-o:before { + content: "\f1c4"; +} +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: "\f1c5"; +} +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: "\f1c6"; +} +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: "\f1c7"; +} +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: "\f1c8"; +} +.fa-file-code-o:before { + content: "\f1c9"; +} +.fa-vine:before { + content: "\f1ca"; +} +.fa-codepen:before { + content: "\f1cb"; +} +.fa-jsfiddle:before { + content: "\f1cc"; +} +.fa-life-bouy:before, +.fa-life-buoy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: "\f1cd"; +} +.fa-circle-o-notch:before { + content: "\f1ce"; +} +.fa-ra:before, +.fa-resistance:before, +.fa-rebel:before { + content: "\f1d0"; +} +.fa-ge:before, +.fa-empire:before { + content: "\f1d1"; +} +.fa-git-square:before { + content: "\f1d2"; +} +.fa-git:before { + content: "\f1d3"; +} +.fa-y-combinator-square:before, +.fa-yc-square:before, +.fa-hacker-news:before { + content: "\f1d4"; +} +.fa-tencent-weibo:before { + content: "\f1d5"; +} +.fa-qq:before { + content: "\f1d6"; +} +.fa-wechat:before, +.fa-weixin:before { + content: "\f1d7"; +} +.fa-send:before, +.fa-paper-plane:before { + content: "\f1d8"; +} +.fa-send-o:before, +.fa-paper-plane-o:before { + content: "\f1d9"; +} +.fa-history:before { + content: "\f1da"; +} +.fa-circle-thin:before { + content: "\f1db"; +} +.fa-header:before { + content: "\f1dc"; +} +.fa-paragraph:before { + content: "\f1dd"; +} +.fa-sliders:before { + content: "\f1de"; +} +.fa-share-alt:before { + content: "\f1e0"; +} +.fa-share-alt-square:before { + content: "\f1e1"; +} +.fa-bomb:before { + content: "\f1e2"; +} +.fa-soccer-ball-o:before, +.fa-futbol-o:before { + content: "\f1e3"; +} +.fa-tty:before { + content: "\f1e4"; +} +.fa-binoculars:before { + content: "\f1e5"; +} +.fa-plug:before { + content: "\f1e6"; +} +.fa-slideshare:before { + content: "\f1e7"; +} +.fa-twitch:before { + content: "\f1e8"; +} +.fa-yelp:before { + content: "\f1e9"; +} +.fa-newspaper-o:before { + content: "\f1ea"; +} +.fa-wifi:before { + content: "\f1eb"; +} +.fa-calculator:before { + content: "\f1ec"; +} +.fa-paypal:before { + content: "\f1ed"; +} +.fa-google-wallet:before { + content: "\f1ee"; +} +.fa-cc-visa:before { + content: "\f1f0"; +} +.fa-cc-mastercard:before { + content: "\f1f1"; +} +.fa-cc-discover:before { + content: "\f1f2"; +} +.fa-cc-amex:before { + content: "\f1f3"; +} +.fa-cc-paypal:before { + content: "\f1f4"; +} +.fa-cc-stripe:before { + content: "\f1f5"; +} +.fa-bell-slash:before { + content: "\f1f6"; +} +.fa-bell-slash-o:before { + content: "\f1f7"; +} +.fa-trash:before { + content: "\f1f8"; +} +.fa-copyright:before { + content: "\f1f9"; +} +.fa-at:before { + content: "\f1fa"; +} +.fa-eyedropper:before { + content: "\f1fb"; +} +.fa-paint-brush:before { + content: "\f1fc"; +} +.fa-birthday-cake:before { + content: "\f1fd"; +} +.fa-area-chart:before { + content: "\f1fe"; +} +.fa-pie-chart:before { + content: "\f200"; +} +.fa-line-chart:before { + content: "\f201"; +} +.fa-lastfm:before { + content: "\f202"; +} +.fa-lastfm-square:before { + content: "\f203"; +} +.fa-toggle-off:before { + content: "\f204"; +} +.fa-toggle-on:before { + content: "\f205"; +} +.fa-bicycle:before { + content: "\f206"; +} +.fa-bus:before { + content: "\f207"; +} +.fa-ioxhost:before { + content: "\f208"; +} +.fa-angellist:before { + content: "\f209"; +} +.fa-cc:before { + content: "\f20a"; +} +.fa-shekel:before, +.fa-sheqel:before, +.fa-ils:before { + content: "\f20b"; +} +.fa-meanpath:before { + content: "\f20c"; +} +.fa-buysellads:before { + content: "\f20d"; +} +.fa-connectdevelop:before { + content: "\f20e"; +} +.fa-dashcube:before { + content: "\f210"; +} +.fa-forumbee:before { + content: "\f211"; +} +.fa-leanpub:before { + content: "\f212"; +} +.fa-sellsy:before { + content: "\f213"; +} +.fa-shirtsinbulk:before { + content: "\f214"; +} +.fa-simplybuilt:before { + content: "\f215"; +} +.fa-skyatlas:before { + content: "\f216"; +} +.fa-cart-plus:before { + content: "\f217"; +} +.fa-cart-arrow-down:before { + content: "\f218"; +} +.fa-diamond:before { + content: "\f219"; +} +.fa-ship:before { + content: "\f21a"; +} +.fa-user-secret:before { + content: "\f21b"; +} +.fa-motorcycle:before { + content: "\f21c"; +} +.fa-street-view:before { + content: "\f21d"; +} +.fa-heartbeat:before { + content: "\f21e"; +} +.fa-venus:before { + content: "\f221"; +} +.fa-mars:before { + content: "\f222"; +} +.fa-mercury:before { + content: "\f223"; +} +.fa-intersex:before, +.fa-transgender:before { + content: "\f224"; +} +.fa-transgender-alt:before { + content: "\f225"; +} +.fa-venus-double:before { + content: "\f226"; +} +.fa-mars-double:before { + content: "\f227"; +} +.fa-venus-mars:before { + content: "\f228"; +} +.fa-mars-stroke:before { + content: "\f229"; +} +.fa-mars-stroke-v:before { + content: "\f22a"; +} +.fa-mars-stroke-h:before { + content: "\f22b"; +} +.fa-neuter:before { + content: "\f22c"; +} +.fa-genderless:before { + content: "\f22d"; +} +.fa-facebook-official:before { + content: "\f230"; +} +.fa-pinterest-p:before { + content: "\f231"; +} +.fa-whatsapp:before { + content: "\f232"; +} +.fa-server:before { + content: "\f233"; +} +.fa-user-plus:before { + content: "\f234"; +} +.fa-user-times:before { + content: "\f235"; +} +.fa-hotel:before, +.fa-bed:before { + content: "\f236"; +} +.fa-viacoin:before { + content: "\f237"; +} +.fa-train:before { + content: "\f238"; +} +.fa-subway:before { + content: "\f239"; +} +.fa-medium:before { + content: "\f23a"; +} +.fa-yc:before, +.fa-y-combinator:before { + content: "\f23b"; +} +.fa-optin-monster:before { + content: "\f23c"; +} +.fa-opencart:before { + content: "\f23d"; +} +.fa-expeditedssl:before { + content: "\f23e"; +} +.fa-battery-4:before, +.fa-battery:before, +.fa-battery-full:before { + content: "\f240"; +} +.fa-battery-3:before, +.fa-battery-three-quarters:before { + content: "\f241"; +} +.fa-battery-2:before, +.fa-battery-half:before { + content: "\f242"; +} +.fa-battery-1:before, +.fa-battery-quarter:before { + content: "\f243"; +} +.fa-battery-0:before, +.fa-battery-empty:before { + content: "\f244"; +} +.fa-mouse-pointer:before { + content: "\f245"; +} +.fa-i-cursor:before { + content: "\f246"; +} +.fa-object-group:before { + content: "\f247"; +} +.fa-object-ungroup:before { + content: "\f248"; +} +.fa-sticky-note:before { + content: "\f249"; +} +.fa-sticky-note-o:before { + content: "\f24a"; +} +.fa-cc-jcb:before { + content: "\f24b"; +} +.fa-cc-diners-club:before { + content: "\f24c"; +} +.fa-clone:before { + content: "\f24d"; +} +.fa-balance-scale:before { + content: "\f24e"; +} +.fa-hourglass-o:before { + content: "\f250"; +} +.fa-hourglass-1:before, +.fa-hourglass-start:before { + content: "\f251"; +} +.fa-hourglass-2:before, +.fa-hourglass-half:before { + content: "\f252"; +} +.fa-hourglass-3:before, +.fa-hourglass-end:before { + content: "\f253"; +} +.fa-hourglass:before { + content: "\f254"; +} +.fa-hand-grab-o:before, +.fa-hand-rock-o:before { + content: "\f255"; +} +.fa-hand-stop-o:before, +.fa-hand-paper-o:before { + content: "\f256"; +} +.fa-hand-scissors-o:before { + content: "\f257"; +} +.fa-hand-lizard-o:before { + content: "\f258"; +} +.fa-hand-spock-o:before { + content: "\f259"; +} +.fa-hand-pointer-o:before { + content: "\f25a"; +} +.fa-hand-peace-o:before { + content: "\f25b"; +} +.fa-trademark:before { + content: "\f25c"; +} +.fa-registered:before { + content: "\f25d"; +} +.fa-creative-commons:before { + content: "\f25e"; +} +.fa-gg:before { + content: "\f260"; +} +.fa-gg-circle:before { + content: "\f261"; +} +.fa-tripadvisor:before { + content: "\f262"; +} +.fa-odnoklassniki:before { + content: "\f263"; +} +.fa-odnoklassniki-square:before { + content: "\f264"; +} +.fa-get-pocket:before { + content: "\f265"; +} +.fa-wikipedia-w:before { + content: "\f266"; +} +.fa-safari:before { + content: "\f267"; +} +.fa-chrome:before { + content: "\f268"; +} +.fa-firefox:before { + content: "\f269"; +} +.fa-opera:before { + content: "\f26a"; +} +.fa-internet-explorer:before { + content: "\f26b"; +} +.fa-tv:before, +.fa-television:before { + content: "\f26c"; +} +.fa-contao:before { + content: "\f26d"; +} +.fa-500px:before { + content: "\f26e"; +} +.fa-amazon:before { + content: "\f270"; +} +.fa-calendar-plus-o:before { + content: "\f271"; +} +.fa-calendar-minus-o:before { + content: "\f272"; +} +.fa-calendar-times-o:before { + content: "\f273"; +} +.fa-calendar-check-o:before { + content: "\f274"; +} +.fa-industry:before { + content: "\f275"; +} +.fa-map-pin:before { + content: "\f276"; +} +.fa-map-signs:before { + content: "\f277"; +} +.fa-map-o:before { + content: "\f278"; +} +.fa-map:before { + content: "\f279"; +} +.fa-commenting:before { + content: "\f27a"; +} +.fa-commenting-o:before { + content: "\f27b"; +} +.fa-houzz:before { + content: "\f27c"; +} +.fa-vimeo:before { + content: "\f27d"; +} +.fa-black-tie:before { + content: "\f27e"; +} +.fa-fonticons:before { + content: "\f280"; +} +.fa-reddit-alien:before { + content: "\f281"; +} +.fa-edge:before { + content: "\f282"; +} +.fa-credit-card-alt:before { + content: "\f283"; +} +.fa-codiepie:before { + content: "\f284"; +} +.fa-modx:before { + content: "\f285"; +} +.fa-fort-awesome:before { + content: "\f286"; +} +.fa-usb:before { + content: "\f287"; +} +.fa-product-hunt:before { + content: "\f288"; +} +.fa-mixcloud:before { + content: "\f289"; +} +.fa-scribd:before { + content: "\f28a"; +} +.fa-pause-circle:before { + content: "\f28b"; +} +.fa-pause-circle-o:before { + content: "\f28c"; +} +.fa-stop-circle:before { + content: "\f28d"; +} +.fa-stop-circle-o:before { + content: "\f28e"; +} +.fa-shopping-bag:before { + content: "\f290"; +} +.fa-shopping-basket:before { + content: "\f291"; +} +.fa-hashtag:before { + content: "\f292"; +} +.fa-bluetooth:before { + content: "\f293"; +} +.fa-bluetooth-b:before { + content: "\f294"; +} +.fa-percent:before { + content: "\f295"; +} +.fa-gitlab:before { + content: "\f296"; +} +.fa-wpbeginner:before { + content: "\f297"; +} +.fa-wpforms:before { + content: "\f298"; +} +.fa-envira:before { + content: "\f299"; +} +.fa-universal-access:before { + content: "\f29a"; +} +.fa-wheelchair-alt:before { + content: "\f29b"; +} +.fa-question-circle-o:before { + content: "\f29c"; +} +.fa-blind:before { + content: "\f29d"; +} +.fa-audio-description:before { + content: "\f29e"; +} +.fa-volume-control-phone:before { + content: "\f2a0"; +} +.fa-braille:before { + content: "\f2a1"; +} +.fa-assistive-listening-systems:before { + content: "\f2a2"; +} +.fa-asl-interpreting:before, +.fa-american-sign-language-interpreting:before { + content: "\f2a3"; +} +.fa-deafness:before, +.fa-hard-of-hearing:before, +.fa-deaf:before { + content: "\f2a4"; +} +.fa-glide:before { + content: "\f2a5"; +} +.fa-glide-g:before { + content: "\f2a6"; +} +.fa-signing:before, +.fa-sign-language:before { + content: "\f2a7"; +} +.fa-low-vision:before { + content: "\f2a8"; +} +.fa-viadeo:before { + content: "\f2a9"; +} +.fa-viadeo-square:before { + content: "\f2aa"; +} +.fa-snapchat:before { + content: "\f2ab"; +} +.fa-snapchat-ghost:before { + content: "\f2ac"; +} +.fa-snapchat-square:before { + content: "\f2ad"; +} +.fa-pied-piper:before { + content: "\f2ae"; +} +.fa-first-order:before { + content: "\f2b0"; +} +.fa-yoast:before { + content: "\f2b1"; +} +.fa-themeisle:before { + content: "\f2b2"; +} +.fa-google-plus-circle:before, +.fa-google-plus-official:before { + content: "\f2b3"; +} +.fa-fa:before, +.fa-font-awesome:before { + content: "\f2b4"; +} +.fa-handshake-o:before { + content: "\f2b5"; +} +.fa-envelope-open:before { + content: "\f2b6"; +} +.fa-envelope-open-o:before { + content: "\f2b7"; +} +.fa-linode:before { + content: "\f2b8"; +} +.fa-address-book:before { + content: "\f2b9"; +} +.fa-address-book-o:before { + content: "\f2ba"; +} +.fa-vcard:before, +.fa-address-card:before { + content: "\f2bb"; +} +.fa-vcard-o:before, +.fa-address-card-o:before { + content: "\f2bc"; +} +.fa-user-circle:before { + content: "\f2bd"; +} +.fa-user-circle-o:before { + content: "\f2be"; +} +.fa-user-o:before { + content: "\f2c0"; +} +.fa-id-badge:before { + content: "\f2c1"; +} +.fa-drivers-license:before, +.fa-id-card:before { + content: "\f2c2"; +} +.fa-drivers-license-o:before, +.fa-id-card-o:before { + content: "\f2c3"; +} +.fa-quora:before { + content: "\f2c4"; +} +.fa-free-code-camp:before { + content: "\f2c5"; +} +.fa-telegram:before { + content: "\f2c6"; +} +.fa-thermometer-4:before, +.fa-thermometer:before, +.fa-thermometer-full:before { + content: "\f2c7"; +} +.fa-thermometer-3:before, +.fa-thermometer-three-quarters:before { + content: "\f2c8"; +} +.fa-thermometer-2:before, +.fa-thermometer-half:before { + content: "\f2c9"; +} +.fa-thermometer-1:before, +.fa-thermometer-quarter:before { + content: "\f2ca"; +} +.fa-thermometer-0:before, +.fa-thermometer-empty:before { + content: "\f2cb"; +} +.fa-shower:before { + content: "\f2cc"; +} +.fa-bathtub:before, +.fa-s15:before, +.fa-bath:before { + content: "\f2cd"; +} +.fa-podcast:before { + content: "\f2ce"; +} +.fa-window-maximize:before { + content: "\f2d0"; +} +.fa-window-minimize:before { + content: "\f2d1"; +} +.fa-window-restore:before { + content: "\f2d2"; +} +.fa-times-rectangle:before, +.fa-window-close:before { + content: "\f2d3"; +} +.fa-times-rectangle-o:before, +.fa-window-close-o:before { + content: "\f2d4"; +} +.fa-bandcamp:before { + content: "\f2d5"; +} +.fa-grav:before { + content: "\f2d6"; +} +.fa-etsy:before { + content: "\f2d7"; +} +.fa-imdb:before { + content: "\f2d8"; +} +.fa-ravelry:before { + content: "\f2d9"; +} +.fa-eercast:before { + content: "\f2da"; +} +.fa-microchip:before { + content: "\f2db"; +} +.fa-snowflake-o:before { + content: "\f2dc"; +} +.fa-superpowers:before { + content: "\f2dd"; +} +.fa-wpexplorer:before { + content: "\f2de"; +} +.fa-meetup:before { + content: "\f2e0"; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} diff --git a/opentcs-documentation/src/docs/_assets/css/font-awesome.css.license b/opentcs-documentation/src/docs/_assets/css/font-awesome.css.license new file mode 100644 index 0000000..31a59a3 --- /dev/null +++ b/opentcs-documentation/src/docs/_assets/css/font-awesome.css.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: @davegandy - http://fontawesome.io - @fontawesome +SPDX-License-Identifier: MIT diff --git a/opentcs-documentation/src/docs/_assets/css/style.css b/opentcs-documentation/src/docs/_assets/css/style.css new file mode 100644 index 0000000..912e33c --- /dev/null +++ b/opentcs-documentation/src/docs/_assets/css/style.css @@ -0,0 +1,102 @@ +/* +SPDX-FileCopyrightText: The openTCS Authors +SPDX-License-Identifier: MIT +*/ +@font-face { + font-family: 'Droid Sans Mono'; + font-style: normal; + font-weight: 400; + src: local(''), + url('../fonts/droid-sans-mono/DroidSansMono.ttf') format('truetype'); +} +@font-face { + font-family: 'Droid Sans Mono'; + font-style: normal; + font-weight: 700; + src: local(''), + url('../fonts/droid-sans-mono/DroidSansMono.ttf') format('truetype'); +} + +/* The following was generated using: https://google-webfonts-helper.herokuapp.com/fonts/noto-serif?subsets=cyrillic,cyrillic-ext,greek,greek-ext,latin,latin-ext,vietnamese */ +/* noto-serif-regular - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Noto Serif'; + font-style: normal; + font-weight: 400; + src: local(''), + url('../fonts/noto-serif/noto-serif-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2') format('woff2'); +} +/* noto-serif-italic - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Noto Serif'; + font-style: italic; + font-weight: 400; + src: local(''), + url('../fonts/noto-serif/noto-serif-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff2') format('woff2'); +} +/* noto-serif-700 - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Noto Serif'; + font-style: normal; + font-weight: 700; + src: local(''), + url('../fonts/noto-serif/noto-serif-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700.woff2') format('woff2'); +} +/* noto-serif-700italic - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Noto Serif'; + font-style: italic; + font-weight: 700; + src: local(''), + url('../fonts/noto-serif/noto-serif-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700italic.woff2') format('woff2'); +} + +/* The following was generated using: https://google-webfonts-helper.herokuapp.com/fonts/open-sans?subsets=cyrillic,cyrillic-ext,greek,greek-ext,hebrew,latin,latin-ext,vietnamese */ +/* open-sans-300 - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + src: local(''), + url('../fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-300.woff2') format('woff2'); +} +/* open-sans-regular - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + src: local(''), + url('../fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2') format('woff2'); +} +/* open-sans-600 - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + src: local(''), + url('../fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.woff2') format('woff2'); +} +/* open-sans-300italic - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 300; + src: local(''), + url('../fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-300italic.woff2') format('woff2'); +} +/* open-sans-italic - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + src: local(''), + url('../fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff2') format('woff2'); +} +/* open-sans-600italic - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 600; + src: local(''), + url('../fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600italic.woff2') format('woff2'); +} diff --git a/opentcs-documentation/src/docs/_assets/fonts/droid-sans-mono/DroidSansMono.ttf b/opentcs-documentation/src/docs/_assets/fonts/droid-sans-mono/DroidSansMono.ttf new file mode 100644 index 0000000..d604425 Binary files /dev/null and b/opentcs-documentation/src/docs/_assets/fonts/droid-sans-mono/DroidSansMono.ttf differ diff --git a/opentcs-documentation/src/docs/_assets/fonts/droid-sans-mono/DroidSansMono.ttf.license b/opentcs-documentation/src/docs/_assets/fonts/droid-sans-mono/DroidSansMono.ttf.license new file mode 100644 index 0000000..fc1de8d --- /dev/null +++ b/opentcs-documentation/src/docs/_assets/fonts/droid-sans-mono/DroidSansMono.ttf.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Adobe Fonts +SPDX-License-Identifier: Apache-2.0 diff --git a/opentcs-documentation/src/docs/_assets/fonts/fontawesome/fontawesome-webfont.woff2 b/opentcs-documentation/src/docs/_assets/fonts/fontawesome/fontawesome-webfont.woff2 new file mode 100644 index 0000000..4d13fc6 Binary files /dev/null and b/opentcs-documentation/src/docs/_assets/fonts/fontawesome/fontawesome-webfont.woff2 differ diff --git a/opentcs-documentation/src/docs/_assets/fonts/fontawesome/fontawesome-webfont.woff2.license b/opentcs-documentation/src/docs/_assets/fonts/fontawesome/fontawesome-webfont.woff2.license new file mode 100644 index 0000000..58ebee7 --- /dev/null +++ b/opentcs-documentation/src/docs/_assets/fonts/fontawesome/fontawesome-webfont.woff2.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Fonticons, Inc. (https://fontawesome.com) +SPDX-License-Identifier: OFL-1.1 diff --git a/opentcs-documentation/src/docs/_assets/fonts/noto-serif/REUSE.toml b/opentcs-documentation/src/docs/_assets/fonts/noto-serif/REUSE.toml new file mode 100644 index 0000000..1d759fc --- /dev/null +++ b/opentcs-documentation/src/docs/_assets/fonts/noto-serif/REUSE.toml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC0-1.0 + +version = 1 + +[[annotations]] +path = ["*.woff2"] +precedence = "closest" +SPDX-FileCopyrightText = "2018 The Noto Project Authors (github.com/googlei18n/noto-fonts)" +SPDX-License-Identifier = "OFL-1.1" diff --git a/opentcs-documentation/src/docs/_assets/fonts/noto-serif/noto-serif-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700.woff2 b/opentcs-documentation/src/docs/_assets/fonts/noto-serif/noto-serif-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700.woff2 new file mode 100644 index 0000000..b4f084c Binary files /dev/null and b/opentcs-documentation/src/docs/_assets/fonts/noto-serif/noto-serif-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700.woff2 differ diff --git a/opentcs-documentation/src/docs/_assets/fonts/noto-serif/noto-serif-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700italic.woff2 b/opentcs-documentation/src/docs/_assets/fonts/noto-serif/noto-serif-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700italic.woff2 new file mode 100644 index 0000000..d2cc6a4 Binary files /dev/null and b/opentcs-documentation/src/docs/_assets/fonts/noto-serif/noto-serif-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700italic.woff2 differ diff --git a/opentcs-documentation/src/docs/_assets/fonts/noto-serif/noto-serif-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff2 b/opentcs-documentation/src/docs/_assets/fonts/noto-serif/noto-serif-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff2 new file mode 100644 index 0000000..f848903 Binary files /dev/null and b/opentcs-documentation/src/docs/_assets/fonts/noto-serif/noto-serif-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff2 differ diff --git a/opentcs-documentation/src/docs/_assets/fonts/noto-serif/noto-serif-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2 b/opentcs-documentation/src/docs/_assets/fonts/noto-serif/noto-serif-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2 new file mode 100644 index 0000000..e0ac12f Binary files /dev/null and b/opentcs-documentation/src/docs/_assets/fonts/noto-serif/noto-serif-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2 differ diff --git a/opentcs-documentation/src/docs/_assets/fonts/open-sans/REUSE.toml b/opentcs-documentation/src/docs/_assets/fonts/open-sans/REUSE.toml new file mode 100644 index 0000000..4d472cd --- /dev/null +++ b/opentcs-documentation/src/docs/_assets/fonts/open-sans/REUSE.toml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC0-1.0 + +version = 1 + +[[annotations]] +path = ["*.woff2"] +precedence = "closest" +SPDX-FileCopyrightText = "2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans)" +SPDX-License-Identifier = "OFL-1.1" diff --git a/opentcs-documentation/src/docs/_assets/fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-300.woff2 b/opentcs-documentation/src/docs/_assets/fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-300.woff2 new file mode 100644 index 0000000..d5d1a2c Binary files /dev/null and b/opentcs-documentation/src/docs/_assets/fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-300.woff2 differ diff --git a/opentcs-documentation/src/docs/_assets/fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-300italic.woff2 b/opentcs-documentation/src/docs/_assets/fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-300italic.woff2 new file mode 100644 index 0000000..1a89b35 Binary files /dev/null and b/opentcs-documentation/src/docs/_assets/fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-300italic.woff2 differ diff --git a/opentcs-documentation/src/docs/_assets/fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.woff2 b/opentcs-documentation/src/docs/_assets/fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.woff2 new file mode 100644 index 0000000..2659995 Binary files /dev/null and b/opentcs-documentation/src/docs/_assets/fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.woff2 differ diff --git a/opentcs-documentation/src/docs/_assets/fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600italic.woff2 b/opentcs-documentation/src/docs/_assets/fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600italic.woff2 new file mode 100644 index 0000000..932bb4d Binary files /dev/null and b/opentcs-documentation/src/docs/_assets/fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600italic.woff2 differ diff --git a/opentcs-documentation/src/docs/_assets/fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff2 b/opentcs-documentation/src/docs/_assets/fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff2 new file mode 100644 index 0000000..f559fd4 Binary files /dev/null and b/opentcs-documentation/src/docs/_assets/fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff2 differ diff --git a/opentcs-documentation/src/docs/_assets/fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2 b/opentcs-documentation/src/docs/_assets/fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2 new file mode 100644 index 0000000..2aa7f33 Binary files /dev/null and b/opentcs-documentation/src/docs/_assets/fonts/open-sans/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2 differ diff --git a/opentcs-documentation/src/docs/developers-guide/01_general-development.adoc b/opentcs-documentation/src/docs/developers-guide/01_general-development.adoc new file mode 100644 index 0000000..d113d25 --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/01_general-development.adoc @@ -0,0 +1,198 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + +== Development with openTCS in general + +=== System requirements + +The openTCS source code is written in Java. +To compile it, you need a Java Development Kit (JDK) 21. +To run the resulting binaries, you need a Java Runtime Environment (JRE) 21. +All other required libraries are included in the openTCS distribution or will be downloaded automatically when building it from source code. + +=== Available artifacts and API compatibility + +The openTCS project publishes artifacts for releases via the link:https://repo1.maven.org/maven2/[Maven Central] artifact repository, so you can easily integrate them with build systems such as Gradle or Maven. +In Gradle build scripts, for example, use something like the following to integrate an openTCS library: + +[source, groovy, subs="attributes"] +---- +repositories { + mavenCentral() +} + +dependencies { + compile group: 'org.opentcs', name: '${ARTIFACT}', version: '{revnumber}' +} +---- + +Set the version number of the openTCS release you actually want to work with, and select the appropriate `${ARTIFACT}` name from the following table: + +[cols="1,1,2", options="header"] +.Artifacts published by the openTCS project +|=== + +|Artifact name |API compatibility between minor releases |Content + +|`opentcs-api-base` +|Yes +|The base API for clients and extensions. +This is what most developers probably want to use. + +|`opentcs-api-injection` +|Yes +|API interfaces and classes used for dependency injection within the kernel and client applications. +This is required in integration projects customizing these applications, e.g. adding components like vehicle driver implementations. + +|`opentcs-common` +|No +|A collection of utility classes used by openTCS components. + +|`opentcs-impl-configuration-gestalt` +|No +|An implementation of the base API's configuration interfaces based on gestalt. + +|`opentcs-kernel-extension-http-services` +|No +|A kernel extension providing the web API implementation. + +|`opentcs-kernel-extension-rmi-services` +|No +|A kernel extension providing the RMI interface implementation. + +|`opentcs-kernel-extension-statistics` +|No +|A kernel extension providing the statistics collection implementation. + +|`opentcs-plantoverview-base` +|No +|The base data structures and components used by the Model Editor and the Operations Desk that don't require third-party libraries. + +|`opentcs-plantoverview-common` +|No +|A collection of classes and components commonly used by the Model Editor and the Operations Desk. + +|`opentcs-plantoverview-panel-loadgenerator` +|No +|The load generator panel implementation for the Operations Desk. + +|`opentcs-plantoverview-panel-resourceallocation` +|No +|The resource allocation panel implemenation for the Operations Desk. + +|`opentcs-plantoverview-panel-statistics` +|No +|The statistics panel implementation for the Operations Desk. + +|`opentcs-plantoverview-themes-default` +|No +|The default themes implementation for the Operations Desk. + +|`opentcs-commadapter-loopback` +|No +|A very basic vehicle driver simulating a virtual vehicle. + +|`opentcs-strategies-default` +|No +|The default implementations of strategies that are used by the kernel application. + +|`opentcs-kernel` +|No +|The kernel application. + +|`opentcs-kernelcontrolcenter` +|No +|The Kernel Control Center application. + +|`opentcs-modeleditor` +|No +|The Model Editor application. + +|`opentcs-operationsdesk` +|No +|The Operations Desk application. + +|=== + +Note that only the basic API libraries provide a documented API that the openTCS developers try to keep compatible between minor releases. +(For these libraries, the rules of https://semver.org/[semantic versioning] are applied.) +All other artifacts' contents can and will change regardless of any compatibility concerns, so if you explicitly use these as dependencies and switch to a different version of openTCS, you may have to adjust and recompile your code. + +=== Third-party dependencies + +The kernel and the client applications depend on the following external frameworks and libraries: + +* SLF4J (https://www.slf4j.org/): + A simple logging facade to keep the actual logging implementation replaceable. +* Google Guice (https://github.com/google/guice): + A light-weight dependency injection framework. +* Gestalt (https://github.com/gestalt-config/gestalt): + A configuration library supporting binding interfaces. +* Google Guava (https://github.com/google/guava): + A collection of small helper classes and methods. + +The kernel application also depends on the following libraries: + +* JGraphT (https://jgrapht.org/): + A library for working with graphs and using algorithms on them. +* Spark (https://sparkjava.com/): + A framework for creating web applications. +* Jackson (https://github.com/FasterXML/jackson): + Provides JSON bindings for Java objects. + +The Model Editor and Operations Desk applications have the following additional dependencies: + +* JHotDraw (https://github.com/wrandelshofer/jhotdraw): + A framework for drawing graph structures (like driving course models). +* Docking Frames (https://www.docking-frames.org/): + A framework for docking and undocking of GUI panels + +For automatic tests, the following dependencies are used: + +* JUnit (https://junit.org/): + A simple unit-testing framework. +* Mockito (https://site.mockito.org/): + A framework for creating mock objects. +* Hamcrest (http://hamcrest.org/): + A framework for assertion matchers that can be used in tests. + +The artifacts for these dependencies are downloaded automatically when building the applications. + +=== Modularity and extensibility + +The openTCS project heavily relies on link:https://github.com/google/guice[Guice] for dependency injection and wiring of components as well as for providing plugin-like extension mechanisms. +In the injection API, relevant classes can be found in the package `org.opentcs.customizations`. +For examples, see <>, <> and <>. + +=== Logging + +The code in the official openTCS distribution uses https://www.slf4j.org/[SLF4J] for logging. +Thus, the actual logging implementation is easily interchangeable by replacing the SLF4J binding in the respective application's classpath. +The kernel and client applications come with SLF4J's bindings for `java.util.logging` by default. +For more information on how to change the actual logging implementation, e.g. to use log4j, please see the SLF4J documentation. + +=== Working with the openTCS source code + +The openTCS project itself uses link:https://gradle.org/[Gradle] as its build management tool. +To build openTCS from source code, just run `gradlew build` from the source distribution's main directory. +For details on how to work with Gradle, please see link:https://docs.gradle.org/[its documentation]. + +These are the main Gradle tasks of the root project you need to know to get started: + +* `build`: Compiles the source code of all subprojects. +* `release`: Builds and packages all system components to a distribution in `build/`. +* `clean`: Cleans up everything produced by the other tasks. + +To work with the source code in your IDE, see the IDE's documentation for Gradle integration. +There is no general recommendation for any specific IDE. +Note, however, that the openTCS source code contains GUI components that have been created with the NetBeans GUI builder. +If you want to edit these, you may want to use the NetBeans IDE. + +=== openTCS kernel APIs + +openTCS provides the following APIs to interact with the kernel: + +* The kernel's Java API for both extending the kernel application as well as interfacing with it via RMI. + See <> for details. +* A web API for interfacing with the kernel via HTTP calls. + See the separate interface documentation that is part of the openTCS distribution for details. diff --git a/opentcs-documentation/src/docs/developers-guide/02_kernel-api.adoc b/opentcs-documentation/src/docs/developers-guide/02_kernel-api.adoc new file mode 100644 index 0000000..b277367 --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/02_kernel-api.adoc @@ -0,0 +1,317 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + +[#section-kernel-api] +== The kernel's Java API + +The interfaces and classes required to use the kernel API are part of the `opentcs-api-base` JAR file, so you should add that to your classpath/declare a dependency on it. +(See <>.) +The basic data structures for plant model components and transport orders you will encounter often are the following: + +.Basic data structures +image::tcsobject_classes.png[] + +The service interfaces that are most often interacted with to fetch and manipulate such data structures are these: + +.TCSObject-related service interfaces +image::service_interfaces_tcsobjects.png[] + +A few more interfaces are available to interact with various parts of the kernel, as shown in the following diagram: + +.Additional service interfaces +image::service_interfaces_others.png[] + +NOTE: _Peripheral*_ are classes/interfaces related to experimental integration of peripheral devices. +These features are not documented in detail, yet, and developers using any of them are on their own, for now. + +=== Acquiring service objects + +To use the services in code running inside the kernel JVM, e.g. a vehicle driver, simply request an instance of e.g. `PlantModelService` to be provided via dependency injection. +You may also work with an instance of `InternalPlantModelService` here, which provides additional methods available only to kernel application components. + +To access the services from another JVM, e.g. in a client that is supposed to create transport orders or to receive status updates for transport orders or vehicles, you need to connect to them via Remote Method Invocation (RMI). +The easiest way to do this is by creating an instance of the `KernelServicePortalBuilder` class and letting it build a `KernelServicePortal` instance for you. +(For now, there isn't much support for user management, so it is recommended to ignore the methods that require user credentials.) +After creating the `KernelServicePortal` instance, you can use it to get service instances and fetch kernel events from it. +See also the class documentation for `KernelServicePortalBuilder` in the base API's JavaDoc documentation. + +// XXX Move this example into a test class. + +[source, java] +---- +KernelServicePortal servicePortal = new KernelServicePortalBuilder().build(); + +// Connect and log in with a kernel somewhere. +servicePortal.login("someHost", 1099); + +// Get a reference to the plant model service... +PlantModelService plantModelService = servicePortal.getPlantModelService(); +// ...and find out the name of the currently loaded model. +String modelName = plantModelService.getLoadedModelName(); + +// Poll events, waiting up to a second if none are currently there. +// This should be done periodically, and probably in a separate thread. +List events = servicePortal.fetchEvents(1000); +---- + +=== Working with transport orders + +A transport order, represented by an instance of the class `TransportOrder`, describes a process to be executed by a vehicle. +Usually, this process is an actual transport of goods from one location to another. +A `TransportOrder` may, however, also just describe a vehicle's movement to a destination position and an optional vehicle operation to be performed. + +All of the following are examples for "transport orders" in openTCS, even if nothing is actually being transported: + +* A classic order for transporting goods from somewhere to somewhere else: +.. Move to location "A" and perform operation "Load cargo" there. +.. Move to location "B" and perform operation "Unload cargo" there. +* Manipulation of transported or stationary goods: +.. Move to location "A" and perform operation "Drill" there. +.. Move to location "B" and perform operation "Hammer" there. +* An order to move the vehicle to a parking position: +.. Move to point "Park 01" (without performing any specific operation). +* An order to recharge the vehicle's battery: +.. Move to location "Recharge station" and perform operation "Charge battery" there. + +==== A transport order's life cycle + +. When a transport order is created, its initial state is `RAW`. +. A user/client sets parameters for the transport order that are supposed to influence the transport process. + These parameters may be e.g. the transport order's deadline, the vehicle that is supposed to process the transport order or a set of generic, usually project-specific properties. +. The transport order is activated, i.e. parameter setup is finished. + Its state is set to `ACTIVE`. +. The kernel's router checks whether routing between the transport order's destinations is possible at all. + If yes, its state is changed to `DISPATCHABLE`. + If routing is not possible, the transport order is marked as `UNROUTABLE` and not processed any further. +. The kernel's dispatcher checks whether all requirements for executing the transport order are fulfilled and a vehicle is available for processing it. + As long as there are any requirements not yet fulfilled or no vehicle can execute it, the transport order is left waiting. +. The kernel's dispatcher assigns the transport order to a vehicle for processing. + Its state is changed to `BEING_PROCESSED`. +** If a transport order that is being processed is withdrawn (by a client/user), its state first changes to `WITHDRAWN` while the vehicle executes any orders that had already been sent to it. + Then the transport order's state changes to `FAILED`. + It is not processed any further. +** If processing of the transport order fails for any reason, it is marked as `FAILED` and not processed any further. +** If the vehicle successfully processes the transport order as a whole, it is marked as `FINISHED`. +. Eventually -- after a longer while or when too many transport orders in a final state have accumulated in the kernel's order pool -- the kernel removes the transport order. + +The following state machine visualizes this life cycle: + +.Transport order states +image::transportorder_states.png[] + +==== Structure and processing of transport orders + +.Transport order classes +image::transportorder_classes.png[] + +A transport order is created by calling `TransportOrderService.createTransportOrder()`. +As its parameter, it expects an instance of `TransportOrderCreationTO` containing the sequence of destinations to visit and the operations a vehicle is supposed to perform there. +The kernel wraps each `Destination` in a newly-created `DriveOrder` instance. +These ``DriveOrder``s are themselves wrapped by the kernel in a single, newly-created `TransportOrder` instance in their given order. + +Once a `TransportOrder` is being assigned to a vehicle by the `Dispatcher`, a `Route` is computed for each of its ``DriveOrder``s. +These ``Route``s are then stored in the corresponding ``DriveOrder``s. + +image::transport_order_course.png[] + +As soon as a vehicle (driver) is able to process a `DriveOrder`, the single ``Step``s of its `Route` are mapped to ``MovementCommand``s. +These ``MovementCommand``s contain all information the vehicle driver needs to reach the final destination and to perform the desired operation there. + +.MovementCommand-related classes +image::movementcommand_classes.png[] + +The ``MovementCommand``s for the partial routes to be travelled are sent to the vehicle driver bit by bit. +The kernel only sends as many ``MovementCommands``s in advance as is required for the vehicle driver to function properly. +It does this to maintain fine-grained control over the paths/resources used by all vehicles. +A vehicle driver may set the maximum number of ``MovementCommand``s it gets in advance by adjusting its command queue capacity. + +As soon as a `DriveOrder` is finished, the `Route` of the next `DriveOrder` is mapped to ``MovementCommand``s. +Once the last `DriveOrder` of a `TransportOrder` is finished, the whole `TransportOrder` is finished, as well. + +==== How to create a transport order + +Create a list of destinations the vehicle is supposed to travel to. +Every destination is described by the name of the destination location in the plant model and an operation the vehicle is supposed to perform there: + +[source, java] +---- +include::{documentation-testSrc}/org/opentcs/documentation/developers_guide/CreateTransportOrderTest.java[tags=createTransportOrder_createDestinations, indent=0] +---- + +Put as many destinations into the list as necessary. +Then create a transport order description with a name for the new transport order and the list of destinations. + +[source, java] +---- +include::{documentation-testSrc}/org/opentcs/documentation/developers_guide/CreateTransportOrderTest.java[tags=createTransportOrder_createTransportOrderCreationTO, indent=0] +---- + +Optionally, express that the full name of the order should be generated by the kernel. +(If you do not do this, you need to ensure that the name of the transport order given above is unique.) + +[source, java] +---- +include::{documentation-testSrc}/org/opentcs/documentation/developers_guide/CreateTransportOrderTest.java[tags=createTransportOrder_setIncompleteName, indent=0] +---- + +Optionally, set more parameters for the transport order, e.g. set a deadline for the order or assign a specific vehicle to it: + +[source, java] +---- +include::{documentation-testSrc}/org/opentcs/documentation/developers_guide/CreateTransportOrderTest.java[tags=createTransportOrder_setMoreOptionalParameters, indent=0] +---- + +Get a `TransportOrderService` (see <>) and ask it to create a transport order using the given description: + +[source, java] +---- +include::{documentation-testSrc}/org/opentcs/documentation/developers_guide/CreateTransportOrderTest.java[tags=createTransportOrder_useServiceToCreateOrder, indent=0] +---- + +Optionally, get a `DispatcherService` and trigger the kernel's dispatcher explicitly to have it check for a vehicle that can process the transport order. +(You only need to do this if you need the dispatcher to be triggered immediately after creating the transport order. +If you do not do this, the dispatcher will still be triggered periodically.) + +[source, java] +---- +include::{documentation-testSrc}/org/opentcs/documentation/developers_guide/CreateTransportOrderTest.java[tags=createTransportOrder_triggerDispatcher, indent=0] +---- + +==== How to create a transport order that sends a vehicle to a point instead of a location + +Create a list containing a single destination to a point, using `Destination.OP_MOVE` as the operation to be executed: + +[source, java] +---- +include::{documentation-testSrc}/org/opentcs/documentation/developers_guide/CreateTransportOrderTest.java[tags=createTransportOrderToPoint_createDestinations, indent=0] +---- + +Create a transport order description with a name for the new transport order and the (single-element) list of destinations: + +[source, java] +---- +include::{documentation-testSrc}/org/opentcs/documentation/developers_guide/CreateTransportOrderTest.java[tags=createTransportOrderToPoint_createTransportOrderCreationTO, indent=0] +---- + +Get a `TransportOrderService` (see <>) and ask it to create a transport order using the given description: + +[source, java] +---- +include::{documentation-testSrc}/org/opentcs/documentation/developers_guide/CreateTransportOrderTest.java[tags=createTransportOrderToPoint_useServiceToCreateOrder, indent=0] +---- + +Optionally, get a `DispatcherService` and trigger the kernel's dispatcher explicitly to have it check for a vehicle that can process the transport order. +(You only need to do this if you need the dispatcher to be triggered immediately after creating the transport order. +If you do not do this, the dispatcher will still be triggered periodically.) + +[source, java] +---- +include::{documentation-testSrc}/org/opentcs/documentation/developers_guide/CreateTransportOrderTest.java[tags=createTransportOrderToPoint_triggerDispatcher, indent=0] +---- + +==== How to work with order sequences + +An order sequence can be used to force a single vehicle to process multiple transport orders in a given order. +Some rules for using order sequences are described in the API documentation for `OrderSequence`, but here is what you would do in general. +First, create an order sequence description, providing a name: + +[source, java] +---- +include::{documentation-testSrc}/org/opentcs/documentation/developers_guide/CreateTransportOrderSequenceTest.java[tags=createOrderSequence_createOrderSequenceCreationTO, indent=0] +---- + +Optionally, express that the full name of the sequence should be generated by the kernel. +(If you do not do this, you need to ensure that the name of the order sequence given above is unique.) + +[source, java] +---- +include::{documentation-testSrc}/org/opentcs/documentation/developers_guide/CreateTransportOrderSequenceTest.java[tags=createOrderSequence_setIncompleteName, indent=0] +---- + +Optionally, set the sequence's failure-fatal flag: + +[source, java] +---- +include::{documentation-testSrc}/org/opentcs/documentation/developers_guide/CreateTransportOrderSequenceTest.java[tags=createOrderSequence_setFailureFatal, indent=0] +---- + +Get a `TransportOrderService` (see <>) and ask it to create an order sequence using the given description: + +[source, java] +---- +include::{documentation-testSrc}/org/opentcs/documentation/developers_guide/CreateTransportOrderSequenceTest.java[tags=createOrderSequence_useServiceToCreateSequence, indent=0] +---- + +Create a description for the transport order as usual, but set the wrapping sequence's name via `withWrappingSequence()` to associate the transport order with the order sequence. +Then, create the transport order using the `TransportOrderService`. + +[source, java] +---- +include::{documentation-testSrc}/org/opentcs/documentation/developers_guide/CreateTransportOrderSequenceTest.java[tags=createOrderSequence_createTransportOrder, indent=0] +---- + +Create and add more orders to the order sequence as necessary. +Eventually, set the order sequence's _complete_ flag to indicate that no further transport orders will be added to it: + +[source, java] +---- +include::{documentation-testSrc}/org/opentcs/documentation/developers_guide/CreateTransportOrderSequenceTest.java[tags=createOrderSequence_markSequenceComplete, indent=0] +---- + +As long as the sequence has not been marked as complete and finished completely, the vehicle selected for its first order will be tied to this sequence. +It will not process any orders not belonging to the same sequence until the whole sequence has been finished. + +Once the _complete_ flag of the sequence has been set and all transport orders belonging to it have been processed, its _finished_ flag will be set by the kernel. + +==== How to withdraw a transport order + +To withdraw a transport order, get a `DispatcherService` (see <>) and ask it to withdraw the order, providing a reference to it: + +[source, java] +---- +include::{documentation-testSrc}/org/opentcs/documentation/developers_guide/WithdrawTransportOrderTest.java[tags=documentation_withdrawTransportOrder, indent=0] +---- + +The second argument indicates whether the vehicle should finish the movements it is already assigned to (`false`) or abort immediately (`true`). + +==== How to withdraw a transport order via a vehicle reference + +To withdraw the transport order that a specific vehicle is currently processing, get a `DispatcherService` (see <>) and ask it to withdraw the order, providing a reference to the vehicle: + +[source, java] +---- +include::{documentation-testSrc}/org/opentcs/documentation/developers_guide/WithdrawTransportOrderTest.java[tags=documentation_withdrawTransportOrderByVehicle, indent=0] +---- + +The second argument indicates whether the vehicle should finish the movements it is already assigned to (`false`) or abort immediately (`true`). + +=== Using the event bus + +Each of the main openTCS applications -- Kernel, Kernel Control Center, Model Editor and Operations Desk -- provides an event bus that can be used to receive or emit event objects application-wide. +To acquire the respective application's event bus instance, request it to be provided via dependency injection. +Any of the following three variants of constructor parameters are equivalent: + +[source, java] +---- +public MyClass(@ApplicationEventBus EventHandler eventHandler) { + ... +} +---- + +[source, java] +---- +public MyClass(@ApplicationEventBus EventSource eventSource) { + ... +} +---- + +[source, java] +---- +public MyClass(@ApplicationEventBus EventBus eventBus) { + ... +} +---- + +Having acquired the `EventHandler`, `EventSource` or `EventBus` instance that way, you can use it to emit event objects to it and/or subscribe to receive event objects. + +Note that, within the Kernel application, event objects should be emitted via the kernel executor to avoid concurrency issues -- see <>. diff --git a/opentcs-documentation/src/docs/developers-guide/03_generate-integration-project.adoc b/opentcs-documentation/src/docs/developers-guide/03_generate-integration-project.adoc new file mode 100644 index 0000000..4a9052a --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/03_generate-integration-project.adoc @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + +== Generating an integration project + +openTCS integration projects for customer- or plant-specific distributions often have a very similar structure. +The openTCS distribution provides a way to easily generate such integration projects. +This way, a developer can get started with customizing and extending openTCS components quickly. + +To generate a template/skeleton for a new integration project, do the following: + +1. Download and unzip the integration project example from the openTCS homepage. +2. Execute the following command from the example project's root directory: + `gradlew cloneProject` + +The integration project will be generated in the `build/` directory. +(Make sure you copy it somewhere else before running the example project's `clean` task the next time.) + +The project and the included classes will have generic names. +You can adjust their names by setting a couple of properties when running the above command. +The following properties are looked at: + +* _integrationName_: + Used for the names of the project itself and the subprojects within it. +* _classPrefix_: + Used for some classes within the subprojects. + +For instance, your command line could look like this: + +[source, shell] +---- +gradlew -PintegrationName=MyGreatProject -PclassPrefix=Great cloneProject +---- + +This would include _MyGreatProject_ in the integration project name, and _Great_ in some class names. + +IMPORTANT: Inserting your own source code into a copy of the baseline openTCS project instead of creating a proper integration project as described above is not recommended. +This is because, when integrating openTCS by copying its source code, you lose the ability to easily upgrade your code to more recent openTCS versions (for bugfixes or new features). diff --git a/opentcs-documentation/src/docs/developers-guide/04_extending-kernel.adoc b/opentcs-documentation/src/docs/developers-guide/04_extending-kernel.adoc new file mode 100644 index 0000000..886e8fe --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/04_extending-kernel.adoc @@ -0,0 +1,334 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + +== Customizing and extending the kernel application + +=== Guice modules + +The openTCS kernel application uses Guice to configure its components. +To modify the wiring of components within the application and to add your own components, you can register custom Guice modules. +Modules are found and registered automatically via `java.util.ServiceLoader`. + +Basically, the following steps are required for customizing the application: + +. Build a JAR file for your custom injection module with the following content: +.. A subclass of `org.opentcs.customizations.kernel.KernelInjectionModule`, which can be found in the base library, must be contained. +Configure your custom components or adjust the application's default wiring in this module. +`KernelInjectionModule` provides a few supporting methods you can use. +.. A plain text file named `META-INF/services/org.opentcs.customizations.kernel.KernelInjectionModule` must also be contained. +This file should contain a single line of text with the fully qualified class name of your module. +. Ensure that the JAR file(s) containing your Guice modules and the implementation of your custom component(s) are part of the class path when you start the kernel application. + +For more information on how the automatic registration works, see the documentation of `java.util.ServiceLoader` in the Java class library. +For more information on how Guice works, see the Guice documentation. + +=== Replacing default kernel components + +The kernel application comes with default implementations for the dispatching, routing and scheduling components. +These default implementations allow the kernel to fulfil all of its responsibilities, but specific use cases might make it necessary to replace them with custom ones. +In such cases, they can be replaced with a custom Guice configuration. + +For each of these components, `KernelInjectionModule` provides a convenience method for (re)binding the implementation. +To replace e.g. the default `Dispatcher` implementation, simply register a Guice module in which you call `bindDispatcher()`. +The module's implementation could look like this: + +[source, java] +---- +@Override +protected void configure() { + configureSomeDispatcherDependencies(); + bindDispatcher(CustomDispatcher.class); +} +---- + +IMPORTANT: Note that all component implementations are bound as singletons. +This is important for the following reason: +Components may be injected and used at multiple places within the kernel application; at the same time, every component may also have to keep an internal state to do its work. +If they were not bound as singletons, a new instance would be created for every injection, each of them with their own, separate internal state. +Build custom components with that in mind, and implement their `initialize()` and `terminate()` methods appropriately! + +=== Customizing transformation of data sent to / received from vehicles + +For transforming data sent to / received from vehicles, the following interfaces in the openTCS API are used: + +* A `MovementCommandTransformer` transforms ``MovementCommand``s before they are sent to the vehicle, e.g. to adjust coordinates in the ``Point``s it contains for the vehicle's coordinate system. +* An `IncomingPoseTransformer` transforms ``Pose``s received by the vehicle, e.g. to adjust coordinates for the plant model coordinate system. +* A `VehicleDataTransformerFactory` provides matching instances of `MovementCommandTransformer` and `IncomingPoseTransformer`. + +To integrate your own transformation implementations, do the following: + +. Create custom implementations of `MovementCommandTransformer` and `IncomingPoseTransformer` that perform the actual custom transformations. +. Create an implementation of `VehicleDataTransformerFactory` that provides instances of your transformer implementations. + +image::transformer_classes.png[] + +To make your factory implementation usable in the kernel, register it as a binding for `VehicleDataTransformerFactory` in your `KernelInjectionModule` implementation. +For example, the transformer factory the kernel already comes with is registered with the following line: + +[source, java] +---- +include::{kernel-guiceSrc}/org/opentcs/kernel/DefaultKernelInjectionModule.java[tags=documentation_registerTransformerFactory] +---- + +Finally, to select your transformer (factory) to be used for a specific vehicle, set a property on the vehicle element in the plant model with key `tcs:vehicleDataTransformer` to the value that your factory's `getName()` method provides. + +=== Developing vehicle drivers + +openTCS supports integration of custom vehicle drivers that implement vehicle-specific communication protocols and thus mediate between the kernel and the vehicle. +Due to its function, a vehicle driver is also called a communication adapter. +The following sections describe which requirements must be met by a driver and which steps are necessary to create and use it. + +==== Classes and interfaces for the kernel + +.Classes of a comm adapter implementation (kernel side) +image::commadapter_classes_kernel.png[] + +When developing a vehicle driver, the most important classes and interfaces in the base library are the following: + +* `VehicleCommAdapter` declares methods that every comm adapter must implement. + These methods are called by components within the kernel, for instance to tell a vehicle that it is supposed to move to the next position in the driving course. + Classes implementing this interface are expected to perform the actual communication with a vehicle, e.g. via TCP, UDP or some field bus. +* `BasicVehicleCommAdapter` is the recommended base class for implementing a `VehicleCommAdapter`. + It primarily provides some basic command queueing. +* `VehicleCommAdapterFactory` describes a factory for `VehicleCommAdapter` instances. + The kernel instantiates and uses one such factory per vehicle driver to create instances of the respective `VehicleCommAdapter` implementation on demand. +* A single `VehicleProcessModel` instance should be provided by every `VehicleCommAdapter` instance in which it keeps the relevant state of both the vehicle and the comm adapter. + This model instance is supposed to be updated to notify the kernel about relevant changes. + The comm adapter implementation should e.g. update the vehicle's current position in the model when it receives that information to allow the kernel and GUI frontends to use it. + Likewise, other components may set values that influence the comm adapter's behaviour in the model, e.g. a time interval for periodic messages the comm adapter sends to the vehicle. + `VehicleProcessModel` may be used as it is, as it contains members for all the information the openTCS kernel itself needs. + However, developers may use driver-specific subclasses of `VehicleProcessModel` to have the comm adapter and other components exchange more than the default set of attributes. + +==== Classes and interfaces for the control center application + +For the kernel control center application, the following interfaces and classes are the most important ones: + +.Classes of a comm adapter implementation (kernel control center side) +image::commadapter_classes_kcc.png[] + +* `VehicleCommAdapterPanel` instances may be created by a `VehicleCommAdapterPanelFactory` e.g. to display information about the associated vehicle or send low-level messages to it. +* `VehicleProcessModelTO` instances should be provided by every `VehicleCommAdapter` instance according to the current state of its `VehicleProcessModel`. + Instances of this model are supposed to be used in a comm adapter's `VehicleCommAdapterPanel` instances for updating their contents only. + Note that `VehicleProcessModelTO` is basically a serializable representation of a comm adapter's `VehicleProcessModel`. + Developers should keep that in mind when creating driver-specific subclasses of `VehicleProcessModelTO`. +* Instances of `VehicleCommAdapterDescription` provide a string describing/identifying the comm adapter implementation. + This string is shown e.g. when the user may select one of a set of driver implementations and should thus be unique. + It is also used for attaching a comm adapter implementation via `VehicleService.attachCommAdapter()`. +* `AdapterCommand` instances can be sent from a panel to a `VehicleCommAdapter` instance via `VehicleService.sendCommAdapterCommand()`. + They are supposed to be executed by the comm adapter and can be used to execute arbitrary methods, e.g. methods of the `VehicleCommAdapter` itself, or update contents of the comm adapter's `VehicleProcessModel`. + Note that `AdapterCommand` instances can only be sent to and processed by the kernel application if they are serializable and present in the kernel application's classpath. + +==== Steps to create a new vehicle driver + +. Create an implementation of `VehicleCommAdapter`: +.. Subclass `BasicVehicleCommAdapter` unless you have a reason not to. + You don't have to, but if you don't, you also need to implement command queue management yourself. +.. Implement the abstract methods of `BasicVehicleCommAdapter` in the derived class to realize communication with the vehicle. +.. In situations in which the state of the vehicle changes in a way that is relevant for the kernel or the comm adapter's custom panels, the comm adapter should call the respective methods on the model. + Most importantly, call `setVehiclePosition()` and `commandExecuted()` on the comm adapter's model when the controlled vehicle's reported state indicates that it has moved to a different position or that it has finished an order. +. Create an implementation of `VehicleCommAdapterFactory` that provides instances of your `VehicleCommAdapter` for given `Vehicle` objects. +. Optional: Create any number of implementations of `VehicleCommAdapterPanel` that the kernel control center application should display for the comm adapter. + Create and return instances of these panels in the `getPanelsFor()` method of your ``VehicleCommAdapterPanelFactory``s implementation. + +See the API documentation for more details. +For an example, refer to the implementation of the loopback comm adapter for virtual vehicles in the openTCS source distribution. +(Note, however, that this implementation does not implement communication with any physical vehicle.) + +==== Registering a vehicle driver with the kernel + +. Create a Guice module for your vehicle driver by creating a subclass of `KernelInjectionModule`. +Implement the `configure()` method and register a binding to your `VehicleCommAdapterFactory`. +For example, the loopback driver that is part of the openTCS distribution registers its own factory class with the following line in its `configure()` method: ++ +[source, java] +---- +include::{loopback-guiceSrc}/org/opentcs/virtualvehicle/LoopbackCommAdapterModule.java[tags=documentation_createCommAdapterModule] +---- + +. In the JAR file containing your driver, ensure that there exists a folder named `META-INF/services/` with a file named `org.opentcs.customizations.kernel.KernelInjectionModule`. +This file should consist of a single line of text holding simply the name of the Guice module class, e.g.: ++ +[source] +---- +org.opentcs.virtualvehicle.LoopbackCommAdapterModule +---- +NOTE: Background: openTCS uses `java.util.ServiceLoader` to automatically find Guice modules on startup, which depends on this file (with this name) being present. +See the JDK's API documentation for more information about how this mechanism works. +. Place the JAR file of your driver including all neccessary resources in the subdirectory `lib/openTCS-extensions/` of the openTCS kernel application's installation directory _before_ the kernel is started. +(The openTCS start scripts include all JAR files in that directory in the application's classpath.) + +Drivers meeting these requirements are found automatically when you start the kernel. + +=== Sending messages to communication adapters + +Sometimes it is required to have some influence on the behaviour of a communication adapter (and thus the vehicle it is associated with) directly from a kernel client - to send a special telegram to the vehicle, for instance. +For these cases, `VehicleService.sendCommAdapterMessage(TCSObjectReference, Object)` provides a one-way communication channel for a client to send a message object to a communication adapter currently associated with a vehicle. +A comm adapter implementing `processMessage()` may interpret message objects sent to it and react in an appropriate way. +Note that the client sending the message may not know which communication adapter implementation is currently associated with the vehicle, so the adapter may or may not be able to understand the message. + +=== Acquiring data from communication adapters + +For getting information from a communication adapter to a kernel client, there are the following ways: + +Communication adapters may publish events via their `VehicleProcessModel` instance to emit information encapsulated in an event for any listeners registered with the kernel. +Apparently, listeners must already be registered before such an event is emitted by the communication adapter, or they will miss it. +To register a client as a listener, use `EventSource.subscribe()`. +You can get the `EventSource` instance used by the kernel through dependency injection by using the qualifier annotation `ApplicationEventBus`. + +Alternatively, communication adapters may use their `VehicleProcessModel` to set properties in the corresponding `Vehicle` object. +Kernel clients may then retrieve the information from it: + +[source, java] +---- +include::{documentation-testSrc}/org/opentcs/documentation/developers_guide/ReceiveCommAdapterMessageTest.java[tags=documentation_receiveMessageFromVehicle, indent=0] +---- + +Communication adapters may also use their `VehicleProcessModel` to set properties in the corresponding `TransportOrder` object a vehicle is currently processing. +This basically works the same way as with the `Vehicle` object: + +[source, java] +---- +include::{documentation-testSrc}/org/opentcs/documentation/developers_guide/ReceiveCommAdapterMessageTest.java[tags=documentation_receiveMessageFromTransportOrder, indent=0] +---- + +Unlike information published via events, data stored as properties in `Vehicle` or `TransportOrder` objects can be retrieved at any time. + +=== Developing peripheral drivers + +In addition to vehicle drivers, openTCS supports the integration of custom peripheral drivers that implement peripheral-specific communication protocols and thus mediate between the kernel and the peripheral device. +In openTCS, a peripheral device is a device a vehicle may interact with along its route, e.g. an elevator or a fire door. +Due to its function, a peripheral driver is also called a peripheral communication adapter. +The following sections describe which requirements must be met by a driver and which steps are necessary to create and use it. + +==== Classes and interfaces for the kernel + +.Classes of a peripheral comm adapter implementation (kernel side) +image::peripheral_commadapter_classes_kernel.png[] + +When developing a peripheral driver, the most important classes and interfaces in the base library are the following: + +* `PeripheralCommAdapter` declares methods that every comm adapter must implement. + These methods are called by components within the kernel, for instance to tell a peripheral device that it is supposed to perform an operation, e.g. for an elevator to move to a specific floor. + Classes implementing this interface are expected to perform the actual communication with a peripheral device, e.g. via TCP, UDP or some field bus. +* `BasicPeripheralCommAdapter` is the recommended base class for implementing a `PeripheralCommAdapter`. + It primarily provides some basic event dispatching with regards to the `PeripheralProcessModel`. +* `PeripheralCommAdapterFactory` describes a factory for `PeripheralCommAdapter` instances. + The kernel instantiates and uses one such factory per peripheral driver to create instances of the respective `PeripheralCommAdapter` implementation on demand. +* A single `PeripheralProcessModel` instance should be provided by every `PeripheralCommAdapter` instance in which it keeps the relevant state of both the peripheral device and the comm adapter. + This model instance is supposed to be updated to notify the kernel about relevant changes. + The comm adapter implementation should e.g. update the peripheral device's current state in the model when it receives that information to allow the kernel and GUI frontends to use it. + `PeripheralProcessModel` may be used as it is, as it contains members for all the information the openTCS kernel itself needs. + However, developers may use driver-specific subclasses of `PeripheralProcessModel` to have the comm adapter and other components exchange more than the default set of attributes. + +==== Classes and interfaces for the control center application + +For the kernel control center application, the following classes and interfaces are the most important: + +.Classes of a peripheral comm adapter implementation (kernel control center side) +image::peripheral_commadapter_classes_kcc.png[] + +* `PeripheralCommAdapterPanel` instances may be created by a `PeripheralCommAdapterPanelFactory` e.g. to display information about the associated peripheral device or send low-level messages to it. +* `PeripheralProcessModel` instances are used in a comm adapter's `PeripheralCommAdapterPanel` instances for updating their contents. + In contrast to a `VehicleCommAdapter`, there is no class such as `PeripheralProcessModelTO`, since `PeripheralProcessModel` already implements the `Serializable` interface. + Developers should keep that in mind when creating driver-specific subclasses of `PeripheralProcessModel`. +* Instances of `PeripheralCommAdapterDescription` provide a string describing/identifying the comm adapter implementation. + This string is shown e.g. when the user may select one of a set of driver implementations and should thus be unique. + It is also used for attaching a comm adapter implementation via `PeripheralService.attachCommAdapter()`. +* `PeripheralAdapterCommand` instances can be sent from a panel to a `PeripheralCommAdapter` instance via `PeripheralService.sendCommAdapterCommand()`. + They are supposed to be executed by the comm adapter and can be used to execute arbitrary methods, e.g. methods of the `PeripheralCommAdapter` itself, or update contents of the comm adapter's `PeripheralProcessModel`. + Note that `PeripheralAdapterCommand` instances can only be sent to and processed by the kernel application if they are serializable and present in the kernel application's classpath. + +==== Steps to create a new peripheral driver + +. Create an implementation of `PeripheralCommAdapter`: +.. Subclass `BasicPeripheralCommAdapter` unless you have a reason not to. +.. Implement the abstract methods of `BasicPeripheralCommAdapter` in the derived class to realize communication with the peripheral device. +.. In situations in which the state of the peripheral device changes in a way that is relevant for the kernel or the comm adapter's custom panels, the comm adapter should call the respective methods on the model and publish a corresponding `PeripheralProcessModelEvent` via the kernel's `ApplicationEventBus`. + Most importantly, call `PeripheralJobCallback.peripheralJobFinished()` on the callback instance provided with `PeripheralCommAdapter.process()` when the controlled peripheral device's reported state indicates that it has finished a job. +. Create an implementation of `PeripheralCommAdapterFactory` that provides instances of your `PeripheralCommAdapter` for given `Location` objects. +. Optional: Create any number of implementations of `PeripheralCommAdapterPanel` that the kernel control center application should display for the comm adapter. + Create and return instances of these panels in the `getPanelsFor()` method of your ``PeripheralCommAdapterPanelFactory``s implementation. + +See the API documentation for more details. +For an example, refer to the implementation of the loopback peripheral comm adapter for virtual peripheral devices in the openTCS source distribution. +(Note, however, that this implementation does not implement communication with any physical peripheral device.) + +==== Registering a peripheral driver with the kernel + +. Create a Guice module for your peripheral driver by creating a subclass of `KernelInjectionModule`. +Implement the `configure()` method and register a binding to your `PeripheralCommAdapterFactory`. +For example, the peripheral loopback driver that is part of the openTCS distribution registers its own factory class with the following line in its `configure()` method: ++ +[source, java] +---- +include::{peripheral-loopback-guiceSrc}/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralKernelModule.java[tags=documentation_createCommAdapterModule] +---- + +. In the JAR file containing your driver, ensure that there exists a folder named `META-INF/services/` with a file named `org.opentcs.customizations.kernel.KernelInjectionModule`. +This file should consist of a single line of text holding simply the name of the Guice module class, e.g.: ++ +[source] +---- +org.opentcs.commadapter.peripheral.loopback.LoopbackPeripheralKernelModule +---- +NOTE: Background: openTCS uses `java.util.ServiceLoader` to automatically find Guice modules on startup, which depends on this file (with this name) being present. +See the JDK's API documentation for more information about how this mechanism works. +. Place the JAR file of your driver including all neccessary resources in the subdirectory `lib/openTCS-extensions/` of the openTCS kernel application's installation directory _before_ the kernel is started. +(The openTCS start scripts include all JAR files in that directory in the application's classpath.) + +Drivers meeting these requirements are found automatically when you start the kernel. + +[#section-kernel-executor] +=== Executing code in kernel context + +Within the kernel, concurrent modifications of the data model -- e.g. contents of the plant model or transport order properties -- need to be synchronized carefully. +Similar to e.g. the Swing framework's Event Dispatcher Thread, a single thread is used for executing one-shot or periodics tasks performing data modifications. +To help with this, an instance of `java.util.concurrent.ScheduledExecutorService` is provided. +Custom code running within the kernel application, including vehicle drivers and implementations of additional funcionality, should also perform changes of the data model via this executor only to avoid concurrency issues. + +To make use of the kernel's executor, use the `@KernelExecutor` qualifier annotation and inject a `ScheduledExecutorService`: + +[source, java] +---- +@Inject +public MyClass(@KernelExecutor ScheduledExecutorService kernelExecutor) { + ... +} +---- + +You can also inject it as a `java.util.concurrent.ExecutorService`: + +[source, java] +---- +@Inject +public MyClass(@KernelExecutor ExecutorService kernelExecutor) { + ... +} +---- + +Injecting a `java.util.concurrent.Executor` is also possible: + +[source, java] +---- +@Inject +public MyClass(@KernelExecutor Executor kernelExecutor) { + ... +} +---- + +Then, you can use it e.g. to lock a path in the plant model in kernel context: + +[source, java] +---- +kernelExecutor.submit(() -> routerService.updatePathLock(ref, true)); +---- + +Due to the single-threaded nature of the kernel executor, tasks submitted to it are executed sequentially, one after another. +This implies that submitting long-running tasks should be avoided, as they would block the execution of subsequent tasks. + +When event objects, e.g. instances of `TCSObjectEvent`, are distributed within the kernel, this always happens in kernel context, i.e. from a task that is run by the kernel executor. +Event handlers should behave accordingly and finish quickly/not block execution for too long. +If processing an event requires time-consuming actions to be taken, these should be executed on a different thread. + +NOTE: As its name indicates, the kernel executor is only available within the kernel application. +It is not available for code running in other applications like the Operations Desk, and it is not required there (for avoiding concurrency issues in the kernel). diff --git a/opentcs-documentation/src/docs/developers-guide/05_extending-control-center.adoc b/opentcs-documentation/src/docs/developers-guide/05_extending-control-center.adoc new file mode 100644 index 0000000..299220a --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/05_extending-control-center.adoc @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + +== Customizing and extending the control center application + +=== Guice modules + +The openTCS kernel control center application uses Guice to configure its components. +To modify the wiring of components within the application and to add your own components, you can register custom Guice modules. +Modules are found and registered automatically via `java.util.ServiceLoader`. + +Basically, the following steps are required for customizing the application: + +. Build a JAR file for your custom injection module with the following content: +.. A subclass of `org.opentcs.customizations.controlcenter.ControlCenterInjectionModule` must be contained. + Configure your custom components or adjust the application's default wiring in this module. + `ControlCenterInjectionModule` provides a few supporting methods you can use. +.. A plain text file named `META-INF/services/org.opentcs.customizations.controlcenter.ControlCenterInjectionModule` must also be contained. + This file should contain a single line of text with the fully qualified class name of your module. +. Ensure that the JAR file(s) containing your Guice modules and the implementation of your custom component(s) are part of the class path when you start the control center application. + +For more information on how the automatic registration works, see the documentation of `java.util.ServiceLoader` in the Java class library. +For more information on how Guice works, see the Guice documentation. + +=== Registering driver panels with the control center + +. Create a Guice module for your vehicle driver by creating a subclass of `ControlCenterInjectionModule`. +Implement the `configure()` method and register a binding to your `VehicleCommAdapterPanelFactory`. +The following example demonstrates how this module's `configure()` method looks like for the loopback driver that is part of the openTCS distribution: ++ +[source, java] +---- +include::{controlCenter-guiceSrc}/org/opentcs/kernelcontrolcenter/LoopbackCommAdapterPanelsModule.java[tags=documentation_createCommAdapterPanelsModule] +---- + +. In the JAR file containing your driver, ensure that there exists a folder named `META-INF/services/` with a file named `org.opentcs.customizations.controlcenter.ControlCenterInjectionModule`. +This file should consist of a single line of text holding simply the name of the Guice module class, e.g.: ++ +[source] +---- +org.opentcs.controlcenter.LoopbackCommAdapterPanelsModule +---- +NOTE: Background: openTCS uses `java.util.ServiceLoader` to automatically find Guice modules on startup, which depends on this file (with this name) being present. +See the JDK's API documentation for more information about how this mechanism works. +. Place the JAR file of your driver including all neccessary resources in the subdirectory `lib/openTCS-extensions/` of the control center application's installation directory _before_ the application is started. +(The openTCS start scripts include all JAR files in that directory in the application's classpath.) + +Panels meeting these requirements are found automatically when you start the kernel control center application. diff --git a/opentcs-documentation/src/docs/developers-guide/06_extending-model-editor-and-operations-desk.adoc b/opentcs-documentation/src/docs/developers-guide/06_extending-model-editor-and-operations-desk.adoc new file mode 100644 index 0000000..b3ac3f5 --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/06_extending-model-editor-and-operations-desk.adoc @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + +== Customizing and extending the Model Editor and the Operations Desk applications + +NOTE: The process of customizing and extending the Model Editor and the Operations Desk is basically identical for both applications. +For the sake of simplicity, this section describes the process using the Operations Desk application as an example. +Where necessary, differences between the two applications are explicitly mentioned. + +=== Guice modules + +Analogous to the kernel application, the Operations Desk application uses Guice to configure its components. +To modify the wiring of components within the application and to add your own components, you can register custom Guice modules. +Modules are found and registered automatically via `java.util.ServiceLoader`. + +Basically, the following steps are required for customizing the application: + +. Build a JAR file for your custom injection module with the following content: +.. A subclass of `PlantOverviewInjectionModule`, which can be found in the base library, must be contained. +Configure your custom components or adjust the application's default wiring in this module. +`PlantOverviewInjectionModule` provides a few supporting methods you can use. +.. A plain text file named `META-INF/services/org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule` must also be contained. +This file should contain a single line of text with the fully qualified class name of your module. +. Ensure that the JAR file(s) containing your Guice modules and the implementation of your custom component(s) are part of the class path when you start the Operations Desk application. + +For more information on how the automatic registration works, see the documentation of `java.util.ServiceLoader` in the Java class library. +For more information on how Guice works, see the Guice documentation. + +=== How to add import/export functionality for plant model data + +It is not uncommon for plant model data to be shared with other systems. +For instance, it is possible that the navigation computer of a vehicle already has detailed information about the plant environment such as important positions and connections between them. +Entering this data manually via the Model Editor application would be a tiresome and error-prone work. +To help relieve users from such work, the Model Editor supports integrating components for importing plant model data from external resources -- e.g. files in a foreign file format -- or exporting plant model data to such resources. + +To integrate a model import component, do the following: + +. Create a class that implements the interface `PlantModelImporter` and implement the methods declared by it: +.. In `importPlantModel()`, implement what is necessary to produce an instance of `PlantModelCreationTO` that describes the plant model. +The actual steps necessary here may vary depending on the source and the type of model data to be imported. +In most cases, however, the process will probably look like the following: +... Show a dialog for the user to select the file to be imported. +... Parse the content of the selected file according to its specific file format. +... Convert the parsed model data to instances of `PointCreationTO`, `PathCreationTO` etc., and fill a newly created `PlantModelCreationTO` instance with them. +... Return the `PlantModelCreationTO` instance. +.. `getDescription()` should return a short description for your importer, for instance "Import from XYZ course data file". +An entry with this text will be shown in the Model Editor's btn:[menu:File[Import plant model]] submenu, and clicking on this entry will result in your `importPlantModel()` implementation to be called. +. Create a Guice module for registering your `PlantModelImporter` with openTCS by subclassing `PlantOverviewInjectionModule`. +Implement the `configure()` method and add a binding to your `PlantModelImporter` using `plantModelImporterBinder()`. +. Build and package the `PlantModelImporter` and Guice module into a JAR file. +. In the JAR file, register the Guice module class as a service of type `PlantOverviewInjectionModule`. +To do that, ensure that the JAR file contains a folder named `META-INF/services/` with a file named `org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule`. +This file should consist of a single line of text holding simply the name of the Guice module class. +(See <> for an example.) +. Place the JAR file in the Model Editor application's class path (subdirectory `lib/openTCS-extensions/` of the application's installation directory) and start the application. + +To integrate a model _export_ component, you follow the same steps, but implement the interface `PlantModelExporter` instead of `PlantModelImporter`. + +=== How to create a plugin panel for the Operations Desk client + +The Operations Desk client offers to integrate custom panels providing project-specific functionality. + +. Implement a subclass of `PluggablePanel`. +. Implement a `PluggablePanelFactory` that produces instances of your `PluggablePanel`. ++ +NOTE: The `PluggablePanelFactory.providesPanel(state)` method is used to determine which `Kernel.State` a factory provides panels for. +For plugin panels that are intended to be used with the Model Editor application only, this method should return `true` for the kernel state `Kernel.State.MODELLING`. +For plugin panels that are intended to be used with the Operations Desk application only, this method should return `true` for the kernel state `Kernel.State.OPERATING`. +Otherwise the plugin panels won't be shown in the respective application. + +. Create a Guice module for your `PluggablePanelFactory` by subclassing `PlantOverviewInjectionModule`. +Implement the `configure()` method and add a binding to your `PluggablePanelFactory` using `pluggablePanelFactoryBinder()`. +For example, the load generator panel that is part of the openTCS distribution is registered with the following line in its module's `configure()` method: ++ +[source, java] +---- +include::{loadGeneratorPanel-guiceSrc}/org/opentcs/guing/plugins/panels/loadgenerator/LoadGeneratorPanelModule.java[tags=documentation_createPluginPanelModule] +---- + +. Build and package the `PluggablePanel`, `PluggablePanelFactory` and Guice module into a JAR file. +. In the JAR file, register the Guice module class as a service of type `PlantOverviewInjectionModule`. +To do that, ensure that the JAR file contains a folder named `META-INF/services/` with a file named `org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule`. +This file should consist of a single line of text holding simply the name of the Guice module class, e.g.: ++ +[source] +---- +org.opentcs.guing.plugins.panels.loadgenerator.LoadGeneratorPanelModule +---- + +. Place the JAR file in the Operations Desk application's class path (subdirectory `lib/openTCS-extensions/` of the application's installation directory) and start the application. + +=== How to create a location/vehicle theme for openTCS + +Locations and vehicles are visualized in the Operations Desk client using configurable themes. +To customize the appearance of locations and vehicles, new theme implementations can be created and integrated into the Operations Desk client. + +. Create a new class which implements `LocationTheme` or `VehicleTheme`. +. Place the JAR file of your theme, containing all required resources, in the subdirectory `lib/openTCS-extensions/` of the openTCS Operations Desk application's installation directory _before_ the application is started. +(The openTCS start scripts include all JAR files in that directory in the application's classpath.) +. Set the `locationThemeClass` or `vehicleThemeClass` in the Operations Desk application's configuration file. + +Vehicles or locations in plant models are then rendered using your custom theme. diff --git a/opentcs-documentation/src/docs/developers-guide/07_supplement-configuration.adoc b/opentcs-documentation/src/docs/developers-guide/07_supplement-configuration.adoc new file mode 100644 index 0000000..27ee09a --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/07_supplement-configuration.adoc @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + +== Application configuration + +As described in the openTCS User's Guide, the openTCS Kernel, Kernel Control Center, Model Editor and Operations Desk applications read their configurations from properties files. +This functionality is provided by the link:https://github.com/gestalt-config/gestalt[gestalt] library. + +=== Supplementing configuration sources using gestalt + +It is possible to register additional configuration sources, e.g. for reading configuration data from network resources or files in different formats. +The mechanism provided by `java.util.ServiceLoader` is used for this. +The following steps are required for registering a configuration source: + +. Build a JAR file with the following content: +.. An implementation of `org.opentcs.configuration.gestalt.SupplementaryConfigSource`. + This interface is part of the `opentcs-impl-configuration-gestalt` artifact, which must be on your project's classpath. +.. A plain text file named `META-INF/services/org.opentcs.configuration.gestalt.SupplementaryConfigSource`. + This file should contain a single line of text with the fully qualified class name of your implementation. +. Ensure that the JAR file is part of the classpath when you start the respective application. + +It is possible to register multiple supplementary configuration sources this way. + +The configuration entries provided by any registered supplementary configuration source may override configuration entries provided by the properties files that are read by default. +Note that the order in which these additional configuration sources are processed is unspecified. + +For more information on how the automatic registration works, see the documentation of `java.util.ServiceLoader` in the Java class library. diff --git a/opentcs-documentation/src/docs/developers-guide/08_translating.adoc b/opentcs-documentation/src/docs/developers-guide/08_translating.adoc new file mode 100644 index 0000000..0d1e0df --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/08_translating.adoc @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + +== Translating the user interfaces + +Each openTCS application with a user interface is prepared for internationalization based on Java's `ResourceBundle` mechanism. +As a result, the applications can be configured to display texts in different languages, provided there is a translation in the form of resource bundle property files. +(How this configuration works is described in the User's Guide.) +The openTCS distribution itself comes with language files for the default language (English) and German. +Additional translations can be integrated primarily by adding JAR files containing property files to the class path. + +The following sections explain how to create and integrate a new translation. + +NOTE: Parts of the texts in the distribution may change between openTCS releases. +While this might not happen often, it still means that, when you update to a new version of openTCS, you may want to check whether your translations are still correct. +If there were textual changes in the openTCS distribution, you may need to update your language files. + +=== Extracting default language files + +To create a new translation pack for an application, you first need to know what texts to translate. +The best way to do this is to look at the existing language files in the openTCS distribution. +These are contained in the applications' JAR files (`opentcs-*.jar`), and are by convention kept in a common directory `/i18n/org/opentcs` inside these JAR files. + +To start your translation work, extract all of the application's language files into a single directory first. +Since JAR files are really only ZIP files, this can be done using any ZIP utility you like. +As an example, to use `unzip` in a shell on a Linux system, issue the following command from the application's `lib/` directory: + +---- +unzip "opentcs-*.jar" "i18n/org/opentcs/*.properties" +---- + +Alternatively, to use link:https://7-zip.org/[7-Zip] in a shell on a Windows system, issue the following command from the application's `lib/` directory: + +---- +7z x -r "opentcs-*.jar" "i18n\org\opentcs\*.properties" +---- + +You will find the extracted language files in the `i18n/` directory, then. +For the Model Editor or Operations Desk application, an excerpt of that directory's contents would look similar to this: + +.... +i18n/ + org/ + opentcs/ + plantoverview/ + mainMenu.properties + mainMenu_de.properties + toolbar.properties + toolbar_de.properties + ... +.... + +Files whose names end with `_de.properties` are German translations. +You will not need these and can delete them. + +=== Creating a translation + +Copy the whole `i18n/` directory with the English language files to a new, separate directory, e.g. `translation/`. +Working with a copy ensures that you still have the English version at hand to look up the original texts when translating. + +Then rename all property files in the new directory so their names contain the appropriate language tag for your translation. +If you are e.g. translating to Norwegian, rename `mainMenu.properties` to `mainMenu_no.properties` and the other files accordingly. +It is important that the base name of the file remains the same and only the language tag is added to it. + +The next step is doing the actual translation work -- open each property file in a text editor and translate the properties' values in it. + +After translating all the files, create a JAR file containing the `i18n/` directory with your language files. +You can do this for instance by simply creating a ZIP file and changing its name to end with `.jar`. + +The result could be a file named e.g. `language-pack-norwegian.jar`, whose contents should look similar to this: + +.... +i18n/ + org/ + opentcs/ + plantoverview/ + mainMenu_no.properties + toolbar_no.properties + ... +.... + +=== Integrating a translation + +Finally, you merely need to add the JAR file you created to the translated application's class path. +After configuring the application to the respective language and restarting it, you should see your translations in the user interface. + +=== Updating a translation + +As development of openTCS proceeds, parts of the applications' language files may change. +This means that your translations may also need to be updated when you move from one version of openTCS to a more recent one. + +To find out what changes were made and may need to be applied to your translations, you could do the following: + +. Extract the language files for the old version of the application, e.g. into a directory `translation_old/`. +. Extract the language files for the new version of the application, e.g. into a directory `translation_new/`. +. Create a link:https://en.wikipedia.org/wiki/Diff[diff] between the two language file versions. + For example, on a Linux system you could run `diff -urN translation_old/ translation_new/ > language_changes.diff` to write a diff to the file `language_changes.diff`. +. Read the diff to see which new language files and/or entries were added, removed or changed. + +Based on the information from the diff, you can apply appropriate changes to your own language files. +Then you merely need to create new JAR files for your translations and add them to the applications' class paths. diff --git a/opentcs-documentation/src/docs/developers-guide/docinfo.html b/opentcs-documentation/src/docs/developers-guide/docinfo.html new file mode 100644 index 0000000..e60c2bb --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/docinfo.html @@ -0,0 +1,7 @@ + + + + diff --git a/opentcs-documentation/src/docs/developers-guide/images/REUSE.toml b/opentcs-documentation/src/docs/developers-guide/images/REUSE.toml new file mode 100644 index 0000000..42aab58 --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/images/REUSE.toml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC0-1.0 + +version = 1 + +[[annotations]] +path = ["**/*.gif", "**/*.jpg", "**/*.png", "**/*.svg"] +precedence = "closest" +SPDX-FileCopyrightText = "The openTCS Authors" +SPDX-License-Identifier = "CC-BY-4.0" diff --git a/opentcs-documentation/src/docs/developers-guide/images/commadapter_classes_kcc.png b/opentcs-documentation/src/docs/developers-guide/images/commadapter_classes_kcc.png new file mode 100644 index 0000000..fa4102e Binary files /dev/null and b/opentcs-documentation/src/docs/developers-guide/images/commadapter_classes_kcc.png differ diff --git a/opentcs-documentation/src/docs/developers-guide/images/commadapter_classes_kcc.puml b/opentcs-documentation/src/docs/developers-guide/images/commadapter_classes_kcc.puml new file mode 100644 index 0000000..cb599f8 --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/images/commadapter_classes_kcc.puml @@ -0,0 +1,46 @@ +@startuml +' SPDX-FileCopyrightText: The openTCS Authors +' SPDX-License-Identifier: CC-BY-4.0 + +skinparam monochrome true +'skinparam linetype ortho +hide empty members +'left to right direction + +together { + interface VehicleCommAdapterPanelFactory + + class "**CustomPanelFactory**" as CustomPanelFactory + + VehicleCommAdapterPanelFactory <|-- CustomPanelFactory +} + +together { + abstract class VehicleCommAdapterPanel + + class "**CustomPanel**" as CustomPanel + + VehicleCommAdapterPanel <|-- CustomPanel +} + +together { + abstract class VehicleCommAdapterDescription + + class "**CustomDescription**" as CustomDescription + + VehicleCommAdapterDescription <|-- CustomDescription +} + +together { + class VehicleProcessModelTO + + class "**CustomProcessModelTO**" as CustomProcessModelTO + + VehicleProcessModelTO <|-- CustomProcessModelTO +} + +CustomPanelFactory ..> CustomPanel : instantiates + +CustomPanel ..> CustomProcessModelTO : consumes + +@enduml diff --git a/opentcs-documentation/src/docs/developers-guide/images/commadapter_classes_kernel.png b/opentcs-documentation/src/docs/developers-guide/images/commadapter_classes_kernel.png new file mode 100644 index 0000000..483ea7b Binary files /dev/null and b/opentcs-documentation/src/docs/developers-guide/images/commadapter_classes_kernel.png differ diff --git a/opentcs-documentation/src/docs/developers-guide/images/commadapter_classes_kernel.puml b/opentcs-documentation/src/docs/developers-guide/images/commadapter_classes_kernel.puml new file mode 100644 index 0000000..048ed85 --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/images/commadapter_classes_kernel.puml @@ -0,0 +1,62 @@ +@startuml +' SPDX-FileCopyrightText: The openTCS Authors +' SPDX-License-Identifier: CC-BY-4.0 + +skinparam monochrome true +'skinparam linetype ortho +hide empty members +'left to right direction + +together { + interface VehicleCommAdapterFactory + + class "**CustomAdapterFactory**" as CustomAdapterFactory + + VehicleCommAdapterFactory <|-- CustomAdapterFactory +} + +together { + interface VehicleCommAdapter + + abstract class BasicVehicleCommAdapter + + class "**CustomAdapter**" as CustomAdapter + + VehicleCommAdapter <|-- BasicVehicleCommAdapter + BasicVehicleCommAdapter <|-- CustomAdapter + + note "Implements communication\nwith a single vehicle." as adapterNote + CustomAdapter .. adapterNote +} + +together { + class VehicleProcessModel + + class "**CustomProcessModel**" as CustomProcessModel + + VehicleProcessModel <|-- CustomProcessModel +} + +together { + abstract class VehicleCommAdapterDescription + + class "**CustomDescription**" as CustomDescription + + VehicleCommAdapterDescription <|-- CustomDescription +} + +together { + class VehicleProcessModelTO + + class "**CustomProcessModelTO**" as CustomProcessModelTO + + VehicleProcessModelTO <|-- CustomProcessModelTO +} + +CustomAdapterFactory ..> CustomAdapter : instantiates +CustomAdapterFactory ..> CustomDescription : provides + +CustomAdapter ..> CustomProcessModelTO : produces + +CustomAdapter "1" --> "1" CustomProcessModel +@enduml diff --git a/opentcs-documentation/src/docs/developers-guide/images/movementcommand_classes.png b/opentcs-documentation/src/docs/developers-guide/images/movementcommand_classes.png new file mode 100644 index 0000000..3c86928 Binary files /dev/null and b/opentcs-documentation/src/docs/developers-guide/images/movementcommand_classes.png differ diff --git a/opentcs-documentation/src/docs/developers-guide/images/movementcommand_classes.puml b/opentcs-documentation/src/docs/developers-guide/images/movementcommand_classes.puml new file mode 100644 index 0000000..95f1646 --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/images/movementcommand_classes.puml @@ -0,0 +1,27 @@ +@startuml +' SPDX-FileCopyrightText: The openTCS Authors +' SPDX-License-Identifier: CC-BY-4.0 + +skinparam monochrome true +'skinparam linetype ortho +left to right direction + +Class MovementCommand { + -operation : String + -properties : Map + -... +} + +Class "Route.Step" as Step { + -path : Path + -sourcePoint : Point + -destinationPoint : Point +} + +Class Location + +note bottom of Location : May not be present if the point of the transport\norder is only about moving the vehicle\nto a different position. + +MovementCommand "1" -- "1" Step +MovementCommand "1" -- "0..1" Location +@enduml diff --git a/opentcs-documentation/src/docs/developers-guide/images/peripheral_commadapter_classes_kcc.png b/opentcs-documentation/src/docs/developers-guide/images/peripheral_commadapter_classes_kcc.png new file mode 100644 index 0000000..945e69f Binary files /dev/null and b/opentcs-documentation/src/docs/developers-guide/images/peripheral_commadapter_classes_kcc.png differ diff --git a/opentcs-documentation/src/docs/developers-guide/images/peripheral_commadapter_classes_kcc.puml b/opentcs-documentation/src/docs/developers-guide/images/peripheral_commadapter_classes_kcc.puml new file mode 100644 index 0000000..b56ae84 --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/images/peripheral_commadapter_classes_kcc.puml @@ -0,0 +1,46 @@ +@startuml +' SPDX-FileCopyrightText: The openTCS Authors +' SPDX-License-Identifier: CC-BY-4.0 + +skinparam monochrome true +'skinparam linetype ortho +hide empty members +'left to right direction + +together { + interface PeripheralCommAdapterPanelFactory + + class "**CustomPanelFactory**" as CustomPanelFactory + + PeripheralCommAdapterPanelFactory <|-- CustomPanelFactory +} + +together { + abstract class PeripheralCommAdapterPanel + + class "**CustomPanel**" as CustomPanel + + PeripheralCommAdapterPanel <|-- CustomPanel +} + +together { + abstract class PeripheralCommAdapterDescription + + class "**CustomDescription**" as CustomDescription + + PeripheralCommAdapterDescription <|-- CustomDescription +} + +together { + class PeripheralProcessModel + + class "**CustomProcessModel**" as CustomProcessModel + + PeripheralProcessModel <|-- CustomProcessModel +} + +CustomPanelFactory ..> CustomPanel : instantiates + +CustomPanel ..> CustomProcessModel : consumes + +@enduml diff --git a/opentcs-documentation/src/docs/developers-guide/images/peripheral_commadapter_classes_kernel.png b/opentcs-documentation/src/docs/developers-guide/images/peripheral_commadapter_classes_kernel.png new file mode 100644 index 0000000..7c456ad Binary files /dev/null and b/opentcs-documentation/src/docs/developers-guide/images/peripheral_commadapter_classes_kernel.png differ diff --git a/opentcs-documentation/src/docs/developers-guide/images/peripheral_commadapter_classes_kernel.puml b/opentcs-documentation/src/docs/developers-guide/images/peripheral_commadapter_classes_kernel.puml new file mode 100644 index 0000000..e5f6927 --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/images/peripheral_commadapter_classes_kernel.puml @@ -0,0 +1,65 @@ +@startuml +' SPDX-FileCopyrightText: The openTCS Authors +' SPDX-License-Identifier: CC-BY-4.0 + +skinparam monochrome true +'skinparam linetype ortho +hide empty members +'left to right direction + +together { + interface PeripheralCommAdapterFactory + + class "**CustomAdapterFactory**" as CustomAdapterFactory + + PeripheralCommAdapterFactory <|-- CustomAdapterFactory +} + +together { + interface PeripheralCommAdapter + + abstract class BasicPeripheralCommAdapter + + class "**CustomAdapter**" as CustomAdapter + + PeripheralCommAdapter <|-- BasicPeripheralCommAdapter + BasicPeripheralCommAdapter <|-- CustomAdapter + + note "Implements communication\nwith a single peripheral device." as adapterNote + CustomAdapter .. adapterNote +} + +together { + class PeripheralProcessModel + + class "**CustomProcessModel**" as CustomProcessModel + + PeripheralProcessModel <|-- CustomProcessModel +} + +together { + abstract class PeripheralCommAdapterDescription + + class "**CustomDescription**" as CustomDescription + + PeripheralCommAdapterDescription <|-- CustomDescription +} + +together { + interface LowLevelCommunicationEvent + + abstract class PeripheralCommAdapterEvent + + class "**PeripheralProcessModelEvent**" as PeripheralProcessModelEvent + + PeripheralCommAdapterEvent <|-- PeripheralProcessModelEvent + LowLevelCommunicationEvent <|-- PeripheralCommAdapterEvent +} + +CustomAdapterFactory ..> CustomAdapter : instantiates +CustomAdapterFactory ..> CustomDescription : provides + +CustomAdapter ..> PeripheralProcessModelEvent : produces + +CustomAdapter "1" --> "1" CustomProcessModel +@enduml diff --git a/opentcs-documentation/src/docs/developers-guide/images/service_interfaces_others.png b/opentcs-documentation/src/docs/developers-guide/images/service_interfaces_others.png new file mode 100644 index 0000000..6d13e22 Binary files /dev/null and b/opentcs-documentation/src/docs/developers-guide/images/service_interfaces_others.png differ diff --git a/opentcs-documentation/src/docs/developers-guide/images/service_interfaces_others.puml b/opentcs-documentation/src/docs/developers-guide/images/service_interfaces_others.puml new file mode 100644 index 0000000..f2c93f0 --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/images/service_interfaces_others.puml @@ -0,0 +1,29 @@ +@startuml +' SPDX-FileCopyrightText: The openTCS Authors +' SPDX-License-Identifier: CC-BY-4.0 + +skinparam monochrome true +'skinparam linetype ortho +'left to right direction +hide empty members + +interface QueryService +interface InternalQueryService + +QueryService <|-- InternalQueryService + +interface PeripheralDispatcherService +interface DispatcherService +interface RouterService +interface SchedulerService + +QueryService -right[hidden]-> PeripheralDispatcherService +PeripheralDispatcherService -right[hidden]-> DispatcherService +DispatcherService -right[hidden]-> RouterService +RouterService -right[hidden]-> SchedulerService + +interface NotificationService + +InternalQueryService -right[hidden]-> NotificationService + +@enduml diff --git a/opentcs-documentation/src/docs/developers-guide/images/service_interfaces_tcsobjects.png b/opentcs-documentation/src/docs/developers-guide/images/service_interfaces_tcsobjects.png new file mode 100644 index 0000000..bc768cb Binary files /dev/null and b/opentcs-documentation/src/docs/developers-guide/images/service_interfaces_tcsobjects.png differ diff --git a/opentcs-documentation/src/docs/developers-guide/images/service_interfaces_tcsobjects.puml b/opentcs-documentation/src/docs/developers-guide/images/service_interfaces_tcsobjects.puml new file mode 100644 index 0000000..1e8b76b --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/images/service_interfaces_tcsobjects.puml @@ -0,0 +1,36 @@ +@startuml +' SPDX-FileCopyrightText: The openTCS Authors +' SPDX-License-Identifier: CC-BY-4.0 + +skinparam monochrome true +'skinparam linetype ortho +'left to right direction +hide empty members + +interface TCSObjectService + +interface PlantModelService +interface TransportOrderService +interface VehicleService +interface PeripheralJobService +interface PeripheralService + +interface InternalPlantModelService +interface InternalTransportOrderService +interface InternalVehicleService +interface InternalPeripheralJobService +interface InternalPeripheralService + +TCSObjectService <|-down- PlantModelService +TCSObjectService <|-down- TransportOrderService +TCSObjectService <|-down- VehicleService +TCSObjectService <|-down- PeripheralJobService +TCSObjectService <|-down- PeripheralService + +PlantModelService <|-down- InternalPlantModelService +TransportOrderService <|-down- InternalTransportOrderService +VehicleService <|-down- InternalVehicleService +PeripheralJobService <|-down- InternalPeripheralJobService +PeripheralService <|-down- InternalPeripheralService + +@enduml diff --git a/opentcs-documentation/src/docs/developers-guide/images/tcsobject_classes.png b/opentcs-documentation/src/docs/developers-guide/images/tcsobject_classes.png new file mode 100644 index 0000000..53c1391 Binary files /dev/null and b/opentcs-documentation/src/docs/developers-guide/images/tcsobject_classes.png differ diff --git a/opentcs-documentation/src/docs/developers-guide/images/tcsobject_classes.puml b/opentcs-documentation/src/docs/developers-guide/images/tcsobject_classes.puml new file mode 100644 index 0000000..9677ccf --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/images/tcsobject_classes.puml @@ -0,0 +1,55 @@ +@startuml +' SPDX-FileCopyrightText: The openTCS Authors +' SPDX-License-Identifier: CC-BY-4.0 + +skinparam monochrome true +'skinparam linetype ortho +hide empty members +'left to right direction + +abstract class TCSObject { +' -name: String +' -reference: TCSObjectReference +' -properties: Map +} + +abstract class TCSResource + +package "Plant model" { + package "Driving course model" { + class Point + class Path + class Location + class LocationType + } + + class Vehicle +} + +package "Transport orders" { + class TransportOrder + class OrderSequence +} + +package "Peripheral jobs" { + class PeripheralJob +} + +TCSObject <|-up- TCSResource + +TCSResource <|-up- Point +TCSResource <|-up- Path +TCSResource <|-up- Location + +TCSObject <|-up- LocationType +TCSObject <|-up- Vehicle + +TCSObject <|-- TransportOrder +TCSObject <|-- OrderSequence + +TCSObject <|-- PeripheralJob + +' This puts the transport order and the peripheral job package below TCSResource, which looks a bit nicer. +TCSResource -[hidden]-> TransportOrder +TCSResource -[hidden]-> PeripheralJob +@enduml diff --git a/opentcs-documentation/src/docs/developers-guide/images/transformer_classes.png b/opentcs-documentation/src/docs/developers-guide/images/transformer_classes.png new file mode 100644 index 0000000..dce97ce Binary files /dev/null and b/opentcs-documentation/src/docs/developers-guide/images/transformer_classes.png differ diff --git a/opentcs-documentation/src/docs/developers-guide/images/transformer_classes.puml b/opentcs-documentation/src/docs/developers-guide/images/transformer_classes.puml new file mode 100644 index 0000000..e88a494 --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/images/transformer_classes.puml @@ -0,0 +1,35 @@ +@startuml +' SPDX-FileCopyrightText: The openTCS Authors +' SPDX-License-Identifier: CC-BY-4.0 + +skinparam monochrome true +hide empty members + +interface MovementCommandTransformer { + +apply(MovementCommand) : MovementCommand +} + +interface IncomingPoseTransformer { + +apply(Pose) : Pose +} + +interface VehicleDataTransformerFactory { + +getName() : String + +createMovementCommandTransformer(Vehicle) : MovementCommandTransformer + +createIncomingPoseTransformer(Vehicle) : IncomingPoseTransformer + +providesTransformersFor(Vehicle) : boolean +} + +class "**MyMovementCommandTransformer**" as MyMovementCommandTransformer +MovementCommandTransformer <|.. MyMovementCommandTransformer + +class "**MyIncomingPoseTransformer**" as MyIncomingPoseTransformer +IncomingPoseTransformer <|.. MyIncomingPoseTransformer + +class "**MyVehicleDataTransformerFactory**" as MyVehicleDataTransformerFactory +VehicleDataTransformerFactory <|.. MyVehicleDataTransformerFactory + +MyVehicleDataTransformerFactory ..> MovementCommandTransformer : <> +MyVehicleDataTransformerFactory ..> IncomingPoseTransformer : <> + +@enduml diff --git a/opentcs-documentation/src/docs/developers-guide/images/transport_order_course.png b/opentcs-documentation/src/docs/developers-guide/images/transport_order_course.png new file mode 100644 index 0000000..0da96b5 Binary files /dev/null and b/opentcs-documentation/src/docs/developers-guide/images/transport_order_course.png differ diff --git a/opentcs-documentation/src/docs/developers-guide/images/transportorder_classes.png b/opentcs-documentation/src/docs/developers-guide/images/transportorder_classes.png new file mode 100644 index 0000000..66cc7c6 Binary files /dev/null and b/opentcs-documentation/src/docs/developers-guide/images/transportorder_classes.png differ diff --git a/opentcs-documentation/src/docs/developers-guide/images/transportorder_classes.puml b/opentcs-documentation/src/docs/developers-guide/images/transportorder_classes.puml new file mode 100644 index 0000000..5ed9ff7 --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/images/transportorder_classes.puml @@ -0,0 +1,42 @@ +@startuml +' SPDX-FileCopyrightText: The openTCS Authors +' SPDX-License-Identifier: CC-BY-4.0 + +skinparam monochrome true +'skinparam linetype ortho +left to right direction + +Class TransportOrder { + -state : TransportOrder.State + -... +} + +Class DriveOrder { + -state : DriveOrder.State + -... +} + +Class "DriveOrder.Destination" as Destination { + -destination : TCSObjectReference + -operation : String + -properties : Map +} + +Class Route { + -costs : long + -... +} + +note bottom of Route : Exists only if a route was\ncomputed for a vehicle. + +Class "Route.Step" as Step { + -path : Path + -sourcePoint : Point + -destinationPoint : Point +} + +TransportOrder "1" *-- "1..*" DriveOrder +DriveOrder "1" -- "1" Destination +DriveOrder "1" -- "0..1" Route +Route "1" *-- "1..*" Step +@enduml diff --git a/opentcs-documentation/src/docs/developers-guide/images/transportorder_states.png b/opentcs-documentation/src/docs/developers-guide/images/transportorder_states.png new file mode 100644 index 0000000..6be8932 Binary files /dev/null and b/opentcs-documentation/src/docs/developers-guide/images/transportorder_states.png differ diff --git a/opentcs-documentation/src/docs/developers-guide/images/transportorder_states.puml b/opentcs-documentation/src/docs/developers-guide/images/transportorder_states.puml new file mode 100644 index 0000000..d39d4b0 --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/images/transportorder_states.puml @@ -0,0 +1,38 @@ +@startuml +' SPDX-FileCopyrightText: The openTCS Authors +' SPDX-License-Identifier: CC-BY-4.0 + +skinparam monochrome true + +' left to right direction + +RAW : The order parameters\nare being set up. +ACTIVE : The order parameters\nare set up. +DISPATCHABLE : Order parameters are valid.\nThe order is ready to be\nprocessed. +BEING_PROCESSED : A vehicle is processing\nthe order. +WITHDRAWN : The order was withdrawn. If a vehicle\nwas assigned to it, it is executing the\nleft-over commands already given. +FINISHED : The order was processed\nsuccessfully. +FAILED : Processing of the order could\nnot be finished successfully. +UNROUTABLE : No complete route to process the\norder with a vehicle was found. + +[*] --> RAW + +RAW -right-> ACTIVE + +ACTIVE --> UNROUTABLE +ACTIVE -right-> DISPATCHABLE + +UNROUTABLE --> [*] + +DISPATCHABLE --> BEING_PROCESSED +DISPATCHABLE --> WITHDRAWN + +BEING_PROCESSED -right-> WITHDRAWN +BEING_PROCESSED --> FAILED +BEING_PROCESSED --> FINISHED + +WITHDRAWN --> FAILED + +FAILED --> [*] +FINISHED --> [*] +@enduml diff --git a/opentcs-documentation/src/docs/developers-guide/opentcs-developers-guide.adoc b/opentcs-documentation/src/docs/developers-guide/opentcs-developers-guide.adoc new file mode 100644 index 0000000..d1d4379 --- /dev/null +++ b/opentcs-documentation/src/docs/developers-guide/opentcs-developers-guide.adoc @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + += openTCS: Developer's Guide +The openTCS developers +:doctype: book +:toc: left +:toclevels: 3 +:sectnums: all +:sectnumlevels: 6 +:imagesdir: images +:icons: font +:source-highlighter: coderay +:coderay-linenums-mode: table +:last-update-label!: +:experimental: +ifdef::backend-pdf[:imagesdir: {imagesoutdir}] +ifdef::backend-html5[:imagesdir: images] + +include::01_general-development.adoc[] + +include::02_kernel-api.adoc[] + +include::03_generate-integration-project.adoc[] + +include::04_extending-kernel.adoc[] + +include::05_extending-control-center.adoc[] + +include::06_extending-model-editor-and-operations-desk.adoc[] + +include::07_supplement-configuration.adoc[] + +include::08_translating.adoc[] diff --git a/opentcs-documentation/src/docs/release-notes/changelog.adoc b/opentcs-documentation/src/docs/release-notes/changelog.adoc new file mode 100644 index 0000000..e7a8cfc --- /dev/null +++ b/opentcs-documentation/src/docs/release-notes/changelog.adoc @@ -0,0 +1,1037 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + += openTCS: Change Log +The openTCS developers +:doctype: article +:toc: left +:toclevels: 3 +:sectnums!: +:icons: font +:source-highlighter: coderay +:coderay-linenums-mode: table +:last-update-label!: +:experimental: + +This change log lists the most relevant changes for past releases in reverse chronological order. +(Note that the openTCS project adheres to https://semver.org/[Semantic Versioning].) + +== Version 6.2 (2024-11-06) + +* New features and enhancements: +** Add support for pluggable transformation of data sent to / received from vehicles, e.g. for conversion between the coordinate system in the plant model and a vehicle-specific one. +** Allow assignment of externally-created recharging orders to vehicles with critical energy level. +** Update web API specification and implementation to version 1.9.0: +*** Add missing _required_ markers for request and response bodies. +*** Include a vehicle's 'sufficiently recharged' and 'fully recharged' energy levels when requesting vehicle data. +* Bugs fixed: +** When receiving a position update from a vehicle, accept any position belonging to the movement commands sent to the vehicle, not just the next one. + This is necessary to support cases in which a vehicle has completed more than one movement command during state/position updates. +** When aggregating ``TCSObjectEvent``s for RMI clients, actually aggregate the oldest and youngest events properly instead of keeping only the youngest one. +** Ask user for confirmation before overwriting files when using the _Save Model As..._ menu item in the Model Editor application. +** Allow the position in `org.opentcs.data.model.Pose` to be `null`. +* Changes affecting developers: +** Use `Pose` to replace and deprecate the previously separate position and orientation angle members in `Vehicle` and `VehicleProcessModel`. +** Update JUnit to 5.11.2. +** Update Hamcrest to 3.0. +** Update Mockito to 5.14.2. +** Update AssertJ to 3.26.3. +** Update ApprovalTests to 24.8.0. +** Update Checkstyle to 10.18.2. +** Update Jackson to 2.18.0. +** Update SLF4J to 2.0.16. +** Update Gradle wrapper to 8.10.2. +** Update Gradle Dependency License Report plugin to 2.9. + +== Version 6.1.2 (2024-09-19) + +* Bugs fixed: +** Properly handle paths that are being traversed in reverse direction in the bounding box edge evaluator. + For such paths, the bounding box at the path's source point is now correctly considered (and not the one at its destination point). +** Ensure the POMs of the published Maven artifacts have their dependencies properly declared. + With the releases of openTCS 6.1 and openTCS 6.1.1, dependencies on openTCS artifacts were using wrongly spelled artifact IDs, making it impossible to include openTCS artifacts as dependencies in projects. + +== Version 6.1.1 (2024-09-16) + +* Bugs fixed: +** Correctly enable/disable controls in the Operations Desk application when it is connected to / disconnected from the kernel. + +== Version 6.1 (2024-09-12) + +* New features and enhancements: +** Ignore path locks and configured edge evaluators when checking for general routability of transport orders. + As paths locks and the results of edge evaluators may change during operation of a plant, it does not seem reasonable to consider them when checking for _general_ routability. +** Reduce the load on RMI clients by aggregating consecutive ``TCSObjectEvent``s for the same object into a single event. +** Reduce the load on the kernel induced by the Operations Desk and Kernel Control Center applications by moderately increasing the interval in which they fetch events form the kernel. +** Add a watchdog task to the kernel which periodically monitors the state of vehicles in the plant model and publishes a user notification in case a vehicle is considered _stranded_ (e.g., in cases where a vehicle is idle but has been assigned a transport order and is therefore expected to do something). +** Add support for specifying a bounding box for a vehicle via the Model Editor application. + A vehicle's bounding box, which, among other things, is defined by a length, width and height, replaces the vehicle's "length" property, which could previously be specified for vehicles. +** Add support for specifying a maximum vehicle bounding box for a point via the Model Editor application. +** Add an edge evaluator that prevents vehicles from being routed to/through points where there is not enough space available (according to the vehicle's bounding box and the maximum allowed bounding box at a point). + For more information, please refer to the user's guide. +** Allow a vehicle's set of energy level thresholds to be modified during runtime via the Operations Desk application. +** Allow the user to actively connect/disconnect the Operations Desk application to/from a kernel. + Add corresponding entries to the application's _File_ menu, which make it possible to change between different kernels during runtime. +** Improve performance when repeatedly computing routes with the same set of resources to be avoided. +** Update web API specification and implementation to version 1.8.0: +*** The endpoint `POST /plantModel/topologyUpdateRequest` now also accepts an optional list of path names allowing the routing topology to be updated selectively. +*** Add an endpoint `PUT /vehicles/:NAME/energyLevelThresholdSet`, which allows a vehicle's set of energy level thresholds to be modified during runtime. +* Bugs fixed: +** Correctly calculate the costs for new routes when rerouting transport orders for which resources to be avoided are defined. +** Use the correct XML schema for v0.0.5 plant model files. +** Correctly restore layer information when loading v0.0.4 or v0.0.5 plant model files. +** Fix handling of forced rerouting: +*** Prevent the kernel executor thread from getting stuck in a loop when forcefully rerouting a vehicle that has reported an unexpected position while waiting for a peripheral job to be finished. +*** Fix an issue where a vehicle would not get rerouted correctly when forcefully rerouting it after it has reported an unexpected position. +*** Prevent a vehicle driver from receiving any further ``MovementCommand``s when the vehicle reported an unexpected position while processing a transport order. + A vehicle driver will continue to receive ``MovementCommand``s after the vehicle has been forcefully rerouted. +*** Prevent vehicles from being forcefully rerouted when there are unfinished peripheral jobs (that have the completion required flag set to `true`). +* Changes affecting developers: +** Deprecate `Point.isHaltingPosition()`. + With openTCS 6.0, the point type `REPORT_POSITION` was removed, which makes this method redundant, as all remaining point types allow halting. + +== Version 6.0 (2024-06-19) + +* Changes affecting developers: +** Update project to Java 21. +** Update slf4j to 2.0.13. +** Update Guice to 7.0.0. +** Use annotations `jakarta.annotation.Nullable` and `jakarta.annotation.Nonnull` instead of `javax.annotation.Nullable` and `javax.annotation.Nonnull`. + For the latter, use of the `javax` namespace was never officially approved, so the former may be considered more official. +** Remove code for reading configuration (interfaces) via cfg4j. + Reading configuration (interfaces) via gestalt, which had already been made the default previously, is now the only integrated variant. +** Remove deprecated code. +* Other changes: +** Replace the configuration prefix 'plantoverviewapp' in the Model Editor and Operations Desk applications (which is reminiscent of the old Plant Overview application) with prefixes that are more suitable for the respective applications. +** Update web API specification and implementation to version 1.7.0: +*** Remove support for the `REPORT_POSITION` point type, which was scheduled for removal with openTCS 6.0. + +[IMPORTANT] +.Migration notes +==== +* When a plant model that was created with an earlier version is intended to be used with openTCS 6.0, it is recommended to first load and save the plant model with the Model Editor of the latest openTCS 5 release, which is openTCS 5.17.1 at the time of this writing. + Otherwise, loading such a plant model with openTCS 6 might fail. +* Integration projects need to update any use of slf4j providers to version 2.0.13, too, or the respective logging backend might not be used. +* Integration projects now need to use injection-related annotations in the `jakarta.inject` namespace, e.g. `jakarta.inject.Inject` or `jakarta.inject.Provider`. +==== + +== Version 5.17.1 (2024-06-19) + +* Bugs fixed: +** Avoid ``NullPointerException``s when rerouting vehicles that process transport orders containing drive order steps that don't have a path. + +== Version 5.17 (2024-05-21) + +* Bugs fixed: +** Avoid `ObjectUnknownException` by cleaning orders related to order sequences only once. +** Correctly claim resources for transport orders with multiple drive orders. + This fixes an issue where allocating the first set of resources for the second drive order in a transport order would fail. +** Allow persistence of plant models (to a file and to the kernel) with paths that contain both vehicle envelopes and peripheral operations. +* Changes affecting developers: +** Update Gradle wrapper to 8.7. + +== Version 5.16 (2024-05-09) + +* New features and enhancements: +** Use more sensible defaults for newly created vehicles' recharge energy level threshold values. +** Add proper support for recalculating the length of "2-Bezier", "3-Bezier" and "Poly-Path" paths to the Model Editor. +** Add support for defining vehicle envelopes at points and paths to the Model Editor. +** Make vehicle resource management configurable. + For more details, see the documentation of the `KernelApplicationConfiguration.vehicleResourceManagementType` configuration entry. +** When computing a route / costs of a route not related to a transport order, it is now possible to define a set of resources (i.e., points, paths or locations) that should be avoided by vehicles. +** Update web API specification and implementation to version 1.6.0: +*** The endpoint `POST /vehicles/{NAME}/routeComputationQuery` now also accepts an optional list of names of resources to avoid. +* Bugs fixed: +** When referencing paths via the `tcs:resourcesToAvoid` property in transport orders, don't implicitly avoid their start and end points, as points can have multiple incoming and outgoing paths. +** Don't create the same peripheral job a second time if the vehicle that triggered the job was rerouted before the job was completed. +* Changes affecting developers: +** Adjust the names of some methods in `VehicleProcessModel` and `VehicleProcessModelTO` by removing the redundant "Vehicle" prefix. + +== Version 5.15 (2024-04-14) + +* New features and enhancements: +** Improve performance of updates to the router's routing topology by allowing it to be updated selectively. + (The routing topology can now be updated only for paths that have actually changed.) +** When computing a route for a transport order, it is now possible to define a set of resources (i.e., points, paths or locations) that should be avoided by vehicles processing the respective transport order. + For this, a property with the key `tcs:resourcesToAvoid` can be set on a transport order to a comma-separated list of resource names. + +== Version 5.14 (2024-03-22) + +* New features and enhancements: +** The creation of ambiguous peripheral jobs (by kernel clients or via the web API) that have the `completionRequired` flag set to `true` is now prevented. + (In those cases it is unclear what should happen to the job's `relatedTransportOrder` (if any) in case the job fails.) +** Add a watchdog task to the kernel which periodically monitors the state of blocks in the plant model and publishes a user notification in case a block is occupied by more than one vehicle. + (Such a situation is usually caused by manually moving vehicles around and leads to deadlock situations.) +** Update web API specification and implementation to version 1.5.0: +*** When retrieving vehicle information via the web API, include the vehicle's orientation angle. +* Bugs fixed: +** Correctly read configuration entries in the `=,...,=` format when using gestalt as the configuration provider. +* Changes affecting developers: +** Provide related `TransportOrder` and `DriveOrder` objects as part of every `MovementCommand`. + This way, vehicle drivers can easily look up a movement command's context without having to explicitly fetch the data via a kernel service call. +** Update Mockito to 5.11.0. +** Update ApprovalTests to 23.0.0. +** Update Jackson to 2.17.0. +** Update Gradle license report plugin to 2.6. + +== Version 5.13 (2024-02-22) + +* New features and enhancements: +** Improve handling of failed peripheral jobs (where the completion required flag is set to `true`) associated with a transport order and withdraw the respective transport order in such cases. +** Properly implement simulation of a recharging operation in the virtual vehicle driver. +** Add an alternative implementation for reading application configuration from properties files using the gestalt library. + This implementation is intended to replace the one using the cfg4j library and is now used by default by the openTCS Kernel, Kernel Control Center, Model Editor and Operations Desk applications. + (Note that, until openTCS 6, the cgf4j implementation can still be used by setting a system property. + For more details, refer to the developer's guide.) +** Improve resource management on vehicle movement: + When a vehicle moves to a new position without having been ordered to move anywhere, allocating and freeing resources is now properly handled. +** Update web API specification and implementation to version 1.4.0: +*** Add an endpoint for triggering updates of the routing topology. +* Bugs fixed: +** Immediately assigning a transport order to a vehicle in the Operations Desk application now works correctly. +** The loopback adapter now properly resumes operation when switching from single step mode to automatic mode. +** Properly set layout coordinates when creating a location on the x or y axis. +* Changes affecting developers: +** Deprecate `MovementCommand.isWithoutOperation()` and introduce `MovementCommand.hasEmptyOperation()` as a replacement. +** Keep track of a vehicle's drive order route progress in the corresponding transport order the vehicle is processing. + Deprecate `Vehicle.getRouteProgressIndex()` because tracking this in the transport order is more consistent. + (Progress in the drive orders list is also tracked in the transport order.) +** Update JUnit to 5.10.2. +** Update JUnit platform launcher to 1.10.2. +** Update ApprovalTests to 22.3.3. +** Update Mockito to 5.10.0. +** Update AssertJ to 3.25.3. +** Update Jackson to 2.16.1. +** Update JAXB Runtime to 2.3.9. +** Update Gradle wrapper to 8.6. +* Other changes: +** Move/Rename a couple of kernel configuration entries: +*** `kernelapp.rerouteOnRoutingTopologyUpdate` replaces `defaultdispatcher.rerouteOnTopologyChanges`. +*** `kernelapp.rerouteOnDriveOrderFinished` replaces `defaultdispatcher.rerouteOnDriveOrderFinished`. +** Eliminate use of Java's `SecurityManager` from the code. + It hasn't been necessary for quite a while, and does not exist any more with Java 21. +** The default strategies for parking and (re)charging vehicles now create transport orders only for vehicles that are actually allowed to process them (according to the respective vehicle's allowed order types). + +== Version 5.12 (2023-12-15) + +* New features and enhancements: +** In the Operations Desk application, show the vehicle that is allocating a resource in the tooltips of points, paths and locations. +** In the Operations Desk application, only offer locations as transport order destinations that are actually linked to at least one point and that have allowed operations. +** In the Operations Desk application, if a vehicle's transport order is withdrawn regularly (i.e. while allowing the vehicle to finish its movements), only the allocated resources in front of the vehicle are highlighted in grey, while the allocated resources behind the vehicle remain highlighted in the vehicle's route color. +** As with transport orders, the event history of order sequences is now also filled with relevant event data. +* Bugs fixed: +** The load generator plugin now avoids unsuitable locations when generating orders. + For example, locations without a link are considered unsuitable, which usually includes locations representing peripheral devices. +** When retrieving a plant model's visual layout via the web API, its properties are now also provided properly. + Previously, a visual layout's properties would always be empty. +* Changes affecting developers: +** Revamp management of `MovementCommand` queues in `BasicVehicleCommAdapter`. +*** Deprecate methods in `VehicleCommAdapter` related to a communication adapter's command queues and introduce new methods with more descriptive names as a replacement. +*** Simplify constructor of `BasicVehicleCommAdapter`. +* Other changes: +** For transport orders created by the default strategies for parking and (re)charging vehicles, corresponding transport order types of "Park" and "Charge" are now set. + +== Version 5.11 (2023-10-26) + +* New features and enhancements: +** Add support for vehicle envelopes. + In an openTCS plant model, envelopes can now be defined for points and paths a vehicle occupies or traverses. + For vehicles, an envelope key can be defined to indicate which envelopes defined at points and paths should be considered for the respective vehicle. + This way, it is now possible to prevent vehicles from allocating physical areas intersecting with areas already allocated by other vehicles. + (Note that the Model Editor application does not provide any means to set envelopes, yet. + At this point, envelopes can only be input programmatically, i.e. via the Java or web API.) +** Update web API specification and implementation to version 1.3.0: +*** Add new endpoints for updating the _locked_ state of paths and locations. +*** Extend the endpoints for creating and retrieving plant models with respect to the newly added support for vehicle envelopes. +*** Add a new endpoint for updating a vehicle's envelope key. +* Bugs fixed: +** When updating the vehicle's prospective next position, actually consider its future movement commands. +** Actually use a vehicle's preferred recharge location if it is defined. +** When rerouting vehicles, properly consider that movement commands are not created for _report points_ along a vehicle's route. +* Changes affecting developers: +** Allow communication adapters to request transport order withdrawals and integration level updates via `VehicleProcessModel`. +** Update Gradle wrapper to 8.4. +** Update Jackson to 2.15.3. +** Update Mockito to 5.6.0. +** Update ApprovalTests to 22.2.0. +** Update Checkstyle to 10.12.4. + +== Version 5.10 (2023-09-23) + +* New features and enhancements: +** Visualize a vehicle's currently allocated resources and the claimed resource of its current drive order in the Operations Desk instead of just the route of its current drive order. +** User notifications are now shown in a table in the Operations Desk. +** Make peripheral adapters selectable in the Kernel Control Center. +** Allow setting the intended vehicle on a transport order through the transport order service or the web API as long as the transport order has not been assigned to a vehicle, yet. +** Add support for immediate assignment of a transport order to its intended vehicle through the dispatcher service. + For more details, see the new "Immediate transport order assignment" section in the user's guide. +** Add support for route computation to the router service. +** Update web API specification and implementation to version 1.2.0: +*** Add support for specifying and retrieving complete plant models via the web API. +*** Keep web API running across kernel mode changes, e.g. when uploading a new plant model. +*** Add a new endpoint for immediate assignment of transport orders to their intended vehicles. +*** Add a new endpoint for querying routes / route costs. +** Remove the kernel messages panel from the Operations Desk; it has been superseded by the user notifications tab. +** Add a configuration entry for enabling/disabling forced withdrawals from the Operations Desk. +** Add a menu item for recalculating the lengths of paths (for now, simply based on the Euclidean distance between the start and end point) to the Model Editor. +** Show peripheral jobs that a vehicle must wait for before it can continue in the vehicle's tooltip. +** In the User's Guide, document for every configuration entry when changes to it are applied by the respective application. +* Bugs fixed: +** Properly check validity of destination operations when creating transport orders. +** Improve legibility of some text elements in the Model Editor and Operations Desk applications that would not be legible on some systems (e.g. Ubuntu 20.04). +** Ensure the Model Editor application is still operable when resetting the window arrangement while a model element is selected. +** When a peripheral job is reported as finished or failed via `PeripheralJobCallback`, ensure that it is properly marked as such, which was previously not the case in some situations. +** Avoid a NullPointerException when resetting a vehicle's position while it is in integration level `TO_BE_NOTICED`. +** Ensure that order reservations for vehicles are properly cleared in case a vehicle's integration level is changed to anything other than `TO_BE_UTILIZED`. +** Show a vehicle's destination in the vehicles panel in the Operations Desk application in cases where the vehicle is processing a transport order with a destination location. +** Show the correct title in the order sequence details panel in the Operations Desk application. +** Properly handle resources for withdrawn orders, fixing an issue where a vehicle would still wait for a pending resource allocation with the transport order remaining in state `WITHDRAWN`. +** Properly handle situations in which vehicles are rerouted more than once during a single drive order, fixing an issue where routes would otherwise not be considered continuous. +** Actually accept priority key `DEADLINE_AT_RISK_FIRST` in the default dispatcher's configuration entries. +* Changes affecting developers: +** Removed documentation for server side web API errors (code 500). +** Introduce data structure `Pose` in the Java API, and use it to replace and deprecate the previously separate position and orientation angle members in `Point` and `PointCreationTO`. +** Integrate Gradle license report plugin. +** Update Gradle wrapper to 8.3. +** Update Jackson to 2.15.2. +** Update JAXB Runtime to 2.3.8. +** Update JGraphT to 1.5.2. +** Update JUnit to 5.10.0. +** Update Mockito to 5.5.0. +** Update ApprovalTests to 19.0.0. +** Update Checkstyle to 10.12.3. +** Update JaCoCo log plugin to 3.1.0. +* Other changes: +** The peripheral jobs panel in the Operations Desk application will now always be shown. + The option to enable or disable it via the configuration file has been removed. +** Rename peripheral operation execution trigger `BEFORE_MOVEMENT` to `AFTER_ALLOCATION`, as this name reflects better when the operation is actually triggered. + The previous name is deprecated but may still be used; it will implicitly be converted to the new name. +** Sync points' layout and model coordinates in the demo plant model. +** Adjust resource management and let a vehicle claim and allocate the destination location(s) of its transport order in addition to points and paths along its route. + +== Version 5.9 (2023-05-31) + +* New features: +** Make use of the vehicle's length for resources management: +*** When releasing resources after a vehicle has completed a movement command, consider the vehicle's length to decide which resources are actually not required any more. +*** Allow vehicle drivers to update the vehicle's length. +*** Have the loopback vehicle driver update the virtual vehicle's length when it performs load/unload operations, and make the length for both cases configurable. +** Add support for working with order sequences via the web API. +** Add support for updating and retrieving a vehicle's allowed order types via the web API. +** Add support for managing peripherals via the web API: +*** A peripheral's driver can be attached and enabled/disabled. +*** A peripheral driver's attachment information can be retreived. +*** Peripheral jobs assigned to a specific peripheral device can be withdrawn. +*** The dispatcher for peripheral jobs can be triggered via an additional route. +** Provide information about available communication adapters for peripheral devices in the Java API. +** Add a detail panel for peripheral jobs to the Operations Desk. +** Add property and history information to the order sequence detail panel in the Operations Desk. +* Bugs fixed: +** When publishing new user notifications and the number of notifications exceeds the kernel's capacity, keep the youngest ones, not the oldest ones. +* Other changes: +** Update Gradle wrapper to 7.6.1. +** Update License Gradle Plugin to 0.16.1. +** Update Gradle Swagger Generator Plugin to 2.19.2. +** Update JUnit 5 to 5.9.3. +** Update ApprovalTests to 18.6.0. +** Deprecate `SchedulerService.fetchSchedulerAllocations()`, as allocations are now part of the `Vehicle` class. +** Deprecate utility class `Enums`, as its methods can easily be implemented with Java streams these days. +** Display properties of plant model elements, transport orders and peripheral jobs in the Model Editor and Operations Desk applications in lexicographically sorted order. + +== Version 5.8.2 (2023-03-21) + +* Fixes: +** Remove a duplicate key from the OpenAPI specification. + +== Version 5.8.1 (2023-02-27) + +* Fixes: +** Properly set the date for 5.8 in the changelog. + +== Version 5.8 (2023-02-27) + +* New features: +** Add support for explicitly triggering rerouting of single vehicles, including optional _forced_ rerouting from a vehicle's current position even if it was not routed to that position by openTCS. +** Add support for withdrawing/aborting peripheral jobs: +*** Peripheral jobs not related to a transport order can be withdrawn via the API. +*** Peripheral jobs that are related to a transport order will implicitly be aborted when the respective transport order is forcibly withdrawn. +** Add `PlantModelService.getPlantModel()`, which returns a representation of the complete plant model. +** Extend web API: +*** The following properties of transport orders can be specified/retrieved: dispensability, peripheral reservation token, wrapping sequence, type. +*** The dispatcher can be triggered via new endpoints: `POST /transportOrders/dispatcher/trigger` and `POST /vehicles/dispatcher/trigger`. + The old `POST /dispatcher/trigger` is now deprecated. +*** Vehicle drivers can be enabled/disabled. +*** Information about a vehicle's available and currently attached drivers can be retrieved. +*** The currently attached driver of a vehicle can be changed. +** Add support for adding additional peripheral job views in the Operations Desk application via the btn:[View] menu. +* Bugs fixed: +** Fix a bug where regularly withdrawing a transport order with peripheral jobs from a vehicle could prevent the withdrawal from being completed. +** Fix a bug where forcibly withdrawing a transport order from a vehicle that is waiting for a peripheral job to finish would prevent any further commands (e.g. for new transport orders) to be sent to the vehicle. +** Fix resource management for cases in which a vehicle's transport order was withdrawn while the vehicle was waiting for a resource allocation. +** Fix resource management / order processing for cases in which the plant model contains report points. +** Fix a bug where the btn:[menu:View[Reset window arrangement]] option in the Operations Desk application would not restore the peripheral job view. +** Fix a bug in the `GET /events` web API endpoint where the type of individual events would not be included in the response. +** Fix a bug where peripheral jobs in a final state (`FINISHED` or `FAILED`) would never be removed from the internal pool. +** Fix a ClassCastException in the Operations Desk application that could happen when a vehicle figure was updated. +** Fix a misnomer in the web API specification: + There is no _category_ in a transport order, it's called a _type_. +* Other changes: +** Update JAXB Runtime to 2.3.7. +** Update Jackson to 2.14.2. +** Update JUnit 5 to 5.9.2. +** Update AssertJ to 3.24.2. +** Update Mockito to 4.11.0. +** Update Gradle wrapper to 6.9.3. +** Update Checkstyle to 10.7.0. + +== Version 5.7 (2022-12-22) + +* Bugs fixed: +** In the web API, set the content type for a reply to `GET /vehicles/{NAME}` to `application/json` as specified. +** When creating peripheral jobs, copy all attributes of the respective peripheral operation, and set the related vehicle and transport order attributes, too. +* Other changes: +** Avoid redundant property updates from vehicle drivers. +** Avoid using webfonts / Google Fonts API in Asciidoctor documentation. +** Add support for working with peripheral jobs to the web API. +** Split the kernel application's `defaultdispatcher.rerouteTrigger` configuration entry into two separate entries: `defaultdispatcher.rerouteOnTopologyChanges` and `defaultdispatcher.rerouteOnDriveOrderFinished`. + +== Version 5.6 (2022-07-23) + +* New features: +** Add explicit support for pausing vehicles, which would previously be implemented using messages sent to the vehicle drivers without being interpreted by the kernel. + Vehicles now have a proper _paused_ state, and `VehicleService` (and the Operations Desk application with it) provides an explicit way to modify it for each individual vehicle. +** Defer resource allocations for paused vehicles. + This keeps vehicles that do not explicitly support pausing from receiving more movement commands, effectively stopping them after they have processed the commands received before pausing. +** Reflect vehicles' paused states in the web API and provide an endpoint to modify them. +** Reflect vehicles' paused states in the operations desk by shading paused vehicles. +* Bugs fixed: +** Fix a bug where adding peripheral operations to a (newly created) path would also affect other paths in a plant model. +** Fix a bug with auto-attaching communication adapters to vehicles that have a preferred communication adapter configured. + Only attach a preferred communication adapter to a vehicle, if the corresponding adapter factory can actually provide an adapter instance for it. +* Other changes: +** Update the demo model provided in the Model Editor application: +*** Add a new section to show the integration and use of peripheral devices. + The demo model now contains a location that represents an exemplary fire door that vehicles have to interact with when traversing the new section. +*** Update the demo model to use the latest model format (v0.0.4). +** Update Spark to 2.9.4. +** Update Jackson to 2.13.3. +** Update AssertJ to 3.23.1. +** Update Mockito to 4.6.1. + +== Version 5.5 (2022-04-26) + +* New features: +** Inform `EdgeEvaluator` implementations about beginning and end of routing graph creation to allow them to optimize computations, e.g. by caching data that does not change while building the graph. +* Other changes: +** Add documentation for peripheral devices and peripheral operations. + Also enable the respective GUI components by default now that there is documentation. +** In the Operations Desk application's dialog for creating peripheral jobs, offer locations attached to a peripheral driver only. +** Replace old references to the Plant Overview application in the developer's and user's guides with references to the Model Editor and/or Operations Desk applications. +** Remove the statistics kernel extension and plugin panel. + They have been moved to the example integration project. +** Update SLF4J to 1.7.36. +** Update Guice to 5.1.0. +** Update Jakarta XML Bind API to 2.3.3. +** Update JAXB Runtime to 2.3.6. +** Update Jackson to 2.13.2 (and its data-binding package to 2.13.2.2). +** Update Sulky ULID to 8.3.0. +** Update JGraphT to 1.5.1. +** Update cfg4j to 4.4.1. +** Update JSR305 to 3.0.2. +** Update JUnit to 5.8.2. +** Update AssertJ to 3.22.0. +** Update Swagger UI to 3.52.5. +** Update the Gradle wrapper to 6.9.2. +** Update Stats Gradle Plugin to 0.2.2. +** Update License Gradle Plugin to 0.14.0. + +== Version 5.4 (2022-02-25) + +* New features: +** Enable vehicle drivers to inspect the whole transport order before accepting it, not just the respective sequence of destination operations. +** Reflect the currently claimed and allocated resources in a vehicle's state. +** Show the currently claimed and allocated resources for a selected vehicle in the properties panel in the Operations Desk application. +** Show all properties of a path's peripheral operations in a table instead of listing only the location and operation names. +** Update web API specification and implementation to version 1.1.0: +*** Add claimed and allocated resources to the vehicle state and vehicle status message specification. +*** Add the precise position to the vehicle state message specification. +*** When creating transport orders, allow clients to provide incomplete transport order names, i.e. have the kernel complete/generate the names. +*** Add an endpoint for explicitly triggering dispatcher runs. +* Other changes: +** Skip the user confirmation for exiting the Kernel Control Center application. +** In the _File_ menu, improve the names of the entries for uploading a model to the kernel and downloading it from the kernel. +** Update Jackson to 2.13.0. +** Update Spark to 2.9.3. + +== Version 5.3 (2021-09-28) + +* New features: +** Properly specify and implement claim semantics in the `Scheduler` interface, allowing custom scheduling strategies to take vehicles' planned future resource allocations into account. +** Introduce `VehicleCommAdapter.canAcceptNextCommand()`, which can be used to (statically or dynamically) influence the amount of movement commands a comm adapter receives from its `VehicleController`. +* Bugs fixed: +** Execute virtual vehicle simulation using the kernel executor to avoid potential deadlocks. +** Restore single-step mode for virtual vehicles. +** Fix immediate withdrawal of transport orders. +** When the Kernel application is started, initialize its components (e.g. dispatcher, router, scheduler) using the kernel executor, especially to avoid scheduling issues with plant models that are loaded with application start up. +** Fix the order sequence details panel which would not load due to some wrong paths to a resource bundle. +** Fix an issue where the Operations Desk was not in sync with the Kernel when using very large models. +** Fix an issue where cutting and pasting elements in the Model Editor would create multiple elements with the same name. +* Other changes: +** Switch to publishing artifacts via the Maven Central artifact repository. + (Previously, artifacts used to be published to JCenter, an artifact repository that has been discontinued.) +** Update the license information: + All components, including the Model Editor and Operations Desk applications, are now licensed under the terms of the MIT license. +** When a vehicle is waiting for resources to be allocated (e.g. because resources are occupied/blocked by another vehicle), allow it to be rerouted from its current position. + (Previously, rerouting was done from the point for which the vehicle was waiting, which could lead to unnecessary waiting times.) +** When a vehicle is rerouted while it is waiting for peripheral interactions to be finished, properly reroute the vehicle from the peripheral's position. +** When loading plant models with the Model Editor and Operations Desk applications, show more fine-grained steps in the corresponding progress bars. +** In the Operations Desk, sort transport orders and peripheral jobs in the respective tables in descending order according to their creation time. +** Reduce the time it takes the Operations Desk to process vehicle updates. +** Update Gradle wrapper to 6.8.3. +** Update JUnit 4 to 4.13.2. +** Update JUnit 5 to 5.7.2. +** Update Hamcrest to 2.2. + +== Version 5.2 (2021-03-29) + +* New features: +** For plant model elements' tooltip texts in the Operations Desk, sort properties lexicographically and colorize vehicles' states. + +== Version 5.1 (2021-03-24) + +* Bugs fixed: +** Made names generated for transport orders to be (really) lexicographically sortable. +* New features: +** Add a `QueryService` to the kernel that can be used to execute generic/custom queries via registered `QueryResponder` instances. +** Add support for creating plant models with multiple layers. +** Add experimental support for peripheral devices, with device interactions triggered by vehicles travelling along paths. + (Note that this is not really documented, yet, and that _experimental_ means that developers using any parts of it are on their own, for now.) +** Add a new version of the XML Schema definition for the openTCS plant model. +** Allow the scheduler to be triggered explicitly via `Scheduler.reschedule()`. +** Show properties in model elements' tooltips. +* Other changes: +** Split the Plant Overview application in two separate applications: + The Model Editor provides model creation and manipulation functionality, while the Operations Desk is used for interacting with a plant while it is in operation. +** Split the Operations Desk's pause button into a pause and a resume button. +** Remove support for groups. + (Layers can now be used to group plant model components.) +** Allow project-specific edge evaluators and routing group mappings to be used. + +== Version 4.20 (2020-12-18) + +* Fixes: +** Default `Scheduler`: Properly handle requests for _same-direction_ blocks for some edge cases. +** Default `Scheduler`: Really free all resources when taking a vehicle out of the driving course. +* Other changes: +** Plant Overview: Improve performance for vehicle state updates. + +== Version 4.19 (2020-07-02) + +* New features: +** As with paths, locations can now be locked via the Plant Overview application to prevent them from being used by vehicles. + +== Version 5.0 (2020-06-05) + +* Remove deprecated code. +** Remove the TCP host interface kernel extension. +** Remove the kernel application's GUI. +* `TCSObject` and its subclasses are now immutable and do no longer implement the `Cloneable` interface. +* Remove the JDOM dependency. +* In `BasicCommunicationAdapter`, use an injected `ExecutorService` (e.g. the kernel executor) instead of starting a separate thread for every vehicle driver instance. +* Add a new and cleaned up version of the XML Schema definition for the openTCS plant model and add new bindings. +* Update project to Java 13. +* Update Mockito to 2.28.2. + +== Version 4.18 (2020-04-09) + +* New features: +** Provide the route to be travelled to vehicle drivers with every movement order, for cases in which vehicles require some information about it. +** Allow supplementary configuration sources to be registered via service loader. +** Allow a configuration reload interval to be set via a system property. +* Other changes: +** Improve performance of loading a plant model file into the kernel. +** Rename transport order category to transport order type. +** Update Spark to 2.9.1. + +== Version 4.17 (2019-12-10) + +* Bugs fixed: +** In the Plant Overview application's "Continuous load" plugin panel, it is now possible to properly remove/delete entries in the drive order and property tables. +** Changing the loopback driver's state through its panel in the Kernel Control Center application now works in all cases. +* Other changes: +** When using the Kernel's RMI interface with SSL enabled, avoid side effects on other components using SSL. + +== Version 4.16.1 (2019-10-30) + +* Bugs fixed: +** Fix creating links between points and locations in the Plant Overview application. + +== Version 4.16 (2019-10-22) + +* New features: +** Optionally have names for transport orders and order sequences generated by the kernel. + Use ULIDs for these generated names by default, to have lexicographically sortable names. +** Add a `publishEvent()` method to the `KernelServicePortal` interface that RMI-Clients can use to publish events on the Kernel application's event bus. +** Enable the Kernel Control Center application to set positions for all simulating vehicle drivers, not only the loopback driver. +* Bugs fixed: +** Paths that have the same start and end components are now displayed properly in the Plant Overview. +** In the Plant Overview's continuous load panel, transport order definitions can now be saved to and restored from XML files again. + (Note that in the course of fixing this issue, the XML files' structure was improved. + Since the feature had been broken for a while and is not part of a public API, backwards compatibility was not maintained for this. + As a result, transport order definition files from old versions of openTCS cannot be restored.) +** Make using the "try it out" buttons in the OpenAPI documentation possible by setting CORS headers in the web API's responses. + +== Version 4.15 (2019-05-02) + +* New features: +** Add history entries for transport orders being deferred or resumed as well as assigned to or reserved for vehicles in the dispatching process. + This makes it easier to find out e.g. why a transport order wasn't assigned to a vehicle, yet. + It also implicitly deprecates transport orders' rejection entries, as history entries provide the same functionality, but for more use cases. +** Expect applications' locales to be set via BCP 47 language tags, making the configuration more flexible and independent from the source code. +** Extend the default router to be able to extract explicitly given routing costs from path properties, too. +* Bugs fixed: +** In case no load or unload operation is defined for a virtual vehicle, use a default value to avoid exceptions. +** Do not (wrongly) set a vehicle's processing state to `IDLE` whenever its integration level is set to `TO_BE_UTILIZED`. +** Avoid potential deadlocks related to using the Plant Overview's resource allocation panel. +* Other changes: +** Disable the Kernel application's integrated control center GUI by default. + It can still be re-enabled via the Kernel configuration, but it has been deprecated for several openTCS releases now and will be removed with the openTCS 5. +** Move all language files for the applications' internationalization to a common hierarchy, remove unused/left-over entries and apply a proper naming pattern to the remaining ones to improve maintainability. + (The language files for the Kernel application's integrated control center GUI are excluded from this, as that GUI will be removed with openTCS 5.) +** Remove support for the Plant Overview application's old model file format (file name extension `.opentcs`). + The old format has been deprecated since openTCS 4.8 in favour of a unified file format (file name extension `.xml`) shared by Kernel and Plant Overview. + Users who still have model files in the old format may want to save them in the current format before updating. +** Remove the menu item to trigger the kernel's dispatching process from the Plant Overview's main menu. + The dispatcher is triggered automatically (and, for special cases in integration projects, periodically), so manual triggering does not need to be involved. + +== Version 4.14 (2019-03-22) + +* Bugs fixed: +** With the `defaultdispatcher.reparkVehiclesToHigherPriorityPositions` configuration enabled: + Prevent a vehicle from being re-parked to positions that have the same priority as the vehicle's current parking position. +** Fix a bug where charging vehicles don't execute transport orders after they have reached the "sufficiently recharged" state. +* Other changes: +** The Kernel application does no longer persist `Color` and `ViewBookmark` elements of the visual layout. + (For some time now, these elements could no longer be created with the PlantOverview application and were ignored when a model was loaded, anyway.) + +== Version 4.13.1 (2019-02-25) + +* Bugs fixed: +** Fix a bug with the loopback communication adapter that prevents resources from being properly released when the "loopback:initialPosition" property is set on vehicles. + +== Version 4.13 (2019-02-18) + +* New features: +** Introduce an event history for transport orders that can be filled with arbitrary event data. +** Introduce `"*"` as a wildcard in a vehicle's processable categories to allow processing of transport orders in _any_ category. +** The Plant Overview's vehicle panel now also shows the current destination of each vehicle. +* Bugs fixed: +** With the `defaultdispatcher.rerouteTrigger` configuration entry set to `DRIVE_ORDER_FINISHED`, ensure that the rerouting is only applied to the vehicle that has actually finished a drive order. +** For vehicles selected in the Plant Overview, re-allow changing their integration levels via the context menu to either "to be utilized" or "to be respected" if any of them is currently processing a transport order, too. +* Other changes: +** Remove the included integration project generator and document usage of the example integration project, instead. +** Update the web API specification to OpenAPI 3. +** Update Gradle to 4.10.3. +** Update Checkstyle to 8.16. +** Update JUnit to 5.3.2. +** Update Guice to 4.2.2. + +== Version 4.12 (2018-12-20) + +* New features: +** Introduce optional priorities for parking positions. + With these, vehicles are parked at the one with the highest priority. + Optionally, vehicles already parking may be reparked to unoccupied positions with higher priorities. +** Provide additional energy levels for vehicles to influence when recharging may be stopped. +** Make the Plant Overview's naming schemes for plant model elements configurable. +** In the Plant Overview, allow multiple vehicles to be selected for changing the integration level or withdrawing transport orders. +* Bugs fixed: +** Prevent a movement order from being sent to a vehicle a second time after the vehicle got rerouted while waiting for resource allocation. + +== Version 4.11 (2018-12-04) + +* New features: +** Introduce a _type_ property for blocks. + A block's type now determines the rules for entering it: +*** Single vehicle only: The resources aggregated in this block can only be used by a single vehicle at the same time. +*** Same direction only: The resources aggregated in this block can be used by multiple vehicles, but only if they enter the block in the same direction. +* Bugs fixed: +** Properly set a point's layout coordinates when it is placed exactly on an axis in the Plant Overview. +** Properly select the correct/clicked-on tree entry in the Plant Overview's blocks tree view when the same element is a member of more than one block. +** Prevent the Kernel application from freezing when loading some larger plant models. +* Other changes: +** Require the user to confirm _immediate_ withdrawals of transport orders in the plant overview, as they have some implications that may lead to collisions or deadlocks in certain situations. +** Improve input validation of unit-based properties for plant model elements. +** Remove the Kernel Control Center's function to reset the position of a vehicle. + Users should now set the vehicle's integration level to `TO_BE_IGNORED`, instead. +** Allow the loopback driver to be disabled completely. +** Minor improvements to the configuration interface API. +** Mark all `AdapterCommand` implementations in the base API as deprecated. + These commands' functionality is specific to the respective communication adapter and should be implemented and used there. + +== Version 4.10 (2018-08-07) + +* New features: +** Introduce an explicit _integration level_ property for vehicles that expresses to what degree a vehicle should be integrated into the system. + (Setting the integration level to `TO_BE_UTILIZED` replaces the manual dispatching that was previously used to integrate a vehicle.) +** Allow recomputing of a vehicle's route after finishing a drive order or on topology changes. +** Allow vehicle themes to define not only the graphics used, but also the content and style of vehicle labels in the Plant Overview. +** Enable the web API to optionally use HTTPS. +** Allow an optional set of properties for meta information to be stored in a model, and use it to store the model file's last-modified time stamp in it. +* Bugs fixed: +** Prevent moving of model elements in the Plant Overview when in mode OPERATING. +** Prevent creation of groups in the Plant Overview when in mode OPERATING. +** Properly handle renaming of paths and path names that do not follow the default naming pattern in the Plant Overview. +** Multiple minor fixes for the integration project generator. +* Other changes: +** When using the Plant Overview or Kernel Control Center with SSL-encrypted RMI, verification of the server certificate is now mandatory. +** Adjust the default docking frames layout in the Plant Overview for mode OPERATING a bit to make better use of wide-screen displays. +** Include web API documentation generated by Swagger in the distribution. + +== Version 4.9.1 (2018-04-26) + +* Bugs fixed: +** Include the `buildSrc/` directory in the source distribution. +** Properly display vehicle routes after adding driving course views in the Plant Overview. +** Properly disconnect the plant overview from the kernel when switching to modelling mode. + +== Version 4.9 (2018-04-16) + +* Bugs fixed: +** Fix jumping mouse cursor when dragging/moving model elements in the Plant Overview in some cases. +* New features: +** Allow the kernel to work headless, i.e. without a GUI. + Introduce a separate Kernel Control Center application that provides the same functionality and can be attached to the kernel as a client. +** Provide a single-threaded executor for sequential processing of tasks in the kernel, which helps avoiding locking and visibility issues. + Use this executor for most tasks, especially the ones manipulating kernel state, that were previously executed concurrently. +** Introduce a web API (HTTP + JSON), intended to replace the proprietary TCP/IP host interface, which is now deprecated. +** Introduce an API for pluggable model import and export implementations in the Plant Overview. +* Other changes: +** Split the Kernel interface into aspect-specific service interfaces. +** Provide a (more) simple event API, including an event bus implementation as a replacement for the previously used MBassador and event hub. +** Overhaul the default dispatcher implementation to improve maintainability and extensibility. +** Allow suggestions for property values in the Plant Overview to depend on the key. +** Improve API and deprecate classes and methods in lots of places. +** Improve default formatting of log output for better readability. + +== Version 4.8.4 (2018-02-12) + +* Bugs fixed: +** Fix erroneous behaviour for renaming of points when points are block members in the plant model. + +== Version 4.8.3 (2018-01-24) + +* Bugs fixed: +** Fix processing of XML messages received via the TCP-based host interface. + +== Version 4.8.2 (2017-12-15) + +* Bugs fixed: +** Properly store links between locations and points in the unified XML file format when the link was drawn from the location instead of from the point. + +== Version 4.8.1 (2017-12-05) + +* Bugs fixed: +** Ensure that marshalling and unmarshalling of XML data always uses UTF-8. + This fixes problems with plant models containing special characters (like German umlauts) e.g. in element names. + +== Version 4.8 (2017-11-28) + +* Bugs fixed: +** Properly copy model coordinates to layout coordinates in the plant overview without invalidating the model. +** Adjust erroneous behaviour in the load generator plugin panel and properly update its GUI elements depending on its state. +* New features: +** Add a category property to transport orders and order sequences and a set of processable categories to vehicles, allowing a finer-grained selection of processable orders. +** Prepare proper encryption for RMI connections. +* Other changes: +** Use the unified (i.e. the kernel's) XML file format to load and save plant models in the plant overview by default. + (The plant overview's previous default file format is still supported for both loading and saving. + Support for the old format will eventually be removed in a future version, though, so users are advised to switch to the new format.) +** Remove some unmaintained features from the loopback adapter and its GUI. + +== Version 4.7 (2017-10-11) + +* Bugs fixed: +** Ensure that scheduler modules are properly terminated. +* New features: +** Allow the colors used for vehicles' routes be defined in the plant model. +** Have the default dispatcher periodically check for idle vehicles that could be dispatched. + This picks up vehicles that have not been in a dispatchable state when dispatching them was previously tried. + +== Version 4.6 (2017-09-25) + +* Bugs fixed: +** Don't mark a drive order as finished if the transport order it belongs to was withdrawn. +** Properly update the vehicles' states in the kernel control center's vehicle list. +** When creating locations, properly attach links to the respective points, too. +** When renaming a point in the plant overview, properly update blocks containing paths starting or ending at this point. +** Avoid NPE when the transport order referenced in a `Vehicle` instance does not exist in the kernel any more. +* New features: +** Allow the kernel's RMI port to be set via configuration. +** Allow preferred parking positions and recharge locations to be set as properties on `Vehicle` instances. +** In XML status channel messages, add a reference to a vehicle's transport order, and vice versa. +** Allow the kernel's order cleanup task to be adjusted via predicates that approve cleanup of transport orders and order sequences. +* Other changes: +** Deprecate `VehicleCommAdapter.State`. It's not really used anywhere, and the enum elements are fuzzy/incomplete, anyway. + +== Version 4.5 (2017-08-10) + +* Switched to a plain JGraphT-based implementation of Dijkstra's algorithm for routing. +* Deprecated static routes. + All routes are supposed to be computed by the router implementation. + (Both the kernel and the plant overview will still be able to load models containing static routes. + The button for creating new static routes in the plant overview has been removed, however.) +* Introduced caching for configuration entries read via binding interfaces. +* Prepared immutability for plant model and transport order objects within the kernel. +* Deprecated dummy references to objects as well as the superfluous ID attribute in `TCSObject`. +* Made JHotDraw and Docking Frames libraries available as Maven artifacts so they do not have to be kept in the sources distribution. +* Updated Mockito to 2.8.47. + +== Version 4.4 (2017-07-17) + +* Fixed a performance issue with building routing tables in the default router caused by excessive calling of methods on a configuration binding interface. +* Introduced a method to explicitly trigger routing topology updates via the `Kernel` interface instead of explicitly updating it whenever a path was locked/unlocked to avoid redundant computations. +* Improved behaviour with scaling the course model in the plant overview. +* Added a mechanism to provide project-specific suggestions for keys and values when editing object properties in the plant overview. +* Added GUI components to set vehicle properties from the loopback driver's panel. +* Deprecated explicit event filters, which make the code more verbose without adding any value. +* Some small bugfixes and improvements. + +== Version 4.3 (2017-06-30) + +* Introduced configuration based on binding interfaces and cfg4j to provide implementations for these, and deprecated the previously used configuration classes. + Implications and side effects: +** Made documentation of configuration entries (for users) easy via annotations. +** Switched configuration files from XML to properties. +** Switched to read-only configuration. +* Improved maintainability and reusability of the default dispatcher implementation. +* Updated Gradle wrapper to 3.5. +* Many small bugfixes and improvements. + +== Version 4.2 (2017-05-29) + +* Simplify the kernel API by using transfer objects to create plant models and transport orders. + Expect plant models to be transferred as a whole instead of updating existing model elements with multiple calls. +* Actually make use of modules in the default scheduler: A scheduler module can be used to influence the allocation process of resources to vehicles (e.g. to wait for infrastructure feedback before letting a vehicle pass a path). +* A location type's (default) symbol can now be overwritten by a location to display an empty symbol. +* Fix a bug where a large plant model could be loaded multiple times when loaded from the kernel into the plant overview. + +== Version 4.1 (2017-04-20) + +* Added functionality for reading and writing the kernel's plant model file format to the plant overview client. +* Added bezier paths with three control points to the plant overview client. +* Added a panel to observe resource allocations to the plant overview client. +* Added a dialog requiring user confirmation before changing the driver associated with a vehicle to prevent accidental changes. +* Improved performance for transferring model data from the plant overview client to the kernel. +* Improved selection of colors used for marking vehicles' routes in the plant overview client. +* Improved performance of routing table computation by computing only one table shared by all vehicles by default. + (Computation of separate tables for vehicles is optionally possible.) +* Many small bugfixes and improvements to code and documentation. + +== Version 4.0.1 (2017-02-25) + +* Fix a potential deadlock in the default scheduler. + +== Version 4.0 (2017-02-11) + +* Split the base library into a base API, an injection API and a library with commonly used utility classes to reduce the load of transitive dependencies for API users. +* Heavily cleaned up the APIs, including some backwards-incompatible changes (mainly renaming and removing previously deprecated elements). + Notable examples: +** Moved vehicle communication adapter base classes to `org.opentcs.drivers.vehicle` and named them more appropriately. +** Removed TCP/IP communication implementation from `org.opentcs.util.communication.tcp` and a few more utility classes. + Maintaining these is out of the openTCS project's scope. +* Greatly improved extension and customization capabilities for both the kernel and plant overview applications by applying dependency injection in more places. +** Communication adapters may now participate with dependency injection. +** Default kernel strategies may now easily be overridden. +* Simplified the default `Scheduler` implementation. +* Switched logging to SLF4J. +* Improved project documentation for both users and developers and migrated to Asciidoctor for easier maintenance. +* Updated Guice to 4.1.0. +* Updated Guava to 19.0. +* Updated JDOM to 2.0.6. +* Updated Gradle to 2.13. +* Many small bugfixes and improvements. + +== Version 3.2 (2016-01-19) + +* Switched to Gradle as the build management system for improved dependency management and release process. +This introduces cleanly separate subprojects for base library, basic strategies library, kernel application, plant overview application and documentation. +It also adds clean separation of application code and Guice configuration. +* Added an event bus-backed event hub implementation for the kernel to distribute events sent by e.g. communication adapters and make it possible to forward them to kernel clients. +Also add method `publishEvent()` to `BasicCommunicationAdapter` to allow communication adapters to use it. +* Adjusted the dispatcher's and kernel's methods for withdrawing transport orders to explicitly state whether the order should be withdrawn regularly or aborted immediately, which makes them deterministic for the caller. +* Moved code for handling transport order states/activations from the kernel to the dispatcher implementation for better separation of concerns. +* Improved the use of dependency injection via Guice in the kernel to make the code more modular. +* Added annotation `@ScheduledApiChange` for marking scheduled incompatible API changes. +* Updated library Guava to 18.0. +* Many small fixes and improvements. + +== Version 3.1.1 (2015-05-06) + +* Fix a crash in the plant overview client that occured when the user tried to add a drive order to a transport order. + +== Version 3.1 (2015-03-23) + +* Fix the encoding of model files written by the plant overview client. +* Fix a problem with renaming points that resulted in broken model files. +* Fix a crash that happened when trying to open a context menu on a vehicle in modelling mode. +* Properly set the scale factor when loading a model from a file. +* Avoid a crash when trying to create a transport order with a model that does not contain any locations/transport order destinations. +* Fix direction indicators of paths not being displayed properly after loading a model from a file. +* Fix outdated documentation in a couple of places. + +== Version 3.0 (2014-11-25) + +* The plant overview client can now be used for offline modelling, i.e. without requiring a permanent connection to the kernel. +* To further reflect these changes, the plant overview client now maintains its operating mode independently from the kernel's state. +If the user sets the mode of the plant overview client to `OPERATING` while the kernel is in modelling mode, an empty model will be displayed and the actual model will be loaded as soon as the connected kernel switches back to operating mode. +Furthermore, this allows to modify the driving course model in the plant overview client while the kernel remains in operating mode. +See the manual for more information. +* The management of course model files was moved to the plant overview client. +As of this version, the kernel stores only a single driving course model which can be persisted by selecting the corresponding menu item in the graphical user interface of the plant overview client. +Changes made to the model in the plant overview client must be explicitly transferred to the kernel. +To migrate all of your existing models to this new version, please refer to the manual. +* Changes made to the Kernel API: +** Method `Set getModelNames()` was changed to `String getModelName()`, as from now on there exists only one model at a time. +** Method `loadModel(String modelName)` no longer requires/accepts a parameter. +** Method `saveModel(String modelName, boolean overwrite)` no longer accepts the `boolean` parameter and overwrites the model automatically. +** Method `removeModel(String rmName)` no longer requires/accepts a parameter. +** Methods `createLayout(byte[] layoutData)` and `setLayoutData(TCSObjectReference ref, byte[] newData)` have been removed along with class `Layout`. +* Updated library Google Guava to 17.0. +* Updated library JAXB to 2.2.7. +* Updated project to Java 8. + +== Version 2.7.1 (2014-06-30) + +* Fixed a potential crash with switching to plant operation mode when the model contained static routes. + +== Version 2.7 (2014-06-25) + +* Updated library Docking Frames to 1.1.2p11. +* Added library Google Guava 16.0.1 for better code readability via small utility methods. +* Added position coordinates to locations. +* Added synchronization of model and layout coordinates for points and locations. +* Fixed reconstruction of routing tables when locking/unlocking paths in plant operation mode. +* Reimplemented the former Dijkstra-based routing table construction, now providing one based on breadth-first search and an alternative based on depth-first search, and use pluggable routing cost functions. +* Implemented a proper life cycle for plant overview plugin panels. +* Modified model management to not allow model names to differ in the case of their spelling only to prevent inconsistencies on Windows systems. +* Replaced the reference on a Location in a MovementCommand with the Location itself to provide more information to the vehicle driver. +* Made more wide-spread use of dependency injection via Guice and refactored, cleaned up and simplified source code in many places, primarily in the plant overview client. +* Many small bugfixes and improvements. + +== Version 2.6.1 (2014-03-14) + +* Properly color the route for vehicles that have just been created and not loaded from an existing plant model. +* Fix loading plant models created by older versions of openTCS that contained certain path liner types. +* Properly set point types as read from the plant model in the plant overview client. +* Do not provide a clickable graphical figure in the plant overview client for vehicles that should actually be invisible. + +== Version 2.6 (2014-02-28) + +* Updated library Docking Frames to 1.1.2p10e. +* Updated library JDOM to 2.0.5. +* Updated library JFreeChart to 1.0.17, including an update of JCommon to 1.0.21. +* Updated library JUnit to 4.11, including the addition of Hamcrest 1.3. +* Updated DocBook style sheets to 1.78.1. +* Added library Google Guice 3.0 for dependency injection and thus better modularity. +* Added library Mockito 1.9.5 to simplify and improve the included unit tests. +* Downgraded the Saxon XSL processor to version 6.5.5, as more recent versions seem to have deficiencies with DocBook to FO transformations. +* Merged the experimental generic client application into the plant overview client, which can now be extended with plugin-like panels providing custom functionality. +* Added plugin panels for load generation and statistics reports into the plant overview client. +* Improved the undo/redo functionality of the plant overview client in modelling mode. +* Temporarily disabled the copy-and-paste functionality of the plant overview client in modelling mode until some major usability issues have been sorted out. +* Improved editing of multiple driving course elements at the same time. +* Temporarily disabled the possibility to add background graphics until this works more reliably. +* Unified look-and-feel and fonts in the kernel control center and the plant overview client and removed the selection menu for different Swing look-and-feels from the kernel control center. +* Improved localization of the plant overview client. +* Removed the kernel's explicit "simulation" mode, which was never fully implemented or used and provided practically no advantages over the normal mode of operation, in which vehicles can be simulated using the loopback driver. +* Fixed/improved GUI layout in multiple places of the kernel control center. +* Many bugfixes and improvements to code and documentation. + +== Version 2.5 (2013-12-18) + +* Added library Docking Frames 1.1.2-P8c. +* Made some panels in the plant overview client (un)dockable. +* Added a panel with an overview of all vehicles and their respective states to the plant overview client. +* Added a pause button to the plant overview client to pause/stop all active vehicles at once. +* Introduced pluggable themes to customize the appearance of locations and vehicles in the plant overview. +* Added generic grouping of driving course elements, primarily to support visualization in the plant overview. +* Translated the user manual to English. +* Many small bugfixes and improvements to both the code and the documentation. + +== Version 2.4.2 (2013-07-29) + +* Updated the XML Schema definitions for the host interface. + +== Version 2.4.1 (2013-05-30) + +* Updated the visualization client, including many bug fixes, usability improvements and internationalization (English and German language). +* Properly included a vehicle's length when persisting/materializing a course model. +* Removed an erroneous JAXB annotation that led to an exception when trying to persist load generator input data in the generic client. +* Changed the startup scripts/batch files to look for extension JARs in `lib/openTCS-extensions/` instead of `lib/`. + +== Version 2.4 (2013-02-07) + +* Updated JDOM to 2.0.4. +* Updated JHotDraw to 7.6. +* Updated Checkstyle to 5.6. +* Integrated Saxon 9.4 and Apache FOP 1.1 into the build for processing the DocBook manual. +* Major overhaul of the visualization client, including: +Integration of both modes (modelling and visualization) into a single application, preparation for proper localization and integration of the course layout information into model data structures, making it easier to create complete models including course layout via the kernel API. +(This basically allows to implement other clients that can create new models or import/convert existing models from other applications.) +Using models containing "old" layout data is still supported but deprecated. +* Changed license of the visualization client to LGPL. +* Improved support for vehicle energy management: +For each vehicle, a specific charging operation may be specified (default: "`CHARGE`"), which will be used by the dispatcher to automatically create orders to recharge the vehicle's energy source. +* Improved strategies for selecting parking positions and charging locations. +* Changed initial processing state of a vehicle to `UNAVAILABLE`, preventing immediate dispatching of vehicles on startup. +* Improved kernel methods for withdrawing orders from vehicles and allow setting a vehicle's processing state to `UNAVAILABLE` to prevent it being dispatched again immediately. +* Added kernel method dispatchVehicle() to allow vehicles in state `UNAVAILABLE` to be dispatched again. +* (Re-)Added 'dispensable' flag to class TransportOrder to indicate that an order may be withdrawn automatically by the dispatcher. +(Primarily used to make parking orders abortable.) +* Improved handling of order sequences. +* Added a simple, preliminary implementation of data collection for statistics based on event data in `org.opentcs.util.statistics`. +* Removed class `VehicleType` and all references to it completely. +All information about the vehicles themselves is stored in Vehicle, now, simplifying the code in which `VehicleType` was used. +* Added `Vehicle.State.UNAVAILABLE` for vehicles that are not in an ERROR state but currently remotely usable, either. +(Examples: manual or semi-automatic modes) +* Added methods `Kernel.sendCommAdapterMessage()` and `CommunicationAdapter.processMessage()` to allow clients to send generic messages to communication adapters associated with vehicles. +* Removed methods `stop()`, `pause()` and `resume()` from communication adapter interface as they had not served any purpose for long time. +* Removed kernel method `getInfoText()`, for which the `query()` method has served as a replacement for a while, now. +* Properly propagate exceptions to clients connected via the RMI proxy. +* Small bug fixes and improvements to code and documentation. + +== Version 2.3 (2012-09-17) + +* Moved sources of the generic client into the main project's source tree. +* Updated JFreeChart to 1.0.14. +* Use JFreeChart for drawing the velocity graph of a communication adapter. +* Instead of emitting an event only after the kernel's state changed, emit an additional one before the state transition. +* Implemented org.opentcs.data.order.OrderSequence for processes spanning more than one transport order that should be processed by a single vehicle. +* Added a set of properties to DriveOrder.Destination and MovementCommand, allowing an order/command to carry additional information for a communication adapter or vehicle, if necessary. +* (Re-)Added `State.CHARGING` and merged `State.DRIVING` and `State.OPERATING` into `State.EXECUTING` in `org.opentcs.data.model.Vehicle`. +* Added a settable threshold for critical and good energy levels of a vehicle. +* Added a vehicle specific charging operation to Vehicle, settable by the communication adapter. +* Recompute routing tables when (un)locking a path. +* Remove `org.opentcs.data.model.Path.Action`, which wasn't really used anywhere and doesn't provide any benefit over a Path's properties. +* Remove a lot of deprecated methods in the kernel interface. +* Replace the existing dispatcher with one that is aware of order sequences and vehicles' energy levels and automatically creates orders to recharge vehicles. +* Deprecated and largely removed references to `org.opentcs.data.model.VehicleType`, simplifying some code. +* Bug fix in `KernelStateOperating.activateTransportOrder()`: +Use our own references to the transport order, not the one we received as a parameter, as that causes problems if the order has been renamed but a reference with the old name is being used by the calling client. +* Moved classes to packages properly separated by functionality, and removed a few utility classes that were not used and didn't provide much. +(This effectively means the API provided by the base JAR changed. +Fixing any resulting broken imports should be the only thing required to use the new version.) + +== Version 2.2 (2012-07-10) + +* Published as free open source software (license: the MIT license, see `LICENSE.txt`) - Requires Java 1.7 +* Update JDOM to 2.0.2. +* Integrated kernel and driver GUI into a single application. +* Basic support for energy management +* Support for dynamic load handling devices reported by vehicles/vehicle drivers to the kernel +* Simplified integration of vehicle drivers: Vehicle drivers in the class path are found automatically using `java.util.ServiceLoader`. +* Automatic backup copies (in `$KERNEL/data/backups/`) when saving models +* Switched from properties to XML for configuration files +* Simplified and more consistent kernel API +* Many small bug fixes and adjustments of the included strategies diff --git a/opentcs-documentation/src/docs/release-notes/contributors.adoc b/opentcs-documentation/src/docs/release-notes/contributors.adoc new file mode 100644 index 0000000..3807543 --- /dev/null +++ b/opentcs-documentation/src/docs/release-notes/contributors.adoc @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + += openTCS: Contributors List +The openTCS developers +:doctype: article +:toc!: +:sectnums!: +:icons: font +:source-highlighter: coderay +:coderay-linenums-mode: table +:last-update-label!: +:experimental: + +== Contributors + +We are grateful for anyone helping us to improve openTCS by contributing code or documentation. +So far, the developers involved were/are (in alphabetical order by last name): + +* Sebastian Bonna +* Hubert Buechter +* Iryna Felko +* Martin Grzenia +* Preity Gupta +* Heinz Huber +* Olaf Krause +* Tobias Marquardt +* Sebastian Naumann +* Volkmar Pontow +* Leonard Schüngel +* Philipp Seifert +* Andreas Trautmann +* Stefan Walter +* Mats Wilhelm +* Mustafa Yalciner +* Youssef Zaki + +If you have contributed to openTCS and we have missed you on this list, please accept our apologies and inform us about it. diff --git a/opentcs-documentation/src/docs/release-notes/docinfo.html b/opentcs-documentation/src/docs/release-notes/docinfo.html new file mode 100644 index 0000000..00f4c26 --- /dev/null +++ b/opentcs-documentation/src/docs/release-notes/docinfo.html @@ -0,0 +1,7 @@ + + + + diff --git a/opentcs-documentation/src/docs/release-notes/faq.adoc b/opentcs-documentation/src/docs/release-notes/faq.adoc new file mode 100644 index 0000000..dd3cc68 --- /dev/null +++ b/opentcs-documentation/src/docs/release-notes/faq.adoc @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + += openTCS: Frequently Asked Questions +The openTCS developers +:doctype: article +:toc: left +:toclevels: 3 +:sectnums: all +:sectnumlevels: 3 +:icons: font +:source-highlighter: coderay +:coderay-linenums-mode: table +:last-update-label!: +:experimental: + +== General questions about the software + +=== Which navigation principles are supported? + +openTCS works independently from specific navigation implementations, so any kind of navigation principle may be used. +Locating and navigating are tasks usually performed on the vehicle side: +The vehicle simply reports its current state, including its position, to the control system, and the control system orders the vehicle to move to a different position. + +Note that openTCS focuses on the dispatching of vehicles for transport orders. +This task should not be confused with lower-level vehicle-side tasks like accelerating, decelerating and steering the vehicle, which are out of openTCS's scope. + +=== What are the requirements to manage a vehicle with openTCS? + +The requirements for this are minimal: + +1. Communication with the vehicle must be possible. + (This implies that the vehicle's communication interface must be specified and accessible.) + How this communication is implemented exactly is not really relevant as long as the communication hardware exists and can be used from Java software. + In many cases, standard Wi-Fi hardware is used. +2. The vehicle must be able to report its current position/state. +3. The vehicle must be able to perform movement orders from its current position to an adjacent position in the driving course/environment given by openTCS. + +=== How many vehicles can be managed at the same time? + +There is virtually no limit imposed by the design of the software. +The hardware equipment used (CPU, RAM, communication bandwidth etc.) can, however, limit the system's effective performance. + +=== Which interfaces for transport orders exist? + +Transport orders can be created interactively by a user via the graphical user interface. + +To create transport orders from other systems, e.g. a warehouse management system, interfaces based on Java Remote Method Invocation (RMI) and webservices are included. +These are documented in the developer's guide. +Additional, custom interfaces for software that cannot use these interfaces can be added easily. + +== Plant models + +=== How can I migrate plant models created with openTCS before version 3.0? + +. Backup the contents of the kernel directory's `data/` subdirectory. +It contains subdirectories with a plant model in a `model.xml` file each. +. Clear the kernel's `data/` directory by removing all its subdirectories. +. Copy one of the `model.xml` files from the backup directories back to the `data/` directory, so that it contains only this single plant model. ++ +_Example: +Assuming your `data/` directory contains three model subdirectories `modelA`, `modelB` and `modelC`. +After step one, the three model directories should have been copied to another location. +After step two, the `data/` directory should be empty and after step three, it should contain only the `model.xml` from directory `modelA`._ +. Start the kernel and have it load your model. +Then start the Model Editor application and select btn:[menu:File[Load current kernel model]] from its menu to read the model data from the kernel. +. In the Model Editor application, select btn:[menu:File[Save Model]] or btn:[menu:File[Save Model As]] from the menu. +The Model Editor application will persist the model data in a file with the given name and the extension `.xml`. ++ +_Example: Following the previous example a file with the name `modelA.xml` should exist now._ +. Delete the `model.xml` file you just moved to the `data/` directory of the kernel. +The migration of this plant model is finished. +. Shut down the kernel, go back to step three and repeat the procedure for the remaining models that you want to migrate. ++ +_Example: Follow steps 3 - 7 with `modelB` and `modelC` instead of `modelA`._ + +=== Why are all transport orders marked `UNROUTABLE` when I only have reporting points in my model? + +Vehicles are not allowed to stop at reporting points. +Hence at least the starting point and the endpoint (usually linked to a location) of a route must be halt points to make routing possible. + +=== How can I create curved paths between two points? + +Select btn:[Bezier] from the path tools and connect two points by clicking on the first point, dragging the mouse to the second point and releasing the mouse button there. +Then activate the selection tool and click on the previously created bezier path. +Two blue control points will appear. +Drag the control points to change the shape of the path. + +=== Why do points have two sets of coordinates (model and layout)? + +The openTCS kernel itself works with a logical driving course model - geometric attributes of e.g. points and paths are not relevant for its core functionality. +It may, however, have to provide (real/physical) coordinates of a destination point to a vehicle, depending on the way the vehicle's navigation works. +With openTCS, these are called the __model coordinates__. + +The __layout coordinates__, on the other hand, are coordinates that are used merely for visualizing the driving course in the Model Editor and Operations Desk applications. +These coordinates will probably be the same as the model coordinates in most cases, but they may differ, e.g. in cases where the driving course is supposed to be modelled/displayed in a distorted way. + +== Transport orders + +=== How can I set priorities for transport orders? + +With openTCS, transport orders do not have a priority attribute. +That is because a transport order's priority may change over time or when other transport orders are added. + +In the end, a single transport order's effective priority depends on the dispatcher implementation used. +With openTCS's default dispatcher, transport orders' __deadline__ attributes are intended to be used for prioritizing - the sooner the deadline, the higher an order's effective priority. +To give a transport order a higher priority from the beginning, you can set its deadline to something earlier than all other orders' deadlines, e.g. to "right now" or a point of time in the past. + +== Vehicle drivers + +=== Which side (driver or vehicle) should be the client/server? + +This should be decided based on project-specific requirements. +You are free to implement it either way. + +== Networking + +=== How do I enable access to the RMI interface for clients when the kernel is running on a host with multiple IP addresses? + +See https://docs.oracle.com/javase/8/docs/technotes/guides/rmi/faq.html#netmultihomed. + +=== Why does the communication between the openTCS kernel application and client applications take an extraordinary amount of time when SSL is enabled? + +This is probably because the kernel is running on a host with multiple IP addresses. +See https://docs.oracle.com/javase/8/docs/technotes/guides/rmi/faq.html#netmultihomed. diff --git a/opentcs-documentation/src/docs/release-notes/index.adoc b/opentcs-documentation/src/docs/release-notes/index.adoc new file mode 100644 index 0000000..1ddd09b --- /dev/null +++ b/opentcs-documentation/src/docs/release-notes/index.adoc @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + += openTCS: Distribution Documentation +The openTCS developers +:doctype: article +:toc!: +:sectnums!: +:icons: font +:source-highlighter: coderay +:coderay-linenums-mode: table +:last-update-label!: +:experimental: + +== Content + +* Release Notes: +** Change Log: link:./changelog.html[HTML] +** Contributors List: link:./contributors.html[HTML] +* User Resources: +** User's Guide: + link:./user/opentcs-users-guide.html[HTML], + link:./user/opentcs-users-guide.pdf[PDF] +** Frequently Asked Questions: link:./faq.html[HTML] +* Developer Resources: +** Developer's Guide: + link:./developer/developers-guide/opentcs-developers-guide.html[HTML], + link:./developer/developers-guide/opentcs-developers-guide.pdf[PDF] +** Base API: + link:./developer/api-base/index.html[JavaDoc documentation] +** Injection API: + link:./developer/api-injection/index.html[JavaDoc documentation] +** Web API: + link:./developer/service-web-api-v1/index.html?docExpansion=list[OpenAPI documentation] diff --git a/opentcs-documentation/src/docs/service-web-api-v1/openapi.yaml b/opentcs-documentation/src/docs/service-web-api-v1/openapi.yaml new file mode 100644 index 0000000..3ac4f3e --- /dev/null +++ b/opentcs-documentation/src/docs/service-web-api-v1/openapi.yaml @@ -0,0 +1,3205 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +# OpenAPI 3 specification: +# - https://swagger.io/docs/specification/ +# - https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md +# Online validator: +# - https://apidevtools.org/swagger-parser/online/ +openapi: 3.0.0 +info: + description: >- + Bodies of HTTP requests and responses, where applicable, are JSON structures. + The encoding used may be UTF-8, UTF-16 or UTF-32. + Where time stamps are used, they are encoded using [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601); the time zone used is UTC. + + + The TCP port to be used for the HTTP requests is configuration-dependent; by default, it is 55200. + + + By default, requests are accepted without requiring any authentication. + Optionally, an access key can be set in the kernel configuration. + The configured value is then expected to be sent by the client in an HTTP header named `X-Api-Access-Key`. + # IMPORTANT: When updating this version number, remember to mention that in the changelog, too! + version: 1.9.0 + title: openTCS web API specification +servers: + - url: http://localhost:55200/v1 + description: openTCS kernel running on localhost +tags: + - name: Transport orders + description: Working with transport orders + - name: Order Sequences + description: Working with order sequences + - name: Vehicles + description: Working with vehicles + - name: Peripheral jobs + description: Working with peripheral jobs + - name: Peripherals + description: Working with peripherals + - name: Plant models + description: Working with plant models + - name: Status + description: Retrieving status updates +security: + - ApiKeyAuth: [ ] +paths: + /transportOrders: + get: + tags: + - Transport orders + summary: Retrieves a set of transport orders. + description: "" + parameters: + - name: intendedVehicle + in: query + description: >- + The name of the vehicle that is intended to process the transport orders to be retrieved. + required: false + schema: + type: string + default: null + responses: + "200": + description: Successful response + content: + application/json: + schema: + title: ArrayOfTransportOrders + type: array + items: + $ref: "#/components/schemas/TransportOrderState" + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find the intended vehicle 'Vehicle-0001'. + "/transportOrders/{NAME}": + get: + tags: + - Transport orders + summary: Retrieves a single named transport order. + description: "" + parameters: + - name: NAME + in: path + description: The name of the transport order to be retrieved. + required: true + schema: + type: string + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/TransportOrderState" + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find transport order 'TOrder-01'. + post: + tags: + - Transport orders + summary: Creates a new transport order with the given name. + description: "" + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/TransportOrderState" + "400": + description: The submitted data is invalid. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not parse JSON input. + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find location 'Storage 01'. + "409": + description: An object with the same name already exists in the model. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Transport order 'TOrder-01' already exists. + parameters: + - name: NAME + in: path + description: The name of the transport order to be created. + required: true + schema: + type: string + example: TOrder-002 + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/TransportOrder" + description: The details of the transport order to be created. + "/transportOrders/{NAME}/immediateAssignment": + post: + tags: + - Transport orders + summary: Immediately assigns the transport order to its intended vehicle. + parameters: + - name: NAME + in: path + description: The name of the transport order to be assigned. + required: true + schema: + type: string + responses: + "200": + description: Successful operation + "400": + description: Referencing transport order with invalid state. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not assign transport order 'TOrder-01' to vehicle 'Vehicle-0001'. + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find transport order 'TOrder-01'. + "/transportOrders/{NAME}/withdrawal": + post: + tags: + - Transport orders + summary: Withdraws the transport order with the given name. + description: "" + parameters: + - name: NAME + in: path + description: The name of the transport order to be withdrawn. + required: true + schema: + type: string + - name: immediate + in: query + description: Whether the transport order should be aborted as quickly as possible. + required: false + schema: + type: boolean + default: false + - name: disableVehicle + in: query + description: Deprecated, explicitly set the vehicle's integration level, instead. + required: false + deprecated: true + schema: + type: boolean + default: false + responses: + "200": + description: Successful operation + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find transport order 'TOrder-01'. + "/transportOrders/{NAME}/intendedVehicle": + put: + tags: + - Transport orders + summary: Updates the transport order's intended vehicle. + description: "" + parameters: + - name: NAME + in: path + description: The name of the transport order to be updated. + required: true + schema: + type: string + - name: vehicle + in: query + description: The name of the vehicle to assign the transport order to. + required: false + schema: + type: string + responses: + "200": + description: Successful operation + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find transport order 'TOrder-01'. + /transportOrders/dispatcher/trigger: + post: + tags: + - Transport orders + summary: Explicitly triggers dispatching of vehicles / transport orders. + description: >- + Triggers the kernel's dispatcher to assign vehicles to transport orders. + This usually happens automatically, but depending on the kernel configuration, explicitly triggering it may be necessary. + responses: + "200": + description: Successful response + /orderSequences: + get: + tags: + - Order Sequences + summary: Retrieves a set of order sequences. + description: "" + parameters: + - name: intendedVehicle + in: query + description: >- + The name of the vehicle that is intended to process the order sequences to be retrieved. + required: false + schema: + type: string + default: null + responses: + "200": + description: Successful response + content: + application/json: + schema: + title: ArrayOfOrderSequences + type: array + items: + $ref: "#/components/schemas/OrderSequenceState" + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find the intended vehicle 'Vehicle-0001'. + "/orderSequences/{NAME}": + get: + tags: + - Order Sequences + summary: Retrieves a single named order sequence. + description: "" + parameters: + - name: NAME + in: path + description: The name of the order sequence to be retrieved. + required: true + schema: + type: string + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/OrderSequenceState" + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find order sequence 'Sequence-002'. + post: + tags: + - Order Sequences + summary: Creates a new order sequence with the given name. + description: "" + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/OrderSequenceState" + "400": + description: The submitted data is invalid. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not parse JSON input. + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find Vehicle 'Vehicle-002'. + "409": + description: An object with the same name already exists in the model. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Order sequence 'Sequence-002' already exists. + parameters: + - name: NAME + in: path + description: The name of the order sequence to be created. + required: true + schema: + type: string + example: OrderSequence-01 + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/OrderSequence" + description: The details of the order sequence to be created. + "/orderSequences/{NAME}/complete": + put: + tags: + - Order Sequences + summary: Sets the complete flag for the named order sequence. + description: "" + parameters: + - name: NAME + in: path + description: The name of the order sequence. + required: true + schema: + type: string + responses: + "200": + description: Successful operation + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find order sequence 'Sequence-002'. + /vehicles: + get: + tags: + - Vehicles + summary: Retrieves a set of vehicles. + description: "" + parameters: + - name: procState + in: query + description: The processing state of the vehicles to be retrieved. + example: IDLE + required: false + schema: + type: string + default: null + enum: + - IDLE + - AWAITING_ORDER + - PROCESSING_ORDER + responses: + "200": + description: Successful response + content: + application/json: + schema: + title: ArrayOfVehicles + type: array + items: + $ref: "#/components/schemas/VehicleState" + "400": + description: The submitted data is invalid. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not parse input. + "/vehicles/{NAME}": + get: + tags: + - Vehicles + summary: Retrieves the vehicle with the given name. + description: "" + parameters: + - name: NAME + in: path + description: The name of the vehicle to be retrieved. + required: true + schema: + type: string + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/VehicleState" + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find vehicle 'Vehicle-0001'. + "/vehicles/{NAME}/withdrawal": + post: + tags: + - Vehicles + summary: Withdraws a transport order processed by the vehicle with the given name. + description: "" + parameters: + - name: NAME + in: path + description: Name of the vehicle processing the transport order to be withdrawn + required: true + schema: + type: string + - name: immediate + in: query + description: Whether the transport order should be aborted as quickly as possible. + required: false + schema: + type: boolean + default: false + - name: disableVehicle + in: query + description: Deprecated, explicitly set the vehicle's integration level, instead. + required: false + deprecated: true + schema: + type: boolean + default: false + responses: + "200": + description: Successful operation + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find vehicle 'Vehicle-0001'. + "/vehicles/{NAME}/rerouteRequest": + post: + tags: + - Vehicles + summary: Reroutes a vehicle with the given name. + description: "" + parameters: + - name: NAME + in: path + description: Name of the vehicle to be rerouted + required: true + schema: + type: string + - name: forced + in: query + description: Whether the vehicle should be rerouted even if it's not where it is expected to be. + required: false + schema: + type: boolean + default: false + responses: + "200": + description: Successful operation + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find vehicle 'Vehicle-0001'. + "/vehicles/{NAME}/integrationLevel": + put: + tags: + - Vehicles + summary: Sets a new integration level for the named vehicle. + description: "" + parameters: + - name: NAME + in: path + description: The name of the vehicle. + required: true + schema: + type: string + - name: newValue + in: query + description: The vehicle's new integration level. + required: true + example: TO_BE_RESPECTED + schema: + type: string + enum: + - TO_BE_UTILIZED + - TO_BE_RESPECTED + - TO_BE_NOTICED + - TO_BE_IGNORED + responses: + "200": + description: Successful operation + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find vehicle 'Vehicle-0001'. + "/vehicles/{NAME}/paused": + put: + tags: + - Vehicles + summary: Sets the paused state for the named vehicle. + description: "" + parameters: + - name: NAME + in: path + description: The name of the vehicle. + required: true + schema: + type: string + - name: newValue + in: query + description: The vehicle's new paused state. + required: true + example: true + schema: + type: boolean + responses: + "200": + description: Successful operation + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find vehicle 'Vehicle-0001'. + "/vehicles/{NAME}/allowedOrderTypes": + put: + tags: + - Vehicles + summary: Sets the allowed order types for the named vehicle. + parameters: + - name: NAME + in: path + description: The name of the vehicle. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AllowedOrderTypes" + description: The list of all order types to be allowed. + responses: + "200": + description: Successful operation + "400": + description: The submitted data is invalid. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not parse JSON input. + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find vehicle 'Vehicle-0001'. + "/vehicles/{NAME}/energyLevelThresholdSet": + put: + tags: + - Vehicles + summary: Sets energy level threshold values for the named vehicle (in percent). + parameters: + - name: NAME + in: path + description: The name of the vehicle. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EnergyLevelThresholdSet" + description: The new set of energy level thresholds. + responses: + "200": + description: Successful operation + "400": + description: The submitted data is invalid. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not parse JSON input. + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find vehicle 'Vehicle-0001'. + "/vehicles/{NAME}/commAdapter/attachmentInformation": + get: + tags: + - Vehicles + summary: Retrieves the driver attachment information of this vehicle. + description: "" + parameters: + - name: NAME + in: path + description: The name of the vehicle. + required: true + schema: + type: string + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/AttachmentInformation" + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find vehicle 'Vehicle-0001'. + "/vehicles/{NAME}/commAdapter/attachment": + put: + tags: + - Vehicles + summary: Attaches the given vehicle driver to this vehicle. + description: "" + parameters: + - name: NAME + in: path + description: The name of the vehicle. + required: true + schema: + type: string + - name: newValue + in: query + description: The description class name of the vehicle driver that is to be attached. + required: true + example: org.opentcs.virtualvehicle.LoopbackCommunicationAdapterDescription + schema: + type: string + responses: + "200": + description: Successful operation + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find vehicle 'Vehicle-0001'. + "400": + description: The submitted value is invalid. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: "Unknown vehicle driver class name: org.opentcs.someVehicle.driver11" + "/vehicles/{NAME}/commAdapter/enabled": + put: + tags: + - Vehicles + summary: Sets the enabled state for the named vehicle's driver. + description: "" + parameters: + - name: NAME + in: path + description: The name of the vehicle. + required: true + schema: + type: string + - name: newValue + in: query + description: The vehicle driver's new enabled state. + required: true + example: true + schema: + type: boolean + responses: + "200": + description: Successful operation + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find vehicle 'Vehicle-0001'. + "/vehicles/{NAME}/routeComputationQuery": + post: + tags: + - Vehicles + summary: Computes routes for the named vehicle to the given destination points. + parameters: + - name: NAME + in: path + description: The name of the vehicle. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/RoutesRequest" + description: The destination points, optional source point and optional list of resources to avoid for the routes to be computed. + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/RoutesResponse" + "400": + description: The submitted request body is invalid. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not parse JSON input. + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: "Unknown source point: Point-X" + "/vehicles/{NAME}/envelopeKey": + put: + tags: + - Vehicles + summary: Sets the envelope key for this vehicle. + description: "" + parameters: + - name: NAME + in: path + description: The name of the vehicle. + required: true + schema: + type: string + - name: newValue + in: query + description: The vehicle's new envelope key. + required: false + example: envelopeType-01 + schema: + type: string + responses: + "200": + description: Successful operation + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find vehicle 'Vehicle-0001'. + /vehicles/dispatcher/trigger: + post: + tags: + - Vehicles + summary: Explicitly triggers dispatching of vehicles / transport orders. + description: >- + Triggers the kernel's dispatcher to assign vehicles to transport orders. + This usually happens automatically, but depending on the kernel configuration, explicitly triggering it may be necessary. + responses: + "200": + description: Successful response + /peripheralJobs: + get: + tags: + - Peripheral jobs + summary: Retrieves a set of peripheral jobs. + description: "" + parameters: + - name: relatedVehicle + in: query + description: >- + The name of the vehicle for which the peripheral jobs to be retrieved were created. + required: false + schema: + type: string + default: null + - name: relatedTransportOrder + in: query + description: >- + The name of the transport order for which the peripheral jobs to be retrieved were created. + required: false + schema: + type: string + default: null + responses: + "200": + description: Successful response + content: + application/json: + schema: + title: ArrayOfPeripheralJobs + type: array + items: + $ref: "#/components/schemas/PeripheralJobState" + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find the related vehicle 'Vehicle-0001'. + "/peripheralJobs/{NAME}": + get: + tags: + - Peripheral jobs + summary: Retrieves a single named peripheral job. + description: "" + parameters: + - name: NAME + in: path + description: The name of the peripheral job to be retrieved. + required: true + schema: + type: string + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/PeripheralJobState" + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find peripheral job 'PJob-01'. + post: + tags: + - Peripheral jobs + summary: Creates a new peripheral job with the given name. + description: "" + parameters: + - name: NAME + in: path + description: The name of the peripheral job to be created. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/PeripheralJob" + description: >- + The details of the peripheral job to be created. + Currently, values provided for `executionTrigger` and `completionRequired` are ignored. + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/PeripheralJobState" + "400": + description: The submitted data is invalid. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not parse JSON input. + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find related vehicle 'Vehicle-0001'. + "409": + description: An object with the same name already exists in the model. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Peripheral job 'PJob-01' already exists. + "/peripheralJobs/{NAME}/withdrawal": + post: + tags: + - Peripheral jobs + summary: Withdraws the peripheral job with the given name. + description: "" + parameters: + - name: NAME + in: path + description: The name of the peripheral job to be withdrawn. + required: true + schema: + type: string + responses: + "200": + description: Successful operation + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find peripheral job 'PJob-01'. + /peripheralJobs/dispatcher/trigger: + post: + tags: + - Peripheral jobs + summary: Explicitly triggers dispatching of peripheral jobs. + description: >- + Triggers the kernel's dispatcher to assign peripheral jobs to peripheral devices. + This usually happens automatically, but depending on the kernel configuration, explicitly triggering it may be necessary. + responses: + "200": + description: Successful response + "/peripherals/{NAME}/commAdapter/attachmentInformation": + get: + tags: + - Peripherals + summary: Retrieves the driver attachment information of this peripheral. + description: "" + parameters: + - name: NAME + in: path + description: The name of the peripheral device/location. + required: true + schema: + type: string + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/PeripheralAttachmentInformation" + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find location 'Fire door 002'. + "/peripherals/{NAME}/commAdapter/attachment": + put: + tags: + - Peripherals + summary: Attaches the given peripheral driver to this peripheral. + description: "" + parameters: + - name: NAME + in: path + description: The name of the peripheral device/location. + required: true + schema: + type: string + - name: newValue + in: query + description: The description class name of the peripheral driver that is to be attached. + required: true + example: org.opentcs.somePeripheral.driver001 + schema: + type: string + responses: + "200": + description: Successful operation + "400": + description: The submitted value is invalid. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: "Unknown peripheral driver class name: org.opentcs.somePeripheral.driver0011" + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find location 'Fire door 003'. + "/peripherals/{NAME}/commAdapter/enabled": + put: + tags: + - Peripherals + summary: Sets the enabled state for the named peripheral's driver. + description: "" + parameters: + - name: NAME + in: path + description: The name of the peripheral device/location. + required: true + schema: + type: string + - name: newValue + in: query + description: The peripheral driver's new enabled state. + required: true + example: true + schema: + type: boolean + responses: + "200": + description: Successful operation + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find location 'Fire door 003'. + /plantModel: + get: + tags: + - Plant models + summary: Retrieves the currently loaded plant model. + responses: + "200": + description: Successful response + content: + application/json: + schema: + $ref: "#/components/schemas/PlantModel" + put: + tags: + - Plant models + summary: Uploads a new plant model with the given information. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/PlantModel" + description: The details of the plant model to be uploaded. + responses: + "200": + description: Successful operation + "400": + description: The submitted plant model is invalid. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not parse JSON input. + /plantModel/topologyUpdateRequest: + post: + tags: + - Plant models + summary: Triggers an update of the routing topology. + description: "" + requestBody: + required: false + content: + application/json: + schema: + $ref: "#/components/schemas/TopologyUpdateRequest" + responses: + "200": + description: Successful response. + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find path 'Path-BA'. + /paths/{NAME}/locked: + put: + tags: + - Plant models + summary: Sets the locked state for the named path. + description: "" + parameters: + - name: NAME + in: path + description: The name of the path. + required: true + schema: + type: string + - name: newValue + in: query + description: The path's new locked state. + required: true + example: true + schema: + type: boolean + responses: + "200": + description: Successful operation + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find path 'Point-0001 --- Point-0002'. + /locations/{NAME}/locked: + put: + tags: + - Plant models + summary: Sets the locked state for the named location. + description: "" + parameters: + - name: NAME + in: path + description: The name of the location. + required: true + schema: + type: string + - name: newValue + in: query + description: The location's new locked state. + required: true + example: true + schema: + type: boolean + responses: + "200": + description: Successful operation + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find location 'Storage 01'. + "/peripherals/{NAME}/withdrawal": + post: + tags: + - Peripherals + summary: Withdraws the peripheral jobs assigned to the given peripheral. + description: "" + parameters: + - name: NAME + in: path + description: The name of the peripheral device/location. + required: true + schema: + type: string + responses: + "200": + description: Successful operation + "404": + description: Referencing object that could not be found. + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Could not find location 'Fire door 003'. + /peripherals/dispatcher/trigger: + post: + tags: + - Peripherals + summary: Explicitly triggers dispatching of peripheral jobs/devices. + description: >- + Triggers the kernel's dispatcher to assign peripheral jobs to peripheral devices. + This usually happens automatically, but depending on the kernel configuration, explicitly triggering it may be necessary. + responses: + "200": + description: Successful response + /events: + get: + tags: + - Status + summary: Retrieves a list of events. + description: >- + This operation uses *long polling* to avoid excessive load on the server: + Set the *timeout* parameter to a value that indicates how long the operation may wait if there currently aren't any events to be returned. + parameters: + - name: minSequenceNo + in: query + description: >- + The minimum sequence number of events to be retrieved. + Can/Should be used to filter out events that have already been retrieved. + (Set this to the maximum sequence number already seen, incremented by 1.) + required: false + schema: + type: integer + format: int64 + default: 0 + - name: maxSequenceNo + in: query + description: >- + The maximum sequence number of events to be retrieved. + Can/Should be used to limit the number of events retrieved. + (Set this to e.g. *minSequenceNo* + 100.) + required: false + schema: + type: integer + format: int64 + # NOTE: Encoding this value as a string is a workaround to prevent the Swagger UI/JavaScript from rounding it. + default: '9223372036854775807' + - name: timeout + in: query + description: >- + The time (in milliseconds) to wait for events to arrive if there currently are not any events to be returned. + May not be greater than 10000. + required: false + schema: + type: integer + format: int64 + default: 1000 + responses: + "200": + description: Successful response + content: + application/json: + schema: + $ref: "#/components/schemas/StatusMessageList" + "400": + description: Invalid parameter value(s). + content: + application/json: + schema: + type: array + items: + type: string + description: Details on the actual error. + example: Parameter 'timeout' is not in the correct range. + /dispatcher/trigger: + post: + deprecated: true + tags: + - Transport orders + - Vehicles + summary: Explicitly triggers dispatching of vehicles / transport orders. + description: >- + Triggers the kernel's dispatcher to assign vehicles to transport orders. + This usually happens automatically, but depending on the kernel configuration, explicitly triggering it may be necessary. + responses: + "200": + description: Successful response +components: + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: X-Api-Access-Key + schemas: + AttachmentInformation: + title: Attachment Information + type: object + additionalProperties: false + properties: + vehicleName: + type: string + description: The name of the vehicle. + example: Vehicle-0001 + availableCommAdapters: + type: array + items: + type: string + description: The list of drivers (as names of description classes) available for this vehicle. + example: [ "org.opentcs.someVehicle.driver001", "org.opentcs.someVehicle.driver002" ] + attachedCommAdapter: + type: string + description: The description class name of the vehicle driver currently attached to this vehicle. + example: org.opentcs.someVehicle.driver001 + required: + - vehicleName + - availableCommAdapters + - attachedCommAdapter + TransportOrderState: + title: Transport Order State + type: object + additionalProperties: false + properties: + dispensable: + type: boolean + description: Whether this order is dispensable (may be withdrawn automatically). + example: false + name: + type: string + description: The name of the transport order. + example: TOrder-01 + type: + type: string + description: The type of the transport order. + example: Park + state: + type: string + enum: + - RAW + - ACTIVE + - DISPATCHABLE + - BEING_PROCESSED + - WITHDRAWN + - FINISHED + - FAILED + - UNROUTABLE + description: The transport order's current state. + intendedVehicle: + type: string + description: The name of the vehicle that is intended to process the transport + order. + example: Vehicle-0001 + processingVehicle: + type: string + description: The name of the vehicle currently processing the transport order. + example: Vehicle-0002 + peripheralReservationToken: + type: string + description: An (optional) token for reserving peripheral devices while processing this transport order. + example: Token-001 + wrappingSequence: + type: string + description: >- + The order sequence this transport order belongs to. + May be `null` in case this order isn't part of any sequence. + example: OrderSequence-01 + destinations: + type: array + items: + $ref: "#/components/schemas/DestinationState" + description: The sequence of destinations of the transport order. + required: + - dispensable + - name + - type + - state + - intendedVehicle + - processingVehicle + - peripheralReservationToken + - wrappingSequence + - destinations + DestinationState: + type: object + additionalProperties: false + properties: + locationName: + type: string + description: The name of the destination location + example: Storage 01 + operation: + type: string + description: The destination operation + example: Store + state: + type: string + enum: + - PRISTINE + - ACTIVE + - TRAVELLING + - OPERATING + - FINISHED + - FAILED + description: The drive order's state + properties: + type: array + minItems: 0 + maxItems: 2147483647 + items: + $ref: "#/components/schemas/Property" + description: The drive order's properties + required: + - locationName + - operation + - state + TransportOrder: + title: Transport Order + type: object + additionalProperties: false + properties: + incompleteName: + type: boolean + description: Whether the name of the transport order is considered to be incomplete. If + set, the kernel will complete the name according to its configuration, e.g. by appending + a suffix to it. It is recommended to set this, as names generated by the kernel can be + guaranteed to be unique, while clients typically cannot guarantee this. + default: false + dispensable: + type: boolean + description: Whether this order is dispensable (may be withdrawn automatically). + default: false + deadline: + type: string + format: date-time + description: The (optional) deadline of the transport order + example: 2018-05-17T06:42:40.396Z + intendedVehicle: + type: string + description: The (optional) intended vehicle of the transport order + example: Vehicle-01 + peripheralReservationToken: + type: string + description: An (optional) token for reserving peripheral devices while processing this transport order. + example: Token-001 + wrappingSequence: + type: string + description: >- + The order sequence this transport order belongs to. + May be `null` in case this order isn't part of any sequence. + example: OrderSequence-01 + type: + type: string + description: The (optional) type of the transport order + example: Park + destinations: + type: array + minItems: 1 + maxItems: 2147483647 + items: + $ref: "#/components/schemas/DestinationOrder" + description: The destinations + properties: + type: array + minItems: 0 + maxItems: 2147483647 + items: + $ref: "#/components/schemas/Property" + description: The transport order's properties + dependencies: + type: array + minItems: 0 + maxItems: 2147483647 + items: + type: string + example: TOrder-001 + description: The transport order's dependencies + required: + - destinations + DestinationOrder: + type: object + additionalProperties: false + properties: + locationName: + type: string + description: The name of the destination location + example: Storage 01 + operation: + type: string + description: The destination operation + example: Load cargo + properties: + type: array + minItems: 0 + maxItems: 2147483647 + items: + $ref: "#/components/schemas/Property" + description: The drive order's properties + required: + - locationName + - operation + AllowedOrderTypes: + title: Allowed Order Types + type: object + properties: + orderTypes: + type: array + items: + type: string + description: The names of the allowed order types. + example: [ "Park", "Load cargo", "Unload cargo" ] + required: + - orderTypes + EnergyLevelThresholdSet: + title: Energy Level Threshold Set + type: object + properties: + energyLevelCritical: + description: The energy level value (in %) at/below which the vehicle _must_ be recharged. + type: integer + minimum: 0 + maximum: 100 + example: 15 + energyLevelGood: + description: The energy level value (in %) at/above which the vehicle _should not_ be recharged. + type: integer + minimum: 0 + maximum: 100 + example: 60 + energyLevelSufficientlyRecharged: + description: The energy level value (in %) at/above which the vehicle is considered sufficiently recharged, i.e. _may_ stop recharging. + type: integer + minimum: 0 + maximum: 100 + example: 50 + energyLevelFullyRecharged: + description: The energy level value (in %) at/above which the vehicle is considered fully recharged, i.e. _should_ stop recharging. + type: integer + minimum: 0 + maximum: 100 + example: 90 + required: + - energyLevelCritical + - energyLevelGood + - energyLevelSufficientlyRecharged + - energyLevelFullyRecharged + OrderSequenceState: + title: Order Sequence State + type: object + additionalProperties: false + properties: + name: + type: string + description: The name of the order sequence. + example: Sequence-001 + type: + type: string + description: The type of the order sequence. + example: Park + orders: + type: array + example: [ "some-order", "another-order", "order-3" ] + items: + type: string + description: The sequence of orders of the order sequence. + finishedIndex: + type: integer + description: >- + The index of the order that was last finished in the sequence. + -1 if none was finished yet. + example: 3 + complete: + type: boolean + description: Indicates whether this order sequence is complete and will not be extended by more orders. + example: false + finished: + type: boolean + description: Indicates whether this order sequence has been processed completely. + example: false + failureFatal: + type: boolean + description: Indicates whether the failure of one order in this sequence is fatal to all subsequent orders. + example: false + intendedVehicle: + type: string + description: >- + The name of the vehicle that is intended to process the order sequence. + If this sequence is free to be processed by any vehicle, this is `null`. + example: Vehicle-0001 + processingVehicle: + type: string + description: >- + The vehicle processing this order sequence, or `null`, if no vehicle has been assigned to it, yet. + example: Vehicle-0002 + properties: + type: array + items: + $ref: "#/components/schemas/Property" + description: The order sequences properties + required: + - name + - type + - orders + - finishedIndex + - complete + - finished + - failureFatal + - intendedVehicle + - processingVehicle + - properties + OrderSequence: + title: Order Sequence + type: object + additionalProperties: false + properties: + incompleteName: + type: boolean + description: >- + Indicates whether the name is incomplete and requires to be completed when creating the actual order sequence. + (How exactly this is done is decided by the kernel.) + example: false + type: + type: string + description: The type of the order sequence. + example: Park + intendedVehicle: + type: string + description: >- + The name of the vehicle that is intended to process the order sequence. + If this sequence is free to be processed by any vehicle, this is `null`. + example: Vehicle-01 + failureFatal: + type: boolean + description: Indicates whether the failure of one order in this sequence is fatal to all subsequent orders. + example: false + properties: + type: array + items: + $ref: "#/components/schemas/Property" + description: The order sequence's properties + required: + - type + - properties + VehicleState: + title: Vehicle State + type: object + additionalProperties: false + properties: + name: + type: string + description: The name of the vehicle + example: Vehicle-0001 + properties: + type: object + additionalProperties: + type: string + description: A set of properties (key-value pairs) associated with this object. + length: + type: integer + description: The vehicle's length (in mm). + example: 1000 + energyLevelGood: + type: integer + description: The value (in %) at/above which the vehicle's energy level is considered + 'good'. + example: 90 + energyLevelCritical: + type: integer + description: The value (in %) at/below which the vehicle's energy level is considered + 'critical'. + example: 30 + energyLevelSufficientlyRecharged: + type: integer + description: The value (in %) at/below which the vehicle's energy level is considered + 'sufficiently recharged'. + example: 30 + energyLevelFullyRecharged: + type: integer + description: The value (in %) at/below which the vehicle's energy level is considered + 'fully recharged'. + example: 90 + energyLevel: + type: integer + description: The vehicle's remaining energy (in %). + example: 60 + integrationLevel: + type: string + enum: + - TO_BE_IGNORED + - TO_BE_NOTICED + - TO_BE_RESPECTED + - TO_BE_UTILIZED + description: The vehicle's integration level. + paused: + type: boolean + description: Whether the vehicle is paused. + example: false + procState: + type: string + enum: + - UNAVAILABLE + - IDLE + - AWAITING_ORDER + - PROCESSING_ORDER + description: The vehicle's current processing state. + transportOrder: + type: string + description: The name of the transport order the vehicle is currently processing. + example: TOrder-01 + currentPosition: + type: string + description: The name of the point which the vehicle currently occupies. + example: Point-0001 + precisePosition: + $ref: "#/components/schemas/PrecisePosition" + orientationAngle: + oneOf: + - type: string + - type: number + format: double + description: >- + The vehicle's current orientation angle (-360..360). + May be a string ("NaN") if the vehicle hasn't provided an orientation angle. + example: 90.0 + state: + type: string + enum: + - UNKNOWN + - UNAVAILABLE + - ERROR + - IDLE + - EXECUTING + - CHARGING + description: The vehicle's current state. + allocatedResources: + type: array + items: + $ref: "#/components/schemas/ResourceSet" + description: The resources already allocated by the vehicle. + example: [ [ "Path-0039--0040", "Point-0040" ], [ "Path-0040--0041", "Point-0041" ] ] + claimedResources: + type: array + items: + $ref: "#/components/schemas/ResourceSet" + description: The resources claimed - i.e. not yet allocated - for the vehicle's route. + example: [ [ "Path-0041--0042", "Point-0042" ], [ "Path-0042--0043", "Point-0043", "Location-2345" ] ] + allowedOrderTypes: + type: array + items: + type: string + description: The allowed order types for this vehicle. + example: [ "OrderType001", "OrderType002" ] + envelopeKey: + type: string + description: The envelope key for this vehicle. + example: envelopeType-01 + required: + - name + - properties + - length + - energyLevelGood + - energyLevelCritical + - energyLevelSufficientlyRecharged + - energyLevelFullyRecharged + - energyLevel + - integrationLevel + - paused + - procState + - orientationAngle + - state + - allocatedResources + - claimedResources + - allowedOrderTypes + ResourceSet: + type: array + items: + type: string + description: Name of the resource + example: [ Point-0042, Path-0041--0042 ] + TopologyUpdateRequest: + title: TopologyUpdateRequest + type: object + additionalProperties: false + properties: + paths: + type: array + items: + type: string + description: >- + The names of the paths to update in the routing topology. + An empty list of paths causes the entire routing topology to be updated. + example: [ Path-0042--0043, Path-0041--0042 ] + required: + - paths + StatusMessageList: + title: Status Message List + type: object + additionalProperties: false + properties: + timeStamp: + type: string + format: date-time + description: The point of time at which this data structure was created + statusMessages: + type: array + items: + oneOf: + - $ref: "#/components/schemas/OrderStatusMessage" + - $ref: "#/components/schemas/VehicleStatusMessage" + - $ref: "#/components/schemas/PeripheralJobStatusMessage" + description: The status messages + required: + - timeStamp + - statusMessages + StatusMessage: + title: AbstractStatusMessage + type: object + properties: + type: + type: string + enum: + - TransportOrder + - Vehicle + - PeripheralJob + sequenceNumber: + type: integer + description: The (unique) sequence number of this status message + example: 123 + creationTimeStamp: + type: string + format: date-time + description: When this status message was created + example: 2018-05-14T07:42:00.343Z + discriminator: + propertyName: type + required: + - type + - sequenceNumber + - creationTimeStamp + OrderStatusMessage: + title: OrderStatusMessage + type: object + additionalProperties: false + allOf: + - $ref: "#/components/schemas/StatusMessage" + - properties: + type: + type: string + enum: + - TransportOrder + default: TransportOrder + sequenceNumber: + example: 124 + orderName: + type: string + description: The (optional) transport order name + example: TOrder-0001 + processingVehicleName: + type: string + description: The processing vehicle's name + example: Vehicle-0001 + orderState: + type: string + enum: + - RAW + - ACTIVE + - DISPATCHABLE + - BEING_PROCESSED + - WITHDRAWN + - FINISHED + - FAILED + - UNROUTABLE + description: The transport order's current state + destinations: + type: array + minItems: 1 + maxItems: 2147483647 + items: + $ref: "#/components/schemas/DestinationState" + description: The transport order's destinations + properties: + type: array + items: + $ref: "#/components/schemas/Property" + description: The transport order's properties + required: + - type + VehicleStatusMessage: + type: object + additionalProperties: false + allOf: + - $ref: "#/components/schemas/StatusMessage" + - properties: + type: + type: string + enum: + - Vehicle + default: Vehicle + sequenceNumber: + example: 125 + vehicleName: + type: string + description: The vehicle's name + example: Vehicle-0001 + transportOrderName: + type: string + description: The name of the transport order the vehicle currently processes + example: TOrder-0001 + position: + type: string + description: The name of the point the vehicle currently occupies + example: Point-0001 + precisePosition: + $ref: "#/components/schemas/PrecisePosition" + vehicleOrientationAngle: + oneOf: + - type: string + - type: number + format: double + description: >- + The vehicle's current orientation angle (-360..360). + May be a string ("NaN") if the vehicle hasn't provided an orientation angle. + example: 90.0 + paused: + type: boolean + description: Whether the vehicle is paused. + example: false + state: + type: string + enum: + - UNKNOWN + - UNAVAILABLE + - ERROR + - IDLE + - EXECUTING + - CHARGING + description: The vehicle's current state + procState: + type: string + enum: + - UNAVAILABLE + - IDLE + - AWAITING_ORDER + - PROCESSING_ORDER + description: The vehicle's current processing state + allocatedResources: + type: array + items: + $ref: "#/components/schemas/ResourceSet" + description: The resources already allocated by the vehicle. + example: [ [ "Path-0039--0040", "Point-0040" ], [ "Path-0040--0041", "Point-0041" ] ] + claimedResources: + type: array + items: + $ref: "#/components/schemas/ResourceSet" + description: The resources claimed - i.e. not yet allocated - for the vehicle's route. + example: [ [ "Path-0041--0042", "Point-0042" ], [ "Path-0042--0043", "Point-0043", "Location-2345" ] ] + title: VehicleStatusMessage + required: + - type + - vehicleName + - state + - procState + - allocatedResources + - claimedResources + PeripheralJobStatusMessage: + title: PeripheralJobStatusMessage + type: object + additionalProperties: false + allOf: + - $ref: "#/components/schemas/StatusMessage" + - $ref: "#/components/schemas/PeripheralJobState" + - properties: + type: + type: string + enum: + - PeripheralJob + default: PeripheralJob + sequenceNumber: + example: 126 + required: + - type + - sequenceNumber + Property: + type: object + additionalProperties: false + properties: + key: + type: string + description: The property's key + example: key1 + value: + type: string + description: The property's value + example: value1 + required: + - key + - value + PrecisePosition: + type: object + additionalProperties: false + properties: + x: + type: integer + description: The position's X coordinate + example: 60 + y: + type: integer + description: The position's Y coordinate + example: 40 + z: + type: integer + description: The position's Z coordinate + example: 0 + required: + - x + - y + - z + PeripheralJobState: + title: Peripheral Job State + type: object + additionalProperties: false + properties: + name: + type: string + description: The name of the peripheral job. + example: PJob-01 + reservationToken: + type: string + description: >- + A token that may be used to reserve a peripheral device. + A peripheral device that is reserved for a specific token can only process jobs which match that reservation token. + example: Vehicle-0001 + relatedVehicle: + type: string + description: >- + The name of the vehicle for which the peripheral job was created. + May be `null`, if the job wasn't created in the context of a transport order being processed by a vehicle. + example: Vehicle-0001 + relatedTransportOrder: + type: string + description: >- + The name of the transport order for which the peripheral job was created. + May be `null`, if the job wasn't created in the context of a transport order being processed by a vehicle. + example: TOrder-01 + peripheralOperation: + $ref: "#/components/schemas/PeripheralOperation" + state: + type: string + description: The peripheral job's current state. + enum: + - TO_BE_PROCESSED + - BEING_PROCESSED + - FINISHED + - FAILED + creationTime: + type: string + format: date-time + description: The point of time at which this peripheral job was created (expressed according to ISO 8601). + example: "2022-01-01T12:00:00Z" + finishedTime: + type: string + format: date-time + description: The point of time at which processing of this peripheral job was finished (expressed according to ISO 8601). + example: "2022-01-01T12:00:00Z" + properties: + type: array + items: + $ref: "#/components/schemas/Property" + description: The peripheral job's properties. + required: + - name + - reservationToken + - relatedVehicle + - relatedTransportOrder + - peripheralOperation + - state + - creationTime + - finishedTime + PeripheralJob: + title: Peripheral Job + type: object + additionalProperties: false + properties: + incompleteName: + type: boolean + description: >- + Whether the name of the peripheral job is considered to be incomplete. + If set, the kernel will complete the name according to its configuration, e.g. by appending a suffix to it. + It is recommended to set this, as names generated by the kernel can be guaranteed to be unique, while clients typically cannot guarantee this. + default: false + reservationToken: + type: string + description: >- + The token that may be used to reserve a peripheral device. + A peripheral device that is reserved for a specific token can only process jobs which match that reservation token. + The reservation token may not be empty. + relatedVehicle: + type: string + description: >- + The name of the vehicle for which the peripheral job was created. + May be `null`, if the job wasn't created in the context of a transport order being processed by a vehicle. + default: null + relatedTransportOrder: + type: string + description: >- + The name of the transport order for which the peripheral job was created. + May be `null`, if the job wasn't created in the context of a transport order being processed by a vehicle. + default: null + peripheralOperation: + $ref: "#/components/schemas/PeripheralOperation" + properties: + type: array + minItems: 0 + maxItems: 2147483647 + items: + $ref: "#/components/schemas/Property" + description: The peripheral jobs's properties. + required: + - reservationToken + - peripheralOperation + PeripheralOperation: + title: Peripheral Operation + description: An operation that is to be executed by a peripheral device. + type: object + additionalProperties: false + properties: + operation: + type: string + description: The operation to be performed by the peripheral device. + example: Open door + locationName: + type: string + description: The name of the location the peripheral device is associated with. + example: Loading Bay + executionTrigger: + type: string + description: The moment at which this operation is to be performed. + enum: + - AFTER_ALLOCATION + - AFTER_MOVEMENT + - IMMEDIATE + default: IMMEDIATE + completionRequired: + type: boolean + description: Whether the completion of this operation is required to allow a vehicle to continue driving. + default: false + required: + - operation + - locationName + PeripheralAttachmentInformation: + title: Attachment Information + type: object + additionalProperties: false + properties: + locationReference: + type: string + description: The name of the location. + example: Fire door 001 + attachedCommAdapter: + type: string + description: The description class name of the peripheral driver currently attached to this location. + example: org.opentcs.somePeripheral.driver001 + required: + - locationReference + - attachedCommAdapter + PlantModel: + title: Plant model + type: object + properties: + name: + type: string + description: The plant model's name. + example: Plant Model 01 + points: + type: array + description: The plant model's points. + items: + $ref: "#/components/schemas/PlantModelPoint" + example: + - name: "Point-A" + position: + x: 15000 + y: 20000 + z: 0 + vehicleOrientationAngle: 90 + type: "HALT_POSITION" + layout: + position: + x: 15000 + y: 20000 + labelOffset: + x: 10 + y: 10 + layerId: 0 + properties: + - name: isExampleProperty + value: true + - name: "Point-B" + position: + x: 30000 + y: 20000 + z: 0 + vehicleOrientationAngle: 90 + type: "HALT_POSITION" + layout: + position: + x: 30000 + y: 20000 + labelOffset: + x: 10 + y: 10 + layerId: 0 + properties: + - name: isExampleProperty + value: true + - name: "Point-C" + position: + x: 10000 + y: 30000 + z: 0 + vehicleOrientationAngle: "NaN" + type: "HALT_POSITION" + layout: + position: + x: 10000 + y: 30000 + labelOffset: + x: 10 + y: 10 + layerId: 0 + - name: "Point-D" + position: + x: 25000 + y: 30000 + z: 0 + vehicleOrientationAngle: "NaN" + type: "HALT_POSITION" + layout: + position: + x: 25000 + y: 30000 + labelOffset: + x: 10 + y: 10 + layerId: 0 + paths: + type: array + description: The plant model's paths. + items: + $ref: "#/components/schemas/PlantModelPath" + example: + - name: "Path-AB" + srcPointName: "Point-A" + destPointName: "Point-B" + maxVelocity: 2500 + maxReverseVelocity: 2500 + locked: false + layout: + connectionType: "DIRECT" + layerId: 0 + properties: + - name: pathPropertyKey + value: exampleValue + - name: "Path-BC" + srcPointName: "Point-B" + destPointName: "Point-C" + maxVelocity: 2500 + maxReverseVelocity: 2500 + layout: + connectionType: "DIRECT" + layerId: 0 + - name: "Path-CA" + srcPointName: "Point-C" + destPointName: "Point-A" + maxVelocity: 2500 + maxReverseVelocity: 2500 + layout: + connectionType: "DIRECT" + layerId: 0 + - name: "Path-CD" + srcPointName: "Point-C" + destPointName: "Point-D" + maxVelocity: 1500 + maxReverseVelocity: 1000 + layout: + connectionType: "DIRECT" + layerId: 0 + - name: "Path-DA" + srcPointName: "Point-D" + destPointName: "Point-A" + maxVelocity: 2500 + maxReverseVelocity: 2500 + layout: + connectionType: "DIRECT" + layerId: 0 + - name: "Path-DB" + srcPointName: "Point-D" + destPointName: "Point-B" + maxVelocity: 2500 + maxReverseVelocity: 2500 + layout: + connectionType: "DIRECT" + layerId: 0 + locationTypes: + type: array + description: The plant model's location types. + items: + $ref: "#/components/schemas/PlantModelLocationType" + example: + - name: "Transfer-station" + allowedOperations: + - Load cargo + - Unload cargo + allowedPeripheralOperations: + - Open door + - Close door + layout: + locationRepresentation: "LOAD_TRANSFER_GENERIC" + properties: + - name: "locationTypePropertyKey" + value: "locationTypePropertyValue" + - name: "Working-station" + allowedOperations: + - Cut + - Drill + layout: + locationRepresentation: "WORKING_GENERIC" + locations: + type: array + description: The plant model's locations. + items: + $ref: "#/components/schemas/PlantModelLocation" + example: + - name: "Storage 01" + typeName: "Transfer-station" + position: + x: 15000 + y: 10000 + z: 0 + links: + - pointName: "Point-A" + locked: false + layout: + position: + x: 15000 + y: 10000 + labelOffset: + x: 10 + y: 10 + locationRepresentation: "LOAD_TRANSFER_ALT_1" + layerId: 0 + - name: "Storage 02" + typeName: "Transfer-station" + position: + x: 30000 + y: 10000 + z: 0 + links: + - pointName: "Point-B" + locked: false + layout: + position: + x: 30000 + y: 10000 + labelOffset: + x: 10 + y: 10 + layerId: 0 + - name: "Workshop" + typeName: "Working-station" + position: + x: 35000 + y: 30000 + z: 0 + links: + - pointName: "Point-D" + locked: false + layout: + position: + x: 35000 + y: 30000 + labelOffset: + x: 10 + y: 10 + layerId: 0 + - name: "Loading Bay" + typeName: "Transfer-station" + position: + x: 0 + y: 30000 + z: 0 + links: + - pointName: "Point-C" + locked: false + layout: + position: + x: 0 + y: 30000 + labelOffset: + x: 10 + y: 10 + layerId: 0 + blocks: + type: array + description: The plant model's blocks. + items: + $ref: "#/components/schemas/PlantModelBlock" + example: + - name: "Block-01" + type: "SINGLE_VEHICLE_ONLY" + memberNames: + - Path-BC + - Path-DA + layout: + color: "#FF0000" + vehicles: + type: array + description: The plant model's vehicles. + items: + $ref: "#/components/schemas/PlantModelVehicle" + example: + - name: "Vehicle-01" + length: 1000 + energyLevelCritical: 15 + energyLevelGood: 50 + energyLevelFullyRecharged: 97 + energyLevelSufficientlyRecharged: 75 + maxVelocity: 1500 + maxReverseVelocity: 750 + layout: + routeColor: "#00FF00" + - name: "Vehicle-02" + length: 1000 + energyLevelCritical: 15 + energyLevelGood: 50 + energyLevelFullyRecharged: 97 + energyLevelSufficientlyRecharged: 75 + maxVelocity: 1500 + maxReverseVelocity: 750 + layout: + routeColor: "#550055" + visualLayout: + $ref: "#/components/schemas/PlantModelVisualLayout" + properties: + type: array + description: The plant model's properties. + items: + example: + name: "modelPropertyExample" + value: "value" + required: + - name + PlantModelPoint: + title: Point + type: object + properties: + name: + type: string + description: This point's name. + example: some point + position: + $ref: "#/components/schemas/PlantModelTriple" + vehicleOrientationAngle: + oneOf: + - type: string + - type: number + format: double + description: >- + The vehicle's (assumed) orientation angle (-360..360) when it is at this position. + May be a string ("NaN") if an orientation angle is not defined for this point. + example: 73.3 + type: + type: string + description: This point's type. + enum: + - HALT_POSITION + - PARK_POSITION + vehicleEnvelopes: + type: array + description: A map of envelope keys to envelopes that vehicles located at this point may occupy. + items: + $ref: "#/components/schemas/PlantModelEnvelope" + layout: + type: object + description: Describes the graphical representation of this point. + properties: + position: + $ref: "#/components/schemas/PlantModelCouple" + labelOffset: + $ref: "#/components/schemas/PlantModelCouple" + layerId: + type: integer + example: 3 + properties: + type: array + description: This point's properties. + items: + $ref: "#/components/schemas/Property" + required: + - name + PlantModelPath: + title: Path + type: object + properties: + name: + type: string + description: This path's name. + example: some path + srcPointName: + type: string + description: The point name this path originates in. + example: some point + destPointName: + type: string + description: The point name this path ends in. + example: another point + length: + type: integer + format: int64 + description: This path's length (in mm). + example: 1300 + maxVelocity: + type: integer + description: >- + The absolute maximum allowed forward velocity on this path (in mm/s). + A value of 0 (default) means forward movement is not allowed on this path. + example: 1000 + maxReverseVelocity: + type: integer + description: >- + The absolute maximum allowed reverse velocity on this path (in mm/s). + A value of 0 (default) means reverse movement is not allowed on this path. + example: 300 + peripheralOperations: + type: array + description: The peripheral operations to be performed when a vehicle travels along this path. + items: + type: object + properties: + operation: + type: string + example: some operation + locationName: + type: string + example: some location + executionTrigger: + type: string + enum: + - AFTER_ALLOCATION + - AFTER_MOVEMENT + completionRequired: + type: boolean + required: + - operation + - locationName + locked: + type: boolean + description: A flag for marking this path as locked (i.e. to prevent vehicles from using it). + vehicleEnvelopes: + type: array + description: A map of envelope keys to envelopes that vehicles traversing this path may occupy. + items: + $ref: "#/components/schemas/PlantModelEnvelope" + layout: + type: object + description: The information regarding the graphical representation of this path. + properties: + connectionType: + type: string + enum: + - DIRECT + - ELBOW + - SLANTED + - POLYPATH + - BEZIER + - BEZIER_3 + controlPoints: + type: array + items: + $ref: "#/components/schemas/PlantModelCouple" + layerId: + type: integer + example: 3 + properties: + items: + $ref: "#/components/schemas/Property" + required: + - name + - srcPointName + - destPointName + PlantModelLocationType: + title: Location Type + type: object + properties: + name: + type: string + description: This location type's name. + example: some location type + allowedOperations: + type: array + description: The allowed operations for this location type. + items: + type: string + example: [ "some operation", "another operation" ] + allowedPeripheralOperations: + type: array + description: The allowed peripheral operations for this location type. + items: + type: string + example: [ "some peripheral operation", "another peripheral operation" ] + layout: + type: object + description: The information regarding the graphical representation of this location type. + properties: + locationRepresentation: + type: string + enum: + - NONE + - DEFAULT + - LOAD_TRANSFER_GENERIC + - LOAD_TRANSFER_ALT_1 + - LOAD_TRANSFER_ALT_2 + - LOAD_TRANSFER_ALT_3 + - LOAD_TRANSFER_ALT_4 + - LOAD_TRANSFER_ALT_5 + - WORKING_GENERIC + - WORKING_ALT_1 + - WORKING_ALT_2 + - RECHARGE_GENERIC + - RECHARGE_ALT_1 + - RECHARGE_ALT_2 + properties: + type: object + description: This location type's properties. + items: + $ref: "#/components/schemas/Property" + required: + - name + PlantModelLocation: + title: Location + type: object + properties: + name: + type: string + description: This location's name. + example: some location + typeName: + type: string + description: The name of this location's type. + example: some location type + position: + $ref: "#/components/schemas/PlantModelTriple" + links: + type: array + description: >- + The links attaching points to this location. + This is a map of point names to allowed operations. + items: + type: object + properties: + pointName: + type: string + example: some point + allowedOperations: + type: array + items: + type: string + example: some operation + locked: + type: boolean + description: A flag for marking this location as locked (i.e. to prevent vehicles from using it). + layout: + type: object + description: The information regarding the graphical representation of this location. + properties: + position: + $ref: "#/components/schemas/PlantModelCouple" + labelOffset: + $ref: "#/components/schemas/PlantModelCouple" + locationRepresentation: + type: string + enum: + - NONE + - DEFAULT + - LOAD_TRANSFER_GENERIC + - LOAD_TRANSFER_ALT_1 + - LOAD_TRANSFER_ALT_2 + - LOAD_TRANSFER_ALT_3 + - LOAD_TRANSFER_ALT_4 + - LOAD_TRANSFER_ALT_5 + - WORKING_GENERIC + - WORKING_ALT_1 + - WORKING_ALT_2 + - RECHARGE_GENERIC + - RECHARGE_ALT_1 + - RECHARGE_ALT_2 + layerId: + type: integer + example: 3 + properties: + type: array + description: This location's properties. + items: + $ref: "#/components/schemas/Property" + required: + - name + - typeName + - position + PlantModelBlock: + title: Block + type: object + properties: + name: + type: string + description: This block's name. + example: some block + type: + type: string + description: This block's type. + enum: + - SINGLE_VEHICLE_ONLY + - SAME_DIRECTION_ONLY + memberNames: + type: array + items: + type: string + description: This block's member names. + example: [ "Path-AB", "Path-BC" ] + layout: + type: object + description: The information regarding the graphical representation of this block. + properties: + color: + type: string + pattern: ^#([A-Fa-f0-9]{6})$ + example: "#FF0000" + properties: + type: array + description: This block's properties. + items: + $ref: "#/components/schemas/Property" + required: + - name + PlantModelVehicle: + title: Vehicle + type: object + properties: + name: + type: string + description: This vehicle's name. + example: some vehicle + length: + type: integer + description: The vehicle's length (in mm). + example: 1000 + energyLevelCritical: + type: integer + description: The energy level value (in %) at/below which the vehicle _must_ be recharged. + example: 15 + energyLevelGood: + type: integer + description: The energy level value (in %) at/above which the vehicle _should not_ be recharged. + example: 60 + energyLevelFullyRecharged: + type: integer + description: The energy level value (in %) at/above which the vehicle is considered fully recharged, i.e. _should_ stop recharging. + example: 90 + energyLevelSufficientlyRecharged: + type: integer + description: The energy level value (in %) at/above which the vehicle is considered sufficiently recharged, i.e. _may_ stop recharging. + example: 50 + maxVelocity: + type: integer + description: The vehicle's maximum velocity (in mm/s). + example: 2000 + maxReverseVelocity: + type: integer + description: The vehicle's maximum reverse velocity (in mm/s). + example: 733 + layout: + type: object + description: The information regarding the graphical representation of this vehicle. + properties: + routeColor: + type: string + pattern: ^#([A-Fa-f0-9]{6})$ + example: "#00FF00" + properties: + type: array + description: This vehicle's properties. + items: + $ref: "#/components/schemas/Property" + required: + - name + PlantModelVisualLayout: + title: Visual Layout + type: object + properties: + name: + type: string + description: This visual layout's name. + example: some visual layout + scaleX: + type: number + description: This layout's scale on the X axis (in mm/pixel). + example: 50.0 + scaleY: + type: number + description: This layout's scale on the Y axis (in mm/pixel). + example: 50.0 + layers: + type: array + description: This layout's layers. + items: + $ref: "#/components/schemas/PlantModelLayer" + layerGroups: + type: array + description: The layout's layer groups. + items: + $ref: "#/components/schemas/PlantModelLayerGroup" + properties: + type: array + description: This visual layout's properties. + items: + example: + name: "visualLayoutProperty" + value: "value" + required: + - name + PlantModelTriple: + title: Triple + type: object + properties: + x: + type: integer + format: int64 + description: The Triple's x value. + example: 1500 + y: + type: integer + format: int64 + description: The Triple's y value. + example: 2000 + z: + type: integer + format: int64 + description: The Triple's z value. + example: 500 + required: + - x + - y + - z + PlantModelCouple: + title: Couple + type: object + properties: + x: + type: integer + format: int64 + description: The Couple's x value. + example: 1500 + y: + type: integer + format: int64 + description: The Couple's y value. + example: 2000 + required: + - x + - y + PlantModelLayer: + title: Layer + type: object + properties: + id: + type: integer + description: The unique ID of this layer. + example: 0 + ordinal: + type: integer + description: >- + The ordinal of this layer. + Layers with a higher ordinal are positioned above layers with a lower ordinal. + example: 0 + visible: + type: boolean + description: Whether this layer is visible or not. + name: + type: string + description: The name of this layer. + example: some layer + groupId: + type: integer + description: The ID of the layer group this layer is assigned to. + example: 0 + required: + - id + - ordinal + - visible + - name + - groupId + PlantModelLayerGroup: + title: Layer Group + type: object + properties: + id: + type: integer + description: The unique ID of this layer group. + example: 0 + name: + type: string + description: The name of this layer group. + example: some layer + visible: + description: Whether this layer group is visible or not. + type: boolean + required: + - id + - name + - visible + PlantModelEnvelope: + title: Envelope + type: object + properties: + envelopeKey: + type: string + description: This envelope's key. + example: envelopeType-01 + vertices: + type: array + description: The sequence of vertices this envelope consists of. + items: + $ref: "#/components/schemas/PlantModelCouple" + example: + - x: 1500 + y: 1750 + - x: 1600 + y: 1820 + - x: 1700 + y: 1890 + required: + - envelopeKey + - vertices + Route: + title: Route + type: object + properties: + destinationPoint: + type: string + description: The computed route's destination point. + example: Point-A + costs: + type: integer + format: int64 + description: The costs for the computed route, or `-1`, if no route could be computed. + example: 33475 + steps: + type: array + description: >- + An array containing the computed route's steps, or `null`, if no route could be + computed. + items: + $ref: "#/components/schemas/Step" + required: + - destinationPoint + - costs + - steps + Step: + title: Single step of a route + type: object + properties: + path: + type: string + description: The path to travel for this step. + example: Point-A --- Point-B + sourcePoint: + type: string + description: The source point for this step. + example: Point-A + destinationPoint: + type: string + description: The destination point for this step. + example: Point-B + vehicleOrientation: + type: string + default: UNDEFINED + enum: + - FORWARD + - BACKWARD + - UNDEFINED + required: + - destinationPoint + - vehicleOrientation + RoutesRequest: + title: Requested routes + type: object + properties: + sourcePoint: + type: string + description: >- + The (optional) starting point for route computation. + If `null` or not set, the vehicle's current position will be used. + example: Point-A + destinationPoints: + type: array + description: The destination point for each route to be computed. + example: + - Point-C + - Point-D + - Point-E + resourcesToAvoid: + type: array + description: The resources to be avoided for each route. + example: + - Path-CA + - Point-B + required: + - destinationPoints + RoutesResponse: + title: Computed routes for different destination points. + type: object + properties: + routes: + type: array + description: The list of computed routes. + items: + $ref: "#/components/schemas/Route" + required: + - Routes + example: + routes: + - destinationPoint: Point-C + costs: 77644 + steps: + - path: Point-A --- Point-B + sourcePoint: Point-A + destinationPoint: Point-B + vehicleOrientation: FORWARD + - path: Point-B --- Point-C + sourcePoint: Point-B + destinationPoint: Point-C + vehicleOrientation: FORWARD + - destinationPoint: Point-D + costs: -1 + steps: null + - destinationPoint: Point-E + costs: 67934 + steps: + - path: Point-A --- Point-D + sourcePoint: Point-A + destinationPoint: Point-D + vehicleOrientation: FORWARD + - path: Point-D --- Point-E + sourcePoint: Point-D + destinationPoint: Point-E + vehicleOrientation: BACKWARD diff --git a/opentcs-documentation/src/docs/users-guide/01_introduction.adoc b/opentcs-documentation/src/docs/users-guide/01_introduction.adoc new file mode 100644 index 0000000..14f62ab --- /dev/null +++ b/opentcs-documentation/src/docs/users-guide/01_introduction.adoc @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + +== Introduction + +=== Purpose of the software + +openTCS (short for _open Transportation Control System_) is a free control system software for coordinating fleets of https://en.wikipedia.org/wiki/Automated_guided_vehicle[automated guided vehicles (AGVs)] and mobile robots, e.g. in production plants. +It should generally be possible to control any automatic vehicle with communication capabilities with it, but AGVs are the main target. + +openTCS controls vehicles independent of their specific characteristics like navigation principle/track guidance system or load handling device. +It can manage vehicles of different types (and performing different tasks) at the same time. + +openTCS itself is not a complete product you use out-of-the-box to control AGVs with. +Primarily, it is a framework/an implementation of the basic data structures and algorithms (routing of vehicles, dispatching orders to them, managing the fleet's traffic) needed for running an AGV system with more than one vehicle. +It tries to be as generic as possible to allow interoperation with vehicles of practically any vendor. + +As a consequence, it is usually necessary to at least create and plug in a vehicle driver (called _communication adapter_ in openTCS-speak) that translates between the abstract interface of the openTCS kernel and the communication protocol your vehicle understands. +(Such vehicle drivers are similar to device drivers in operating systems, in a way.) +Depending on your needs, it might also be necessary to adapt algorithms or add project-specific strategies. + +=== System requirements + +openTCS does not come with any specific hardware requirements. +CPU power and RAM capacity highly depend on the use case, e.g. the size and complexity of the driving course and the number of vehicles managed. +Some kind of networking hardware -- in most cases simply a standard Ethernet controller -- is required for communicating with the vehicles (and possibly other systems, like a warehouse management system). + +To run openTCS, a Java Runtime Environment (JRE) version 21 is required. +(The directory `bin` of the installed JRE, for example `C:\Program Files\Eclipse Adoptium\jdk-21.0.3.9-hotspot\bin`, should be included in the enviroment variable PATH to be able to use the included start scripts.) + +IMPORTANT: Due to a limitation in a software library used by openTCS (namely: Docking Frames), some JREs are currently not compatible with openTCS. +This is true e.g. for the JRE provided by Oracle. +The recommended JRE to use is the one provided by the https://adoptium.net/[Adoptium project]. + +=== Licensing + +openTCS is being maintained by the openTCS team at the https://www.iml.fraunhofer.de/[Fraunhofer Institute for Material Flow and Logistics]. + +The openTCS source code is licensed under the terms of the MIT License. +Please note that openTCS is distributed without any warranty - without even the implied warranty of merchantability or fitness for a particular purpose. +Please refer to the license (`LICENSE.txt`) for details. + +=== Further documentation + +If you intend to extend and customize openTCS, please also see the Developer's Guide and the JavaDoc documentation that is part of the openTCS distribution. + +=== Support + +Please note that, while Fraunhofer IML is happy to be able to release openTCS to the public as free software and would like to see it used and improved continuously, the development team cannot provide unlimited free support for it. + +If you have technical/support questions, please post them on the project's discussion forum, where the community and the developers involved will respond as time permits. +You can find the discussion forum at https://github.com/openTCS/opentcs/discussions. +Please remember to include enough data in your problem report to help the developers help you, e.g.: + +* The applications' log files, contained in the subdirectory `log/` of the kernel, kernel control center, model editor and operations desk application +* The plant model you are working with, contained in the subdirectory `data/` of the kernel and/or model editor application diff --git a/opentcs-documentation/src/docs/users-guide/02_system-overview.adoc b/opentcs-documentation/src/docs/users-guide/02_system-overview.adoc new file mode 100644 index 0000000..6ce0c6c --- /dev/null +++ b/opentcs-documentation/src/docs/users-guide/02_system-overview.adoc @@ -0,0 +1,377 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + +== System overview + +=== System components and structure + +openTCS consists of the following components running as separate processes and working together in a client-server architecture: + +* Kernel (server process), running vehicle-independent strategies and drivers for controlled vehicles +* Clients +** Model editor for modelling the plant model +** Operations desk for visualizing the plant model during plant operation +** Kernel control center for controlling and monitoring the kernel, e.g. providing a detailed view of vehicles/their associated drivers +** Arbitrary clients for communicating with other systems, e.g. for process control or warehouse management + +.openTCS system overview +image::system_overview.png[] + +The purpose of the openTCS kernel is to provide an abstract driving model of a transportation system/plant, to manage transport orders and to compute routes for the vehicles. +Clients can communicate with this server process to, for instance, modify the plant model, to visualize the driving course and the processing of transport orders and to create new transport orders. + +Three major strategy modules within the kernel implement processing of transport orders: + +* A dispatcher that decides which transport order should be processed by which vehicle. + Additionally, it needs to decide what vehicles should do in certain situation, e.g. when there aren't any transport orders or when a vehicle is running low on energy. +* A router which finds optimal routes for vehicles to reach their destinations. +* A scheduler that manages resource allocations for traffic management, i.e. to avoid vehicles crashing into each other. + +The openTCS distribution comes with default implementations for each of these strategies. +These implementations can be easily replaced by a developer to adapt to environment-specific requirements. + +The driver framework that is part of the openTCS kernel manages communication channels and associates vehicle drivers with vehicles. +A vehicle driver is an adapter between kernel and vehicle and translates each vehicle-specific communication protocol to the kernel's internal communication schemes and vice versa. +Furthermore, a driver may offer low-level functionality to the user via the kernel control center client, e.g. manually sending telegrams to the associated vehicle. +By using suitable vehicle drivers, vehicles of different types can be managed simultaneously by a single openTCS instance. + +The model editor client that is part of the openTCS distribution allows editing of plant models, which can be loaded into the kernel. +This includes, for instance, the definition of load-change stations, driving tracks and vehicles. + +The operations desk client that is part of the openTCS distribution is used to display the transportation system's general state and any active transport processes, and to create new transport orders interactively. + +The kernel control center client that is part of the openTCS distribution allows controlling and monitoring the kernel. +Part of that is assigning vehicle drivers to vehicles and controlling them by enabling the communication and monitoring them by displaying vehicle state information, for instance. + +Other clients, e.g. to control higher-level plant processes, can be implemented and attached. +For Java clients, the openTCS kernel provides an interface based on Java RMI (Remote Method Invocation). +Additionally, openTCS provides a web API for creating and withdrawing transport orders and retrieving transport order status updates. + +=== Plant model elements + +In openTCS, a plant model consists of a set of the following elements. +The attributes of these elements that are relevant for the plant model, e.g. the coordinates of a point or the length of a path, can be edited using the model editor client. + +==== Point + +Points are logical mappings of discrete vehicle positions in the driving course. +In plant operation mode, vehicles are ordered (and thus move) from one point to another in the model. +A point carries the following attributes: + +* A _type_, which is one of these three: +** _Halt position_: + Indicates a position at which a vehicle may halt temporarily while processing an order, e.g. for executing an operation. + The vehicle is expected to report in when it arrives at such a position. + It may not remain here for longer than necessary, though. + Halt position is the default type for points when modelling with the model editor client. +** _Reporting position_ (deprecated, scheduled for removal in openTCS 6.0): + Indicates a position at which a vehicle is expected to report in _only_. + Vehicles will not be ordered to a reporting position, and halting or even parking at such a position is not allowed. + Therefore, a route that only consists of reporting points will be unroutable because the vehicle is not able to halt at any position. +** _Park position_: + Indicates a position at which a vehicle may halt for longer periods of time when it is not processing orders. + The vehicle is also expected to report in when it arrives at such a position. +* A _position_, i.e. the point's coordinates in the plant's coordinate system. +* A _vehicle orientation angle_, which expresses the vehicle's assumed/expected orientation while it occupies the point. +* A set of _vehicle envelopes_ describing the areas occupied by vehicles located at the point. +* A _maximum vehicle bounding box_ describing the maximum bounding box that a vehicle located at the point may have (see <> for more information). + +NOTE: In openTCS, an angle of 0 degrees is at the 3 o'clock position, and a positive value indicates a counter-clockwise rotation. + +===== Layout coordinates vs model coordinates + +A point has two sets of coordinates: layout coordinates and model coordinates. +The layout coordinates are merely intended for the graphical presentation in the model editor and operations desk clients, while the model coordinates are data that a vehicle driver could potentially use or send to the vehicle it communicates with (e.g. if the vehicle needs the exact coordinates of a destination point for navigation). +Both coordinate sets are not tied to each other per se, i.e. they may differ. +This is to allow coordinates that the system works with internally to be different from the presentation; for example, you may want to provide a distorted view on the driving course simply because some paths in your plant are very long and you mainly want to view all points/locations closely +together. +Dragging points and therefore changing their position in the graphical presentation only affects the corresponding layout coordinates. + +To synchronize the layout coordinates with the model coordinates or the other way around you have two options: + +* Select btn:[menu:Actions[Copy model values to layout]] or btn:[menu:Actions[Copy layout values to model]] to synchronize them globally. +* Select a single layout element, right click it and select btn:[menu:Context menu[Copy model values to layout]] or btn:[menu:Context menu[Copy layout values to model]] to synchronize them only for the selected element. + +===== Bounding box + +.A bounding box in openTCS +image::bounding-box.drawio.png[] + +A bounding box is characterised by a reference point that is -- by default -- located in the center of the bounding box's base (i.e. at height 0), named _base center_ for the remainder of this section. +The length and width of the bounding box are symmetrical in relation to the base center and the height is measured from the base of the bounding box. + +Optionally, a reference point offset describes the position of the reference point in relation to the base center. +The coordinates of the reference point refer to a coordinate system whose origin is located at the base center and whose axes run along the longitudinal and transverse axes of the bounding box -- i.e. the x-axis runs along the length and the y-axis along the width of the bounding box. + +For a vehicle, the bounding box is oriented so that its longitudinal axis runs parallel to the longitudinal axis of the vehicle. +For the reference point offset, positive x values indicate an offset in the forward direction of the vehicle, positive y values an offset towards the left-hand side. +As an example, a vehicle's physical reference point -- i.e. the point which its reported coordinates refer to -- and the reference point of its bounding box are probably always aligned. + +For a point, the bounding box is oriented according to the orientation angle of the point so that the longitudinal axis of the bounding box runs parallel to the longitudinal axis of a vehicle located at the point. +For the reference point offset, positive x values indicate an offset in the forward direction of the vehicle, positive y values an offset towards the left-hand side. + +The following figure shows examples of bounding boxes for a vehicle (on the left) and a point (on the right). +(Although a bounding box is three-dimensional in openTCS, the example bounding boxes shown here are only two-dimensional for an easy-to-understand visualisation.) + +.Bounding boxes for vehicles and points +image::bounding-box-for-vehicle-and-point.drawio.png[] + +In both cases, the blue dots represent the base centers of the respective bounding boxes and the green dots represent their reference points. +The dashed line represents the perimeter of the respective bounding box. +On the right, the orange dot represents the actual plant model point. +In the example above, the bounding boxes have the following properties: + +[cols="1,1,1,1,1,1", options="header"] +|=== +|Element +|Length [mm] +|Width [mm] +|Height [mm] +|Reference offset x [mm] +|Reference offset y [mm] + +|Vehicle +|1100 +|700 +|_omitted_ +|-300 +|0 + +|Point +|1700 +|1100 +|_omitted_ +|-500 +|-100 +|=== + +As an additional example, the following figure shows the bounding boxes in relation to each other and what it would look like if the vehicle was located at the point. +(Note that the reference points of both bounding boxes are aligned.) + +.Relation of vehicle and point bounding boxes +image::bounding-box-vehicle-on-point.drawio.png[] + +In this example, the point's bounding box encloses the vehicle's bounding box completely. +However, there may be situations where this is not the case and where the vehicle's bounding box would protrude beyond one or more sides of the point's bounding box. +To prevent a vehicle from being sent to a point in such situations, the router provides a dedicated cost function -- see <>. + +==== Path + +Paths are connections between points that are navigable for vehicles. +A path's main attributes, next to its source and destination point, are: + +* Its _length_, which may be relevant information for a vehicle in plant operation mode. + Depending on the router configuration, it may also be used for computing routing costs/finding an optimal route to a destination point. +* A _maximum velocity_ and _maximum reverse velocity_, which may be relevant information for a vehicle in plant operation mode. + Depending on the router configuration, it may also be used for computing routing costs/finding an optimal route to a destination point. +* A _locked_ flag, which, when set, tells the router that the path may not be used when computing routes for vehicles. +* A sequence of _peripheral operations_ describing operations that are to be performed by peripheral devices (in their given order) when a vehicle traverses the path. +* A set of _vehicle envelopes_ describing the areas occupied by vehicles traversing the path. + +===== Peripheral operation + +A peripheral operation's attributes are: + +* A reference to the _location_ representing the peripheral device by which the operation is to be performed -- see <>. +* The actual _operation_ to be performed by the peripheral device. +* An _execution trigger_ defining the moment at which the operation is to be performed. + The supported values are: + ** `BEFORE_MOVEMENT`: The execution of the operation should be triggered _before_ a vehicle traverses the path. + ** `AFTER_MOVEMENT`: The execution of the operation should be triggered _after_ a vehicle has traversed the path. +* A _completion required_ flag, which, when set, requires the operation to be completed to allow a vehicle to continue driving. + This flag works in combination with the execution trigger. + With the `BEFORE_MOVEMENT` execution trigger and the completion required flag set to `true`, a vehicle has to wait at the path's source point until the operation is completed. + With the `AFTER_MOVEMENT` execution trigger and the completion required flag set to `true`, a vehicle has to wait at the path's destination point until the operation is completed. + +==== Location + +Locations are markers for points at which vehicles may execute special operations (load or unload cargo, charge their battery etc.). +A location's attributes are: + +* Its _type_, basically defining which operations are allowed at the location -- see <>. +* A set of _links_ to points that the location can be reached from. + To be of any use for vehicles in the plant model, a location needs to be linked to at least one point. +* A _locked_ flag, which, when set, tells the dispatcher that transport orders requiring an operation at the location may not be assigned to vehicles. + +Additionally, locations can map peripheral devices for the purpose of communicating with them and allowing vehicles to interact with them (e.g. opening/closing fire doors along paths). +See <> for details on how to add and configure peripheral devices. + +==== Location type + +Location types are abstract elements that group locations. +A location type has only two relevant attributes: + +* A set of _allowed/supported vehicle operations_, defining which operations a vehicle may execute at locations of this type. +* A set of _allowed/supported peripheral operations_, defining which operations peripheral devices mapped to locations of this type may execute. + +==== Vehicle + +Vehicles map physical vehicles for the purpose of communicating with them and visualizing their positions and other characteristics. +A vehicle provides the following attributes: + +* A set of energy level thresholds, which is composed as follows: +** A _critical energy level_, which is the threshold below which the vehicle's energy level is considered critical. + This value may be used at plant operation time to decide when it is crucial to recharge a vehicle's energy storage. +** A _good energy level_, which is the threshold above which the vehicle's energy level is considered good. + This value may be used at plant operation time to decide when it is unnecessary to recharge a vehicle's energy storage. + When configuring this value, it must be greater than or equal to the _critical energy level_. +** A _sufficiently recharged energy level_, which is the threshold above which the vehicle is considered sufficiently recharged. + This value may be used at plant operation time to decide when a vehicle may stop charging. +** A _fully recharged energy level_, which is the threshold above which the vehicle is considered being fully recharged. + This value may be used at plant operation time to decide when a vehicle should stop charging. + When configuring this value, it must be greater than or equal to the _sufficiently recharged energy level_. +* A _maximum velocity_ and _maximum reverse velocity_. + Depending on the router configuration, it may be used for computing routing costs/finding an optimal route to a destination point. +* An _integration level_, indicating how far the vehicle is currently allowed to be integrated into the system. + A vehicle's integration level can only be adjusted with the operations desk client, not with the model editor client. + A vehicle can be + ** ..._ignored_: + The vehicle and its reported position will be ignored completely, thus the vehicle will not be displayed in the operations desk. + The vehicle is not available for transport orders. + ** ..._noticed_: + The vehicle will be displayed at its reported position in the operations desk, but no resources will be allocated in the system for that position. + The vehicle is not available for transport orders. + ** ..._respected_: + The resources for the vehicle's reported position will be allocated. + The vehicle is not available for transport orders. + ** ..._utilized_: + The vehicle is available for transport orders and will be utilized by the openTCS. +* A _paused_ flag, indicating whether the vehicle is currently paused or not. + A vehicle that is paused is supposed not to move/operate. + In case it is currently moving when its paused flag is set, it is expected to stop as soon as possible. + Some vehicle types may not support stopping before reaching their movement commands' destination. + In such cases, openTCS will still ensure no further movement commands are sent to vehicles as long as they are paused. +* A set of _allowed transport order types_, which are strings used for filtering transport orders (by their type) that are allowed to be assigned to the vehicle. + Also see <>. +* A _route color_, which is the color used for visualizing the route the vehicle is taking to its destination. +* An _envelope key_, indicating which envelopes (defined at points and paths) should be considered for the vehicle. +* A _bounding box_ describing the physical dimensions of the vehicle (see <> for more information). + +==== Block + +Blocks (or block areas) are areas for which special traffic rules may apply. +They can be useful to prevent deadlock situations, e.g. at path intersections or dead ends. +A block has two relevant attributes: + +* A set of _members_, i.e. resources (points, paths and/or locations) that the block is composed of. +* A _type_, which determines the rules for entering a block: +** _Single vehicle only_: + The resources aggregated in this block can only be used by a single vehicle at the same time. + This is the default type for blocks when modelling with the model editor client. +** _Same direction only_: + The resources aggregated in this block can be used by multiple vehicles at the same time, but only if they traverse the block in the same direction. + +NOTE: The direction in which a vehicle traverses a block is determined using the first allocation request containing resources that are part of the block -- see <>. +For the requested resources (usually a point and a path) the path is checked for a property with the key `tcs:blockEntryDirection`. +The property's value may be an arbitrary character string (including the empty string). +If there is no such property the path's name is being used as the direction. + +==== Layer + +Layers are abstract elements that group points, paths, locations and links. +They can be useful for modelling complex plants and dividing plant sections into logical groups (e.g. floors in a multi-floor plant). +A layer has the following properties: + +* An _active_ flag, which indicates whether a layer is currently set as the active (drawing) layer. + There can only be one active layer at a time. + This property is shown only in the model editor client. +* A _visible_ flag, which indicates whether a layer is shown or hidden. + When a layer is hidden, the model elements it contains are not displayed. +* A descriptive _name_. +* A _group_, that the layer is assigned to -- see <>. + A layer can only be assigned to one layer group at a time. +* A _group visible_ flag, which indicates whether the layer group the layer is assigned to is shown or hidden -- see <>. + +In addition to the properties listed above, layers also have an ordinal number (which is not displayed) that defines the order of the layers in relation to each other. +The order of the layers is represented by the order of the entries in the "Layers" table in the Model Editor and the Operations Desk clients. +The topmost entry corresponds to the topmost layer (which is displayed above all other layers) and the bottommost entry corresponds to the bottommost layer (which is displayed below all other layers). + +==== Layer group + +Layer groups are abstract elements that group layers. +A layer group has the following properties: + +* A descriptive _name_. +* A _visible_ flag, which indicates whether the layer group is shown or hidden. + When a layer group is hidden, the model elements contained in all layers assigned to it are not displayed. + The visibility state of a layer group doesn't affect the visibility state of the layers assigned to it. + +=== Plant operation elements + +Transport orders and order sequences are elements that are available only at plant operation time. +Their attributes are primarily set when the respective elements are created. + +==== Transport order + +A transport order is a parameterized sequence of movements and operations to be processed by a vehicle. +When creating a transport order, the following attributes can be set: + +* A sequence of _destinations_ that the processing vehicle must process (in their given order). + Each destination consists of a location that the vehicle must travel to and an operation that it must perform there. +* An optional _deadline_, indicating when the transport order is supposed to have been processed. +* An optional _type_, which is a string used for filtering vehicles that may be assigned to the transport order. + A vehicle may only be assigned to a transport order if the order's type is in the vehicle's set of allowed order types. + (Examples for potentially useful types are `"Transport"` and `"Maintenance"`.) +* An optional _intended vehicle_, telling the dispatcher to assign the transport order to the specified vehicle instead of selecting one automatically. +* An optional set of _dependencies_, i.e. references to other transport orders that need to be processed before the transport order. + Dependencies are transitive, meaning that if order A depends on order B and order B depends on order C, C must be processed first, then B, then A. + As a result, dependencies are a means to impose an order on sets of transport orders. + (They do not, however, implicitly require all the transport orders to be processed by the same vehicle. + This can optionally be achieved by also setting the _intended vehicle_ attribute of the transport orders.) + The following image shows an example of dependencies between multiple transport orders: + +.Transport order dependencies +image::transportorder_dependencies_example.png[] + +==== Order sequence + +NOTE: The operations desk application currently does not provide a way to create order sequences. +They can only be created programmatically, using dedicated clients that are not part of the openTCS distribution. + +An order sequence describes a process spanning multiple transport orders which are to be executed subsequently -- in the exact order defined by the sequence -- by a single vehicle. +Once a vehicle is assigned to an order sequence, it may not process transport orders not belonging to the sequence, until the latter is finished. + +Order sequences are useful when a complex process to be executed by one and the same vehicle cannot be mapped to a single transport order. +This can be the case, for instance, when the details of some steps in the process become known only after processing previous steps. + +An order sequence carries the following attributes: + +* A sequence of _transport orders_, which may be extended as long the complete flag (see below) is not set, yet. +* A _complete_ flag, indicating that no further transport orders will be added to the sequence. + This cannot be reset. +* A _failure fatal_ flag, indicating that, if one transport order in the sequence fails, all orders following it should immediately be considered as failed, too. +* A _finished_ flag, indicating that the order sequence has been processed (and the vehicle is not bound to it, anymore). + An order sequence can only be marked as finished if it has been marked as complete before. +* An optional _type_ -- see <>. + An order sequence and all transport orders it contains (must) share the same type. +* An optional _intended vehicle_, telling the dispatcher to assign the order sequence to the specified vehicle instead of selecting one automatically. + If set, all transport orders added to the order sequence must carry the same intended vehicle value. + +.An order sequence +image::ordersequence_example.png[] + +==== Peripheral job + +A peripheral job describes an operation to be performed by a peripheral device. +A peripheral job carries the following attributes: + +* An _operation_ to be performed by a peripheral device -- see <>. +* A _reservation token_ that may be used to reserve a peripheral device. + A peripheral device that is reserved for a specific token can only process jobs which match that reservation token -- see <>. +* An optional _related vehicle_ referencing the vehicle by which the peripheral job was created. +* An optional _related transport order_ referencing the transport order in which context the peripheral job was created. + +=== Common element attributes + +==== Unique name + +Every plant model and plant operation element has a unique name identifying it in the system, regardless of what type of element it is. +Two elements may not be given the same name, even if e.g. one is a point and the other one is a transport order. + +==== Generic properties + +In addition to the listed attributes, it is possible to define arbitrary properties as key-value pairs for all driving course elements, which for example can be read and evaluated by vehicle drivers or client software. +Both the key and the value can be arbitrary character strings. +For example, a key-value pair `"IP address"`:``"192.168.23.42"`` could be defined for a vehicle in the model, stating which IP address is to be used to communicate with the vehicle; a vehicle driver could now check during runtime whether a value for the key `"IP address"` was defined, and if yes, use it to automatically configure the communication channel to the vehicle. +Another use for these generic attributes can be vehicle-specific actions to be executed on certain paths in the model. +If a vehicle should, for instance, issue an acoustic warning and/or turn on the right-hand direction indicator when currently on a certain path, attributes with the keys `"acoustic warning"` and/or `"right-hand direction indicator"` could be defined for this path and evaluated by the respective vehicle driver. diff --git a/opentcs-documentation/src/docs/users-guide/03_operating-opentcs.adoc b/opentcs-documentation/src/docs/users-guide/03_operating-opentcs.adoc new file mode 100644 index 0000000..91fc60d --- /dev/null +++ b/opentcs-documentation/src/docs/users-guide/03_operating-opentcs.adoc @@ -0,0 +1,344 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + +== Operating the system + +To create or to edit the plant model of a transport system, use the Model Editor application. + +As a graphical frontend for a transportation control system based on an existing plant model, use the Operations Desk application. +Note that the Operations Desk application always requires a running openTCS kernel that it can connect to. + +Starting an application is done by executing the respective Unix shell script (`\*.sh`) or Windows batch file (`*.bat`). + +=== Constructing a new plant model + +These instructions demonstrate how a new plant model is created and filled with driving course elements so that it can eventually be used for plant operation. + +==== Starting the Model Editor + +. Launch the Model Editor (`startModelEditor.bat/.sh`). +. The Model Editor will start with a new, empty model, but you can also load a model from a file (btn:[menu:File[Load Model]]) or the current kernel model (btn:[menu:File[Load current kernel model]]). + The latter option requires a running kernel that the Model Editor client can connect to. +. Use the graphical user interface of the Model Editor client to create an arbitrary driving course for your respective application/project. + How you can add elements like points, paths and vehicles to your driving course is explained in detail in the following section. + Whenever you want to start over, select btn:[menu:File[New Model]] from the main menu. + +==== Adding elements to the plant model + +.Control elements in the Model Editor client +image::screenshot_modelling.png[] + +. Create three points by selecting the point tool from the driving course elements toolbar (see red frame in the screenshot above) and click on three positions on the drawing area. +. Link the three points with paths to a closed loop by +.. selecting the path tool from the driving course elements toolbar. +.. clicking on a point, dragging the path to the next point and releasing the mouse button there. +. Create two locations by selecting the location tool from the driving course elements toolbar and clicking on any two free positions on the drawing area. + As a location type does not yet exist in the plant model, a new one is created implicitly when creating the first location, which can be seen in the tree view to the left of the drawing area. +. Link the two locations with (different) points by +.. selecting the link tool from the driving course elements toolbar. +.. clicking on a location, dragging the link to a point and releasing the mouse button. +. Create a new vehicle by clicking on the vehicle button in the driving course elements toolbar. +. Define the allowed operations for vehicles at the newly created locations by +.. selecting the locations' type in the tree view to the left of the drawing area (see blue frame in the screenshot above). +.. clicking the value cell labelled btn:[Supported vehicle operations] in the property window below the tree view. +.. entering the allowed operations as arbitrary text in the dialog shown, for instance `"Load cargo"` and `"Unload cargo"`. +.. Optionally, you can choose a symbol for locations of the selected type by editing the property `"Symbol"`. + +IMPORTANT: You will not be able to create any transport orders and assign them to vehicles when operating the system unless you create locations in your plant model, link these locations to points in the driving course and define the operations that vehicles may execute with the respective location types. + +===== Adding and configuring peripheral devices + +As described in <>, locations can be used to map peripheral devices. +This means that, after executing the steps described above, there are now two locations available in the plant model that can potentially be used to integrate two peripheral devices. +To integrate an exemplary peripheral device, the following additional steps are required: + +. Associate a location with a peripheral device by +.. selecting the location in the tree view to the left of the drawing area (e.g. "Location-0001" in the screenshot above). +.. clicking the value cell labelled btn:[Miscellaneous] in the property window below the tree view. +.. adding a key-value pair with the key `tcs:loopbackPeripheral` and an empty value. +. Define the allowed operations for the peripheral device associated with this location by +.. selecting the locations' type in the tree view to the left of the drawing area (e.g. "LType-0001" in the screenshot above). +.. clicking the value cell labelled btn:[Supported peripheral operations] in the property window below the tree view. +.. entering the allowed operations as arbitrary text in the dialog shown, for instance `"Open door"` and `"Close door"`. +. Optionally, define peripheral operations on paths by +.. selecting a path in the tree view to the left of the drawing area (e.g. "Point-0001 --- Point-0002" in the screenshot above). +.. clicking the value cell labelled btn:[Peripheral operations] in the property window below the tree view. +.. configuring and adding peripheral operations via the dialog shown. + +NOTE: The steps above describe the process of associating a location with a virtual peripheral device that is controlled by the loopback peripheral driver. +Unlike vehicles, which don't require any additional configuration for the loopback vehicle driver to be assigned to them, locations specifically require the aforementioned property for the peripheral loopback driver to be assigned to them. + +IMPORTANT: You will not be able to create any peripheral jobs and assign them to peripheral devices when operating the system unless you create locations in your plant model, associate these locations with peripheral devices and define the operations that peripheral devices may execute with the respective location types. + +==== Working with vehicle envelopes + +As described in <>, vehicle envelopes can be defined at points and paths. +A vehicle envelope is a sequence of vertices (i.e., points with x and y coordinates) that, when connected in their defined order, represent the area that may be occupied by a vehicle. +In addition to the sequence of vertices, an _envelope key_ is always assigned to a vehicle envelope. +By referencing an envelope key, a vehicle indicates which envelopes (that may be defined at points or paths) should be considered when it allocates resources. +(For more details on this, see <>.) +With this, it is possible to prevent vehicles from allocating areas intersecting with areas already allocated by other vehicles. + +NOTE: If an envelope key is set for a vehicle, but no envelope with the respective key is defined at a specific resource (i.e. point or path), no envelope will be considered for the vehicle when it allocates that resource. + +.Defining and editing vehicle envelopes for points and paths +image::screenshot_envelope_editing.drawio.png[] + +.Setting an envelope key for vehicles +image::screenshot_vehicle_envelope_key.drawio.png[] + +==== Working with layers and layer groups + +In addition to adding elements to the plant model, the Model Editor allows plant models to be created with different layers and layer groups. +For more details on the properties of layers and layer groups see <> and <>. + +NOTE: With the Operations Desk application it's only possible to show/hide layers and layer groups, but not to manipulate them. + +===== Layers + +A layer groups points, paths, locations and links and allows the driving course elements it contains to be shown or hidden on demand. +Layers can be created, removed and edited using the panel shown in the screenshot below (see red frame). +There are a few things to keep in mind when working with layers: + +* There always has to be at least one layer. +* When adding a new layer, it always becomes the active layer. +* Removing a layer results in the driving course elements it contains to be removed as well. +* When adding model elements (i.e. points, paths, etc.) they are always placed on the active layer. +* Links (between locations and points) are always placed on the same layer the respective location is placed on, regardless of the active layer. +* When performing "copy & paste", "cut & paste" or "duplicate" operations on driving course elements, the copies are always placed on the active layer. + +NOTE: In the Operations Desk application the visibility of layers (and layer groups) also affects whether vehicle elements are displayed in the plant model or not. +Vehicle elements inherit the visibility of the point at which they are reported. +If a vehicle is reported at a point that is part of a hidden layer (or layer group), the vehicle element is not displayed in the plant model. + +.Layers panel (Toolbar buttons: Add layer, Remove (selected) layer, Move (selected) layer up, Move (selected) layer down) +image::screenshot_layers_panel.png[] + +===== Layer groups + +A layer group groups, as the name implies, one or more layers and allows multiple layers to be shown or hidden at once. +Layer groups can be created, removed and edited using the panel shown in the screenshot below (see red frame). +There are a few things to keep in mind when working with layer groups: + +* There always has to be at least one layer group. +* Removing a layer group results in all layers assigned to it to be removed as well. + +.Layer groups panel (Toolbar buttons: Add layer group, Remove (selected) layer group) +image::screenshot_layer_groups_panel.png[] + +==== Saving the plant model + +You have two options to save the model: on your local hard drive or in a running kernel instance the Model Editor can connect to. + +===== Saving the model locally + +Select btn:[menu:File[Save Model]] or btn:[menu:File[Save Model As...]] and enter a file name for the model. + +===== Loading the model into a running kernel + +Select btn:[menu:File[Upload model to kernel]] and your model will be loaded into the kernel, letting you use it for operating a plant. +This, though, requires you to save it locally first. +Note that any model that was previously loaded in the kernel will be replaced, as the kernel can only store a single model at a time. + +=== Operating the plant + +These instructions explain how the newly created model that was loaded into the kernel can be used for plant operation, how vehicle drivers are used and how transport orders can be created and processed by a vehicle. + +==== Starting components for system operation + +.Operations Desk application displaying plant model +image::screenshot_operations_desk.png[] + +. Start the kernel (`startKernel.bat/.sh`). +.. If this is your first time running the kernel, you need to load an existing plant model from the Model Editor into the kernel first. +(See <>). +. Start the Kernel Control Center application (`startKernelControlCenter.bat/.sh`) +. Start the Operations Desk application (`startOperationsDesk.bat/.sh`) +. In the Kernel Control Center, select the btn:[Vehicle driver] tab. +Then select, configure and start driver for each vehicle in the model. +.. The list on the left-hand side of the window shows all vehicles in the chosen model. +.. A detailed view for a vehicle can be seen on the right-hand side of the driver panel after double-clicking on the vehicle name in the list. +The specific design of this detailed view depends on the driver associated with the vehicle. +Usually, status information sent by the vehicle (e.g. current position and mode of operation) is displayed and low-level settings (e.g. for the vehicle's IP address) are provided here. +.. Right-clicking on the list of vehicles shows a popup menu that allows to attach drivers to selected vehicles. +.. For a vehicle to be controlled by the system, a driver needs to be attached to the vehicle and enabled. +(For testing purposes without real vehicles that could communicate with the system, the so-called loopback driver can be used, which provides a virtual vehicle that roughly simulates a real one.) +How you attach and enable a vehicle driver is explained in detail in <>. + +.Driver panel with detailed view of a vehicle +image::screenshot_driver_panel.png[] + +==== Configuring vehicle drivers + +. Switch to the Kernel Control Center application. +. Associate a vehicle with the loopback driver by right-clicking on the vehicle in the vehicle list of the driver panel and selecting the menu entry btn:[menu:Driver[Loopback adapter (virtual vehicle)]]. +. Open the detailed view of the vehicle by double-clicking on the vehicle's name in the list. +. In the detailed view of the vehicle that is now shown to the right of the vehicle list, select the btn:[Loopback options] tab. +. Enable the driver by ticking the checkbox btn:[Enable loopback adapter] in the btn:[Loopback options] tab or the checkbox in the btn:[Enabled?] column of the vehicle list. +. In the btn:[Loopback options] tab or in the vehicles list, select a point from the plant model to have the loopback adapter report this point to the kernel as the (virtual) vehicle's current position. + In the btn:[Loopback options] tab, this can be done by clicking on the btn:[Position] text field. + (In a real-world application, a vehicle driver communicating with a real vehicle would automatically report the vehicle's current position to the kernel as soon as it is known.) +. Switch to the Operations Desk client. + An icon representing the vehicle should now be shown at the point on which you placed it using the loopback driver. +. Right-click on the vehicle and select btn:[menu:Context menu[Change integration level > ...to utilize this vehicle for transport orders]] to allow the kernel to dispatch the vehicle. + The vehicle is then available for processing orders, which is indicated by an integration level `TO_BE_UTILIZED` in the property panel at the bottom left of the Operations Desk application's window. + (You can revert this by right-clicking on the vehicle and selecting btn:[menu:Context menu[Change integration level > ...to respect this vehicle's position]] in the context menu. + The integration level shown is now `TO_BE_RESPECTED` and the vehicle will not be dispatched for transport orders any more.) + +==== Creating a transport order + +To create a transport order, the Operations Desk application provides a dialog window presented when selecting btn:[menu:Actions[Create transport order...]] from the menu. +Transport orders are defined as a sequence of destination locations at which operations are to be performed by the vehicle processing the order. +You can select a destination location and operation from a dropdown menu. +You may also optionally select the vehicle intended to process this order. +If none is explicitly selected, the control system automatically assigns the order to a vehicle according to its internal, configurable strategies (see <>). +You may also optionally select or define a type for the transport order to be created. +Furthermore, a transport order can be given a deadline specifying the point of time at which the order should be finished at the latest. +This deadline will primarily be considered when there are multiple transport orders in the pool and openTCS needs to decide which to assign next. + +To create a new transport order, do the following: + +. Select the menu entry btn:[menu:Actions[Create transport order...]]. +. In the dialog shown, click the btn:[Add] button and select a location as the destination and an operation which the vehicle should perform there. + You can add an arbitrary number of destinations to the order this way. + They will be processed in the given order. +. After creating the transport order with the given destinations by clicking btn:[OK], the kernel will look for a vehicle that can process the order. + If a vehicle is found, it is assigned the order immediately and the route computed for it will be highlighted in the Operations Desk application. + The loopback driver then simulates the vehicle's movement to the destinations and the execution of the operations. + +==== Withdrawing transport orders using the Operations Desk application + +A transport order can be withdrawn from a vehicle that is currently processing it. +When withdrawing a transport order, its processing will be cancelled and the vehicle (driver) will not receive any further movement commands for it. +A withdrawal can be issued by right-clicking on the respective vehicle in the Operations Desk application, selecting btn:[menu:Context menu[Withdraw transport order]] and then selecting one of the following actions: + +* '...and let the vehicle finish movement': + The vehicle will process any movement commands it has already received and will stop after processing them. + This type of withdrawal is what should normally be used for withdrawing a transport order from a vehicle. +* '...and stop the vehicle immediately': + In addition to what happens in the case of a regular withdrawal, the vehicle is also asked to discard all movement commands it has already received. + (This _should_ make it come to a halt very soon in most cases. + However, if and how far exactly it will still move highly depends on the vehicle's type, its current situation and how communication between openTCS and this type of vehicle works.) + Furthermore, all reservations for resources on the withdrawn route (i.e. the next paths and points) except for the vehicle's currently reported position are cancelled, making these resources available to other vehicles. + This forced withdrawal should be used with great care and usually only when the vehicle is currently _not moving_! + +CAUTION: Since forced withdrawal frees paths and points previously reserved for the vehicle, it is possible that other vehicles acquire and use these resources themselves right after the withdrawal. +At the same time, if the vehicle was moving when the withdrawal was issued, it may - depending on its type - not have come to a halt, yet, and still move along the route it had previously been ordered to follow. +As the latter movement is not coordinated by openTCS, this can result in a _collision or deadlock_ between the vehicles! +For this reason, it is highly recommended to issue a forced withdrawal only if it is required for some reason, and only if the vehicle has already come to a halt on a position in the driving course or if other vehicles need not be taken into account. +In all other cases, the regular withdrawal should be used. + +Processing of a withdrawn transport order _cannot_ be resumed later. +To resume a transportation process that was interrupted by withdrawing a transport order, you need to create a new transport order, which may, of course, contain the same destinations as the withdrawn one. +Note, however, that the new transport order may not be created with the same name. +The reason for this is: + +a. Names of transport orders need to be unique. +b. Withdrawing a transport order only aborts its processing, but does not remove it from the kernel's memory, yet. + The transport order data is kept as historical information for a while before it is completely removed. + (For how long the old order is kept depends on the kernel application's configuration -- see <>.) + +As a result, a name used for a transport order may eventually be reused, but only after the actual data of the old order has been removed. + +==== Continuous creation of transport orders + +NOTE: The Operations Desk application can easily be extended via custom plugins. +As a reference, a simple load generator plugin is included which here also serves as a demonstration of how the system looks like during operation. +Details about how custom plugins can be created and integrated into the Operations Desk application can be found in the developer's guide. + +. In the Operations Desk application, select btn:[menu:View[Plugins > Continuous load]] from the menu. +. Choose a trigger for creating new transport orders: + New orders will either be created only once, or whenever the number of unprocessed orders in the system drops below a specified limit, or after a specified timeout has expired. +. By using an order profile you may decide whether the transport orders`' destinations should be chosen randomly or whether you want to choose them yourself. ++ +Using btn:[Create orders randomly], you define the number of transport orders that are to be generated at a time, and the number of destinations a single transport order should contain. +Since the destinations will be selected randomly, the orders created do not necessarily make sense for a real-world system. ++ +Using btn:[Create orders according to definition], you can define an arbitrary number of transport orders, each with an arbitrary number of destinations and properties, and save and load your list of transport orders. +. Start the order generator by activating the corresponding checkbox at the bottom of the btn:[Continuous load] panel. + The load generator will then generate transport orders according to its configuration until the checkbox is deactivated or the panel is closed. + +==== Configuring peripheral device drivers + +NOTE: In order to perform the following steps, make sure you first associate a location with a peripheral device, as described in <>. + +. Switch to the Kernel Control Center application. +. Select the btn:[Peripheral driver] tab. +** _A location should already be associated with the peripheral loopback driver._ +. Open the detailed view of the location by double-clicking on the location's name in the list. +. In the detailed view of the location that is now shown to the right of the peripheral device list, select the btn:[Loopback options] tab. +. Enable the driver by ticking the checkbox in the btn:[Enabled?] column of the peripheral device list. +. Switch to the Operations Desk client. +. The peripheral device is now available for processing peripheral jobs. + +==== Creating a peripheral job + +A peripheral job is defined as a single operation that is to be performed by the peripheral device processing the job. +Peripheral jobs can be created either explicitly or implicitly; both ways are described in the following sections. + +NOTE: For information on how peripheral jobs are assigned to peripheral devices, see <>. +For information on how the control system's internal strategies for assigning peripheral jobs can be configured, see <>. + +===== Explicit creation of peripheral jobs + +To create a new peripheral job, do the following in the Operations Desk application: + +. Select the menu entry btn:[menu:Actions[Create peripheral job...]]. +. In the dialog shown, select the location associated with the peripheral device that should be assigned with the peripheral job and an operation which the peripheral device should perform. + Additionally, enter some arbitrary text for the reservation token. + (For the moment, the reservation token doesn't really matter. + The only important thing is that it is not empty. + For more details on the reservation token, see <>.) +. After creating the peripheral job by clicking btn:[OK], the kernel will try to assign it to the peripheral device associated with the given location to process the job. + Once the peripheral job is assigned, the loopback driver simulates the peripheral devices's execution of the operation. + +NOTE: While it is fine to use this option for creating peripheral jobs, the next option is the preferred one as it allows direct interaction between vehicles and peripheral devices. + +===== Implicit creation of peripheral jobs + +NOTE: In order for implicit creation of peripheral jobs to work, make sure to first define peripheral operations on paths as described in <>. + +Peripheral jobs can be created implicitly by vehicles as they traverse paths that have peripheral operations defined on them. +When exactly a peripheral job is created for a peripheral operation defined on a path depends on the configuration of the peripheral operation itself. +As described in <>, the _execution trigger_ of a peripheral operation defines when the operation is to be performed by the peripheral device. +Note the following regarding the creation time of the peripheral job belonging to a peripheral operation: + +* `AFTER_ALLOCATION` (previously named `BEFORE_MOVEMENT`): The peripheral job is created as soon as the resource for the corresponding path has been allocated for the vehicle. + This means that if a vehicle can accept multiple (points and) paths in advance, a peripheral job may be created before the vehicle even reaches the actual path. +* `AFTER_MOVEMENT`: The peripheral job is created as soon as the vehicle has completely traversed the path and reported the corresponding movement command as executed (i.e. when the vehicle reached the respective end point of the path). + +==== Withdrawing peripheral jobs using the Operations Desk application + +How a peripheral job can be withdrawn depends on whether it is related to a transport order: + +* Peripheral jobs that are not related to transport orders and peripheral jobs that are related to transport orders in a final state (finished or failed) can be withdrawn by selecting them in the peripheral jobs table and clicking the withdrawal button shown above the table. +* Peripheral jobs that are related to transport orders will implicitly be aborted when the respective transport order is withdrawn. + Withdrawing them independently from the related transport order is not supported (unless the related transport order is already in a final state -- see above.) +** For transport orders that are withdrawn regularly, related peripheral jobs are aborted after the vehicle has completed its pending movements. +** For transport orders that are withdrawn forcibly, related peripheral jobs are aborted immediately. + +===== Consequence of peripheral jobs failing + +When a peripheral job fails, the related transport order (if any) is implicitly withdrawn under the following circumstances: + +* The peripheral job was created implicitly by a vehicle traversing a path (see <>). +* The _completion required_ flag of the peripheral job's peripheral operation is set to `true`. + +In such cases, it is not reasonable to continue the transport order, as the _completion required_ flag not only indicates that the vehicle should wait for the peripheral operation to be completed, but also for it to be completed _successfully_. +Otherwise, the prerequisites for continuing driving / the transport order may not be met. + +==== Removing a vehicle from a running system + +There may be situations in which you want to remove a single vehicle from a system, e.g. because the vehicle temporarily cannot be used by openTCS due to a hardware defect that has to be dealt with first. +The following steps will ensure that no further transport orders are assigned to the vehicle and that the resources it might still be occupying are freed for use by other vehicles. + +. In the Operations Desk application, right-click on the vehicle and select btn:[menu:Context menu[Change integration level > ...to ignore this vehicle]] to disable the vehicle for transport order processing and to free the point in the driving course that the vehicle is occupying. +. In the Kernel Control Center application, disable the vehicle's driver by unticking the checkbox btn:[Enable loopback adapter] in the btn:[Loopback options] tab or the checkbox in the btn:[Enabled?] column of the vehicle list. + +==== Pausing and resuming the operation of vehicles + +The Operations Desk application provides two buttons with which the operation of vehicles can be paused or resumed. +However, in order for these buttons to have any effect, the respective vehicle drivers for the vehicles in use have to implement and support this feature. + +.Pause and resume buttons in the Operations Desk application +image::screenshot_operations_desk_pause_and_resume.png[] diff --git a/opentcs-documentation/src/docs/users-guide/04_default-strategies.adoc b/opentcs-documentation/src/docs/users-guide/04_default-strategies.adoc new file mode 100644 index 0000000..6b5454a --- /dev/null +++ b/opentcs-documentation/src/docs/users-guide/04_default-strategies.adoc @@ -0,0 +1,244 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + +== Default strategies + +openTCS comes with a default implementation for each of the strategy modules. +These implementations can easily be replaced to adapt to project-specific requirements. +(See developer's guide.) + +=== Default dispatcher + +When either a transport order or a vehicle becomes available, the dispatcher needs to decide what should happen with which transport order and which vehicle should do what. +To make this decision, the default dispatcher takes the following steps: + +. New transport orders are prepared for processing. + This includes checking general routability and unfinished dependencies. +. Updates of processes that are currently active are performed. + This includes: +** Withdrawals of transport orders +** Successful completion of transport orders +** Assignment of subsequent transport orders for vehicles that are processing order sequences +. Vehicles that are currently unoccupied are assigned to processable transport orders, if possible. +** Criteria for a vehicle to be taken into account are: +*** It must be at a known position in the driving course. +*** It may not be assigned to a transport order, or the assigned transport order must be _dispensable_. + That is the case with parking orders, for instance, or with recharging orders if the vehicle's energy level is not critical. +*** Its energy level must not be critical. + (Vehicles with critical energy level are taken into account only for transport orders with which the first destination operation is a recharge operation.) +** Criteria for a transport order to be taken into account are: +*** It must be generally dispatchable. +*** It must not be part of an order sequence that is already being processed by a vehicle. +** The assignment mechanics are as following: +*** If there are less unoccupied vehicles than processable transport orders, the list of vehicles is sorted by configurable criteria. + The default dispatcher then iterates over the sorted list and, for every vehicle, finds all orders processable by it, computes the required routes, sorts the candidates by configurable criteria and assigns the first one. +*** If there are less processable transport orders than unoccupied vehicles, the list of transport orders is sorted by configurable criteria. + The default dispatcher then iterates over the sorted list and, for every transport order, finds all vehicles that could process it, computes the required routes, sorts the candidates by configurable criteria and assigns the first one. +*** For configuration options regarding the sorting criteria, see <>. +. Vehicles that are still unoccupied are sent to a recharging location, if possible. +** Criteria for a vehicle to be taken into account are: +*** It must be at a known position in the driving course. +*** Its energy level is _degraded_. +. Vehicles that are still unoccupied are sent to a parking position, if possible. +** Criteria for a vehicle to be taken into account are: +*** It must be at a known position in the driving course. +*** It must not be at a parking position already. + +==== Default parking position selection + +When sending a vehicle to a parking position, the closest (according to the router) unoccupied position is selected by default. +It is possible to assign fixed positions to vehicles instead, by setting properties with the following keys on them: + +* `tcs:preferredParkingPosition`: + Expected to be the name of a point in the model. + If this point is already occupied, the closest unoccupied parking position (if any) is selected instead. +* `tcs:assignedParkingPosition`: + Expected to be the name of a point in the model. + If this point is already occupied, the vehicle is not sent to any other parking position, i.e. remains where it is. + Takes precedence over `tcs:preferredParkingPosition`. + +==== Optional parking position priorities + +Optionally (see <> for how to enable it), parking positions may be explicitly prioritized, and vehicles can be reparked in a kind of "parking position queues". +This can be desirable e.g. to park vehicles close to locations that are frequent first destinations for transport orders. +(For example, imagine a plant in which goods are transported from A to B all the time. +Even if there currently aren't any transport orders, it might nevertheless be a good idea to prefer parking positions near A to reduce reaction times when transport orders arrive.) + +To assign a priority to a parking position, set a property with the key `tcs:parkingPositionPriority` on the point. +The property's value should be a decimal integer, with lower values resulting in a higher priority for the parking position. + +==== Default recharging location selection + +When sending a vehicle to a recharge location, the closest (according to the router) unoccupied position is selected by default. +It is possible to assign fixed positions to vehicles instead, by setting properties with the following keys on them: + +* `tcs:preferredRechargeLocation`: + Expected to be the name of a location. + If this location is already occupied, the closest unoccupied recharging location (if any) is selected instead. +* `tcs:assignedRechargeLocation`: + Expected to be the name of a location. + If this location is already occupied, the vehicle is not sent to any other recharging location. + Takes precedence over `tcs:preferredRechargeLocation`. + +==== Immediate transport order assignment + +In addition to the _implicit_ assignment of transport orders according to the flow and rules described in the previous sections, transport orders can also be assigned _explicitly_ (i.e. immediately). +Immediate assignment of transport orders is supported for transport orders that have their intended vehicle set. +This can be helpful in situations where a transport order and its intended vehicle are generally in a state where an assignment would be possible, but is prevented by certain filter criteria in the regular dispatcher flow. + +Although the immediate assignment of transport orders bypasses some of the filter criteria in the regular dispatcher flow, it works only in specific situations. +Regarding the transport order's state: + +* The transport order's state must be `DISPATCHABLE`. +* The transport order must not be part of an order sequence. +* The transport order's intended vehicle must be set. + +As for the (intended) vehicle's state: + +* The vehicle's processing state must be `IDLE`. +* The vehicle's state must be `IDLE` or `CHARGING`. +* The vehicle's integration level must be `TO_BE_UTILIZED`. +* The vehicle must be reported at a known position. +* The vehicle must not process an order sequence. + +NOTE: In addition to the state of the respective transport order and its intended vehicle, the dispatcher may have further implementation-specific reasons to reject an immediate assignment. + +=== Default router + +The default router finds the cheapest route from one position in the driving course to another one. +(It uses an implementation of link:https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm[Dijkstra's algorithm] to do that.) +It takes into account paths that have been locked, but not positions and/or assumed future behaviour of other vehicles. +As a result, it does not route around slower or stopped vehicles blocking the way. + +==== Cost functions + +The cost function used for evaluating the paths in the driving course can be selected via configuration. +(See <>, the relevant configuration entry is `defaultrouter.shortestpath.edgeEvaluators`.) +The following cost functions/configuration options are available: + +* `DISTANCE` (default): + Routing costs are equal to the paths' lengths. +* `TRAVELTIME`: + Routing costs are computed as the expected time to travel on the paths (in seconds), i.e. as path length divided by maximum allowed vehicle speed. +* `EXPLICIT_PROPERTIES`: + Routing costs for a vehicle on a path are taken from path properties with keys `tcs:routingCostForward` and `tcs:routingCostReverse`. + The `` to be used is the vehicle's routing group (see <>). + As an example, if a vehicle's routing group is set to "Example", routing costs for this vehicle would be taken from path properties with keys `tcs:routingCostForwardExample` and `tcs:routingCostReverseExample`. + This way, different routing costs can be assigned to a path, e.g. for different types of vehicles. + + Note that, for this cost function to work properly, the values of the routing cost properties should be decimal integers. + An exception to this is the string `Infinity`, which the property value can be set to, indicating that the path may not be used by vehicles of the respective routing group at all. +* `HOPS`: + The routing costs for every path in the model is 1, which results in the route with the least paths/points being chosen. +* `BOUNDING_BOX`: + Routing costs for a vehicle on a path are determined by comparing the vehicle's bounding box with the maximum allowed bounding box at the path's destination point -- see <>. + If the vehicle's bounding box protrudes beyond a destination point's bounding box, the routing costs for the corresponding path are considered infinitely high, indicating that the path may not be used by the vehicle at all. + Otherwise, the routing costs for the corresponding path are 0. + This can be used to prevent vehicles from being routed to/through points where there is insufficient space available. + +Developers can integrate additional custom cost functions using the openTCS API. + +More than one cost function can be selected in the configuration by listing them separated by commas. +The costs computed by the respective functions are then added up. +For example, when using `"DISTANCE, TRAVELTIME"`, costs for routes are computed as the sum of the paths' lengths and the time a vehicle needs to pass it. + +NOTE: Adding distances to durations obviously does not make sense. +It is the user's responsibility to choose a configuration that is usable and appropriate for the respective use case. + +==== Routing groups + +It is possible to treat vehicles in a plant differently when computing their routes. +This may be desirable if they have different characteristics and actually have different optimal routes through the driving course. +For this to work, the paths in the model or the cost function used need to reflect this difference. +This isn't done by default -- the default router computes routes for all vehicles the same way unless told otherwise. +To let the router know that it should compute routes for a vehicle separately, set a property with the key `tcs:routingGroup` to an arbitrary string. +(Vehicles that have the same value set share the same routing table, and the empty string is the default value for all vehicles.) + +==== Avoiding/Excluding resources when computing routes + +When computing a route for a transport order, it is possible to define a set of resources (i.e., points, paths or locations) that should be avoided by vehicles processing the respective transport order. +For this, a property with the key `tcs:resourcesToAvoid` can be set on a transport order to a comma-separated list of resource names. + +=== Default scheduler + +The default scheduler implements a simple strategy for traffic management. +It does this by allowing only mutually exclusive use of resources in the plant model (points, paths and locations), as described below. + +==== Allocating resources + +When an allocation of a set of resources for a vehicle is requested, the scheduler performs the following checks to determine whether the allocation can be granted immediately: + +. Check if the vehicle requesting the resources is _not_ paused. +. Check if the requested resources are generally available for the vehicle. +. Check if the requested resources are part of a block with the type `SINGLE_VEHICLE_ONLY`. + If not, skip this check. + If yes, expand the requested resource set to the effective resource set and check if the expanded resources are available for the vehicle. +. Check if the requested resources are part of a block with the type `SAME_DIRECTION_ONLY`. + If not, skip this check. + If yes, check if the direction in which the vehicle intends to traverse the block is the same the block is already being traversed by other vehicles. +. Check if the areas related to the requested resources are available for the vehicle and not allocated by other vehicles (provided that the vehicle requesting the resources references an envelope key and the requested resources define vehicle envelopes with that key). + +If all checks succeed, the allocation is made. +If any of the checks fail, the allocation is queued for later. + +==== Freeing resources + +Whenever resources are freed (e.g. when a vehicle has finished its movement to the next point and the vehicle driver reports this to the kernel), the allocations waiting in the queue are checked (in the order the requests happened). +Any allocations that can now be made are made. +Allocations that cannot be made are kept waiting. + +==== Fairness of scheduling + +This strategy ensures that resources are used when they are available. +It does not, however, strictly ensure fairness/avoid starvation: +Vehicles waiting for allocation of a large resource set may theoretically wait forever if other vehicles can keep allocating subsets of those resources continuously. +Such situations are likely a hint at problems in the plant model graph's topology, which is why this deficiency is considered acceptable for the default implementation. + +=== Default peripheral job dispatcher + +When either a peripheral job or a peripheral device becomes available, the peripheral job dispatcher needs to decide what should happen with which peripheral job and which peripheral device should do what. +To make this decision, the default peripheral job dispatcher takes the following steps: + +. Peripheral devices that are currently unoccupied but have their reservation token set are assigned to processable peripheral jobs, if possible. +** Criteria for a peripheral device to be taken into account are: +*** It must not be assigned to a peripheral job. +*** It must have its reservation token set. +** Criteria for a peripheral job to be taken into account are: +*** It must match the reservation token of a peripheral device. +*** It must be processable by a peripheral device. +** If there are multiple peripheral jobs that meet these criteria, the oldest one according to the creation time is assigned first. +. Peripheral devices that could not be assigned to a peripheral job with a matching reservation token have their reservation released. +** The release of reserved peripheral devices is performed via a replaceable strategy. + The default strategy releases peripheral devices according to the following rules: +*** A peripheral device's state must be `IDLE`. +*** A peripheral device's processing state must be `IDLE`. +*** A peripheral device's reservation token must be set. +. Peripheral devices that are currently unoccupied and do not have their reservation token set are assigned to processable peripheral jobs, if possible. +** Criteria for a peripheral device to be taken into account are: +*** It must not be assigned to a peripheral job. +*** It must not have its reservation token set. +** Criteria for a peripheral job to be taken into account are: +*** It must be generally available to be processed by a peripheral device. +*** It must be processable by a peripheral device. +** The selection of a peripheral job for a peripheral device is performed via a replaceable strategy. + The default strategy selects peripheral jobs according to the following rules: +*** The location of a peripheral job's operation must match the given location. +*** If there are multiple peripheral jobs that meet these criteria, the oldest one according to the creation time is selected. + +==== Reservation token + +As described above, reservation tokens are relevant for the assignment of peripheral jobs to peripheral devices. +This section describes the different types of reservation tokens: + +. Reservation tokens for transport orders. +** Optionally, transport orders can be provided with a reservation token. +** If a transport order's reservation token is set, it is used for peripheral jobs that are created in the context of the transport order (i.e., for peripheral jobs that are created implicitly by vehicles processing a transport order - see <>). +. Reservation tokens for peripheral jobs. +** Peripheral jobs must always be provided with a reservation token. +** For peripheral jobs that are created implicitly by vehicles as they traverse paths that have peripheral operations defined on them, the reservation token is set to +*** the reservation token of the transport order the corresponding vehicle is processing +*** or the name of the vehicle, if the reservation token on the transport order is not set. +. Reservation tokens for locations that represent peripheral devices. +** Initially, the reservation token of a location representing a peripheral device is not set. + This indicates that the peripheral device is generally available to accept a peripheral job with any reservation token. +** Once the peripheral device is assigned a peripheral job, the location's reservation token is set to the peripheral job's reservation token. + As a result, the peripheral device is only available for peripheral jobs with the same reservation token until the peripheral device's reservation is released (i.e., until the peripheral device's reservation token is reset). diff --git a/opentcs-documentation/src/docs/users-guide/05_configuring-opentcs.adoc b/opentcs-documentation/src/docs/users-guide/05_configuring-opentcs.adoc new file mode 100644 index 0000000..83aadde --- /dev/null +++ b/opentcs-documentation/src/docs/users-guide/05_configuring-opentcs.adoc @@ -0,0 +1,192 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + +== Configuring openTCS + +=== Application language + +By default, all openTCS applications with user interfaces display texts in English language. +The applications are prepared for internationalization, though, and can be configured to display texts in a different language, provided there is a translation for it. +The openTCS distribution comes with the default (English) language and a German translation. +Additional translations can be integrated -- how this is done is described in the Developer's Guide. + +For setting the language, each application has a configuration entry that needs to be set to a _language tag_ for the language to use. +(See <>, <> and <>.) +Examples for language tags are: + +* "en" for English +* "de" for German +* "no" for Norwegian +* "zh" for Chinese + +By default, the configuration entries are set to "en", resulting in English texts. +Since a German translation is included, you can switch e.g. the Operations Desk application to German by setting its `locale` configuration entry to "de". +(Note that the application needs to be restarted for this.) + +Configuring an application to use a language for which there is no translation will result in the default (English) language to be used. + +=== Kernel configuration + +The kernel application reads its configuration data from the following files: + +. `config/opentcs-kernel-defaults-baseline.properties`, +. `config/opentcs-kernel-defaults-custom.properties` and +. `config/opentcs-kernel.properties`. + +The files are read in this order, and configuration values set in one file can be overridden in any subsequent one. +For users, it is recommended to leave the first two files untouched and set overriding values and project-specific configuration data in `opentcs-kernel.properties` only. + +==== Kernel application configuration entries + +The kernel application itself can be configured using the following configuration entries: + +include::{configdoc}/KernelApplicationConfigurationEntries.adoc[] + +==== Order pool configuration entries + +The kernel's transport order pool can be configured using the following configuration entries: + +include::{configdoc}/OrderPoolConfigurationEntries.adoc[] + +==== Default dispatcher configuration entries + +The default dispatcher can be configured using the following configuration entries: + +include::{configdoc}/DefaultDispatcherConfigurationEntries.adoc[] + +==== Default router configuration entries + +The default router can be configured using the following configuration entries: + +include::{configdoc}/DefaultRouterConfigurationEntries.adoc[] + +The shortest path algorithm can be configured using the following configuration entries: + +include::{configdoc}/ShortestPathConfigurationEntries.adoc[] + +The edge evaluator `EXPLICIT_PROPERTIES` can be configured using the following configuration entries: + +include::{configdoc}/ExplicitPropertiesConfigurationEntries.adoc[] + +==== Default peripheral job dispatcher configuration entries + +The default peripheral job dispatcher can be configured using the following configuration entries: + +include::{configdoc}/DefaultPeripheralJobDispatcherConfigurationEntries.adoc[] + +==== Admin web API configuration entries + +The kernel's admin web API can be configured using the following configuration entries: + +include::{configdoc}/AdminWebApiConfigurationEntries.adoc[] + +==== Service web API configuration entries + +The kernel's service web API can be configured using the following configuration entries: + +include::{configdoc}/ServiceWebApiConfigurationEntries.adoc[] + +==== RMI kernel interface configuration entries + +The kernel's RMI interface can be configured using the following configuration entries: + +include::{configdoc}/RmiKernelInterfaceConfigurationEntries.adoc[] + +==== SSL server-side encryption configuration entries + +The kernel's SSL encryption can be configured using the following configuration entries: + +include::{configdoc}/KernelSslConfigurationEntries.adoc[] + +==== Virtual vehicle configuration entries + +The virtual vehicle (loopback communication adapter) can be configured using the following configuration entries: + +include::{configdoc}/VirtualVehicleConfigurationEntries.adoc[] + +==== Virtual peripheral configuration entries + +The virtual peripheral (peripheral loopback communication adapter) can be configured using the following configuration entries: + +include::{configdoc}/VirtualPeripheralConfigurationEntries.adoc[] + +==== Watchdog configuration entries + +The watchdog can be configured using the following configuration entries: + +include::{configdoc}/WatchdogConfigurationEntries.adoc[] + +=== Kernel Control Center configuration + +The kernel control center application reads its configuration data from the following files: + +. `config/opentcs-kernelcontrolcenter-defaults-baseline.properties`, +. `config/opentcs-kernelcontrolcenter-defaults-custom.properties` and +. `config/opentcs-kernelcontrolcenter.properties`. + +The files are read in this order, and configuration values set in one file can be overridden in any subsequent one. +For users, it is recommended to leave the first two files untouched and set overriding values and project-specific configuration data in `opentcs-kernelcontrolcenter.properties` only. + +==== Kernel Control Center application configuration entries + +The kernel control center application itself can be configured using the following configuration entries: + +include::{configdoc}/KernelControlCenterApplicationConfigurationEntries.adoc[] + +==== SSL KCC-side application configuration entries + +The kernel control center application's SSL connections can be configured using the following configuration entries: + +include::{configdoc}/KccSslConfigurationEntries.adoc[] + +=== Model Editor configuration + +The model editor application reads its configuration data from the following files: + +. `config/opentcs-modeleditor-defaults-baseline.properties`, +. `config/opentcs-modeleditor-defaults-custom.properties`, +. `config/opentcs-modeleditor.properties`. + +The files are read in this order, and configuration values set in one file can be overridden in any subsequent one. +For users, it is recommended to leave the first two files untouched and set overriding values and project-specific configuration data in `opentcs-modeleditor.properties` only. + +==== Model Editor application configuration entries + +The model editor application itself can be configured using the following configuration entries: + +include::{configdoc}/ModelEditorConfigurationEntries.adoc[] + +==== SSL model editor-side application configuration entries + +The model editor application's SSL connections can be configured using the following configuration entries: + +include::{configdoc}/PoSslConfigurationEntries.adoc[] + +==== Model Editor element naming scheme configuration entries + +The model editor application's element naming schemes can be configured using the following configuration entries: + +include::{configdoc}/PO_ElementNamingSchemeConfigurationEntries.adoc[] + +=== Operations Desk configuration + +The operations desk application reads its configuration data from the following files: + +. `config/opentcs-operationsdesk-defaults-baseline.properties`, +. `config/opentcs-operationsdesk-defaults-custom.properties`, +. `config/opentcs-operationsdesk.properties`. + +The files are read in this order, and configuration values set in one file can be overridden in any subsequent one. +For users, it is recommended to leave the first two files untouched and set overriding values and project-specific configuration data in `opentcs-operationsdesk.properties` only. + +==== Operations Desk application configuration entries + +The operations desk application itself can be configured using the following configuration entries: + +include::{configdoc}/OperationsDeskConfigurationEntries.adoc[] + +==== SSL operation desk-side application configuration entries + +The operation desk application's SSL connections can be configured using the following configuration entries: + +include::{configdoc}/PoSslConfigurationEntries.adoc[] diff --git a/opentcs-documentation/src/docs/users-guide/06_advanced-usage-examples.adoc b/opentcs-documentation/src/docs/users-guide/06_advanced-usage-examples.adoc new file mode 100644 index 0000000..fd72c27 --- /dev/null +++ b/opentcs-documentation/src/docs/users-guide/06_advanced-usage-examples.adoc @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + +== Advanced usage examples + +=== Configuring automatic startup + +. To automatically enable vehicle drivers on startup, set the kernel application's configuration parameter `kernelapp.autoEnableDriversOnStartup` to `true`. + +=== Automatically selecting a specific vehicle driver on startup + +Automatic attachment of vehicle drivers by default works as follows: +The kernel asks every available vehicle driver if it can attach to a given vehicle and selects the first one that can. +It asks the loopback driver last, as that one is always available and can attach to any vehicle, but should not prevent actual vehicle drivers to be attached. +As a result, if there is only one driver for your vehicle(s), you usually do not have to do anything for it to be selected. + +In some less common cases, you may have multiple vehicle drivers registered with the kernel that can all attach to the vehicles in your plant model. +To automatically select a specific driver in such cases, set a property with the key `tcs:preferredAdapterClass` on the vehicles, with its value being the name of the Java class implementing the driver's adapter factory. +(If you do not know this class name, ask the developer who provided the vehicle driver to you for it.) + +=== Configuring a virtual vehicle's characteristics + +The loopback driver supports some (limited) configuration of the virtual vehicle's characteristics via properties set in the plant model. +You can set the properties the following way: + +. Start the Model Editor application and create or load a plant model. +. In the Model Editor application's tree view of the plant model, select a vehicle. +. In the table showing the vehicle's properties, click into the value field labelled btn:[Miscellaneous]. + In the dialog shown, add a property key and value according to the list below. +. Save the model and upload it to the kernel as described in <>. + +The loopback driver interprets properties with the following keys: + +* `loopback:initialPosition`: + Set the property value to the name of a point in the plant model. + When started, the loopback adapter will set the virtual vehicle's current position to this. + (Default value: not set) +* `loopback:acceleration`: + Set the property value to a positive integer representing an acceleration in mm/s^2^. + The loopback adapter will simulate vehicle movement with the given acceleration. + (Default value: 500) +* `loopback:deceleration`: + Set the property value to a negative integer representing an acceleration in mm/s^2^. + The loopback adapter will simulate vehicle movement with the given deceleration. + (Default value: -500) +* `loopback:loadOperation`: + Set the property value to a string representing the virtual vehicle's load operation. + When the virtual vehicle executes this operation, the loopback adapter will set the its load handling device's state to _full_. + (Default value: not set) +* `loopback:unloadOperation`: + Set the property value to a string representing the virtual vehicle's unload operation. + When the virtual vehicle executes this operation, the loopback adapter will set its load handling device's state to _empty_. + (Default value: not set) +* `loopback:operatingTime`: + Set the property value to a positive integer representing the virtual vehicle's operating time in milliseconds. + When the virtual vehicle executes an operation, the loopback adapter will simulate an operating time accordingly. + (Default value: 5000) + +=== Transforming coordinates sent to / received from vehicles + +It is possible that not all vehicles managed by an openTCS kernel instance are using the same coordinate system as the one in the plant model. +To ease integration with such situations, the kernel can transform coordinates when they are sent to / received from vehicles. + +Out of the box, the kernel can apply offsets to coordinate and orientation angle values. +To make use of this, do the following: + +1. On a vehicle element in the plant model, set a property with key `tcs:vehicleDataTransformer` to value `OFFSET_TRANSFORMER`. +2. Set a property with key `tcs:offsetTransformer.x` to an offset value, e.g. 10. + The value is expected to be an integer in millimeters and may also be negative. +3. Also set properties with keys `tcs:offsetTransformer.y` and `tcs:offsetTransformer.z` for offsets on the other two axes. +4. Also set a property with key `tcs:offsetTransformer.orientation` for an orientation angle offset. + The value is expected to be a floating-point value in degrees and may also be negative. + +With this configuration, the given offset values will be added to coordinate and orientation angle values that are sent to the vehicle, and will be subtracted from coordinate and orientation angle values that are received by the vehicle. + +NOTE: This mechanism is extensible: +Developers can integrate their own transformation components with the openTCS kernel to apply custom, complex transformations to data sent to / received from vehicles. + +=== Running kernel and its clients on separate systems + +The kernel and its clients (the Model Editor, Operations Desk and Kernel Control Center applications) communicate via the Java Remote Method Invocation (RMI) mechanism. +This makes it possible to run the kernel and the clients on separate hosts, as long as a usable network connection between these systems exists. + +By default, the Model Editor, the Operations Desk and the Kernel Control Center are configured to connect to a kernel running on the same host. +To connect them to a kernel running on a remote host, e.g. on a host named myhost.example.com, do the following: + +* For the Model Editor, set the configuration parameter `modeleditor.connectionBookmarks` to `SomeDescription|myhost.example.com|1099`. +* For the Operations Desk, set the configuration parameter `operationsdesk.connectionBookmarks` to `SomeDescription|myhost.example.com|1099`. +* For the Kernel Control Center, set the configuration parameter `kernelcontrolcenter.connectionBookmarks` to `SomeDescription|myhost.example.com|1099`. + +The configuration value can be a comma-separated list of `||` sets. +The applications will automatically try to connect to the first host in the list. +If that fails, they will show a dialog to select an entry or enter a different address to connect to. + +=== Encrypting communication with the kernel + +By default, client applications and the kernel communicate via plain Java Remote Method Invocation (RMI) calls or HTTP requests. +These communication channels can optionally be encrypted via SSL/TLS. +To achieve this, do the following: + +. Generate a keystore/truststore pair (`keystore.p12` and `truststore.p12`). +.. You can use the Unix shell script or Windows batch file (`generateKeystores.sh/.bat`) provided in the kernel application's directory for this. +.. The scripts use the key and certificate management tool 'keytool' that is included in both the Java JDK and JRE. + If 'keytool' is not contained in the system's `Path` environment variable the `KEYTOOL_PATH` variable in the respective script needs to be modified to point to the location where the 'keytool' is located. +.. By default, the generated files are placed in the kernel application's `config` directory. +. Copy the `truststore.p12` file to the client application's (Model Editor, Operations Desk or Kernel Control Center) `config` directory. + Leave the file in the kernel application's `config` directory as well. +. In the kernel's configuration file, enable SSL for the RMI interface and/or for the web service interface. + (See <> and/or <> for a description of the configuration entries.) +. If you enabled SSL for the RMI interface, you need to enable it in the Model Editor's, Operations Desk's and the Kernel Control Center's configuration files, too. + (See <>, <> and <> for a description of the configuration entries.) + +=== Configuring automatic parking and recharging + +By default, idle vehicles remain where they are after processing their last transport order. +You can change this in the kernel's configuration file: + +* To order vehicles to charging locations automatically, set the configuration parameter `defaultdispatcher.rechargeIdleVehicles` to `true`. + The default dispatcher will then look for locations at which the idle vehicle's recharge operation is possible and create orders to send it to such a location (if unoccupied). + (Note that the string used for the operation is driver-specific.) +* To order vehicles to parking positions automatically, set the configuration parameter `defaultdispatcher.parkIdleVehicles` to `true`. + The default dispatcher will then look for unoccupied parking positions and create orders to send the idle vehicle there. + +=== Configuring order pool cleanup + +By default, openTCS checks every minute for finished or failed transport orders and peripheral jobs that are older than 24 hours. +These orders and jobs are removed from the pool. +To customize this behaviour, do the following: + +. Set the configuration entry `orderpool.sweepInterval` to a value according to your needs. + The default value is 60.000 (milliseconds, corresponding to an interval of one minute). +. Set the configuration entry `orderpool.sweepAge` to a maximum age of finished orders and jobs according to your needs. + The default value is 86.400.000 (milliseconds, corresponding to 24 hours that a finished order or job should be kept in the pool). + +=== Using model element properties for project-specific data + +Every object in the plant model - i.e. points, paths, locations, location types and vehicles - can be augmented with arbitrary project-specific data that can be used, e.g. by vehicle drivers, custom client applications, etc.. +Possible uses for such data could be informing the vehicle driver about additional actions to be performed by a vehicle when moving along a path in the model (e.g. flashing direction indicators, displaying a text string on a display, giving an acoustic warning) or controlling the behaviour of peripheral systems (e.g. automatic fire protection gates). + +The data can be stored in properties, i.e. key-value pairs attached to the model elements, where both the key and the corresponding value are text strings. +These key-value pairs can be created and edited using the Model Editor application: +Simply select the model element you want to add a key-value pair to and click into the value field labelled btn:[Miscellaneous] in the properties table. +In the dialog shown, set the key-value pairs you need to store your project-specific information. + +NOTE: For your project-specific key-value pairs, you may specify arbitrary keys. +openTCS itself will not make any use of this data; it will merely store it and provide it for custom vehicle drivers and/or other extensions. +You should, however, not use any keys starting with `"tcs:"` for storing project-specific data. +Any keys with this prefix are reserved for official openTCS features, and using them could lead to collisions. diff --git a/opentcs-documentation/src/docs/users-guide/docinfo.html b/opentcs-documentation/src/docs/users-guide/docinfo.html new file mode 100644 index 0000000..c38b145 --- /dev/null +++ b/opentcs-documentation/src/docs/users-guide/docinfo.html @@ -0,0 +1,7 @@ + + + + diff --git a/opentcs-documentation/src/docs/users-guide/images/REUSE.toml b/opentcs-documentation/src/docs/users-guide/images/REUSE.toml new file mode 100644 index 0000000..42aab58 --- /dev/null +++ b/opentcs-documentation/src/docs/users-guide/images/REUSE.toml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC0-1.0 + +version = 1 + +[[annotations]] +path = ["**/*.gif", "**/*.jpg", "**/*.png", "**/*.svg"] +precedence = "closest" +SPDX-FileCopyrightText = "The openTCS Authors" +SPDX-License-Identifier = "CC-BY-4.0" diff --git a/opentcs-documentation/src/docs/users-guide/images/bounding-box-for-vehicle-and-point.drawio.png b/opentcs-documentation/src/docs/users-guide/images/bounding-box-for-vehicle-and-point.drawio.png new file mode 100644 index 0000000..7485528 Binary files /dev/null and b/opentcs-documentation/src/docs/users-guide/images/bounding-box-for-vehicle-and-point.drawio.png differ diff --git a/opentcs-documentation/src/docs/users-guide/images/bounding-box-vehicle-on-point.drawio.png b/opentcs-documentation/src/docs/users-guide/images/bounding-box-vehicle-on-point.drawio.png new file mode 100644 index 0000000..144e2ed Binary files /dev/null and b/opentcs-documentation/src/docs/users-guide/images/bounding-box-vehicle-on-point.drawio.png differ diff --git a/opentcs-documentation/src/docs/users-guide/images/bounding-box.drawio.png b/opentcs-documentation/src/docs/users-guide/images/bounding-box.drawio.png new file mode 100644 index 0000000..eba5437 Binary files /dev/null and b/opentcs-documentation/src/docs/users-guide/images/bounding-box.drawio.png differ diff --git a/opentcs-documentation/src/docs/users-guide/images/ordersequence_example.png b/opentcs-documentation/src/docs/users-guide/images/ordersequence_example.png new file mode 100644 index 0000000..d41d399 Binary files /dev/null and b/opentcs-documentation/src/docs/users-guide/images/ordersequence_example.png differ diff --git a/opentcs-documentation/src/docs/users-guide/images/ordersequence_example.puml b/opentcs-documentation/src/docs/users-guide/images/ordersequence_example.puml new file mode 100644 index 0000000..51ce491 --- /dev/null +++ b/opentcs-documentation/src/docs/users-guide/images/ordersequence_example.puml @@ -0,0 +1,44 @@ +@startuml +' SPDX-FileCopyrightText: The openTCS Authors +' SPDX-License-Identifier: CC-BY-4.0 + +skinparam monochrome true +left to right direction + +object "Order Sequence 1" as orderSequence1 { + orders = { + Transport Order 1, + Transport Order 2, + Transport Order 3, + Transport Order 4\n} + processingVehicle = "Vehicle 3" +} + +object "Transport Order 1" as transportOrder1 { + dependencies = {} + wrappingSequence = "Order Sequence 1" + processingVehicle = "Vehicle 3" +} + +object "Transport Order 2" as transportOrder2 { + dependencies = {} + wrappingSequence = "Order Sequence 1" + processingVehicle = "Vehicle 3" +} + +object "Transport Order 3" as transportOrder3 { + dependencies = {} + wrappingSequence = "Order Sequence 1" + processingVehicle = "Vehicle 3" +} + +object "Transport Order 4" as transportOrder4 { + dependencies = {} + wrappingSequence = "Order Sequence 1" + processingVehicle = "Vehicle 3" +} + +transportOrder1 <-- transportOrder2 +transportOrder2 <-- transportOrder3 +transportOrder3 <-- transportOrder4 +@enduml diff --git a/opentcs-documentation/src/docs/users-guide/images/screenshot_driver_panel.png b/opentcs-documentation/src/docs/users-guide/images/screenshot_driver_panel.png new file mode 100644 index 0000000..bf0c9e4 Binary files /dev/null and b/opentcs-documentation/src/docs/users-guide/images/screenshot_driver_panel.png differ diff --git a/opentcs-documentation/src/docs/users-guide/images/screenshot_envelope_editing.drawio.png b/opentcs-documentation/src/docs/users-guide/images/screenshot_envelope_editing.drawio.png new file mode 100644 index 0000000..18310c7 Binary files /dev/null and b/opentcs-documentation/src/docs/users-guide/images/screenshot_envelope_editing.drawio.png differ diff --git a/opentcs-documentation/src/docs/users-guide/images/screenshot_layer_groups_panel.png b/opentcs-documentation/src/docs/users-guide/images/screenshot_layer_groups_panel.png new file mode 100644 index 0000000..13536ee Binary files /dev/null and b/opentcs-documentation/src/docs/users-guide/images/screenshot_layer_groups_panel.png differ diff --git a/opentcs-documentation/src/docs/users-guide/images/screenshot_layers_panel.png b/opentcs-documentation/src/docs/users-guide/images/screenshot_layers_panel.png new file mode 100644 index 0000000..bcb81db Binary files /dev/null and b/opentcs-documentation/src/docs/users-guide/images/screenshot_layers_panel.png differ diff --git a/opentcs-documentation/src/docs/users-guide/images/screenshot_modelling.png b/opentcs-documentation/src/docs/users-guide/images/screenshot_modelling.png new file mode 100644 index 0000000..1338af3 Binary files /dev/null and b/opentcs-documentation/src/docs/users-guide/images/screenshot_modelling.png differ diff --git a/opentcs-documentation/src/docs/users-guide/images/screenshot_operations_desk.png b/opentcs-documentation/src/docs/users-guide/images/screenshot_operations_desk.png new file mode 100644 index 0000000..2d354ae Binary files /dev/null and b/opentcs-documentation/src/docs/users-guide/images/screenshot_operations_desk.png differ diff --git a/opentcs-documentation/src/docs/users-guide/images/screenshot_operations_desk_pause_and_resume.png b/opentcs-documentation/src/docs/users-guide/images/screenshot_operations_desk_pause_and_resume.png new file mode 100644 index 0000000..bb3071a Binary files /dev/null and b/opentcs-documentation/src/docs/users-guide/images/screenshot_operations_desk_pause_and_resume.png differ diff --git a/opentcs-documentation/src/docs/users-guide/images/screenshot_vehicle_envelope_key.drawio.png b/opentcs-documentation/src/docs/users-guide/images/screenshot_vehicle_envelope_key.drawio.png new file mode 100644 index 0000000..cd8faf9 Binary files /dev/null and b/opentcs-documentation/src/docs/users-guide/images/screenshot_vehicle_envelope_key.drawio.png differ diff --git a/opentcs-documentation/src/docs/users-guide/images/system_overview.png b/opentcs-documentation/src/docs/users-guide/images/system_overview.png new file mode 100644 index 0000000..2d92e37 Binary files /dev/null and b/opentcs-documentation/src/docs/users-guide/images/system_overview.png differ diff --git a/opentcs-documentation/src/docs/users-guide/images/system_overview.puml b/opentcs-documentation/src/docs/users-guide/images/system_overview.puml new file mode 100644 index 0000000..f6c5279 --- /dev/null +++ b/opentcs-documentation/src/docs/users-guide/images/system_overview.puml @@ -0,0 +1,51 @@ +@startuml +' SPDX-FileCopyrightText: The openTCS Authors +' SPDX-License-Identifier: CC-BY-4.0 + +skinparam monochrome true + +'left to right direction + +agent Kernel [ + Kernel + <&caret-right> Dispatching + <&caret-right> Routing + <&caret-right> Scheduling +] +interface "RMI Interface" as RmiInterface +interface "Web API" as WebApi +interface "Vehicle driver #1" as VehicleDriver1 +interface "Vehicle driver #2" as VehicleDriver2 +interface "Vehicle driver #n" as VehicleDriverN + +agent "Model Editor" as ModelEditor +agent "Operations Desk" as OperationsDesk +agent "Kernel Control Center" as KernelControlCenter + +agent "Third-party client\n(ERP, WMS, ...)" as Client1 +agent "Third-party client\n(ERP, WMS, ...)" as Client2 +agent "Third-party client\n(ERP, WMS, ...)" as Client3 + +agent "Vehicle #1" as Vehicle1 +agent "Vehicle #2" as Vehicle2 +agent "Vehicle #n" as VehicleN + +RmiInterface -- Kernel : openTCS API +WebApi -- Kernel : openTCS API + +ModelEditor .. RmiInterface : RMI +OperationsDesk .. RmiInterface : RMI +KernelControlCenter .. RmiInterface : RMI + +Client1 .. RmiInterface : RMI +Client2 .. WebApi : HTTP +Client3 .. WebApi : HTTP + +Kernel -- VehicleDriver1 : openTCS API +Kernel -- VehicleDriver2 : openTCS API +Kernel -- VehicleDriverN : openTCS API + +VehicleDriver1 .. Vehicle1 : Vehicle Protocol +VehicleDriver2 .. Vehicle2 : Vehicle Protocol +VehicleDriverN .. VehicleN : Vehicle Protocol +@enduml diff --git a/opentcs-documentation/src/docs/users-guide/images/transportorder_dependencies_example.png b/opentcs-documentation/src/docs/users-guide/images/transportorder_dependencies_example.png new file mode 100644 index 0000000..e6171a3 Binary files /dev/null and b/opentcs-documentation/src/docs/users-guide/images/transportorder_dependencies_example.png differ diff --git a/opentcs-documentation/src/docs/users-guide/images/transportorder_dependencies_example.puml b/opentcs-documentation/src/docs/users-guide/images/transportorder_dependencies_example.puml new file mode 100644 index 0000000..62b8716 --- /dev/null +++ b/opentcs-documentation/src/docs/users-guide/images/transportorder_dependencies_example.puml @@ -0,0 +1,33 @@ +@startuml +' SPDX-FileCopyrightText: The openTCS Authors +' SPDX-License-Identifier: CC-BY-4.0 + +skinparam monochrome true +left to right direction + +object "Transport Order 1" as transportOrder1 { + dependencies = {} + wrappingSequence = null + processingVehicle = "Vehicle 3" +} +object "Transport Order 2" as transportOrder2 { + dependencies = {Transport Order 1} + wrappingSequence = null + processingVehicle = "Vehicle 1" +} +object "Transport Order 3" as transportOrder3 { + dependencies = {} + wrappingSequence = null + processingVehicle = "Vehicle 7" +} +object "Transport Order 4" as transportOrder4 { + dependencies = {Transport Order 2, Transport Order 3} + wrappingSequence = null + processingVehicle = "Vehicle 2" +} + +transportOrder1 <-- transportOrder2 + +transportOrder3 <-- transportOrder4 +transportOrder2 <-- transportOrder4 +@enduml diff --git a/opentcs-documentation/src/docs/users-guide/opentcs-users-guide.adoc b/opentcs-documentation/src/docs/users-guide/opentcs-users-guide.adoc new file mode 100644 index 0000000..606aa0b --- /dev/null +++ b/opentcs-documentation/src/docs/users-guide/opentcs-users-guide.adoc @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: CC-BY-4.0 + += openTCS: User's Guide +The openTCS developers +:doctype: book +:toc: left +:toclevels: 3 +:sectnums: all +:sectnumlevels: 6 +:imagesdir: images +:icons: font +:source-highlighter: coderay +:coderay-linenums-mode: table +:last-update-label!: +:experimental: + +// TIP: Always have the comprehensive http://asciidoctor.org/docs/asciidoc-syntax-quick-reference[QuickReference] handy. + +include::01_introduction.adoc[] + +include::02_system-overview.adoc[] + +include::03_operating-opentcs.adoc[] + +include::04_default-strategies.adoc[] + +include::05_configuring-opentcs.adoc[] + +include::06_advanced-usage-examples.adoc[] diff --git a/opentcs-documentation/src/main/java/org/opentcs/documentation/ConfigDocGenerator.java b/opentcs-documentation/src/main/java/org/opentcs/documentation/ConfigDocGenerator.java new file mode 100644 index 0000000..1340e4e --- /dev/null +++ b/opentcs-documentation/src/main/java/org/opentcs/documentation/ConfigDocGenerator.java @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.documentation; + +import static org.opentcs.util.Assertions.checkArgument; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.SortedSet; +import java.util.TreeSet; +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Generates a file documenting an application's configuration entries. + */ +public class ConfigDocGenerator { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ConfigDocGenerator.class); + + /** + * Prevents instantiation. + */ + private ConfigDocGenerator() { + } + + /** + * Generates a file documenting an application's configuration entries. + * + * @param args The arguments are expected to be pairs of (1) the fully qualified name of a + * configuration interface and (2) the name of a file to write the documentation to. + * @throws Exception In case there was a problem processing the input. + */ + public static void main(String[] args) + throws Exception { + checkArgument(args.length >= 2, "Expected at least 2 arguments, got %d.", args.length); + checkArgument(args.length % 2 == 0, "Expected even number of arguments, got %d.", args.length); + + for (int i = 0; i < args.length; i += 2) { + processConfigurationInterface(args[i], args[i + 1]); + } + } + + private static void processConfigurationInterface(String className, String outputFilePath) + throws ClassNotFoundException { + Class clazz = ConfigDocGenerator.class.getClassLoader().loadClass(className); + String prefix = extractPrefix(clazz); + + SortedSet configurationEntries = new TreeSet<>(); + for (Method method : clazz.getMethods()) { + configurationEntries.add(extractEntry(method, prefix)); + } + + checkArgument( + !configurationEntries.isEmpty(), + "No configuration keys in {}.", + clazz.getName() + ); + + generateFile(outputFilePath, configurationEntries); + } + + private static String extractPrefix(Class clazz) { + ConfigurationPrefix annotation = clazz.getAnnotation(ConfigurationPrefix.class); + checkArgument(annotation != null, "Missing prefix annotation at class %s", clazz.getName()); + return annotation.value(); + } + + private static Entry extractEntry(Method method, String prefix) { + ConfigurationEntry annotation = method.getAnnotation(ConfigurationEntry.class); + checkArgument(annotation != null, "Missing entry annotation at method %s", method.getName()); + return new Entry( + prefix, + method.getName(), + annotation.type(), + changesAppliedWhen(annotation.changesApplied()), + annotation.description(), + annotation.orderKey() + ); + } + + /** + * Writes the configuration entries to a file using AsciiDoc syntax. + * + * @param outputFilePath The output file path to write to. + * @param configurationEntries The configuration entries. + */ + private static void generateFile( + String outputFilePath, + Collection configurationEntries + ) { + try (PrintWriter writer = new PrintWriter(new FileWriter(outputFilePath, true))) { + for (Entry entry : configurationEntries) { + writeEntry(writer, entry); + } + + writer.println(); + } + catch (IOException ex) { + LOG.error("", ex); + } + } + + private static void writeEntry(final PrintWriter writer, Entry entry) { + writer.print("`"); + writer.print(entry.prefix); + writer.print('.'); + writer.print(entry.name); + writer.println("`::"); + + writer.print("* Type: "); + writer.println(entry.type); + + writer.print("* Trigger for changes to be applied: "); + writer.println(entry.changesApplied); + + writer.print("* Description: "); + writer.println(String.join(" +\n", entry.description)); + } + + private static String changesAppliedWhen(ConfigurationEntry.ChangesApplied changesApplied) { + switch (changesApplied) { + case ON_APPLICATION_START: + return "on application start"; + case ON_NEW_PLANT_MODEL: + return "when/after plant model is loaded"; + case INSTANTLY: + return "instantly"; + default: + case UNSPECIFIED: + return "unspecified"; + } + } + + /** + * Describes a configuration entry. + */ + private static class Entry + implements + Comparable { + + /** + * The prefix of this configuration entry. + */ + private final String prefix; + /** + * The name of this configuration entry. + */ + private final String name; + /** + * A description for the data type of this configuration entry. + */ + private final String type; + /** + * Whether a change of the configuration value requires a restart of the application. + */ + private final String changesApplied; + /** + * A description for this configuration entry. + */ + private final String[] description; + /** + * A key for sorting entries. + */ + private final String orderKey; + + Entry( + String prefix, + String name, + String type, + String changesApplied, + String[] description, + String orderKey + ) { + this.prefix = prefix; + this.name = name; + this.type = type; + this.changesApplied = changesApplied; + this.description = description; + this.orderKey = orderKey; + } + + @Override + public int compareTo(Entry entry) { + int result = this.orderKey.compareTo(entry.orderKey); + if (result == 0) { + result = this.name.compareTo(entry.name); + } + return result; + } + } +} diff --git a/opentcs-documentation/src/test/java/org/opentcs/documentation/developers_guide/CreateTransportOrderSequenceTest.java b/opentcs-documentation/src/test/java/org/opentcs/documentation/developers_guide/CreateTransportOrderSequenceTest.java new file mode 100644 index 0000000..6c16bfb --- /dev/null +++ b/opentcs-documentation/src/test/java/org/opentcs/documentation/developers_guide/CreateTransportOrderSequenceTest.java @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.documentation.developers_guide; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.access.to.order.DestinationCreationTO; +import org.opentcs.access.to.order.OrderSequenceCreationTO; +import org.opentcs.access.to.order.TransportOrderCreationTO; +import org.opentcs.components.kernel.services.InternalTransportOrderService; +import org.opentcs.components.kernel.services.TransportOrderService; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; + +/** + * Test for the developers asciidoc documentation to show how a tranport order sequence is created. + * This test has no meaning and it just exists for the documentation to refer to and to guarantee + * an example that compiles. + */ +class CreateTransportOrderSequenceTest { + + private TransportOrderService orderService; + + @BeforeEach + void setUp() { + orderService = mock(InternalTransportOrderService.class); + when(orderService.createOrderSequence(any(OrderSequenceCreationTO.class))) + .thenReturn(new OrderSequence("OrderSequence")); + when(orderService.createTransportOrder(any(TransportOrderCreationTO.class))) + .thenReturn( + new TransportOrder( + "Transportorder", + Collections.singletonList( + new DriveOrder( + new DriveOrder.Destination(getSampleDestinationLocation().getReference()) + .withOperation("some operation") + ) + ) + ) + ); + } + + @Test + void demoCreateOrderSequence() { + // Note: Keep these lines to a maximum of 80 characters for the documentation! + + // tag::createOrderSequence_createOrderSequenceCreationTO[] + OrderSequenceCreationTO sequenceTO + = new OrderSequenceCreationTO("MyOrderSequence"); + // end::createOrderSequence_createOrderSequenceCreationTO[] + + // tag::createOrderSequence_setIncompleteName[] + sequenceTO = sequenceTO.withIncompleteName(true); + // end::createOrderSequence_setIncompleteName[] + + // tag::createOrderSequence_setFailureFatal[] + sequenceTO = sequenceTO.withFailureFatal(true); + // end::createOrderSequence_setFailureFatal[] + + // tag::createOrderSequence_useServiceToCreateSequence[] + TransportOrderService transportOrderService = getATransportOrderService(); + OrderSequence orderSequence + = transportOrderService.createOrderSequence(sequenceTO); + // end::createOrderSequence_useServiceToCreateSequence[] + + // tag::createOrderSequence_createTransportOrder[] + TransportOrderCreationTO orderTO + = new TransportOrderCreationTO( + "MyOrder", + List.of( + new DestinationCreationTO("Some location", "Some operation") + ) + ) + .withIncompleteName(true) + .withWrappingSequence(orderSequence.getName()); + + transportOrderService.createTransportOrder(orderTO); + // end::createOrderSequence_createTransportOrder[] + + // tag::createOrderSequence_markSequenceComplete[] + transportOrderService.markOrderSequenceComplete( + orderSequence.getReference() + ); + // end::createOrderSequence_markSequenceComplete[] + } + + private Location getSampleDestinationLocation() { + return new Location( + "Location", + new LocationType("LocationType").getReference() + ); + } + + private TransportOrderService getATransportOrderService() { + return orderService; + } +} diff --git a/opentcs-documentation/src/test/java/org/opentcs/documentation/developers_guide/CreateTransportOrderTest.java b/opentcs-documentation/src/test/java/org/opentcs/documentation/developers_guide/CreateTransportOrderTest.java new file mode 100644 index 0000000..fd421db --- /dev/null +++ b/opentcs-documentation/src/test/java/org/opentcs/documentation/developers_guide/CreateTransportOrderTest.java @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.documentation.developers_guide; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.access.to.order.DestinationCreationTO; +import org.opentcs.access.to.order.TransportOrderCreationTO; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.components.kernel.services.InternalTransportOrderService; +import org.opentcs.components.kernel.services.TransportOrderService; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.DriveOrder.Destination; +import org.opentcs.data.order.TransportOrder; + +/** + * Test for the developers asciidoc documentation to show how a tranport order is created. + * This test has no meaning and it just exists for the documentation to refer to and to guarantee + * an example that compiles. + */ +class CreateTransportOrderTest { + + private TransportOrderService orderService; + private DispatcherService dispService; + + @BeforeEach + void setUp() { + orderService = mock(InternalTransportOrderService.class); + dispService = mock(DispatcherService.class); + when(orderService.createTransportOrder(any(TransportOrderCreationTO.class))) + .thenReturn( + new TransportOrder( + "Transportorder", + Collections.singletonList( + new DriveOrder( + new Destination(someDestinationLocation().getReference()) + .withOperation(getDestinationOperation()) + ) + ) + ) + ); + } + + @Test + void demoCreateTransportOrderToLocation() { + // Note: Keep these lines to a maximum of 80 characters for the documentation! + + // tag::createTransportOrder_createDestinations[] + List destinations + = List.of( + new DestinationCreationTO("Some location", "Some operation") + ); + // end::createTransportOrder_createDestinations[] + + // tag::createTransportOrder_createTransportOrderCreationTO[] + TransportOrderCreationTO orderTO + = new TransportOrderCreationTO("MyTransportOrder", destinations); + // end::createTransportOrder_createTransportOrderCreationTO[] + + // tag::createTransportOrder_setIncompleteName[] + orderTO = orderTO.withIncompleteName(true); + // end::createTransportOrder_setIncompleteName[] + + // tag::createTransportOrder_setMoreOptionalParameters[] + orderTO = orderTO + .withIntendedVehicleName("Some vehicle") + .withDeadline(Instant.now().plus(1, ChronoUnit.HOURS)); + // end::createTransportOrder_setMoreOptionalParameters[] + + // tag::createTransportOrder_useServiceToCreateOrder[] + TransportOrderService transportOrderService = getATransportOrderService(); + transportOrderService.createTransportOrder(orderTO); + // end::createTransportOrder_useServiceToCreateOrder[] + + // tag::createTransportOrder_triggerDispatcher[] + DispatcherService dispatcherService = getADispatcherService(); + dispatcherService.dispatch(); + // end::createTransportOrder_triggerDispatcher[] + } + + @Test + void demoCreateTransportOrderToPoint() { + // Note: Keep these lines to a maximum of 80 characters for the documentation! + + // tag::createTransportOrderToPoint_createDestinations[] + List destinations + = List.of( + new DestinationCreationTO("Some point", Destination.OP_MOVE) + ); + // end::createTransportOrderToPoint_createDestinations[] + + // tag::createTransportOrderToPoint_createTransportOrderCreationTO[] + TransportOrderCreationTO orderTO + = new TransportOrderCreationTO("MyTransportOrder", destinations) + .withIntendedVehicleName("Some vehicle") + .withIncompleteName(true); + // end::createTransportOrderToPoint_createTransportOrderCreationTO[] + + // tag::createTransportOrderToPoint_useServiceToCreateOrder[] + TransportOrderService transportOrderService = getATransportOrderService(); + transportOrderService.createTransportOrder(orderTO); + // end::createTransportOrderToPoint_useServiceToCreateOrder[] + + // tag::createTransportOrderToPoint_triggerDispatcher[] + DispatcherService dispatcherService = getADispatcherService(); + dispatcherService.dispatch(); + // end::createTransportOrderToPoint_triggerDispatcher[] + } + + private Location someDestinationLocation() { + return new Location("Location", new LocationType("LocationType").getReference()); + } + + private TransportOrderService getATransportOrderService() { + return orderService; + } + + private DispatcherService getADispatcherService() { + return dispService; + } + + private String getDestinationOperation() { + return ""; + } +} diff --git a/opentcs-documentation/src/test/java/org/opentcs/documentation/developers_guide/ReceiveCommAdapterMessageTest.java b/opentcs-documentation/src/test/java/org/opentcs/documentation/developers_guide/ReceiveCommAdapterMessageTest.java new file mode 100644 index 0000000..3a1517d --- /dev/null +++ b/opentcs-documentation/src/test/java/org/opentcs/documentation/developers_guide/ReceiveCommAdapterMessageTest.java @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.documentation.developers_guide; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; + +/** + * Tests showing how to receive messages from communication adapters. + */ +class ReceiveCommAdapterMessageTest { + + @Test + void receiveInformationViaVehicleProperty() { + // tag::documentation_receiveMessageFromVehicle[] + Vehicle vehicle = getSomeVehicle(); + String value = vehicle.getProperty("someKey"); + processPropertyValue(value); + // end::documentation_receiveMessageFromVehicle[] + } + + @Test + void receiveInformationViaTransportOrderProperty() { + // tag::documentation_receiveMessageFromTransportOrder[] + TransportOrder transportOrder = getSomeTransportOrder(); + String value = transportOrder.getProperty("someKey"); + processPropertyValue(value); + // end::documentation_receiveMessageFromTransportOrder[] + } + + private Vehicle getSomeVehicle() { + return new Vehicle("some-vehicle"); + } + + private TransportOrder getSomeTransportOrder() { + return new TransportOrder("some-order", List.of()); + } + + private void processPropertyValue(String propertyValue) { + } +} diff --git a/opentcs-documentation/src/test/java/org/opentcs/documentation/developers_guide/WithdrawTransportOrderTest.java b/opentcs-documentation/src/test/java/org/opentcs/documentation/developers_guide/WithdrawTransportOrderTest.java new file mode 100644 index 0000000..fcd3066 --- /dev/null +++ b/opentcs-documentation/src/test/java/org/opentcs/documentation/developers_guide/WithdrawTransportOrderTest.java @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.documentation.developers_guide; + +import static org.mockito.Mockito.mock; + +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.TransportOrder; + +/** + * Test for the developers asciidoc documentation to show how a tranport order is withdrawn. + * This test has no meaning and it just exists for the documentation to refer to and to guarantee + * an example that compiles. + */ +class WithdrawTransportOrderTest { + + private DispatcherService dispService; + + @BeforeEach + void setUp() { + dispService = mock(DispatcherService.class); + } + + @Test + void shouldWithdrawTransportOrder() { + TransportOrder someOrder = getSomeTransportOrder(); + + // tag::documentation_withdrawTransportOrder[] + DispatcherService dispatcherService = getADispatcherService(); + dispatcherService.withdrawByTransportOrder(someOrder.getReference(), true); + // end::documentation_withdrawTransportOrder[] + } + + @Test + void shouldWithdrawTransportOrderByVehicle() { + Vehicle curVehicle = getSomeVehicle(); + + // tag::documentation_withdrawTransportOrderByVehicle[] + DispatcherService dispatcherService = getADispatcherService(); + dispatcherService.withdrawByVehicle(curVehicle.getReference(), true); + // end::documentation_withdrawTransportOrderByVehicle[] + } + + private Location getSampleDestinationLocation() { + return new Location("Location", new LocationType("LocationType").getReference()); + } + + private DispatcherService getADispatcherService() { + return dispService; + } + + private TransportOrder getSomeTransportOrder() { + return new TransportOrder( + "Transportorder", + Collections.singletonList( + new DriveOrder( + new DriveOrder.Destination(getSampleDestinationLocation().getReference()) + .withOperation("some-operation") + ) + ) + ); + } + + private Vehicle getSomeVehicle() { + return new Vehicle("some-vehicle"); + } +} diff --git a/opentcs-impl-configuration-gestalt/build.gradle b/opentcs-impl-configuration-gestalt/build.gradle new file mode 100644 index 0000000..38b3c06 --- /dev/null +++ b/opentcs-impl-configuration-gestalt/build.gradle @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply from: "${rootDir}/gradle/java-project.gradle" +apply from: "${rootDir}/gradle/java-codequality.gradle" +apply from: "${rootDir}/gradle/publishing-java.gradle" + +dependencies { + api project(':opentcs-api-base') + + implementation group: 'com.github.gestalt-config', name: 'gestalt-core', version: '0.24.3' +} + +task release { + dependsOn build +} diff --git a/opentcs-impl-configuration-gestalt/gradle.properties b/opentcs-impl-configuration-gestalt/gradle.properties new file mode 100644 index 0000000..14431ac --- /dev/null +++ b/opentcs-impl-configuration-gestalt/gradle.properties @@ -0,0 +1,40 @@ +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineMethodParams=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAfterDotInChainedMethodCalls=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineDisjunctiveCatchTypes=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineFor=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineImplements=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.sortMembersByVisibility=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.visibilityOrder=PUBLIC;PROTECTED;DEFAULT;PRIVATE +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.enable-indent=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineArrayInit=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineCallArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.keepGettersAndSettersTogether=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classMembersOrder=STATIC FIELD;FIELD;STATIC_INIT;CONSTRUCTOR;INSTANCE_INIT;STATIC METHOD;METHOD;STATIC CLASS;CLASS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapCommentText=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.importGroupsOrder=* +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize=4 +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineAnnotationArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineTryResources=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.preserveNewLinesInComments=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineParenthesized=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineThrows=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=100 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project diff --git a/opentcs-impl-configuration-gestalt/src/main/java/org/opentcs/configuration/gestalt/GestaltConfigurationBindingProvider.java b/opentcs-impl-configuration-gestalt/src/main/java/org/opentcs/configuration/gestalt/GestaltConfigurationBindingProvider.java new file mode 100644 index 0000000..a81fcd5 --- /dev/null +++ b/opentcs-impl-configuration-gestalt/src/main/java/org/opentcs/configuration/gestalt/GestaltConfigurationBindingProvider.java @@ -0,0 +1,196 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.configuration.gestalt; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkState; + +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; +import org.github.gestalt.config.Gestalt; +import org.github.gestalt.config.builder.GestaltBuilder; +import org.github.gestalt.config.decoder.ProxyDecoderMode; +import org.github.gestalt.config.entity.GestaltConfig; +import org.github.gestalt.config.exceptions.GestaltException; +import org.github.gestalt.config.reload.TimedConfigReloadStrategy; +import org.github.gestalt.config.source.ConfigSource; +import org.github.gestalt.config.source.ConfigSourcePackage; +import org.github.gestalt.config.source.FileConfigSourceBuilder; +import org.opentcs.configuration.ConfigurationBindingProvider; +import org.opentcs.configuration.ConfigurationException; +import org.opentcs.configuration.gestalt.decoders.ClassPathDecoder; +import org.opentcs.configuration.gestalt.decoders.MapLiteralDecoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A configuration binding provider implementation using gestalt. + */ +public class GestaltConfigurationBindingProvider + implements + ConfigurationBindingProvider { + + /** + * This class's logger. + */ + private static final Logger LOG + = LoggerFactory.getLogger(GestaltConfigurationBindingProvider.class); + /** + * The key of the (system) property containing the reload interval. + */ + private static final String PROPKEY_RELOAD_INTERVAL = "opentcs.configuration.reload.interval"; + /** + * The default reload interval. + */ + private static final long DEFAULT_RELOAD_INTERVAL = 10000; + /** + * Default configuration file name. + */ + private final Path defaultsPath; + /** + * Supplementary configuration files. + */ + private final Path[] supplementaryPaths; + /** + * The configuration entry point. + */ + private final Gestalt gestalt; + + /** + * Creates a new instance. + * + * @param defaultsPath Default configuration file name. + * @param supplementaryPaths Supplementary configuration file names. + */ + public GestaltConfigurationBindingProvider(Path defaultsPath, Path... supplementaryPaths) { + this.defaultsPath = requireNonNull(defaultsPath, "defaultsPath"); + this.supplementaryPaths = requireNonNull(supplementaryPaths, "supplementaryPaths"); + + this.gestalt = buildGestalt(); + } + + @Override + public T get(String prefix, Class type) { + try { + return gestalt.getConfig(prefix, type); + } + catch (GestaltException e) { + throw new ConfigurationException( + String.format("Cannot get configuration value for prefix: '%s'", prefix), + e + ); + } + } + + private Gestalt buildGestalt() { + GestaltConfig gestaltConfig = new GestaltConfig(); + gestaltConfig.setTreatMissingValuesAsErrors(true); + gestaltConfig.setProxyDecoderMode(ProxyDecoderMode.PASSTHROUGH); + + try { + Gestalt provider = new GestaltBuilder() + .setGestaltConfig(gestaltConfig) + .useCacheDecorator(true) + .addDefaultDecoders() + .addDecoder(new ClassPathDecoder()) + .addDecoder(new MapLiteralDecoder()) + .addSources(buildSources()) + .build(); + provider.loadConfigs(); + + return provider; + } + catch (GestaltException e) { + throw new ConfigurationException( + "An error occured while creating gestalt configuration binding provider", + e + ); + } + } + + private List buildSources() + throws GestaltException { + Duration reloadInterval = reloadInterval(); + List sources = new ArrayList<>(); + + // A file for baseline defaults MUST exist in the distribution. + checkState( + defaultsPath.toFile().isFile(), + "Required default configuration file {} does not exist.", + defaultsPath.toFile().getAbsolutePath() + ); + LOG.info( + "Using default configuration file {}...", + defaultsPath.toFile().getAbsolutePath() + ); + sources.add( + FileConfigSourceBuilder.builder() + .setPath(defaultsPath) + .addConfigReloadStrategy(new TimedConfigReloadStrategy(reloadInterval)) + .build() + ); + + // Files with supplementary configuration MAY exist in the distribution. + for (Path supplementaryPath : supplementaryPaths) { + if (supplementaryPath.toFile().isFile()) { + LOG.info( + "Using overrides from supplementary configuration file {}...", + supplementaryPath.toFile().getAbsolutePath() + ); + sources.add( + FileConfigSourceBuilder.builder() + .setPath(supplementaryPath) + .addConfigReloadStrategy(new TimedConfigReloadStrategy(reloadInterval)) + .build() + ); + } + else { + LOG.warn( + "Supplementary configuration file {} not found, skipped.", + supplementaryPath.toFile().getAbsolutePath() + ); + } + } + + for (ConfigSource source : ServiceLoader.load(SupplementaryConfigSource.class)) { + LOG.info( + "Using overrides from additional configuration source implementation {}...", + source.getClass() + ); + sources.add( + new ConfigSourcePackage( + source, + List.of(new TimedConfigReloadStrategy(reloadInterval)) + ) + ); + } + return sources; + } + + private Duration reloadInterval() { + String valueString = System.getProperty(PROPKEY_RELOAD_INTERVAL); + + if (valueString == null) { + LOG.info("Using default configuration reload interval ({} ms).", DEFAULT_RELOAD_INTERVAL); + return Duration.ofMillis(DEFAULT_RELOAD_INTERVAL); + } + + try { + long value = Long.parseLong(valueString); + LOG.info("Using configuration reload interval of {} ms.", value); + return Duration.ofMillis(value); + } + catch (NumberFormatException exc) { + LOG.warn( + "Could not parse '{}', using default configuration reload interval ({} ms).", + valueString, + DEFAULT_RELOAD_INTERVAL, + exc + ); + return Duration.ofMillis(DEFAULT_RELOAD_INTERVAL); + } + } +} diff --git a/opentcs-impl-configuration-gestalt/src/main/java/org/opentcs/configuration/gestalt/SupplementaryConfigSource.java b/opentcs-impl-configuration-gestalt/src/main/java/org/opentcs/configuration/gestalt/SupplementaryConfigSource.java new file mode 100644 index 0000000..294897b --- /dev/null +++ b/opentcs-impl-configuration-gestalt/src/main/java/org/opentcs/configuration/gestalt/SupplementaryConfigSource.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.configuration.gestalt; + +import org.github.gestalt.config.source.ConfigSource; + +/** + * A supplementary source providing configuration overrides. + */ +public interface SupplementaryConfigSource + extends + ConfigSource { + +} diff --git a/opentcs-impl-configuration-gestalt/src/main/java/org/opentcs/configuration/gestalt/decoders/ClassPathDecoder.java b/opentcs-impl-configuration-gestalt/src/main/java/org/opentcs/configuration/gestalt/decoders/ClassPathDecoder.java new file mode 100644 index 0000000..72fc3c9 --- /dev/null +++ b/opentcs-impl-configuration-gestalt/src/main/java/org/opentcs/configuration/gestalt/decoders/ClassPathDecoder.java @@ -0,0 +1,129 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.configuration.gestalt.decoders; + +import org.github.gestalt.config.decoder.Decoder; +import org.github.gestalt.config.decoder.DecoderContext; +import org.github.gestalt.config.decoder.Priority; +import org.github.gestalt.config.entity.ValidationError; +import org.github.gestalt.config.entity.ValidationLevel; +import org.github.gestalt.config.node.ConfigNode; +import org.github.gestalt.config.node.NodeType; +import org.github.gestalt.config.reflect.TypeCapture; +import org.github.gestalt.config.tag.Tags; +import org.github.gestalt.config.utils.ValidateOf; + +/** + * A decoder to decode fully qualified class names to their representative class object. + * + * This decoder looks through the class path to find a class with the specified class name + * and returns the class object. It will fail if the specified class cannot be found or + * the specified class cannot be assigned to the type that is expected to be returned. + */ +public class ClassPathDecoder + implements + Decoder> { + + /** + * Creates a new instance. + */ + public ClassPathDecoder() { + } + + @Override + public Priority priority() { + return Priority.MEDIUM; + } + + @Override + public String name() { + return ClassPathDecoder.class.getName(); + } + + @Override + public boolean canDecode(String string, Tags tags, ConfigNode cn, TypeCapture tc) { + return Class.class.isAssignableFrom(tc.getRawType()) && tc.hasParameter(); + } + + @Override + public ValidateOf> decode( + String path, + Tags tags, + ConfigNode node, + TypeCapture type, + DecoderContext context + ) { + // This decoder only decodes nodes of type leaf. For other types the default decoders + // `ArrayDecoder` and `ObjectDecoder` will eventually call this decoder if necessary. + if (node.getNodeType() != NodeType.LEAF) { + return ValidateOf.inValid( + new ValidationError.DecodingExpectedLeafNodeType(path, node, this.name()) + ); + } + // Look for a class with the configured name. The class must be assignable to the + // class this decoder is expected to return via the type capture. + return node.getValue().map(className -> { + try { + Class configuredClass = Class.forName(className); + if (type.getFirstParameterType().isAssignableFrom(configuredClass)) { + return ValidateOf.>valid(configuredClass); + } + else { + return ValidateOf.>inValid( + new CannotCast(className, type.getFirstParameterType().getName()) + ); + } + } + catch (ClassNotFoundException e) { + return ValidateOf.>inValid(new ClassNotFound(className)); + } + }).orElse( + ValidateOf.>inValid( + new ValidationError.DecodingLeafMissingValue(path, this.name()) + ) + ); + } + + /** + * The configured class cannot be cast to the class expected by the decoder. + */ + public static class CannotCast + extends + ValidationError { + + private final String from; + private final String to; + + public CannotCast(String from, String to) { + super(ValidationLevel.ERROR); + this.from = from; + this.to = to; + } + + @Override + public String description() { + return "The class `" + this.from + "` cannot be cast to `" + this.to + "`."; + } + } + + /** + * The configured class cannot be found in the class path. + */ + public static class ClassNotFound + extends + ValidationError { + + private final String className; + + public ClassNotFound(String className) { + super(ValidationLevel.ERROR); + this.className = className; + } + + @Override + public String description() { + return "The class `" + this.className + "` cannot be found."; + } + } + +} diff --git a/opentcs-impl-configuration-gestalt/src/main/java/org/opentcs/configuration/gestalt/decoders/MapLiteralDecoder.java b/opentcs-impl-configuration-gestalt/src/main/java/org/opentcs/configuration/gestalt/decoders/MapLiteralDecoder.java new file mode 100644 index 0000000..135812f --- /dev/null +++ b/opentcs-impl-configuration-gestalt/src/main/java/org/opentcs/configuration/gestalt/decoders/MapLiteralDecoder.java @@ -0,0 +1,142 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.configuration.gestalt.decoders; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.github.gestalt.config.decoder.Decoder; +import org.github.gestalt.config.decoder.DecoderContext; +import org.github.gestalt.config.decoder.Priority; +import org.github.gestalt.config.entity.ValidationError; +import org.github.gestalt.config.entity.ValidationLevel; +import org.github.gestalt.config.node.ConfigNode; +import org.github.gestalt.config.node.LeafNode; +import org.github.gestalt.config.node.NodeType; +import org.github.gestalt.config.reflect.TypeCapture; +import org.github.gestalt.config.tag.Tags; +import org.github.gestalt.config.utils.ValidateOf; + +/** + * A decoder to read map literals in the form + * {@code =,=,...,=}, where the key-value pairs + * (i.e. map entries) are separated by commas as a delimiter. + */ +public class MapLiteralDecoder + implements + Decoder> { + + public MapLiteralDecoder() { + } + + @Override + public Priority priority() { + return Priority.HIGH; + } + + @Override + public String name() { + return MapLiteralDecoder.class.getName(); + } + + @Override + public boolean canDecode(String path, Tags tags, ConfigNode node, TypeCapture type) { + return node.getNodeType() == NodeType.LEAF + && Map.class.isAssignableFrom(type.getRawType()) + && type.getParameterTypes().size() == 2; + } + + @Override + public ValidateOf> decode( + String path, + Tags tags, + ConfigNode node, + TypeCapture type, + DecoderContext decoderContext + ) { + // This decoder only decodes nodes of type leaf. For other types the default decoders + // `ArrayDecoder` and `ObjectDecoder` will eventually call this decoder if necessary. + if (node.getNodeType() != NodeType.LEAF) { + return ValidateOf.inValid( + new ValidationError.DecodingExpectedLeafNodeType(path, node, this.name()) + ); + } + + if (node.getValue().isEmpty()) { + return ValidateOf.inValid( + new ValidationError.LeafNodesHaveNoValues(path) + ); + } + + List errors = new ArrayList<>(); + Map result = new HashMap<>(); + + // Split the node value on ',' to seperate it into `key=value` pairs and split those + // again into the `key` and `value`. Then decode the key and value to the required types. + for (String entry : node.getValue().get().split(",")) { + if (entry.isBlank()) { + continue; + } + + String[] keyValuePair = entry.split("="); + if (keyValuePair.length != 2) { + errors.add(new MapEntryFormatInvalid(entry)); + continue; + } + + // Decode the key string to the required key type. + ValidateOf key = decoderContext.getDecoderService() + .decodeNode( + path, + tags, + new LeafNode(keyValuePair[0].trim()), + type.getFirstParameterType(), + decoderContext + ); + if (key.hasErrors()) { + errors.addAll(key.getErrors()); + continue; + } + + // Decode the value string to the required value type. + ValidateOf value = decoderContext.getDecoderService() + .decodeNode( + path, + tags, + new LeafNode(keyValuePair[1].trim()), + type.getSecondParameterType(), + decoderContext + ); + if (value.hasErrors()) { + errors.addAll(value.getErrors()); + continue; + } + + result.put(key.results(), value.results()); + } + + return ValidateOf.validateOf(result, errors); + } + + /** + * A validation error for map entries not in the format {@code =}. + */ + public static class MapEntryFormatInvalid + extends + ValidationError { + + private final String rawEntry; + + public MapEntryFormatInvalid(String rawEntry) { + super(ValidationLevel.ERROR); + this.rawEntry = rawEntry; + } + + @Override + public String description() { + return "Map entry is not in the format '=':" + rawEntry; + } + } + +} diff --git a/opentcs-impl-configuration-gestalt/src/test/java/org/opentcs/configuration/gestalt/DummyClass.java b/opentcs-impl-configuration-gestalt/src/test/java/org/opentcs/configuration/gestalt/DummyClass.java new file mode 100644 index 0000000..b99b92d --- /dev/null +++ b/opentcs-impl-configuration-gestalt/src/test/java/org/opentcs/configuration/gestalt/DummyClass.java @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.configuration.gestalt; + +import java.util.Objects; + +/** + * A dummy class for testing sample configuration entries. + */ +public class DummyClass { + + private final String name; + private final String surname; + private final int age; + + public DummyClass() { + name = ""; + surname = ""; + age = 0; + } + + public DummyClass(String paramString) { + String[] split = paramString.split("\\|", 3); + name = split[0]; + surname = split[1]; + age = Integer.parseInt(split[2]); + } + + public DummyClass(String name, String surname, int age) { + this.name = name; + this.surname = surname; + this.age = age; + } + + public String getName() { + return name; + } + + public String getSurname() { + return surname; + } + + public int getAge() { + return age; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof DummyClass)) { + return false; + } + DummyClass other = (DummyClass) o; + return name.equals(other.name) && surname.equals(other.surname) && age == other.age; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 23 * hash + Objects.hashCode(this.name); + hash = 23 * hash + Objects.hashCode(this.surname); + hash = 23 * hash + this.age; + return hash; + } + + @Override + public String toString() { + return getName() + " - " + getSurname() + ":" + getAge(); + } +} diff --git a/opentcs-impl-configuration-gestalt/src/test/java/org/opentcs/configuration/gestalt/SampleConfigurationTest.java b/opentcs-impl-configuration-gestalt/src/test/java/org/opentcs/configuration/gestalt/SampleConfigurationTest.java new file mode 100644 index 0000000..8120c55 --- /dev/null +++ b/opentcs-impl-configuration-gestalt/src/test/java/org/opentcs/configuration/gestalt/SampleConfigurationTest.java @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.configuration.gestalt; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.net.URISyntaxException; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.configuration.ConfigurationBindingProvider; + +/** + * Tests for reading configuration entries with gestalt. + */ +public class SampleConfigurationTest { + + private ConfigurationBindingProvider input; + + @BeforeEach + void setUp() { + input = gestaltConfigurationBindingProvider(); + } + + @Test + void testBoolean() { + SampleConfig config = input.get(SampleConfig.PREFIX, SampleConfig.class); + assertThat(config.simpleBoolean(), is(true)); + } + + @Test + void testInteger() { + SampleConfig config = input.get(SampleConfig.PREFIX, SampleConfig.class); + assertThat(config.simpleInteger(), is(600)); + } + + @Test + void testString() { + SampleConfig config = input.get(SampleConfig.PREFIX, SampleConfig.class); + assertThat(config.simpleString(), is("HelloWorld")); + } + + @Test + void testEnum() { + SampleConfig config = input.get(SampleConfig.PREFIX, SampleConfig.class); + assertThat(config.simpleEnum(), is(SampleConfigurationTest.DummyEnum.ORDER)); + } + + @Test + void testStringList() { + SampleConfig config = input.get(SampleConfig.PREFIX, SampleConfig.class); + assertThat(config.stringList(), equalTo(List.of("A", "B", "C"))); + } + + @Test + void testStringMap() { + SampleConfig config = input.get(SampleConfig.PREFIX, SampleConfig.class); + assertThat(config.stringMap(), equalTo(Map.of("A", "1", "B", "2", "C", "3"))); + } + + @Test + void testEnumMap() { + SampleConfig config = input.get(SampleConfig.PREFIX, SampleConfig.class); + assertThat(config.enumMap(), equalTo(Map.of(DummyEnum.ORDER, "1", DummyEnum.POINT, "2"))); + } + + @Test + void testObjectList() { + SampleConfig config = input.get(SampleConfig.PREFIX, SampleConfig.class); + assertThat( + config.objectList(), equalTo( + List.of( + new DummyClass("A", "B", 1), + new DummyClass("C", "D", 2) + ) + ) + ); + } + + @Test + void testStringConstructor() { + SampleConfig config = input.get(SampleConfig.PREFIX, SampleConfig.class); + assertThat(config.stringConstructor(), equalTo(new DummyClass("A", "B", 1))); + } + + @Test + void testClassPath() { + SampleConfig config = input.get(SampleConfig.PREFIX, SampleConfig.class); + assertThat(config.classPath(), is(DummyClass.class)); + } + + private static ConfigurationBindingProvider gestaltConfigurationBindingProvider() { + try { + return new GestaltConfigurationBindingProvider( + Paths.get( + Thread.currentThread().getContextClassLoader() + .getResource("org/opentcs/configuration/gestalt/sampleConfig.properties").toURI() + ) + ); + } + catch (URISyntaxException ex) { + Logger.getLogger(SampleConfigurationTest.class.getName()).log(Level.SEVERE, null, ex); + assertFalse(true); + return null; + } + } + + public interface SampleConfig { + + /** + * This configuration's prefix. + */ + String PREFIX = "sampleConfig"; + + boolean simpleBoolean(); + + int simpleInteger(); + + String simpleString(); + + SampleConfigurationTest.DummyEnum simpleEnum(); + + List stringList(); + + Map stringMap(); + + Map enumMap(); + + List objectList(); + + DummyClass stringConstructor(); + + Class classPath(); + + } + + public enum DummyEnum { + /** + * Vehicle. + */ + VEHICLE, + /** + * Point. + */ + POINT, + /** + * Order. + */ + ORDER + } +} diff --git a/opentcs-impl-configuration-gestalt/src/test/java/org/opentcs/configuration/gestalt/decoders/MapLiteralDecoderTest.java b/opentcs-impl-configuration-gestalt/src/test/java/org/opentcs/configuration/gestalt/decoders/MapLiteralDecoderTest.java new file mode 100644 index 0000000..c3e9c72 --- /dev/null +++ b/opentcs-impl-configuration-gestalt/src/test/java/org/opentcs/configuration/gestalt/decoders/MapLiteralDecoderTest.java @@ -0,0 +1,167 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.configuration.gestalt.decoders; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.github.gestalt.config.Gestalt; +import org.github.gestalt.config.builder.GestaltBuilder; +import org.github.gestalt.config.decoder.DecoderContext; +import org.github.gestalt.config.decoder.DecoderRegistry; +import org.github.gestalt.config.decoder.DecoderService; +import org.github.gestalt.config.exceptions.GestaltConfigurationException; +import org.github.gestalt.config.exceptions.GestaltException; +import org.github.gestalt.config.lexer.SentenceLexer; +import org.github.gestalt.config.node.ConfigNodeService; +import org.github.gestalt.config.node.LeafNode; +import org.github.gestalt.config.path.mapper.StandardPathMapper; +import org.github.gestalt.config.reflect.TypeCapture; +import org.github.gestalt.config.source.MapConfigSourceBuilder; +import org.github.gestalt.config.tag.Tags; +import org.github.gestalt.config.utils.ValidateOf; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests the map literal decoder {@link MapLiteralDecoder} for Gestalt. + */ +class MapLiteralDecoderTest { + + private ConfigNodeService configNodeService; + private SentenceLexer lexer; + private DecoderService decoderService; + + @BeforeEach + void setup() + throws GestaltConfigurationException { + configNodeService = mock(ConfigNodeService.class); + lexer = mock(SentenceLexer.class); + decoderService = new DecoderRegistry( + Collections.singletonList(new MapLiteralDecoder()), + configNodeService, + lexer, + List.of(new StandardPathMapper()) + ); + } + + @Test + void shouldDecodeMapLiteral() + throws GestaltException { + Map config = Map.of("entry_path", "AAAA=1, BBBB=2, CCCC=3"); + Gestalt gestalt = buildGestaltConfig(config); + + Map result = gestalt.getConfig( + "entry_path", + new TypeCapture>() { + } + ); + + assertEquals(result.get("AAAA"), 1); + assertEquals(result.get("BBBB"), 2); + assertEquals(result.get("CCCC"), 3); + } + + @Test + void shouldDecodeMapLiteralToEnum() + throws GestaltException { + Map config = Map.of("entry_path", "Foo=1, Bar=2, Baz=3"); + Gestalt gestalt = buildGestaltConfig(config); + + Map result = gestalt.getConfig( + "entry_path", + new TypeCapture>() { + } + ); + + assertEquals(result.get(Things.Foo), 1); + assertEquals(result.get(Things.Bar), 2); + assertEquals(result.get(Things.Baz), 3); + } + + @Test + void emptyLeafShouldGiveAnEmptyMap() + throws GestaltException { + Map config = Map.of("entry_path", ""); + Gestalt gestalt = buildGestaltConfig(config); + + Map result = gestalt.getConfig( + "entry_path", + new TypeCapture>() { + } + ); + + assertTrue(result.isEmpty()); + } + + @Test + void shouldGiveErrorWhenWrongDelimiterIsUsed() { + MapLiteralDecoder decoder = new MapLiteralDecoder(); + + ValidateOf> result = decoder.decode( + "entry_path", + Tags.of(), + new LeafNode("AAAA=1; BBBB=2; CCCC=3"), + new TypeCapture>() { + }, + new DecoderContext(decoderService, null) + ); + assertThat(result.getErrors(), hasSize(1)); + assertThat( + result.getErrors().get(0), + instanceOf(MapLiteralDecoder.MapEntryFormatInvalid.class) + ); + } + + @Test + void shouldGiveErrorWhenWrongAsignmentIsUsed() { + MapLiteralDecoder decoder = new MapLiteralDecoder(); + + ValidateOf> result = decoder.decode( + "entry_path", + Tags.of(), + new LeafNode("AAAA~1, BBBB~2, CCCC~3"), + new TypeCapture>() { + }, + new DecoderContext(decoderService, null) + ); + assertThat(result.getErrors(), hasSize(3)); + assertThat( + result.getErrors().get(0), + instanceOf(MapLiteralDecoder.MapEntryFormatInvalid.class) + ); + assertThat( + result.getErrors().get(1), + instanceOf(MapLiteralDecoder.MapEntryFormatInvalid.class) + ); + assertThat( + result.getErrors().get(2), + instanceOf(MapLiteralDecoder.MapEntryFormatInvalid.class) + ); + } + + private enum Things { + Foo, + Bar, + Baz + } + + private Gestalt buildGestaltConfig(Map config) + throws GestaltException { + Gestalt gestalt = new GestaltBuilder() + .addDefaultDecoders() + .addDecoder(new ClassPathDecoder()) + .addDecoder(new MapLiteralDecoder()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(config).build()) + .build(); + gestalt.loadConfigs(); + return gestalt; + } +} diff --git a/opentcs-impl-configuration-gestalt/src/test/resources/org/opentcs/configuration/gestalt/sampleConfig.properties b/opentcs-impl-configuration-gestalt/src/test/resources/org/opentcs/configuration/gestalt/sampleConfig.properties new file mode 100644 index 0000000..46192d0 --- /dev/null +++ b/opentcs-impl-configuration-gestalt/src/test/resources/org/opentcs/configuration/gestalt/sampleConfig.properties @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +sampleConfig.simpleBoolean = true +sampleConfig.simpleInteger = 600 +sampleConfig.simpleString = HelloWorld +sampleConfig.simpleEnum= ORDER +sampleConfig.stringList = A,B,C +sampleConfig.stringMap = A=1,B=2,C=3 +sampleConfig.enumMap = ORDER=1,POINT=2 +sampleConfig.objectList = A|B|1,C|D|2 +sampleConfig.stringConstructor = A|B|1 +sampleConfig.classPath = org.opentcs.configuration.gestalt.DummyClass diff --git a/opentcs-kernel-extension-http-services/build.gradle b/opentcs-kernel-extension-http-services/build.gradle new file mode 100644 index 0000000..b607004 --- /dev/null +++ b/opentcs-kernel-extension-http-services/build.gradle @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply from: "${rootDir}/gradle/java-project.gradle" +apply from: "${rootDir}/gradle/java-codequality.gradle" +apply from: "${rootDir}/gradle/guice-project.gradle" +apply from: "${rootDir}/gradle/publishing-java.gradle" + +dependencies { + api project(':opentcs-api-injection') + api project(':opentcs-common') + + api group: 'com.sparkjava', name: 'spark-core', version: '2.9.4' + + api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.18.0' + api group: 'com.fasterxml.jackson.module', name: 'jackson-module-jsonSchema', version: '2.18.0' + api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.18.0' +} + +task release { + dependsOn build +} diff --git a/opentcs-kernel-extension-http-services/gradle.properties b/opentcs-kernel-extension-http-services/gradle.properties new file mode 100644 index 0000000..14431ac --- /dev/null +++ b/opentcs-kernel-extension-http-services/gradle.properties @@ -0,0 +1,40 @@ +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineMethodParams=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAfterDotInChainedMethodCalls=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineDisjunctiveCatchTypes=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineFor=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineImplements=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.sortMembersByVisibility=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.visibilityOrder=PUBLIC;PROTECTED;DEFAULT;PRIVATE +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.enable-indent=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineArrayInit=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineCallArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.keepGettersAndSettersTogether=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classMembersOrder=STATIC FIELD;FIELD;STATIC_INIT;CONSTRUCTOR;INSTANCE_INIT;STATIC METHOD;METHOD;STATIC CLASS;CLASS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapCommentText=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.importGroupsOrder=* +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize=4 +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineAnnotationArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineTryResources=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.preserveNewLinesInComments=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineParenthesized=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineThrows=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=100 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project diff --git a/opentcs-kernel-extension-http-services/src/guiceConfig/java/org/opentcs/kernel/extensions/adminwebapi/AdminWebApiModule.java b/opentcs-kernel-extension-http-services/src/guiceConfig/java/org/opentcs/kernel/extensions/adminwebapi/AdminWebApiModule.java new file mode 100644 index 0000000..7cdefcc --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/guiceConfig/java/org/opentcs/kernel/extensions/adminwebapi/AdminWebApiModule.java @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.adminwebapi; + +import jakarta.inject.Singleton; +import org.opentcs.customizations.kernel.KernelInjectionModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Configures the admin web API extension. + */ +public class AdminWebApiModule + extends + KernelInjectionModule { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(AdminWebApiModule.class); + + /** + * Creates a new instance. + */ + public AdminWebApiModule() { + } + + @Override + protected void configure() { + AdminWebApiConfiguration configuration + = getConfigBindingProvider().get( + AdminWebApiConfiguration.PREFIX, + AdminWebApiConfiguration.class + ); + + if (!configuration.enable()) { + LOG.info("Admin web API disabled by configuration."); + return; + } + + bind(AdminWebApiConfiguration.class) + .toInstance(configuration); + + extensionsBinderAllModes().addBinding() + .to(AdminWebApi.class) + .in(Singleton.class); + } +} diff --git a/opentcs-kernel-extension-http-services/src/guiceConfig/java/org/opentcs/kernel/extensions/servicewebapi/ServiceWebApiModule.java b/opentcs-kernel-extension-http-services/src/guiceConfig/java/org/opentcs/kernel/extensions/servicewebapi/ServiceWebApiModule.java new file mode 100644 index 0000000..7183864 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/guiceConfig/java/org/opentcs/kernel/extensions/servicewebapi/ServiceWebApiModule.java @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi; + +import jakarta.inject.Singleton; +import org.opentcs.customizations.kernel.KernelInjectionModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Configures the service web API extension. + */ +public class ServiceWebApiModule + extends + KernelInjectionModule { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ServiceWebApiModule.class); + + /** + * Creates a new instance. + */ + public ServiceWebApiModule() { + } + + @Override + protected void configure() { + ServiceWebApiConfiguration configuration + = getConfigBindingProvider().get( + ServiceWebApiConfiguration.PREFIX, + ServiceWebApiConfiguration.class + ); + + if (!configuration.enable()) { + LOG.info("Service web API disabled by configuration."); + return; + } + + bind(ServiceWebApiConfiguration.class) + .toInstance(configuration); + + extensionsBinderAllModes().addBinding() + .to(ServiceWebApi.class) + .in(Singleton.class); + } +} diff --git a/opentcs-kernel-extension-http-services/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.kernel.KernelInjectionModule b/opentcs-kernel-extension-http-services/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.kernel.KernelInjectionModule new file mode 100644 index 0000000..e42e2ca --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.kernel.KernelInjectionModule @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT + +org.opentcs.kernel.extensions.adminwebapi.AdminWebApiModule +org.opentcs.kernel.extensions.servicewebapi.ServiceWebApiModule diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/adminwebapi/AdminWebApi.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/adminwebapi/AdminWebApi.java new file mode 100644 index 0000000..730c00c --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/adminwebapi/AdminWebApi.java @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.adminwebapi; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.KernelExtension; +import org.opentcs.kernel.extensions.adminwebapi.v1.V1RequestHandler; +import org.opentcs.kernel.extensions.servicewebapi.HttpConstants; +import spark.Service; + +/** + * Provides an HTTP interface for basic administration needs. + */ +public class AdminWebApi + implements + KernelExtension { + + /** + * The interface configuration. + */ + private final AdminWebApiConfiguration configuration; + /** + * Handles requests for API version 1. + */ + private final V1RequestHandler v1RequestHandler; + /** + * The actual HTTP service. + */ + private Service service; + /** + * Whether this kernel extension is initialized. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param configuration The interface configuration. + * @param v1RequestHandler Handles requests for API version 1. + */ + @Inject + public AdminWebApi( + AdminWebApiConfiguration configuration, + V1RequestHandler v1RequestHandler + ) { + this.configuration = requireNonNull(configuration, "configuration"); + this.v1RequestHandler = requireNonNull(v1RequestHandler, "v1RequestHandler"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + service = Service.ignite() + .ipAddress(configuration.bindAddress()) + .port(configuration.bindPort()); + + service.path("/v1", () -> { + service.get("/version", v1RequestHandler::handleGetVersion); + service.get("/status", v1RequestHandler::handleGetStatus); + service.delete("/kernel", v1RequestHandler::handleDeleteKernel); + } + ); + service.exception(IllegalArgumentException.class, (exception, request, response) -> { + response.status(400); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + response.body(exception.getMessage()); + }); + service.exception(IllegalStateException.class, (exception, request, response) -> { + response.status(500); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + response.body(exception.getMessage()); + }); + + initialized = true; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + service.stop(); + + initialized = false; + } + + @Override + public boolean isInitialized() { + return initialized; + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/adminwebapi/AdminWebApiConfiguration.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/adminwebapi/AdminWebApiConfiguration.java new file mode 100644 index 0000000..8c61bd9 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/adminwebapi/AdminWebApiConfiguration.java @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.adminwebapi; + +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; + +/** + * Configuration entries for the administration web API. + */ +@ConfigurationPrefix(AdminWebApiConfiguration.PREFIX) +public interface AdminWebApiConfiguration { + + /** + * The prefix for all configuration entries here. + */ + String PREFIX = "adminwebapi"; + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to enable the admin interface.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0" + ) + boolean enable(); + + @ConfigurationEntry( + type = "IP address", + description = "Address to which to bind the HTTP server, e.g. 0.0.0.0. (Default: 127.0.0.1.)", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "1" + ) + String bindAddress(); + + @ConfigurationEntry( + type = "Integer", + description = "Port to which to bind the HTTP server.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "2" + ) + int bindPort(); + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/adminwebapi/v1/Status.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/adminwebapi/v1/Status.java new file mode 100644 index 0000000..b8a9bd2 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/adminwebapi/v1/Status.java @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.adminwebapi.v1; + +/** + * Describes the kernel process's current status. + */ +public class Status { + + private String heapSize = String.valueOf(Runtime.getRuntime().totalMemory()); + + private String maxHeapSize = String.valueOf(Runtime.getRuntime().maxMemory()); + + private String freeInHeap = String.valueOf(Runtime.getRuntime().freeMemory()); + + public Status() { + } + + public String getHeapSize() { + return heapSize; + } + + public void setHeapSize(String heapSize) { + this.heapSize = heapSize; + } + + public String getMaxHeapSize() { + return maxHeapSize; + } + + public void setMaxHeapSize(String maxHeapSize) { + this.maxHeapSize = maxHeapSize; + } + + public String getFreeInHeap() { + return freeInHeap; + } + + public void setFreeInHeap(String freeInHeap) { + this.freeInHeap = freeInHeap; + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/adminwebapi/v1/V1RequestHandler.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/adminwebapi/v1/V1RequestHandler.java new file mode 100644 index 0000000..dc942c1 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/adminwebapi/v1/V1RequestHandler.java @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.adminwebapi.v1; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import jakarta.inject.Inject; +import java.io.IOException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.opentcs.access.Kernel; +import org.opentcs.access.LocalKernel; +import org.opentcs.components.Lifecycle; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Request; +import spark.Response; + +/** + * Handles requests and produces responses for version 1 of the admin web API. + */ +public class V1RequestHandler + implements + Lifecycle { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(V1RequestHandler.class); + /** + * Maps between objects and their JSON representations. + */ + private final ObjectMapper objectMapper + = new ObjectMapper() + .registerModule(new JavaTimeModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + /** + * The local kernel. + */ + private final LocalKernel kernel; + /** + * Used to schedule kernel shutdowns. + */ + private final ScheduledExecutorService kernelExecutor; + /** + * Whether this instance is initialized. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param kernel The local kernel. + * @param kernelExecutor Use to schedule kernel shutdowns. + */ + @Inject + public V1RequestHandler( + LocalKernel kernel, + @KernelExecutor + ScheduledExecutorService kernelExecutor + ) { + this.kernel = requireNonNull(kernel, "kernel"); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + initialized = false; + } + + public Object handleGetVersion(Request request, Response response) { + return toJson(new Version()); + } + + public Object handleGetStatus(Request request, Response response) { + return toJson(new Status()); + } + + public Object handleDeleteKernel(Request request, Response response) { + LOG.info("Initiating kernel shutdown as requested from {}...", request.ip()); + kernelExecutor.schedule(() -> kernel.setState(Kernel.State.SHUTDOWN), 1, TimeUnit.SECONDS); + return ""; + } + + private T fromJson(String jsonString, Class clazz) + throws IllegalArgumentException { + try { + return objectMapper.readValue(jsonString, clazz); + } + catch (IOException exc) { + throw new IllegalArgumentException("Could not parse JSON input", exc); + } + } + + private String toJson(Object object) + throws IllegalStateException { + try { + return objectMapper + .writerWithDefaultPrettyPrinter() + .writeValueAsString(object); + } + catch (JsonProcessingException exc) { + throw new IllegalStateException("Could not produce JSON output", exc); + } + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/adminwebapi/v1/Version.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/adminwebapi/v1/Version.java new file mode 100644 index 0000000..1cc64a0 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/adminwebapi/v1/Version.java @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.adminwebapi.v1; + +import org.opentcs.util.Environment; + +/** + * Describes the version of the running kernel. + */ +public class Version { + + private String baselineVersion = Environment.getBaselineVersion(); + + private String customizationName = Environment.getCustomizationName(); + + private String customizationVersion = Environment.getCustomizationVersion(); + + public Version() { + } + + public String getBaselineVersion() { + return baselineVersion; + } + + public void setBaselineVersion(String baselineVersion) { + this.baselineVersion = baselineVersion; + } + + public String getCustomizationName() { + return customizationName; + } + + public void setCustomizationName(String customizationName) { + this.customizationName = customizationName; + } + + public String getCustomizationVersion() { + return customizationVersion; + } + + public void setCustomizationVersion(String customizationVersion) { + this.customizationVersion = customizationVersion; + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/Authenticator.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/Authenticator.java new file mode 100644 index 0000000..7e2897b --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/Authenticator.java @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi; + +import static java.util.Objects.requireNonNull; + +import com.google.common.base.Strings; +import jakarta.inject.Inject; +import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Request; + +/** + * Authenticates incoming requests. + */ +public class Authenticator { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(Authenticator.class); + /** + * Defines the required access rules. + */ + private final ServiceWebApiConfiguration configuration; + + /** + * Creates a new instance. + * + * @param configuration Defines the required access rules. + */ + @Inject + public Authenticator(ServiceWebApiConfiguration configuration) { + this.configuration = requireNonNull(configuration, "configuration"); + } + + /** + * Checks whether authentication is required and the given request is authenticated. + * + * @param request The request to be checked. + * @return true if, and only if, authentication is required and the given request is + * authenticated. + */ + public boolean isAuthenticated(Request request) { + requireNonNull(request, "request"); + + String requestAccessKey = request.headers(HttpConstants.HEADER_NAME_ACCESS_KEY); + LOG.debug( + "Provided access key in header is '{}', required value is '{}'", + requestAccessKey, + configuration.accessKey() + ); + + // Any empty access key indicates authentication is not required. + if (Strings.isNullOrEmpty(configuration.accessKey())) { + LOG.debug("No access key, authentication not required."); + return true; + } + + return Objects.equals(requestAccessKey, configuration.accessKey()); + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/HttpConstants.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/HttpConstants.java new file mode 100644 index 0000000..0fd401d --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/HttpConstants.java @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi; + +/** + * Defines some HTTP-related constants. + */ +public class HttpConstants { + + /** + * Name of the header that is expected to contain the API access keys. + */ + public static final String HEADER_NAME_ACCESS_KEY = "X-Api-Access-Key"; + /** + * Content type for plain text. + */ + public static final String CONTENT_TYPE_TEXT_PLAIN_UTF8 = "text/plain; charset=utf-8"; + /** + * Content type for JSON structures. + */ + public static final String CONTENT_TYPE_APPLICATION_JSON_UTF8 = "application/json; charset=utf-8"; + + /** + * Prevents instantiation. + */ + private HttpConstants() { + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/JsonBinder.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/JsonBinder.java new file mode 100644 index 0000000..9b8635c --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/JsonBinder.java @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.io.IOException; + +/** + * Binds JSON strings to objects and vice versa. + */ +public class JsonBinder { + + /** + * Maps between objects and their JSON representations. + */ + private final ObjectMapper objectMapper + = new ObjectMapper() + .registerModule(new JavaTimeModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + /** + * Creates a new instance. + */ + public JsonBinder() { + } + + /** + * Maps the given JSON string to an object. + * + * @param The type of object to map to. + * @param jsonString The JSON string. + * @param clazz The type of object to map to. + * @return The object created from the JSON string. + * @throws IllegalArgumentException In case there was a problem mapping the given object from + * JSON. + * (An IllegalArgumentException is mapped to HTTP status code 400, indicating a client error.) + */ + public T fromJson(String jsonString, Class clazz) + throws IllegalArgumentException { + try { + return objectMapper.readValue(jsonString, clazz); + } + catch (IOException exc) { + throw new IllegalArgumentException("Could not parse JSON input", exc); + } + } + + /** + * Maps the given object to a JSON string. + * + * @param object The object to be mapped. + * @return The JSON string representation of the object. + * @throws IllegalStateException In case there was a problem mapping the given object to JSON. + * (An IllegalStateException is mapped to HTTP status code 500, indicating an internal error.) + */ + public String toJson(Object object) + throws IllegalStateException { + try { + return objectMapper + .writerWithDefaultPrettyPrinter() + .writeValueAsString(object); + } + catch (JsonProcessingException exc) { + throw new IllegalStateException("Could not produce JSON output", exc); + } + } + + /** + * Maps the given throwable to a JSON string. + * + * @param t The throwable to be mapped. + * @return A JSON string for the given throwable, consisting of a single-element array containing + * the throwable's message. + * @throws IllegalStateException In case there was a problem mapping the given object to JSON. + * (An IllegalStateException is mapped to HTTP status code 500, indicating an internal error.) + */ + public String toJson(Throwable t) + throws IllegalStateException { + try { + return objectMapper + .writerWithDefaultPrettyPrinter() + .writeValueAsString(objectMapper.createArrayNode().add(t.getMessage())); + } + catch (JsonProcessingException exc) { + throw new IllegalStateException("Could not produce JSON output", exc); + } + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/KernelExecutorWrapper.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/KernelExecutorWrapper.java new file mode 100644 index 0000000..27fa4d2 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/KernelExecutorWrapper.java @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.customizations.kernel.KernelExecutor; + +/** + * Calls callables/runnables via the kernel executor and waits for the outcome. + */ +public class KernelExecutorWrapper { + + private final ExecutorService kernelExecutor; + + /** + * Creates a new instance. + * + * @param kernelExecutor The kernel executor. + */ + @Inject + public KernelExecutorWrapper( + @KernelExecutor + ExecutorService kernelExecutor + ) { + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + } + + /** + * Calls the given callable via the kernel executor and waits for the outcome. + * + * @param The callable's return type. + * @param callable The callable. + * @return The result of the call. + * @throws IllegalStateException In case the call via the kernel executor was unexpectedly + * interrupted. + * @throws RuntimeException In case an exception was thrown from the callable. If the exception + * thrown is a {@code RuntimeException}, it is forwarded directly; if it is not a + * {@code RuntimeException}, it is wrapped in a {@link KernelRuntimeException}. + */ + public T callAndWait(Callable callable) + throws IllegalStateException, + RuntimeException { + requireNonNull(callable, "callable"); + + try { + return kernelExecutor.submit(callable).get(); + } + catch (InterruptedException exc) { + throw new IllegalStateException("Unexpectedly interrupted"); + } + catch (ExecutionException exc) { + if (exc.getCause() instanceof RuntimeException) { + throw (RuntimeException) exc.getCause(); + } + throw new KernelRuntimeException(exc.getCause()); + } + } + + /** + * Calls the given runnable via the kernel executor and waits for the outcome. + * + * @param runnable The runnable. + * @throws IllegalStateException In case the call via the kernel executor was unexpectedly + * interrupted. + * @throws RuntimeException In case an exception was thrown from the runnable. If the exception + * thrown is a {@code RuntimeException}, it is forwarded directly; if it is not a + * {@code RuntimeException}, it is wrapped in a {@link KernelRuntimeException}. + */ + public void callAndWait(Runnable runnable) + throws IllegalStateException, + RuntimeException { + requireNonNull(runnable, "runnable"); + + callAndWait(Executors.callable(runnable)); + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/RequestHandler.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/RequestHandler.java new file mode 100644 index 0000000..48410bd --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/RequestHandler.java @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi; + +import org.opentcs.components.Lifecycle; +import spark.Service; + +/** + * A request handler. + */ +public interface RequestHandler + extends + Lifecycle { + + /** + * Registers the handler's routes with the given service. + * + * @param service The service to register the routes with. + */ + void addRoutes(Service service); +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/ServiceWebApi.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/ServiceWebApi.java new file mode 100644 index 0000000..d95d0c2 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/ServiceWebApi.java @@ -0,0 +1,187 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi; + +import static java.util.Objects.requireNonNull; + +import com.google.common.util.concurrent.Uninterruptibles; +import jakarta.inject.Inject; +import java.util.concurrent.TimeUnit; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.SslParameterSet; +import org.opentcs.components.kernel.KernelExtension; +import org.opentcs.data.ObjectExistsException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.kernel.extensions.servicewebapi.v1.V1RequestHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Service; + +/** + * Provides an HTTP interface for basic administration needs. + */ +public class ServiceWebApi + implements + KernelExtension { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ServiceWebApi.class); + /** + * The interface configuration. + */ + private final ServiceWebApiConfiguration configuration; + /** + * Authenticates incoming requests. + */ + private final Authenticator authenticator; + /** + * Handles requests for API version 1. + */ + private final V1RequestHandler v1RequestHandler; + /** + * Binds JSON data to objects and vice versa. + */ + private final JsonBinder jsonBinder; + /** + * The connection encryption configuration. + */ + private final SslParameterSet sslParamSet; + /** + * The actual HTTP service. + */ + private Service service; + /** + * Whether this kernel extension is initialized. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param configuration The interface configuration. + * @param sslParamSet The SSL parameter set. + * @param authenticator Authenticates incoming requests. + * @param jsonBinder Binds JSON data to objects and vice versa. + * @param v1RequestHandler Handles requests for API version 1. + */ + @Inject + public ServiceWebApi( + ServiceWebApiConfiguration configuration, + SslParameterSet sslParamSet, + Authenticator authenticator, + JsonBinder jsonBinder, + V1RequestHandler v1RequestHandler + ) { + this.configuration = requireNonNull(configuration, "configuration"); + this.sslParamSet = requireNonNull(sslParamSet, "sslParamSet"); + this.authenticator = requireNonNull(authenticator, "authenticator"); + this.jsonBinder = requireNonNull(jsonBinder, "jsonBinder"); + this.v1RequestHandler = requireNonNull(v1RequestHandler, "v1RequestHandler"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + v1RequestHandler.initialize(); + + service = Service.ignite() + .ipAddress(configuration.bindAddress()) + .port(configuration.bindPort()); + + if (configuration.useSsl()) { + service.secure( + sslParamSet.getKeystoreFile().getAbsolutePath(), + sslParamSet.getKeystorePassword(), + null, + null + ); + } + else { + LOG.warn("Encryption disabled, connections will not be secured!"); + } + + service.before((request, response) -> { + if (!authenticator.isAuthenticated(request)) { + // Delay the response a bit to slow down brute force attacks. + Uninterruptibles.sleepUninterruptibly(2, TimeUnit.SECONDS); + service.halt(403, "Not authenticated."); + } + + // Add a CORS header to allow cross-origin requests from all hosts. + // This also makes using the "try it out" buttons in the Swagger UI documentation possible. + response.header("Access-Control-Allow-Origin", "*"); + }); + + // Reflect that we allow cross-origin requests for any headers and methods. + service.options( + "/*", + (request, response) -> { + String accessControlRequestHeaders = request.headers("Access-Control-Request-Headers"); + if (accessControlRequestHeaders != null) { + response.header("Access-Control-Allow-Headers", accessControlRequestHeaders); + } + + String accessControlRequestMethod = request.headers("Access-Control-Request-Method"); + if (accessControlRequestMethod != null) { + response.header("Access-Control-Allow-Methods", accessControlRequestMethod); + } + + return "OK"; + } + ); + + // Register routes for API versions here. + service.path("/v1", () -> v1RequestHandler.addRoutes(service)); + + service.exception(IllegalArgumentException.class, (exception, request, response) -> { + response.status(400); + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + response.body(jsonBinder.toJson(exception)); + }); + service.exception(ObjectUnknownException.class, (exception, request, response) -> { + response.status(404); + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + response.body(jsonBinder.toJson(exception)); + }); + service.exception(ObjectExistsException.class, (exception, request, response) -> { + response.status(409); + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + response.body(jsonBinder.toJson(exception)); + }); + service.exception(KernelRuntimeException.class, (exception, request, response) -> { + response.status(500); + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + response.body(jsonBinder.toJson(exception)); + }); + service.exception(IllegalStateException.class, (exception, request, response) -> { + response.status(500); + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + response.body(jsonBinder.toJson(exception)); + }); + + initialized = true; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + v1RequestHandler.terminate(); + service.stop(); + + initialized = false; + } + + @Override + public boolean isInitialized() { + return initialized; + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/ServiceWebApiConfiguration.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/ServiceWebApiConfiguration.java new file mode 100644 index 0000000..67ab524 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/ServiceWebApiConfiguration.java @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi; + +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; + +/** + * Configuration entries for the service web API. + */ +@ConfigurationPrefix(ServiceWebApiConfiguration.PREFIX) +public interface ServiceWebApiConfiguration { + + /** + * The prefix for all configuration entries here. + */ + String PREFIX = "servicewebapi"; + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to enable the interface.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0" + ) + boolean enable(); + + @ConfigurationEntry( + type = "IP address", + description = "Address to which to bind the HTTP server, e.g. 0.0.0.0 or 127.0.0.1.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "1" + ) + String bindAddress(); + + @ConfigurationEntry( + type = "Integer", + description = "Port to which to bind the HTTP server.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "2" + ) + int bindPort(); + + @ConfigurationEntry( + type = "String", + description = "Key allowing access to the API.", + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "3" + ) + String accessKey(); + + @ConfigurationEntry( + type = "Integer", + description = "Maximum number of status events to be kept.", + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "4" + ) + int statusEventsCapacity(); + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to use SSL to encrypt connections.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "5" + ) + boolean useSsl(); +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/Filters.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/Filters.java new file mode 100644 index 0000000..5b94e05 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/Filters.java @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import jakarta.annotation.Nullable; +import java.util.Objects; +import java.util.function.Predicate; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.peripherals.PeripheralJob; + +/** + * Provides some commonly used object filters. + */ +public class Filters { + + /** + * Prevents instantiation. + */ + private Filters() { + } + + /** + * Returns a predicate that is true only for transport orders whose intended vehicle is the given + * one. + * In case the given vehicle reference is null, all transport orders are accepted. + * + * @param vehicleRef The vehicle reference. + * @return A predicate that is true only for transport orders whose intended vehicle is the given + * one. + */ + public static Predicate transportOrderWithIntendedVehicle( + @Nullable + TCSObjectReference vehicleRef + ) { + return vehicleRef == null + ? order -> true + : order -> Objects.equals(vehicleRef, order.getIntendedVehicle()); + } + + /** + * Returns a predicate that is true only for order sequences whose intended vehicle is the given + * one. + * In case the given vehicle reference is null, all order sequences are accepted. + * + * @param vehicleRef The vehicle reference. + * @return A predicate that is true only for order sequences whose intended vehicle is the given + * one. + */ + public static Predicate orderSequenceWithIntendedVehicle( + @Nullable + TCSObjectReference vehicleRef + ) { + return vehicleRef == null + ? sequence -> true + : sequence -> Objects.equals(vehicleRef, sequence.getIntendedVehicle()); + } + + /** + * Returns a predicate that is true only for peripheral jobs whose related vehicle is the given + * one. + * In case the given vehicle reference is null, all peripheral jobs are accepted. + * + * @param vehicleRef The vehicle reference. + * @return A predicate that is true only for peripheral jobs whose related vehicle is the given + * one. + */ + public static Predicate peripheralJobWithRelatedVehicle( + @Nullable + TCSObjectReference vehicleRef + ) { + return vehicleRef == null + ? job -> true + : job -> Objects.equals(vehicleRef, job.getRelatedVehicle()); + } + + /** + * Returns a predicate that is true only for peripheral jobs whose related transport order is the + * given one. + * In case the given vehicle reference is null, all peripheral jobs are accepted. + * + * @param orderRef The transport order reference. + * @return A predicate that is true only for peripheral jobs whose related transport order is the + * given one. + */ + public static Predicate peripheralJobWithRelatedTransportOrder( + @Nullable + TCSObjectReference orderRef + ) { + return orderRef == null + ? job -> true + : job -> Objects.equals(orderRef, job.getRelatedTransportOrder()); + } + + /** + * Returns a predicate that is true only for vehicles whose processing state is the given one. + * In case the given procState is null, all vehicles are accepted. + * + * @param procState The processing state. + * @return A predicate that is true only for vehicles whose processing state is the given one. + */ + public static Predicate vehicleWithProcState( + @Nullable + Vehicle.ProcState procState + ) { + return procState == null + ? vehicle -> true + : vehicle -> Objects.equals(procState, vehicle.getProcState()); + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/LocationHandler.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/LocationHandler.java new file mode 100644 index 0000000..baf2572 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/LocationHandler.java @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import org.opentcs.components.kernel.services.PlantModelService; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.model.Location; +import org.opentcs.kernel.extensions.servicewebapi.KernelExecutorWrapper; + +/** + * Handles requests related to locations. + */ +public class LocationHandler { + + private final PlantModelService plantModelService; + private final KernelExecutorWrapper executorWrapper; + + /** + * Creates a new instance. + * + * @param plantModelService Used to retrieve and update location instances. + * @param executorWrapper Executes calls via the kernel executor and waits for the outcome. + */ + @Inject + public LocationHandler( + PlantModelService plantModelService, + KernelExecutorWrapper executorWrapper + ) { + this.plantModelService = requireNonNull(plantModelService, "plantModelService"); + this.executorWrapper = requireNonNull(executorWrapper, "executorWrapper"); + } + + /** + * Updates the locked state of the location with the given name. + * + * @param locationName The name of the location to update. + * @param lockedValue The location's new locked state (a boolean as a string). + * @throws ObjectUnknownException If a location with the given name could not be found. + */ + public void updateLocationLock( + @Nonnull + String locationName, + String lockedValue + ) + throws ObjectUnknownException { + executorWrapper.callAndWait(() -> { + Location location = plantModelService.fetchObject(Location.class, locationName); + if (location == null) { + throw new ObjectUnknownException("Unknown location: " + locationName); + } + + plantModelService.updateLocationLock( + location.getReference(), + Boolean.parseBoolean(lockedValue) + ); + }); + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/PathHandler.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/PathHandler.java new file mode 100644 index 0000000..4ad6040 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/PathHandler.java @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import org.opentcs.components.kernel.services.PlantModelService; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.model.Path; +import org.opentcs.kernel.extensions.servicewebapi.KernelExecutorWrapper; + +/** + * Handles requests related to paths. + */ +public class PathHandler { + + private final TCSObjectService objectService; + private final KernelExecutorWrapper executorWrapper; + private final PlantModelService plantModelService; + + /** + * Creates a new instance. + * + * @param objectService Used to retrieve path instances. + * @param plantModelService Used to update path locks. + * @param executorWrapper Executes calls via the kernel executor and waits for the outcome. + */ + @Inject + public PathHandler( + TCSObjectService objectService, + KernelExecutorWrapper executorWrapper, + PlantModelService plantModelService + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.executorWrapper = requireNonNull(executorWrapper, "executorWrapper"); + this.plantModelService = requireNonNull(plantModelService, "plantModelService"); + } + + /** + * Updates the locked state of the path with the given name. + * + * @param pathName The name of the path to update. + * @param lockedValue The path's new locked state (a boolean as a string). + * @throws ObjectUnknownException If a path with the given name could not be found. + */ + public void updatePathLock( + @Nonnull + String pathName, + String lockedValue + ) + throws ObjectUnknownException { + executorWrapper.callAndWait(() -> { + Path path = objectService.fetchObject(Path.class, pathName); + if (path == null) { + throw new ObjectUnknownException("Unknown path: " + pathName); + } + plantModelService.updatePathLock(path.getReference(), Boolean.parseBoolean(lockedValue)); + }); + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/PeripheralHandler.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/PeripheralHandler.java new file mode 100644 index 0000000..7295e6a --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/PeripheralHandler.java @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.services.PeripheralService; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.model.Location; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; +import org.opentcs.drivers.peripherals.management.PeripheralAttachmentInformation; +import org.opentcs.kernel.extensions.servicewebapi.KernelExecutorWrapper; + +/** + * Handles requests related to peripherals. + */ +public class PeripheralHandler { + + private final PeripheralService peripheralService; + private final KernelExecutorWrapper executorWrapper; + + /** + * Creates a new instance. + * + * @param peripheralService The service used to manage peripherals. + * @param executorWrapper Executes calls via the kernel executor and waits for the outcome. + */ + @Inject + public PeripheralHandler( + PeripheralService peripheralService, + KernelExecutorWrapper executorWrapper + ) { + this.peripheralService = requireNonNull(peripheralService, "peripheralService"); + this.executorWrapper = requireNonNull(executorWrapper, "executorWrapper"); + } + + public void putPeripheralCommAdapter(String name, String value) + throws ObjectUnknownException { + requireNonNull(name, "name"); + requireNonNull(value, "value"); + + executorWrapper.callAndWait(() -> { + Location location = peripheralService.fetchObject(Location.class, name); + if (location == null) { + throw new ObjectUnknownException("Unknown location: " + name); + } + + PeripheralCommAdapterDescription newAdapter + = peripheralService.fetchAttachmentInformation(location.getReference()) + .getAvailableCommAdapters() + .stream() + .filter(description -> description.getClass().getName().equals(value)) + .findAny() + .orElseThrow( + () -> new IllegalArgumentException( + "Unknown peripheral driver class name: " + value + ) + ); + + peripheralService.attachCommAdapter(location.getReference(), newAdapter); + }); + } + + public void putPeripheralCommAdapterEnabled(String name, String value) + throws ObjectUnknownException, + IllegalArgumentException { + requireNonNull(name, "name"); + requireNonNull(value, "value"); + + executorWrapper.callAndWait(() -> { + Location location = peripheralService.fetchObject(Location.class, name); + if (location == null) { + throw new ObjectUnknownException("Unknown location: " + name); + } + + if (Boolean.parseBoolean(value)) { + peripheralService.enableCommAdapter(location.getReference()); + } + else { + peripheralService.disableCommAdapter(location.getReference()); + } + + }); + } + + public PeripheralAttachmentInformation getPeripheralCommAdapterAttachmentInformation(String name) + throws ObjectUnknownException { + requireNonNull(name, "name"); + + return executorWrapper.callAndWait(() -> { + Location location = peripheralService.fetchObject(Location.class, name); + if (location == null) { + throw new ObjectUnknownException("Unknown location: " + name); + } + + return peripheralService.fetchAttachmentInformation(location.getReference()); + }); + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/PeripheralJobDispatcherHandler.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/PeripheralJobDispatcherHandler.java new file mode 100644 index 0000000..f7140e1 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/PeripheralJobDispatcherHandler.java @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.services.PeripheralDispatcherService; +import org.opentcs.components.kernel.services.PeripheralJobService; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.model.Location; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.kernel.extensions.servicewebapi.KernelExecutorWrapper; + +/** + * Handles requests related to peripheral job dispatching. + */ +public class PeripheralJobDispatcherHandler { + + private final PeripheralJobService jobService; + private final PeripheralDispatcherService jobDispatcherService; + private final KernelExecutorWrapper executorWrapper; + + /** + * Creates a new instance. + * + * @param jobService Used to create peripheral jobs. + * @param jobDispatcherService Used to dispatch peripheral jobs. + * @param executorWrapper Executes calls via the kernel executor and waits for the outcome. + */ + @Inject + public PeripheralJobDispatcherHandler( + PeripheralJobService jobService, + PeripheralDispatcherService jobDispatcherService, + KernelExecutorWrapper executorWrapper + ) { + this.jobService = requireNonNull(jobService, "jobService"); + this.jobDispatcherService = requireNonNull(jobDispatcherService, "jobDispatcherService"); + this.executorWrapper = requireNonNull(executorWrapper, "executorWrapper"); + } + + public void triggerJobDispatcher() { + executorWrapper.callAndWait(() -> jobDispatcherService.dispatch()); + } + + public void withdrawPeripheralJobByLocation(String name) + throws ObjectUnknownException { + requireNonNull(name, "name"); + + executorWrapper.callAndWait(() -> { + Location location = jobService.fetchObject(Location.class, name); + if (location == null) { + throw new ObjectUnknownException("Unknown location: " + name); + } + + jobDispatcherService.withdrawByLocation(location.getReference()); + }); + } + + public void withdrawPeripheralJob(String name) + throws ObjectUnknownException { + requireNonNull(name, "name"); + + executorWrapper.callAndWait(() -> { + PeripheralJob job = jobService.fetchObject(PeripheralJob.class, name); + if (job == null) { + throw new ObjectUnknownException("Unknown peripheral job: " + name); + } + + jobDispatcherService.withdrawByPeripheralJob(job.getReference()); + }); + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/PeripheralJobHandler.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/PeripheralJobHandler.java new file mode 100644 index 0000000..04f5981 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/PeripheralJobHandler.java @@ -0,0 +1,190 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import org.opentcs.access.to.peripherals.PeripheralJobCreationTO; +import org.opentcs.access.to.peripherals.PeripheralOperationCreationTO; +import org.opentcs.components.kernel.services.PeripheralDispatcherService; +import org.opentcs.components.kernel.services.PeripheralJobService; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.kernel.extensions.servicewebapi.KernelExecutorWrapper; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.GetPeripheralJobResponseTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PostPeripheralJobRequestTO; + +/** + * Handles requests related to peripheral jobs. + */ +public class PeripheralJobHandler { + + private final PeripheralJobService jobService; + private final PeripheralDispatcherService jobDispatcherService; + private final KernelExecutorWrapper executorWrapper; + + /** + * Creates a new instance. + * + * @param jobService Used to create peripheral jobs. + * @param jobDispatcherService Used to dispatch peripheral jobs. + * @param executorWrapper Executes calls via the kernel executor and waits for the outcome. + */ + @Inject + public PeripheralJobHandler( + PeripheralJobService jobService, + PeripheralDispatcherService jobDispatcherService, + KernelExecutorWrapper executorWrapper + ) { + this.jobService = requireNonNull(jobService, "jobService"); + this.jobDispatcherService = requireNonNull(jobDispatcherService, "jobDispatcherService"); + this.executorWrapper = requireNonNull(executorWrapper, "executorWrapper"); + } + + public PeripheralJob createPeripheralJob(String name, PostPeripheralJobRequestTO job) { + requireNonNull(name, "name"); + requireNonNull(job, "job"); + + return executorWrapper.callAndWait(() -> { + // Check if the vehicle, location and transport order exist. + if (job.getRelatedVehicle() != null + && jobService.fetchObject(Vehicle.class, job.getRelatedVehicle()) == null) { + throw new ObjectUnknownException("Unknown vehicle: " + job.getRelatedVehicle()); + } + if (job.getRelatedTransportOrder() != null + && jobService.fetchObject( + TransportOrder.class, + job.getRelatedTransportOrder() + ) == null) { + throw new ObjectUnknownException( + "Unknown transport order: " + job.getRelatedTransportOrder() + ); + } + if (job.getPeripheralOperation().getLocationName() != null + && jobService.fetchObject( + Location.class, + job.getPeripheralOperation().getLocationName() + ) == null) { + throw new ObjectUnknownException( + "Unknown location: " + job.getPeripheralOperation().getLocationName() + ); + } + + // Peripheral jobs created via the web API are expected to be executed immediately and + // require no completion. Therefore, explicitly ignore the corresponding provided values. + PeripheralOperationCreationTO operationCreationTO = new PeripheralOperationCreationTO( + job.getPeripheralOperation().getOperation(), + job.getPeripheralOperation().getLocationName() + ); + + PeripheralJobCreationTO jobCreationTO = new PeripheralJobCreationTO( + name, + job.getReservationToken(), + operationCreationTO + ) + .withIncompleteName(job.isIncompleteName()); + if (job.getProperties() != null) { + jobCreationTO = jobCreationTO.withProperties( + job.getProperties().stream() + .collect( + Collectors.toMap( + property -> property.getKey(), + property -> property.getValue() + ) + ) + ); + } + if (job.getRelatedTransportOrder() != null) { + jobCreationTO = jobCreationTO.withRelatedTransportOrderName(job.getRelatedTransportOrder()); + } + if (job.getRelatedVehicle() != null) { + jobCreationTO = jobCreationTO.withRelatedVehicleName(job.getRelatedVehicle()); + } + + return jobService.createPeripheralJob(jobCreationTO); + }); + } + + /** + * Returns all peripheral jobs, optionally filtered using the given parameters. + * + * @param relatedVehicle Which vehicle to filter peripheral jobs for. Not filtered if the value is + * null. + * @param relatedTransportOrder Which transport order to filter peripheral jobs for. Not filtered + * if the value is null. + * @return List of peripheral job states. + */ + public List getPeripheralJobs( + @Nullable + String relatedVehicle, + @Nullable + String relatedTransportOrder + ) { + return executorWrapper.callAndWait(() -> { + // If a related vehicle is set, make sure it exists. + TCSObjectReference relatedVehicleRef + = Optional.ofNullable(relatedVehicle) + .map(name -> jobService.fetchObject(Vehicle.class, name)) + .map(Vehicle::getReference) + .orElse(null); + + if (relatedVehicle != null && relatedVehicleRef == null) { + throw new ObjectUnknownException("Unknown vehicle: " + relatedVehicle); + } + + // If a related transport order is set, make sure it exists. + TCSObjectReference relatedOrderRef + = Optional.ofNullable(relatedTransportOrder) + .map(name -> jobService.fetchObject(TransportOrder.class, name)) + .map(TransportOrder::getReference) + .orElse(null); + + if (relatedTransportOrder != null && relatedOrderRef == null) { + throw new ObjectUnknownException("Unknown oransport order: " + relatedVehicle); + } + + return jobService.fetchObjects( + PeripheralJob.class, + Filters.peripheralJobWithRelatedVehicle(relatedVehicleRef) + .and(Filters.peripheralJobWithRelatedTransportOrder(relatedOrderRef)) + ) + .stream() + .map(GetPeripheralJobResponseTO::fromPeripheralJob) + .sorted(Comparator.comparing(GetPeripheralJobResponseTO::getName)) + .collect(Collectors.toList()); + }); + } + + /** + * Find a peripheral job by name. + * + * @param name The name of the peripheral job. + * @return The peripheral job state. + */ + public GetPeripheralJobResponseTO getPeripheralJobByName( + @Nonnull + String name + ) { + requireNonNull(name, "name"); + + return executorWrapper.callAndWait(() -> { + PeripheralJob job = jobService.fetchObject(PeripheralJob.class, name); + if (job == null) { + throw new ObjectUnknownException("Unknown peripheral job: " + name); + } + + return GetPeripheralJobResponseTO.fromPeripheralJob(job); + }); + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/PlantModelHandler.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/PlantModelHandler.java new file mode 100644 index 0000000..119e868 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/PlantModelHandler.java @@ -0,0 +1,151 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.components.kernel.services.PlantModelService; +import org.opentcs.components.kernel.services.RouterService; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.PlantModel; +import org.opentcs.kernel.extensions.servicewebapi.KernelExecutorWrapper; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PlantModelTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PostTopologyUpdateRequestTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.converter.BlockConverter; +import org.opentcs.kernel.extensions.servicewebapi.v1.converter.LocationConverter; +import org.opentcs.kernel.extensions.servicewebapi.v1.converter.LocationTypeConverter; +import org.opentcs.kernel.extensions.servicewebapi.v1.converter.PathConverter; +import org.opentcs.kernel.extensions.servicewebapi.v1.converter.PointConverter; +import org.opentcs.kernel.extensions.servicewebapi.v1.converter.PropertyConverter; +import org.opentcs.kernel.extensions.servicewebapi.v1.converter.VehicleConverter; +import org.opentcs.kernel.extensions.servicewebapi.v1.converter.VisualLayoutConverter; + +/** + * Handles requests related to plant models. + */ +public class PlantModelHandler { + + /** + * Used to set or retrieve plant models. + */ + private final PlantModelService plantModelService; + /** + * Executes calls via the kernel executor and waits for the outcome. + */ + private final KernelExecutorWrapper executorWrapper; + + private final PointConverter pointConverter; + private final PathConverter pathConverter; + private final LocationTypeConverter locationTypeConverter; + private final LocationConverter locationConverter; + private final BlockConverter blockConverter; + private final VehicleConverter vehicleConverter; + private final VisualLayoutConverter visualLayoutConverter; + private final PropertyConverter propertyConverter; + private final RouterService routerService; + + /** + * Creates a new instance. + * + * @param plantModelService Used to set or retrieve plant models. + * @param executorWrapper Executes calls via the kernel executor and waits for the outcome. + * @param pointConverter Converts point instances. + * @param pathConverter Converts path instances. + * @param locationTypeConverter Converts location type instances. + * @param locationConverter Converts location instances. + * @param blockConverter Converts block instances. + * @param vehicleConverter Converts vehicle instances. + * @param visualLayoutConverter Converts visual layout instances. + * @param propertyConverter Converts property instances. + * @param routerService Provides methods concerning the router. + */ + @Inject + public PlantModelHandler( + PlantModelService plantModelService, + KernelExecutorWrapper executorWrapper, + PointConverter pointConverter, + PathConverter pathConverter, + LocationTypeConverter locationTypeConverter, + LocationConverter locationConverter, + BlockConverter blockConverter, + VehicleConverter vehicleConverter, + VisualLayoutConverter visualLayoutConverter, + PropertyConverter propertyConverter, + RouterService routerService + ) { + this.plantModelService = requireNonNull(plantModelService, "plantModelService"); + this.executorWrapper = requireNonNull(executorWrapper, "executorWrapper"); + this.pointConverter = requireNonNull(pointConverter, "pointConverter"); + this.pathConverter = requireNonNull(pathConverter, "pathConverter"); + this.locationTypeConverter = requireNonNull(locationTypeConverter, "locationTypeConverter"); + this.locationConverter = requireNonNull(locationConverter, "locationConverter"); + this.blockConverter = requireNonNull(blockConverter, "blockConverter"); + this.vehicleConverter = requireNonNull(vehicleConverter, "vehicleConverter"); + this.visualLayoutConverter = requireNonNull(visualLayoutConverter, "visualLayoutConverter"); + this.propertyConverter = requireNonNull(propertyConverter, "propertyConverter"); + this.routerService = requireNonNull(routerService, "routerService"); + } + + public void putPlantModel(PlantModelTO putPlantModel) + throws ObjectUnknownException, + IllegalArgumentException { + requireNonNull(putPlantModel, "putPlantModel"); + + PlantModelCreationTO plantModelCreationTO = new PlantModelCreationTO(putPlantModel.getName()) + .withPoints(pointConverter.toPointCreationTOs(putPlantModel.getPoints())) + .withPaths(pathConverter.toPathCreationTOs(putPlantModel.getPaths())) + .withLocationTypes( + locationTypeConverter.toLocationTypeCreationTOs(putPlantModel.getLocationTypes()) + ) + .withLocations(locationConverter.toLocationCreationTOs(putPlantModel.getLocations())) + .withBlocks(blockConverter.toBlockCreationTOs(putPlantModel.getBlocks())) + .withVehicles(vehicleConverter.toVehicleCreationTOs(putPlantModel.getVehicles())) + .withVisualLayout( + visualLayoutConverter.toVisualLayoutCreationTO(putPlantModel.getVisualLayout()) + ) + .withProperties(propertyConverter.toPropertyMap(putPlantModel.getProperties())); + + executorWrapper.callAndWait(() -> plantModelService.createPlantModel(plantModelCreationTO)); + } + + public PlantModelTO getPlantModel() { + PlantModel plantModel = plantModelService.getPlantModel(); + return new PlantModelTO(plantModel.getName()) + .setPoints(pointConverter.toPointTOs(plantModel.getPoints())) + .setPaths(pathConverter.toPathTOs(plantModel.getPaths())) + .setLocationTypes(locationTypeConverter.toLocationTypeTOs(plantModel.getLocationTypes())) + .setLocations(locationConverter.toLocationTOs(plantModel.getLocations())) + .setBlocks(blockConverter.toBlockTOs(plantModel.getBlocks())) + .setVehicles(vehicleConverter.toVehicleTOs(plantModel.getVehicles())) + .setVisualLayout(visualLayoutConverter.toVisualLayoutTO(plantModel.getVisualLayout())) + .setProperties(propertyConverter.toPropertyTOs(plantModel.getProperties())); + } + + public void requestTopologyUpdate(PostTopologyUpdateRequestTO request) + throws ObjectUnknownException { + executorWrapper.callAndWait( + () -> routerService.updateRoutingTopology(toResourceReferences(request.getPaths())) + ); + } + + private Set> toResourceReferences(List paths) { + Set> pathsToUpdate = new HashSet<>(); + + for (String name : paths) { + Path path = plantModelService.fetchObject(Path.class, name); + if (path == null) { + throw new ObjectUnknownException("Unknown path: " + name); + } + pathsToUpdate.add(path.getReference()); + } + + return pathsToUpdate; + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/StatusEventDispatcher.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/StatusEventDispatcher.java new file mode 100644 index 0000000..dc8788d --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/StatusEventDispatcher.java @@ -0,0 +1,214 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkInRange; + +import jakarta.inject.Inject; +import java.util.Collection; +import java.util.SortedMap; +import java.util.TreeMap; +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelStateTransitionEvent; +import org.opentcs.components.Lifecycle; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.kernel.extensions.servicewebapi.ServiceWebApiConfiguration; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.GetEventsResponseTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.getevents.OrderStatusMessage; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.getevents.PeripheralJobStatusMessage; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.getevents.StatusMessage; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.getevents.VehicleStatusMessage; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.event.EventSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides descriptions of recent events. + */ +public class StatusEventDispatcher + implements + Lifecycle, + EventHandler { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(StatusEventDispatcher.class); + /** + * The interface configuration. + */ + private final ServiceWebApiConfiguration configuration; + /** + * Where we register for application events. + */ + private final EventSource eventSource; + /** + * The events collected. + */ + private final SortedMap events = new TreeMap<>(); + /** + * The number of events collected so far. + */ + private long eventCount; + /** + * Whether this instance is initialized. + */ + private boolean initialized; + /** + * Whether we are collecting events. + */ + private boolean eventCollectingOn; + + @Inject + public StatusEventDispatcher( + ServiceWebApiConfiguration configuration, + @ApplicationEventBus + EventSource eventSource + ) { + this.configuration = requireNonNull(configuration, "configuration"); + this.eventSource = requireNonNull(eventSource, "eventSource"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + eventSource.subscribe(this); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + eventSource.unsubscribe(this); + + initialized = false; + } + + @Override + public void onEvent(Object event) { + if (event instanceof KernelStateTransitionEvent) { + handleStateTransition((KernelStateTransitionEvent) event); + } + + if (!eventCollectingOn) { + return; + } + + if (event instanceof TCSObjectEvent) { + handleObjectEvent((TCSObjectEvent) event); + } + } + + /** + * Provides a list of events within the given range, waiting at most timeout + * milliseconds for new events if there currently aren't any. + * + * @param minSequenceNo The minimum sequence number for accepted events. + * @param maxSequenceNo The maximum sequence number for accepted events. + * @param timeout The maximum time to wait for events (in ms) if there currently aren't any. + * @return A list of events within the given range. + */ + public GetEventsResponseTO fetchEvents(long minSequenceNo, long maxSequenceNo, long timeout) + throws IllegalArgumentException { + checkInRange(minSequenceNo, 0, Long.MAX_VALUE, "minSequenceNo"); + checkInRange(maxSequenceNo, minSequenceNo, Long.MAX_VALUE, "maxSequenceNo"); + checkInRange(timeout, 0, Long.MAX_VALUE, "timeout"); + + GetEventsResponseTO result = new GetEventsResponseTO(); + synchronized (events) { + Collection messages = events.subMap(minSequenceNo, maxSequenceNo).values(); + if (messages.isEmpty()) { + try { + events.wait(timeout); + } + catch (InterruptedException exc) { + LOG.warn("Unexpectedly interrupted", exc); + } + } + messages = events.subMap(minSequenceNo, maxSequenceNo).values(); + result.getStatusMessages().addAll(messages); + } + return result; + } + + private void handleStateTransition(KernelStateTransitionEvent event) { + boolean wasOn = eventCollectingOn; + eventCollectingOn + = event.getEnteredState() == Kernel.State.OPERATING && event.isTransitionFinished(); + + // When switching collecting of events on, ensure we start clean. + if (!wasOn && eventCollectingOn) { + synchronized (events) { + eventCount = 0; + events.clear(); + } + } + } + + private void handleObjectEvent(TCSObjectEvent event) { + TCSObject object = event.getCurrentOrPreviousObjectState(); + if (object instanceof TransportOrder) { + synchronized (events) { + addOrderStatusMessage((TransportOrder) object, eventCount); + eventCount++; + cleanUpEvents(); + events.notifyAll(); + } + } + else if (object instanceof Vehicle) { + synchronized (events) { + addVehicleStatusMessage((Vehicle) object, eventCount); + eventCount++; + cleanUpEvents(); + events.notifyAll(); + } + } + else if (object instanceof PeripheralJob) { + synchronized (events) { + addPeripheralStatusMessage((PeripheralJob) object, eventCount); + eventCount++; + cleanUpEvents(); + events.notifyAll(); + } + } + } + + private void addOrderStatusMessage(TransportOrder order, long sequenceNumber) { + events.put(sequenceNumber, OrderStatusMessage.fromTransportOrder(order, sequenceNumber)); + } + + private void addVehicleStatusMessage(Vehicle vehicle, long sequenceNumber) { + events.put(sequenceNumber, VehicleStatusMessage.fromVehicle(vehicle, sequenceNumber)); + } + + private void addPeripheralStatusMessage(PeripheralJob job, long sequenceNumber) { + events.put(sequenceNumber, PeripheralJobStatusMessage.fromPeripheralJob(job, sequenceNumber)); + } + + private void cleanUpEvents() { + // XXX Sanitize maxEventCount + int maxEventCount = configuration.statusEventsCapacity(); + while (events.size() > maxEventCount) { + events.remove(events.firstKey()); + } + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/TransportOrderDispatcherHandler.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/TransportOrderDispatcherHandler.java new file mode 100644 index 0000000..eff1d37 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/TransportOrderDispatcherHandler.java @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.components.kernel.services.VehicleService; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.kernel.extensions.servicewebapi.KernelExecutorWrapper; + +/** + * Handles requests related to transport order dispatching. + */ +public class TransportOrderDispatcherHandler { + + private final VehicleService vehicleService; + private final DispatcherService dispatcherService; + private final KernelExecutorWrapper executorWrapper; + + /** + * Creates a new instance. + * + * @param vehicleService Used to update vehicle state. + * @param dispatcherService Used to withdraw transport orders. + * @param executorWrapper Executes calls via the kernel executor and waits for the outcome. + */ + @Inject + public TransportOrderDispatcherHandler( + VehicleService vehicleService, + DispatcherService dispatcherService, + KernelExecutorWrapper executorWrapper + ) { + this.vehicleService = requireNonNull(vehicleService, "vehicleService"); + this.dispatcherService = requireNonNull(dispatcherService, "dispatcherService"); + this.executorWrapper = requireNonNull(executorWrapper, "executorWrapper"); + } + + public void triggerDispatcher() { + executorWrapper.callAndWait(() -> dispatcherService.dispatch()); + } + + public void tryImmediateAssignment(String name) + throws ObjectUnknownException, + IllegalArgumentException { + requireNonNull(name, "name"); + + executorWrapper.callAndWait(() -> { + TransportOrder order = vehicleService.fetchObject(TransportOrder.class, name); + if (order == null) { + throw new ObjectUnknownException("Unknown transport order: " + name); + } + + dispatcherService.assignNow(order.getReference()); + }); + } + + public void withdrawByTransportOrder(String name, boolean immediate, boolean disableVehicle) + throws ObjectUnknownException { + requireNonNull(name, "name"); + + executorWrapper.callAndWait(() -> { + if (vehicleService.fetchObject(TransportOrder.class, name) == null) { + throw new ObjectUnknownException("Unknown transport order: " + name); + } + + TransportOrder order = vehicleService.fetchObject(TransportOrder.class, name); + if (disableVehicle && order.getProcessingVehicle() != null) { + vehicleService.updateVehicleIntegrationLevel( + order.getProcessingVehicle(), + Vehicle.IntegrationLevel.TO_BE_RESPECTED + ); + } + + dispatcherService.withdrawByTransportOrder(order.getReference(), immediate); + }); + } + + public void withdrawByVehicle(String name, boolean immediate, boolean disableVehicle) + throws ObjectUnknownException { + requireNonNull(name, "name"); + + executorWrapper.callAndWait(() -> { + Vehicle vehicle = vehicleService.fetchObject(Vehicle.class, name); + if (vehicle == null) { + throw new ObjectUnknownException("Unknown vehicle: " + name); + } + + if (disableVehicle) { + vehicleService.updateVehicleIntegrationLevel( + vehicle.getReference(), + Vehicle.IntegrationLevel.TO_BE_RESPECTED + ); + } + + dispatcherService.withdrawByVehicle(vehicle.getReference(), immediate); + }); + } + + public void reroute(String vehicleName, boolean forced) + throws ObjectUnknownException { + requireNonNull(vehicleName, "vehicleName"); + + executorWrapper.callAndWait(() -> { + Vehicle vehicle = vehicleService.fetchObject(Vehicle.class, vehicleName); + if (vehicle == null) { + throw new ObjectUnknownException("Unknown vehicle: " + vehicleName); + } + + dispatcherService.reroute( + vehicle.getReference(), + forced ? ReroutingType.FORCED : ReroutingType.REGULAR + ); + }); + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/TransportOrderHandler.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/TransportOrderHandler.java new file mode 100644 index 0000000..56e108d --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/TransportOrderHandler.java @@ -0,0 +1,276 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.to.order.DestinationCreationTO; +import org.opentcs.access.to.order.OrderSequenceCreationTO; +import org.opentcs.access.to.order.TransportOrderCreationTO; +import org.opentcs.components.kernel.services.TransportOrderService; +import org.opentcs.data.ObjectExistsException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderConstants; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.kernel.extensions.servicewebapi.KernelExecutorWrapper; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.GetOrderSequenceResponseTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.GetTransportOrderResponseTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PostOrderSequenceRequestTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PostTransportOrderRequestTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.posttransportorder.Destination; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.Property; + +/** + * Handles requests related to transport orders and order sequences. + */ +public class TransportOrderHandler { + + private final TransportOrderService orderService; + private final KernelExecutorWrapper executorWrapper; + + /** + * Creates a new instance. + * + * @param orderService The service we use to get the transport orders. + * @param executorWrapper Executes calls via the kernel executor and waits for the outcome. + */ + @Inject + public TransportOrderHandler( + TransportOrderService orderService, + KernelExecutorWrapper executorWrapper + ) { + this.orderService = requireNonNull(orderService, "orderService"); + this.executorWrapper = requireNonNull(executorWrapper, "executorWrapper"); + } + + public TransportOrder createOrder(String name, PostTransportOrderRequestTO order) + throws ObjectUnknownException, + ObjectExistsException, + KernelRuntimeException, + IllegalStateException { + requireNonNull(name, "name"); + requireNonNull(order, "order"); + + TransportOrderCreationTO to + = new TransportOrderCreationTO(name, destinations(order)) + .withIncompleteName(order.isIncompleteName()) + .withDispensable(order.isDispensable()) + .withIntendedVehicleName(order.getIntendedVehicle()) + .withDependencyNames(dependencyNames(order.getDependencies())) + .withDeadline(deadline(order)) + .withPeripheralReservationToken(order.getPeripheralReservationToken()) + .withWrappingSequence(order.getWrappingSequence()) + .withType(order.getType() == null ? OrderConstants.TYPE_NONE : order.getType()) + .withProperties(properties(order.getProperties())); + + return executorWrapper.callAndWait(() -> { + return orderService.createTransportOrder(to); + }); + } + + public void updateTransportOrderIntendedVehicle( + String orderName, + @Nullable + String vehicleName + ) + throws ObjectUnknownException { + requireNonNull(orderName, "orderName"); + + executorWrapper.callAndWait(() -> { + TransportOrder order = orderService.fetchObject(TransportOrder.class, orderName); + if (order == null) { + throw new ObjectUnknownException("Unknown transport order: " + orderName); + } + Vehicle vehicle = null; + if (vehicleName != null) { + vehicle = orderService.fetchObject(Vehicle.class, vehicleName); + if (vehicle == null) { + throw new ObjectUnknownException("Unknown vehicle: " + vehicleName); + } + } + + orderService.updateTransportOrderIntendedVehicle( + order.getReference(), + vehicle != null ? vehicle.getReference() : null + ); + }); + } + + /** + * Find all transport orders and filters depending on the given parameters. + * + * @param intendedVehicle The filter parameter for the name of the + * intended vehicle for the transport order. The filtering is disabled for this parameter if the + * value is null. + * @return A list of transport orders that match the filter. + */ + public List getTransportOrders( + @Nullable + String intendedVehicle + ) { + return executorWrapper.callAndWait(() -> { + TCSObjectReference intendedVehicleRef + = Optional.ofNullable(intendedVehicle) + .map(name -> orderService.fetchObject(Vehicle.class, name)) + .map(Vehicle::getReference) + .orElse(null); + + if (intendedVehicle != null && intendedVehicleRef == null) { + throw new ObjectUnknownException("Unknown vehicle: " + intendedVehicle); + } + + return orderService.fetchObjects( + TransportOrder.class, + Filters.transportOrderWithIntendedVehicle(intendedVehicleRef) + ) + .stream() + .map(GetTransportOrderResponseTO::fromTransportOrder) + .sorted(Comparator.comparing(GetTransportOrderResponseTO::getName)) + .collect(Collectors.toList()); + }); + } + + /** + * Finds the transport order with the given name. + * + * @param name The name of the requested transport order. + * @return A single transport order with the given name. + * @throws ObjectUnknownException If a transport order with the given name does not exist. + */ + public GetTransportOrderResponseTO getTransportOrderByName(String name) + throws ObjectUnknownException { + requireNonNull(name, "name"); + + return executorWrapper.callAndWait(() -> { + return Optional.ofNullable(orderService.fetchObject(TransportOrder.class, name)) + .map(GetTransportOrderResponseTO::fromTransportOrder) + .orElseThrow(() -> new ObjectUnknownException("Unknown transport order: " + name)); + }); + } + + public OrderSequence createOrderSequence(String name, PostOrderSequenceRequestTO sequence) + throws ObjectUnknownException, + ObjectExistsException, + KernelRuntimeException, + IllegalStateException { + requireNonNull(name, "name"); + requireNonNull(sequence, "sequence"); + + OrderSequenceCreationTO to = new OrderSequenceCreationTO(name) + .withFailureFatal(sequence.isFailureFatal()) + .withIncompleteName(sequence.isIncompleteName()) + .withIntendedVehicleName(sequence.getIntendedVehicle()) + .withProperties(properties(sequence.getProperties())) + .withType(sequence.getType()); + + return executorWrapper.callAndWait(() -> { + return orderService.createOrderSequence(to); + }); + } + + public void putOrderSequenceComplete(String name) + throws ObjectUnknownException, + IllegalStateException { + requireNonNull(name, "name"); + + executorWrapper.callAndWait(() -> { + OrderSequence orderSequence = orderService.fetchObject(OrderSequence.class, name); + if (orderSequence == null) { + throw new ObjectUnknownException("Unknown order sequence: " + name); + } + orderService.markOrderSequenceComplete(orderSequence.getReference()); + }); + } + + public List getOrderSequences( + @Nullable + String intendedVehicle + ) { + return executorWrapper.callAndWait(() -> { + TCSObjectReference intendedVehicleRef + = Optional.ofNullable(intendedVehicle) + .map(name -> orderService.fetchObject(Vehicle.class, name)) + .map(Vehicle::getReference) + .orElse(null); + + if (intendedVehicle != null && intendedVehicleRef == null) { + throw new ObjectUnknownException("Unknown vehicle: " + intendedVehicle); + } + + return orderService.fetchObjects( + OrderSequence.class, + Filters.orderSequenceWithIntendedVehicle(intendedVehicleRef) + ) + .stream() + .map(GetOrderSequenceResponseTO::fromOrderSequence) + .sorted(Comparator.comparing(GetOrderSequenceResponseTO::getName)) + .collect(Collectors.toList()); + }); + } + + public GetOrderSequenceResponseTO getOrderSequenceByName(String name) + throws ObjectUnknownException { + requireNonNull(name, "name"); + + return executorWrapper.callAndWait(() -> { + return Optional.ofNullable(orderService.fetchObject(OrderSequence.class, name)) + .map(GetOrderSequenceResponseTO::fromOrderSequence) + .orElseThrow(() -> new ObjectUnknownException("Unknown transport order: " + name)); + }); + } + + private List destinations(PostTransportOrderRequestTO order) { + List result = new ArrayList<>(order.getDestinations().size()); + + for (Destination dest : order.getDestinations()) { + DestinationCreationTO to = new DestinationCreationTO( + dest.getLocationName(), + dest.getOperation() + ); + + if (dest.getProperties() != null) { + for (Property prop : dest.getProperties()) { + to = to.withProperty(prop.getKey(), prop.getValue()); + } + } + + result.add(to); + } + + return result; + } + + private Set dependencyNames(List dependencies) { + return dependencies == null ? new HashSet<>() : new HashSet<>(dependencies); + } + + private Instant deadline(PostTransportOrderRequestTO order) { + return order.getDeadline() == null ? Instant.MAX : order.getDeadline(); + } + + private Map properties(List properties) { + Map result = new HashMap<>(); + if (properties != null) { + for (Property prop : properties) { + result.put(prop.getKey(), prop.getValue()); + } + } + return result; + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/V1RequestHandler.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/V1RequestHandler.java new file mode 100644 index 0000000..d18a55e --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/V1RequestHandler.java @@ -0,0 +1,735 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.List; +import java.util.concurrent.ExecutionException; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.data.ObjectExistsException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.kernel.extensions.servicewebapi.HttpConstants; +import org.opentcs.kernel.extensions.servicewebapi.JsonBinder; +import org.opentcs.kernel.extensions.servicewebapi.RequestHandler; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.GetOrderSequenceResponseTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.GetPeripheralAttachmentInfoResponseTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.GetPeripheralJobResponseTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.GetTransportOrderResponseTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.GetVehicleAttachmentInfoResponseTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PlantModelTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PostOrderSequenceRequestTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PostPeripheralJobRequestTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PostTopologyUpdateRequestTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PostTransportOrderRequestTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PostVehicleRoutesRequestTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PostVehicleRoutesResponseTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PutVehicleAllowedOrderTypesTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PutVehicleEnergyLevelThresholdSetTO; +import spark.QueryParamsMap; +import spark.Request; +import spark.Response; +import spark.Service; + +/** + * Handles requests and produces responses for version 1 of the web API. + */ +public class V1RequestHandler + implements + RequestHandler { + + private final JsonBinder jsonBinder; + private final StatusEventDispatcher statusEventDispatcher; + private final TransportOrderDispatcherHandler orderDispatcherHandler; + private final TransportOrderHandler transportOrderHandler; + private final PeripheralJobHandler peripheralJobHandler; + private final PeripheralJobDispatcherHandler jobDispatcherHandler; + private final PlantModelHandler plantModelHandler; + private final VehicleHandler vehicleHandler; + private final PathHandler pathHandler; + private final LocationHandler locationHandler; + private final PeripheralHandler peripheralHandler; + + private boolean initialized; + + @Inject + public V1RequestHandler( + JsonBinder jsonBinder, + StatusEventDispatcher statusEventDispatcher, + TransportOrderDispatcherHandler orderDispatcherHandler, + TransportOrderHandler transportOrderHandler, + PeripheralJobHandler peripheralJobHandler, + PeripheralJobDispatcherHandler jobDispatcherHandler, + PlantModelHandler plantModelHandler, + VehicleHandler vehicleHandler, + PathHandler pathHandler, + LocationHandler locationHandler, + PeripheralHandler peripheralHandler + ) { + this.jsonBinder = requireNonNull(jsonBinder, "jsonBinder"); + this.statusEventDispatcher = requireNonNull(statusEventDispatcher, "statusEventDispatcher"); + this.orderDispatcherHandler = requireNonNull(orderDispatcherHandler, "orderDispatcherHandler"); + this.transportOrderHandler = requireNonNull(transportOrderHandler, "transportOrderHandler"); + this.peripheralJobHandler = requireNonNull(peripheralJobHandler, "peripheralJobHandler"); + this.jobDispatcherHandler = requireNonNull(jobDispatcherHandler, "jobDispatcherHandler"); + this.plantModelHandler = requireNonNull(plantModelHandler, "plantModelHandler"); + this.vehicleHandler = requireNonNull(vehicleHandler, "vehicleHandler"); + this.pathHandler = requireNonNull(pathHandler, "pathHandler"); + this.locationHandler = requireNonNull(locationHandler, "locationHandler"); + this.peripheralHandler = requireNonNull(peripheralHandler, "peripheralHandler"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + statusEventDispatcher.initialize(); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + statusEventDispatcher.terminate(); + + initialized = false; + } + + @Override + public void addRoutes(Service service) { + requireNonNull(service, "service"); + + service.get( + "/events", + this::handleGetEvents + ); + service.post( + "/vehicles/dispatcher/trigger", + this::handlePostDispatcherTrigger + ); + service.post( + "/vehicles/:NAME/routeComputationQuery", + this::handleGetVehicleRoutes + ); + service.put( + "/vehicles/:NAME/commAdapter/attachment", + this::handlePutVehicleCommAdapterAttachment + ); + service.get( + "/vehicles/:NAME/commAdapter/attachmentInformation", + this::handleGetVehicleCommAdapterAttachmentInfo + ); + service.put( + "/vehicles/:NAME/commAdapter/enabled", + this::handlePutVehicleCommAdapterEnabled + ); + service.put( + "/vehicles/:NAME/paused", + this::handlePutVehiclePaused + ); + service.put( + "/vehicles/:NAME/integrationLevel", + this::handlePutVehicleIntegrationLevel + ); + service.post( + "/vehicles/:NAME/withdrawal", + this::handlePostWithdrawalByVehicle + ); + service.post( + "/vehicles/:NAME/rerouteRequest", + this::handlePostVehicleRerouteRequest + ); + service.put( + "/vehicles/:NAME/allowedOrderTypes", + this::handlePutVehicleAllowedOrderTypes + ); + service.put( + "/vehicles/:NAME/energyLevelThresholdSet", + this::handlePutVehicleEnergyLevelThresholdSet + ); + service.put( + "/vehicles/:NAME/envelopeKey", + this::handlePutVehicleEnvelopeKey + ); + service.get( + "/vehicles/:NAME", + this::handleGetVehicleByName + ); + service.get( + "/vehicles", + this::handleGetVehicles + ); + service.post( + "/transportOrders/dispatcher/trigger", + this::handlePostDispatcherTrigger + ); + service.post( + "/transportOrders/:NAME/immediateAssignment", + this::handlePostImmediateAssignment + ); + service.post( + "/transportOrders/:NAME/withdrawal", + this::handlePostWithdrawalByOrder + ); + service.post( + "/transportOrders/:NAME", + this::handlePostTransportOrder + ); + service.put( + "/transportOrders/:NAME/intendedVehicle", + this::handlePutTransportOrderIntendedVehicle + ); + service.get( + "/transportOrders/:NAME", + this::handleGetTransportOrderByName + ); + service.get( + "/transportOrders", + this::handleGetTransportOrders + ); + service.post( + "/orderSequences/:NAME", + this::handlePostOrderSequence + ); + service.get( + "/orderSequences", + this::handleGetOrderSequences + ); + service.get( + "/orderSequences/:NAME", + this::handleGetOrderSequenceByName + ); + service.put( + "/orderSequences/:NAME/complete", + this::handlePutOrderSequenceComplete + ); + service.put( + "/plantModel", + this::handlePutPlantModel + ); + service.get( + "/plantModel", + this::handleGetPlantModel + ); + service.post( + "/plantModel/topologyUpdateRequest", + this::handlePostUpdateTopology + ); + service.put( + "/paths/:NAME/locked", + this::handlePutPathLocked + ); + service.put( + "/locations/:NAME/locked", + this::handlePutLocationLocked + ); + service.post( + "/dispatcher/trigger", + this::handlePostDispatcherTrigger + ); + service.post( + "/peripherals/dispatcher/trigger", + this::handlePostPeripheralJobsDispatchTrigger + ); + service.post( + "/peripherals/:NAME/withdrawal", + this::handlePostPeripheralWithdrawal + ); + service.put( + "/peripherals/:NAME/commAdapter/enabled", + this::handlePutPeripheralCommAdapterEnabled + ); + service.get( + "/peripherals/:NAME/commAdapter/attachmentInformation", + this::handleGetPeripheralCommAdapterAttachmentInfo + ); + service.put( + "/peripherals/:NAME/commAdapter/attachment", + this::handlePutPeripheralCommAdapterAttachment + ); + service.get( + "/peripheralJobs", + this::handleGetPeripheralJobs + ); + service.get( + "/peripheralJobs/:NAME", + this::handleGetPeripheralJobsByName + ); + service.post( + "/peripheralJobs/:NAME", + this::handlePostPeripheralJobsByName + ); + service.post( + "/peripheralJobs/:NAME/withdrawal", + this::handlePostPeripheralJobWithdrawal + ); + service.post( + "/peripheralJobs/dispatcher/trigger", + this::handlePostPeripheralJobsDispatchTrigger + ); + } + + private Object handlePostDispatcherTrigger(Request request, Response response) + throws KernelRuntimeException { + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + orderDispatcherHandler.triggerDispatcher(); + return ""; + } + + private Object handleGetEvents(Request request, Response response) + throws IllegalArgumentException, + IllegalStateException { + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + return jsonBinder.toJson( + statusEventDispatcher.fetchEvents( + minSequenceNo(request), + maxSequenceNo(request), + timeout(request) + ) + ); + } + + private Object handlePutVehicleCommAdapterEnabled(Request request, Response response) + throws ObjectUnknownException, + IllegalArgumentException { + vehicleHandler.putVehicleCommAdapterEnabled( + request.params(":NAME"), + valueIfKeyPresent(request.queryMap(), "newValue") + ); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + return ""; + } + + private Object handleGetVehicleCommAdapterAttachmentInfo(Request request, Response response) + throws ObjectUnknownException, + IllegalArgumentException { + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + return jsonBinder.toJson( + GetVehicleAttachmentInfoResponseTO.fromAttachmentInformation( + vehicleHandler.getVehicleCommAdapterAttachmentInformation( + request.params(":NAME") + ) + ) + ); + } + + private Object handleGetVehicleRoutes(Request request, Response response) + throws ObjectUnknownException, + IllegalArgumentException { + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + return jsonBinder.toJson( + PostVehicleRoutesResponseTO.fromMap( + vehicleHandler.getVehicleRoutes( + request.params(":NAME"), + jsonBinder.fromJson(request.body(), PostVehicleRoutesRequestTO.class) + ) + ) + ); + } + + private Object handlePutVehicleCommAdapterAttachment(Request request, Response response) + throws ObjectUnknownException, + IllegalArgumentException { + vehicleHandler.putVehicleCommAdapter( + request.params(":NAME"), + valueIfKeyPresent(request.queryMap(), "newValue") + ); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + return ""; + } + + private Object handlePostTransportOrder(Request request, Response response) + throws ObjectUnknownException, + ObjectExistsException, + IllegalArgumentException, + IllegalStateException { + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + return jsonBinder.toJson( + GetTransportOrderResponseTO.fromTransportOrder( + transportOrderHandler.createOrder( + request.params(":NAME"), + jsonBinder.fromJson(request.body(), PostTransportOrderRequestTO.class) + ) + ) + ); + } + + private Object handlePutTransportOrderIntendedVehicle(Request request, Response response) + throws ObjectUnknownException { + transportOrderHandler.updateTransportOrderIntendedVehicle( + request.params(":NAME"), + request.queryParamOrDefault("vehicle", null) + ); + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + return ""; + } + + private Object handlePostOrderSequence(Request request, Response response) + throws ObjectUnknownException, + ObjectExistsException, + IllegalArgumentException, + IllegalStateException { + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + return jsonBinder.toJson( + GetOrderSequenceResponseTO.fromOrderSequence( + transportOrderHandler.createOrderSequence( + request.params(":NAME"), + jsonBinder.fromJson(request.body(), PostOrderSequenceRequestTO.class) + ) + ) + ); + } + + private Object handleGetOrderSequences(Request request, Response response) { + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + return jsonBinder.toJson( + transportOrderHandler.getOrderSequences( + valueIfKeyPresent(request.queryMap(), "intendedVehicle") + ) + ); + } + + private Object handleGetOrderSequenceByName(Request request, Response response) { + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + return jsonBinder.toJson( + transportOrderHandler.getOrderSequenceByName(request.params(":NAME")) + ); + } + + private Object handlePutOrderSequenceComplete(Request request, Response response) + throws ObjectUnknownException, + IllegalArgumentException, + InterruptedException, + ExecutionException { + transportOrderHandler.putOrderSequenceComplete(request.params(":NAME")); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + return ""; + } + + private Object handlePostImmediateAssignment(Request request, Response response) + throws ObjectUnknownException { + orderDispatcherHandler.tryImmediateAssignment(request.params(":NAME")); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + return ""; + } + + private Object handlePostWithdrawalByOrder(Request request, Response response) + throws ObjectUnknownException { + orderDispatcherHandler.withdrawByTransportOrder( + request.params(":NAME"), + immediate(request), + disableVehicle(request) + ); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + return ""; + } + + private Object handlePostWithdrawalByVehicle(Request request, Response response) + throws ObjectUnknownException { + orderDispatcherHandler.withdrawByVehicle( + request.params(":NAME"), + immediate(request), + disableVehicle(request) + ); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + return ""; + } + + private Object handlePostPeripheralJobWithdrawal(Request request, Response response) + throws KernelRuntimeException { + jobDispatcherHandler.withdrawPeripheralJob(request.params(":NAME")); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + return ""; + } + + private Object handlePostVehicleRerouteRequest(Request request, Response response) + throws ObjectUnknownException { + orderDispatcherHandler.reroute(request.params(":NAME"), forced(request)); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + return ""; + } + + private Object handleGetTransportOrders(Request request, Response response) { + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + return jsonBinder.toJson( + transportOrderHandler.getTransportOrders( + valueIfKeyPresent(request.queryMap(), "intendedVehicle") + ) + ); + } + + private Object handlePutPlantModel(Request request, Response response) + throws ObjectUnknownException, + IllegalArgumentException { + plantModelHandler.putPlantModel(jsonBinder.fromJson(request.body(), PlantModelTO.class)); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + return ""; + } + + private Object handleGetPlantModel(Request request, Response response) { + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + return jsonBinder.toJson(plantModelHandler.getPlantModel()); + } + + private Object handlePostUpdateTopology(Request request, Response response) + throws ObjectUnknownException, + KernelRuntimeException { + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + if (request.body().isBlank()) { + plantModelHandler.requestTopologyUpdate(new PostTopologyUpdateRequestTO(List.of())); + } + else { + plantModelHandler + .requestTopologyUpdate( + jsonBinder.fromJson(request.body(), PostTopologyUpdateRequestTO.class) + ); + } + return ""; + } + + private Object handlePutPathLocked(Request request, Response response) { + pathHandler.updatePathLock( + request.params(":NAME"), + valueIfKeyPresent(request.queryMap(), "newValue") + ); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + return ""; + } + + private Object handlePutLocationLocked(Request request, Response response) { + locationHandler.updateLocationLock( + request.params(":NAME"), + valueIfKeyPresent(request.queryMap(), "newValue") + ); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + return ""; + } + + private Object handleGetTransportOrderByName(Request request, Response response) { + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + return jsonBinder.toJson( + transportOrderHandler.getTransportOrderByName(request.params(":NAME")) + ); + } + + private Object handleGetVehicles(Request request, Response response) + throws IllegalArgumentException { + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + return jsonBinder.toJson( + vehicleHandler.getVehiclesState( + valueIfKeyPresent( + request.queryMap(), + "procState" + ) + ) + ); + } + + private Object handleGetVehicleByName(Request request, Response response) + throws ObjectUnknownException { + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + return jsonBinder.toJson( + vehicleHandler.getVehicleStateByName(request.params(":NAME")) + ); + } + + private Object handlePutVehicleIntegrationLevel(Request request, Response response) + throws ObjectUnknownException, + IllegalArgumentException { + vehicleHandler.putVehicleIntegrationLevel( + request.params(":NAME"), + valueIfKeyPresent(request.queryMap(), "newValue") + ); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + return ""; + } + + private Object handlePutVehiclePaused(Request request, Response response) + throws ObjectUnknownException, + IllegalArgumentException { + vehicleHandler.putVehiclePaused( + request.params(":NAME"), + valueIfKeyPresent(request.queryMap(), "newValue") + ); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + return ""; + } + + private Object handlePutVehicleAllowedOrderTypes(Request request, Response response) + throws ObjectUnknownException, + IllegalArgumentException { + vehicleHandler.putVehicleAllowedOrderTypes( + request.params(":NAME"), + jsonBinder.fromJson(request.body(), PutVehicleAllowedOrderTypesTO.class) + ); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + return ""; + } + + private Object handlePutVehicleEnergyLevelThresholdSet(Request request, Response response) + throws ObjectUnknownException, + IllegalArgumentException { + vehicleHandler.putVehicleEnergyLevelThresholdSet( + request.params(":NAME"), + jsonBinder.fromJson(request.body(), PutVehicleEnergyLevelThresholdSetTO.class) + ); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + return ""; + } + + private Object handlePutVehicleEnvelopeKey(Request request, Response response) + throws ObjectUnknownException, + IllegalArgumentException { + vehicleHandler.putVehicleEnvelopeKey( + request.params(":NAME"), + valueIfKeyPresent(request.queryMap(), "newValue") + ); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + return ""; + } + + private Object handlePostPeripheralWithdrawal(Request request, Response response) + throws KernelRuntimeException { + jobDispatcherHandler.withdrawPeripheralJobByLocation(request.params(":NAME")); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + return ""; + } + + private Object handlePutPeripheralCommAdapterEnabled(Request request, Response response) + throws ObjectUnknownException, + IllegalArgumentException { + peripheralHandler.putPeripheralCommAdapterEnabled( + request.params(":NAME"), + valueIfKeyPresent(request.queryMap(), "newValue") + ); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + return ""; + } + + private Object handleGetPeripheralCommAdapterAttachmentInfo(Request request, Response response) + throws ObjectUnknownException, + IllegalArgumentException { + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + return jsonBinder.toJson( + GetPeripheralAttachmentInfoResponseTO.fromAttachmentInformation( + peripheralHandler.getPeripheralCommAdapterAttachmentInformation( + request.params(":NAME") + ) + ) + ); + } + + private Object handleGetPeripheralJobs(Request request, Response response) { + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + return jsonBinder.toJson( + peripheralJobHandler.getPeripheralJobs( + valueIfKeyPresent(request.queryMap(), "relatedVehicle"), + valueIfKeyPresent(request.queryMap(), "relatedTransportOrder") + ) + ); + } + + private Object handlePutPeripheralCommAdapterAttachment(Request request, Response response) + throws ObjectUnknownException, + IllegalArgumentException { + peripheralHandler.putPeripheralCommAdapter( + request.params(":NAME"), + valueIfKeyPresent(request.queryMap(), "newValue") + ); + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + return ""; + } + + private Object handleGetPeripheralJobsByName(Request request, Response response) { + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + return jsonBinder.toJson( + peripheralJobHandler.getPeripheralJobByName(request.params(":NAME")) + ); + } + + private Object handlePostPeripheralJobsByName(Request request, Response response) { + response.type(HttpConstants.CONTENT_TYPE_APPLICATION_JSON_UTF8); + return jsonBinder.toJson( + GetPeripheralJobResponseTO.fromPeripheralJob( + peripheralJobHandler.createPeripheralJob( + request.params(":NAME"), + jsonBinder.fromJson(request.body(), PostPeripheralJobRequestTO.class) + ) + ) + ); + } + + private Object handlePostPeripheralJobsDispatchTrigger(Request request, Response response) { + response.type(HttpConstants.CONTENT_TYPE_TEXT_PLAIN_UTF8); + jobDispatcherHandler.triggerJobDispatcher(); + return ""; + } + + private String valueIfKeyPresent(QueryParamsMap queryParams, String key) { + if (queryParams.hasKey(key)) { + return queryParams.value(key); + } + else { + return null; + } + } + + private long minSequenceNo(Request request) + throws IllegalArgumentException { + String param = request.queryParamOrDefault("minSequenceNo", "0"); + try { + return Long.parseLong(param); + } + catch (NumberFormatException exc) { + throw new IllegalArgumentException("Malformed minSequenceNo: " + param); + } + } + + private long maxSequenceNo(Request request) + throws IllegalArgumentException { + String param = request.queryParamOrDefault("maxSequenceNo", String.valueOf(Long.MAX_VALUE)); + try { + return Long.parseLong(param); + } + catch (NumberFormatException exc) { + throw new IllegalArgumentException("Malformed minSequenceNo: " + param); + } + } + + private long timeout(Request request) + throws IllegalArgumentException { + String param = request.queryParamOrDefault("timeout", "1000"); + try { + // Allow a maximum timeout of 10 seconds so server threads are only bound for a limited time. + return Math.min(10000, Long.parseLong(param)); + } + catch (NumberFormatException exc) { + throw new IllegalArgumentException("Malformed timeout: " + param); + } + } + + private boolean immediate(Request request) { + return Boolean.parseBoolean(request.queryParamOrDefault("immediate", "false")); + } + + private boolean disableVehicle(Request request) { + return Boolean.parseBoolean(request.queryParamOrDefault("disableVehicle", "false")); + } + + private boolean forced(Request request) { + return Boolean.parseBoolean(request.queryParamOrDefault("forced", "false")); + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/VehicleHandler.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/VehicleHandler.java new file mode 100644 index 0000000..ac7fc6d --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/VehicleHandler.java @@ -0,0 +1,334 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.components.kernel.services.RouterService; +import org.opentcs.components.kernel.services.VehicleService; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.model.Vehicle.EnergyLevelThresholdSet; +import org.opentcs.data.order.Route; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; +import org.opentcs.drivers.vehicle.management.VehicleAttachmentInformation; +import org.opentcs.kernel.extensions.servicewebapi.KernelExecutorWrapper; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.GetVehicleResponseTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PostVehicleRoutesRequestTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PutVehicleAllowedOrderTypesTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PutVehicleEnergyLevelThresholdSetTO; + +/** + * Handles requests related to vehicles. + */ +public class VehicleHandler { + + private final VehicleService vehicleService; + private final RouterService routerService; + private final KernelExecutorWrapper executorWrapper; + + /** + * Creates a new instance. + * + * @param vehicleService Used to update vehicle instances. + * @param routerService Used to get information about potential routes. + * @param executorWrapper Executes calls via the kernel executor and waits for the outcome. + */ + @Inject + public VehicleHandler( + VehicleService vehicleService, + RouterService routerService, + KernelExecutorWrapper executorWrapper + ) { + this.vehicleService = requireNonNull(vehicleService, "vehicleService"); + this.routerService = requireNonNull(routerService, "routerService"); + this.executorWrapper = requireNonNull(executorWrapper, "executorWrapper"); + } + + /** + * Find all vehicles orders and filters depending on the given parameters. + * + * @param procStateName The filter parameter for the processing state of the vehicle. + * The filtering is disabled for this parameter if the value is null. + * @return A list of vehicles, that match the filter. + * @throws IllegalArgumentException If procStateName could not be parsed. + */ + public List getVehiclesState( + @Nullable + String procStateName + ) + throws IllegalArgumentException { + return executorWrapper.callAndWait(() -> { + Vehicle.ProcState pState = procStateName == null + ? null + : Vehicle.ProcState.valueOf(procStateName); + + return vehicleService.fetchObjects(Vehicle.class, Filters.vehicleWithProcState(pState)) + .stream() + .map(GetVehicleResponseTO::fromVehicle) + .sorted(Comparator.comparing(GetVehicleResponseTO::getName)) + .collect(Collectors.toList()); + }); + } + + /** + * Finds the vehicle with the given name. + * + * @param name The name of the requested vehicle. + * @return A single vehicle that has the given name. + * @throws ObjectUnknownException If a vehicle with the given name does not exist. + */ + public GetVehicleResponseTO getVehicleStateByName(String name) + throws ObjectUnknownException { + requireNonNull(name, "name"); + + return executorWrapper.callAndWait(() -> { + return Optional.ofNullable(vehicleService.fetchObject(Vehicle.class, name)) + .map(GetVehicleResponseTO::fromVehicle) + .orElseThrow(() -> new ObjectUnknownException("Unknown vehicle: " + name)); + }); + } + + public void putVehicleIntegrationLevel(String name, String value) + throws ObjectUnknownException, + IllegalArgumentException { + requireNonNull(name, "name"); + requireNonNull(value, "value"); + + executorWrapper.callAndWait(() -> { + Vehicle vehicle = vehicleService.fetchObject(Vehicle.class, name); + if (vehicle == null) { + throw new ObjectUnknownException("Unknown vehicle: " + name); + } + + vehicleService.updateVehicleIntegrationLevel( + vehicle.getReference(), + Vehicle.IntegrationLevel.valueOf(value) + ); + }); + } + + public void putVehiclePaused(String name, String value) + throws ObjectUnknownException, + IllegalArgumentException { + requireNonNull(name, "name"); + requireNonNull(value, "value"); + + executorWrapper.callAndWait(() -> { + Vehicle vehicle = vehicleService.fetchObject(Vehicle.class, name); + if (vehicle == null) { + throw new ObjectUnknownException("Unknown vehicle: " + name); + } + + vehicleService.updateVehiclePaused(vehicle.getReference(), Boolean.parseBoolean(value)); + }); + } + + public void putVehicleEnvelopeKey(String name, String value) + throws ObjectUnknownException, + IllegalArgumentException { + requireNonNull(name, "name"); + + executorWrapper.callAndWait(() -> { + Vehicle vehicle = vehicleService.fetchObject(Vehicle.class, name); + if (vehicle == null) { + throw new ObjectUnknownException("Unknown vehicle: " + name); + } + + vehicleService.updateVehicleEnvelopeKey(vehicle.getReference(), value); + }); + } + + public void putVehicleCommAdapterEnabled(String name, String value) + throws ObjectUnknownException, + IllegalArgumentException { + requireNonNull(name, "name"); + requireNonNull(value, "value"); + + executorWrapper.callAndWait(() -> { + Vehicle vehicle = vehicleService.fetchObject(Vehicle.class, name); + if (vehicle == null) { + throw new ObjectUnknownException("Unknown vehicle: " + name); + } + + if (Boolean.parseBoolean(value)) { + vehicleService.enableCommAdapter(vehicle.getReference()); + } + else { + vehicleService.disableCommAdapter(vehicle.getReference()); + } + }); + } + + public VehicleAttachmentInformation getVehicleCommAdapterAttachmentInformation(String name) + throws ObjectUnknownException { + requireNonNull(name, "name"); + + return executorWrapper.callAndWait(() -> { + Vehicle vehicle = vehicleService.fetchObject(Vehicle.class, name); + if (vehicle == null) { + throw new ObjectUnknownException("Unknown vehicle: " + name); + } + + return vehicleService.fetchAttachmentInformation(vehicle.getReference()); + }); + } + + public void putVehicleCommAdapter(String name, String value) + throws ObjectUnknownException { + requireNonNull(name, "name"); + requireNonNull(value, "value"); + + executorWrapper.callAndWait(() -> { + Vehicle vehicle = vehicleService.fetchObject(Vehicle.class, name); + if (vehicle == null) { + throw new ObjectUnknownException("Unknown vehicle: " + name); + } + + VehicleCommAdapterDescription newAdapter + = vehicleService.fetchAttachmentInformation(vehicle.getReference()) + .getAvailableCommAdapters() + .stream() + .filter(description -> description.getClass().getName().equals(value)) + .findAny() + .orElseThrow( + () -> new IllegalArgumentException("Unknown vehicle driver class name: " + value) + ); + vehicleService.attachCommAdapter(vehicle.getReference(), newAdapter); + }); + } + + public void putVehicleAllowedOrderTypes( + String name, + PutVehicleAllowedOrderTypesTO allowedOrderTypes + ) + throws ObjectUnknownException { + requireNonNull(name, "name"); + requireNonNull(allowedOrderTypes, "allowedOrderTypes"); + + executorWrapper.callAndWait(() -> { + Vehicle vehicle = vehicleService.fetchObject(Vehicle.class, name); + if (vehicle == null) { + throw new ObjectUnknownException("Unknown vehicle: " + name); + } + vehicleService.updateVehicleAllowedOrderTypes( + vehicle.getReference(), new HashSet<>(allowedOrderTypes.getOrderTypes()) + ); + }); + } + + public void putVehicleEnergyLevelThresholdSet( + String name, + PutVehicleEnergyLevelThresholdSetTO energyLevelThresholdSet + ) + throws ObjectUnknownException { + requireNonNull(name, "name"); + requireNonNull(energyLevelThresholdSet, "energyLevelThresholdSet"); + + executorWrapper.callAndWait(() -> { + Vehicle vehicle = vehicleService.fetchObject(Vehicle.class, name); + if (vehicle == null) { + throw new ObjectUnknownException("Unknown vehicle: " + name); + } + + vehicleService.updateVehicleEnergyLevelThresholdSet( + vehicle.getReference(), + new EnergyLevelThresholdSet( + energyLevelThresholdSet.getEnergyLevelCritical(), + energyLevelThresholdSet.getEnergyLevelGood(), + energyLevelThresholdSet.getEnergyLevelSufficientlyRecharged(), + energyLevelThresholdSet.getEnergyLevelFullyRecharged() + ) + ); + }); + } + + public Map, Route> getVehicleRoutes( + String name, + PostVehicleRoutesRequestTO request + ) + throws ObjectUnknownException { + requireNonNull(name, "name"); + requireNonNull(request, "request"); + + return executorWrapper.callAndWait(() -> { + Vehicle vehicle = vehicleService.fetchObject(Vehicle.class, name); + if (vehicle == null) { + throw new ObjectUnknownException("Unknown vehicle: " + name); + } + + TCSObjectReference sourcePointRef; + if (request.getSourcePoint() == null) { + if (vehicle.getCurrentPosition() == null) { + throw new IllegalArgumentException("Unknown vehicle position: " + vehicle.getName()); + } + sourcePointRef = vehicle.getCurrentPosition(); + } + else { + Point sourcePoint = vehicleService.fetchObject(Point.class, request.getSourcePoint()); + if (sourcePoint == null) { + throw new ObjectUnknownException("Unknown source point: " + request.getSourcePoint()); + } + sourcePointRef = sourcePoint.getReference(); + } + + Set> destinationPointRefs = request.getDestinationPoints() + .stream() + .map(destPointName -> { + Point destPoint = vehicleService.fetchObject(Point.class, destPointName); + if (destPoint == null) { + throw new ObjectUnknownException("Unknown destination point: " + destPointName); + } + return destPoint.getReference(); + }) + .collect(Collectors.toSet()); + + Set> resourcesToAvoid = new HashSet<>(); + + if (request.getResourcesToAvoid() != null) { + for (String resourceName : request.getResourcesToAvoid()) { + Point point = vehicleService.fetchObject(Point.class, resourceName); + if (point != null) { + resourcesToAvoid.add(point.getReference()); + continue; + } + + Path path = vehicleService.fetchObject(Path.class, resourceName); + if (path != null) { + resourcesToAvoid.add(path.getReference()); + continue; + } + + Location location = vehicleService.fetchObject(Location.class, resourceName); + if (location != null) { + resourcesToAvoid.add(location.getReference()); + continue; + } + + throw new ObjectUnknownException("Unknown resource: " + resourceName); + } + } + + return routerService.computeRoutes( + vehicle.getReference(), + sourcePointRef, + destinationPointRefs, + resourcesToAvoid + ); + }); + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetEventsResponseTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetEventsResponseTO.java new file mode 100644 index 0000000..6708ce9 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetEventsResponseTO.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import static java.util.Objects.requireNonNull; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.getevents.StatusMessage; + +/** + * A set of status messages sent via the status channel. + */ +public class GetEventsResponseTO { + + private Instant timeStamp = Instant.now(); + + private List statusMessages = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public GetEventsResponseTO() { + } + + public List getStatusMessages() { + return statusMessages; + } + + public GetEventsResponseTO setStatusMessages(List statusMessages) { + this.statusMessages = requireNonNull(statusMessages, "statusMessages"); + return this; + } + + public Instant getTimeStamp() { + return timeStamp; + } + + public GetEventsResponseTO setTimeStamp(Instant timeStamp) { + this.timeStamp = requireNonNull(timeStamp, "timeStamp"); + return this; + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetOrderSequenceResponseTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetOrderSequenceResponseTO.java new file mode 100644 index 0000000..302c7b3 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetOrderSequenceResponseTO.java @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.order.OrderConstants; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.Property; + +/** + * The current state of an order sequence. + */ +public class GetOrderSequenceResponseTO { + + @Nonnull + private String name; + + @Nonnull + private String type = OrderConstants.TYPE_NONE; + + @Nonnull + private List orders = List.of(); + + private int finishedIndex; + + private boolean complete; + + private boolean finished; + + private boolean failureFatal; + + @Nullable + private String intendedVehicle; + + @Nullable + private String processingVehicle; + + @Nonnull + private List properties = List.of(); + + public GetOrderSequenceResponseTO( + @Nonnull + String name + ) { + this.name = requireNonNull(name, "name"); + } + + @Nonnull + public String getName() { + return name; + } + + public GetOrderSequenceResponseTO setName( + @Nonnull + String name + ) { + this.name = requireNonNull(name, "name"); + return this; + } + + @Nonnull + public String getType() { + return type; + } + + public GetOrderSequenceResponseTO setType( + @Nonnull + String type + ) { + this.type = requireNonNull(type, "type"); + return this; + } + + @Nonnull + public List getOrders() { + return orders; + } + + public GetOrderSequenceResponseTO setOrders( + @Nonnull + List orders + ) { + this.orders = requireNonNull(orders, "orders"); + return this; + } + + public int getFinishedIndex() { + return finishedIndex; + } + + public GetOrderSequenceResponseTO setFinishedIndex(int finishedIndex) { + this.finishedIndex = finishedIndex; + return this; + } + + public boolean isComplete() { + return complete; + } + + public GetOrderSequenceResponseTO setComplete(boolean complete) { + this.complete = complete; + return this; + } + + public boolean isFinished() { + return finished; + } + + public GetOrderSequenceResponseTO setFinished(boolean finished) { + this.finished = finished; + return this; + } + + public boolean isFailureFatal() { + return failureFatal; + } + + public GetOrderSequenceResponseTO setFailureFatal(boolean failureFatal) { + this.failureFatal = failureFatal; + return this; + } + + @Nullable + public String getIntendedVehicle() { + return intendedVehicle; + } + + public GetOrderSequenceResponseTO setIntendedVehicle( + @Nullable + String intendedVehicle + ) { + this.intendedVehicle = intendedVehicle; + return this; + } + + @Nullable + public String getProcessingVehicle() { + return processingVehicle; + } + + public GetOrderSequenceResponseTO setProcessingVehicle( + @Nullable + String processingVehicle + ) { + this.processingVehicle = processingVehicle; + return this; + } + + @Nonnull + public List getProperties() { + return properties; + } + + public GetOrderSequenceResponseTO setProperties( + @Nonnull + List properties + ) { + this.properties = requireNonNull(properties, "properties"); + return this; + } + + public static GetOrderSequenceResponseTO fromOrderSequence(OrderSequence orderSequence) { + return new GetOrderSequenceResponseTO(orderSequence.getName()) + .setComplete(orderSequence.isComplete()) + .setFailureFatal(orderSequence.isFailureFatal()) + .setFinished(orderSequence.isFinished()) + .setFinishedIndex(orderSequence.getFinishedIndex()) + .setType(orderSequence.getType()) + .setOrders( + orderSequence.getOrders() + .stream() + .map(TCSObjectReference::getName) + .collect(Collectors.toList()) + ) + .setProcessingVehicle(nameOfNullableReference(orderSequence.getProcessingVehicle())) + .setIntendedVehicle(nameOfNullableReference(orderSequence.getIntendedVehicle())) + .setProperties(convertProperties(orderSequence.getProperties())); + } + + private static String nameOfNullableReference( + @Nullable + TCSObjectReference reference + ) { + return reference == null ? null : reference.getName(); + } + + private static List convertProperties(Map properties) { + return properties.entrySet().stream() + .map(property -> new Property(property.getKey(), property.getValue())) + .collect(Collectors.toList()); + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetPeripheralAttachmentInfoResponseTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetPeripheralAttachmentInfoResponseTO.java new file mode 100644 index 0000000..e3f2147 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetPeripheralAttachmentInfoResponseTO.java @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.List; +import java.util.stream.Collectors; +import org.opentcs.drivers.peripherals.management.PeripheralAttachmentInformation; + +/** + */ +public class GetPeripheralAttachmentInfoResponseTO { + + @Nonnull + private String locationName; + + @Nonnull + private List availableCommAdapters; + + @Nonnull + private String attachedCommAdapter; + + public GetPeripheralAttachmentInfoResponseTO( + @Nonnull + String locationName, + @Nonnull + String attachedCommAdapter, + @Nonnull + List availableCommAdapters + ) { + this.locationName = requireNonNull(locationName, "locationName"); + this.attachedCommAdapter = requireNonNull(attachedCommAdapter, "attachedCommAdapter"); + this.availableCommAdapters = requireNonNull(availableCommAdapters, "availableCommAdapters"); + } + + @Nonnull + public String getLocationName() { + return locationName; + } + + public GetPeripheralAttachmentInfoResponseTO setLocationName( + @Nonnull + String locationName + ) { + this.locationName = requireNonNull(locationName, "locationName"); + return this; + } + + @Nonnull + public List getAvailableCommAdapters() { + return availableCommAdapters; + } + + public GetPeripheralAttachmentInfoResponseTO setAvailableCommAdapters( + @Nonnull + List availableCommAdapters + ) { + this.availableCommAdapters = requireNonNull(availableCommAdapters, "availableCommAdapters"); + return this; + } + + @Nonnull + public String getAttachedCommAdapter() { + return attachedCommAdapter; + } + + public GetPeripheralAttachmentInfoResponseTO setAttachedCommAdapter( + @Nonnull + String attachedCommAdapter + ) { + this.attachedCommAdapter = requireNonNull(attachedCommAdapter, "attachedCommAdapter"); + return this; + } + + public static GetPeripheralAttachmentInfoResponseTO fromAttachmentInformation( + @Nullable + PeripheralAttachmentInformation peripheralAttachmentInfo + ) { + if (peripheralAttachmentInfo == null) { + return null; + } + + List availableAdapters = peripheralAttachmentInfo.getAvailableCommAdapters() + .stream() + .map(description -> description.getClass().getName()) + .collect(Collectors.toList()); + + return new GetPeripheralAttachmentInfoResponseTO( + peripheralAttachmentInfo.getLocationReference().getName(), + peripheralAttachmentInfo.getAttachedCommAdapter().getClass().getName(), + availableAdapters + ); + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetPeripheralJobResponseTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetPeripheralJobResponseTO.java new file mode 100644 index 0000000..aa988b3 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetPeripheralJobResponseTO.java @@ -0,0 +1,142 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import java.time.Instant; +import java.util.List; +import java.util.stream.Collectors; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.data.peripherals.PeripheralJob.State; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PeripheralOperationDescription; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.Property; + +/** + * The current state of a peripheral job. + */ +public class GetPeripheralJobResponseTO { + + private String name; + + private String reservationToken; + + private String relatedVehicle; + + private String relatedTransportOrder; + + private PeripheralOperationDescription peripheralOperation; + + private State state; + + private Instant creationTime; + + private Instant finishedTime; + + private List properties; + + public GetPeripheralJobResponseTO() { + } + + public String getName() { + return name; + } + + public GetPeripheralJobResponseTO setName(String name) { + this.name = name; + return this; + } + + public String getReservationToken() { + return reservationToken; + } + + public GetPeripheralJobResponseTO setReservationToken(String reservationToken) { + this.reservationToken = reservationToken; + return this; + } + + public String getRelatedVehicle() { + return relatedVehicle; + } + + public GetPeripheralJobResponseTO setRelatedVehicle(String relatedVehicle) { + this.relatedVehicle = relatedVehicle; + return this; + } + + public String getRelatedTransportOrder() { + return relatedTransportOrder; + } + + public GetPeripheralJobResponseTO setRelatedTransportOrder(String relatedTransportOrder) { + this.relatedTransportOrder = relatedTransportOrder; + return this; + } + + public PeripheralOperationDescription getPeripheralOperation() { + return peripheralOperation; + } + + public GetPeripheralJobResponseTO setPeripheralOperation( + PeripheralOperationDescription peripheralOperation + ) { + this.peripheralOperation = peripheralOperation; + return this; + } + + public State getState() { + return state; + } + + public GetPeripheralJobResponseTO setState(State state) { + this.state = state; + return this; + } + + public Instant getCreationTime() { + return creationTime; + } + + public GetPeripheralJobResponseTO setCreationTime(Instant creationTime) { + this.creationTime = creationTime; + return this; + } + + public Instant getFinishedTime() { + return finishedTime; + } + + public GetPeripheralJobResponseTO setFinishedTime(Instant finishedTime) { + this.finishedTime = finishedTime; + return this; + } + + public List getProperties() { + return properties; + } + + public GetPeripheralJobResponseTO setProperties(List properties) { + this.properties = properties; + return this; + } + + public static GetPeripheralJobResponseTO fromPeripheralJob(PeripheralJob job) { + GetPeripheralJobResponseTO state = new GetPeripheralJobResponseTO(); + state.name = job.getName(); + state.reservationToken = job.getReservationToken(); + if (job.getRelatedVehicle() != null) { + state.relatedVehicle = job.getRelatedVehicle().getName(); + } + if (job.getRelatedTransportOrder() != null) { + state.relatedTransportOrder = job.getRelatedTransportOrder().getName(); + } + state.peripheralOperation + = PeripheralOperationDescription.fromPeripheralOperation(job.getPeripheralOperation()); + state.state = job.getState(); + state.creationTime = job.getCreationTime(); + state.finishedTime = job.getFinishedTime(); + state.properties = job.getProperties().entrySet().stream() + .map(entry -> new Property(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + return state; + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetTransportOrderResponseTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetTransportOrderResponseTO.java new file mode 100644 index 0000000..eb9aec5 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetTransportOrderResponseTO.java @@ -0,0 +1,165 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.DestinationState; + +/** + */ +public class GetTransportOrderResponseTO { + + private boolean dispensable; + + private String name = ""; + + private String peripheralReservationToken; + + private String wrappingSequence; + + private String type = ""; + + private TransportOrder.State state = TransportOrder.State.RAW; + + private String intendedVehicle; + + private String processingVehicle; + + private List destinations = new ArrayList<>(); + + public GetTransportOrderResponseTO() { + } + + public boolean isDispensable() { + return dispensable; + } + + public GetTransportOrderResponseTO setDispensable(boolean dispensable) { + this.dispensable = dispensable; + return this; + } + + public String getName() { + return name; + } + + public GetTransportOrderResponseTO setName(String name) { + this.name = requireNonNull(name, "name"); + return this; + } + + public String getPeripheralReservationToken() { + return peripheralReservationToken; + } + + public GetTransportOrderResponseTO setPeripheralReservationToken( + String peripheralReservationToken + ) { + this.peripheralReservationToken = peripheralReservationToken; + return this; + } + + public String getWrappingSequence() { + return wrappingSequence; + } + + public GetTransportOrderResponseTO setWrappingSequence(String wrappingSequence) { + this.wrappingSequence = wrappingSequence; + return this; + } + + public String getType() { + return type; + } + + public GetTransportOrderResponseTO setType(String type) { + this.type = type; + return this; + } + + public TransportOrder.State getState() { + return state; + } + + public GetTransportOrderResponseTO setState(TransportOrder.State state) { + this.state = requireNonNull(state, "state"); + return this; + } + + public String getIntendedVehicle() { + return intendedVehicle; + } + + public GetTransportOrderResponseTO setIntendedVehicle(String intendedVehicle) { + this.intendedVehicle = intendedVehicle; + return this; + } + + public String getProcessingVehicle() { + return processingVehicle; + } + + public GetTransportOrderResponseTO setProcessingVehicle(String processingVehicle) { + this.processingVehicle = processingVehicle; + return this; + } + + public List getDestinations() { + return destinations; + } + + public GetTransportOrderResponseTO setDestinations(List destinations) { + this.destinations = requireNonNull(destinations, "destinations"); + return this; + } + + /** + * Creates a new instance from a TransportOrder. + * + * @param transportOrder The transport order to create an instance from. + * @return A new instance containing the data from the given transport order. + */ + public static GetTransportOrderResponseTO fromTransportOrder(TransportOrder transportOrder) { + if (transportOrder == null) { + return null; + } + GetTransportOrderResponseTO transportOrderState = new GetTransportOrderResponseTO(); + transportOrderState.setDispensable(transportOrder.isDispensable()); + transportOrderState.setName(transportOrder.getName()); + transportOrderState.setPeripheralReservationToken( + transportOrder.getPeripheralReservationToken() + ); + transportOrderState.setWrappingSequence( + nameOfNullableReference(transportOrder.getWrappingSequence()) + ); + transportOrderState.setType(transportOrder.getType()); + transportOrderState.setDestinations( + transportOrder.getAllDriveOrders() + .stream() + .map(driveOrder -> DestinationState.fromDriveOrder(driveOrder)) + .collect(Collectors.toList()) + ); + transportOrderState.setIntendedVehicle( + nameOfNullableReference(transportOrder.getIntendedVehicle()) + ); + transportOrderState.setProcessingVehicle( + nameOfNullableReference(transportOrder.getProcessingVehicle()) + ); + transportOrderState.setState(transportOrder.getState()); + return transportOrderState; + } + + private static String nameOfNullableReference( + @Nullable + TCSObjectReference reference + ) { + return reference == null ? null : reference.getName(); + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleAttachmentInfoResponseTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleAttachmentInfoResponseTO.java new file mode 100644 index 0000000..10c56e1 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleAttachmentInfoResponseTO.java @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.stream.Collectors; +import org.opentcs.drivers.vehicle.management.VehicleAttachmentInformation; + +/** + * Arranges the data from a vehicle's AttachmentInformation for transferring. + */ +public class GetVehicleAttachmentInfoResponseTO { + + /** + * The vehicle this attachment information belongs to. + */ + private String vehicleName; + /** + * The list of comm adapters available to be attached to the referenced vehicle. + */ + private List availableCommAdapters; + /** + * The comm adapter attached to the referenced vehicle. + */ + private String attachedCommAdapter; + + public GetVehicleAttachmentInfoResponseTO() { + } + + public GetVehicleAttachmentInfoResponseTO setVehicleName(String vehicleName) { + this.vehicleName = requireNonNull(vehicleName, "vehicleName"); + return this; + } + + public String getVehicleName() { + return vehicleName; + } + + public GetVehicleAttachmentInfoResponseTO setAvailableCommAdapters( + List availableCommAdapters + ) { + this.availableCommAdapters = requireNonNull(availableCommAdapters, "availableCommAdapters"); + return this; + } + + public List getAvailableCommAdapters() { + return availableCommAdapters; + } + + public GetVehicleAttachmentInfoResponseTO setAttachedCommAdapter(String attachedCommAdapter) { + this.attachedCommAdapter = requireNonNull(attachedCommAdapter, "attachedCommAdapter"); + return this; + } + + public String getAttachedCommAdapter() { + return attachedCommAdapter; + } + + /** + * Creates a new instance from AttachmentInformation. + * + * @param attachmentInformation The AttachmentInformation to create an + * instance from. + * @return A new instance containing the data from the given AttachmentInformation. + */ + public static GetVehicleAttachmentInfoResponseTO fromAttachmentInformation( + VehicleAttachmentInformation attachmentInformation + ) { + if (attachmentInformation == null) { + return null; + } + GetVehicleAttachmentInfoResponseTO attachmentInformationTO + = new GetVehicleAttachmentInfoResponseTO(); + + attachmentInformationTO.setVehicleName( + attachmentInformation.getVehicleReference() + .getName() + ); + attachmentInformationTO.setAvailableCommAdapters( + attachmentInformation.getAvailableCommAdapters() + .stream() + .map(description -> description.getClass().getName()) + .collect(Collectors.toList()) + ); + attachmentInformationTO.setAttachedCommAdapter( + attachmentInformation.getAttachedCommAdapter() + .getClass() + .getName() + ); + + return attachmentInformationTO; + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleResponseTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleResponseTO.java new file mode 100644 index 0000000..7c8657b --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleResponseTO.java @@ -0,0 +1,391 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.model.Vehicle.IntegrationLevel; +import org.opentcs.data.model.Vehicle.ProcState; +import org.opentcs.data.model.Vehicle.State; +import org.opentcs.util.annotations.ScheduledApiChange; + +/** + */ +public class GetVehicleResponseTO { + + private String name; + + private Map properties = new HashMap<>(); + + private int length; + + private int energyLevelGood; + + private int energyLevelCritical; + + private int energyLevelSufficientlyRecharged; + + private int energyLevelFullyRecharged; + + private int energyLevel; + + private IntegrationLevel integrationLevel = IntegrationLevel.TO_BE_RESPECTED; + + private boolean paused; + + private ProcState procState = ProcState.IDLE; + + private String transportOrder; + + private String currentPosition; + + private PrecisePosition precisePosition; + + private double orientationAngle; + + private State state = State.UNKNOWN; + + private List> allocatedResources = new ArrayList<>(); + + private List> claimedResources = new ArrayList<>(); + + private List allowedOrderTypes = new ArrayList<>(); + + private String envelopeKey; + + public GetVehicleResponseTO() { + } + + public String getName() { + return name; + } + + public GetVehicleResponseTO setName(String name) { + this.name = requireNonNull(name, "name"); + return this; + } + + public int getLength() { + return length; + } + + public GetVehicleResponseTO setLength(int length) { + this.length = length; + return this; + } + + public int getEnergyLevelGood() { + return energyLevelGood; + } + + public GetVehicleResponseTO setEnergyLevelGood(int energyLevelGood) { + this.energyLevelGood = energyLevelGood; + return this; + } + + public int getEnergyLevelCritical() { + return energyLevelCritical; + } + + public GetVehicleResponseTO setEnergyLevelCritical(int energyLevelCritical) { + this.energyLevelCritical = energyLevelCritical; + return this; + } + + public int getEnergyLevelSufficientlyRecharged() { + return energyLevelSufficientlyRecharged; + } + + public GetVehicleResponseTO setEnergyLevelSufficientlyRecharged( + int energyLevelSufficientlyRecharged + ) { + this.energyLevelSufficientlyRecharged = energyLevelSufficientlyRecharged; + return this; + } + + public int getEnergyLevelFullyRecharged() { + return energyLevelFullyRecharged; + } + + public GetVehicleResponseTO setEnergyLevelFullyRecharged(int energyLevelFullyRecharged) { + this.energyLevelFullyRecharged = energyLevelFullyRecharged; + return this; + } + + public int getEnergyLevel() { + return energyLevel; + } + + public GetVehicleResponseTO setEnergyLevel(int energyLevel) { + this.energyLevel = energyLevel; + return this; + } + + public IntegrationLevel getIntegrationLevel() { + return integrationLevel; + } + + public GetVehicleResponseTO setIntegrationLevel(IntegrationLevel integrationLevel) { + this.integrationLevel = requireNonNull(integrationLevel, "integrationLevel"); + return this; + } + + public boolean isPaused() { + return paused; + } + + public GetVehicleResponseTO setPaused(boolean paused) { + this.paused = paused; + return this; + } + + public ProcState getProcState() { + return procState; + } + + public GetVehicleResponseTO setProcState(Vehicle.ProcState procState) { + this.procState = requireNonNull(procState, "procState"); + return this; + } + + public String getTransportOrder() { + return transportOrder; + } + + public GetVehicleResponseTO setTransportOrder(String transportOrder) { + this.transportOrder = transportOrder; + return this; + } + + public String getCurrentPosition() { + return currentPosition; + } + + public GetVehicleResponseTO setCurrentPosition(String currentPosition) { + this.currentPosition = currentPosition; + return this; + } + + public PrecisePosition getPrecisePosition() { + return precisePosition; + } + + public GetVehicleResponseTO setPrecisePosition(PrecisePosition precisePosition) { + this.precisePosition = precisePosition; + return this; + } + + public double getOrientationAngle() { + return orientationAngle; + } + + public GetVehicleResponseTO setOrientationAngle(double orientationAngle) { + this.orientationAngle = orientationAngle; + return this; + } + + public State getState() { + return state; + } + + public GetVehicleResponseTO setState(State state) { + this.state = requireNonNull(state, "state"); + return this; + } + + public List> getAllocatedResources() { + return allocatedResources; + } + + public GetVehicleResponseTO setAllocatedResources(List> allocatedResources) { + this.allocatedResources = requireNonNull(allocatedResources, "allocatedResources"); + return this; + } + + public List> getClaimedResources() { + return claimedResources; + } + + public GetVehicleResponseTO setClaimedResources(List> claimedResources) { + this.claimedResources = requireNonNull(claimedResources, "claimedResources"); + return this; + } + + public Map getProperties() { + return properties; + } + + public GetVehicleResponseTO setProperties(Map properties) { + this.properties = requireNonNull(properties, "properties"); + return this; + } + + public List getAllowedOrderTypes() { + return allowedOrderTypes; + } + + public GetVehicleResponseTO setAllowedOrderTypes(List allowedOrderTypes) { + this.allowedOrderTypes = requireNonNull(allowedOrderTypes, "allowedOrderTypes"); + return this; + } + + @ScheduledApiChange(when = "7.0", details = "Envelope key will become non-null.") + @Nullable + public String getEnvelopeKey() { + return envelopeKey; + } + + @ScheduledApiChange(when = "7.0", details = "Envelope key will become non-null.") + public GetVehicleResponseTO setEnvelopeKey( + @Nullable + String envelopeKey + ) { + this.envelopeKey = envelopeKey; + return this; + } + + /** + * Creates a VehicleState instance from a Vehicle instance. + * + * @param vehicle The vehicle whose properties will be used to create a VehicleState + * instance. + * @return A new VehicleState instance filled with data from the given vehicle. + */ + public static GetVehicleResponseTO fromVehicle(Vehicle vehicle) { + if (vehicle == null) { + return null; + } + GetVehicleResponseTO vehicleState = new GetVehicleResponseTO(); + vehicleState.setName(vehicle.getName()); + vehicleState.setProperties(vehicle.getProperties()); + vehicleState.setLength((int) vehicle.getBoundingBox().getLength()); + vehicleState.setEnergyLevelCritical( + vehicle.getEnergyLevelThresholdSet().getEnergyLevelCritical() + ); + vehicleState.setEnergyLevelGood(vehicle.getEnergyLevelThresholdSet().getEnergyLevelGood()); + vehicleState.setEnergyLevelSufficientlyRecharged( + vehicle.getEnergyLevelThresholdSet().getEnergyLevelSufficientlyRecharged() + ); + vehicleState.setEnergyLevelFullyRecharged( + vehicle.getEnergyLevelThresholdSet().getEnergyLevelFullyRecharged() + ); + vehicleState.setEnergyLevel(vehicle.getEnergyLevel()); + vehicleState.setIntegrationLevel(vehicle.getIntegrationLevel()); + vehicleState.setPaused(vehicle.isPaused()); + vehicleState.setProcState(vehicle.getProcState()); + vehicleState.setTransportOrder(nameOfNullableReference(vehicle.getTransportOrder())); + vehicleState.setCurrentPosition(nameOfNullableReference(vehicle.getCurrentPosition())); + if (vehicle.getPose().getPosition() != null) { + vehicleState.setPrecisePosition( + new PrecisePosition( + vehicle.getPose().getPosition().getX(), + vehicle.getPose().getPosition().getY(), + vehicle.getPose().getPosition().getZ() + ) + ); + } + else { + vehicleState.setPrecisePosition(null); + } + vehicleState.setOrientationAngle(vehicle.getPose().getOrientationAngle()); + vehicleState.setState(vehicle.getState()); + vehicleState.setAllocatedResources(toListOfListOfNames(vehicle.getAllocatedResources())); + vehicleState.setClaimedResources(toListOfListOfNames(vehicle.getClaimedResources())); + vehicleState.setEnvelopeKey(vehicle.getEnvelopeKey()); + vehicleState.setAllowedOrderTypes( + vehicle.getAllowedOrderTypes() + .stream() + .sorted() + .collect(Collectors.toCollection(ArrayList::new)) + ); + return vehicleState; + } + + private static String nameOfNullableReference( + @Nullable + TCSObjectReference reference + ) { + return reference == null ? null : reference.getName(); + } + + private static List> toListOfListOfNames( + List>> resources + ) { + List> result = new ArrayList<>(resources.size()); + + for (Set> resSet : resources) { + result.add( + resSet.stream() + .map(resRef -> resRef.getName()) + .collect(Collectors.toList()) + ); + } + + return result; + } + + /** + * A precise position of a vehicle. + */ + public static class PrecisePosition { + + private long x; + + private long y; + + private long z; + + /** + * Creates a new instance. + */ + public PrecisePosition() { + } + + /** + * Creates a new instance. + * + * @param x x value + * @param y y value + * @param z z value + */ + public PrecisePosition(long x, long y, long z) { + this.x = x; + this.y = y; + this.z = z; + } + + public long getX() { + return x; + } + + public void setX(long x) { + this.x = x; + } + + public long getY() { + return y; + } + + public void setY(long y) { + this.y = y; + } + + public long getZ() { + return z; + } + + public void setZ(long z) { + this.z = z; + } + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PlantModelTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PlantModelTO.java new file mode 100644 index 0000000..a6d2b5d --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PlantModelTO.java @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nonnull; +import java.util.List; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.BlockTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.LocationTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.LocationTypeTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.PathTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.PointTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.VehicleTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.VisualLayoutTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PropertyTO; + +/** + */ +public class PlantModelTO { + + private String name; + private List points = List.of(); + private List paths = List.of(); + private List locationTypes = List.of(); + private List locations = List.of(); + private List blocks = List.of(); + private List vehicles = List.of(); + private VisualLayoutTO visualLayout = new VisualLayoutTO("unnamed"); + private List properties = List.of(); + + @JsonCreator + public PlantModelTO( + @Nonnull + @JsonProperty(value = "name", required = true) + String name + ) { + this.name = requireNonNull(name, "name"); + } + + @Nonnull + public String getName() { + return name; + } + + public PlantModelTO setName( + @Nonnull + String name + ) { + this.name = requireNonNull(name, "name"); + return this; + } + + @Nonnull + public List getProperties() { + return properties; + } + + public PlantModelTO setProperties( + @Nonnull + List properties + ) { + this.properties = requireNonNull(properties, "properties"); + return this; + } + + @Nonnull + public List getPoints() { + return points; + } + + public PlantModelTO setPoints( + @Nonnull + List points + ) { + this.points = requireNonNull(points, "points"); + return this; + } + + @Nonnull + public List getPaths() { + return paths; + } + + public PlantModelTO setPaths( + @Nonnull + List paths + ) { + this.paths = requireNonNull(paths, "paths"); + return this; + } + + @Nonnull + public List getLocations() { + return locations; + } + + public PlantModelTO setLocations( + @Nonnull + List locations + ) { + this.locations = requireNonNull(locations, "locations"); + return this; + } + + @Nonnull + public List getLocationTypes() { + return locationTypes; + } + + public PlantModelTO setLocationTypes( + @Nonnull + List locationTypes + ) { + this.locationTypes = requireNonNull(locationTypes, "locationTypes"); + return this; + } + + @Nonnull + public List getBlocks() { + return blocks; + } + + public PlantModelTO setBlocks( + @Nonnull + List blocks + ) { + this.blocks = requireNonNull(blocks, "blocks"); + return this; + } + + @Nonnull + public List getVehicles() { + return vehicles; + } + + public PlantModelTO setVehicles( + @Nonnull + List vehicles + ) { + this.vehicles = requireNonNull(vehicles, "vehicles"); + return this; + } + + @Nonnull + public VisualLayoutTO getVisualLayout() { + return visualLayout; + } + + public PlantModelTO setVisualLayout( + @Nonnull + VisualLayoutTO visualLayout + ) { + this.visualLayout = requireNonNull(visualLayout, "visualLayout"); + return this; + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostOrderSequenceRequestTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostOrderSequenceRequestTO.java new file mode 100644 index 0000000..938fa22 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostOrderSequenceRequestTO.java @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.List; +import org.opentcs.data.order.OrderConstants; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.Property; + +/** + * An order sequence to be created by the kernel. + */ +public class PostOrderSequenceRequestTO { + + private boolean incompleteName; + + @Nonnull + private String type = OrderConstants.TYPE_NONE; + + @Nullable + private String intendedVehicle; + + private boolean failureFatal; + + @Nonnull + private List properties = List.of(); + + public PostOrderSequenceRequestTO() { + + } + + public boolean isIncompleteName() { + return incompleteName; + } + + public PostOrderSequenceRequestTO setIncompleteName(boolean incompleteName) { + this.incompleteName = incompleteName; + return this; + } + + @Nonnull + public String getType() { + return type; + } + + public PostOrderSequenceRequestTO setType( + @Nonnull + String type + ) { + this.type = requireNonNull(type, "type"); + return this; + } + + @Nullable + public String getIntendedVehicle() { + return intendedVehicle; + } + + public PostOrderSequenceRequestTO setIntendedVehicle( + @Nullable + String intendedVehicle + ) { + this.intendedVehicle = intendedVehicle; + return this; + } + + public boolean isFailureFatal() { + return failureFatal; + } + + public PostOrderSequenceRequestTO setFailureFatal(boolean failureFatal) { + this.failureFatal = failureFatal; + return this; + } + + @Nonnull + public List getProperties() { + return properties; + } + + public PostOrderSequenceRequestTO setProperties( + @Nonnull + List properties + ) { + this.properties = requireNonNull(properties, "properties"); + return this; + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostPeripheralJobRequestTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostPeripheralJobRequestTO.java new file mode 100644 index 0000000..1b92cda --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostPeripheralJobRequestTO.java @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import java.util.List; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PeripheralOperationDescription; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.Property; + +/** + * A peripheral job to be processed by the kernel. + */ +public class PostPeripheralJobRequestTO { + + private boolean incompleteName; + + private String reservationToken; + + private String relatedVehicle; + + private String relatedTransportOrder; + + private PeripheralOperationDescription peripheralOperation; + + private List properties; + + public PostPeripheralJobRequestTO() { + } + + public boolean isIncompleteName() { + return incompleteName; + } + + public PostPeripheralJobRequestTO setIncompleteName(boolean incompleteName) { + this.incompleteName = incompleteName; + return this; + } + + public String getReservationToken() { + return reservationToken; + } + + public PostPeripheralJobRequestTO setReservationToken(String reservationToken) { + this.reservationToken = reservationToken; + return this; + } + + public String getRelatedVehicle() { + return relatedVehicle; + } + + public PostPeripheralJobRequestTO setRelatedVehicle(String relatedVehicle) { + this.relatedVehicle = relatedVehicle; + return this; + } + + public String getRelatedTransportOrder() { + return relatedTransportOrder; + } + + public PostPeripheralJobRequestTO setRelatedTransportOrder(String relatedTransportOrder) { + this.relatedTransportOrder = relatedTransportOrder; + return this; + } + + public PeripheralOperationDescription getPeripheralOperation() { + return peripheralOperation; + } + + public PostPeripheralJobRequestTO setPeripheralOperation( + PeripheralOperationDescription peripheralOperation + ) { + this.peripheralOperation = peripheralOperation; + return this; + } + + public List getProperties() { + return properties; + } + + public PostPeripheralJobRequestTO setProperties(List properties) { + this.properties = properties; + return this; + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostTopologyUpdateRequestTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostTopologyUpdateRequestTO.java new file mode 100644 index 0000000..d7f3d83 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostTopologyUpdateRequestTO.java @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nonnull; +import java.util.List; + +/** + * A list of paths that are to be updated when the routing topology gets updated. + */ +public class PostTopologyUpdateRequestTO { + + @Nonnull + private List paths; + + @JsonCreator + public PostTopologyUpdateRequestTO( + @Nonnull + @JsonProperty(value = "paths", required = true) + List paths + ) { + this.paths = requireNonNull(paths, "paths"); + } + + @Nonnull + public List getPaths() { + return paths; + } + + public PostTopologyUpdateRequestTO setPaths( + @Nonnull + List paths + ) { + this.paths = requireNonNull(paths, "paths"); + return this; + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostTransportOrderRequestTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostTransportOrderRequestTO.java new file mode 100644 index 0000000..883b1e7 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostTransportOrderRequestTO.java @@ -0,0 +1,210 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.time.Instant; +import java.util.List; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.posttransportorder.Destination; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.Property; + +/** + * A transport order to be processed by the kernel. + */ +public class PostTransportOrderRequestTO { + + private boolean incompleteName; + + private boolean dispensable; + + private Instant deadline; + + private String intendedVehicle; + + private String peripheralReservationToken; + + private String wrappingSequence; + + private String type; + + private List destinations; + + private List properties; + + private List dependencies; + + // CHECKSTYLE:OFF (because of very long parameter declarations) + @JsonCreator + public PostTransportOrderRequestTO( + @JsonProperty(required = false, value = "incompleteName") + boolean incompleteName, + @JsonProperty(required = false, value = "dispensable") + boolean dispensable, + @Nullable + @JsonProperty(required = false, value = "deadline") + Instant deadline, + @Nullable + @JsonProperty(required = false, value = "intendedVehicle") + String intendedVehicle, + @Nullable + @JsonProperty(required = false, value = "peripheralReservationToken") + String peripheralReservationToken, + @Nullable + @JsonProperty(required = false, value = "wrappingSequence") + String wrappingSequence, + @Nullable + @JsonProperty(required = false, value = "type") + String type, + @Nonnull + @JsonProperty(required = true, value = "destinations") + List destinations, + @Nullable + @JsonProperty(required = false, value = "properties") + List properties, + @Nullable + @JsonProperty(required = false, value = "dependencies") + List dependencies + ) { + this.incompleteName = incompleteName; + this.dispensable = dispensable; + this.deadline = deadline; + this.intendedVehicle = intendedVehicle; + this.peripheralReservationToken = peripheralReservationToken; + this.wrappingSequence = wrappingSequence; + this.type = type; + this.destinations = requireNonNull(destinations, "destinations"); + this.properties = properties; + this.dependencies = dependencies; + } + // CHECKSTYLE:ON + + public PostTransportOrderRequestTO() { + } + + public boolean isIncompleteName() { + return incompleteName; + } + + public PostTransportOrderRequestTO setIncompleteName(boolean incompleteName) { + this.incompleteName = incompleteName; + return this; + } + + public boolean isDispensable() { + return dispensable; + } + + public PostTransportOrderRequestTO setDispensable(boolean dispensable) { + this.dispensable = dispensable; + return this; + } + + @Nullable + public Instant getDeadline() { + return deadline; + } + + public PostTransportOrderRequestTO setDeadline( + @Nullable + Instant deadline + ) { + this.deadline = deadline; + return this; + } + + @Nullable + public String getIntendedVehicle() { + return intendedVehicle; + } + + public PostTransportOrderRequestTO setIntendedVehicle( + @Nullable + String intendedVehicle + ) { + this.intendedVehicle = intendedVehicle; + return this; + } + + @Nullable + public String getPeripheralReservationToken() { + return peripheralReservationToken; + } + + public PostTransportOrderRequestTO setPeripheralReservationToken( + @Nullable + String peripheralReservationToken + ) { + this.peripheralReservationToken = peripheralReservationToken; + return this; + } + + @Nullable + public String getWrappingSequence() { + return wrappingSequence; + } + + public PostTransportOrderRequestTO setWrappingSequence( + @Nullable + String wrappingSequence + ) { + this.wrappingSequence = wrappingSequence; + return this; + } + + @Nullable + public String getType() { + return type; + } + + public PostTransportOrderRequestTO setType( + @Nullable + String type + ) { + this.type = type; + return this; + } + + @Nonnull + public List getDestinations() { + return destinations; + } + + public PostTransportOrderRequestTO setDestinations( + @Nonnull + List destinations + ) { + this.destinations = requireNonNull(destinations, "destinations"); + return this; + } + + @Nullable + public List getProperties() { + return properties; + } + + public PostTransportOrderRequestTO setProperties( + @Nullable + List properties + ) { + this.properties = properties; + return this; + } + + @Nullable + public List getDependencies() { + return dependencies; + } + + public PostTransportOrderRequestTO setDependencies( + @Nullable + List dependencies + ) { + this.dependencies = dependencies; + return this; + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostVehicleRoutesRequestTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostVehicleRoutesRequestTO.java new file mode 100644 index 0000000..63ea9c6 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostVehicleRoutesRequestTO.java @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.List; + +/** + * + */ +public class PostVehicleRoutesRequestTO { + + private String sourcePoint; + private List destinationPoints; + private List resourcesToAvoid; + + @JsonCreator + @SuppressWarnings("checkstyle:LineLength") + public PostVehicleRoutesRequestTO( + @Nonnull + @JsonProperty(value = "destinationPoints", required = true) + List destinationPoints + ) { + this.destinationPoints = requireNonNull(destinationPoints, "destinationPoints"); + } + + @Nullable + public String getSourcePoint() { + return sourcePoint; + } + + public PostVehicleRoutesRequestTO setSourcePoint( + @Nullable + String sourcePoint + ) { + this.sourcePoint = sourcePoint; + return this; + } + + @Nonnull + public List getDestinationPoints() { + return destinationPoints; + } + + public PostVehicleRoutesRequestTO setDestinationPoints( + @Nonnull + List destinationPoints + ) { + this.destinationPoints = requireNonNull(destinationPoints, "destinationPoints"); + return this; + } + + @Nullable + public List getResourcesToAvoid() { + return resourcesToAvoid; + } + + public PostVehicleRoutesRequestTO setResourcesToAvoid( + @Nullable + List resourcesToAvoid + ) { + this.resourcesToAvoid = resourcesToAvoid; + return this; + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostVehicleRoutesResponseTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostVehicleRoutesResponseTO.java new file mode 100644 index 0000000..18b51b3 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostVehicleRoutesResponseTO.java @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Point; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.Route.Step; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.getvehicleroutes.RouteTO; + +/** + * + */ +public class PostVehicleRoutesResponseTO { + + private List routes = List.of(); + + public PostVehicleRoutesResponseTO() { + + } + + @Nonnull + public List getRoutes() { + return routes; + } + + public PostVehicleRoutesResponseTO setRoutes( + @Nonnull + List routes + ) { + this.routes = requireNonNull(routes, "routes"); + return this; + } + + public static PostVehicleRoutesResponseTO fromMap( + Map, Route> routeMap + ) { + return new PostVehicleRoutesResponseTO() + .setRoutes( + routeMap.entrySet().stream() + .map(PostVehicleRoutesResponseTO::toRouteTO) + .collect(Collectors.toList()) + ); + } + + private static RouteTO toRouteTO(Map.Entry, Route> entry) { + if (entry.getValue() == null) { + return new RouteTO() + .setDestinationPoint(entry.getKey().getName()) + .setCosts(-1) + .setSteps(null); + } + + return new RouteTO() + .setDestinationPoint(entry.getKey().getName()) + .setCosts(entry.getValue().getCosts()) + .setSteps(toSteps(entry.getValue().getSteps())); + } + + private static List toSteps(List steps) { + return steps.stream() + .map( + step -> new RouteTO.Step() + .setDestinationPoint(step.getDestinationPoint().getName()) + .setSourcePoint( + (step.getSourcePoint() != null) ? step.getSourcePoint().getName() : null + ) + .setPath((step.getPath() != null) ? step.getPath().getName() : null) + .setVehicleOrientation(step.getVehicleOrientation().name()) + ) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PutVehicleAllowedOrderTypesTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PutVehicleAllowedOrderTypesTO.java new file mode 100644 index 0000000..8841ee1 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PutVehicleAllowedOrderTypesTO.java @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nonnull; +import java.util.List; + +/** + * An update for a vehicle's list of allowed order types. + */ +public class PutVehicleAllowedOrderTypesTO { + + @Nonnull + private List orderTypes; + + @JsonCreator + public PutVehicleAllowedOrderTypesTO( + @Nonnull + @JsonProperty(value = "orderTypes", required = true) + List orderTypes + ) { + this.orderTypes = requireNonNull(orderTypes, "orderTypes"); + } + + @Nonnull + public List getOrderTypes() { + return orderTypes; + } + + public PutVehicleAllowedOrderTypesTO setOrderTypes( + @Nonnull + List orderTypes + ) { + this.orderTypes = requireNonNull(orderTypes, "orderTypes"); + return this; + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PutVehicleEnergyLevelThresholdSetTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PutVehicleEnergyLevelThresholdSetTO.java new file mode 100644 index 0000000..1cfd24d --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PutVehicleEnergyLevelThresholdSetTO.java @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * An update for a vehicle's energy level threshold set. + */ +public class PutVehicleEnergyLevelThresholdSetTO { + + private int energyLevelCritical; + private int energyLevelGood; + private int energyLevelSufficientlyRecharged; + private int energyLevelFullyRecharged; + + @JsonCreator + public PutVehicleEnergyLevelThresholdSetTO( + @JsonProperty(value = "energyLevelCritical", required = true) + int energyLevelCritical, + @JsonProperty(value = "energyLevelGood", required = true) + int energyLevelGood, + @JsonProperty(value = "energyLevelSufficientlyRecharged", required = true) + int energyLevelSufficientlyRecharged, + @JsonProperty(value = "energyLevelFullyRecharged", required = true) + int energyLevelFullyRecharged + ) { + this.energyLevelCritical = energyLevelCritical; + this.energyLevelGood = energyLevelGood; + this.energyLevelSufficientlyRecharged = energyLevelSufficientlyRecharged; + this.energyLevelFullyRecharged = energyLevelFullyRecharged; + } + + public int getEnergyLevelCritical() { + return energyLevelCritical; + } + + public PutVehicleEnergyLevelThresholdSetTO setEnergyLevelCritical(int energyLevelCritical) { + this.energyLevelCritical = energyLevelCritical; + return this; + } + + public int getEnergyLevelGood() { + return energyLevelGood; + } + + public PutVehicleEnergyLevelThresholdSetTO setEnergyLevelGood(int energyLevelGood) { + this.energyLevelGood = energyLevelGood; + return this; + } + + public int getEnergyLevelSufficientlyRecharged() { + return energyLevelSufficientlyRecharged; + } + + public PutVehicleEnergyLevelThresholdSetTO setEnergyLevelSufficientlyRecharged( + int energyLevelSufficientlyRecharged + ) { + this.energyLevelSufficientlyRecharged = energyLevelSufficientlyRecharged; + return this; + } + + public int getEnergyLevelFullyRecharged() { + return energyLevelFullyRecharged; + } + + public PutVehicleEnergyLevelThresholdSetTO setEnergyLevelFullyRecharged( + int energyLevelFullyRecharged + ) { + this.energyLevelFullyRecharged = energyLevelFullyRecharged; + return this; + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/getevents/OrderStatusMessage.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/getevents/OrderStatusMessage.java new file mode 100644 index 0000000..59455c7 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/getevents/OrderStatusMessage.java @@ -0,0 +1,190 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.getevents; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.DestinationState; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.Property; + +/** + * A status message containing details about a transport order. + */ +public class OrderStatusMessage + extends + StatusMessage { + + private String orderName; + + private String processingVehicleName; + + private OrderState orderState; + + private List destinations = new ArrayList<>(); + + private List properties = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public OrderStatusMessage() { + } + + @Override + public OrderStatusMessage setSequenceNumber(long sequenceNumber) { + return (OrderStatusMessage) super.setSequenceNumber(sequenceNumber); + } + + @Override + public OrderStatusMessage setCreationTimeStamp(Instant creationTimeStamp) { + return (OrderStatusMessage) super.setCreationTimeStamp(creationTimeStamp); + } + + public String getOrderName() { + return orderName; + } + + public OrderStatusMessage setOrderName(String orderName) { + this.orderName = orderName; + return this; + } + + public String getProcessingVehicleName() { + return processingVehicleName; + } + + public OrderStatusMessage setProcessingVehicleName(String processingVehicleName) { + this.processingVehicleName = processingVehicleName; + return this; + } + + public OrderState getOrderState() { + return orderState; + } + + public OrderStatusMessage setOrderState(OrderState orderState) { + this.orderState = orderState; + return this; + } + + public List getDestinations() { + return destinations; + } + + public OrderStatusMessage setDestinations(List destinations) { + this.destinations = destinations; + return this; + } + + public List getProperties() { + return properties; + } + + public OrderStatusMessage setProperties(List properties) { + this.properties = properties; + return this; + } + + public static OrderStatusMessage fromTransportOrder( + TransportOrder order, + long sequenceNumber + ) { + return fromTransportOrder(order, sequenceNumber, Instant.now()); + } + + public static OrderStatusMessage fromTransportOrder( + TransportOrder order, + long sequenceNumber, + Instant creationTimeStamp + ) { + OrderStatusMessage orderMessage = new OrderStatusMessage(); + orderMessage.setSequenceNumber(sequenceNumber); + orderMessage.setCreationTimeStamp(creationTimeStamp); + orderMessage.setOrderName(order.getName()); + orderMessage.setProcessingVehicleName( + order.getProcessingVehicle() == null ? null : order.getProcessingVehicle().getName() + ); + orderMessage.setOrderState(OrderState.fromTransportOrderState(order.getState())); + for (DriveOrder curDriveOrder : order.getAllDriveOrders()) { + orderMessage.getDestinations().add(DestinationState.fromDriveOrder(curDriveOrder)); + } + for (Map.Entry mapEntry : order.getProperties().entrySet()) { + orderMessage.getProperties().add(new Property(mapEntry.getKey(), mapEntry.getValue())); + } + return orderMessage; + } + + /** + * The various states a transport order may be in. + */ + public enum OrderState { + /** + * A transport order's initial state. + */ + RAW, + /** + * Indicates a transport order's parameters have been set up completely and the kernel should + * dispatch it when possible. + */ + ACTIVE, + /** + * Marks a transport order as ready to be dispatched to a vehicle. + */ + DISPATCHABLE, + /** + * Marks a transport order as being processed by a vehicle. + */ + BEING_PROCESSED, + /** + * Indicates the transport order is withdrawn from a processing vehicle but not yet in its + * final state, as the vehicle has not yet finished/cleaned up. + */ + WITHDRAWN, + /** + * Marks a transport order as successfully completed. + */ + FINISHED, + /** + * General failure state that marks a transport order as failed. + */ + FAILED, + /** + * Failure state that marks a transport order as unroutable. + */ + UNROUTABLE; + + /** + * Maps a transpor order's {@link TransportOrder#state state} to the corresponding + * {@link OrderState}. + * + * @param state The transport order's state. + * @return The corresponding OrderState. + */ + public static OrderState fromTransportOrderState(TransportOrder.State state) { + switch (state) { + case RAW: + return RAW; + case ACTIVE: + return ACTIVE; + case DISPATCHABLE: + return DISPATCHABLE; + case BEING_PROCESSED: + return BEING_PROCESSED; + case WITHDRAWN: + return WITHDRAWN; + case FINISHED: + return FINISHED; + case FAILED: + return FAILED; + case UNROUTABLE: + return UNROUTABLE; + default: + throw new IllegalArgumentException("Unknown transport order state."); + } + } + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/getevents/PeripheralJobStatusMessage.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/getevents/PeripheralJobStatusMessage.java new file mode 100644 index 0000000..543f455 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/getevents/PeripheralJobStatusMessage.java @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.getevents; + +import java.time.Instant; +import java.util.List; +import java.util.stream.Collectors; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.data.peripherals.PeripheralJob.State; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PeripheralOperationDescription; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.Property; + +/** + * A status message containing information about a peripheral job. + */ +public class PeripheralJobStatusMessage + extends + StatusMessage { + + private String name; + + private String reservationToken; + + private String relatedVehicle; + + private String relatedTransportOrder; + + private PeripheralOperationDescription peripheralOperation; + + private State state; + + private Instant creationTime; + + private Instant finishedTime; + + private List properties; + + /** + * Creates a new instance. + */ + public PeripheralJobStatusMessage() { + } + + @Override + public PeripheralJobStatusMessage setSequenceNumber(long sequenceNumber) { + return (PeripheralJobStatusMessage) super.setSequenceNumber(sequenceNumber); + } + + @Override + public PeripheralJobStatusMessage setCreationTimeStamp(Instant creationTimeStamp) { + return (PeripheralJobStatusMessage) super.setCreationTimeStamp(creationTimeStamp); + } + + public String getName() { + return name; + } + + public PeripheralJobStatusMessage setName(String name) { + this.name = name; + return this; + } + + public String getReservationToken() { + return reservationToken; + } + + public PeripheralJobStatusMessage setReservationToken(String reservationToken) { + this.reservationToken = reservationToken; + return this; + } + + public String getRelatedVehicle() { + return relatedVehicle; + } + + public PeripheralJobStatusMessage setRelatedVehicle(String relatedVehicle) { + this.relatedVehicle = relatedVehicle; + return this; + } + + public String getRelatedTransportOrder() { + return relatedTransportOrder; + } + + public PeripheralJobStatusMessage setRelatedTransportOrder(String relatedTransportOrder) { + this.relatedTransportOrder = relatedTransportOrder; + return this; + } + + public PeripheralOperationDescription getPeripheralOperation() { + return peripheralOperation; + } + + public PeripheralJobStatusMessage setPeripheralOperation( + PeripheralOperationDescription peripheralOperation + ) { + this.peripheralOperation = peripheralOperation; + return this; + } + + public State getState() { + return state; + } + + public PeripheralJobStatusMessage setState(State state) { + this.state = state; + return this; + } + + public Instant getCreationTime() { + return creationTime; + } + + public PeripheralJobStatusMessage setCreationTime(Instant creationTime) { + this.creationTime = creationTime; + return this; + } + + public Instant getFinishedTime() { + return finishedTime; + } + + public PeripheralJobStatusMessage setFinishedTime(Instant finishedTime) { + this.finishedTime = finishedTime; + return this; + } + + public List getProperties() { + return properties; + } + + public PeripheralJobStatusMessage setProperties(List properties) { + this.properties = properties; + return this; + } + + public static PeripheralJobStatusMessage fromPeripheralJob( + PeripheralJob job, + long sequenceNumber + ) { + return fromPeripheralJob(job, sequenceNumber, Instant.now()); + } + + public static PeripheralJobStatusMessage fromPeripheralJob( + PeripheralJob job, + long sequenceNumber, + Instant creationTimestamp + ) { + PeripheralJobStatusMessage message = new PeripheralJobStatusMessage(); + message.setSequenceNumber(sequenceNumber); + message.setCreationTimeStamp(creationTimestamp); + + message.setName(job.getName()); + message.setReservationToken(job.getReservationToken()); + if (job.getRelatedVehicle() != null) { + message.setRelatedVehicle(job.getRelatedVehicle().getName()); + } + if (job.getRelatedTransportOrder() != null) { + message.setRelatedTransportOrder(job.getRelatedTransportOrder().getName()); + } + message.setPeripheralOperation( + PeripheralOperationDescription.fromPeripheralOperation(job.getPeripheralOperation()) + ); + message.setState(job.getState()); + message.setCreationTime(job.getCreationTime()); + message.setFinishedTime(job.getFinishedTime()); + message.setProperties( + job.getProperties().entrySet().stream() + .map(entry -> new Property(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()) + ); + + return message; + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/getevents/StatusMessage.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/getevents/StatusMessage.java new file mode 100644 index 0000000..3d8cb5c --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/getevents/StatusMessage.java @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.getevents; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import java.time.Instant; + +/** + * A generic status message. + */ +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes( + { + @JsonSubTypes.Type(value = OrderStatusMessage.class, name = "TransportOrder"), + @JsonSubTypes.Type(value = VehicleStatusMessage.class, name = "Vehicle"), + @JsonSubTypes.Type(value = PeripheralJobStatusMessage.class, name = "PeripheralJob") + } +) +public abstract class StatusMessage { + + private long sequenceNumber; + + private Instant creationTimeStamp = Instant.now(); + + /** + * Creates a new instance. + */ + public StatusMessage() { + } + + public long getSequenceNumber() { + return sequenceNumber; + } + + public StatusMessage setSequenceNumber(long sequenceNumber) { + this.sequenceNumber = sequenceNumber; + return this; + } + + public Instant getCreationTimeStamp() { + return creationTimeStamp; + } + + public StatusMessage setCreationTimeStamp(Instant creationTimeStamp) { + this.creationTimeStamp = creationTimeStamp; + return this; + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/getevents/VehicleStatusMessage.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/getevents/VehicleStatusMessage.java new file mode 100644 index 0000000..d693b81 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/getevents/VehicleStatusMessage.java @@ -0,0 +1,261 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.getevents; + +import static java.util.Objects.requireNonNull; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Vehicle; + +/** + * A status message containing information about a vehicle. + */ +public class VehicleStatusMessage + extends + StatusMessage { + + private String vehicleName = ""; + + private String transportOrderName = ""; + + private String position; + + private PrecisePosition precisePosition; + + private double orientationAngle; + + private boolean paused; + + private Vehicle.State state; + + private Vehicle.ProcState procState; + + private List> allocatedResources = new ArrayList<>(); + + private List> claimedResources = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public VehicleStatusMessage() { + } + + @Override + public VehicleStatusMessage setSequenceNumber(long sequenceNumber) { + return (VehicleStatusMessage) super.setSequenceNumber(sequenceNumber); + } + + @Override + public VehicleStatusMessage setCreationTimeStamp(Instant creationTimeStamp) { + return (VehicleStatusMessage) super.setCreationTimeStamp(creationTimeStamp); + } + + public String getVehicleName() { + return vehicleName; + } + + public VehicleStatusMessage setVehicleName(String vehicleName) { + this.vehicleName = vehicleName; + return this; + } + + public String getTransportOrderName() { + return transportOrderName; + } + + public VehicleStatusMessage setTransportOrderName(String transportOrderName) { + this.transportOrderName = transportOrderName; + return this; + } + + public String getPosition() { + return position; + } + + public VehicleStatusMessage setPosition(String position) { + this.position = position; + return this; + } + + public PrecisePosition getPrecisePosition() { + return precisePosition; + } + + public VehicleStatusMessage setPrecisePosition(PrecisePosition precisePosition) { + this.precisePosition = precisePosition; + return this; + } + + public double getOrientationAngle() { + return orientationAngle; + } + + public VehicleStatusMessage setOrientationAngle(double orientationAngle) { + this.orientationAngle = orientationAngle; + return this; + } + + public boolean isPaused() { + return paused; + } + + public VehicleStatusMessage setPaused(boolean paused) { + this.paused = paused; + return this; + } + + public Vehicle.State getState() { + return state; + } + + public VehicleStatusMessage setState(Vehicle.State state) { + this.state = state; + return this; + } + + public Vehicle.ProcState getProcState() { + return procState; + } + + public VehicleStatusMessage setProcState(Vehicle.ProcState procState) { + this.procState = procState; + return this; + } + + public List> getAllocatedResources() { + return allocatedResources; + } + + public VehicleStatusMessage setAllocatedResources(List> allocatedResources) { + this.allocatedResources = requireNonNull(allocatedResources, "allocatedResources"); + return this; + } + + public List> getClaimedResources() { + return claimedResources; + } + + public VehicleStatusMessage setClaimedResources(List> claimedResources) { + this.claimedResources = requireNonNull(claimedResources, "claimedResources"); + return this; + } + + public static VehicleStatusMessage fromVehicle( + Vehicle vehicle, + long sequenceNumber + ) { + return fromVehicle(vehicle, sequenceNumber, Instant.now()); + } + + public static VehicleStatusMessage fromVehicle( + Vehicle vehicle, + long sequenceNumber, + Instant creationTimeStamp + ) { + VehicleStatusMessage vehicleMessage = new VehicleStatusMessage(); + vehicleMessage.setSequenceNumber(sequenceNumber); + vehicleMessage.setCreationTimeStamp(creationTimeStamp); + vehicleMessage.setVehicleName(vehicle.getName()); + vehicleMessage.setTransportOrderName( + vehicle.getTransportOrder() == null ? null : vehicle.getTransportOrder().getName() + ); + vehicleMessage.setPosition( + vehicle.getCurrentPosition() == null ? null : vehicle.getCurrentPosition().getName() + ); + vehicleMessage.setPaused(vehicle.isPaused()); + vehicleMessage.setState(vehicle.getState()); + vehicleMessage.setProcState(vehicle.getProcState()); + if (vehicle.getPose().getPosition() != null) { + vehicleMessage.setPrecisePosition( + new PrecisePosition( + vehicle.getPose().getPosition().getX(), + vehicle.getPose().getPosition().getY(), + vehicle.getPose().getPosition().getZ() + ) + ); + } + else { + vehicleMessage.setPrecisePosition(null); + } + vehicleMessage.setOrientationAngle(vehicle.getPose().getOrientationAngle()); + vehicleMessage.setAllocatedResources(toListOfListOfNames(vehicle.getAllocatedResources())); + vehicleMessage.setClaimedResources(toListOfListOfNames(vehicle.getClaimedResources())); + return vehicleMessage; + } + + private static List> toListOfListOfNames( + List>> resources + ) { + List> result = new ArrayList<>(resources.size()); + + for (Set> resSet : resources) { + result.add( + resSet.stream() + .map(resRef -> resRef.getName()) + .collect(Collectors.toList()) + ); + } + + return result; + } + + /** + * A precise position of a vehicle. + */ + public static class PrecisePosition { + + private long x; + + private long y; + + private long z; + + /** + * Creates a new instance. + */ + public PrecisePosition() { + } + + /** + * Creates a new instance. + * + * @param x x value + * @param y y value + * @param z z value + */ + public PrecisePosition(long x, long y, long z) { + this.x = x; + this.y = y; + this.z = z; + } + + public long getX() { + return x; + } + + public void setX(long x) { + this.x = x; + } + + public long getY() { + return y; + } + + public void setY(long y) { + this.y = y; + } + + public long getZ() { + return z; + } + + public void setZ(long z) { + this.z = z; + } + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/getvehicleroutes/RouteTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/getvehicleroutes/RouteTO.java new file mode 100644 index 0000000..b1d57ed --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/getvehicleroutes/RouteTO.java @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.getvehicleroutes; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.List; +import org.opentcs.data.model.Vehicle; + +/** + * The web API representation of a route. + */ +public class RouteTO { + + private String destinationPoint = ""; + private long costs = -1; + private List steps; + + public RouteTO() { + } + + @Nonnull + public String getDestinationPoint() { + return destinationPoint; + } + + public RouteTO setDestinationPoint( + @Nonnull + String destinationPoint + ) { + this.destinationPoint = requireNonNull(destinationPoint, "destinationPoint"); + return this; + } + + public long getCosts() { + return costs; + } + + public RouteTO setCosts(long costs) { + this.costs = costs; + return this; + } + + public List getSteps() { + return steps; + } + + public RouteTO setSteps(List steps) { + this.steps = steps; + return this; + } + + public static class Step { + + private String path; + private String sourcePoint; + private String destinationPoint = ""; + private String vehicleOrientation = Vehicle.Orientation.UNDEFINED.name(); + + public Step() { + } + + @Nullable + public String getPath() { + return path; + } + + public Step setPath(String path) { + this.path = path; + return this; + } + + @Nullable + public String getSourcePoint() { + return sourcePoint; + } + + public Step setSourcePoint(String sourcePoint) { + this.sourcePoint = sourcePoint; + return this; + } + + @Nonnull + public String getDestinationPoint() { + return destinationPoint; + } + + public Step setDestinationPoint( + @Nonnull + String destinationPoint + ) { + this.destinationPoint = requireNonNull(destinationPoint, "destinationPoint"); + return this; + } + + @Nonnull + public String getVehicleOrientation() { + return vehicleOrientation; + } + + public Step setVehicleOrientation( + @Nonnull + String vehicleOrientation + ) { + this.vehicleOrientation = requireNonNull(vehicleOrientation, "vehicleOrientation"); + return this; + } + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/BlockTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/BlockTO.java new file mode 100644 index 0000000..9c9d319 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/BlockTO.java @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nonnull; +import java.util.List; +import java.util.Set; +import org.opentcs.data.model.Block; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PropertyTO; + +/** + */ +public class BlockTO { + + private String name; + private String type = Block.Type.SINGLE_VEHICLE_ONLY.name(); + private Layout layout = new Layout(); + private Set memberNames = Set.of(); + private List properties = List.of(); + + @JsonCreator + public BlockTO( + @Nonnull + @JsonProperty(value = "name", required = true) + String name + ) { + this.name = requireNonNull(name, "name"); + } + + @Nonnull + public String getName() { + return name; + } + + public BlockTO setName( + @Nonnull + String name + ) { + this.name = requireNonNull(name, "name"); + return this; + } + + @Nonnull + public List getProperties() { + return properties; + } + + public BlockTO setProperties( + @Nonnull + List properties + ) { + this.properties = requireNonNull(properties, "properties"); + return this; + } + + @Nonnull + public String getType() { + return type; + } + + public BlockTO setType( + @Nonnull + String type + ) { + this.type = requireNonNull(type, "type"); + return this; + } + + @Nonnull + public Layout getLayout() { + return layout; + } + + public BlockTO setLayout( + @Nonnull + Layout layout + ) { + this.layout = requireNonNull(layout, "layout"); + return this; + } + + @Nonnull + public Set getMemberNames() { + return memberNames; + } + + public BlockTO setMemberNames( + @Nonnull + Set memberNames + ) { + this.memberNames = requireNonNull(memberNames, "memberNames"); + return this; + } + + public static class Layout { + + private String color = "#FF0000"; + + public Layout() { + } + + @Nonnull + public String getColor() { + return color; + } + + public Layout setColor( + @Nonnull + String color + ) { + this.color = requireNonNull(color, "color"); + return this; + } + + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/LayerGroupTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/LayerGroupTO.java new file mode 100644 index 0000000..9799885 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/LayerGroupTO.java @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nonnull; + +/** + */ +public class LayerGroupTO { + + private int id; + private String name; + private boolean visible; + + @JsonCreator + public LayerGroupTO( + @JsonProperty(value = "id", required = true) + int id, + @Nonnull + @JsonProperty(value = "name", required = true) + String name, + @JsonProperty(value = "visible", required = true) + boolean visible + ) { + this.id = id; + this.name = requireNonNull(name, "name"); + this.visible = visible; + } + + public int getId() { + return id; + } + + public LayerGroupTO setId(int id) { + this.id = id; + return this; + } + + @Nonnull + public String getName() { + return name; + } + + public LayerGroupTO setName( + @Nonnull + String name + ) { + this.name = requireNonNull(name, "name"); + return this; + } + + public boolean isVisible() { + return visible; + } + + public LayerGroupTO setVisible(boolean visible) { + this.visible = visible; + return this; + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/LayerTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/LayerTO.java new file mode 100644 index 0000000..cc92d70 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/LayerTO.java @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nonnull; + +/** + */ +public class LayerTO { + + private int id; + private int ordinal; + private boolean visible; + private String name; + private int groupId; + + @JsonCreator + public LayerTO( + @JsonProperty(value = "id", required = true) + int id, + @JsonProperty(value = "ordinal", required = true) + int ordinal, + @JsonProperty(value = "visible", required = true) + boolean visible, + @Nonnull + @JsonProperty(value = "name", required = true) + String name, + @JsonProperty(value = "groupId", required = true) + int groupId + ) { + this.id = id; + this.ordinal = ordinal; + this.visible = visible; + this.name = requireNonNull(name, "name"); + this.groupId = groupId; + } + + public int getId() { + return id; + } + + public LayerTO setId(int id) { + this.id = id; + return this; + } + + public int getOrdinal() { + return ordinal; + } + + public LayerTO setOrdinal(int ordinal) { + this.ordinal = ordinal; + return this; + } + + public boolean isVisible() { + return visible; + } + + public LayerTO setVisible(boolean visible) { + this.visible = visible; + return this; + } + + @Nonnull + public String getName() { + return name; + } + + public LayerTO setName( + @Nonnull + String name + ) { + this.name = requireNonNull(name, "name"); + return this; + } + + public int getGroupId() { + return groupId; + } + + public LayerTO setGroupId(int groupId) { + this.groupId = groupId; + return this; + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/LocationTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/LocationTO.java new file mode 100644 index 0000000..49e2f88 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/LocationTO.java @@ -0,0 +1,197 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nonnull; +import java.util.List; +import org.opentcs.data.model.visualization.LocationRepresentation; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.CoupleTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.LinkTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PropertyTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.TripleTO; + +/** + */ +public class LocationTO { + + private String name; + private String typeName; + private TripleTO position; + private List links = List.of(); + private boolean locked; + private Layout layout = new Layout(); + private List properties = List.of(); + + @JsonCreator + public LocationTO( + @Nonnull + @JsonProperty(value = "name", required = true) + String name, + @Nonnull + @JsonProperty(value = "typeName", required = true) + String typeName, + @Nonnull + @JsonProperty(value = "position", required = true) + TripleTO position + ) { + this.name = requireNonNull(name, "name"); + this.typeName = requireNonNull(typeName, "typeName"); + this.position = requireNonNull(position, "position"); + } + + @Nonnull + public String getName() { + return name; + } + + public LocationTO setName( + @Nonnull + String name + ) { + this.name = requireNonNull(name, "name"); + return this; + } + + @Nonnull + public List getProperties() { + return properties; + } + + public LocationTO setProperties( + @Nonnull + List properties + ) { + this.properties = requireNonNull(properties, "properties"); + return this; + } + + @Nonnull + public String getTypeName() { + return typeName; + } + + public LocationTO setTypeName( + @Nonnull + String typeName + ) { + this.typeName = requireNonNull(typeName, "typeName"); + return this; + } + + @Nonnull + public TripleTO getPosition() { + return position; + } + + public LocationTO setPosition( + @Nonnull + TripleTO position + ) { + this.position = requireNonNull(position, "position"); + return this; + } + + @Nonnull + public List getLinks() { + return links; + } + + public LocationTO setLinks( + @Nonnull + List links + ) { + this.links = requireNonNull(links, "links"); + return this; + } + + public boolean isLocked() { + return locked; + } + + public LocationTO setLocked(boolean locked) { + this.locked = locked; + return this; + } + + @Nonnull + public Layout getLayout() { + return layout; + } + + @Nonnull + public LocationTO setLayout( + @Nonnull + Layout layout + ) { + this.layout = requireNonNull(layout, "layout"); + return this; + } + + public static class Layout { + + private CoupleTO position = new CoupleTO(0, 0); + private CoupleTO labelOffset = new CoupleTO(0, 0); + private String locationRepresentation = LocationRepresentation.DEFAULT.name(); + private int layerId; + + public Layout() { + + } + + @Nonnull + public CoupleTO getPosition() { + return position; + } + + public Layout setPosition( + @Nonnull + CoupleTO position + ) { + this.position = requireNonNull(position, "position"); + return this; + } + + @Nonnull + public CoupleTO getLabelOffset() { + return labelOffset; + } + + public Layout setLabelOffset( + @Nonnull + CoupleTO labelOffset + ) { + this.labelOffset = requireNonNull(labelOffset, "labelOffset"); + return this; + } + + @Nonnull + public String getLocationRepresentation() { + return locationRepresentation; + } + + public Layout setLocationRepresentation( + @Nonnull + String locationRepresentation + ) { + this.locationRepresentation = requireNonNull( + locationRepresentation, "locationRepresentation" + ); + return this; + } + + public int getLayerId() { + return layerId; + } + + public Layout setLayerId(int layerId) { + this.layerId = layerId; + return this; + } + + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/LocationTypeTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/LocationTypeTO.java new file mode 100644 index 0000000..4275b16 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/LocationTypeTO.java @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nonnull; +import java.util.List; +import org.opentcs.data.model.visualization.LocationRepresentation; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PropertyTO; + +/** + */ +public class LocationTypeTO { + + private String name; + private List allowedOperations = List.of(); + private List allowedPeripheralOperations = List.of(); + private Layout layout = new Layout(); + private List properties = List.of(); + + @JsonCreator + public LocationTypeTO( + @Nonnull + @JsonProperty(value = "name", required = true) + String name + ) { + this.name = requireNonNull(name, "name"); + } + + @Nonnull + public String getName() { + return name; + } + + public LocationTypeTO setName( + @Nonnull + String name + ) { + this.name = requireNonNull(name, "name"); + return this; + } + + @Nonnull + public List getProperties() { + return properties; + } + + public LocationTypeTO setProperties( + @Nonnull + List properties + ) { + this.properties = requireNonNull(properties, "properties"); + return this; + } + + @Nonnull + public List getAllowedOperations() { + return allowedOperations; + } + + public LocationTypeTO setAllowedOperations( + @Nonnull + List allowedOperations + ) { + this.allowedOperations = requireNonNull(allowedOperations, "allowedOperations"); + return this; + } + + @Nonnull + public List getAllowedPeripheralOperations() { + return allowedPeripheralOperations; + } + + public LocationTypeTO setAllowedPeripheralOperations( + @Nonnull + List allowedPeripheralOperations + ) { + this.allowedPeripheralOperations = requireNonNull( + allowedPeripheralOperations, + "allowedPeripheralOperations" + ); + return this; + } + + @Nonnull + public Layout getLayout() { + return layout; + } + + public LocationTypeTO setLayout( + @Nonnull + Layout layout + ) { + this.layout = requireNonNull(layout, "layout"); + return this; + } + + public static class Layout { + + private String locationRepresentation = LocationRepresentation.NONE.name(); + + public Layout() { + + } + + @Nonnull + public String getLocationRepresentation() { + return locationRepresentation; + } + + public Layout setLocationRepresentation( + @Nonnull + String locationRepresentation + ) { + this.locationRepresentation = requireNonNull( + locationRepresentation, + "locationRepresentation" + ); + return this; + } + + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/PathTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/PathTO.java new file mode 100644 index 0000000..23aacb4 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/PathTO.java @@ -0,0 +1,223 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nonnull; +import java.util.List; +import org.opentcs.data.model.Path; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.CoupleTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.EnvelopeTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PropertyTO; + +/** + */ +public class PathTO { + + private String name; + private String srcPointName; + private String destPointName; + private long length = 1; + private int maxVelocity; + private int maxReverseVelocity; + private List peripheralOperations = List.of(); + private boolean locked; + private Layout layout = new Layout(); + private List vehicleEnvelopes = List.of(); + private List properties = List.of(); + + @JsonCreator + public PathTO( + @Nonnull + @JsonProperty(value = "name", required = true) + String name, + @Nonnull + @JsonProperty(value = "srcPointName", required = true) + String srcPointName, + @Nonnull + @JsonProperty(value = "destPointName", required = true) + String destPointName + ) { + this.name = requireNonNull(name, "name"); + this.srcPointName = requireNonNull(srcPointName, "srcPointName"); + this.destPointName = requireNonNull(destPointName, "destPointName"); + } + + @Nonnull + public String getName() { + return name; + } + + public PathTO setName( + @Nonnull + String name + ) { + this.name = requireNonNull(name, "name"); + return this; + } + + @Nonnull + public List getProperties() { + return properties; + } + + public PathTO setProperties( + @Nonnull + List properties + ) { + this.properties = requireNonNull(properties, "properties"); + return this; + } + + @Nonnull + public String getSrcPointName() { + return srcPointName; + } + + public PathTO setSrcPointName( + @Nonnull + String srcPointName + ) { + this.srcPointName = requireNonNull(srcPointName, "srcPointName"); + return this; + } + + @Nonnull + public String getDestPointName() { + return destPointName; + } + + public PathTO setDestPointName( + @Nonnull + String destPointName + ) { + this.destPointName = requireNonNull(destPointName, "destPointName"); + return this; + } + + public long getLength() { + return length; + } + + public PathTO setLength(long length) { + this.length = length; + return this; + } + + public int getMaxVelocity() { + return maxVelocity; + } + + public PathTO setMaxVelocity(int maxVelocity) { + this.maxVelocity = maxVelocity; + return this; + } + + public int getMaxReverseVelocity() { + return maxReverseVelocity; + } + + public PathTO setMaxReverseVelocity(int maxReverseVelocity) { + this.maxReverseVelocity = maxReverseVelocity; + return this; + } + + @Nonnull + public List getPeripheralOperations() { + return peripheralOperations; + } + + public PathTO setPeripheralOperations( + @Nonnull + List peripheralOperations + ) { + this.peripheralOperations = requireNonNull(peripheralOperations, "peripheralOperations"); + return this; + } + + public boolean isLocked() { + return locked; + } + + public PathTO setLocked(boolean locked) { + this.locked = locked; + return this; + } + + @Nonnull + public Layout getLayout() { + return layout; + } + + public PathTO setLayout( + @Nonnull + Layout layout + ) { + this.layout = requireNonNull(layout, "layout"); + return this; + } + + @Nonnull + public List getVehicleEnvelopes() { + return vehicleEnvelopes; + } + + public PathTO setVehicleEnvelopes( + @Nonnull + List vehicleEnvelopes + ) { + this.vehicleEnvelopes = requireNonNull(vehicleEnvelopes, "vehicleEnvelopes"); + return this; + } + + public static class Layout { + + private String connectionType = Path.Layout.ConnectionType.DIRECT.name(); + private List controlPoints = List.of(); + private int layerId; + + public Layout() { + + } + + @Nonnull + public String getConnectionType() { + return connectionType; + } + + public Layout setConnectionType( + @Nonnull + String connectionType + ) { + this.connectionType = requireNonNull(connectionType, "connectionType"); + return this; + } + + @Nonnull + public List getControlPoints() { + return controlPoints; + } + + public Layout setControlPoints( + @Nonnull + List controlPoints + ) { + this.controlPoints = requireNonNull(controlPoints, "controlPoints"); + return this; + } + + public int getLayerId() { + return layerId; + } + + public Layout setLayerId(int layerId) { + this.layerId = layerId; + return this; + } + + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/PeripheralOperationTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/PeripheralOperationTO.java new file mode 100644 index 0000000..3dfc76a --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/PeripheralOperationTO.java @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nonnull; +import org.opentcs.data.peripherals.PeripheralOperation; + +/** + */ +public class PeripheralOperationTO { + + private String operation; + private String locationName; + private String executionTrigger = PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION.name(); + private boolean completionRequired; + + @JsonCreator + public PeripheralOperationTO( + @Nonnull + @JsonProperty(value = "operation", required = true) + String operation, + @Nonnull + @JsonProperty(value = "locationName", required = true) + String locationName + ) { + this.operation = requireNonNull(operation, "operation"); + this.locationName = requireNonNull(locationName, "locationName"); + } + + @Nonnull + public String getOperation() { + return operation; + } + + public PeripheralOperationTO setOperation( + @Nonnull + String operation + ) { + this.operation = requireNonNull(operation, "operation"); + return this; + } + + @Nonnull + public String getLocationName() { + return locationName; + } + + public PeripheralOperationTO setLocationName( + @Nonnull + String locationName + ) { + this.locationName = requireNonNull(locationName, "locationName"); + return this; + } + + @Nonnull + public String getExecutionTrigger() { + return executionTrigger; + } + + public PeripheralOperationTO setExecutionTrigger( + @Nonnull + String executionTrigger + ) { + this.executionTrigger = requireNonNull(executionTrigger, "executionTrigger"); + return this; + } + + public boolean isCompletionRequired() { + return completionRequired; + } + + public PeripheralOperationTO setCompletionRequired(boolean completionRequired) { + this.completionRequired = completionRequired; + return this; + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/PointTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/PointTO.java new file mode 100644 index 0000000..b7f9be5 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/PointTO.java @@ -0,0 +1,172 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nonnull; +import java.util.List; +import org.opentcs.data.model.Point; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.CoupleTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.EnvelopeTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PropertyTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.TripleTO; + +/** + */ +public class PointTO { + + private String name; + private TripleTO position = new TripleTO(0, 0, 0); + private double vehicleOrientationAngle = Double.NaN; + private String type = Point.Type.HALT_POSITION.name(); + private Layout layout = new Layout(); + private List vehicleEnvelopes = List.of(); + private List properties = List.of(); + + @JsonCreator + public PointTO( + @Nonnull + @JsonProperty(value = "name", required = true) + String name + ) { + this.name = requireNonNull(name, "name"); + } + + @Nonnull + public String getName() { + return name; + } + + public PointTO setName( + @Nonnull + String name + ) { + this.name = requireNonNull(name, "name"); + return this; + } + + @Nonnull + public List getProperties() { + return properties; + } + + public PointTO setProperties( + @Nonnull + List properties + ) { + this.properties = requireNonNull(properties, "properties"); + return this; + } + + @Nonnull + public TripleTO getPosition() { + return position; + } + + public PointTO setPosition( + @Nonnull + TripleTO position + ) { + this.position = requireNonNull(position, "position"); + return this; + } + + public double getVehicleOrientationAngle() { + return vehicleOrientationAngle; + } + + public PointTO setVehicleOrientationAngle(double vehicleOrientationAngle) { + this.vehicleOrientationAngle = vehicleOrientationAngle; + return this; + } + + @Nonnull + public String getType() { + return type; + } + + public PointTO setType( + @Nonnull + String type + ) { + this.type = requireNonNull(type, "type"); + return this; + } + + @Nonnull + public Layout getLayout() { + return layout; + } + + public PointTO setLayout( + @Nonnull + Layout pointLayout + ) { + this.layout = requireNonNull(pointLayout, "pointLayout"); + return this; + } + + @Nonnull + public List getVehicleEnvelopes() { + return vehicleEnvelopes; + } + + public PointTO setVehicleEnvelopes( + @Nonnull + List vehicleEnvelopes + ) { + this.vehicleEnvelopes = requireNonNull(vehicleEnvelopes, "vehicleEnvelopes"); + return this; + } + + public static class Layout { + + private CoupleTO position = new CoupleTO(0, 0); + private CoupleTO labelOffset = new CoupleTO(0, 0); + private int layerId; + + public Layout() { + + } + + @Nonnull + public CoupleTO getPosition() { + return position; + } + + public Layout setPosition( + @Nonnull + CoupleTO position + ) { + this.position = requireNonNull(position, "position"); + return this; + } + + @Nonnull + public CoupleTO getLabelOffset() { + return labelOffset; + } + + public Layout setLabelOffset( + @Nonnull + CoupleTO labelOffset + ) { + this.labelOffset = requireNonNull(labelOffset, "labelOffset"); + return this; + } + + public int getLayerId() { + return layerId; + } + + public Layout setLayerId(int layerId) { + this.layerId = layerId; + return this; + } + + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/VehicleTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/VehicleTO.java new file mode 100644 index 0000000..4f3d341 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/VehicleTO.java @@ -0,0 +1,162 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nonnull; +import java.util.List; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PropertyTO; + +/** + */ +public class VehicleTO { + + private String name; + private int length = 1000; + private int energyLevelCritical = 30; + private int energyLevelGood = 90; + private int energyLevelFullyRecharged = 90; + private int energyLevelSufficientlyRecharged = 30; + private int maxVelocity = 1000; + private int maxReverseVelocity = 1000; + private Layout layout = new Layout(); + private List properties = List.of(); + + @JsonCreator + public VehicleTO( + @Nonnull + @JsonProperty(value = "name", required = true) + String name + ) { + this.name = requireNonNull(name, "name"); + } + + @Nonnull + public String getName() { + return name; + } + + public VehicleTO setName( + @Nonnull + String name + ) { + this.name = requireNonNull(name, "name"); + return this; + } + + @Nonnull + public List getProperties() { + return properties; + } + + public VehicleTO setProperties( + @Nonnull + List properties + ) { + this.properties = requireNonNull(properties, "properties"); + return this; + } + + public int getLength() { + return length; + } + + public VehicleTO setLength(int length) { + this.length = length; + return this; + } + + public int getEnergyLevelCritical() { + return energyLevelCritical; + } + + public VehicleTO setEnergyLevelCritical(int energyLevelCritical) { + this.energyLevelCritical = energyLevelCritical; + return this; + } + + public int getEnergyLevelGood() { + return energyLevelGood; + } + + public VehicleTO setEnergyLevelGood(int energyLevelGood) { + this.energyLevelGood = energyLevelGood; + return this; + } + + public int getEnergyLevelFullyRecharged() { + return energyLevelFullyRecharged; + } + + public VehicleTO setEnergyLevelFullyRecharged(int energyLevelFullyRecharged) { + this.energyLevelFullyRecharged = energyLevelFullyRecharged; + return this; + } + + public int getEnergyLevelSufficientlyRecharged() { + return energyLevelSufficientlyRecharged; + } + + public VehicleTO setEnergyLevelSufficientlyRecharged(int energyLevelSufficientlyRecharged) { + this.energyLevelSufficientlyRecharged = energyLevelSufficientlyRecharged; + return this; + } + + public int getMaxVelocity() { + return maxVelocity; + } + + public VehicleTO setMaxVelocity(int maxVelocity) { + this.maxVelocity = maxVelocity; + return this; + } + + public int getMaxReverseVelocity() { + return maxReverseVelocity; + } + + public VehicleTO setMaxReverseVelocity(int maxReverseVelocity) { + this.maxReverseVelocity = maxReverseVelocity; + return this; + } + + @Nonnull + public Layout getLayout() { + return layout; + } + + public VehicleTO setLayout( + @Nonnull + Layout layout + ) { + this.layout = requireNonNull(layout, "layout"); + return this; + } + + public static class Layout { + + private String routeColor = "#00FF00"; + + public Layout() { + + } + + @Nonnull + public String getRouteColor() { + return routeColor; + } + + public Layout setRouteColor( + @Nonnull + String routeColor + ) { + this.routeColor = requireNonNull(routeColor, "routeColor"); + return this; + } + + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/VisualLayoutTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/VisualLayoutTO.java new file mode 100644 index 0000000..62bfe96 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/plantmodel/VisualLayoutTO.java @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nonnull; +import java.util.List; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PropertyTO; + +/** + */ +public class VisualLayoutTO { + + private String name; + private double scaleX = 50.0; + private double scaleY = 50.0; + private List layers = List.of(new LayerTO(0, 0, true, "layer0", 0)); + private List layerGroups = List.of(new LayerGroupTO(0, "layerGroup0", true)); + private List properties = List.of(); + + @JsonCreator + public VisualLayoutTO( + @Nonnull + @JsonProperty(value = "name", required = true) + String name + ) { + this.name = requireNonNull(name, "name"); + } + + @Nonnull + public String getName() { + return name; + } + + public VisualLayoutTO setName( + @Nonnull + String name + ) { + this.name = requireNonNull(name, "name"); + return this; + } + + @Nonnull + public List getProperties() { + return properties; + } + + public VisualLayoutTO setProperties( + @Nonnull + List properties + ) { + this.properties = requireNonNull(properties, "properties"); + return this; + } + + public double getScaleX() { + return scaleX; + } + + public VisualLayoutTO setScaleX(double scaleX) { + this.scaleX = scaleX; + return this; + } + + public double getScaleY() { + return scaleY; + } + + public VisualLayoutTO setScaleY(double scaleY) { + this.scaleY = scaleY; + return this; + } + + @Nonnull + public List getLayers() { + return layers; + } + + public VisualLayoutTO setLayers( + @Nonnull + List layers + ) { + this.layers = requireNonNull(layers, "layers"); + return this; + } + + @Nonnull + public List getLayerGroups() { + return layerGroups; + } + + public VisualLayoutTO setLayerGroups( + @Nonnull + List layerGroups + ) { + this.layerGroups = requireNonNull(layerGroups, "layerGroups"); + return this; + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/posttransportorder/Destination.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/posttransportorder/Destination.java new file mode 100644 index 0000000..5013ba5 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/posttransportorder/Destination.java @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.posttransportorder; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.List; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.Property; + +/** + * A destination of a transport. + */ +public class Destination { + + private String locationName; + + private String operation; + + private List properties; + + @JsonCreator + public Destination( + @Nonnull + @JsonProperty(required = true, value = "locationName") + String locationName, + @Nonnull + @JsonProperty(required = true, value = "operation") + String operation, + @Nullable + @JsonProperty(required = false, value = "properties") + List properties + ) { + this.locationName = requireNonNull(locationName, "locationName"); + this.operation = requireNonNull(operation, "operation"); + this.properties = properties; + } + + public Destination() { + } + + @Nonnull + public String getLocationName() { + return locationName; + } + + public Destination setLocationName( + @Nonnull + String locationName + ) { + this.locationName = requireNonNull(locationName, "locationName"); + return this; + } + + @Nonnull + public String getOperation() { + return operation; + } + + public Destination setOperation( + @Nonnull + String operation + ) { + this.operation = requireNonNull(operation, "operation"); + return this; + } + + @Nullable + public List getProperties() { + return properties; + } + + public Destination setProperties( + @Nullable + List properties + ) { + this.properties = properties; + return this; + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/CoupleTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/CoupleTO.java new file mode 100644 index 0000000..6b9e1fe --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/CoupleTO.java @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + */ +public class CoupleTO { + + private long x; + private long y; + + @JsonCreator + public CoupleTO( + @JsonProperty(value = "x", required = true) + long x, + @JsonProperty(value = "y", required = true) + long y + ) { + this.x = x; + this.y = y; + } + + public long getX() { + return x; + } + + public CoupleTO setX(long x) { + this.x = x; + return this; + } + + public long getY() { + return y; + } + + public CoupleTO setY(long y) { + this.y = y; + return this; + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/DestinationState.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/DestinationState.java new file mode 100644 index 0000000..7d2a01c --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/DestinationState.java @@ -0,0 +1,145 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.opentcs.data.order.DriveOrder; + +/** + * A {@link org.opentcs.data.order.DriveOrder DriveOrder}'s destination. + */ +public class DestinationState { + + private String locationName = ""; + + private String operation = ""; + + private State state = State.PRISTINE; + + private List properties = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public DestinationState() { + } + + @Nonnull + public String getLocationName() { + return locationName; + } + + public DestinationState setLocationName( + @Nonnull + String name + ) { + this.locationName = requireNonNull(name, "name"); + return this; + } + + @Nonnull + public String getOperation() { + return operation; + } + + public DestinationState setOperation( + @Nonnull + String operation + ) { + this.operation = requireNonNull(operation, "operation"); + return this; + } + + @Nonnull + public State getState() { + return state; + } + + public DestinationState setState( + @Nonnull + State state + ) { + this.state = requireNonNull(state, "state"); + return this; + } + + @Nonnull + public List getProperties() { + return properties; + } + + public DestinationState setProperties( + @Nonnull + List properties + ) { + this.properties = requireNonNull(properties, "properties"); + return this; + } + + public static DestinationState fromDriveOrder(DriveOrder driveOrder) { + if (driveOrder == null) { + return null; + } + DestinationState destination = new DestinationState(); + destination.setLocationName(driveOrder.getDestination().getDestination().getName()); + destination.setOperation(driveOrder.getDestination().getOperation()); + destination.setState(mapDriveOrderState(driveOrder.getState())); + + for (Map.Entry mapEntry : driveOrder.getDestination().getProperties() + .entrySet()) { + destination.getProperties().add(new Property(mapEntry.getKey(), mapEntry.getValue())); + } + return destination; + } + + private static DestinationState.State mapDriveOrderState(DriveOrder.State driveOrderState) { + switch (driveOrderState) { + case PRISTINE: + return DestinationState.State.PRISTINE; + case TRAVELLING: + return DestinationState.State.TRAVELLING; + case OPERATING: + return DestinationState.State.OPERATING; + case FINISHED: + return DestinationState.State.FINISHED; + case FAILED: + return DestinationState.State.FAILED; + default: + throw new IllegalArgumentException("Unhandled drive order state: " + driveOrderState); + } + } + + /** + * This enumeration defines the various states a DriveOrder may be in. + */ + public enum State { + + /** + * A DriveOrder's initial state, indicating it being still untouched/not + * being processed. + */ + PRISTINE, + /** + * Indicates this drive order being processed at the moment. + */ + TRAVELLING, + /** + * Indicates the vehicle processing an order is currently executing an + * operation. + */ + OPERATING, + /** + * Marks a DriveOrder as successfully completed. + */ + FINISHED, + /** + * Marks a DriveOrder as failed. + */ + FAILED; + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/EnvelopeTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/EnvelopeTO.java new file mode 100644 index 0000000..eeb16f9 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/EnvelopeTO.java @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nonnull; +import java.util.List; + +/** + */ +public class EnvelopeTO { + + private String key; + private List vertices; + + @JsonCreator + public EnvelopeTO( + @Nonnull + @JsonProperty(value = "key", required = true) + String key, + @Nonnull + @JsonProperty(value = "vertices", required = true) + List vertices + ) { + this.key = requireNonNull(key, "key"); + this.vertices = requireNonNull(vertices, "vertices"); + } + + @Nonnull + public String getKey() { + return key; + } + + public EnvelopeTO setKey( + @Nonnull + String key + ) { + this.key = requireNonNull(key, "key"); + return this; + } + + @Nonnull + public List getVertices() { + return vertices; + } + + public EnvelopeTO setVertices( + @Nonnull + List vertices + ) { + this.vertices = requireNonNull(vertices, "vertices"); + return this; + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/LinkTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/LinkTO.java new file mode 100644 index 0000000..4dfc583 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/LinkTO.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.Set; + +/** + */ +public class LinkTO { + + private String pointName = ""; + private Set allowedOperations = Set.of(); + + public LinkTO() { + } + + @Nonnull + public String getPointName() { + return pointName; + } + + public LinkTO setPointName( + @Nonnull + String pointName + ) { + this.pointName = requireNonNull(pointName, "pointName"); + return this; + } + + @Nonnull + public Set getAllowedOperations() { + return allowedOperations; + } + + public LinkTO setAllowedOperations( + @Nonnull + Set allowedOperations + ) { + this.allowedOperations = requireNonNull(allowedOperations, "allowedOperations"); + return this; + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/PeripheralOperationDescription.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/PeripheralOperationDescription.java new file mode 100644 index 0000000..5eab02c --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/PeripheralOperationDescription.java @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared; + +import org.opentcs.data.peripherals.PeripheralOperation; +import org.opentcs.data.peripherals.PeripheralOperation.ExecutionTrigger; + +/** + * Describes a peripheral operation. + */ +public class PeripheralOperationDescription { + + private String operation; + + private String locationName; + + private ExecutionTrigger executionTrigger; + + private boolean completionRequired; + + public PeripheralOperationDescription() { + } + + public String getOperation() { + return operation; + } + + public PeripheralOperationDescription setOperation(String operation) { + this.operation = operation; + return this; + } + + public String getLocationName() { + return locationName; + } + + public PeripheralOperationDescription setLocationName(String locationName) { + this.locationName = locationName; + return this; + } + + public ExecutionTrigger getExecutionTrigger() { + return executionTrigger; + } + + public PeripheralOperationDescription setExecutionTrigger(ExecutionTrigger executionTrigger) { + this.executionTrigger = executionTrigger; + return this; + } + + public boolean isCompletionRequired() { + return completionRequired; + } + + public PeripheralOperationDescription setCompletionRequired(boolean completionRequired) { + this.completionRequired = completionRequired; + return this; + } + + public static PeripheralOperationDescription fromPeripheralOperation( + PeripheralOperation operation + ) { + PeripheralOperationDescription state = new PeripheralOperationDescription(); + state.operation = operation.getOperation(); + state.locationName = operation.getLocation().getName(); + state.executionTrigger = operation.getExecutionTrigger(); + state.completionRequired = operation.isCompletionRequired(); + return state; + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/Property.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/Property.java new file mode 100644 index 0000000..c30ff3a --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/Property.java @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; + +/** + * A key-value property. + */ +public class Property { + + private String key; + + private String value; + + public Property( + @Nonnull + @JsonProperty(required = true, value = "key") + String key, + @Nullable + @JsonProperty(required = false, value = "value") + String value + ) { + this.key = requireNonNull(key, "key"); + this.value = value; + } + + /** + * Returns the property key. + * + * @return The property key. + */ + @Nonnull + public String getKey() { + return key; + } + + /** + * Sets the property key. + * + * @param key The new key. + */ + public void setKey( + @Nonnull + String key + ) { + this.key = requireNonNull(key, "key"); + } + + /** + * Returns the property value. + * + * @return The property value. + */ + @Nullable + public String getValue() { + return value; + } + + /** + * Sets the property value. + * + * @param value The new value. + */ + public void setValue( + @Nullable + String value + ) { + this.value = value; + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/PropertyTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/PropertyTO.java new file mode 100644 index 0000000..dd7188d --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/PropertyTO.java @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nonnull; + +/** + */ +public class PropertyTO { + + private String name; + private String value; + + @JsonCreator + public PropertyTO( + @Nonnull + @JsonProperty(value = "name", required = true) + String name, + @Nonnull + @JsonProperty(value = "value", required = true) + String value + ) { + this.name = requireNonNull(name, "name"); + this.value = requireNonNull(value, "value"); + } + + @Nonnull + public String getName() { + return name; + } + + public PropertyTO setName( + @Nonnull + String name + ) { + this.name = requireNonNull(name, "name"); + return this; + } + + @Nonnull + public String getValue() { + return value; + } + + public PropertyTO setValue( + @Nonnull + String value + ) { + this.value = requireNonNull(value, "value"); + return this; + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/TripleTO.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/TripleTO.java new file mode 100644 index 0000000..6170ce3 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/shared/TripleTO.java @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + */ +public class TripleTO { + + private long x; + private long y; + private long z; + + @JsonCreator + public TripleTO( + @JsonProperty(value = "x", required = true) + long x, + @JsonProperty(value = "y", required = true) + long y, + @JsonProperty(value = "z", required = true) + long z + ) { + this.x = x; + this.y = y; + this.z = z; + } + + public long getX() { + return x; + } + + public TripleTO setX(long x) { + this.x = x; + return this; + } + + public long getY() { + return y; + } + + public TripleTO setY(long y) { + this.y = y; + return this; + } + + public long getZ() { + return z; + } + + public TripleTO setZ(long z) { + this.z = z; + return this; + } + +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/BlockConverter.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/BlockConverter.java new file mode 100644 index 0000000..42d5f27 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/BlockConverter.java @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.converter; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.access.to.model.BlockCreationTO; +import org.opentcs.data.model.Block; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.BlockTO; +import org.opentcs.util.Colors; + +/** + * Includes the conversion methods for all Block classes. + */ +public class BlockConverter { + + private final PropertyConverter pConverter; + + @Inject + public BlockConverter(PropertyConverter pConverter) { + this.pConverter = requireNonNull(pConverter, "pConverter"); + } + + public List toBlockCreationTOs(List blocks) { + return blocks.stream() + .map( + block -> new BlockCreationTO(block.getName()) + .withProperties(pConverter.toPropertyMap(block.getProperties())) + .withMemberNames(block.getMemberNames()) + .withType(Block.Type.valueOf(block.getType())) + .withLayout( + new BlockCreationTO.Layout( + Colors.decodeFromHexRGB(block.getLayout().getColor()) + ) + ) + ) + .collect(Collectors.toCollection(ArrayList::new)); + } + + public List toBlockTOs(Set blocks) { + return blocks.stream() + .map( + block -> new BlockTO(block.getName()) + .setType(block.getType().name()) + .setMemberNames(convertMemberNames(block.getMembers())) + .setLayout( + new BlockTO.Layout() + .setColor(Colors.encodeToHexRGB(block.getLayout().getColor())) + ) + .setProperties(pConverter.toPropertyTOs(block.getProperties())) + ) + .sorted(Comparator.comparing(BlockTO::getName)) + .collect(Collectors.toList()); + } + + private Set convertMemberNames(Set> members) { + return members.stream() + .map(TCSResourceReference::getName) + .collect(Collectors.toSet()); + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/EnvelopeConverter.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/EnvelopeConverter.java new file mode 100644 index 0000000..6f427de --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/EnvelopeConverter.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.converter; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Envelope; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.CoupleTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.EnvelopeTO; + +/** + * Includes the conversion methods for all Envelope classes. + */ +public class EnvelopeConverter { + + public EnvelopeConverter() { + } + + public Map toVehicleEnvelopeMap(List envelopeEntries) { + return envelopeEntries.stream() + .collect( + Collectors.toMap( + EnvelopeTO::getKey, + entry -> { + List couples = entry.getVertices().stream() + .map(coupleTO -> new Couple(coupleTO.getX(), coupleTO.getY())) + .collect(Collectors.toList()); + return new Envelope(couples); + } + ) + ); + } + + public List toEnvelopeTOs(Map envelopeMap) { + return envelopeMap.entrySet().stream() + .map( + entry -> new EnvelopeTO( + entry.getKey(), + entry.getValue().getVertices().stream() + .map(couple -> new CoupleTO(couple.getX(), couple.getY())) + .collect(Collectors.toList()) + ) + ) + .sorted(Comparator.comparing(EnvelopeTO::getKey)) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/LocationConverter.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/LocationConverter.java new file mode 100644 index 0000000..1058a66 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/LocationConverter.java @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.converter; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.access.to.model.LocationCreationTO; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Triple; +import org.opentcs.data.model.visualization.LocationRepresentation; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.LocationTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.CoupleTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.LinkTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.TripleTO; + +/** + * Includes the conversion methods for all Location classes. + */ +public class LocationConverter { + + private final PropertyConverter pConverter; + + @Inject + public LocationConverter(PropertyConverter pConverter) { + this.pConverter = requireNonNull(pConverter, "pConverter"); + } + + public List toLocationCreationTOs(List locations) { + return locations.stream() + .map( + location -> new LocationCreationTO( + location.getName(), + location.getTypeName(), + new Triple( + location.getPosition().getX(), + location.getPosition().getY(), + location.getPosition().getZ() + ) + ) + .withProperties(pConverter.toPropertyMap(location.getProperties())) + .withLinks(toLinkMap(location.getLinks())) + .withLocked(location.isLocked()) + .withLayout( + new LocationCreationTO.Layout( + new Couple( + location.getLayout().getPosition().getX(), + location.getLayout().getPosition().getY() + ), + new Couple( + location.getLayout().getLabelOffset().getX(), + location.getLayout().getLabelOffset().getY() + ), + LocationRepresentation.valueOf( + location.getLayout().getLocationRepresentation() + ), + location.getLayout().getLayerId() + ) + ) + ) + .collect(Collectors.toCollection(ArrayList::new)); + } + + public List toLocationTOs(Set locations) { + return locations.stream() + .map( + location -> new LocationTO( + location.getName(), + location.getType().getName(), + new TripleTO( + location.getPosition().getX(), + location.getPosition().getY(), + location.getPosition().getZ() + ) + ) + .setLocked(location.isLocked()) + .setProperties(pConverter.toPropertyTOs(location.getProperties())) + .setLinks(toLinkTOs(location.getAttachedLinks())) + .setLayout( + new LocationTO.Layout() + .setLayerId(location.getLayout().getLayerId()) + .setLocationRepresentation( + location.getLayout().getLocationRepresentation().name() + ) + .setLabelOffset( + new CoupleTO( + location.getLayout().getLabelOffset().getX(), + location.getLayout().getLabelOffset().getY() + ) + ) + .setPosition( + new CoupleTO( + location.getLayout().getPosition().getX(), + location.getLayout().getPosition().getY() + ) + ) + ) + ) + .sorted(Comparator.comparing(LocationTO::getName)) + .collect(Collectors.toList()); + } + + private Map> toLinkMap(List links) { + return links.stream() + .collect(Collectors.toMap(LinkTO::getPointName, LinkTO::getAllowedOperations)); + } + + private List toLinkTOs(Set links) { + return links.stream() + .map( + link -> new LinkTO() + .setPointName(link.getPoint().getName()) + .setAllowedOperations(link.getAllowedOperations()) + ) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/LocationTypeConverter.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/LocationTypeConverter.java new file mode 100644 index 0000000..d3ce082 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/LocationTypeConverter.java @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.converter; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.access.to.model.LocationTypeCreationTO; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.visualization.LocationRepresentation; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.LocationTypeTO; + +/** + * Includes the conversion methods for all LocationType classes. + */ +public class LocationTypeConverter { + + private final PropertyConverter pConverter; + + @Inject + public LocationTypeConverter(PropertyConverter pConverter) { + this.pConverter = requireNonNull(pConverter, "pConverter"); + } + + public List toLocationTypeCreationTOs(List locTypes) { + return locTypes.stream() + .map( + locationType -> new LocationTypeCreationTO(locationType.getName()) + .withAllowedOperations(locationType.getAllowedOperations()) + .withAllowedPeripheralOperations( + locationType.getAllowedPeripheralOperations() + ) + .withProperties(pConverter.toPropertyMap(locationType.getProperties())) + .withLayout( + new LocationTypeCreationTO.Layout( + LocationRepresentation.valueOf( + locationType.getLayout().getLocationRepresentation() + ) + ) + ) + ) + .collect(Collectors.toCollection(ArrayList::new)); + } + + public List toLocationTypeTOs(Set locationTypes) { + return locationTypes.stream() + .map( + locationType -> new LocationTypeTO(locationType.getName()) + .setProperties(pConverter.toPropertyTOs(locationType.getProperties())) + .setAllowedOperations(locationType.getAllowedOperations()) + .setAllowedPeripheralOperations(locationType.getAllowedPeripheralOperations()) + .setLayout( + new LocationTypeTO.Layout() + .setLocationRepresentation( + locationType.getLayout().getLocationRepresentation().name() + ) + ) + ) + .sorted(Comparator.comparing(LocationTypeTO::getName)) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PathConverter.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PathConverter.java new file mode 100644 index 0000000..9b1898a --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PathConverter.java @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.converter; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.access.to.model.PathCreationTO; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Path; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.PathTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.CoupleTO; + +/** + * Includes the conversion methods for all Path classes. + */ +public class PathConverter { + + private final PropertyConverter pConverter; + private final PeripheralOperationConverter pOConverter; + private final EnvelopeConverter envelopeConverter; + + @Inject + public PathConverter( + PropertyConverter pConverter, PeripheralOperationConverter pOConverter, + EnvelopeConverter envelopeConverter + ) { + this.pConverter = requireNonNull(pConverter, "pConverter"); + this.pOConverter = requireNonNull(pOConverter, "pOConverter"); + this.envelopeConverter = requireNonNull(envelopeConverter, "envelopeConverter"); + } + + public List toPathTOs(Set paths) { + return paths.stream() + .map( + path -> new PathTO( + path.getName(), + path.getSourcePoint().getName(), + path.getDestinationPoint().getName() + ) + .setLength(path.getLength()) + .setLocked(path.isLocked()) + .setMaxReverseVelocity(path.getMaxReverseVelocity()) + .setMaxVelocity(path.getMaxVelocity()) + .setVehicleEnvelopes(envelopeConverter.toEnvelopeTOs(path.getVehicleEnvelopes())) + .setProperties(pConverter.toPropertyTOs(path.getProperties())) + .setPeripheralOperations( + pOConverter.toPeripheralOperationsTOs(path.getPeripheralOperations()) + ) + .setLayout( + new PathTO.Layout() + .setLayerId(path.getLayout().getLayerId()) + .setConnectionType(path.getLayout().getConnectionType().name()) + .setControlPoints(toCoupleTOs(path.getLayout().getControlPoints())) + ) + ) + .sorted(Comparator.comparing(PathTO::getName)) + .collect(Collectors.toList()); + } + + public List toPathCreationTOs(List paths) { + return paths.stream() + .map( + path -> new PathCreationTO( + path.getName(), + path.getSrcPointName(), + path.getDestPointName() + ) + .withName(path.getName()) + .withProperties(pConverter.toPropertyMap(path.getProperties())) + .withLength(path.getLength()) + .withMaxVelocity(path.getMaxVelocity()) + .withMaxReverseVelocity(path.getMaxReverseVelocity()) + .withLocked(path.isLocked()) + .withLayout(toPathCreationTOLayout(path.getLayout())) + .withVehicleEnvelopes( + envelopeConverter + .toVehicleEnvelopeMap(path.getVehicleEnvelopes()) + ) + .withPeripheralOperations( + pOConverter.toPeripheralOperationCreationTOs(path.getPeripheralOperations()) + ) + ) + .collect(Collectors.toCollection(ArrayList::new)); + } + + private PathCreationTO.Layout toPathCreationTOLayout(PathTO.Layout layout) { + return new PathCreationTO.Layout( + Path.Layout.ConnectionType.valueOf(layout.getConnectionType()), + layout.getControlPoints() + .stream() + .map(cp -> new Couple(cp.getX(), cp.getY())) + .collect(Collectors.toList()), + layout.getLayerId() + ); + } + + private List toCoupleTOs(List controlPoints) { + return controlPoints.stream() + .map(cp -> new CoupleTO(cp.getX(), cp.getY())) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PeripheralOperationConverter.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PeripheralOperationConverter.java new file mode 100644 index 0000000..577fac9 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PeripheralOperationConverter.java @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.converter; + +import java.util.List; +import java.util.stream.Collectors; +import org.opentcs.access.to.peripherals.PeripheralOperationCreationTO; +import org.opentcs.data.peripherals.PeripheralOperation; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.PeripheralOperationTO; + +/** + * Includes the conversion methods for all PeripheralOperation classes. + */ +public class PeripheralOperationConverter { + + public PeripheralOperationConverter() { + } + + public List toPeripheralOperationsTOs( + List peripheralOperations + ) { + return peripheralOperations.stream() + .map( + perOp -> new PeripheralOperationTO( + perOp.getOperation(), + perOp.getLocation().getName() + ) + .setCompletionRequired(perOp.isCompletionRequired()) + .setExecutionTrigger( + perOp.getExecutionTrigger().name() + ) + ) + .collect(Collectors.toList()); + } + + public List toPeripheralOperationCreationTOs( + List perOps + ) { + return perOps.stream() + .map( + perOp -> new PeripheralOperationCreationTO( + perOp.getOperation(), + perOp.getLocationName() + ) + .withCompletionRequired(perOp.isCompletionRequired()) + .withExecutionTrigger( + PeripheralOperation.ExecutionTrigger.valueOf( + perOp.getExecutionTrigger() + ) + ) + ) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PointConverter.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PointConverter.java new file mode 100644 index 0000000..7f4c36d --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PointConverter.java @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.converter; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.access.to.model.PointCreationTO; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Pose; +import org.opentcs.data.model.Triple; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.PointTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.CoupleTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.TripleTO; + +/** + * Includes the conversion methods for all Point classes. + */ +public class PointConverter { + + private final PropertyConverter pConverter; + private final EnvelopeConverter envelopeConverter; + + @Inject + public PointConverter(PropertyConverter pConverter, EnvelopeConverter envelopeConverter) { + this.pConverter = requireNonNull(pConverter, "pConverter"); + this.envelopeConverter = requireNonNull(envelopeConverter, "envelopeConverter"); + } + + public List toPointTOs(Set points) { + return points.stream() + .map( + point -> new PointTO(point.getName()) + .setPosition( + new TripleTO( + point.getPose().getPosition().getX(), + point.getPose().getPosition().getY(), + point.getPose().getPosition().getZ() + ) + ) + .setType(point.getType().name()) + .setVehicleOrientationAngle(point.getPose().getOrientationAngle()) + .setVehicleEnvelopes(envelopeConverter.toEnvelopeTOs(point.getVehicleEnvelopes())) + .setProperties(pConverter.toPropertyTOs(point.getProperties())) + .setLayout( + new PointTO.Layout() + .setLabelOffset( + new CoupleTO( + point.getLayout().getLabelOffset().getX(), + point.getLayout().getLabelOffset().getY() + ) + ) + .setPosition( + new CoupleTO( + point.getLayout().getPosition().getX(), + point.getLayout().getPosition().getY() + ) + ) + .setLayerId(point.getLayout().getLayerId()) + ) + ) + .sorted(Comparator.comparing(PointTO::getName)) + .collect(Collectors.toCollection(ArrayList::new)); + } + + public List toPointCreationTOs(List points) { + return points.stream() + .map( + point -> new PointCreationTO(point.getName()) + .withProperties(pConverter.toPropertyMap(point.getProperties())) + .withPose( + new Pose( + new Triple( + point.getPosition().getX(), + point.getPosition().getY(), + point.getPosition().getZ() + ), + point.getVehicleOrientationAngle() + ) + ) + .withType(Point.Type.valueOf(point.getType())) + .withLayout( + new PointCreationTO.Layout( + new Couple( + point.getLayout().getPosition().getX(), + point.getLayout().getPosition().getY() + ), + new Couple( + point.getLayout().getLabelOffset().getX(), + point.getLayout().getLabelOffset().getY() + ), + point.getLayout().getLayerId() + ) + ) + .withVehicleEnvelopes( + envelopeConverter + .toVehicleEnvelopeMap(point.getVehicleEnvelopes()) + ) + ) + .collect(Collectors.toCollection(ArrayList::new)); + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PropertyConverter.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PropertyConverter.java new file mode 100644 index 0000000..4dac6b4 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PropertyConverter.java @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.converter; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PropertyTO; + +/** + * Includes the conversion methods for all Property classes. + */ +public class PropertyConverter { + + public PropertyConverter() { + } + + public List toPropertyTOs(Map properties) { + return properties.entrySet().stream() + .map(property -> new PropertyTO(property.getKey(), property.getValue())) + .sorted(Comparator.comparing(PropertyTO::getName)) + .collect(Collectors.toList()); + } + + public Map toPropertyMap(List properties) { + return properties.stream() + .collect(Collectors.toMap(PropertyTO::getName, PropertyTO::getValue)); + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/VehicleConverter.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/VehicleConverter.java new file mode 100644 index 0000000..a6a6444 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/VehicleConverter.java @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.converter; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.access.to.model.BoundingBoxCreationTO; +import org.opentcs.access.to.model.VehicleCreationTO; +import org.opentcs.data.model.Vehicle; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.VehicleTO; +import org.opentcs.util.Colors; + +/** + * Includes the conversion methods for all Vehicle classes. + */ +public class VehicleConverter { + + private final PropertyConverter pConverter; + + @Inject + public VehicleConverter(PropertyConverter pConverter) { + this.pConverter = requireNonNull(pConverter, "pConverter"); + } + + public List toVehicleCreationTOs(List vehicles) { + return vehicles.stream() + .map( + vehicle -> new VehicleCreationTO(vehicle.getName()) + .withProperties(pConverter.toPropertyMap(vehicle.getProperties())) + .withBoundingBox(new BoundingBoxCreationTO(vehicle.getLength(), 1000, 1000)) + .withEnergyLevelThresholdSet( + new VehicleCreationTO.EnergyLevelThresholdSet( + vehicle.getEnergyLevelCritical(), + vehicle.getEnergyLevelGood(), + vehicle.getEnergyLevelSufficientlyRecharged(), + vehicle.getEnergyLevelFullyRecharged() + ) + ) + .withMaxVelocity(vehicle.getMaxVelocity()) + .withMaxReverseVelocity(vehicle.getMaxReverseVelocity()) + .withLayout( + new VehicleCreationTO.Layout( + Colors.decodeFromHexRGB(vehicle.getLayout().getRouteColor()) + ) + ) + ) + .collect(Collectors.toCollection(ArrayList::new)); + } + + public List toVehicleTOs(Set vehicles) { + return vehicles.stream() + .map( + vehicle -> new VehicleTO(vehicle.getName()) + .setLength((int) vehicle.getBoundingBox().getLength()) + .setEnergyLevelCritical( + vehicle.getEnergyLevelThresholdSet().getEnergyLevelCritical() + ) + .setEnergyLevelGood(vehicle.getEnergyLevelThresholdSet().getEnergyLevelGood()) + .setEnergyLevelFullyRecharged( + vehicle.getEnergyLevelThresholdSet().getEnergyLevelFullyRecharged() + ) + .setEnergyLevelSufficientlyRecharged( + vehicle.getEnergyLevelThresholdSet().getEnergyLevelSufficientlyRecharged() + ) + .setMaxVelocity(vehicle.getMaxVelocity()) + .setMaxReverseVelocity(vehicle.getMaxReverseVelocity()) + .setLayout( + new VehicleTO.Layout() + .setRouteColor(Colors.encodeToHexRGB(vehicle.getLayout().getRouteColor())) + ) + .setProperties(pConverter.toPropertyTOs(vehicle.getProperties())) + ) + .sorted(Comparator.comparing(VehicleTO::getName)) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/VisualLayoutConverter.java b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/VisualLayoutConverter.java new file mode 100644 index 0000000..d6263f9 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/main/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/VisualLayoutConverter.java @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.converter; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.List; +import java.util.stream.Collectors; +import org.opentcs.access.to.model.VisualLayoutCreationTO; +import org.opentcs.data.model.visualization.Layer; +import org.opentcs.data.model.visualization.LayerGroup; +import org.opentcs.data.model.visualization.VisualLayout; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.LayerGroupTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.LayerTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.VisualLayoutTO; + +/** + * Includes the conversion methods for all VisualLayout classes. + */ +public class VisualLayoutConverter { + + private final PropertyConverter pConverter; + + @Inject + public VisualLayoutConverter(PropertyConverter pConverter) { + this.pConverter = requireNonNull(pConverter, "pConverter"); + } + + public VisualLayoutCreationTO toVisualLayoutCreationTO(VisualLayoutTO vLayout) { + return new VisualLayoutCreationTO(vLayout.getName()) + .withProperties(pConverter.toPropertyMap(vLayout.getProperties())) + .withScaleX(vLayout.getScaleX()) + .withScaleY(vLayout.getScaleY()) + .withLayers(convertLayers(vLayout.getLayers())) + .withLayerGroups(convertLayerGroups(vLayout.getLayerGroups())); + } + + public VisualLayoutTO toVisualLayoutTO(VisualLayout visualLayout) { + return new VisualLayoutTO(visualLayout.getName()) + .setProperties(pConverter.toPropertyTOs(visualLayout.getProperties())) + .setScaleX(visualLayout.getScaleX()) + .setScaleY(visualLayout.getScaleY()) + .setLayers(toLayerTOs(visualLayout.getLayers())) + .setLayerGroups(toLayerGroupTOs(visualLayout.getLayerGroups())); + } + + private List convertLayerGroups(List layerGroups) { + return layerGroups.stream() + .map( + layerGroup -> new LayerGroup( + layerGroup.getId(), + layerGroup.getName(), + layerGroup.isVisible() + ) + ) + .collect(Collectors.toList()); + } + + private List toLayerGroupTOs(List layerGroups) { + return layerGroups.stream() + .map( + layerGroup -> new LayerGroupTO( + layerGroup.getId(), + layerGroup.getName(), + layerGroup.isVisible() + ) + ) + .collect(Collectors.toList()); + } + + private List convertLayers(List layers) { + return layers.stream() + .map( + layer -> new Layer( + layer.getId(), + layer.getOrdinal(), + layer.isVisible(), + layer.getName(), + layer.getGroupId() + ) + ) + .collect(Collectors.toList()); + } + + private List toLayerTOs(List layers) { + return layers.stream() + .map( + layer -> new LayerTO( + layer.getId(), + layer.getOrdinal(), + layer.isVisible(), + layer.getName(), + layer.getGroupId() + ) + ) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/AuthenticatorTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/AuthenticatorTest.java new file mode 100644 index 0000000..243c80c --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/AuthenticatorTest.java @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import spark.Request; + +/** + */ +class AuthenticatorTest { + + private ServiceWebApiConfiguration configuration; + + private Authenticator authenticator; + + @BeforeEach + void setUp() { + configuration = mock(ServiceWebApiConfiguration.class); + authenticator = new Authenticator(configuration); + } + + @Test + void allowRequestsIfNoAccessKeyConfigured() { + when(configuration.accessKey()).thenReturn(""); + + assertTrue(authenticator.isAuthenticated(aRequestWithAccessKey(null))); + assertTrue(authenticator.isAuthenticated(aRequestWithAccessKey(""))); + assertTrue(authenticator.isAuthenticated(aRequestWithAccessKey("some random value"))); + } + + @Test + void allowRequestIfCorrectAccessKeyGiven() { + when(configuration.accessKey()).thenReturn("my access key"); + + assertTrue(authenticator.isAuthenticated(aRequestWithAccessKey("my access key"))); + } + + @Test + void disallowRequestIfWrongAccessKeyGiven() { + when(configuration.accessKey()).thenReturn("my access key"); + + assertFalse(authenticator.isAuthenticated(aRequestWithAccessKey(null))); + assertFalse(authenticator.isAuthenticated(aRequestWithAccessKey(""))); + assertFalse(authenticator.isAuthenticated(aRequestWithAccessKey("some random value"))); + } + + private Request aRequestWithAccessKey(String accessKey) { + Request request = mock(Request.class); + + when(request.headers(HttpConstants.HEADER_NAME_ACCESS_KEY)).thenReturn(accessKey); + + return request; + } + +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/JsonBinderTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/JsonBinderTest.java new file mode 100644 index 0000000..c567a8d --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/JsonBinderTest.java @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +import org.approvaltests.Approvals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link JsonBinder}. + */ +class JsonBinderTest { + + private JsonBinder jsonBinder; + + @BeforeEach + void setUp() { + jsonBinder = new JsonBinder(); + } + + @Test + void writeAndParseObject() { + String json = jsonBinder.toJson(new TestObject().setName("some-name")); + + Approvals.verify(json); + + TestObject parsedObject = jsonBinder.fromJson(json, TestObject.class); + + assertThat(parsedObject.getName(), is(equalTo("some-name"))); + } + + @Test + void writeAndParseThrowable() { + Approvals.verify(jsonBinder.toJson(new TestException("some-message"))); + } + + private static class TestObject { + + private String name; + + public String getName() { + return name; + } + + public TestObject setName(String name) { + this.name = name; + return this; + } + } + + private static class TestException + extends + Exception { + + TestException(String message) { + super(message); + } + + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/JsonBinderTest.writeAndParseObject.approved.txt b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/JsonBinderTest.writeAndParseObject.approved.txt new file mode 100644 index 0000000..9845255 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/JsonBinderTest.writeAndParseObject.approved.txt @@ -0,0 +1,3 @@ +{ + "name" : "some-name" +} \ No newline at end of file diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/JsonBinderTest.writeAndParseThrowable.approved.txt b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/JsonBinderTest.writeAndParseThrowable.approved.txt new file mode 100644 index 0000000..db9de17 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/JsonBinderTest.writeAndParseThrowable.approved.txt @@ -0,0 +1 @@ +[ "some-message" ] \ No newline at end of file diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/KernelExecutorWrapperTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/KernelExecutorWrapperTest.java new file mode 100644 index 0000000..7b30c47 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/KernelExecutorWrapperTest.java @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.data.ObjectExistsException; +import org.opentcs.data.ObjectUnknownException; + +/** + * Tests for {@link KernelExecutorWrapper}. + */ +class KernelExecutorWrapperTest { + + private ExecutorService executorService; + private KernelExecutorWrapper executorWrapper; + + @BeforeEach + void setUp() { + this.executorService = Executors.newSingleThreadExecutor(); + this.executorWrapper = new KernelExecutorWrapper(executorService); + } + + @AfterEach + void tearDown() { + executorService.shutdown(); + } + + @Test + void returnValueReturnedInCallable() { + assertThat( + executorWrapper.callAndWait(() -> "my result"), + is("my result") + ); + } + + @Test + void forwardCausingExceptionIfRuntimeException() { + assertThrows( + ObjectUnknownException.class, + () -> { + executorWrapper.callAndWait(() -> { + throw new ObjectUnknownException("some exception"); + }); + } + ); + + assertThrows( + ObjectExistsException.class, + () -> { + executorWrapper.callAndWait(() -> { + throw new ObjectExistsException("some exception"); + }); + } + ); + } + + @Test + void wrapUnhandledExceptionInKernelRuntimeException() { + assertThrows( + KernelRuntimeException.class, + () -> { + executorWrapper.callAndWait(() -> { + throw new Exception("some exception"); + }); + } + ); + + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/FiltersTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/FiltersTest.java new file mode 100644 index 0000000..10ec612 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/FiltersTest.java @@ -0,0 +1,286 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.data.peripherals.PeripheralOperation; + +/** + * Unit tests for {@link Filters}. + */ +class FiltersTest { + + @Test + void acceptTransportOrdersRegardlessOfIntendedVehicle() { + assertThat( + Filters.transportOrderWithIntendedVehicle(null) + .test( + new TransportOrder("some-order", List.of()) + .withIntendedVehicle(null) + ), + is(true) + ); + + assertThat( + Filters.transportOrderWithIntendedVehicle(null) + .test( + new TransportOrder("some-order", List.of()) + .withIntendedVehicle(new Vehicle("some-vehicle").getReference()) + ), + is(true) + ); + } + + @Test + void acceptTransportOrdersWithGivenIntendedVehicle() { + Vehicle vehicle = new Vehicle("some-vehicle"); + + assertThat( + Filters.transportOrderWithIntendedVehicle(vehicle.getReference()) + .test( + new TransportOrder("some-order", List.of()) + .withIntendedVehicle(null) + ), + is(false) + ); + + assertThat( + Filters.transportOrderWithIntendedVehicle(null) + .test( + new TransportOrder("some-order", List.of()) + .withIntendedVehicle(vehicle.getReference()) + ), + is(true) + ); + } + + @Test + void acceptOrderSequencesRegardlessOfIntendedVehicle() { + assertThat( + Filters.orderSequenceWithIntendedVehicle(null) + .test( + new OrderSequence("some-sequence") + .withIntendedVehicle(null) + ), + is(true) + ); + + assertThat( + Filters.orderSequenceWithIntendedVehicle(null) + .test( + new OrderSequence("some-sequence") + .withIntendedVehicle(new Vehicle("some-vehicle").getReference()) + ), + is(true) + ); + } + + @Test + void acceptOrderSequenceWithGivenIntendedVehicle() { + Vehicle vehicle = new Vehicle("some-vehicle"); + + assertThat( + Filters.orderSequenceWithIntendedVehicle(vehicle.getReference()) + .test( + new OrderSequence("some-sequence") + .withIntendedVehicle(null) + ), + is(false) + ); + + assertThat( + Filters.orderSequenceWithIntendedVehicle(null) + .test( + new OrderSequence("some-sequence") + .withIntendedVehicle(new Vehicle("some-vehicle").getReference()) + ), + is(true) + ); + } + + @Test + void acceptPeripheralJobsRegardlessOfRelatedVehicle() { + Location location + = new Location("some-location", new LocationType("some-location-type").getReference()); + PeripheralJob job = new PeripheralJob( + "some-job", + "some-token", + new PeripheralOperation( + location.getReference(), + "some-operation", + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + true + ) + ); + + assertThat( + Filters.peripheralJobWithRelatedVehicle(null) + .test( + job.withRelatedVehicle(null) + ), + is(true) + ); + + assertThat( + Filters.peripheralJobWithRelatedVehicle(null) + .test( + job.withRelatedVehicle(new Vehicle("some-vehicle").getReference()) + ), + is(true) + ); + } + + @Test + void acceptPeripheralJobsWithGivenRelatedVehicle() { + Location location + = new Location("some-location", new LocationType("some-location-type").getReference()); + PeripheralJob job = new PeripheralJob( + "some-job", + "some-token", + new PeripheralOperation( + location.getReference(), + "some-operation", + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + true + ) + ); + Vehicle vehicle = new Vehicle("some-vehicle"); + + assertThat( + Filters.peripheralJobWithRelatedVehicle(vehicle.getReference()) + .test( + job.withRelatedVehicle(null) + ), + is(false) + ); + + assertThat( + Filters.peripheralJobWithRelatedVehicle(vehicle.getReference()) + .test( + job.withRelatedVehicle(vehicle.getReference()) + ), + is(true) + ); + } + + @Test + void acceptPeripheralJobsRegardlessOfRelatedTransportOrder() { + Location location + = new Location("some-location", new LocationType("some-location-type").getReference()); + PeripheralJob job = new PeripheralJob( + "some-job", + "some-token", + new PeripheralOperation( + location.getReference(), + "some-operation", + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + true + ) + ); + + assertThat( + Filters.peripheralJobWithRelatedTransportOrder(null) + .test( + job.withRelatedTransportOrder(null) + ), + is(true) + ); + + assertThat( + Filters.peripheralJobWithRelatedTransportOrder(null) + .test( + job.withRelatedTransportOrder( + new TransportOrder("some-order", List.of()).getReference() + ) + ), + is(true) + ); + } + + @Test + void acceptPeripheralJobsWithGivenRelatedTransportOrder() { + Location location + = new Location("some-location", new LocationType("some-location-type").getReference()); + PeripheralJob job = new PeripheralJob( + "some-job", + "some-token", + new PeripheralOperation( + location.getReference(), + "some-operation", + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + true + ) + ); + TransportOrder order = new TransportOrder("some-order", List.of()); + + assertThat( + Filters.peripheralJobWithRelatedTransportOrder(order.getReference()) + .test( + job.withRelatedTransportOrder(null) + ), + is(false) + ); + + assertThat( + Filters.peripheralJobWithRelatedTransportOrder(order.getReference()) + .test( + job.withRelatedTransportOrder(order.getReference()) + ), + is(true) + ); + } + + @ParameterizedTest + @EnumSource(Vehicle.ProcState.class) + void acceptVehiclesWithAnyProcState(Vehicle.ProcState procState) { + assertThat( + Filters.vehicleWithProcState(null) + .test( + new Vehicle("some-vehicle") + .withProcState(procState) + ), + is(true) + ); + } + + @Test + void acceptVehiclesWithGivenProcStateOnly() { + assertThat( + Filters.vehicleWithProcState(Vehicle.ProcState.IDLE) + .test( + new Vehicle("some-vehicle") + .withProcState(Vehicle.ProcState.IDLE) + ), + is(true) + ); + + assertThat( + Filters.vehicleWithProcState(Vehicle.ProcState.IDLE) + .test( + new Vehicle("some-vehicle") + .withProcState(Vehicle.ProcState.AWAITING_ORDER) + ), + is(false) + ); + assertThat( + Filters.vehicleWithProcState(Vehicle.ProcState.IDLE) + .test( + new Vehicle("some-vehicle") + .withProcState(Vehicle.ProcState.PROCESSING_ORDER) + ), + is(false) + ); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/LocationHandlerTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/LocationHandlerTest.java new file mode 100644 index 0000000..094efe5 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/LocationHandlerTest.java @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +import java.util.concurrent.Executors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.opentcs.components.kernel.services.PlantModelService; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.kernel.extensions.servicewebapi.KernelExecutorWrapper; + +/** + * Tests for {@link LocationHandler}. + */ +class LocationHandlerTest { + + private PlantModelService plantModelService; + private KernelExecutorWrapper executorWrapper; + + private LocationHandler handler; + + private Location location; + + @BeforeEach + void setUp() { + plantModelService = mock(); + executorWrapper = new KernelExecutorWrapper(Executors.newSingleThreadExecutor()); + + handler = new LocationHandler(plantModelService, executorWrapper); + + location = new Location( + "some-location", + new LocationType("some-location-type").getReference() + ); + given(plantModelService.fetchObject(Location.class, "some-location")) + .willReturn(location); + } + + @Test + void lockLocation() { + handler.updateLocationLock("some-location", "true"); + + then(plantModelService).should().updateLocationLock(location.getReference(), true); + } + + @ParameterizedTest + @ValueSource(strings = {"false", "flase", "some-value-that-is-not-true"}) + void unlockLocationOnAnyNontrueValue(String value) { + handler.updateLocationLock("some-location", value); + + then(plantModelService).should().updateLocationLock(location.getReference(), false); + } + + @ParameterizedTest + @ValueSource(strings = {"true", "false"}) + void throwOnLockUnknownLocation(String value) { + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.updateLocationLock("some-unknown-location", value)); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/PathHandlerTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/PathHandlerTest.java new file mode 100644 index 0000000..b64de04 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/PathHandlerTest.java @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +import java.util.concurrent.Executors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.opentcs.components.kernel.services.PlantModelService; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.kernel.extensions.servicewebapi.KernelExecutorWrapper; + +/** + * Tests for {@link PathHandler}. + */ +class PathHandlerTest { + + private TCSObjectService objectService; + private PlantModelService plantModelService; + private KernelExecutorWrapper executorWrapper; + + private PathHandler handler; + + private Path path; + + @BeforeEach + void setUp() { + objectService = mock(); + plantModelService = mock(); + executorWrapper = new KernelExecutorWrapper(Executors.newSingleThreadExecutor()); + + handler = new PathHandler(objectService, executorWrapper, plantModelService); + + path = new Path( + "some-path", + new Point("some-point-1").getReference(), + new Point("some-point-2").getReference() + ); + given(objectService.fetchObject(Path.class, "some-path")) + .willReturn(path); + } + + @Test + void lockPath() { + handler.updatePathLock("some-path", "true"); + + then(plantModelService).should().updatePathLock(path.getReference(), true); + } + + @ParameterizedTest + @ValueSource(strings = {"false", "flase", "some-value-that-is-not-true"}) + void unlockPathOnAnyNontrueValue(String value) { + handler.updatePathLock("some-path", value); + + then(plantModelService).should().updatePathLock(path.getReference(), false); + } + + @ParameterizedTest + @ValueSource(strings = {"true", "false"}) + void throwOnLockUnknownPath(String value) { + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.updatePathLock("some-unknown-path", value)); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/PeripheralHandlerTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/PeripheralHandlerTest.java new file mode 100644 index 0000000..f0e0250 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/PeripheralHandlerTest.java @@ -0,0 +1,157 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +import java.util.List; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.opentcs.common.peripherals.NullPeripheralCommAdapterDescription; +import org.opentcs.components.kernel.services.PeripheralService; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; +import org.opentcs.drivers.peripherals.management.PeripheralAttachmentInformation; +import org.opentcs.kernel.extensions.servicewebapi.KernelExecutorWrapper; + +/** + * Unit tests for {@link PeripheralJobHandler}. + */ +class PeripheralHandlerTest { + + private PeripheralService peripheralService; + private KernelExecutorWrapper executorWrapper; + + private PeripheralHandler handler; + + private Location location; + private PeripheralCommAdapterDescription adapterDescriptionNull; + private PeripheralCommAdapterDescription adapterDescriptionMock; + private PeripheralAttachmentInformation attachmentInfo; + + @BeforeEach + void setUp() { + peripheralService = mock(); + executorWrapper = new KernelExecutorWrapper(Executors.newSingleThreadExecutor()); + + handler = new PeripheralHandler(peripheralService, executorWrapper); + + location = new Location("some-location", new LocationType("some-location-type").getReference()); + adapterDescriptionNull = new NullPeripheralCommAdapterDescription(); + adapterDescriptionMock = new MockPeripheralCommAdapterDescription(); + attachmentInfo = new PeripheralAttachmentInformation( + location.getReference(), + List.of( + adapterDescriptionNull, + adapterDescriptionMock + ), + adapterDescriptionNull + ); + + given(peripheralService.fetchObject(Location.class, "some-location")) + .willReturn(location); + given(peripheralService.fetchAttachmentInformation(location.getReference())) + .willReturn(attachmentInfo); + } + + @Test + void attachNullPeripheralAdapter() { + handler.putPeripheralCommAdapter( + "some-location", + NullPeripheralCommAdapterDescription.class.getName() + ); + + then(peripheralService) + .should() + .attachCommAdapter(location.getReference(), adapterDescriptionNull); + } + + @Test + void attachMockPeripheralAdapter() { + handler.putPeripheralCommAdapter( + "some-location", + MockPeripheralCommAdapterDescription.class.getName() + ); + + then(peripheralService) + .should() + .attachCommAdapter(location.getReference(), adapterDescriptionMock); + } + + @Test + void throwOnAttachAdapterForUnknownLocation() { + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy( + () -> handler.putPeripheralCommAdapter( + "some-unknown-location", + NullPeripheralCommAdapterDescription.class.getName() + ) + ); + } + + @Test + void throwOnAttachUnknownAdapter() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy( + () -> handler.putPeripheralCommAdapter( + "some-location", + "some-unknown-adapter-class-name" + ) + ); + } + + @Test + void enableCommAdapter() { + handler.putPeripheralCommAdapterEnabled("some-location", "true"); + + then(peripheralService).should().enableCommAdapter(location.getReference()); + } + + @ParameterizedTest + @ValueSource(strings = {"false", "flase", "some-value-that-is-not-true"}) + void disableCommAdapterOnAnyNontrueValue(String value) { + handler.putPeripheralCommAdapterEnabled("some-location", value); + + then(peripheralService).should().disableCommAdapter(location.getReference()); + } + + @ParameterizedTest + @ValueSource(strings = {"true ", "false"}) + void throwOnEnableUnknownLocation(String value) { + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.putPeripheralCommAdapterEnabled("some-unknown-location", value)); + } + + @Test + void fetchAttachmentInformation() { + assertThat(handler.getPeripheralCommAdapterAttachmentInformation("some-location")) + .isSameAs(attachmentInfo); + } + + @Test + void throwOnFetchInfoForUnknownLocation() { + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy( + () -> handler.getPeripheralCommAdapterAttachmentInformation("some-unknown-location") + ); + } + + static class MockPeripheralCommAdapterDescription + extends + PeripheralCommAdapterDescription { + + @Override + public String getDescription() { + return "some-peripheral-comm-adapter"; + } + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/PeripheralJobDispatcherHandlerTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/PeripheralJobDispatcherHandlerTest.java new file mode 100644 index 0000000..1373a9f --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/PeripheralJobDispatcherHandlerTest.java @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +import java.util.concurrent.Executors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.services.PeripheralDispatcherService; +import org.opentcs.components.kernel.services.PeripheralJobService; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.data.peripherals.PeripheralOperation; +import org.opentcs.kernel.extensions.servicewebapi.KernelExecutorWrapper; + +/** + * Unit tests for {@link PeripheralJobDispatcherHandler}. + */ +class PeripheralJobDispatcherHandlerTest { + + private PeripheralJobService jobService; + private PeripheralDispatcherService jobDispatcherService; + private KernelExecutorWrapper executorWrapper; + + private PeripheralJobDispatcherHandler handler; + + private Location location; + private PeripheralJob job; + + @BeforeEach + void setUp() { + jobService = mock(); + jobDispatcherService = mock(); + executorWrapper = new KernelExecutorWrapper(Executors.newSingleThreadExecutor()); + + handler = new PeripheralJobDispatcherHandler( + jobService, + jobDispatcherService, + executorWrapper + ); + + location = new Location("some-location", new LocationType("some-location-type").getReference()); + job = new PeripheralJob( + "some-job", + "some-token", + new PeripheralOperation( + location.getReference(), + "some-operation", + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + true + ) + ); + + given(jobService.fetchObject(Location.class, "some-location")) + .willReturn(location); + given(jobService.fetchObject(PeripheralJob.class, "some-job")) + .willReturn(job); + } + + @Test + void withdrawPeripheralJobByLocation() { + handler.withdrawPeripheralJobByLocation("some-location"); + then(jobDispatcherService).should().withdrawByLocation(location.getReference()); + } + + @Test + void throwOnWithdrawPeripheralJobByUnknownLocation() { + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.withdrawPeripheralJobByLocation("some-unknown-location")); + } + + @Test + void withdrawPeripheralJob() { + handler.withdrawPeripheralJob("some-job"); + then(jobDispatcherService).should().withdrawByPeripheralJob(job.getReference()); + } + + @Test + void throwOnWithdrawUnknownPeripheralJob() { + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.withdrawPeripheralJob("some-unknown-job")); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/PeripheralJobHandlerTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/PeripheralJobHandlerTest.java new file mode 100644 index 0000000..3240ba6 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/PeripheralJobHandlerTest.java @@ -0,0 +1,267 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.theInstance; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.opentcs.access.to.peripherals.PeripheralJobCreationTO; +import org.opentcs.components.kernel.services.PeripheralDispatcherService; +import org.opentcs.components.kernel.services.PeripheralJobService; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.data.peripherals.PeripheralOperation; +import org.opentcs.kernel.extensions.servicewebapi.KernelExecutorWrapper; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.GetPeripheralJobResponseTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PostPeripheralJobRequestTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PeripheralOperationDescription; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.Property; + +/** + * Unit tests for {@link PeripheralJobHandler}. + */ +class PeripheralJobHandlerTest { + + private PeripheralJobService jobService; + private PeripheralDispatcherService jobDispatcherService; + private KernelExecutorWrapper executorWrapper; + + private PeripheralJobHandler handler; + + @BeforeEach + void setUp() { + jobService = mock(); + jobDispatcherService = mock(); + executorWrapper = new KernelExecutorWrapper(Executors.newSingleThreadExecutor()); + + handler = new PeripheralJobHandler( + jobService, + jobDispatcherService, + executorWrapper + ); + } + + @Test + void createPeripheralJob() { + // Arrange + Location location + = new Location("some-location", new LocationType("some-location-type").getReference()); + Vehicle vehicle = new Vehicle("some-vehicle"); + TransportOrder order = new TransportOrder("some-order", List.of()); + PeripheralJob job = new PeripheralJob( + "some-job", + "some-token", + new PeripheralOperation( + location.getReference(), + "some-operation", + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + true + ) + ) + .withRelatedVehicle(vehicle.getReference()) + .withRelatedTransportOrder(order.getReference()) + .withProperty("some-prop-key", "some-prop-value"); + given(jobService.fetchObject(Location.class, "some-location")) + .willReturn(location); + given(jobService.fetchObject(Vehicle.class, "some-vehicle")) + .willReturn(vehicle); + given(jobService.fetchObject(TransportOrder.class, "some-order")) + .willReturn(order); + given(jobService.createPeripheralJob(any(PeripheralJobCreationTO.class))) + .willReturn(job); + + // Act + PeripheralJob result = handler.createPeripheralJob( + "some-job", + new PostPeripheralJobRequestTO() + .setIncompleteName(false) + .setReservationToken("some-token") + .setPeripheralOperation( + new PeripheralOperationDescription() + .setLocationName("some-location") + .setOperation("some-operation") + .setExecutionTrigger(PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION) + .setCompletionRequired(true) + ) + .setRelatedVehicle("some-vehicle") + .setRelatedTransportOrder("some-order") + .setProperties(List.of(new Property("some-prop-key", "some-prop-value"))) + ); + + // Assert + assertThat(result, is(theInstance(job))); + then(jobService).should().createPeripheralJob(any(PeripheralJobCreationTO.class)); + } + + @Test + void retrievePeripheralJobsUnfiltered() { + // Arrange + Location location + = new Location("some-location", new LocationType("some-location-type").getReference()); + PeripheralJob job1 = new PeripheralJob( + "some-job", + "some-token", + new PeripheralOperation( + location.getReference(), + "some-operation", + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + true + ) + ); + PeripheralJob job2 = new PeripheralJob( + "some-job-2", + "some-token", + new PeripheralOperation( + location.getReference(), + "some-operation", + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + true + ) + ); + given( + jobService.fetchObjects(ArgumentMatchers.>any(), any()) + ) + .willReturn(Set.of(job1, job2)); + + // Act + List result = handler.getPeripheralJobs(null, null); + + // Assert + assertThat(result, hasSize(2)); + then(jobService).should().fetchObjects(ArgumentMatchers.>any(), any()); + } + + @Test + void retrievePeripheralJobsFilteredByRelatedVehicle() { + // Arrange + Location location + = new Location("some-location", new LocationType("some-location-type").getReference()); + PeripheralJob job1 = new PeripheralJob( + "some-job", + "some-token", + new PeripheralOperation( + location.getReference(), + "some-operation", + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + true + ) + ); + PeripheralJob job2 = new PeripheralJob( + "some-job-2", + "some-token", + new PeripheralOperation( + location.getReference(), + "some-operation", + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + true + ) + ); + Vehicle vehicle = new Vehicle("some-vehicle"); + given(jobService.fetchObject(Vehicle.class, "some-vehicle")) + .willReturn(vehicle); + given( + jobService.fetchObjects(ArgumentMatchers.>any(), any()) + ) + .willReturn(Set.of(job1, job2)); + + // Act & Assert: happy path + List result = handler.getPeripheralJobs("some-vehicle", null); + + assertThat(result, hasSize(2)); + then(jobService).should().fetchObjects(ArgumentMatchers.>any(), any()); + + // Act & Assert: nonexistent vehicle + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.getPeripheralJobs("some-unknown-vehicle", null)); + } + + @Test + void retrievePeripheralJobsFilteredByRelatedTransportOrder() { + // Arrange + Location location + = new Location("some-location", new LocationType("some-location-type").getReference()); + PeripheralJob job1 = new PeripheralJob( + "some-job", + "some-token", + new PeripheralOperation( + location.getReference(), + "some-operation", + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + true + ) + ); + PeripheralJob job2 = new PeripheralJob( + "some-job-2", + "some-token", + new PeripheralOperation( + location.getReference(), + "some-operation", + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + true + ) + ); + TransportOrder transportOrder = new TransportOrder("some-order", List.of()); + given(jobService.fetchObject(TransportOrder.class, "some-order")) + .willReturn(transportOrder); + given( + jobService.fetchObjects(ArgumentMatchers.>any(), any()) + ) + .willReturn(Set.of(job1, job2)); + + // Act & Assert: happy path + List result = handler.getPeripheralJobs(null, "some-order"); + + assertThat(result, hasSize(2)); + then(jobService).should().fetchObjects(ArgumentMatchers.>any(), any()); + + // Act & Assert: nonexistent vehicle + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.getPeripheralJobs(null, "some-unknown-order")); + } + + @Test + void retrievPeripheralJobByName() { + // Arrange + Location location + = new Location("some-location", new LocationType("some-location-type").getReference()); + PeripheralJob job = new PeripheralJob( + "some-job", + "some-token", + new PeripheralOperation( + location.getReference(), + "some-operation", + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + true + ) + ); + given(jobService.fetchObject(PeripheralJob.class, "some-job")) + .willReturn(job); + + // Act & Assert: happy path + GetPeripheralJobResponseTO result = handler.getPeripheralJobByName("some-job"); + assertThat(result, is(notNullValue())); + then(jobService).should().fetchObject(PeripheralJob.class, "some-job"); + + // Act & Assert: nonexistent order + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.getPeripheralJobByName("some-unknown-order")); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/PlantModelHandlerTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/PlantModelHandlerTest.java new file mode 100644 index 0000000..b6213ac --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/PlantModelHandlerTest.java @@ -0,0 +1,253 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.components.kernel.services.PlantModelService; +import org.opentcs.components.kernel.services.RouterService; +import org.opentcs.data.model.Block; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.PlantModel; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.model.visualization.VisualLayout; +import org.opentcs.kernel.extensions.servicewebapi.KernelExecutorWrapper; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PlantModelTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PostTopologyUpdateRequestTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.BlockTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.LocationTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.LocationTypeTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.PathTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.PointTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.VehicleTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.VisualLayoutTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PropertyTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.TripleTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.converter.BlockConverter; +import org.opentcs.kernel.extensions.servicewebapi.v1.converter.EnvelopeConverter; +import org.opentcs.kernel.extensions.servicewebapi.v1.converter.LocationConverter; +import org.opentcs.kernel.extensions.servicewebapi.v1.converter.LocationTypeConverter; +import org.opentcs.kernel.extensions.servicewebapi.v1.converter.PathConverter; +import org.opentcs.kernel.extensions.servicewebapi.v1.converter.PeripheralOperationConverter; +import org.opentcs.kernel.extensions.servicewebapi.v1.converter.PointConverter; +import org.opentcs.kernel.extensions.servicewebapi.v1.converter.PropertyConverter; +import org.opentcs.kernel.extensions.servicewebapi.v1.converter.VehicleConverter; +import org.opentcs.kernel.extensions.servicewebapi.v1.converter.VisualLayoutConverter; + +/** + * Unit tests for {@link PlantModelHandler}. + */ +class PlantModelHandlerTest { + + private PlantModelService orderService; + private KernelExecutorWrapper executorWrapper; + private PlantModelHandler handler; + private RouterService routerService; + + @BeforeEach + void setUp() { + orderService = mock(); + executorWrapper = new KernelExecutorWrapper(Executors.newSingleThreadExecutor()); + EnvelopeConverter envelopeConverter = new EnvelopeConverter(); + PropertyConverter propertyConverter = new PropertyConverter(); + routerService = mock(); + + handler = new PlantModelHandler( + orderService, + executorWrapper, + new PointConverter(propertyConverter, envelopeConverter), + new PathConverter( + propertyConverter, + new PeripheralOperationConverter(), + envelopeConverter + ), + new LocationTypeConverter(propertyConverter), + new LocationConverter(propertyConverter), + new BlockConverter(propertyConverter), + new VehicleConverter(propertyConverter), + new VisualLayoutConverter(propertyConverter), + propertyConverter, + routerService + ); + } + + @Test + void putPlantModel() { + // Act + handler.putPlantModel( + new PlantModelTO("name") + .setPoints( + List.of( + new PointTO("some-point"), + new PointTO("some-other-point") + ) + ) + .setPaths( + List.of( + new PathTO("some-path", "src-point", "dest-point") + ) + ) + .setLocationTypes( + List.of( + new LocationTypeTO("some-loc-type"), + new LocationTypeTO("some-other-loc-type") + ) + ) + .setLocations( + List.of( + new LocationTO( + "some-location", + "some-loc-type", + new TripleTO(1, 2, 3) + ), + new LocationTO( + "some-other-location", + "some-other-loc-type", + new TripleTO(4, 5, 6) + ) + ) + ) + .setBlocks( + List.of( + new BlockTO("some-block") + ) + ) + .setVehicles( + List.of( + new VehicleTO("some-vehicle"), + new VehicleTO("some-other-vehicle") + ) + ) + .setVisualLayout(new VisualLayoutTO("some-layout")) + .setProperties( + List.of( + new PropertyTO("some-key", "some-value") + ) + ) + ); + + // Assert + ArgumentCaptor captor + = ArgumentCaptor.forClass(PlantModelCreationTO.class); + then(orderService).should().createPlantModel(captor.capture()); + assertThat(captor.getValue().getPoints()).hasSize(2); + assertThat(captor.getValue().getPaths()).hasSize(1); + assertThat(captor.getValue().getLocationTypes()).hasSize(2); + assertThat(captor.getValue().getLocations()).hasSize(2); + assertThat(captor.getValue().getBlocks()).hasSize(1); + assertThat(captor.getValue().getVehicles()).hasSize(2); + assertThat(captor.getValue().getVisualLayout()).isNotNull(); + assertThat(captor.getValue().getProperties()).hasSize(1); + } + + @Test + void getPlantModel() { + // Arrange + PlantModel plantModel + = new PlantModel("some-plant-model") + .withPoints( + Set.of( + new Point("some-point"), + new Point("some-other-point") + ) + ) + .withPaths( + Set.of( + new Path( + "some-path", + new Point("src-point").getReference(), + new Point("dest-point").getReference() + ) + ) + ) + .withLocationTypes( + Set.of( + new LocationType("some-loc-type"), + new LocationType("some-other-loc-type") + ) + ) + .withLocations( + Set.of( + new Location( + "some-location", + new LocationType("loc-type").getReference() + ), + new Location( + "some-other-location", + new LocationType("loc-other-type").getReference() + ) + ) + ) + .withBlocks( + Set.of( + new Block("some-block") + ) + ) + .withVehicles( + Set.of( + new Vehicle("some-vehicle"), + new Vehicle("some-other-vehicle") + ) + ) + .withVisualLayout(new VisualLayout("some-layout")) + .withProperties( + Map.of("some-key", "some-value") + ); + given(orderService.getPlantModel()) + .willReturn(plantModel); + + // Act + PlantModelTO to = handler.getPlantModel(); + + // Assert + then(orderService).should().getPlantModel(); + assertThat(to) + .returns("some-plant-model", PlantModelTO::getName); + assertThat(to.getPoints()).hasSize(2); + assertThat(to.getPaths()).hasSize(1); + assertThat(to.getLocationTypes()).hasSize(2); + assertThat(to.getLocations()).hasSize(2); + assertThat(to.getBlocks()).hasSize(1); + assertThat(to.getVehicles()).hasSize(2); + assertThat(to.getVisualLayout()).isNotNull(); + assertThat(to.getProperties()).hasSize(1); + } + + @Test + void requestTopologyUpdate() { + handler.requestTopologyUpdate( + new PostTopologyUpdateRequestTO(List.of()) + ); + then(routerService).should().updateRoutingTopology(Set.of()); + } + + @Test + void requestTopologyUpdateWithPath() { + Path path1 = new Path( + "path1", + new Point("source").getReference(), + new Point("dest").getReference() + ); + + given(orderService.fetchObject(Path.class, "path1")).willReturn(path1); + + handler.requestTopologyUpdate( + new PostTopologyUpdateRequestTO(List.of("path1")) + ); + then(routerService).should().updateRoutingTopology(Set.of(path1.getReference())); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/StatusEventDispatcherTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/StatusEventDispatcherTest.java new file mode 100644 index 0000000..c31b74f --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/StatusEventDispatcherTest.java @@ -0,0 +1,163 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelStateTransitionEvent; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.data.peripherals.PeripheralOperation; +import org.opentcs.kernel.extensions.servicewebapi.ServiceWebApiConfiguration; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.GetEventsResponseTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.getevents.OrderStatusMessage; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.getevents.PeripheralJobStatusMessage; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.getevents.VehicleStatusMessage; +import org.opentcs.util.event.EventSource; +import org.opentcs.util.event.SimpleEventBus; + +/** + * Unit tests for {@link StatusEventDispatcher}. + */ +class StatusEventDispatcherTest { + + private ServiceWebApiConfiguration configuration; + private EventSource eventSource; + private StatusEventDispatcher statusEventDispatcher; + + @BeforeEach + void setUp() { + configuration = mock(ServiceWebApiConfiguration.class); + eventSource = new SimpleEventBus(); + statusEventDispatcher = new StatusEventDispatcher(configuration, eventSource); + + given(configuration.statusEventsCapacity()) + .willReturn(10); + + statusEventDispatcher.initialize(); + } + + @AfterEach + void tearDown() { + statusEventDispatcher.terminate(); + } + + @Test + void returnEmptyListInitially() { + GetEventsResponseTO result = statusEventDispatcher.fetchEvents(0, Long.MAX_VALUE, 1); + + assertThat(result.getStatusMessages()).isEmpty(); + } + + @Test + void suppressEventCollectionInModellingMode() { + // Arrange + statusEventDispatcher.onEvent( + new KernelStateTransitionEvent(Kernel.State.MODELLING, Kernel.State.OPERATING, true) + ); + statusEventDispatcher.onEvent( + new KernelStateTransitionEvent(Kernel.State.OPERATING, Kernel.State.MODELLING, true) + ); + + TransportOrder order = new TransportOrder("some-order", List.of()); + for (int i = 0; i < 5; i++) { + statusEventDispatcher.onEvent( + new TCSObjectEvent(order, order, TCSObjectEvent.Type.OBJECT_MODIFIED) + ); + } + + // Act + GetEventsResponseTO result = statusEventDispatcher.fetchEvents(0, Long.MAX_VALUE, 1); + + // Assert + assertThat(result.getStatusMessages()).isEmpty(); + } + + @Test + void respectConfiguredCapacity() { + // Arrange + statusEventDispatcher.onEvent( + new KernelStateTransitionEvent(Kernel.State.MODELLING, Kernel.State.OPERATING, true) + ); + + TransportOrder order = new TransportOrder("some-order", List.of()); + for (int i = 0; i < 20; i++) { + statusEventDispatcher.onEvent( + new TCSObjectEvent(order, order, TCSObjectEvent.Type.OBJECT_MODIFIED) + ); + } + + // Act + GetEventsResponseTO result = statusEventDispatcher.fetchEvents(0, Long.MAX_VALUE, 1); + + // Assert + assertThat(result.getStatusMessages()).hasSize(10); + assertThat(result.getStatusMessages().get(9).getSequenceNumber()).isEqualTo(19); + } + + @Test + void processEventsForRelatedObjects() { + // Arrange + statusEventDispatcher.onEvent( + new KernelStateTransitionEvent(Kernel.State.MODELLING, Kernel.State.OPERATING, true) + ); + + TransportOrder order = new TransportOrder("some-order", List.of()); + Vehicle vehicle = new Vehicle("some-vehicle"); + PeripheralJob job = new PeripheralJob( + "some-job", + "some-token", + new PeripheralOperation( + new Location( + "some-location", + new LocationType("some-location-type").getReference() + ).getReference(), + "some-operation", + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + true + ) + ); + Point point = new Point("some-point"); + + statusEventDispatcher.onEvent( + new TCSObjectEvent(order, order, TCSObjectEvent.Type.OBJECT_MODIFIED) + ); + statusEventDispatcher.onEvent( + new TCSObjectEvent(vehicle, vehicle, TCSObjectEvent.Type.OBJECT_MODIFIED) + ); + statusEventDispatcher.onEvent( + new TCSObjectEvent(job, job, TCSObjectEvent.Type.OBJECT_MODIFIED) + ); + // Events for other object types, e.g. points, should be ignored. + statusEventDispatcher.onEvent( + new TCSObjectEvent(point, point, TCSObjectEvent.Type.OBJECT_MODIFIED) + ); + + // Act + GetEventsResponseTO list = statusEventDispatcher.fetchEvents(0, Long.MAX_VALUE, 1); + + // Assert + assertThat(list.getStatusMessages()).hasSize(3); + assertThat(list.getStatusMessages().get(0)) + .isInstanceOf(OrderStatusMessage.class) + .matches(msg -> msg.getSequenceNumber() == 0); + assertThat(list.getStatusMessages().get(1)) + .isInstanceOf(VehicleStatusMessage.class) + .matches(msg -> msg.getSequenceNumber() == 1); + assertThat(list.getStatusMessages().get(2)) + .isInstanceOf(PeripheralJobStatusMessage.class) + .matches(msg -> msg.getSequenceNumber() == 2); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/TransportOrderDispatcherHandlerTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/TransportOrderDispatcherHandlerTest.java new file mode 100644 index 0000000..7e1c698 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/TransportOrderDispatcherHandlerTest.java @@ -0,0 +1,162 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +import java.util.List; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.components.kernel.services.VehicleService; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.kernel.extensions.servicewebapi.KernelExecutorWrapper; + +/** + * Unit tests for {@link TransportOrderDispatcherHandler}. + */ +class TransportOrderDispatcherHandlerTest { + + private VehicleService vehicleService; + private DispatcherService dispatcherService; + private KernelExecutorWrapper executorWrapper; + private TransportOrderDispatcherHandler handler; + + private Vehicle vehicle; + private TransportOrder order; + + @BeforeEach + void setUp() { + vehicleService = mock(); + dispatcherService = mock(); + executorWrapper = new KernelExecutorWrapper(Executors.newSingleThreadExecutor()); + + handler = new TransportOrderDispatcherHandler( + vehicleService, + dispatcherService, + executorWrapper + ); + + vehicle = new Vehicle("some-vehicle"); + order = new TransportOrder("some-order", List.of()) + .withProcessingVehicle(vehicle.getReference()); + + given(vehicleService.fetchObject(Vehicle.class, "some-vehicle")) + .willReturn(vehicle); + given(vehicleService.fetchObject(TransportOrder.class, "some-order")) + .willReturn(order); + } + + @Test + void triggerDispatcher() { + handler.triggerDispatcher(); + + then(dispatcherService).should().dispatch(); + } + + @Test + void tryImmediateAssignmentUsingKnownOrder() { + handler.tryImmediateAssignment("some-order"); + + then(dispatcherService).should().assignNow(order.getReference()); + } + + @Test + void throwOnImmediateAssignmentUsingUnknownOrderName() { + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.tryImmediateAssignment("some-unknown-order")); + } + + @Test + void withdrawByTransportOrderRegularly() { + handler.withdrawByTransportOrder("some-order", false, false); + + then(dispatcherService).should().withdrawByTransportOrder(order.getReference(), false); + } + + @Test + void withdrawByTransportOrderImmediately() { + handler.withdrawByTransportOrder("some-order", true, false); + + then(dispatcherService).should().withdrawByTransportOrder(order.getReference(), true); + } + + @Test + void withdrawByTransportOrderAlsoDisablingVehicle() { + handler.withdrawByTransportOrder("some-order", false, true); + + then(vehicleService) + .should() + .updateVehicleIntegrationLevel( + vehicle.getReference(), + Vehicle.IntegrationLevel.TO_BE_RESPECTED + ); + then(dispatcherService).should().withdrawByTransportOrder(order.getReference(), false); + } + + @Test + void throwOnWithdrawUsingUnknownOrderName() { + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.withdrawByTransportOrder("some-unknown-order", false, false)); + } + + @Test + void withdrawByVehicleRegularly() { + handler.withdrawByVehicle("some-vehicle", false, false); + + then(dispatcherService).should().withdrawByVehicle(vehicle.getReference(), false); + } + + @Test + void withdrawByVehicleImmediately() { + handler.withdrawByVehicle("some-vehicle", true, false); + + then(dispatcherService).should().withdrawByVehicle(vehicle.getReference(), true); + } + + @Test + void withdrawByVehicleAlsoDisablingVehicle() { + handler.withdrawByVehicle("some-vehicle", false, true); + + then(vehicleService) + .should() + .updateVehicleIntegrationLevel( + vehicle.getReference(), + Vehicle.IntegrationLevel.TO_BE_RESPECTED + ); + then(dispatcherService).should().withdrawByVehicle(vehicle.getReference(), false); + } + + @Test + void throwOnWithdrawUsingUnknownVehicleName() { + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.withdrawByVehicle("some-unknown-vehicle", false, false)); + } + + @Test + void rerouteRegularly() { + handler.reroute("some-vehicle", false); + + then(dispatcherService).should().reroute(vehicle.getReference(), ReroutingType.REGULAR); + } + + @Test + void rerouteForcibly() { + handler.reroute("some-vehicle", true); + + then(dispatcherService).should().reroute(vehicle.getReference(), ReroutingType.FORCED); + } + + @Test + void throwOnRerouteUsingUnknownVehicleName() { + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.reroute("some-unknown-vehicle", false)); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/TransportOrderHandlerTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/TransportOrderHandlerTest.java new file mode 100644 index 0000000..85ac67c --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/TransportOrderHandlerTest.java @@ -0,0 +1,341 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.from; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.theInstance; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.opentcs.access.to.order.DestinationCreationTO; +import org.opentcs.access.to.order.OrderSequenceCreationTO; +import org.opentcs.access.to.order.TransportOrderCreationTO; +import org.opentcs.components.kernel.services.TransportOrderService; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.kernel.extensions.servicewebapi.KernelExecutorWrapper; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.GetOrderSequenceResponseTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.GetTransportOrderResponseTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PostOrderSequenceRequestTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PostTransportOrderRequestTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.posttransportorder.Destination; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.Property; + +/** + * Unit tests for {@link TransportOrderHandler}. + */ +class TransportOrderHandlerTest { + + private TransportOrderService orderService; + private KernelExecutorWrapper executorWrapper; + private TransportOrderHandler handler; + + @BeforeEach + void setUp() { + orderService = mock(); + executorWrapper = new KernelExecutorWrapper(Executors.newSingleThreadExecutor()); + + handler = new TransportOrderHandler(orderService, executorWrapper); + } + + @Test + void createTransportOrder() { + // Arrange + TransportOrder transportOrder = new TransportOrder("some-order", List.of()); + given(orderService.createTransportOrder(any(TransportOrderCreationTO.class))) + .willReturn(transportOrder); + + // Act + TransportOrder result = handler.createOrder( + "some-order", + new PostTransportOrderRequestTO( + false, + false, + Instant.MAX, + null, + null, + null, + null, + List.of( + new Destination( + "some-location", + "some-operation", + List.of( + new Property("some-dest-key", "some-dest-value") + ) + ) + ), + List.of( + new Property("some-key", "some-value") + ), + null + ) + ); + + // Assert + assertThat(result, is(theInstance(transportOrder))); + + ArgumentCaptor captor + = ArgumentCaptor.forClass(TransportOrderCreationTO.class); + then(orderService).should().createTransportOrder(captor.capture()); + assertThat(captor.getValue()) + .returns("some-order", from(TransportOrderCreationTO::getName)) + .returns(false, from(TransportOrderCreationTO::hasIncompleteName)) + .returns(false, from(TransportOrderCreationTO::isDispensable)) + .returns(Instant.MAX, from(TransportOrderCreationTO::getDeadline)) + .returns(null, from(TransportOrderCreationTO::getWrappingSequence)) + .returns(null, from(TransportOrderCreationTO::getPeripheralReservationToken)) + .returns(Set.of(), from(TransportOrderCreationTO::getDependencyNames)) + .returns(Map.of("some-key", "some-value"), from(TransportOrderCreationTO::getProperties)); + assertThat(captor.getValue().getDestinations()).hasSize(1); + assertThat(captor.getValue().getDestinations().get(0)) + .returns("some-location", from(DestinationCreationTO::getDestLocationName)) + .returns("some-operation", from(DestinationCreationTO::getDestOperation)) + .returns( + Map.of("some-dest-key", "some-dest-value"), + from(DestinationCreationTO::getProperties) + ); + } + + @Test + void setTransportOrderIntendedVehicle() { + // Arrange + TransportOrder transportOrder = new TransportOrder("some-order", List.of()); + Vehicle vehicle = new Vehicle("some-vehicle"); + + given(orderService.fetchObject(TransportOrder.class, "some-order")) + .willReturn(transportOrder); + given(orderService.fetchObject(Vehicle.class, "some-vehicle")) + .willReturn(vehicle); + + // Act & Assert: set to vehicle + handler.updateTransportOrderIntendedVehicle("some-order", "some-vehicle"); + then(orderService).should().updateTransportOrderIntendedVehicle( + transportOrder.getReference(), + vehicle.getReference() + ); + + // Act & Assert: set to null + handler.updateTransportOrderIntendedVehicle("some-order", null); + then(orderService).should().updateTransportOrderIntendedVehicle( + transportOrder.getReference(), + null + ); + + // Act & Assert: nonexistent transport order + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.updateTransportOrderIntendedVehicle("some-unknown-order", null)); + + // Act & Assert: nonexistent vehicle + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy( + () -> handler.updateTransportOrderIntendedVehicle("some-order", "some-unknown-vehicle") + ); + } + + @Test + void retrieveTransportOrdersUnfiltered() { + // Arrange + TransportOrder transportOrder1 = new TransportOrder("some-order", List.of()); + TransportOrder transportOrder2 = new TransportOrder("some-order-2", List.of()); + + given( + orderService.fetchObjects(ArgumentMatchers.>any(), any()) + ) + .willReturn(Set.of(transportOrder1, transportOrder2)); + + // Act + List result = handler.getTransportOrders(null); + + // Assert + assertThat(result, hasSize(2)); + then(orderService).should().fetchObjects(ArgumentMatchers.>any(), any()); + } + + @Test + void retrieveTransportOrdersFilteredByIntendedVehicle() { + // Arrange + TransportOrder transportOrder1 = new TransportOrder("some-order", List.of()); + TransportOrder transportOrder2 = new TransportOrder("some-order-2", List.of()); + Vehicle vehicle = new Vehicle("some-vehicle"); + + given( + orderService.fetchObject(Vehicle.class, "some-vehicle") + ) + .willReturn(vehicle); + given( + orderService.fetchObjects(ArgumentMatchers.>any(), any()) + ) + .willReturn(Set.of(transportOrder1, transportOrder2)); + + // Act & Assert: happy path + List result = handler.getTransportOrders("some-vehicle"); + assertThat(result, hasSize(2)); + then(orderService).should().fetchObjects(ArgumentMatchers.>any(), any()); + + // Act & Assert: nonexistent vehicle + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.getTransportOrders("some-other-vehicle")); + } + + @Test + void retrieveTransportOrderByName() { + // Arrange + TransportOrder transportOrder = new TransportOrder("some-order", List.of()); + + given( + orderService.fetchObject(TransportOrder.class, "some-order") + ) + .willReturn(transportOrder); + + // Act & Assert: happy path + GetTransportOrderResponseTO result = handler.getTransportOrderByName("some-order"); + assertThat(result, is(notNullValue())); + then(orderService).should().fetchObject(TransportOrder.class, "some-order"); + + // Act & Assert: nonexistent order + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.getTransportOrderByName("some-other-order")); + } + + @Test + void createOrderSequence() { + // Arrange + OrderSequence orderSequence = new OrderSequence("some-sequence"); + given(orderService.createOrderSequence(any(OrderSequenceCreationTO.class))) + .willReturn(orderSequence); + + // Act + OrderSequence result = handler.createOrderSequence( + "some-sequence", + new PostOrderSequenceRequestTO() + .setIncompleteName(false) + .setFailureFatal(false) + .setIntendedVehicle(null) + .setType("some-type") + .setProperties( + List.of( + new Property("some-key", "some-value") + ) + ) + ); + + // Assert + assertThat(result, is(theInstance(orderSequence))); + + ArgumentCaptor captor + = ArgumentCaptor.forClass(OrderSequenceCreationTO.class); + then(orderService).should().createOrderSequence(captor.capture()); + assertThat(captor.getValue()) + .returns(false, from(OrderSequenceCreationTO::hasIncompleteName)) + .returns(null, from(OrderSequenceCreationTO::getIntendedVehicleName)) + .returns(false, from(OrderSequenceCreationTO::isFailureFatal)) + .returns("some-type", from(OrderSequenceCreationTO::getType)) + .returns( + Map.of("some-key", "some-value"), + from(OrderSequenceCreationTO::getProperties) + ); + } + + @Test + void setOrderSequenceComplete() { + // Arrange + OrderSequence orderSequence = new OrderSequence("some-sequence"); + + given(orderService.fetchObject(OrderSequence.class, "some-sequence")) + .willReturn(orderSequence); + + // Act & Assert: happy path + handler.putOrderSequenceComplete("some-sequence"); + then(orderService).should().markOrderSequenceComplete(orderSequence.getReference()); + + // Act & Assert: nonexistent order sequence + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.putOrderSequenceComplete("some-other-sequence")); + } + + @Test + void retrieveOrderSequencesUnfiltered() { + // Arrange + OrderSequence sequence1 = new OrderSequence("some-sequence"); + OrderSequence sequence2 = new OrderSequence("some-sequence-2"); + + given( + orderService.fetchObjects(ArgumentMatchers.>any(), any()) + ) + .willReturn(Set.of(sequence1, sequence2)); + + // Act + List result = handler.getOrderSequences(null); + + // Assert + assertThat(result, hasSize(2)); + then(orderService).should().fetchObjects(ArgumentMatchers.>any(), any()); + } + + @Test + void retrieveOrderSequencesFilteredByIntendedVehicle() { + // Arrange + OrderSequence sequence1 = new OrderSequence("some-sequence"); + OrderSequence sequence2 = new OrderSequence("some-sequence-2"); + Vehicle vehicle = new Vehicle("some-vehicle"); + + given( + orderService.fetchObject(Vehicle.class, "some-vehicle") + ) + .willReturn(vehicle); + given( + orderService.fetchObjects(ArgumentMatchers.>any(), any()) + ) + .willReturn(Set.of(sequence1, sequence2)); + + // Act & Assert: happy path + List result = handler.getOrderSequences("some-vehicle"); + assertThat(result, hasSize(2)); + then(orderService).should().fetchObjects(ArgumentMatchers.>any(), any()); + + // Act & Assert: nonexistent vehicle + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.getOrderSequences("some-other-vehicle")); + } + + @Test + void retrieveOrderSequenceByName() { + // Arrange + OrderSequence orderSequence = new OrderSequence("some-sequence"); + + given( + orderService.fetchObject(OrderSequence.class, "some-sequence") + ) + .willReturn(orderSequence); + + // Act & Assert: happy path + GetOrderSequenceResponseTO result = handler.getOrderSequenceByName("some-sequence"); + assertThat(result, is(notNullValue())); + then(orderService).should().fetchObject(OrderSequence.class, "some-sequence"); + + // Act & Assert: nonexistent order + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.getOrderSequenceByName("some-other-sequence")); + } + +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/VehicleHandlerTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/VehicleHandlerTest.java new file mode 100644 index 0000000..bade49c --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/VehicleHandlerTest.java @@ -0,0 +1,470 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executors; +import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.opentcs.components.kernel.services.RouterService; +import org.opentcs.components.kernel.services.VehicleService; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; +import org.opentcs.drivers.vehicle.management.VehicleAttachmentInformation; +import org.opentcs.kernel.extensions.servicewebapi.KernelExecutorWrapper; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.GetVehicleResponseTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PostVehicleRoutesRequestTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.PutVehicleAllowedOrderTypesTO; + +/** + * Unit tests for {@link VehicleHandler}. + */ +class VehicleHandlerTest { + + private VehicleService vehicleService; + private RouterService routerService; + private KernelExecutorWrapper executorWrapper; + + private VehicleHandler handler; + + private Vehicle vehicle; + private VehicleCommAdapterDescription adapterDescriptionMock; + private VehicleAttachmentInformation attachmentInfo; + + @BeforeEach + void setUp() { + vehicleService = mock(); + routerService = mock(); + executorWrapper = new KernelExecutorWrapper(Executors.newSingleThreadExecutor()); + + handler = new VehicleHandler(vehicleService, routerService, executorWrapper); + + vehicle = new Vehicle("some-vehicle"); + adapterDescriptionMock = new MockVehicleCommAdapterDescription(); + + attachmentInfo = new VehicleAttachmentInformation( + vehicle.getReference(), + List.of(adapterDescriptionMock), + adapterDescriptionMock + ); + + given(vehicleService.fetchObject(Vehicle.class, "some-vehicle")) + .willReturn(vehicle); + given(vehicleService.fetchAttachmentInformation(vehicle.getReference())) + .willReturn(attachmentInfo); + } + + @Test + void attachMockVehicleAdapter() { + // Act + handler.putVehicleCommAdapter( + "some-vehicle", + MockVehicleCommAdapterDescription.class.getName() + ); + + // Assert + then(vehicleService) + .should() + .attachCommAdapter(vehicle.getReference(), adapterDescriptionMock); + } + + @Test + void throwOnAttachAdapterForUnknownVehicle() { + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy( + () -> handler.putVehicleCommAdapter( + "some-unknown-vehicle", + MockVehicleCommAdapterDescription.class.getName() + ) + ); + } + + @Test + void throwOnAttachUnknownAdapter() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy( + () -> handler.putVehicleCommAdapter( + "some-vehicle", + "some-unknown-adapter-class-name" + ) + ); + } + + @Test + void enableCommAdapter() { + handler.putVehicleCommAdapterEnabled("some-vehicle", "true"); + + then(vehicleService).should().enableCommAdapter(vehicle.getReference()); + } + + @ParameterizedTest + @ValueSource(strings = {"false", "flase", "some-value-that-is-not-true"}) + void disableCommAdapterOnAnyNontrueValue(String value) { + handler.putVehicleCommAdapterEnabled("some-vehicle", value); + + then(vehicleService).should().disableCommAdapter(vehicle.getReference()); + } + + @ParameterizedTest + @ValueSource(strings = {"true ", "false"}) + void throwOnEnableUnknownVehicle(String value) { + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.putVehicleCommAdapterEnabled("some-unknown-vehicle", value)); + } + + @Test + void fetchAttachmentInformation() { + assertThat(handler.getVehicleCommAdapterAttachmentInformation("some-vehicle")) + .isSameAs(attachmentInfo); + } + + @Test + void throwOnFetchInfoForUnknownLocation() { + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy( + () -> handler.getVehicleCommAdapterAttachmentInformation("some-unknown-vehicle") + ); + } + + @ParameterizedTest + @EnumSource(Vehicle.ProcState.class) + void retrieveVehiclesByProcState(Vehicle.ProcState procState) { + // Arrange + Vehicle vehicleWithProcState = vehicle.withProcState(procState); + given(vehicleService.fetchObjects(ArgumentMatchers.>any(), any())) + .willReturn(Set.of(vehicleWithProcState)); + + // Act & Assert + List result = handler.getVehiclesState(procState.name()); + MatcherAssert.assertThat(result, hasSize(1)); + then(vehicleService).should().fetchObjects(ArgumentMatchers.>any(), any()); + } + + @Test + void throwOnRetrieveVehiclesForUnknownProcState() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> handler.getVehiclesState("some-unknown-proc-state")); + } + + @Test + void retrieveVehicleByName() { + // Act & Assert: happy path + GetVehicleResponseTO result = handler.getVehicleStateByName("some-vehicle"); + MatcherAssert.assertThat(result, is(notNullValue())); + then(vehicleService).should().fetchObject(Vehicle.class, "some-vehicle"); + + // Act & Assert: nonexistent vehicle + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.getVehicleStateByName("some-other-vehicle")); + } + + @ParameterizedTest + @EnumSource(Vehicle.IntegrationLevel.class) + void updateVehicleIntegrationLevel(Vehicle.IntegrationLevel integrationLevel) { + handler.putVehicleIntegrationLevel("some-vehicle", integrationLevel.name()); + then(vehicleService) + .should() + .updateVehicleIntegrationLevel(vehicle.getReference(), integrationLevel); + } + + @Test + void throwOnUpdateIntegrationLevelForUnknownVehicleOrIntegrationLevel() { + // Act & Assert: nonexistent vehicle + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy( + () -> handler.putVehicleIntegrationLevel( + "some-unknown-vehicle", + "TO_BE_UTILIZED" + ) + ); + + // Act & Assert: unknown integration level + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy( + () -> handler.putVehicleIntegrationLevel( + "some-vehicle", + "some-unknown-integration-level" + ) + ); + } + + @Test + void pauseVehicle() { + handler.putVehiclePaused("some-vehicle", "true"); + + then(vehicleService).should().updateVehiclePaused(vehicle.getReference(), true); + } + + @ParameterizedTest + @ValueSource(strings = {"false", "flase", "some-value-that-is-not-true"}) + void unpauseVehicleOnAnyNontrueValue(String value) { + handler.putVehiclePaused("some-vehicle", value); + + then(vehicleService).should().updateVehiclePaused(vehicle.getReference(), false); + } + + @ParameterizedTest + @ValueSource(strings = {"true ", "false"}) + void throwOnPauseUnknownVehicle(String value) { + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.putVehiclePaused("some-unknown-vehicle", value)); + } + + @Test + void setVehicleEnvelopeKey() { + handler.putVehicleEnvelopeKey("some-vehicle", "some-key"); + + then(vehicleService).should().updateVehicleEnvelopeKey(vehicle.getReference(), "some-key"); + } + + @Test + void nullVehicleEnvelopeKey() { + handler.putVehicleEnvelopeKey("some-vehicle", null); + + then(vehicleService).should().updateVehicleEnvelopeKey(vehicle.getReference(), null); + } + + @Test + void throwOnSetEnvelopeUnknownVehicle() { + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy(() -> handler.putVehicleEnvelopeKey("some-unknown-vehicle", "some-key")); + } + + @Test + void updateVehicleAllowedOrderTypes() { + // Act + handler.putVehicleAllowedOrderTypes( + "some-vehicle", + new PutVehicleAllowedOrderTypesTO(List.of("some-order-type", "some-other-order-type")) + ); + + // Assert + @SuppressWarnings("unchecked") + ArgumentCaptor> captor = ArgumentCaptor.forClass(Set.class); + then(vehicleService) + .should() + .updateVehicleAllowedOrderTypes(eq(vehicle.getReference()), captor.capture()); + assertThat(captor.getValue()) + .hasSize(2) + .contains("some-order-type", "some-other-order-type"); + } + + @Test + void throwOnUpdateAllowedOrderTypesForUnknownVehicle() { + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy( + () -> handler.putVehicleAllowedOrderTypes( + "some-unknown-vehicle", + new PutVehicleAllowedOrderTypesTO(List.of()) + ) + ); + } + + @Test + void retrieveVehicleRoutesForCurrentPosition() { + // Arrange + Point vehiclePosition = new Point("some-point"); + Point destinationPoint1 = new Point("some-destination-point"); + Point destinationPoint2 = new Point("some-destination-point-2"); + Vehicle vehicleWithPosition = vehicle.withCurrentPosition(vehiclePosition.getReference()); + given(vehicleService.fetchObject(Point.class, "some-point")) + .willReturn(vehiclePosition); + given(vehicleService.fetchObject(Point.class, "some-destination-point")) + .willReturn(destinationPoint1); + given(vehicleService.fetchObject(Point.class, "some-destination-point-2")) + .willReturn(destinationPoint2); + given(vehicleService.fetchObject(Vehicle.class, "some-vehicle")) + .willReturn(vehicleWithPosition); + + // Act & Assert: happy path + handler.getVehicleRoutes( + "some-vehicle", + new PostVehicleRoutesRequestTO( + List.of("some-destination-point", "some-destination-point-2") + ) + ); + + then(routerService) + .should() + .computeRoutes( + vehicle.getReference(), + vehiclePosition.getReference(), + Set.of(destinationPoint1.getReference(), destinationPoint2.getReference()), + Set.of() + ); + + // Act & Assert: nonexistent vehicle + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy( + () -> handler.getVehicleRoutes( + "some-unknown-vehicle", + new PostVehicleRoutesRequestTO(List.of("some-destination-point")) + ) + ); + + // Act & Assert: nonexistent destination point + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy( + () -> handler.getVehicleRoutes( + "some-vehicle", + new PostVehicleRoutesRequestTO(List.of("some-unknown-destination-point")) + ) + ); + + // Act & Assert: unknown vehicle position + given(vehicleService.fetchObject(Vehicle.class, "some-vehicle")) + .willReturn(vehicle); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy( + () -> handler.getVehicleRoutes( + "some-vehicle", + new PostVehicleRoutesRequestTO(List.of("some-destination-point")) + ) + ); + } + + @Test + void retrieveVehicleRoutesForPositionProvidedInRequest() { + // Arrange + Point sourcePoint = new Point("some-source-point"); + Point destinationPoint1 = new Point("some-destination-point"); + Point destinationPoint2 = new Point("some-destination-point-2"); + given(vehicleService.fetchObject(Point.class, "some-source-point")) + .willReturn(sourcePoint); + given(vehicleService.fetchObject(Point.class, "some-destination-point")) + .willReturn(destinationPoint1); + given(vehicleService.fetchObject(Point.class, "some-destination-point-2")) + .willReturn(destinationPoint2); + + // Act & Assert: happy path + handler.getVehicleRoutes( + "some-vehicle", + new PostVehicleRoutesRequestTO( + List.of("some-destination-point", "some-destination-point-2") + ).setSourcePoint("some-source-point") + ); + + then(routerService) + .should() + .computeRoutes( + vehicle.getReference(), + sourcePoint.getReference(), + Set.of(destinationPoint1.getReference(), destinationPoint2.getReference()), + Set.of() + ); + + // Act & Assert: nonexistent source point + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy( + () -> handler.getVehicleRoutes( + "some-vehicle", + new PostVehicleRoutesRequestTO(List.of("some-destination-point")) + .setSourcePoint("some-unknown-source-point") + ) + ); + } + + @Test + void retrieveVehicleRoutesForResourcesToAvoid() { + // Arrange + Point sourcePoint = new Point("some-source-point"); + Point destinationPoint1 = new Point("some-destination-point"); + Point destinationPoint2 = new Point("some-destination-point-2"); + Point pointToAvoid = new Point("some-point-to-avoid"); + Path pathToAvoid = new Path( + "some-path", + sourcePoint.getReference(), + destinationPoint1.getReference() + ); + Location locationToAvoid = new Location( + "some-location", + new LocationType("some-locType").getReference() + ); + given(vehicleService.fetchObject(Point.class, "some-source-point")) + .willReturn(sourcePoint); + given(vehicleService.fetchObject(Point.class, "some-destination-point")) + .willReturn(destinationPoint1); + given(vehicleService.fetchObject(Point.class, "some-destination-point-2")) + .willReturn(destinationPoint2); + given(vehicleService.fetchObject(Point.class, "some-point-to-avoid")) + .willReturn(pointToAvoid); + given(vehicleService.fetchObject(Path.class, "some-path")) + .willReturn(pathToAvoid); + given(vehicleService.fetchObject(Location.class, "some-location")) + .willReturn(locationToAvoid); + + // Act & Assert: happy path + handler.getVehicleRoutes( + "some-vehicle", + new PostVehicleRoutesRequestTO( + List.of("some-destination-point", "some-destination-point-2") + ) + .setSourcePoint("some-source-point") + .setResourcesToAvoid( + List.of("some-point-to-avoid", "some-path", "some-location") + ) + ); + + then(routerService) + .should() + .computeRoutes( + vehicle.getReference(), + sourcePoint.getReference(), + Set.of(destinationPoint1.getReference(), destinationPoint2.getReference()), + Set.of( + pointToAvoid.getReference(), + pathToAvoid.getReference(), + locationToAvoid.getReference() + ) + ); + + // Act & Assert: nonexistent resource to avoid + assertThatExceptionOfType(ObjectUnknownException.class) + .isThrownBy( + () -> handler.getVehicleRoutes( + "some-vehicle", + new PostVehicleRoutesRequestTO(List.of("some-destination-point")) + .setSourcePoint("some-source-point") + .setResourcesToAvoid(List.of("some-unknown-resource")) + ) + ); + } + + static class MockVehicleCommAdapterDescription + extends + VehicleCommAdapterDescription { + + @Override + public String getDescription() { + return "some-vehicle-comm-adapter"; + } + + @Override + public boolean isSimVehicleCommAdapter() { + return false; + } + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetEventsResponseTOTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetEventsResponseTOTest.java new file mode 100644 index 0000000..b494998 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetEventsResponseTOTest.java @@ -0,0 +1,133 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import java.time.Instant; +import java.util.List; +import org.approvaltests.Approvals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.data.peripherals.PeripheralOperation; +import org.opentcs.kernel.extensions.servicewebapi.JsonBinder; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.getevents.OrderStatusMessage; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.getevents.PeripheralJobStatusMessage; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.getevents.VehicleStatusMessage; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.DestinationState; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PeripheralOperationDescription; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.Property; + +/** + * Unit tests for {@link GetEventsResponseTO}. + */ +class GetEventsResponseTOTest { + + private JsonBinder jsonBinder; + + @BeforeEach + void setUp() { + jsonBinder = new JsonBinder(); + } + + @ParameterizedTest + @ValueSource(doubles = {Double.NaN, 90.0}) + void jsonSample(double orientationAngle) { + GetEventsResponseTO to + = new GetEventsResponseTO() + .setTimeStamp(Instant.EPOCH) + .setStatusMessages( + List.of( + createVehicleStatusMessage(0).setOrientationAngle(orientationAngle), + createOrderStatusMessage(1), + createPeripheralJobStatusMessage(2) + ) + ); + + Approvals.verify( + jsonBinder.toJson(to), + Approvals.NAMES.withParameters("orientationAngle-" + orientationAngle) + ); + } + + private VehicleStatusMessage createVehicleStatusMessage(long sequenceNo) { + return new VehicleStatusMessage() + .setSequenceNumber(sequenceNo) + .setCreationTimeStamp(Instant.EPOCH) + .setVehicleName("some-vehicle") + .setTransportOrderName("some-transport-order") + .setPosition("some-point") + .setPrecisePosition(new VehicleStatusMessage.PrecisePosition(1, 2, 3)) + .setPaused(false) + .setState(Vehicle.State.IDLE) + .setProcState(Vehicle.ProcState.IDLE) + .setAllocatedResources( + List.of( + List.of("some-path", "some-point"), + List.of("some-other-path", "some-other-point") + ) + ) + .setClaimedResources( + List.of( + List.of("some-path", "some-point"), + List.of("some-other-path", "some-other-point") + ) + ); + } + + private OrderStatusMessage createOrderStatusMessage(long sequenceNo) { + return new OrderStatusMessage() + .setSequenceNumber(sequenceNo) + .setCreationTimeStamp(Instant.EPOCH) + .setOrderName("some-order") + .setProcessingVehicleName("some-vehicle") + .setOrderState(OrderStatusMessage.OrderState.BEING_PROCESSED) + .setDestinations( + List.of( + new DestinationState() + .setLocationName("some-location") + .setOperation("some-operation") + .setState(DestinationState.State.TRAVELLING) + .setProperties( + List.of( + new Property("some-key", "some-value"), + new Property("some-other-key", "some-other-value") + ) + ) + ) + ) + .setProperties( + List.of( + new Property("some-key", "some-value"), + new Property("some-other-key", "some-other-value") + ) + ); + } + + private PeripheralJobStatusMessage createPeripheralJobStatusMessage(long sequenceNo) { + return new PeripheralJobStatusMessage() + .setSequenceNumber(sequenceNo) + .setCreationTimeStamp(Instant.EPOCH) + .setName("some-peripheral-job") + .setReservationToken("some-token") + .setRelatedVehicle("some-vehicle") + .setRelatedTransportOrder("some-order") + .setPeripheralOperation( + new PeripheralOperationDescription() + .setOperation("some-operation") + .setLocationName("some-location") + .setExecutionTrigger(PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION) + .setCompletionRequired(true) + ) + .setState(PeripheralJob.State.BEING_PROCESSED) + .setCreationTime(Instant.EPOCH) + .setFinishedTime(Instant.MAX) + .setProperties( + List.of( + new Property("some-key", "some-value"), + new Property("some-other-key", "some-other-value") + ) + ); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetEventsResponseTOTest.jsonSample.orientationAngle-90.0.approved.txt b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetEventsResponseTOTest.jsonSample.orientationAngle-90.0.approved.txt new file mode 100644 index 0000000..4def57a --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetEventsResponseTOTest.jsonSample.orientationAngle-90.0.approved.txt @@ -0,0 +1,72 @@ +{ + "timeStamp" : "1970-01-01T00:00:00Z", + "statusMessages" : [ { + "type" : "Vehicle", + "sequenceNumber" : 0, + "creationTimeStamp" : "1970-01-01T00:00:00Z", + "vehicleName" : "some-vehicle", + "transportOrderName" : "some-transport-order", + "position" : "some-point", + "precisePosition" : { + "x" : 1, + "y" : 2, + "z" : 3 + }, + "orientationAngle" : 90.0, + "paused" : false, + "state" : "IDLE", + "procState" : "IDLE", + "allocatedResources" : [ [ "some-path", "some-point" ], [ "some-other-path", "some-other-point" ] ], + "claimedResources" : [ [ "some-path", "some-point" ], [ "some-other-path", "some-other-point" ] ] + }, { + "type" : "TransportOrder", + "sequenceNumber" : 1, + "creationTimeStamp" : "1970-01-01T00:00:00Z", + "orderName" : "some-order", + "processingVehicleName" : "some-vehicle", + "orderState" : "BEING_PROCESSED", + "destinations" : [ { + "locationName" : "some-location", + "operation" : "some-operation", + "state" : "TRAVELLING", + "properties" : [ { + "key" : "some-key", + "value" : "some-value" + }, { + "key" : "some-other-key", + "value" : "some-other-value" + } ] + } ], + "properties" : [ { + "key" : "some-key", + "value" : "some-value" + }, { + "key" : "some-other-key", + "value" : "some-other-value" + } ] + }, { + "type" : "PeripheralJob", + "sequenceNumber" : 2, + "creationTimeStamp" : "1970-01-01T00:00:00Z", + "name" : "some-peripheral-job", + "reservationToken" : "some-token", + "relatedVehicle" : "some-vehicle", + "relatedTransportOrder" : "some-order", + "peripheralOperation" : { + "operation" : "some-operation", + "locationName" : "some-location", + "executionTrigger" : "AFTER_ALLOCATION", + "completionRequired" : true + }, + "state" : "BEING_PROCESSED", + "creationTime" : "1970-01-01T00:00:00Z", + "finishedTime" : "+1000000000-12-31T23:59:59.999999999Z", + "properties" : [ { + "key" : "some-key", + "value" : "some-value" + }, { + "key" : "some-other-key", + "value" : "some-other-value" + } ] + } ] +} \ No newline at end of file diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetEventsResponseTOTest.jsonSample.orientationAngle-NaN.approved.txt b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetEventsResponseTOTest.jsonSample.orientationAngle-NaN.approved.txt new file mode 100644 index 0000000..0bed5a2 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetEventsResponseTOTest.jsonSample.orientationAngle-NaN.approved.txt @@ -0,0 +1,72 @@ +{ + "timeStamp" : "1970-01-01T00:00:00Z", + "statusMessages" : [ { + "type" : "Vehicle", + "sequenceNumber" : 0, + "creationTimeStamp" : "1970-01-01T00:00:00Z", + "vehicleName" : "some-vehicle", + "transportOrderName" : "some-transport-order", + "position" : "some-point", + "precisePosition" : { + "x" : 1, + "y" : 2, + "z" : 3 + }, + "orientationAngle" : "NaN", + "paused" : false, + "state" : "IDLE", + "procState" : "IDLE", + "allocatedResources" : [ [ "some-path", "some-point" ], [ "some-other-path", "some-other-point" ] ], + "claimedResources" : [ [ "some-path", "some-point" ], [ "some-other-path", "some-other-point" ] ] + }, { + "type" : "TransportOrder", + "sequenceNumber" : 1, + "creationTimeStamp" : "1970-01-01T00:00:00Z", + "orderName" : "some-order", + "processingVehicleName" : "some-vehicle", + "orderState" : "BEING_PROCESSED", + "destinations" : [ { + "locationName" : "some-location", + "operation" : "some-operation", + "state" : "TRAVELLING", + "properties" : [ { + "key" : "some-key", + "value" : "some-value" + }, { + "key" : "some-other-key", + "value" : "some-other-value" + } ] + } ], + "properties" : [ { + "key" : "some-key", + "value" : "some-value" + }, { + "key" : "some-other-key", + "value" : "some-other-value" + } ] + }, { + "type" : "PeripheralJob", + "sequenceNumber" : 2, + "creationTimeStamp" : "1970-01-01T00:00:00Z", + "name" : "some-peripheral-job", + "reservationToken" : "some-token", + "relatedVehicle" : "some-vehicle", + "relatedTransportOrder" : "some-order", + "peripheralOperation" : { + "operation" : "some-operation", + "locationName" : "some-location", + "executionTrigger" : "AFTER_ALLOCATION", + "completionRequired" : true + }, + "state" : "BEING_PROCESSED", + "creationTime" : "1970-01-01T00:00:00Z", + "finishedTime" : "+1000000000-12-31T23:59:59.999999999Z", + "properties" : [ { + "key" : "some-key", + "value" : "some-value" + }, { + "key" : "some-other-key", + "value" : "some-other-value" + } ] + } ] +} \ No newline at end of file diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetOrderSequenceResponseTOTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetOrderSequenceResponseTOTest.java new file mode 100644 index 0000000..f35ab03 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetOrderSequenceResponseTOTest.java @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import java.util.List; +import org.approvaltests.Approvals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.kernel.extensions.servicewebapi.JsonBinder; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.Property; + +/** + */ +class GetOrderSequenceResponseTOTest { + + private JsonBinder jsonBinder; + + @BeforeEach + void setUp() { + jsonBinder = new JsonBinder(); + } + + @Test + void jsonSample() { + GetOrderSequenceResponseTO to = new GetOrderSequenceResponseTO("some-order-sequence") + .setType("Charge") + .setOrders(List.of("some-order", "another-order", "order-3")) + .setFinishedIndex(3) + .setComplete(false) + .setFinished(false) + .setFailureFatal(true) + .setIntendedVehicle("some-vehicle") + .setProcessingVehicle(null) + .setProperties( + List.of( + new Property("some-key", "some-value"), + new Property("another-key", "another-value") + ) + ); + + Approvals.verify(jsonBinder.toJson(to)); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetOrderSequenceResponseTOTest.jsonSample.approved.txt b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetOrderSequenceResponseTOTest.jsonSample.approved.txt new file mode 100644 index 0000000..a0bdf4e --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetOrderSequenceResponseTOTest.jsonSample.approved.txt @@ -0,0 +1,18 @@ +{ + "name" : "some-order-sequence", + "type" : "Charge", + "orders" : [ "some-order", "another-order", "order-3" ], + "finishedIndex" : 3, + "complete" : false, + "finished" : false, + "failureFatal" : true, + "intendedVehicle" : "some-vehicle", + "processingVehicle" : null, + "properties" : [ { + "key" : "some-key", + "value" : "some-value" + }, { + "key" : "another-key", + "value" : "another-value" + } ] +} \ No newline at end of file diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetPeripheralAttachmentInfoResponseTOTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetPeripheralAttachmentInfoResponseTOTest.java new file mode 100644 index 0000000..1dbd645 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetPeripheralAttachmentInfoResponseTOTest.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import java.util.List; +import org.approvaltests.Approvals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.kernel.extensions.servicewebapi.JsonBinder; + +/** + * Unit tests for {@link GetVehicleAttachmentInfoResponseTO}. + */ +class GetPeripheralAttachmentInfoResponseTOTest { + + private JsonBinder jsonBinder; + + @BeforeEach + void setUp() { + jsonBinder = new JsonBinder(); + } + + @Test + void jsonSample() { + GetPeripheralAttachmentInfoResponseTO to + = new GetPeripheralAttachmentInfoResponseTO( + "Location001", + "com.example.someperipheraldriver.descriptionclass", + List.of( + "com.example.someperipheraldriver.descriptionclass", + "com.example.someotherperipheraldriver.descriptionclass2" + ) + ); + + Approvals.verify(jsonBinder.toJson(to)); + } + +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetPeripheralAttachmentInfoResponseTOTest.jsonSample.approved.txt b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetPeripheralAttachmentInfoResponseTOTest.jsonSample.approved.txt new file mode 100644 index 0000000..eb3ee82 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetPeripheralAttachmentInfoResponseTOTest.jsonSample.approved.txt @@ -0,0 +1,5 @@ +{ + "locationName" : "Location001", + "availableCommAdapters" : [ "com.example.someperipheraldriver.descriptionclass", "com.example.someotherperipheraldriver.descriptionclass2" ], + "attachedCommAdapter" : "com.example.someperipheraldriver.descriptionclass" +} \ No newline at end of file diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetPeripheralJobResponseTOTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetPeripheralJobResponseTOTest.java new file mode 100644 index 0000000..ff49133 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetPeripheralJobResponseTOTest.java @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import java.time.Instant; +import java.util.List; +import org.approvaltests.Approvals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.data.peripherals.PeripheralOperation; +import org.opentcs.kernel.extensions.servicewebapi.JsonBinder; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PeripheralOperationDescription; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.Property; + +/** + * Unit tests for {@link GetPeripheralJobResponseTO}. + */ +class GetPeripheralJobResponseTOTest { + + private JsonBinder jsonBinder; + + @BeforeEach + void setUp() { + jsonBinder = new JsonBinder(); + } + + @Test + void jsonSample() { + GetPeripheralJobResponseTO to + = new GetPeripheralJobResponseTO() + .setName("some-peripheral-job") + .setReservationToken("some-token") + .setRelatedVehicle("some-vehicle") + .setRelatedTransportOrder("some-order") + .setPeripheralOperation( + new PeripheralOperationDescription() + .setOperation("some-operation") + .setLocationName("some-location") + .setExecutionTrigger(PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION) + .setCompletionRequired(true) + ) + .setState(PeripheralJob.State.BEING_PROCESSED) + .setCreationTime(Instant.EPOCH) + .setFinishedTime(Instant.MAX) + .setProperties( + List.of( + new Property("some-key", "some-value"), + new Property("some-other-key", "some-other-value") + ) + ); + + Approvals.verify(jsonBinder.toJson(to)); + } + +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetPeripheralJobResponseTOTest.jsonSample.approved.txt b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetPeripheralJobResponseTOTest.jsonSample.approved.txt new file mode 100644 index 0000000..6ebe7df --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetPeripheralJobResponseTOTest.jsonSample.approved.txt @@ -0,0 +1,22 @@ +{ + "name" : "some-peripheral-job", + "reservationToken" : "some-token", + "relatedVehicle" : "some-vehicle", + "relatedTransportOrder" : "some-order", + "peripheralOperation" : { + "operation" : "some-operation", + "locationName" : "some-location", + "executionTrigger" : "AFTER_ALLOCATION", + "completionRequired" : true + }, + "state" : "BEING_PROCESSED", + "creationTime" : "1970-01-01T00:00:00Z", + "finishedTime" : "+1000000000-12-31T23:59:59.999999999Z", + "properties" : [ { + "key" : "some-key", + "value" : "some-value" + }, { + "key" : "some-other-key", + "value" : "some-other-value" + } ] +} \ No newline at end of file diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetTransportOrderResponseTOTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetTransportOrderResponseTOTest.java new file mode 100644 index 0000000..9fbdbf5 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetTransportOrderResponseTOTest.java @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import java.util.List; +import org.approvaltests.Approvals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.kernel.extensions.servicewebapi.JsonBinder; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.DestinationState; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.Property; + +/** + * Unit tests for {@link GetTransportOrderResponseTO}. + */ +class GetTransportOrderResponseTOTest { + + private JsonBinder jsonBinder; + + @BeforeEach + void setUp() { + jsonBinder = new JsonBinder(); + } + + @Test + void jsonSample() { + GetTransportOrderResponseTO to + = new GetTransportOrderResponseTO() + .setDispensable(true) + .setName("some-order") + .setPeripheralReservationToken("some-token") + .setWrappingSequence("some-sequence") + .setType("some-type") + .setState(TransportOrder.State.BEING_PROCESSED) + .setIntendedVehicle("some-vehicle") + .setProcessingVehicle("some-vehicle") + .setDestinations( + List.of( + new DestinationState() + .setLocationName("some-location") + .setOperation("some-operation") + .setState(DestinationState.State.TRAVELLING) + .setProperties( + List.of( + new Property("some-key", "some-value"), + new Property("some-other-key", "some-other-value") + ) + ) + ) + ); + + Approvals.verify(jsonBinder.toJson(to)); + } + +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetTransportOrderResponseTOTest.jsonSample.approved.txt b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetTransportOrderResponseTOTest.jsonSample.approved.txt new file mode 100644 index 0000000..a0b386d --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetTransportOrderResponseTOTest.jsonSample.approved.txt @@ -0,0 +1,22 @@ +{ + "dispensable" : true, + "name" : "some-order", + "peripheralReservationToken" : "some-token", + "wrappingSequence" : "some-sequence", + "type" : "some-type", + "state" : "BEING_PROCESSED", + "intendedVehicle" : "some-vehicle", + "processingVehicle" : "some-vehicle", + "destinations" : [ { + "locationName" : "some-location", + "operation" : "some-operation", + "state" : "TRAVELLING", + "properties" : [ { + "key" : "some-key", + "value" : "some-value" + }, { + "key" : "some-other-key", + "value" : "some-other-value" + } ] + } ] +} \ No newline at end of file diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleAttachmentInfoResponseTOTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleAttachmentInfoResponseTOTest.java new file mode 100644 index 0000000..9c7044d --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleAttachmentInfoResponseTOTest.java @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import java.util.List; +import org.approvaltests.Approvals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.kernel.extensions.servicewebapi.JsonBinder; + +/** + * Unit tests for {@link GetVehicleAttachmentInfoResponseTO}. + */ +class GetVehicleAttachmentInfoResponseTOTest { + + private JsonBinder jsonBinder; + + @BeforeEach + void setUp() { + jsonBinder = new JsonBinder(); + } + + @Test + void jsonSample() { + GetVehicleAttachmentInfoResponseTO to + = new GetVehicleAttachmentInfoResponseTO() + .setVehicleName("some-vehicle") + .setAvailableCommAdapters( + List.of( + "com.example.somedriver.descriptionclass", + "com.example.someotherdriver.descriptionclass" + ) + ) + .setAttachedCommAdapter("com.example.somedriver.descriptionclass"); + + Approvals.verify(jsonBinder.toJson(to)); + } + +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleAttachmentInfoResponseTOTest.jsonSample.approved.txt b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleAttachmentInfoResponseTOTest.jsonSample.approved.txt new file mode 100644 index 0000000..3cc5be8 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleAttachmentInfoResponseTOTest.jsonSample.approved.txt @@ -0,0 +1,5 @@ +{ + "vehicleName" : "some-vehicle", + "availableCommAdapters" : [ "com.example.somedriver.descriptionclass", "com.example.someotherdriver.descriptionclass" ], + "attachedCommAdapter" : "com.example.somedriver.descriptionclass" +} \ No newline at end of file diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleResponseTOTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleResponseTOTest.java new file mode 100644 index 0000000..edadb00 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleResponseTOTest.java @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import org.approvaltests.Approvals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.opentcs.data.model.Vehicle; +import org.opentcs.kernel.extensions.servicewebapi.JsonBinder; + +/** + * Unit tests for {@link GetVehicleResponseTO}. + */ +class GetVehicleResponseTOTest { + + private JsonBinder jsonBinder; + + @BeforeEach + void setUp() { + jsonBinder = new JsonBinder(); + } + + @ParameterizedTest + @ValueSource(doubles = {Double.NaN, 90.0}) + void jsonSample(double orientationAngle) { + GetVehicleResponseTO to + = new GetVehicleResponseTO() + .setName("some-vehicle") + .setProperties( + new TreeMap<>( + Map.of( + "some-key", "some-value", + "some-other-key", "some-other-value" + ) + ) + ) + .setLength(1234) + .setEnergyLevelGood(90) + .setEnergyLevelCritical(30) + .setEnergyLevelSufficientlyRecharged(30) + .setEnergyLevelFullyRecharged(90) + .setEnergyLevel(48) + .setIntegrationLevel(Vehicle.IntegrationLevel.TO_BE_UTILIZED) + .setPaused(false) + .setProcState(Vehicle.ProcState.PROCESSING_ORDER) + .setTransportOrder("some-order") + .setCurrentPosition("some-point") + .setPrecisePosition(new GetVehicleResponseTO.PrecisePosition(1, 2, 3)) + .setOrientationAngle(orientationAngle) + .setState(Vehicle.State.EXECUTING) + .setEnvelopeKey("envelopeType-01") + .setAllocatedResources( + List.of( + List.of("some-path", "some-point"), + List.of("some-other-path", "some-other-point") + ) + ) + .setClaimedResources( + List.of( + List.of("some-path", "some-point"), + List.of("some-other-path", "some-other-point") + ) + ) + .setAllowedOrderTypes( + List.of( + "OrderType001", + "OrderType002" + ) + ); + + Approvals.verify( + jsonBinder.toJson(to), + Approvals.NAMES.withParameters("orientationAngle-" + orientationAngle) + ); + } + +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleResponseTOTest.jsonSample.orientationAngle-90.0.approved.txt b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleResponseTOTest.jsonSample.orientationAngle-90.0.approved.txt new file mode 100644 index 0000000..ab4e2b6 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleResponseTOTest.jsonSample.orientationAngle-90.0.approved.txt @@ -0,0 +1,29 @@ +{ + "name" : "some-vehicle", + "properties" : { + "some-key" : "some-value", + "some-other-key" : "some-other-value" + }, + "length" : 1234, + "energyLevelGood" : 90, + "energyLevelCritical" : 30, + "energyLevelSufficientlyRecharged" : 30, + "energyLevelFullyRecharged" : 90, + "energyLevel" : 48, + "integrationLevel" : "TO_BE_UTILIZED", + "paused" : false, + "procState" : "PROCESSING_ORDER", + "transportOrder" : "some-order", + "currentPosition" : "some-point", + "precisePosition" : { + "x" : 1, + "y" : 2, + "z" : 3 + }, + "orientationAngle" : 90.0, + "state" : "EXECUTING", + "allocatedResources" : [ [ "some-path", "some-point" ], [ "some-other-path", "some-other-point" ] ], + "claimedResources" : [ [ "some-path", "some-point" ], [ "some-other-path", "some-other-point" ] ], + "allowedOrderTypes" : [ "OrderType001", "OrderType002" ], + "envelopeKey" : "envelopeType-01" +} \ No newline at end of file diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleResponseTOTest.jsonSample.orientationAngle-NaN.approved.txt b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleResponseTOTest.jsonSample.orientationAngle-NaN.approved.txt new file mode 100644 index 0000000..81c0f28 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/GetVehicleResponseTOTest.jsonSample.orientationAngle-NaN.approved.txt @@ -0,0 +1,29 @@ +{ + "name" : "some-vehicle", + "properties" : { + "some-key" : "some-value", + "some-other-key" : "some-other-value" + }, + "length" : 1234, + "energyLevelGood" : 90, + "energyLevelCritical" : 30, + "energyLevelSufficientlyRecharged" : 30, + "energyLevelFullyRecharged" : 90, + "energyLevel" : 48, + "integrationLevel" : "TO_BE_UTILIZED", + "paused" : false, + "procState" : "PROCESSING_ORDER", + "transportOrder" : "some-order", + "currentPosition" : "some-point", + "precisePosition" : { + "x" : 1, + "y" : 2, + "z" : 3 + }, + "orientationAngle" : "NaN", + "state" : "EXECUTING", + "allocatedResources" : [ [ "some-path", "some-point" ], [ "some-other-path", "some-other-point" ] ], + "claimedResources" : [ [ "some-path", "some-point" ], [ "some-other-path", "some-other-point" ] ], + "allowedOrderTypes" : [ "OrderType001", "OrderType002" ], + "envelopeKey" : "envelopeType-01" +} \ No newline at end of file diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PlantModelTOTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PlantModelTOTest.java new file mode 100644 index 0000000..c1b63a4 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PlantModelTOTest.java @@ -0,0 +1,199 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import java.util.List; +import java.util.Set; +import org.approvaltests.Approvals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.kernel.extensions.servicewebapi.JsonBinder; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.BlockTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.LayerGroupTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.LayerTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.LocationTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.LocationTypeTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.PathTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.PeripheralOperationTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.PointTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.VehicleTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.VisualLayoutTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.CoupleTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.EnvelopeTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PropertyTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.TripleTO; + +/** + */ +class PlantModelTOTest { + + private JsonBinder jsonBinder; + + @BeforeEach + void setUp() { + jsonBinder = new JsonBinder(); + } + + @Test + void jsonSample() { + PlantModelTO to = new PlantModelTO("some-name") + .setPoints( + List.of( + new PointTO("some-point") + .setProperties(List.of(new PropertyTO("point-prop", "point-value"))) + .setPosition(new TripleTO(25000, -15000, 0)) + .setVehicleOrientationAngle(73.3) + .setType("PARK_POSITION") + .setVehicleEnvelopes( + List.of( + new EnvelopeTO( + "envelopeType-01", + List.of( + new CoupleTO(25500, -15500), + new CoupleTO(25500, -14500), + new CoupleTO(24500, -14500), + new CoupleTO(24500, -15500) + ) + ) + ) + ) + .setLayout( + new PointTO.Layout() + .setPosition(new CoupleTO(25000, -15000)) + .setLabelOffset(new CoupleTO(-10, -20)) + .setLayerId(0) + ), + new PointTO("some-point2") + .setProperties(List.of(new PropertyTO("point-prop", "point-value"))) + .setPosition(new TripleTO(18000, -15000, 0)) + .setLayout( + new PointTO.Layout() + .setPosition(new CoupleTO(18000, -15000)) + .setLabelOffset(new CoupleTO(-10, -20)) + .setLayerId(0) + ), + new PointTO("some-point3") + .setProperties(List.of(new PropertyTO("point-prop", "point-value"))) + .setPosition(new TripleTO(25000, -9000, 0)) + .setLayout( + new PointTO.Layout() + .setPosition(new CoupleTO(25000, -9000)) + .setLabelOffset(new CoupleTO(-10, -20)) + .setLayerId(0) + ) + ) + ) + .setPaths( + List.of( + new PathTO("some-path", "some-point", "some-point2") + .setProperties(List.of(new PropertyTO("path-prop", "path-value"))) + .setLength(3) + .setMaxVelocity(13) + .setMaxReverseVelocity(3) + .setLocked(true) + .setVehicleEnvelopes( + List.of( + new EnvelopeTO( + "envelopeType-01", + List.of( + new CoupleTO(25500, -15500), + new CoupleTO(25500, -14500), + new CoupleTO(17500, -14500), + new CoupleTO(17500, -15500) + ) + ) + ) + ) + .setPeripheralOperations( + List.of( + new PeripheralOperationTO( + "some-op", + "some-location" + ) + .setExecutionTrigger("AFTER_ALLOCATION") + .setCompletionRequired(true) + ) + ) + .setLayout( + new PathTO.Layout() + .setConnectionType("SLANTED") + .setLayerId(0) + .setControlPoints( + List.of( + new CoupleTO(43000, 30000), + new CoupleTO(44000, 31000), + new CoupleTO(45000, 32000) + ) + ) + ), + new PathTO("another-path", "some-point2", "some-point3") + ) + ) + .setLocationTypes( + List.of( + new LocationTypeTO("some-locationType") + .setProperties(List.of(new PropertyTO("locType-prop", "locType-value"))) + .setAllowedOperations( + List.of("some-operation", "another-operation", "operation3") + ) + .setAllowedPeripheralOperations( + List.of("some-perOp", "another-perOp", "perOp3") + ) + .setLayout( + new LocationTypeTO.Layout() + .setLocationRepresentation("WORKING_GENERIC") + ) + ) + ) + .setLocations( + List.of( + new LocationTO( + "some-location", + "some-locationType", + new TripleTO(30000, -15000, 0) + ) + .setLocked(true) + .setLayout( + new LocationTO.Layout() + .setPosition(new CoupleTO(30000, -15000)) + .setLabelOffset(new CoupleTO(-10, -20)) + .setLocationRepresentation("LOAD_TRANSFER_GENERIC") + ) + ) + ) + .setBlocks( + List.of( + new BlockTO("some-block") + .setProperties(List.of(new PropertyTO("block-prop", "block-value"))) + .setType("SAME_DIRECTION_ONLY") + .setMemberNames(Set.of("some-point2")) + .setLayout(new BlockTO.Layout()) + ) + ) + .setVehicles( + List.of( + new VehicleTO("some-vehicle") + .setProperties(List.of(new PropertyTO("vehicle-prop", "vehicle-value"))) + .setLength(1456) + .setEnergyLevelCritical(10) + .setEnergyLevelGood(30) + .setEnergyLevelSufficientlyRecharged(60) + .setMaxVelocity(2000) + .setMaxReverseVelocity(733) + .setLayout(new VehicleTO.Layout().setRouteColor("#123456")) + ) + ) + .setVisualLayout( + new VisualLayoutTO("some-visualLayout") + .setProperties(List.of(new PropertyTO("vLayout-prop", "vLayout-value"))) + .setScaleX(65) + .setScaleY(65) + .setLayers(List.of(new LayerTO(0, 0, true, "layer0", 0))) + .setLayerGroups(List.of(new LayerGroupTO(0, "layerGroup0", true))) + ) + .setProperties(List.of(new PropertyTO("plantModel-prop", "value"))); + + Approvals.verify(jsonBinder.toJson(to)); + } + +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PlantModelTOTest.jsonSample.approved.txt b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PlantModelTOTest.jsonSample.approved.txt new file mode 100644 index 0000000..4d24f8a --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PlantModelTOTest.jsonSample.approved.txt @@ -0,0 +1,249 @@ +{ + "name" : "some-name", + "points" : [ { + "name" : "some-point", + "position" : { + "x" : 25000, + "y" : -15000, + "z" : 0 + }, + "vehicleOrientationAngle" : 73.3, + "type" : "PARK_POSITION", + "layout" : { + "position" : { + "x" : 25000, + "y" : -15000 + }, + "labelOffset" : { + "x" : -10, + "y" : -20 + }, + "layerId" : 0 + }, + "vehicleEnvelopes" : [ { + "key" : "envelopeType-01", + "vertices" : [ { + "x" : 25500, + "y" : -15500 + }, { + "x" : 25500, + "y" : -14500 + }, { + "x" : 24500, + "y" : -14500 + }, { + "x" : 24500, + "y" : -15500 + } ] + } ], + "properties" : [ { + "name" : "point-prop", + "value" : "point-value" + } ] + }, { + "name" : "some-point2", + "position" : { + "x" : 18000, + "y" : -15000, + "z" : 0 + }, + "vehicleOrientationAngle" : "NaN", + "type" : "HALT_POSITION", + "layout" : { + "position" : { + "x" : 18000, + "y" : -15000 + }, + "labelOffset" : { + "x" : -10, + "y" : -20 + }, + "layerId" : 0 + }, + "vehicleEnvelopes" : [ ], + "properties" : [ { + "name" : "point-prop", + "value" : "point-value" + } ] + }, { + "name" : "some-point3", + "position" : { + "x" : 25000, + "y" : -9000, + "z" : 0 + }, + "vehicleOrientationAngle" : "NaN", + "type" : "HALT_POSITION", + "layout" : { + "position" : { + "x" : 25000, + "y" : -9000 + }, + "labelOffset" : { + "x" : -10, + "y" : -20 + }, + "layerId" : 0 + }, + "vehicleEnvelopes" : [ ], + "properties" : [ { + "name" : "point-prop", + "value" : "point-value" + } ] + } ], + "paths" : [ { + "name" : "some-path", + "srcPointName" : "some-point", + "destPointName" : "some-point2", + "length" : 3, + "maxVelocity" : 13, + "maxReverseVelocity" : 3, + "peripheralOperations" : [ { + "operation" : "some-op", + "locationName" : "some-location", + "executionTrigger" : "AFTER_ALLOCATION", + "completionRequired" : true + } ], + "locked" : true, + "layout" : { + "connectionType" : "SLANTED", + "controlPoints" : [ { + "x" : 43000, + "y" : 30000 + }, { + "x" : 44000, + "y" : 31000 + }, { + "x" : 45000, + "y" : 32000 + } ], + "layerId" : 0 + }, + "vehicleEnvelopes" : [ { + "key" : "envelopeType-01", + "vertices" : [ { + "x" : 25500, + "y" : -15500 + }, { + "x" : 25500, + "y" : -14500 + }, { + "x" : 17500, + "y" : -14500 + }, { + "x" : 17500, + "y" : -15500 + } ] + } ], + "properties" : [ { + "name" : "path-prop", + "value" : "path-value" + } ] + }, { + "name" : "another-path", + "srcPointName" : "some-point2", + "destPointName" : "some-point3", + "length" : 1, + "maxVelocity" : 0, + "maxReverseVelocity" : 0, + "peripheralOperations" : [ ], + "locked" : false, + "layout" : { + "connectionType" : "DIRECT", + "controlPoints" : [ ], + "layerId" : 0 + }, + "vehicleEnvelopes" : [ ], + "properties" : [ ] + } ], + "locationTypes" : [ { + "name" : "some-locationType", + "allowedOperations" : [ "some-operation", "another-operation", "operation3" ], + "allowedPeripheralOperations" : [ "some-perOp", "another-perOp", "perOp3" ], + "layout" : { + "locationRepresentation" : "WORKING_GENERIC" + }, + "properties" : [ { + "name" : "locType-prop", + "value" : "locType-value" + } ] + } ], + "locations" : [ { + "name" : "some-location", + "typeName" : "some-locationType", + "position" : { + "x" : 30000, + "y" : -15000, + "z" : 0 + }, + "links" : [ ], + "locked" : true, + "layout" : { + "position" : { + "x" : 30000, + "y" : -15000 + }, + "labelOffset" : { + "x" : -10, + "y" : -20 + }, + "locationRepresentation" : "LOAD_TRANSFER_GENERIC", + "layerId" : 0 + }, + "properties" : [ ] + } ], + "blocks" : [ { + "name" : "some-block", + "type" : "SAME_DIRECTION_ONLY", + "layout" : { + "color" : "#FF0000" + }, + "memberNames" : [ "some-point2" ], + "properties" : [ { + "name" : "block-prop", + "value" : "block-value" + } ] + } ], + "vehicles" : [ { + "name" : "some-vehicle", + "length" : 1456, + "energyLevelCritical" : 10, + "energyLevelGood" : 30, + "energyLevelFullyRecharged" : 90, + "energyLevelSufficientlyRecharged" : 60, + "maxVelocity" : 2000, + "maxReverseVelocity" : 733, + "layout" : { + "routeColor" : "#123456" + }, + "properties" : [ { + "name" : "vehicle-prop", + "value" : "vehicle-value" + } ] + } ], + "visualLayout" : { + "name" : "some-visualLayout", + "scaleX" : 65.0, + "scaleY" : 65.0, + "layers" : [ { + "id" : 0, + "ordinal" : 0, + "visible" : true, + "name" : "layer0", + "groupId" : 0 + } ], + "layerGroups" : [ { + "id" : 0, + "name" : "layerGroup0", + "visible" : true + } ], + "properties" : [ { + "name" : "vLayout-prop", + "value" : "vLayout-value" + } ] + }, + "properties" : [ { + "name" : "plantModel-prop", + "value" : "value" + } ] +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostOrderSequenceRequestTOTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostOrderSequenceRequestTOTest.java new file mode 100644 index 0000000..91f20e4 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostOrderSequenceRequestTOTest.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import java.util.List; +import org.approvaltests.Approvals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.kernel.extensions.servicewebapi.JsonBinder; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.Property; + +/** + */ +class PostOrderSequenceRequestTOTest { + + private JsonBinder jsonBinder; + + @BeforeEach + void setUp() { + jsonBinder = new JsonBinder(); + } + + @Test + void jsonSample() { + PostOrderSequenceRequestTO to = new PostOrderSequenceRequestTO() + .setIncompleteName(true) + .setType("Transport") + .setIntendedVehicle("some-vehicle") + .setFailureFatal(true) + .setProperties( + List.of( + new Property("some-key", "some-value"), + new Property("another-key", "another-value") + ) + ); + + Approvals.verify(jsonBinder.toJson(to)); + } + +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostOrderSequenceRequestTOTest.jsonSample.approved.txt b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostOrderSequenceRequestTOTest.jsonSample.approved.txt new file mode 100644 index 0000000..2c50604 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostOrderSequenceRequestTOTest.jsonSample.approved.txt @@ -0,0 +1,13 @@ +{ + "incompleteName" : true, + "type" : "Transport", + "intendedVehicle" : "some-vehicle", + "failureFatal" : true, + "properties" : [ { + "key" : "some-key", + "value" : "some-value" + }, { + "key" : "another-key", + "value" : "another-value" + } ] +} \ No newline at end of file diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostPeripheralJobRequestTOTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostPeripheralJobRequestTOTest.java new file mode 100644 index 0000000..a01cf64 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostPeripheralJobRequestTOTest.java @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import java.util.List; +import org.approvaltests.Approvals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.peripherals.PeripheralOperation; +import org.opentcs.kernel.extensions.servicewebapi.JsonBinder; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PeripheralOperationDescription; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.Property; + +/** + * Unit tests for {@link PostPeripheralJobRequestTO}. + */ +class PostPeripheralJobRequestTOTest { + + private JsonBinder jsonBinder; + + @BeforeEach + void setUp() { + jsonBinder = new JsonBinder(); + } + + @Test + void jsonSample() { + PostPeripheralJobRequestTO to + = new PostPeripheralJobRequestTO() + .setIncompleteName(true) + .setReservationToken("some-token") + .setRelatedVehicle("some-vehicle") + .setRelatedTransportOrder("some-order") + .setPeripheralOperation( + new PeripheralOperationDescription() + .setOperation("some-operation") + .setLocationName("some-location") + .setExecutionTrigger(PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION) + .setCompletionRequired(true) + ) + .setProperties( + List.of( + new Property("some-key", "some-value"), + new Property("some-other-key", "some-other-value") + ) + ); + + Approvals.verify(jsonBinder.toJson(to)); + } + +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostPeripheralJobRequestTOTest.jsonSample.approved.txt b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostPeripheralJobRequestTOTest.jsonSample.approved.txt new file mode 100644 index 0000000..864fe85 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostPeripheralJobRequestTOTest.jsonSample.approved.txt @@ -0,0 +1,19 @@ +{ + "incompleteName" : true, + "reservationToken" : "some-token", + "relatedVehicle" : "some-vehicle", + "relatedTransportOrder" : "some-order", + "peripheralOperation" : { + "operation" : "some-operation", + "locationName" : "some-location", + "executionTrigger" : "AFTER_ALLOCATION", + "completionRequired" : true + }, + "properties" : [ { + "key" : "some-key", + "value" : "some-value" + }, { + "key" : "some-other-key", + "value" : "some-other-value" + } ] +} \ No newline at end of file diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostTransportOrderRequestTOTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostTransportOrderRequestTOTest.java new file mode 100644 index 0000000..2f14072 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostTransportOrderRequestTOTest.java @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import java.time.Instant; +import java.util.List; +import org.approvaltests.Approvals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.kernel.extensions.servicewebapi.JsonBinder; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.posttransportorder.Destination; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.Property; + +/** + * Unit tests for {@link PostTransportOrderRequestTO}. + */ +class PostTransportOrderRequestTOTest { + + private JsonBinder jsonBinder; + + @BeforeEach + void setUp() { + jsonBinder = new JsonBinder(); + } + + @Test + void jsonSample() { + PostTransportOrderRequestTO to + = new PostTransportOrderRequestTO() + .setIncompleteName(true) + .setDispensable(true) + .setDeadline(Instant.EPOCH) + .setIntendedVehicle("some-vehicle") + .setPeripheralReservationToken("some-token") + .setWrappingSequence("some-sequence") + .setType("some-type") + .setDestinations( + List.of( + new Destination() + .setLocationName("some-location") + .setOperation("some-operation") + .setProperties( + List.of( + new Property("some-key", "some-value"), + new Property("some-other-key", "some-other-value") + ) + ) + ) + ) + .setProperties( + List.of( + new Property("some-key", "some-value"), + new Property("some-other-key", "some-other-value") + ) + ) + .setDependencies( + List.of( + "some-other-order", + "another-order" + ) + ); + + Approvals.verify(jsonBinder.toJson(to)); + } + +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostTransportOrderRequestTOTest.jsonSample.approved.txt b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostTransportOrderRequestTOTest.jsonSample.approved.txt new file mode 100644 index 0000000..75fb69d --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostTransportOrderRequestTOTest.jsonSample.approved.txt @@ -0,0 +1,28 @@ +{ + "incompleteName" : true, + "dispensable" : true, + "deadline" : "1970-01-01T00:00:00Z", + "intendedVehicle" : "some-vehicle", + "peripheralReservationToken" : "some-token", + "wrappingSequence" : "some-sequence", + "type" : "some-type", + "destinations" : [ { + "locationName" : "some-location", + "operation" : "some-operation", + "properties" : [ { + "key" : "some-key", + "value" : "some-value" + }, { + "key" : "some-other-key", + "value" : "some-other-value" + } ] + } ], + "properties" : [ { + "key" : "some-key", + "value" : "some-value" + }, { + "key" : "some-other-key", + "value" : "some-other-value" + } ], + "dependencies" : [ "some-other-order", "another-order" ] +} \ No newline at end of file diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostVehicleRoutesRequestTOTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostVehicleRoutesRequestTOTest.java new file mode 100644 index 0000000..78ea6cb --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostVehicleRoutesRequestTOTest.java @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import java.util.List; +import org.approvaltests.Approvals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.kernel.extensions.servicewebapi.JsonBinder; + +/** + * Unit tests for {@link PostVehicleRoutesRequestTO}. + */ +class PostVehicleRoutesRequestTOTest { + + private JsonBinder jsonBinder; + + @BeforeEach + void setUp() { + jsonBinder = new JsonBinder(); + } + + @Test + void jsonSample() { + Approvals.verify( + jsonBinder.toJson( + new PostVehicleRoutesRequestTO(List.of("C", "F")) + .setSourcePoint("A") + .setResourcesToAvoid(List.of("A", "B")) + ) + ); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostVehicleRoutesRequestTOTest.jsonSample.approved.txt b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostVehicleRoutesRequestTOTest.jsonSample.approved.txt new file mode 100644 index 0000000..8ad7017 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostVehicleRoutesRequestTOTest.jsonSample.approved.txt @@ -0,0 +1,5 @@ +{ + "destinationPoints" : [ "C", "F" ], + "sourcePoint" : "A", + "resourcesToAvoid" : [ "A", "B" ] +} \ No newline at end of file diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostVehicleRoutesResponseTOTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostVehicleRoutesResponseTOTest.java new file mode 100644 index 0000000..793fc37 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostVehicleRoutesResponseTOTest.java @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import java.util.List; +import org.approvaltests.Approvals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.kernel.extensions.servicewebapi.JsonBinder; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.getvehicleroutes.RouteTO; + +/** + * Unit tests for {@link PostVehicleRoutesResponseTO}. + */ +class PostVehicleRoutesResponseTOTest { + + private JsonBinder jsonBinder; + + @BeforeEach + void setUp() { + jsonBinder = new JsonBinder(); + } + + @Test + void jsonSample() { + Approvals.verify( + jsonBinder.toJson( + new PostVehicleRoutesResponseTO() + .setRoutes( + List.of( + new RouteTO() + .setDestinationPoint("C") + .setCosts(1376) + .setSteps( + List.of( + new RouteTO.Step() + .setSourcePoint("A") + .setDestinationPoint("B") + .setPath("A --- B") + .setVehicleOrientation("FORWARD"), + new RouteTO.Step() + .setSourcePoint("B") + .setDestinationPoint("C") + .setPath("B --- C") + .setVehicleOrientation("FORWARD") + ) + ), + new RouteTO() + .setDestinationPoint("E") + .setCosts(-1) + .setSteps(null), + new RouteTO() + .setDestinationPoint("F") + .setCosts(4682) + .setSteps( + List.of( + new RouteTO.Step() + .setSourcePoint("D") + .setDestinationPoint("E") + .setPath("D --- E") + .setVehicleOrientation("BACKWARD"), + new RouteTO.Step() + .setSourcePoint("E") + .setDestinationPoint("F") + .setPath("E --- F") + .setVehicleOrientation("UNDEFINED") + ) + ) + ) + ) + ) + ); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostVehicleRoutesResponseTOTest.jsonSample.approved.txt b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostVehicleRoutesResponseTOTest.jsonSample.approved.txt new file mode 100644 index 0000000..f4a7b0c --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PostVehicleRoutesResponseTOTest.jsonSample.approved.txt @@ -0,0 +1,35 @@ +{ + "routes" : [ { + "destinationPoint" : "C", + "costs" : 1376, + "steps" : [ { + "path" : "A --- B", + "sourcePoint" : "A", + "destinationPoint" : "B", + "vehicleOrientation" : "FORWARD" + }, { + "path" : "B --- C", + "sourcePoint" : "B", + "destinationPoint" : "C", + "vehicleOrientation" : "FORWARD" + } ] + }, { + "destinationPoint" : "E", + "costs" : -1, + "steps" : null + }, { + "destinationPoint" : "F", + "costs" : 4682, + "steps" : [ { + "path" : "D --- E", + "sourcePoint" : "D", + "destinationPoint" : "E", + "vehicleOrientation" : "BACKWARD" + }, { + "path" : "E --- F", + "sourcePoint" : "E", + "destinationPoint" : "F", + "vehicleOrientation" : "UNDEFINED" + } ] + } ] +} \ No newline at end of file diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PutVehicleAllowedOrderTypesTOTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PutVehicleAllowedOrderTypesTOTest.java new file mode 100644 index 0000000..922e67e --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PutVehicleAllowedOrderTypesTOTest.java @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.binding; + +import java.util.List; +import org.approvaltests.Approvals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.kernel.extensions.servicewebapi.JsonBinder; + +/** + */ +class PutVehicleAllowedOrderTypesTOTest { + + private JsonBinder jsonBinder; + + @BeforeEach + void setUp() { + jsonBinder = new JsonBinder(); + } + + @Test + void jsonSample() { + PutVehicleAllowedOrderTypesTO to + = new PutVehicleAllowedOrderTypesTO( + List.of( + "some-orderType", + "another-orderType", + "orderType-3" + ) + ); + + Approvals.verify(jsonBinder.toJson(to)); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PutVehicleAllowedOrderTypesTOTest.jsonSample.approved.txt b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PutVehicleAllowedOrderTypesTOTest.jsonSample.approved.txt new file mode 100644 index 0000000..ce92e7e --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/binding/PutVehicleAllowedOrderTypesTOTest.jsonSample.approved.txt @@ -0,0 +1,3 @@ +{ + "orderTypes" : [ "some-orderType", "another-orderType", "orderType-3" ] +} \ No newline at end of file diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/BlockConverterTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/BlockConverterTest.java new file mode 100644 index 0000000..8ff78d6 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/BlockConverterTest.java @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.converter; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.awt.Color; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.access.to.model.BlockCreationTO; +import org.opentcs.data.model.Block; +import org.opentcs.data.model.Point; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.BlockTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PropertyTO; +import org.opentcs.util.Colors; + +/** + * Tests for {@link BlockConverter}. + */ +class BlockConverterTest { + + private BlockConverter blockConverter; + private PropertyConverter propertyConverter; + + private Map propertyMap; + private List propertyList; + + @BeforeEach + void setUp() { + propertyConverter = mock(); + blockConverter = new BlockConverter(propertyConverter); + + propertyMap = Map.of("some-key", "some-value"); + propertyList = List.of(new PropertyTO("some-key", "some-value")); + when(propertyConverter.toPropertyTOs(propertyMap)).thenReturn(propertyList); + when(propertyConverter.toPropertyMap(propertyList)).thenReturn(propertyMap); + } + + @Test + void checkToBlockCreationTOs() { + BlockTO blockTO = new BlockTO("block1") + .setType(Block.Type.SINGLE_VEHICLE_ONLY.name()) + .setMemberNames(Set.of("member1")) + .setLayout(new BlockTO.Layout()) + .setProperties(propertyList); + + List result = blockConverter.toBlockCreationTOs(List.of(blockTO)); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getName(), is("block1")); + assertThat(result.get(0).getType(), is(Block.Type.SINGLE_VEHICLE_ONLY)); + assertThat(result.get(0).getMemberNames(), hasSize(1)); + assertThat(result.get(0).getMemberNames(), contains("member1")); + assertThat( + result.get(0).getLayout().getColor(), + is(Colors.decodeFromHexRGB("#FF0000")) + ); + assertThat(result.get(0).getProperties(), is(aMapWithSize(1))); + assertThat(result.get(0).getProperties(), is(propertyMap)); + } + + @Test + void checkToBlockTOs() { + Block block1 = new Block("B1") + .withType(Block.Type.SAME_DIRECTION_ONLY) + .withMembers(Set.of(new Point("point1").getReference())) + .withLayout(new Block.Layout()) + .withProperties(propertyMap); + + List result = blockConverter.toBlockTOs(Set.of(block1)); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getName(), is("B1")); + assertThat(result.get(0).getType(), is("SAME_DIRECTION_ONLY")); + assertThat(result.get(0).getMemberNames(), hasSize(1)); + assertThat(result.get(0).getMemberNames(), contains("point1")); + assertThat(result.get(0).getLayout().getColor(), is(Colors.encodeToHexRGB(Color.RED))); + assertThat(result.get(0).getProperties(), hasSize(1)); + assertThat(result.get(0).getProperties(), is(propertyList)); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/EnvelopeConverterTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/EnvelopeConverterTest.java new file mode 100644 index 0000000..e543ac5 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/EnvelopeConverterTest.java @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.converter; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.samePropertyValuesAs; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Envelope; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.CoupleTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.EnvelopeTO; + +/** + * Tests for {@link EnvelopeConverter}. + */ +class EnvelopeConverterTest { + + private EnvelopeConverter envelopeConverter; + + @BeforeEach + void setUp() { + envelopeConverter = new EnvelopeConverter(); + } + + @Test + void checkVehicleEnvelopeMap() { + EnvelopeTO envelopeTo = new EnvelopeTO("E1", List.of(new CoupleTO(1, 1))); + + Map result = envelopeConverter.toVehicleEnvelopeMap(List.of(envelopeTo)); + + assertThat(result, is(aMapWithSize(1))); + assertThat(result, hasKey("E1")); + assertThat( + result.get("E1").getVertices().get(0), + samePropertyValuesAs(new Couple(1, 1)) + ); + } + + @Test + void checkEnvelopeTOs() { + Map envelopeMap = Map.of("E1", new Envelope(List.of(new Couple(1, 1)))); + + List result = envelopeConverter.toEnvelopeTOs(envelopeMap); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getKey(), is("E1")); + assertThat(result.get(0).getVertices(), hasSize(1)); + assertThat(result.get(0).getVertices().get(0), samePropertyValuesAs(new CoupleTO(1, 1))); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/LocationConverterTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/LocationConverterTest.java new file mode 100644 index 0000000..9f59119 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/LocationConverterTest.java @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.converter; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.samePropertyValuesAs; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.access.to.model.LocationCreationTO; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Triple; +import org.opentcs.data.model.visualization.LocationRepresentation; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.LocationTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.CoupleTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.LinkTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PropertyTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.TripleTO; + +/** + * Tests for {@link LocationConverter}. + */ +class LocationConverterTest { + + private LocationConverter locationConverter; + private PropertyConverter propertyConverter; + + private Map propertyMap; + private List propertyList; + + @BeforeEach + void setUp() { + propertyConverter = mock(); + locationConverter = new LocationConverter(propertyConverter); + + propertyMap = Map.of("some-key", "some-value"); + propertyList = List.of(new PropertyTO("some-key", "some-value")); + when(propertyConverter.toPropertyTOs(propertyMap)).thenReturn(propertyList); + when(propertyConverter.toPropertyMap(propertyList)).thenReturn(propertyMap); + } + + @Test + void checkLocationCreationTOs() { + LocationTO locationTo = new LocationTO("loc1", "T1", new TripleTO(1, 1, 1)) + .setLinks( + List.of( + new LinkTO() + .setPointName("point1") + .setAllowedOperations( + Set.of(LocationRepresentation.LOAD_TRANSFER_GENERIC.name()) + ) + ) + ) + .setLocked(true) + .setLayout( + new LocationTO.Layout() + .setPosition(new CoupleTO(2, 2)) + .setLabelOffset(new CoupleTO(3, 3)) + .setLayerId(4) + .setLocationRepresentation(LocationRepresentation.LOAD_TRANSFER_GENERIC.name()) + ) + .setProperties(propertyList); + + List result = locationConverter.toLocationCreationTOs(List.of(locationTo)); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getName(), is("loc1")); + assertThat(result.get(0).getTypeName(), is("T1")); + assertThat(result.get(0).getPosition(), is(new Triple(1, 1, 1))); + assertThat(result.get(0).getLinks(), is(aMapWithSize(1))); + assertThat( + result.get(0).getLinks(), + hasEntry("point1", Set.of(LocationRepresentation.LOAD_TRANSFER_GENERIC.name())) + ); + assertTrue(result.get(0).isLocked()); + assertThat(result.get(0).getLayout().getPosition(), is(new Couple(2, 2))); + assertThat(result.get(0).getLayout().getLabelOffset(), is(new Couple(3, 3))); + assertThat( + result.get(0).getLayout().getLocationRepresentation(), + is(LocationRepresentation.LOAD_TRANSFER_GENERIC) + ); + assertThat(result.get(0).getLayout().getLayerId(), is(4)); + assertThat(result.get(0).getProperties(), is(aMapWithSize(1))); + assertThat(result.get(0).getProperties(), is(propertyMap)); + } + + @Test + void checkToLocationTOs() { + Location location = new Location("L1", new LocationType("LT1").getReference()) + .withPosition(new Triple(1, 1, 1)) + .withAttachedLinks( + Set.of( + new Location.Link( + new Location("L1", new LocationType("LT1").getReference()).getReference(), + new Point("P1").getReference() + ) + .withAllowedOperations(Set.of("alle")) + ) + ) + .withLocked(false) + .withLayout( + new Location.Layout( + new Couple(1, 1), + new Couple(2, 2), + LocationRepresentation.LOAD_TRANSFER_GENERIC, + 3 + ) + ) + .withProperties(propertyMap); + + List result = locationConverter.toLocationTOs(Set.of(location)); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getName(), is("L1")); + assertThat(result.get(0).getTypeName(), is("LT1")); + assertThat(result.get(0).getPosition(), samePropertyValuesAs(new TripleTO(1, 1, 1))); + assertThat(result.get(0).getLinks(), hasSize(1)); + assertThat(result.get(0).getLinks().get(0).getPointName(), is("P1")); + assertThat(result.get(0).getLinks().get(0).getAllowedOperations(), hasSize(1)); + assertThat(result.get(0).getLinks().get(0).getAllowedOperations(), contains("alle")); + assertFalse(result.get(0).isLocked()); + assertThat(result.get(0).getLayout().getPosition(), samePropertyValuesAs(new CoupleTO(1, 1))); + assertThat( + result.get(0).getLayout().getLabelOffset(), + samePropertyValuesAs(new CoupleTO(2, 2)) + ); + assertThat( + result.get(0).getLayout().getLocationRepresentation(), + is(LocationRepresentation.LOAD_TRANSFER_GENERIC.name()) + ); + assertThat(result.get(0).getLayout().getLayerId(), is(3)); + assertThat(result.get(0).getProperties(), hasSize(1)); + assertThat(result.get(0).getProperties(), is(propertyList)); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/LocationTypeConverterTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/LocationTypeConverterTest.java new file mode 100644 index 0000000..52fa4ad --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/LocationTypeConverterTest.java @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.converter; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.access.to.model.LocationTypeCreationTO; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.visualization.LocationRepresentation; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.LocationTypeTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PropertyTO; + +/** + * Tests for {@link LocationTypeConverter}. + */ +class LocationTypeConverterTest { + + private LocationTypeConverter locationTypeConverter; + private PropertyConverter propertyConverter; + + private Map propertyMap; + private List propertyList; + + @BeforeEach + void setUp() { + propertyConverter = mock(); + locationTypeConverter = new LocationTypeConverter(propertyConverter); + + propertyMap = Map.of("some-key", "some-value"); + propertyList = List.of(new PropertyTO("some-key", "some-value")); + when(propertyConverter.toPropertyTOs(propertyMap)).thenReturn(propertyList); + when(propertyConverter.toPropertyMap(propertyList)).thenReturn(propertyMap); + } + + @Test + void checkToLocationTypeCreationTOs() { + LocationTypeTO locTypeTo = new LocationTypeTO("LT1") + .setAllowedOperations(List.of("O1")) + .setAllowedPeripheralOperations(List.of("PO1")) + .setLayout( + new LocationTypeTO.Layout() + .setLocationRepresentation(LocationRepresentation.RECHARGE_ALT_1.name()) + ) + .setProperties(propertyList); + + List result + = locationTypeConverter.toLocationTypeCreationTOs(List.of(locTypeTo)); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getName(), is("LT1")); + assertThat(result.get(0).getAllowedOperations(), hasSize(1)); + assertThat(result.get(0).getAllowedOperations(), contains("O1")); + assertThat(result.get(0).getAllowedPeripheralOperations(), hasSize(1)); + assertThat(result.get(0).getAllowedPeripheralOperations(), contains("PO1")); + assertThat( + result.get(0).getLayout().getLocationRepresentation(), + is(LocationRepresentation.RECHARGE_ALT_1) + ); + assertThat(result.get(0).getProperties(), is(aMapWithSize(1))); + assertThat(result.get(0).getProperties(), is(propertyMap)); + } + + @Test + void checkToLocationTypeTOs() { + LocationType locType = new LocationType("LT1") + .withAllowedOperations(List.of("O1")) + .withAllowedPeripheralOperations(List.of("PO1")) + .withLayout( + new LocationType.Layout() + .withLocationRepresentation(LocationRepresentation.LOAD_TRANSFER_GENERIC) + ) + .withProperties(propertyMap); + + List result = locationTypeConverter.toLocationTypeTOs(Set.of(locType)); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getName(), is("LT1")); + assertThat(result.get(0).getAllowedOperations(), hasSize(1)); + assertThat(result.get(0).getAllowedOperations(), contains("O1")); + assertThat(result.get(0).getAllowedPeripheralOperations(), hasSize(1)); + assertThat(result.get(0).getAllowedPeripheralOperations(), contains("PO1")); + assertThat( + result.get(0).getLayout().getLocationRepresentation(), + is(LocationRepresentation.LOAD_TRANSFER_GENERIC.name()) + ); + assertThat(result.get(0).getProperties(), hasSize(1)); + assertThat(result.get(0).getProperties(), is(propertyList)); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PathConverterTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PathConverterTest.java new file mode 100644 index 0000000..778aeca --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PathConverterTest.java @@ -0,0 +1,186 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.converter; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.samePropertyValuesAs; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.access.to.model.PathCreationTO; +import org.opentcs.access.to.peripherals.PeripheralOperationCreationTO; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Envelope; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.peripherals.PeripheralOperation; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.PathTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.PeripheralOperationTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.CoupleTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.EnvelopeTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PropertyTO; + +/** + * Tests for {@link PathConverter}. + */ +class PathConverterTest { + + private PathConverter pathConverter; + private PropertyConverter propertyConverter; + private PeripheralOperationConverter peripheralOpConverter; + private EnvelopeConverter envelopeConverter; + + private Map propertyMap; + private List propertyList; + private Map envelopeMap; + private List envelopeList; + private PeripheralOperationTO peripheralOperationTO; + private List peripheralOperationList; + private List peripheralOperationTOList; + private PeripheralOperationCreationTO peripheralOperationCreationTO; + + @BeforeEach + void setUp() { + propertyConverter = mock(); + peripheralOpConverter = mock(); + envelopeConverter = mock(); + pathConverter = new PathConverter(propertyConverter, peripheralOpConverter, envelopeConverter); + + propertyMap = Map.of("some-key", "some-value"); + propertyList = List.of(new PropertyTO("some-key", "some-value")); + when(propertyConverter.toPropertyTOs(propertyMap)).thenReturn(propertyList); + when(propertyConverter.toPropertyMap(propertyList)).thenReturn(propertyMap); + + envelopeMap = Map.of("some-envelope-key", new Envelope(List.of(new Couple(2, 2)))); + envelopeList = List.of(new EnvelopeTO("some-envelope-key", List.of(new CoupleTO(2, 2)))); + when(envelopeConverter.toEnvelopeTOs(envelopeMap)).thenReturn(envelopeList); + when(envelopeConverter.toVehicleEnvelopeMap(envelopeList)).thenReturn(envelopeMap); + + peripheralOperationList = List.of( + new PeripheralOperation( + new Location( + "some-location", + new LocationType("some-location-type").getReference() + ).getReference(), + "some-operation", + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + true + ) + ); + peripheralOperationTO = new PeripheralOperationTO("some-operation", "some-location") + .setExecutionTrigger(PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION.name()) + .setCompletionRequired(true); + peripheralOperationTOList = List.of(peripheralOperationTO); + peripheralOperationCreationTO + = new PeripheralOperationCreationTO("some-operation", "some-location") + .withExecutionTrigger(PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION) + .withCompletionRequired(true); + when(peripheralOpConverter.toPeripheralOperationsTOs(peripheralOperationList)) + .thenReturn(List.of(peripheralOperationTO)); + when(peripheralOpConverter.toPeripheralOperationCreationTOs(peripheralOperationTOList)) + .thenReturn(List.of(peripheralOperationCreationTO)); + } + + @Test + void checkToPathTOs() { + Path path1 = new Path("Path1", new Point("p1").getReference(), new Point("p2").getReference()) + .withLength(3) + .withMaxVelocity(6) + .withMaxReverseVelocity(6) + .withPeripheralOperations(peripheralOperationList) + .withLocked(true) + .withVehicleEnvelopes(envelopeMap) + .withLayout( + new Path.Layout( + Path.Layout.ConnectionType.POLYPATH, + List.of(new Couple(2, 2)), + 4 + ) + ) + .withProperties(propertyMap); + + List result = pathConverter.toPathTOs(Set.of(path1)); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getName(), is("Path1")); + assertThat(result.get(0).getSrcPointName(), is("p1")); + assertThat(result.get(0).getDestPointName(), is("p2")); + assertThat(result.get(0).getLength(), is(3L)); + assertThat(result.get(0).getMaxVelocity(), is(6)); + assertThat(result.get(0).getMaxReverseVelocity(), is(6)); + assertThat(result.get(0).getPeripheralOperations(), hasSize(1)); + assertThat(result.get(0).getPeripheralOperations(), contains(peripheralOperationTO)); + assertTrue(result.get(0).isLocked()); + assertThat(result.get(0).getVehicleEnvelopes(), hasSize(1)); + assertThat(result.get(0).getVehicleEnvelopes(), is(envelopeList)); + assertThat(result.get(0).getLayout().getLayerId(), is(4)); + assertThat( + result.get(0).getLayout().getConnectionType(), + is(Path.Layout.ConnectionType.POLYPATH.name()) + ); + assertThat(result.get(0).getLayout().getControlPoints(), hasSize(1)); + assertThat( + result.get(0).getLayout().getControlPoints().get(0), + samePropertyValuesAs(new CoupleTO(2, 2)) + ); + assertThat(result.get(0).getProperties(), hasSize(1)); + assertThat(result.get(0).getProperties(), is(propertyList)); + } + + @Test + void checkToPathCreationTOs() { + PathTO path1 = new PathTO("Path1", "srcP", "desP") + .setLength(3) + .setMaxVelocity(6) + .setMaxReverseVelocity(6) + .setPeripheralOperations(peripheralOperationTOList) + .setLocked(true) + .setVehicleEnvelopes(envelopeList) + .setLayout( + new PathTO.Layout() + .setConnectionType(Path.Layout.ConnectionType.POLYPATH.name()) + .setControlPoints(List.of(new CoupleTO(1, 1))) + .setLayerId(4) + ) + .setProperties(propertyList); + + List result = pathConverter.toPathCreationTOs(List.of(path1)); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getName(), is("Path1")); + assertThat(result.get(0).getSrcPointName(), is("srcP")); + assertThat(result.get(0).getDestPointName(), is("desP")); + assertThat(result.get(0).getLength(), is(3L)); + assertThat(result.get(0).getMaxVelocity(), is(6)); + assertThat(result.get(0).getMaxReverseVelocity(), is(6)); + assertThat(result.get(0).getPeripheralOperations(), hasSize(1)); + assertThat(result.get(0).getPeripheralOperations(), contains(peripheralOperationCreationTO)); + assertTrue(result.get(0).isLocked()); + assertThat(result.get(0).getVehicleEnvelopes(), is(aMapWithSize(1))); + assertThat(result.get(0).getVehicleEnvelopes(), is(envelopeMap)); + assertThat(result.get(0).getLayout().getLayerId(), is(4)); + assertThat( + result.get(0).getLayout().getConnectionType(), + is(Path.Layout.ConnectionType.POLYPATH) + ); + assertThat(result.get(0).getLayout().getControlPoints(), hasSize(1)); + assertThat( + result.get(0).getLayout().getControlPoints().get(0), + samePropertyValuesAs(new Couple(1, 1)) + ); + assertThat(result.get(0).getProperties(), is(aMapWithSize(1))); + assertThat(result.get(0).getProperties(), is(propertyMap)); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PeripheralOperationConverterTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PeripheralOperationConverterTest.java new file mode 100644 index 0000000..863763e --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PeripheralOperationConverterTest.java @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.converter; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.access.to.peripherals.PeripheralOperationCreationTO; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.peripherals.PeripheralOperation; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.PeripheralOperationTO; + +/** + * Tests for {@link PeripheralOperationConverter}. + */ +class PeripheralOperationConverterTest { + + private PeripheralOperationConverter peripheralOpConverter; + + @BeforeEach + void setUp() { + peripheralOpConverter = new PeripheralOperationConverter(); + } + + @Test + void checkToPeripheralOperationCreationTOs() { + PeripheralOperationTO peripheralOp = new PeripheralOperationTO("all", "l1") + .setExecutionTrigger(PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION.name()) + .setCompletionRequired(true); + + List result + = peripheralOpConverter.toPeripheralOperationCreationTOs(List.of(peripheralOp)); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getOperation(), is("all")); + assertThat(result.get(0).getLocationName(), is("l1")); + assertThat( + result.get(0).getExecutionTrigger(), + is(PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION) + ); + assertTrue(result.get(0).isCompletionRequired()); + } + + @Test + void checkToPeripheralOperationsTOs() { + PeripheralOperation peripheralOp = new PeripheralOperation( + new Location("L1", new LocationType("LT1").getReference()).getReference(), + "operation", + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + true + ); + + List result + = peripheralOpConverter.toPeripheralOperationsTOs(List.of(peripheralOp)); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getOperation(), is("operation")); + assertThat(result.get(0).getLocationName(), is("L1")); + assertThat( + result.get(0).getExecutionTrigger(), + is(PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION.name()) + ); + assertTrue(result.get(0).isCompletionRequired()); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PointConverterTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PointConverterTest.java new file mode 100644 index 0000000..c061698 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PointConverterTest.java @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.converter; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.samePropertyValuesAs; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.access.to.model.PointCreationTO; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Envelope; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Pose; +import org.opentcs.data.model.Triple; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.PointTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.CoupleTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.EnvelopeTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PropertyTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.TripleTO; + +/** + * Tests for {@link PointConverter}. + */ +class PointConverterTest { + + private PointConverter pointConverter; + private PropertyConverter propertyConverter; + private EnvelopeConverter envelopeConverter; + + private Map propertyMap; + private List propertyList; + private Map envelopeMap; + private List envelopeList; + + @BeforeEach + void setUp() { + propertyConverter = mock(); + envelopeConverter = mock(); + pointConverter = new PointConverter(propertyConverter, envelopeConverter); + + propertyMap = Map.of("some-key", "some-value"); + propertyList = List.of(new PropertyTO("some-key", "some-value")); + when(propertyConverter.toPropertyTOs(propertyMap)).thenReturn(propertyList); + when(propertyConverter.toPropertyMap(propertyList)).thenReturn(propertyMap); + + envelopeMap = Map.of("some-envelope-key", new Envelope(List.of(new Couple(2, 2)))); + envelopeList = List.of(new EnvelopeTO("some-envelope-key", List.of(new CoupleTO(2, 2)))); + when(envelopeConverter.toEnvelopeTOs(envelopeMap)).thenReturn(envelopeList); + when(envelopeConverter.toVehicleEnvelopeMap(envelopeList)).thenReturn(envelopeMap); + } + + @Test + void checkToPointTOs() { + Point point1 = new Point("P1") + .withPose(new Pose(new Triple(1, 1, 1), 0.5)) + .withType(Point.Type.HALT_POSITION) + .withVehicleEnvelopes(envelopeMap) + .withLayout(new Point.Layout(new Couple(3, 3), new Couple(4, 4), 7)) + .withProperties(propertyMap); + + List result = pointConverter.toPointTOs(Set.of(point1)); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getName(), is("P1")); + assertThat(result.get(0).getPosition(), samePropertyValuesAs(new TripleTO(1, 1, 1))); + assertThat(result.get(0).getVehicleOrientationAngle(), is(0.5)); + assertThat(result.get(0).getType(), is(Point.Type.HALT_POSITION.name())); + assertThat(result.get(0).getVehicleEnvelopes(), hasSize(1)); + assertThat(result.get(0).getVehicleEnvelopes(), is(envelopeList)); + assertThat(result.get(0).getLayout().getPosition(), samePropertyValuesAs(new CoupleTO(3, 3))); + assertThat( + result.get(0).getLayout().getLabelOffset(), + samePropertyValuesAs(new CoupleTO(4, 4)) + ); + assertThat(result.get(0).getLayout().getLayerId(), is(7)); + assertThat(result.get(0).getProperties(), hasSize(1)); + assertThat(result.get(0).getProperties(), is(propertyList)); + } + + @Test + void checkToPointCreationTOs() { + PointTO point1 = new PointTO("P1") + .setPosition(new TripleTO(1, 1, 1)) + .setVehicleOrientationAngle(0.8) + .setType(Point.Type.HALT_POSITION.name()) + .setVehicleEnvelopes(envelopeList) + .setLayout( + new PointTO.Layout() + .setPosition(new CoupleTO(3, 3)) + .setLabelOffset(new CoupleTO(4, 4)) + .setLayerId(9) + ) + .setProperties(propertyList); + + List result = pointConverter.toPointCreationTOs(List.of(point1)); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getName(), is("P1")); + assertThat(result.get(0).getPose(), samePropertyValuesAs(new Pose(new Triple(1, 1, 1), 0.8))); + assertThat(result.get(0).getType(), is(Point.Type.HALT_POSITION)); + assertThat(result.get(0).getVehicleEnvelopes(), is(aMapWithSize(1))); + assertThat(result.get(0).getVehicleEnvelopes(), is(envelopeMap)); + assertThat(result.get(0).getLayout().getPosition(), samePropertyValuesAs(new Couple(3, 3))); + assertThat(result.get(0).getLayout().getLabelOffset(), samePropertyValuesAs(new Couple(4, 4))); + assertThat(result.get(0).getLayout().getLayerId(), is(9)); + assertThat(result.get(0).getProperties(), is(aMapWithSize(1))); + assertThat(result.get(0).getProperties(), is(propertyMap)); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PropertyConverterTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PropertyConverterTest.java new file mode 100644 index 0000000..85f45a6 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/PropertyConverterTest.java @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.converter; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.samePropertyValuesAs; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PropertyTO; + +/** + * Tests for {@link PropertyConverter}. + */ +class PropertyConverterTest { + + private PropertyConverter propertyConverter; + + @BeforeEach + void setUp() { + propertyConverter = new PropertyConverter(); + } + + @Test + void checkToPropertyTos() { + Map property = Map.of("P1", "1"); + + List result = propertyConverter.toPropertyTOs(property); + + assertThat(result, hasSize(1)); + assertThat(result.get(0), samePropertyValuesAs(new PropertyTO("P1", "1"))); + } + + @Test + void checkToPropertyMap() { + PropertyTO propTo = new PropertyTO("P1", "1"); + + Map result = propertyConverter.toPropertyMap(List.of(propTo)); + + assertThat(result, is(aMapWithSize(1))); + assertThat(result, hasEntry("P1", "1")); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/VehicleConverterTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/VehicleConverterTest.java new file mode 100644 index 0000000..bab2749 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/VehicleConverterTest.java @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.converter; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.awt.Color; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.access.to.model.VehicleCreationTO; +import org.opentcs.data.model.BoundingBox; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.model.Vehicle.EnergyLevelThresholdSet; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.VehicleTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PropertyTO; +import org.opentcs.util.Colors; + +/** + * Tests for {@link VehicleConverter}. + */ +class VehicleConverterTest { + + private VehicleConverter vehicleConverter; + private PropertyConverter propertyConverter; + + private Map propertyMap; + private List propertyList; + + @BeforeEach + void setUp() { + propertyConverter = mock(); + vehicleConverter = new VehicleConverter(propertyConverter); + + propertyMap = Map.of("some-key", "some-value"); + propertyList = List.of(new PropertyTO("some-key", "some-value")); + when(propertyConverter.toPropertyTOs(propertyMap)).thenReturn(propertyList); + when(propertyConverter.toPropertyMap(propertyList)).thenReturn(propertyMap); + } + + @Test + void checkToVehicleCreationTOs() { + VehicleTO vehicleTo = new VehicleTO("V1") + .setLength(1000) + .setEnergyLevelGood(90) + .setEnergyLevelCritical(30) + .setEnergyLevelFullyRecharged(90) + .setEnergyLevelSufficientlyRecharged(30) + .setMaxVelocity(1000) + .setMaxReverseVelocity(1000) + .setLayout(new VehicleTO.Layout()) + .setProperties(propertyList); + + List result = vehicleConverter.toVehicleCreationTOs(List.of(vehicleTo)); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getName(), is("V1")); + assertThat(result.get(0).getBoundingBox().getLength(), is(1000L)); + assertThat(result.get(0).getEnergyLevelThresholdSet().getEnergyLevelGood(), is(90)); + assertThat(result.get(0).getEnergyLevelThresholdSet().getEnergyLevelCritical(), is(30)); + assertThat(result.get(0).getEnergyLevelThresholdSet().getEnergyLevelFullyRecharged(), is(90)); + assertThat( + result.get(0).getEnergyLevelThresholdSet().getEnergyLevelSufficientlyRecharged(), is(30) + ); + assertThat(result.get(0).getMaxVelocity(), is(1000)); + assertThat(result.get(0).getMaxReverseVelocity(), is(1000)); + assertThat(result.get(0).getLayout().getRouteColor(), is(Colors.decodeFromHexRGB("#00FF00"))); + assertThat(result.get(0).getProperties(), is(aMapWithSize(1))); + assertThat(result.get(0).getProperties(), is(propertyMap)); + } + + @Test + void checkToVehicleTOs() { + Vehicle vehicle = new Vehicle("V1") + .withBoundingBox(new BoundingBox(1000, 1000, 1000)) + .withEnergyLevelThresholdSet(new EnergyLevelThresholdSet(30, 90, 30, 90)) + .withMaxVelocity(1000) + .withMaxReverseVelocity(1000) + .withLayout(new Vehicle.Layout()) + .withProperties(propertyMap); + + List result = vehicleConverter.toVehicleTOs(Set.of(vehicle)); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getName(), is("V1")); + assertThat(result.get(0).getLength(), is(1000)); + assertThat(result.get(0).getEnergyLevelGood(), is(90)); + assertThat(result.get(0).getEnergyLevelCritical(), is(30)); + assertThat(result.get(0).getEnergyLevelFullyRecharged(), is(90)); + assertThat(result.get(0).getEnergyLevelSufficientlyRecharged(), is(30)); + assertThat(result.get(0).getMaxVelocity(), is(1000)); + assertThat(result.get(0).getMaxReverseVelocity(), is(1000)); + assertThat(result.get(0).getLayout().getRouteColor(), is(Colors.encodeToHexRGB(Color.RED))); + assertThat(result.get(0).getProperties(), hasSize(1)); + assertThat(result.get(0).getProperties(), is(propertyList)); + } +} diff --git a/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/VisualLayoutConverterTest.java b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/VisualLayoutConverterTest.java new file mode 100644 index 0000000..700e643 --- /dev/null +++ b/opentcs-kernel-extension-http-services/src/test/java/org/opentcs/kernel/extensions/servicewebapi/v1/converter/VisualLayoutConverterTest.java @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.servicewebapi.v1.converter; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.samePropertyValuesAs; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.access.to.model.VisualLayoutCreationTO; +import org.opentcs.data.model.visualization.Layer; +import org.opentcs.data.model.visualization.LayerGroup; +import org.opentcs.data.model.visualization.VisualLayout; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.LayerGroupTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.LayerTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.plantmodel.VisualLayoutTO; +import org.opentcs.kernel.extensions.servicewebapi.v1.binding.shared.PropertyTO; + +/** + * Tests for {@link VisualLayoutConverter}. + */ +class VisualLayoutConverterTest { + + private VisualLayoutConverter visualLayoutConverter; + private PropertyConverter propertyConverter; + + private Map propertyMap; + private List propertyList; + + @BeforeEach + void setUp() { + propertyConverter = mock(); + visualLayoutConverter = new VisualLayoutConverter(propertyConverter); + + propertyMap = Map.of("some-key", "some-value"); + propertyList = List.of(new PropertyTO("some-key", "some-value")); + when(propertyConverter.toPropertyTOs(propertyMap)).thenReturn(propertyList); + when(propertyConverter.toPropertyMap(propertyList)).thenReturn(propertyMap); + } + + @Test + void checkToVisualLayoutCreationTO() { + VisualLayoutTO vLayout = new VisualLayoutTO("V1") + .setScaleX(50.0) + .setScaleY(50.0) + .setLayers(List.of(new LayerTO(1, 2, true, "L1", 3))) + .setLayerGroups(List.of(new LayerGroupTO(1, "Lg1", true))) + .setProperties(propertyList); + + VisualLayoutCreationTO result = visualLayoutConverter.toVisualLayoutCreationTO(vLayout); + + assertThat(result.getName(), is("V1")); + assertThat(result.getScaleX(), is(50.0)); + assertThat(result.getScaleY(), is(50.0)); + assertThat(result.getLayers(), hasSize(1)); + assertThat(result.getLayers().get(0), samePropertyValuesAs(new Layer(1, 2, true, "L1", 3))); + assertThat(result.getLayerGroups(), hasSize(1)); + assertThat( + result.getLayerGroups().get(0), + samePropertyValuesAs(new LayerGroup(1, "Lg1", true)) + ); + assertThat(result.getProperties(), is(aMapWithSize(1))); + assertThat(result.getProperties(), is(propertyMap)); + } + + @Test + void checkVisualLayoutTO() { + VisualLayout vLayout = new VisualLayout("V1") + .withScaleX(50.0) + .withScaleY(50.0) + .withLayers(List.of(new Layer(1, 2, true, "L1", 3))) + .withLayerGroups(List.of(new LayerGroup(3, "G1", true))) + .withProperties(propertyMap); + + VisualLayoutTO result = visualLayoutConverter.toVisualLayoutTO(vLayout); + + assertThat(result.getName(), is("V1")); + assertThat(result.getScaleX(), is(50.00)); + assertThat(result.getScaleY(), is(50.00)); + assertThat(result.getLayers(), hasSize(1)); + assertThat(result.getLayers().get(0), samePropertyValuesAs(new LayerTO(1, 2, true, "L1", 3))); + assertThat(result.getLayerGroups(), hasSize(1)); + assertThat( + result.getLayerGroups().get(0), + samePropertyValuesAs(new LayerGroupTO(3, "G1", true)) + ); + assertThat(result.getProperties(), is(propertyList)); + } +} diff --git a/opentcs-kernel-extension-rmi-services/build.gradle b/opentcs-kernel-extension-rmi-services/build.gradle new file mode 100644 index 0000000..f6741e7 --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/build.gradle @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply from: "${rootDir}/gradle/java-project.gradle" +apply from: "${rootDir}/gradle/java-codequality.gradle" +apply from: "${rootDir}/gradle/guice-project.gradle" +apply from: "${rootDir}/gradle/publishing-java.gradle" + +dependencies { + api project(':opentcs-api-injection') + api project(':opentcs-common') +} + +task release { + dependsOn build +} diff --git a/opentcs-kernel-extension-rmi-services/gradle.properties b/opentcs-kernel-extension-rmi-services/gradle.properties new file mode 100644 index 0000000..14431ac --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/gradle.properties @@ -0,0 +1,40 @@ +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineMethodParams=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAfterDotInChainedMethodCalls=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineDisjunctiveCatchTypes=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineFor=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineImplements=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.sortMembersByVisibility=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.visibilityOrder=PUBLIC;PROTECTED;DEFAULT;PRIVATE +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.enable-indent=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineArrayInit=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineCallArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.keepGettersAndSettersTogether=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classMembersOrder=STATIC FIELD;FIELD;STATIC_INIT;CONSTRUCTOR;INSTANCE_INIT;STATIC METHOD;METHOD;STATIC CLASS;CLASS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapCommentText=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.importGroupsOrder=* +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize=4 +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineAnnotationArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineTryResources=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.preserveNewLinesInComments=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineParenthesized=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineThrows=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=100 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project diff --git a/opentcs-kernel-extension-rmi-services/src/guiceConfig/java/org/opentcs/kernel/extensions/rmi/RmiServicesModule.java b/opentcs-kernel-extension-rmi-services/src/guiceConfig/java/org/opentcs/kernel/extensions/rmi/RmiServicesModule.java new file mode 100644 index 0000000..ec814e8 --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/guiceConfig/java/org/opentcs/kernel/extensions/rmi/RmiServicesModule.java @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import com.google.inject.multibindings.Multibinder; +import jakarta.inject.Singleton; +import org.opentcs.access.rmi.factories.NullSocketFactoryProvider; +import org.opentcs.access.rmi.factories.SecureSocketFactoryProvider; +import org.opentcs.access.rmi.factories.SocketFactoryProvider; +import org.opentcs.customizations.kernel.KernelInjectionModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Configures the RMI services extension. + */ +public class RmiServicesModule + extends + KernelInjectionModule { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(RmiServicesModule.class); + + /** + * Creates a new instance. + */ + public RmiServicesModule() { + } + + @Override + protected void configure() { + RmiKernelInterfaceConfiguration configuration + = getConfigBindingProvider().get( + RmiKernelInterfaceConfiguration.PREFIX, + RmiKernelInterfaceConfiguration.class + ); + + if (!configuration.enable()) { + LOG.info("RMI services disabled by configuration."); + return; + } + + bind(RmiKernelInterfaceConfiguration.class) + .toInstance(configuration); + bind(RegistryProvider.class) + .in(Singleton.class); + bind(UserManager.class) + .in(Singleton.class); + bind(UserAccountProvider.class) + .to(DefaultUserAccountProvider.class); + + if (configuration.useSsl()) { + bind(SocketFactoryProvider.class) + .to(SecureSocketFactoryProvider.class) + .in(Singleton.class); + } + else { + LOG.warn("SSL encryption disabled, connections will not be secured!"); + bind(SocketFactoryProvider.class) + .to(NullSocketFactoryProvider.class) + .in(Singleton.class); + } + + Multibinder remoteServices + = Multibinder.newSetBinder(binder(), KernelRemoteService.class); + remoteServices.addBinding().to(StandardRemotePlantModelService.class); + remoteServices.addBinding().to(StandardRemoteTransportOrderService.class); + remoteServices.addBinding().to(StandardRemoteVehicleService.class); + remoteServices.addBinding().to(StandardRemoteNotificationService.class); + remoteServices.addBinding().to(StandardRemoteRouterService.class); + remoteServices.addBinding().to(StandardRemoteDispatcherService.class); + remoteServices.addBinding().to(StandardRemoteQueryService.class); + remoteServices.addBinding().to(StandardRemotePeripheralService.class); + remoteServices.addBinding().to(StandardRemotePeripheralJobService.class); + remoteServices.addBinding().to(StandardRemotePeripheralDispatcherService.class); + + extensionsBinderAllModes().addBinding() + .to(StandardRemoteKernelClientPortal.class) + .in(Singleton.class); + } +} diff --git a/opentcs-kernel-extension-rmi-services/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.kernel.KernelInjectionModule b/opentcs-kernel-extension-rmi-services/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.kernel.KernelInjectionModule new file mode 100644 index 0000000..9f22769 --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.kernel.KernelInjectionModule @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT + +org.opentcs.kernel.extensions.rmi.RmiServicesModule diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/DefaultUserAccountProvider.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/DefaultUserAccountProvider.java new file mode 100644 index 0000000..601df41 --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/DefaultUserAccountProvider.java @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; +import org.opentcs.common.GuestUserCredentials; + +/** + * The default impelementation of {@link UserAccountProvider}. + * Provides only one (guest) user account. + * + * @see GuestUserCredentials + */ +public class DefaultUserAccountProvider + implements + UserAccountProvider { + + public DefaultUserAccountProvider() { + } + + @Override + public Set getUserAccounts() { + return new HashSet<>( + Arrays.asList( + new UserAccount( + GuestUserCredentials.USER, + GuestUserCredentials.PASSWORD, + EnumSet.allOf(UserPermission.class) + ) + ) + ); + } +} diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/EventBuffer.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/EventBuffer.java new file mode 100644 index 0000000..d99130a --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/EventBuffer.java @@ -0,0 +1,165 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.util.event.EventHandler; + +/** + * Stores events and keeps them until a client fetches them. + */ +public class EventBuffer + implements + EventHandler { + + /** + * The buffered events. + */ + private final Deque events = new LinkedList<>(); + /** + * This buffer's event filter. + */ + private Predicate eventFilter; + /** + * A flag indicating whether this event buffer's client is currently waiting for an event. + */ + private boolean waitingClient; + + /** + * Creates a new instance + * + * @param eventFilter This buffer's initial event filter. + */ + public EventBuffer( + @Nonnull + Predicate eventFilter + ) { + this.eventFilter = requireNonNull(eventFilter, "eventFilter"); + } + + // Methods declared in interface EventListener start here + @Override + public void onEvent(Object event) { + requireNonNull(event, "event"); + synchronized (events) { + if (eventFilter.test(event)) { + if (!tryMergeWithPreviousEvent(event)) { + events.add(event); + } + + // If the client is waiting for an event, wake it up, since there is one now. + if (waitingClient) { + events.notify(); + } + } + } + } + + // Methods not declared in any interface start here + /** + * Returns a list of events that are currently stored in this buffer and + * clears the buffer. + * If the buffer is currently empty, block until an event arrives, or for the + * specified amount of time to pass, whichever occurs first. + * + * @param timeout The maximum amount of time (in ms) to wait for an event to + * arrive. Must be at least 0 (in which case this method will return + * immediately, without waiting for an event to arrive). + * @return A list of events that are currently stored in this buffer. + * @throws IllegalArgumentException If timeout is less than 0. + */ + public List getEvents(long timeout) + throws IllegalArgumentException { + checkArgument(timeout >= 0, "timeout < 0: %s", timeout); + synchronized (events) { + if (timeout > 0 && events.isEmpty()) { + waitingClient = true; + try { + events.wait(timeout); + } + catch (InterruptedException exc) { + throw new IllegalStateException("Unexpectedly interrupted", exc); + } + finally { + waitingClient = false; + } + } + List result = new ArrayList<>(events); + events.clear(); + return result; + } + } + + /** + * Checks whether a client is currently waiting for events arriving in this + * buffer. + * + * @return true if a client is currently waiting, else + * false. + */ + public boolean hasWaitingClient() { + synchronized (events) { + return waitingClient; + } + } + + /** + * Sets this buffer's event filter. + * + * @param eventFilter This buffer's new event filter. + */ + public void setEventFilter( + @Nonnull + Predicate eventFilter + ) { + synchronized (events) { + this.eventFilter = requireNonNull(eventFilter); + } + } + + /** + * If possible, merge the given new event with the previous one in the buffer. + * + * @param event The new event. + * @return true if the new event was merged with the previous one. + */ + private boolean tryMergeWithPreviousEvent(Object event) { + if (!(event instanceof TCSObjectEvent currentEvent) + || !(events.peekLast() instanceof TCSObjectEvent previousEvent)) { + return false; + } + + if (currentEvent.getType() != TCSObjectEvent.Type.OBJECT_MODIFIED + || previousEvent.getType() != TCSObjectEvent.Type.OBJECT_MODIFIED) { + return false; + } + + if (!Objects.equals( + currentEvent.getCurrentObjectState().getReference(), + previousEvent.getCurrentObjectState().getReference() + )) { + return false; + } + + events.removeLast(); + events.add( + new TCSObjectEvent( + currentEvent.getCurrentObjectState(), + previousEvent.getPreviousObjectState(), + TCSObjectEvent.Type.OBJECT_MODIFIED + ) + ); + + return true; + } +} diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/KernelRemoteService.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/KernelRemoteService.java new file mode 100644 index 0000000..6673af4 --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/KernelRemoteService.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import java.util.concurrent.ExecutionException; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.components.Lifecycle; + +/** + * A base class for kernel-side implementations of remote services. + */ +public abstract class KernelRemoteService + implements + Lifecycle { + + /** + * The message to log when a service method execution failed. + */ + private static final String EXECUTION_FAILED_MESSAGE = "Failed to execute service method"; + + /** + * Wraps the given exception into a suitable {@link RuntimeException}. + * + * @param exc The exception to find a runtime exception for. + * @return The runtime exception. + */ + protected RuntimeException findSuitableExceptionFor(Exception exc) { + if (exc instanceof InterruptedException) { + return new IllegalStateException("Unexpectedly interrupted"); + } + if (exc instanceof ExecutionException + && exc.getCause() instanceof RuntimeException) { + return (RuntimeException) exc.getCause(); + } + return new KernelRuntimeException(EXECUTION_FAILED_MESSAGE, exc.getCause()); + } +} diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/RegistryProvider.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/RegistryProvider.java new file mode 100644 index 0000000..bd7171b --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/RegistryProvider.java @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import org.opentcs.access.rmi.factories.SocketFactoryProvider; +import org.opentcs.components.Lifecycle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides the one {@link Registry} instance used for RMI communication. + */ +public class RegistryProvider + implements + Lifecycle { + + /** + * This class' logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(RegistryProvider.class); + /** + * Provides socket factories used to create RMI registries. + */ + private final SocketFactoryProvider socketFactoryProvider; + /** + * This class' configuration. + */ + private final RmiKernelInterfaceConfiguration configuration; + /** + * The actual registry instance. + */ + private Registry registry; + /** + * Whether this provider is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param socketFactoryProvider The socket factory provider used for RMI. + * @param configuration This class' configuration. + */ + @Inject + public RegistryProvider( + @Nonnull + SocketFactoryProvider socketFactoryProvider, + @Nonnull + RmiKernelInterfaceConfiguration configuration + ) { + this.socketFactoryProvider = requireNonNull(socketFactoryProvider, "socketFactoryProvider"); + this.configuration = requireNonNull(configuration, "configuration"); + } + + @Override + public void initialize() { + if (isInitialized()) { + LOG.debug("Already initialized."); + return; + } + + installRegistry(); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + LOG.debug("Not initialized."); + return; + } + + registry = null; + + initialized = false; + } + + @Nonnull + public Registry get() { + return registry; + } + + private void installRegistry() { + try { + LOG.debug("Trying to create a local registry..."); + registry = LocateRegistry.createRegistry( + configuration.registryPort(), + socketFactoryProvider.getClientSocketFactory(), + socketFactoryProvider.getServerSocketFactory() + ); + // Make sure the registry is running + registry.list(); + } + catch (RemoteException ex) { + LOG.error("Couldn't create a working local registry."); + registry = null; + throw new RuntimeException(ex); + } + } +} diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/RmiKernelInterfaceConfiguration.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/RmiKernelInterfaceConfiguration.java new file mode 100644 index 0000000..2add05c --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/RmiKernelInterfaceConfiguration.java @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import org.opentcs.access.rmi.services.RemoteKernelServicePortal; +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; + +/** + * Provides methods to configure the {@link RemoteKernelServicePortal} and the + * {@link KernelRemoteService}s. + */ +@ConfigurationPrefix(RmiKernelInterfaceConfiguration.PREFIX) +public interface RmiKernelInterfaceConfiguration { + + /** + * This configuration's prefix. + */ + String PREFIX = "rmikernelinterface"; + + @ConfigurationEntry( + type = "Boolean", + description = {"Whether to enable the interface."}, + orderKey = "0", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START + ) + Boolean enable(); + + @ConfigurationEntry( + type = "Integer", + description = "The TCP port of the RMI.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_address_1" + ) + int registryPort(); + + @ConfigurationEntry( + type = "Integer", + description = "The TCP port of the remote kernel service portal.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_address_3" + ) + int remoteKernelServicePortalPort(); + + @ConfigurationEntry( + type = "Integer", + description = "The TCP port of the remote plant model service.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_address_4" + ) + int remotePlantModelServicePort(); + + @ConfigurationEntry( + type = "Integer", + description = "The TCP port of the remote transport order service.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_address_5" + ) + int remoteTransportOrderServicePort(); + + @ConfigurationEntry( + type = "Integer", + description = "The TCP port of the remote vehicle service.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_address_6" + ) + int remoteVehicleServicePort(); + + @ConfigurationEntry( + type = "Integer", + description = "The TCP port of the remote notification service.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_address_7" + ) + int remoteNotificationServicePort(); + + @ConfigurationEntry( + type = "Integer", + description = "The TCP port of the remote scheduler service.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_address_8" + ) + int remoteSchedulerServicePort(); + + @ConfigurationEntry( + type = "Integer", + description = "The TCP port of the remote router service.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_address_9" + ) + int remoteRouterServicePort(); + + @ConfigurationEntry( + type = "Integer", + description = "The TCP port of the remote dispatcher service.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_address_10" + ) + int remoteDispatcherServicePort(); + + @ConfigurationEntry( + type = "Integer", + description = "The TCP port of the remote query service.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_address_11" + ) + int remoteQueryServicePort(); + + @ConfigurationEntry( + type = "Integer", + description = "The TCP port of the remote peripheral service.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_address_12" + ) + int remotePeripheralServicePort(); + + @ConfigurationEntry( + type = "Integer", + description = "The TCP port of the remote peripheral job service.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_address_13" + ) + int remotePeripheralJobServicePort(); + + @ConfigurationEntry( + type = "Long", + description = "The interval for cleaning out inactive clients (in ms).", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "2_sweeping" + ) + long clientSweepInterval(); + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to use SSL to encrypt connections.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_address_11" + ) + boolean useSsl(); +} diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteDispatcherService.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteDispatcherService.java new file mode 100644 index 0000000..46f184c --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteDispatcherService.java @@ -0,0 +1,247 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.Registry; +import java.rmi.server.UnicastRemoteObject; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.access.rmi.factories.SocketFactoryProvider; +import org.opentcs.access.rmi.services.RegistrationName; +import org.opentcs.access.rmi.services.RemoteDispatcherService; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.data.order.TransportOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is the standard implementation of the {@link RemoteDispatcherService} interface. + *

+ * Upon creation, an instance of this class registers itself with the RMI registry by the name + * {@link RegistrationName#REMOTE_DISPATCHER_SERVICE}. + *

+ */ +public class StandardRemoteDispatcherService + extends + KernelRemoteService + implements + RemoteDispatcherService { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(StandardRemoteDispatcherService.class); + /** + * The dispatcher service to invoke methods on. + */ + private final DispatcherService dispatcherService; + /** + * The user manager. + */ + private final UserManager userManager; + /** + * Provides configuration data. + */ + private final RmiKernelInterfaceConfiguration configuration; + /** + * Provides socket factories used for RMI. + */ + private final SocketFactoryProvider socketFactoryProvider; + /** + * Provides the registry with which this remote service registers. + */ + private final RegistryProvider registryProvider; + /** + * Executes tasks modifying kernel data. + */ + private final ExecutorService kernelExecutor; + /** + * The registry with which this remote service registers. + */ + private Registry rmiRegistry; + /** + * Whether this remote service is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param dispatcherService The dispatcher service. + * @param userManager The user manager. + * @param configuration This class' configuration. + * @param socketFactoryProvider The socket factory provider used for RMI. + * @param registryProvider The provider for the registry with which this remote service registers. + * @param kernelExecutor Executes tasks modifying kernel data. + */ + @Inject + public StandardRemoteDispatcherService( + DispatcherService dispatcherService, + UserManager userManager, + RmiKernelInterfaceConfiguration configuration, + SocketFactoryProvider socketFactoryProvider, + RegistryProvider registryProvider, + @KernelExecutor + ExecutorService kernelExecutor + ) { + this.dispatcherService = requireNonNull(dispatcherService, "dispatcherService"); + this.userManager = requireNonNull(userManager, "userManager"); + this.configuration = requireNonNull(configuration, "configuration"); + this.socketFactoryProvider = requireNonNull(socketFactoryProvider, "socketFactoryProvider"); + this.registryProvider = requireNonNull(registryProvider, "registryProvider"); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + rmiRegistry = registryProvider.get(); + + // Export this instance via RMI. + try { + LOG.debug("Exporting proxy..."); + UnicastRemoteObject.exportObject( + this, + configuration.remoteDispatcherServicePort(), + socketFactoryProvider.getClientSocketFactory(), + socketFactoryProvider.getServerSocketFactory() + ); + LOG.debug("Binding instance with RMI registry..."); + rmiRegistry.rebind(RegistrationName.REMOTE_DISPATCHER_SERVICE, this); + } + catch (RemoteException exc) { + LOG.error("Could not export or bind with RMI registry", exc); + return; + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + try { + LOG.debug("Unbinding from RMI registry..."); + rmiRegistry.unbind(RegistrationName.REMOTE_DISPATCHER_SERVICE); + LOG.debug("Unexporting RMI interface..."); + UnicastRemoteObject.unexportObject(this, true); + } + catch (RemoteException | NotBoundException exc) { + LOG.warn("Exception shutting down RMI interface", exc); + } + + initialized = false; + } + + @Override + public void dispatch(ClientID clientId) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_ORDER); + + try { + kernelExecutor.submit(() -> dispatcherService.dispatch()).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void withdrawByVehicle( + ClientID clientId, + TCSObjectReference ref, + boolean immediateAbort + ) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_ORDER); + + try { + kernelExecutor.submit(() -> dispatcherService.withdrawByVehicle(ref, immediateAbort)) + .get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void withdrawByTransportOrder( + ClientID clientId, + TCSObjectReference ref, + boolean immediateAbort + ) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_ORDER); + + try { + kernelExecutor.submit(() -> dispatcherService.withdrawByTransportOrder(ref, immediateAbort)) + .get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void reroute( + ClientID clientId, + TCSObjectReference ref, + ReroutingType reroutingType + ) + throws RemoteException { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_ORDER); + + try { + kernelExecutor.submit(() -> dispatcherService.reroute(ref, reroutingType)) + .get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void rerouteAll(ClientID clientId, ReroutingType reroutingType) + throws RemoteException { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_ORDER); + + try { + kernelExecutor.submit(() -> dispatcherService.rerouteAll(reroutingType)) + .get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void assignNow(ClientID clientId, TCSObjectReference ref) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_ORDER); + + try { + kernelExecutor.submit(() -> dispatcherService.assignNow(ref)) + .get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } +} diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteKernelClientPortal.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteKernelClientPortal.java new file mode 100644 index 0000000..7bcb42c --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteKernelClientPortal.java @@ -0,0 +1,233 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.Registry; +import java.rmi.server.UnicastRemoteObject; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import org.opentcs.access.CredentialsException; +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.LocalKernel; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.access.rmi.factories.SocketFactoryProvider; +import org.opentcs.access.rmi.services.RegistrationName; +import org.opentcs.access.rmi.services.RemoteKernelServicePortal; +import org.opentcs.components.kernel.KernelExtension; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.kernel.extensions.rmi.UserManager.ClientEntry; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is the standard implementation of the {@link RemoteKernelServicePortal} interface. + *

+ * Upon creation, an instance of this class registers itself with the RMI registry by the name + * {@link RegistrationName#REMOTE_KERNEL_CLIENT_PORTAL}. + *

+ */ +public class StandardRemoteKernelClientPortal + implements + RemoteKernelServicePortal, + KernelExtension { + + /** + * This class' logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(StandardRemoteKernelClientPortal.class); + /** + * The kernel. + */ + private final Kernel kernel; + /** + * The kernel's remote services. + */ + private final Set remoteServices; + /** + * The user manager. + */ + private final UserManager userManager; + /** + * Provides configuration data. + */ + private final RmiKernelInterfaceConfiguration configuration; + /** + * Provides socket factories used for RMI. + */ + private final SocketFactoryProvider socketFactoryProvider; + /** + * Provides the registry with which this remote portal registers. + */ + private final RegistryProvider registryProvider; + /** + * The event handler to publish events to. + */ + private final EventHandler eventHandler; + /** + * The registry with which this remote portal registers. + */ + private Registry rmiRegistry; + /** + * Whether this remote portal is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param kernel The kernel. + * @param remoteServices The kernel's remote services. + * @param userManager The user manager. + * @param configuration This class' configuration. + * @param socketFactoryProvider The socket factory provider used for RMI. + * @param registryProvider The provider for the registry with which this remote portal registers. + * @param eventHandler The event handler to publish events to. + */ + @Inject + public StandardRemoteKernelClientPortal( + LocalKernel kernel, + Set remoteServices, + UserManager userManager, + RmiKernelInterfaceConfiguration configuration, + SocketFactoryProvider socketFactoryProvider, + RegistryProvider registryProvider, + @ApplicationEventBus + EventHandler eventHandler + ) { + this.kernel = requireNonNull(kernel, "kernel"); + this.remoteServices = requireNonNull(remoteServices, "remoteServices"); + this.userManager = requireNonNull(userManager, "userManager"); + this.configuration = requireNonNull(configuration, "configuration"); + this.socketFactoryProvider = requireNonNull(socketFactoryProvider, "socketFactoryProvider"); + this.registryProvider = requireNonNull(registryProvider, "registryProvider"); + this.eventHandler = requireNonNull(eventHandler, "eventHandler"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + registryProvider.initialize(); + userManager.initialize(); + + rmiRegistry = registryProvider.get(); + // Export this instance via RMI. + try { + LOG.debug("Exporting proxy..."); + UnicastRemoteObject.exportObject( + this, + configuration.remoteKernelServicePortalPort(), + socketFactoryProvider.getClientSocketFactory(), + socketFactoryProvider.getServerSocketFactory() + ); + LOG.debug("Binding instance with RMI registry..."); + rmiRegistry.rebind(RegistrationName.REMOTE_KERNEL_CLIENT_PORTAL, this); + LOG.debug("Bound instance {} with registry {}.", rmiRegistry.list(), rmiRegistry); + } + catch (RemoteException exc) { + LOG.error("Could not export or bind with RMI registry", exc); + return; + } + + for (KernelRemoteService remoteService : remoteServices) { + remoteService.initialize(); + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + for (KernelRemoteService remoteService : remoteServices) { + remoteService.terminate(); + } + + try { + LOG.debug("Unbinding from RMI registry..."); + rmiRegistry.unbind(RegistrationName.REMOTE_KERNEL_CLIENT_PORTAL); + LOG.debug("Unexporting RMI interface..."); + UnicastRemoteObject.unexportObject(this, true); + } + catch (RemoteException | NotBoundException exc) { + LOG.warn("Exception shutting down RMI interface", exc); + } + + userManager.terminate(); + registryProvider.terminate(); + initialized = false; + } + + @Override + public ClientID login(String userName, String password, Predicate eventFilter) + throws CredentialsException { + requireNonNull(userName, "userName"); + requireNonNull(password, "password"); + + UserAccount account = userManager.getUser(userName); + if (account == null || !account.getPassword().equals(password)) { + LOG.debug("Authentication failed for user {}.", userName); + throw new CredentialsException("Authentication failed for user " + userName); + } + + // Generate a new ID for the client. + ClientID clientId = new ClientID(userName); + // Add an entry for the newly connected client. + ClientEntry clientEntry = new ClientEntry(userName, account.getPermissions()); + clientEntry.getEventBuffer().setEventFilter(eventFilter); + userManager.registerClient(clientId, clientEntry); + LOG.debug("New client named {} logged in", clientId.getClientName()); + return clientId; + } + + @Override + public void logout(ClientID clientID) { + requireNonNull("clientID"); + + // Forget the client so it won't be able to call methods on this kernel and won't receive + // events any more. + userManager.unregisterClient(clientID); + LOG.debug("Client named {} logged out", clientID.getClientName()); + } + + @Override + public Kernel.State getState(ClientID clientId) { + userManager.verifyCredentials(clientId, UserPermission.READ_DATA); + + return kernel.getState(); + } + + @Override + public List fetchEvents(ClientID clientId, long timeout) + throws RemoteException { + userManager.verifyCredentials(clientId, UserPermission.READ_DATA); + + return userManager.pollEvents(clientId, timeout); + } + + @Override + public void publishEvent(ClientID clientId, Object event) + throws KernelRuntimeException { + userManager.verifyCredentials(clientId, UserPermission.PUBLISH_MESSAGES); + + eventHandler.onEvent(event); + } +} diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteNotificationService.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteNotificationService.java new file mode 100644 index 0000000..6ca81cd --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteNotificationService.java @@ -0,0 +1,178 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.Registry; +import java.rmi.server.UnicastRemoteObject; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.function.Predicate; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.access.rmi.factories.SocketFactoryProvider; +import org.opentcs.access.rmi.services.RegistrationName; +import org.opentcs.access.rmi.services.RemoteNotificationService; +import org.opentcs.components.kernel.services.NotificationService; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.data.notification.UserNotification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is the standard implementation of the {@link RemoteNotificationService} interface. + *

+ * Upon creation, an instance of this class registers itself with the RMI registry by the name + * {@link RegistrationName#REMOTE_NOTIFICATION_SERVICE}. + *

+ */ +public class StandardRemoteNotificationService + extends + KernelRemoteService + implements + RemoteNotificationService { + + /** + * This class's logger. + */ + private static final Logger LOG + = LoggerFactory.getLogger(StandardRemoteNotificationService.class); + /** + * The notification service to invoke methods on. + */ + private final NotificationService notificationService; + /** + * The user manager. + */ + private final UserManager userManager; + /** + * Provides configuration data. + */ + private final RmiKernelInterfaceConfiguration configuration; + /** + * Provides socket factories used for RMI. + */ + private final SocketFactoryProvider socketFactoryProvider; + /** + * Provides the registry with which this remote service registers. + */ + private final RegistryProvider registryProvider; + /** + * Executes tasks modifying kernel data. + */ + private final ExecutorService kernelExecutor; + /** + * The registry with which this remote service registers. + */ + private Registry rmiRegistry; + /** + * Whether this remote service is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param notificationService The notification service. + * @param userManager The user manager. + * @param configuration This class' configuration. + * @param socketFactoryProvider The socket factory provider used for RMI. + * @param registryProvider The provider for the registry with which this remote service registers. + * @param kernelExecutor Executes tasks modifying kernel data. + */ + @Inject + public StandardRemoteNotificationService( + NotificationService notificationService, + UserManager userManager, + RmiKernelInterfaceConfiguration configuration, + SocketFactoryProvider socketFactoryProvider, + RegistryProvider registryProvider, + @KernelExecutor + ExecutorService kernelExecutor + ) { + this.notificationService = requireNonNull(notificationService, "plantModelService"); + this.userManager = requireNonNull(userManager, "userManager"); + this.configuration = requireNonNull(configuration, "configuration"); + this.socketFactoryProvider = requireNonNull(socketFactoryProvider, "socketFactoryProvider"); + this.registryProvider = requireNonNull(registryProvider, "registryProvider"); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + rmiRegistry = registryProvider.get(); + + // Export this instance via RMI. + try { + LOG.debug("Exporting proxy..."); + UnicastRemoteObject.exportObject( + this, + configuration.remoteNotificationServicePort(), + socketFactoryProvider.getClientSocketFactory(), + socketFactoryProvider.getServerSocketFactory() + ); + LOG.debug("Binding instance with RMI registry..."); + rmiRegistry.rebind(RegistrationName.REMOTE_NOTIFICATION_SERVICE, this); + } + catch (RemoteException exc) { + LOG.error("Could not export or bind with RMI registry", exc); + return; + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + try { + LOG.debug("Unbinding from RMI registry..."); + rmiRegistry.unbind(RegistrationName.REMOTE_NOTIFICATION_SERVICE); + LOG.debug("Unexporting RMI interface..."); + UnicastRemoteObject.unexportObject(this, true); + } + catch (RemoteException | NotBoundException exc) { + LOG.warn("Exception shutting down RMI interface", exc); + } + + initialized = false; + } + + @Override + public List fetchUserNotifications( + ClientID clientId, + Predicate predicate + ) { + userManager.verifyCredentials(clientId, UserPermission.READ_DATA); + + return notificationService.fetchUserNotifications(predicate); + } + + @Override + public void publishUserNotification(ClientID clientId, UserNotification notification) { + userManager.verifyCredentials(clientId, UserPermission.PUBLISH_MESSAGES); + + try { + kernelExecutor.submit(() -> notificationService.publishUserNotification(notification)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } +} diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemotePeripheralDispatcherService.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemotePeripheralDispatcherService.java new file mode 100644 index 0000000..7451b58 --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemotePeripheralDispatcherService.java @@ -0,0 +1,194 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.Registry; +import java.rmi.server.UnicastRemoteObject; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.access.rmi.factories.SocketFactoryProvider; +import org.opentcs.access.rmi.services.RegistrationName; +import org.opentcs.access.rmi.services.RemotePeripheralDispatcherService; +import org.opentcs.components.kernel.services.PeripheralDispatcherService; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.peripherals.PeripheralJob; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is the standard implementation of the {@link RemotePeripheralDispatcherService} + * interface. + *

+ * Upon creation, an instance of this class registers itself with the RMI registry by the name + * {@link RegistrationName#REMOTE_PERIPHERAL_DISPATCHER_SERVICE}. + *

+ */ +public class StandardRemotePeripheralDispatcherService + extends + KernelRemoteService + implements + RemotePeripheralDispatcherService { + + /** + * This class's logger. + */ + private static final Logger LOG + = LoggerFactory.getLogger(StandardRemotePeripheralDispatcherService.class); + /** + * The peripheral dispatcher service to invoke methods on. + */ + private final PeripheralDispatcherService dispatcherService; + /** + * The user manager. + */ + private final UserManager userManager; + /** + * Provides configuration data. + */ + private final RmiKernelInterfaceConfiguration configuration; + /** + * Provides socket factories used for RMI. + */ + private final SocketFactoryProvider socketFactoryProvider; + /** + * Provides the registry with which this remote service registers. + */ + private final RegistryProvider registryProvider; + /** + * Executes tasks modifying kernel data. + */ + private final ExecutorService kernelExecutor; + /** + * The registry with which this remote service registers. + */ + private Registry rmiRegistry; + /** + * Whether this remote service is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param dispatcherService The peripheral dispatcher service. + * @param userManager The user manager. + * @param configuration This class' configuration. + * @param socketFactoryProvider The socket factory provider used for RMI. + * @param registryProvider The provider for the registry with which this remote service registers. + * @param kernelExecutor Executes tasks modifying kernel data. + */ + @Inject + public StandardRemotePeripheralDispatcherService( + PeripheralDispatcherService dispatcherService, + UserManager userManager, + RmiKernelInterfaceConfiguration configuration, + SocketFactoryProvider socketFactoryProvider, + RegistryProvider registryProvider, + @KernelExecutor + ExecutorService kernelExecutor + ) { + this.dispatcherService = requireNonNull(dispatcherService, "dispatcherService"); + this.userManager = requireNonNull(userManager, "userManager"); + this.configuration = requireNonNull(configuration, "configuration"); + this.socketFactoryProvider = requireNonNull(socketFactoryProvider, "socketFactoryProvider"); + this.registryProvider = requireNonNull(registryProvider, "registryProvider"); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + rmiRegistry = registryProvider.get(); + + // Export this instance via RMI. + try { + LOG.debug("Exporting proxy..."); + UnicastRemoteObject.exportObject( + this, + configuration.remoteDispatcherServicePort(), + socketFactoryProvider.getClientSocketFactory(), + socketFactoryProvider.getServerSocketFactory() + ); + LOG.debug("Binding instance with RMI registry..."); + rmiRegistry.rebind(RegistrationName.REMOTE_PERIPHERAL_DISPATCHER_SERVICE, this); + } + catch (RemoteException exc) { + LOG.error("Could not export or bind with RMI registry", exc); + return; + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + try { + LOG.debug("Unbinding from RMI registry..."); + rmiRegistry.unbind(RegistrationName.REMOTE_PERIPHERAL_DISPATCHER_SERVICE); + LOG.debug("Unexporting RMI interface..."); + UnicastRemoteObject.unexportObject(this, true); + } + catch (RemoteException | NotBoundException exc) { + LOG.warn("Exception shutting down RMI interface", exc); + } + + initialized = false; + } + + @Override + public void dispatch(ClientID clientId) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_PERIPHERAL_JOBS); + + try { + kernelExecutor.submit(() -> dispatcherService.dispatch()).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void withdrawByLocation(ClientID clientId, TCSResourceReference ref) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_PERIPHERAL_JOBS); + + try { + kernelExecutor.submit(() -> dispatcherService.withdrawByLocation(ref)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void withdrawByPeripheralJob(ClientID clientId, TCSObjectReference ref) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_PERIPHERAL_JOBS); + + try { + kernelExecutor.submit(() -> dispatcherService.withdrawByPeripheralJob(ref)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } +} diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemotePeripheralJobService.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemotePeripheralJobService.java new file mode 100644 index 0000000..3474a68 --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemotePeripheralJobService.java @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.Registry; +import java.rmi.server.UnicastRemoteObject; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.access.rmi.factories.SocketFactoryProvider; +import org.opentcs.access.rmi.services.RegistrationName; +import org.opentcs.access.rmi.services.RemotePeripheralJobService; +import org.opentcs.access.to.peripherals.PeripheralJobCreationTO; +import org.opentcs.components.kernel.services.PeripheralJobService; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.data.peripherals.PeripheralJob; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is the standard implementation of the {@link RemotePeripheralJobService} interface. + *

+ * Upon creation, an instance of this class registers itself with the RMI registry by the name + * {@link RegistrationName#REMOTE_PERIPHERAL_JOB_SERVICE}. + *

+ */ +public class StandardRemotePeripheralJobService + extends + StandardRemoteTCSObjectService + implements + RemotePeripheralJobService { + + /** + * This class's logger. + */ + private static final Logger LOG + = LoggerFactory.getLogger(StandardRemotePeripheralJobService.class); + /** + * The peripheral job service to invoke methods on. + */ + private final PeripheralJobService peripheralJobService; + /** + * The user manager. + */ + private final UserManager userManager; + /** + * Provides configuration data. + */ + private final RmiKernelInterfaceConfiguration configuration; + /** + * Provides socket factories used for RMI. + */ + private final SocketFactoryProvider socketFactoryProvider; + /** + * Provides the registry with which this remote service registers. + */ + private final RegistryProvider registryProvider; + /** + * Executes tasks modifying kernel data. + */ + private final ExecutorService kernelExecutor; + /** + * The registry with which this remote service registers. + */ + private Registry rmiRegistry; + /** + * Whether this remote service is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param peripheralJobService The peripheral job service. + * @param userManager The user manager. + * @param configuration This class' configuration. + * @param socketFactoryProvider The socket factory provider used for RMI. + * @param registryProvider The provider for the registry with which this remote service registers. + * @param kernelExecutor Executes tasks modifying kernel data. + */ + @Inject + public StandardRemotePeripheralJobService( + PeripheralJobService peripheralJobService, + UserManager userManager, + RmiKernelInterfaceConfiguration configuration, + SocketFactoryProvider socketFactoryProvider, + RegistryProvider registryProvider, + @KernelExecutor + ExecutorService kernelExecutor + ) { + super(peripheralJobService, userManager, kernelExecutor); + this.peripheralJobService = requireNonNull(peripheralJobService, "transportOrderService"); + this.userManager = requireNonNull(userManager, "userManager"); + this.configuration = requireNonNull(configuration, "configuration"); + this.socketFactoryProvider = requireNonNull(socketFactoryProvider, "socketFactoryProvider"); + this.registryProvider = requireNonNull(registryProvider, "registryProvider"); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + rmiRegistry = registryProvider.get(); + + // Export this instance via RMI. + try { + LOG.debug("Exporting proxy..."); + UnicastRemoteObject.exportObject( + this, + configuration.remotePeripheralJobServicePort(), + socketFactoryProvider.getClientSocketFactory(), + socketFactoryProvider.getServerSocketFactory() + ); + LOG.debug("Binding instance with RMI registry..."); + rmiRegistry.rebind(RegistrationName.REMOTE_PERIPHERAL_JOB_SERVICE, this); + } + catch (RemoteException exc) { + LOG.error("Could not export or bind with RMI registry", exc); + return; + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + try { + LOG.debug("Unbinding from RMI registry..."); + rmiRegistry.unbind(RegistrationName.REMOTE_PERIPHERAL_JOB_SERVICE); + LOG.debug("Unexporting RMI interface..."); + UnicastRemoteObject.unexportObject(this, true); + } + catch (RemoteException | NotBoundException exc) { + LOG.warn("Exception shutting down RMI interface", exc); + } + + initialized = false; + } + + @Override + public PeripheralJob createPeripheralJob(ClientID clientId, PeripheralJobCreationTO to) + throws RemoteException { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_PERIPHERAL_JOBS); + + try { + return kernelExecutor.submit(() -> peripheralJobService.createPeripheralJob(to)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } +} diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemotePeripheralService.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemotePeripheralService.java new file mode 100644 index 0000000..3b552f9 --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemotePeripheralService.java @@ -0,0 +1,235 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.Registry; +import java.rmi.server.UnicastRemoteObject; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.access.rmi.factories.SocketFactoryProvider; +import org.opentcs.access.rmi.services.RegistrationName; +import org.opentcs.access.rmi.services.RemotePeripheralService; +import org.opentcs.components.kernel.services.PeripheralService; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.drivers.peripherals.PeripheralAdapterCommand; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; +import org.opentcs.drivers.peripherals.PeripheralProcessModel; +import org.opentcs.drivers.peripherals.management.PeripheralAttachmentInformation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is the standard implementation of the {@link RemotePeripheralService} interface. + *

+ * Upon creation, an instance of this class registers itself with the RMI registry by the name + * {@link RegistrationName#REMOTE_PERIPHERAL_SERVICE}. + *

+ */ +public class StandardRemotePeripheralService + extends + StandardRemoteTCSObjectService + implements + RemotePeripheralService { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(StandardRemotePeripheralService.class); + /** + * The peripheral service to invoke methods on. + */ + private final PeripheralService peripheralService; + /** + * The user manager. + */ + private final UserManager userManager; + /** + * Provides configuration data. + */ + private final RmiKernelInterfaceConfiguration configuration; + /** + * Provides socket factories used for RMI. + */ + private final SocketFactoryProvider socketFactoryProvider; + /** + * Provides the registry with which this remote service registers. + */ + private final RegistryProvider registryProvider; + /** + * Executes tasks modifying kernel data. + */ + private final ExecutorService kernelExecutor; + /** + * The registry with which this remote service registers. + */ + private Registry rmiRegistry; + /** + * Whether this remote service is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param peripheralService The peripheral service. + * @param userManager The user manager. + * @param configuration This class' configuration. + * @param socketFactoryProvider The socket factory provider used for RMI. + * @param registryProvider The provider for the registry with which this remote service registers. + * @param kernelExecutor Executes tasks modifying kernel data. + */ + @Inject + public StandardRemotePeripheralService( + PeripheralService peripheralService, + UserManager userManager, + RmiKernelInterfaceConfiguration configuration, + SocketFactoryProvider socketFactoryProvider, + RegistryProvider registryProvider, + @KernelExecutor + ExecutorService kernelExecutor + ) { + super(peripheralService, userManager, kernelExecutor); + this.peripheralService = requireNonNull(peripheralService, "peripheralService"); + this.userManager = requireNonNull(userManager, "userManager"); + this.configuration = requireNonNull(configuration, "configuration"); + this.socketFactoryProvider = requireNonNull(socketFactoryProvider, "socketFactoryProvider"); + this.registryProvider = requireNonNull(registryProvider, "registryProvider"); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + rmiRegistry = registryProvider.get(); + + // Export this instance via RMI. + try { + LOG.debug("Exporting proxy..."); + UnicastRemoteObject.exportObject( + this, + configuration.remotePeripheralServicePort(), + socketFactoryProvider.getClientSocketFactory(), + socketFactoryProvider.getServerSocketFactory() + ); + LOG.debug("Binding instance with RMI registry..."); + rmiRegistry.rebind(RegistrationName.REMOTE_PERIPHERAL_SERVICE, this); + } + catch (RemoteException exc) { + LOG.error("Could not export or bind with RMI registry", exc); + return; + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + try { + LOG.debug("Unbinding from RMI registry..."); + rmiRegistry.unbind(RegistrationName.REMOTE_PERIPHERAL_SERVICE); + LOG.debug("Unexporting RMI interface..."); + UnicastRemoteObject.unexportObject(this, true); + } + catch (RemoteException | NotBoundException exc) { + LOG.warn("Exception shutting down RMI interface", exc); + } + + initialized = false; + } + + @Override + public void attachCommAdapter( + ClientID clientId, + TCSResourceReference ref, + PeripheralCommAdapterDescription description + ) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_PERIPHERALS); + + try { + kernelExecutor.submit(() -> peripheralService.attachCommAdapter(ref, description)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void disableCommAdapter(ClientID clientId, TCSResourceReference ref) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_PERIPHERALS); + + try { + kernelExecutor.submit(() -> peripheralService.disableCommAdapter(ref)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void enableCommAdapter(ClientID clientId, TCSResourceReference ref) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_PERIPHERALS); + + try { + kernelExecutor.submit(() -> peripheralService.enableCommAdapter(ref)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public PeripheralAttachmentInformation fetchAttachmentInformation( + ClientID clientId, + TCSResourceReference ref + ) { + userManager.verifyCredentials(clientId, UserPermission.READ_DATA); + + return peripheralService.fetchAttachmentInformation(ref); + } + + @Override + public PeripheralProcessModel fetchProcessModel( + ClientID clientId, + TCSResourceReference ref + ) { + userManager.verifyCredentials(clientId, UserPermission.READ_DATA); + + return peripheralService.fetchProcessModel(ref); + } + + @Override + public void sendCommAdapterCommand( + ClientID clientId, + TCSResourceReference ref, + PeripheralAdapterCommand command + ) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_PERIPHERALS); + + try { + kernelExecutor.submit(() -> peripheralService.sendCommAdapterCommand(ref, command)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } +} diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemotePlantModelService.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemotePlantModelService.java new file mode 100644 index 0000000..9ebe700 --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemotePlantModelService.java @@ -0,0 +1,227 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.Registry; +import java.rmi.server.UnicastRemoteObject; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.access.rmi.factories.SocketFactoryProvider; +import org.opentcs.access.rmi.services.RegistrationName; +import org.opentcs.access.rmi.services.RemotePlantModelService; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.components.kernel.services.PlantModelService; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.PlantModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is the standard implementation of the {@link RemotePlantModelService} interface. + *

+ * Upon creation, an instance of this class registers itself with the RMI registry by the name + * {@link RegistrationName#REMOTE_PLANT_MODEL_SERVICE}. + *

+ */ +public class StandardRemotePlantModelService + extends + StandardRemoteTCSObjectService + implements + RemotePlantModelService { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(StandardRemotePlantModelService.class); + /** + * The plant model service to invoke methods on. + */ + private final PlantModelService plantModelService; + /** + * The user manager. + */ + private final UserManager userManager; + /** + * Provides configuration data. + */ + private final RmiKernelInterfaceConfiguration configuration; + /** + * Provides socket factories used for RMI. + */ + private final SocketFactoryProvider socketFactoryProvider; + /** + * Provides the registry with which this remote service registers. + */ + private final RegistryProvider registryProvider; + /** + * Executes tasks modifying kernel data. + */ + private final ExecutorService kernelExecutor; + /** + * The registry with which this remote service registers. + */ + private Registry rmiRegistry; + /** + * Whether this remote service is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param plantModelService The plant model service. + * @param userManager The user manager. + * @param configuration This class' configuration. + * @param socketFactoryProvider The socket factory provider used for RMI. + * @param registryProvider The provider for the registry with which this remote service registers. + * @param kernelExecutor Executes tasks modifying kernel data. + */ + @Inject + public StandardRemotePlantModelService( + PlantModelService plantModelService, + UserManager userManager, + RmiKernelInterfaceConfiguration configuration, + SocketFactoryProvider socketFactoryProvider, + RegistryProvider registryProvider, + @KernelExecutor + ExecutorService kernelExecutor + ) { + super(plantModelService, userManager, kernelExecutor); + this.plantModelService = requireNonNull(plantModelService, "plantModelService"); + this.userManager = requireNonNull(userManager, "userManager"); + this.configuration = requireNonNull(configuration, "configuration"); + this.socketFactoryProvider = requireNonNull(socketFactoryProvider, "socketFactoryProvider"); + this.registryProvider = requireNonNull(registryProvider, "registryProvider"); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + rmiRegistry = registryProvider.get(); + + // Export this instance via RMI. + try { + LOG.debug("Exporting proxy..."); + UnicastRemoteObject.exportObject( + this, + configuration.remotePlantModelServicePort(), + socketFactoryProvider.getClientSocketFactory(), + socketFactoryProvider.getServerSocketFactory() + ); + LOG.debug("Binding instance with RMI registry..."); + rmiRegistry.rebind(RegistrationName.REMOTE_PLANT_MODEL_SERVICE, this); + } + catch (RemoteException exc) { + LOG.error("Could not export or bind with RMI registry", exc); + return; + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + try { + LOG.debug("Unbinding from RMI registry..."); + rmiRegistry.unbind(RegistrationName.REMOTE_PLANT_MODEL_SERVICE); + LOG.debug("Unexporting RMI interface..."); + UnicastRemoteObject.unexportObject(this, true); + } + catch (RemoteException | NotBoundException exc) { + LOG.warn("Exception shutting down RMI interface", exc); + } + + initialized = false; + } + + @Override + public PlantModel getPlantModel(ClientID clientId) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_MODEL); + + try { + return kernelExecutor.submit(() -> plantModelService.getPlantModel()).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void createPlantModel(ClientID clientId, PlantModelCreationTO to) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_MODEL); + + try { + kernelExecutor.submit(() -> plantModelService.createPlantModel(to)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public String getModelName(ClientID clientId) { + userManager.verifyCredentials(clientId, UserPermission.READ_DATA); + + return plantModelService.getModelName(); + } + + @Override + public Map getModelProperties(ClientID clientId) { + userManager.verifyCredentials(clientId, UserPermission.READ_DATA); + + return plantModelService.getModelProperties(); + } + + @Override + public void updateLocationLock( + ClientID clientId, + TCSObjectReference ref, + boolean locked + ) + throws RemoteException { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_MODEL); + + try { + kernelExecutor.submit(() -> plantModelService.updateLocationLock(ref, locked)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void updatePathLock(ClientID clientId, TCSObjectReference ref, boolean locked) + throws RemoteException { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_MODEL); + + try { + kernelExecutor.submit(() -> plantModelService.updatePathLock(ref, locked)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } +} diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteQueryService.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteQueryService.java new file mode 100644 index 0000000..21994f1 --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteQueryService.java @@ -0,0 +1,167 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.Registry; +import java.rmi.server.UnicastRemoteObject; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.access.rmi.factories.SocketFactoryProvider; +import org.opentcs.access.rmi.services.RegistrationName; +import org.opentcs.access.rmi.services.RemoteQueryService; +import org.opentcs.components.kernel.Query; +import org.opentcs.components.kernel.services.QueryService; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is the standard implementation of the {@link RemoteQueryService} interface. + *

+ * Upon creation, an instance of this class registers itself with the RMI registry by the name + * {@link RegistrationName#REMOTE_QUERY_SERVICE}. + *

+ */ +public class StandardRemoteQueryService + extends + KernelRemoteService + implements + RemoteQueryService { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(StandardRemoteQueryService.class); + /** + * The query service to invoke methods on. + */ + private final QueryService queryService; + /** + * The user manager. + */ + private final UserManager userManager; + /** + * Provides configuration data. + */ + private final RmiKernelInterfaceConfiguration configuration; + /** + * Provides socket factories used for RMI. + */ + private final SocketFactoryProvider socketFactoryProvider; + /** + * Provides the registry with which this remote service registers. + */ + private final RegistryProvider registryProvider; + /** + * Executes tasks modifying kernel data. + */ + private final ExecutorService kernelExecutor; + /** + * The registry with which this remote service registers. + */ + private Registry rmiRegistry; + /** + * Whether this remote service is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param queryService The query service. + * @param userManager The user manager. + * @param configuration This class' configuration. + * @param socketFactoryProvider The socket factory provider used for RMI. + * @param registryProvider The provider for the registry with which this remote service registers. + * @param kernelExecutor Executes tasks modifying kernel data. + */ + @Inject + public StandardRemoteQueryService( + QueryService queryService, + UserManager userManager, + RmiKernelInterfaceConfiguration configuration, + SocketFactoryProvider socketFactoryProvider, + RegistryProvider registryProvider, + @KernelExecutor + ExecutorService kernelExecutor + ) { + this.queryService = requireNonNull(queryService, "queryService"); + this.userManager = requireNonNull(userManager, "userManager"); + this.configuration = requireNonNull(configuration, "configuration"); + this.socketFactoryProvider = requireNonNull(socketFactoryProvider, "socketFactoryProvider"); + this.registryProvider = requireNonNull(registryProvider, "registryProvider"); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + rmiRegistry = registryProvider.get(); + + // Export this instance via RMI. + try { + LOG.debug("Exporting proxy..."); + UnicastRemoteObject.exportObject( + this, + configuration.remoteQueryServicePort(), + socketFactoryProvider.getClientSocketFactory(), + socketFactoryProvider.getServerSocketFactory() + ); + LOG.debug("Binding instance with RMI registry..."); + rmiRegistry.rebind(RegistrationName.REMOTE_QUERY_SERVICE, this); + } + catch (RemoteException exc) { + LOG.error("Could not export or bind with RMI registry", exc); + return; + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + try { + LOG.debug("Unbinding from RMI registry..."); + rmiRegistry.unbind(RegistrationName.REMOTE_QUERY_SERVICE); + LOG.debug("Unexporting RMI interface..."); + UnicastRemoteObject.unexportObject(this, true); + } + catch (RemoteException | NotBoundException exc) { + LOG.warn("Exception shutting down RMI interface", exc); + } + + initialized = false; + } + + @Override + public T query(ClientID clientId, Query query) { + userManager.verifyCredentials(clientId, UserPermission.READ_DATA); + + try { + return kernelExecutor.submit(() -> queryService.query(query)) + .get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + +} diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteRouterService.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteRouterService.java new file mode 100644 index 0000000..f0421e9 --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteRouterService.java @@ -0,0 +1,199 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.Registry; +import java.rmi.server.UnicastRemoteObject; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.access.rmi.factories.SocketFactoryProvider; +import org.opentcs.access.rmi.services.RegistrationName; +import org.opentcs.access.rmi.services.RemoteRouterService; +import org.opentcs.components.kernel.services.RouterService; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.Route; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is the standard implementation of the {@link RemoteRouterService} interface. + *

+ * Upon creation, an instance of this class registers itself with the RMI registry by the name + * {@link RegistrationName#REMOTE_ROUTER_SERVICE}. + *

+ */ +public class StandardRemoteRouterService + extends + KernelRemoteService + implements + RemoteRouterService { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(StandardRemoteRouterService.class); + /** + * The scheduler service to invoke methods on. + */ + private final RouterService routerService; + /** + * The user manager. + */ + private final UserManager userManager; + /** + * Provides configuration data. + */ + private final RmiKernelInterfaceConfiguration configuration; + /** + * Provides socket factories used for RMI. + */ + private final SocketFactoryProvider socketFactoryProvider; + /** + * Provides the registry with which this remote service registers. + */ + private final RegistryProvider registryProvider; + /** + * Executes tasks modifying kernel data. + */ + private final ExecutorService kernelExecutor; + /** + * The registry with which this remote service registers. + */ + private Registry rmiRegistry; + /** + * Whether this remote service is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param routerService The router service. + * @param userManager The user manager. + * @param configuration This class' configuration. + * @param socketFactoryProvider The socket factory provider used for RMI. + * @param registryProvider The provider for the registry with which this remote service registers. + * @param kernelExecutor Executes tasks modifying kernel data. + */ + @Inject + public StandardRemoteRouterService( + RouterService routerService, + UserManager userManager, + RmiKernelInterfaceConfiguration configuration, + SocketFactoryProvider socketFactoryProvider, + RegistryProvider registryProvider, + @KernelExecutor + ExecutorService kernelExecutor + ) { + this.routerService = requireNonNull(routerService, "routerService"); + this.userManager = requireNonNull(userManager, "userManager"); + this.configuration = requireNonNull(configuration, "configuration"); + this.socketFactoryProvider = requireNonNull(socketFactoryProvider, "socketFactoryProvider"); + this.registryProvider = requireNonNull(registryProvider, "registryProvider"); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + rmiRegistry = registryProvider.get(); + + // Export this instance via RMI. + try { + LOG.debug("Exporting proxy..."); + UnicastRemoteObject.exportObject( + this, + configuration.remoteRouterServicePort(), + socketFactoryProvider.getClientSocketFactory(), + socketFactoryProvider.getServerSocketFactory() + ); + LOG.debug("Binding instance with RMI registry..."); + rmiRegistry.rebind(RegistrationName.REMOTE_ROUTER_SERVICE, this); + } + catch (RemoteException exc) { + LOG.error("Could not export or bind with RMI registry", exc); + return; + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + try { + LOG.debug("Unbinding from RMI registry..."); + rmiRegistry.unbind(RegistrationName.REMOTE_ROUTER_SERVICE); + LOG.debug("Unexporting RMI interface..."); + UnicastRemoteObject.unexportObject(this, true); + } + catch (RemoteException | NotBoundException exc) { + LOG.warn("Exception shutting down RMI interface", exc); + } + + initialized = false; + } + + @Override + public void updateRoutingTopology(ClientID clientId, Set> refs) + throws RemoteException { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_MODEL); + + try { + kernelExecutor.submit(() -> routerService.updateRoutingTopology(refs)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public Map, Route> computeRoutes( + ClientID clientId, + TCSObjectReference vehicleRef, + TCSObjectReference sourcePointRef, + Set> destinationPointRefs, + Set> resourcesToAvoid + ) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_MODEL); + + try { + return kernelExecutor.submit( + () -> routerService.computeRoutes( + vehicleRef, + sourcePointRef, + destinationPointRefs, + resourcesToAvoid + ) + ) + .get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } +} diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteTCSObjectService.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteTCSObjectService.java new file mode 100644 index 0000000..ad798cc --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteTCSObjectService.java @@ -0,0 +1,129 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nullable; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.function.Predicate; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.access.rmi.services.RemoteTCSObjectService; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectReference; + +/** + * This class is the standard implementation of the {@link RemoteTCSObjectService} interface. + */ +public abstract class StandardRemoteTCSObjectService + extends + KernelRemoteService + implements + RemoteTCSObjectService { + + /** + * The object service to invoke methods on. + */ + private final TCSObjectService objectService; + /** + * The user manager. + */ + private final UserManager userManager; + /** + * Executes tasks modifying kernel data. + */ + private final ExecutorService kernelExecutor; + + /** + * Creates a new instance. + * + * @param objectService The object service. + * @param userManager The user manager. + * @param kernelExecutor Executes tasks modifying kernel data. + */ + public StandardRemoteTCSObjectService( + TCSObjectService objectService, + UserManager userManager, + @KernelExecutor + ExecutorService kernelExecutor + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.userManager = requireNonNull(userManager, "userManager"); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + } + + @Override + public > T fetchObject( + ClientID clientId, Class clazz, + TCSObjectReference ref + ) { + userManager.verifyCredentials(clientId, UserPermission.READ_DATA); + + return objectService.fetchObject(clazz, ref); + } + + @Override + public > T fetchObject(ClientID clientId, Class clazz, String name) { + userManager.verifyCredentials(clientId, UserPermission.READ_DATA); + + return objectService.fetchObject(clazz, name); + } + + @Override + public > Set fetchObjects(ClientID clientId, Class clazz) { + userManager.verifyCredentials(clientId, UserPermission.READ_DATA); + + return objectService.fetchObjects(clazz); + } + + @Override + public > Set fetchObjects( + ClientID clientId, + Class clazz, + Predicate predicate + ) { + userManager.verifyCredentials(clientId, UserPermission.READ_DATA); + + return objectService.fetchObjects(clazz, predicate); + } + + @Override + public void updateObjectProperty( + ClientID clientId, + TCSObjectReference ref, + String key, + @Nullable + String value + ) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_MODEL); + + try { + kernelExecutor.submit(() -> objectService.updateObjectProperty(ref, key, value)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void appendObjectHistoryEntry( + ClientID clientId, + TCSObjectReference ref, + ObjectHistory.Entry entry + ) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_MODEL); + + try { + kernelExecutor.submit(() -> objectService.appendObjectHistoryEntry(ref, entry)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + +} diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteTransportOrderService.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteTransportOrderService.java new file mode 100644 index 0000000..792bf2b --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteTransportOrderService.java @@ -0,0 +1,217 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.Registry; +import java.rmi.server.UnicastRemoteObject; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.access.rmi.factories.SocketFactoryProvider; +import org.opentcs.access.rmi.services.RegistrationName; +import org.opentcs.access.rmi.services.RemoteTransportOrderService; +import org.opentcs.access.to.order.OrderSequenceCreationTO; +import org.opentcs.access.to.order.TransportOrderCreationTO; +import org.opentcs.components.kernel.services.TransportOrderService; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is the standard implementation of the {@link RemoteTransportOrderService} interface. + *

+ * Upon creation, an instance of this class registers itself with the RMI registry by the name + * {@link RegistrationName#REMOTE_TRANSPORT_ORDER_SERVICE}. + *

+ */ +public class StandardRemoteTransportOrderService + extends + StandardRemoteTCSObjectService + implements + RemoteTransportOrderService { + + /** + * This class's logger. + */ + private static final Logger LOG + = LoggerFactory.getLogger(StandardRemoteTransportOrderService.class); + /** + * The transport order service to invoke methods on. + */ + private final TransportOrderService transportOrderService; + /** + * The user manager. + */ + private final UserManager userManager; + /** + * Provides configuration data. + */ + private final RmiKernelInterfaceConfiguration configuration; + /** + * Provides socket factories used for RMI. + */ + private final SocketFactoryProvider socketFactoryProvider; + /** + * Provides the registry with which this remote service registers. + */ + private final RegistryProvider registryProvider; + /** + * Executes tasks modifying kernel data. + */ + private final ExecutorService kernelExecutor; + /** + * The registry with which this remote service registers. + */ + private Registry rmiRegistry; + /** + * Whether this remote service is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param transportOrderService The transport order service. + * @param userManager The user manager. + * @param configuration This class' configuration. + * @param socketFactoryProvider The socket factory provider used for RMI. + * @param registryProvider The provider for the registry with which this remote service registers. + * @param kernelExecutor Executes tasks modifying kernel data. + */ + @Inject + public StandardRemoteTransportOrderService( + TransportOrderService transportOrderService, + UserManager userManager, + RmiKernelInterfaceConfiguration configuration, + SocketFactoryProvider socketFactoryProvider, + RegistryProvider registryProvider, + @KernelExecutor + ExecutorService kernelExecutor + ) { + super(transportOrderService, userManager, kernelExecutor); + this.transportOrderService = requireNonNull(transportOrderService, "transportOrderService"); + this.userManager = requireNonNull(userManager, "userManager"); + this.configuration = requireNonNull(configuration, "configuration"); + this.socketFactoryProvider = requireNonNull(socketFactoryProvider, "socketFactoryProvider"); + this.registryProvider = requireNonNull(registryProvider, "registryProvider"); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + rmiRegistry = registryProvider.get(); + + // Export this instance via RMI. + try { + LOG.debug("Exporting proxy..."); + UnicastRemoteObject.exportObject( + this, + configuration.remoteTransportOrderServicePort(), + socketFactoryProvider.getClientSocketFactory(), + socketFactoryProvider.getServerSocketFactory() + ); + LOG.debug("Binding instance with RMI registry..."); + rmiRegistry.rebind(RegistrationName.REMOTE_TRANSPORT_ORDER_SERVICE, this); + } + catch (RemoteException exc) { + LOG.error("Could not export or bind with RMI registry", exc); + return; + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + try { + LOG.debug("Unbinding from RMI registry..."); + rmiRegistry.unbind(RegistrationName.REMOTE_TRANSPORT_ORDER_SERVICE); + LOG.debug("Unexporting RMI interface..."); + UnicastRemoteObject.unexportObject(this, true); + } + catch (RemoteException | NotBoundException exc) { + LOG.warn("Exception shutting down RMI interface", exc); + } + + initialized = false; + } + + @Override + public OrderSequence createOrderSequence(ClientID clientId, OrderSequenceCreationTO to) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_ORDER); + + try { + return kernelExecutor.submit(() -> transportOrderService.createOrderSequence(to)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public TransportOrder createTransportOrder(ClientID clientId, TransportOrderCreationTO to) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_ORDER); + + try { + return kernelExecutor.submit(() -> transportOrderService.createTransportOrder(to)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void markOrderSequenceComplete(ClientID clientId, TCSObjectReference ref) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_ORDER); + + try { + kernelExecutor.submit(() -> transportOrderService.markOrderSequenceComplete(ref)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void updateTransportOrderIntendedVehicle( + ClientID clientId, + TCSObjectReference orderRef, + TCSObjectReference vehicleRef + ) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_ORDER); + + try { + kernelExecutor.submit( + () -> transportOrderService.updateTransportOrderIntendedVehicle( + orderRef, + vehicleRef + ) + ).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } +} diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteVehicleService.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteVehicleService.java new file mode 100644 index 0000000..4a86818 --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/StandardRemoteVehicleService.java @@ -0,0 +1,347 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.Registry; +import java.rmi.server.UnicastRemoteObject; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.access.rmi.factories.SocketFactoryProvider; +import org.opentcs.access.rmi.services.RegistrationName; +import org.opentcs.access.rmi.services.RemoteVehicleService; +import org.opentcs.components.kernel.services.VehicleService; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.model.Vehicle.EnergyLevelThresholdSet; +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; +import org.opentcs.drivers.vehicle.management.VehicleAttachmentInformation; +import org.opentcs.drivers.vehicle.management.VehicleProcessModelTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is the standard implementation of the {@link RemoteVehicleService} interface. + *

+ * Upon creation, an instance of this class registers itself with the RMI registry by the name + * {@link RegistrationName#REMOTE_VEHICLE_SERVICE}. + *

+ */ +public class StandardRemoteVehicleService + extends + StandardRemoteTCSObjectService + implements + RemoteVehicleService { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(StandardRemoteVehicleService.class); + /** + * The vehicle service to invoke methods on. + */ + private final VehicleService vehicleService; + /** + * The user manager. + */ + private final UserManager userManager; + /** + * Provides configuration data. + */ + private final RmiKernelInterfaceConfiguration configuration; + /** + * Provides socket factories used for RMI. + */ + private final SocketFactoryProvider socketFactoryProvider; + /** + * Provides the registry with which this remote service registers. + */ + private final RegistryProvider registryProvider; + /** + * Executes tasks modifying kernel data. + */ + private final ExecutorService kernelExecutor; + /** + * The registry with which this remote service registers. + */ + private Registry rmiRegistry; + /** + * Whether this remote service is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param vehicleService The vehicle service. + * @param userManager The user manager. + * @param configuration This class' configuration. + * @param socketFactoryProvider The socket factory provider used for RMI. + * @param registryProvider The provider for the registry with which this remote service registers. + * @param kernelExecutor Executes tasks modifying kernel data. + */ + @Inject + public StandardRemoteVehicleService( + VehicleService vehicleService, + UserManager userManager, + RmiKernelInterfaceConfiguration configuration, + SocketFactoryProvider socketFactoryProvider, + RegistryProvider registryProvider, + @KernelExecutor + ExecutorService kernelExecutor + ) { + super(vehicleService, userManager, kernelExecutor); + this.vehicleService = requireNonNull(vehicleService, "vehicleService"); + this.userManager = requireNonNull(userManager, "userManager"); + this.configuration = requireNonNull(configuration, "configuration"); + this.socketFactoryProvider = requireNonNull(socketFactoryProvider, "socketFactoryProvider"); + this.registryProvider = requireNonNull(registryProvider, "registryProvider"); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + rmiRegistry = registryProvider.get(); + + // Export this instance via RMI. + try { + LOG.debug("Exporting proxy..."); + UnicastRemoteObject.exportObject( + this, + configuration.remoteVehicleServicePort(), + socketFactoryProvider.getClientSocketFactory(), + socketFactoryProvider.getServerSocketFactory() + ); + LOG.debug("Binding instance with RMI registry..."); + rmiRegistry.rebind(RegistrationName.REMOTE_VEHICLE_SERVICE, this); + } + catch (RemoteException exc) { + LOG.error("Could not export or bind with RMI registry", exc); + return; + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + try { + LOG.debug("Unbinding from RMI registry..."); + rmiRegistry.unbind(RegistrationName.REMOTE_VEHICLE_SERVICE); + LOG.debug("Unexporting RMI interface..."); + UnicastRemoteObject.unexportObject(this, true); + } + catch (RemoteException | NotBoundException exc) { + LOG.warn("Exception shutting down RMI interface", exc); + } + + initialized = false; + } + + @Override + public void attachCommAdapter( + ClientID clientId, + TCSObjectReference ref, + VehicleCommAdapterDescription description + ) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_VEHICLES); + + try { + kernelExecutor.submit(() -> vehicleService.attachCommAdapter(ref, description)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void disableCommAdapter(ClientID clientId, TCSObjectReference ref) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_VEHICLES); + + try { + kernelExecutor.submit(() -> vehicleService.disableCommAdapter(ref)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void enableCommAdapter(ClientID clientId, TCSObjectReference ref) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_VEHICLES); + + try { + kernelExecutor.submit(() -> vehicleService.enableCommAdapter(ref)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public VehicleAttachmentInformation fetchAttachmentInformation( + ClientID clientId, + TCSObjectReference ref + ) { + userManager.verifyCredentials(clientId, UserPermission.READ_DATA); + + return vehicleService.fetchAttachmentInformation(ref); + } + + @Override + public VehicleProcessModelTO fetchProcessModel( + ClientID clientId, + TCSObjectReference ref + ) { + userManager.verifyCredentials(clientId, UserPermission.READ_DATA); + + return vehicleService.fetchProcessModel(ref); + } + + @Override + public void sendCommAdapterCommand( + ClientID clientId, + TCSObjectReference ref, + AdapterCommand command + ) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_VEHICLES); + + try { + kernelExecutor.submit(() -> vehicleService.sendCommAdapterCommand(ref, command)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void sendCommAdapterMessage( + ClientID clientId, + TCSObjectReference vehicleRef, + Object message + ) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_VEHICLES); + + try { + kernelExecutor.submit(() -> vehicleService.sendCommAdapterMessage(vehicleRef, message)).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void updateVehicleIntegrationLevel( + ClientID clientId, + TCSObjectReference ref, + Vehicle.IntegrationLevel integrationLevel + ) + throws RemoteException { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_VEHICLES); + + try { + kernelExecutor.submit( + () -> vehicleService.updateVehicleIntegrationLevel(ref, integrationLevel) + ).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void updateVehiclePaused( + ClientID clientId, + TCSObjectReference ref, + boolean paused + ) + throws RemoteException { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_VEHICLES); + + try { + kernelExecutor.submit( + () -> vehicleService.updateVehiclePaused(ref, paused) + ).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void updateVehicleEnergyLevelThresholdSet( + ClientID clientId, + TCSObjectReference ref, + EnergyLevelThresholdSet energyLevelThresholdSet + ) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_VEHICLES); + + try { + kernelExecutor.submit( + () -> vehicleService.updateVehicleEnergyLevelThresholdSet(ref, energyLevelThresholdSet) + ) + .get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void updateVehicleAllowedOrderTypes( + ClientID clientId, TCSObjectReference ref, + Set allowedOrderTypes + ) { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_VEHICLES); + + try { + kernelExecutor.submit( + () -> vehicleService.updateVehicleAllowedOrderTypes(ref, allowedOrderTypes) + ) + .get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } + + @Override + public void updateVehicleEnvelopeKey( + ClientID clientId, + TCSObjectReference ref, + String envelopeKey + ) + throws RemoteException { + userManager.verifyCredentials(clientId, UserPermission.MODIFY_VEHICLES); + + try { + kernelExecutor.submit( + () -> vehicleService.updateVehicleEnvelopeKey(ref, envelopeKey) + ).get(); + } + catch (InterruptedException | ExecutionException exc) { + throw findSuitableExceptionFor(exc); + } + } +} diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/UserAccount.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/UserAccount.java new file mode 100644 index 0000000..5ab4daf --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/UserAccount.java @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import static java.util.Objects.requireNonNull; + +import java.io.Serializable; +import java.util.Set; + +/** + * Instances of this class store user account data, including name, password + * and granted permissions of the user. + */ +public class UserAccount + implements + Serializable { + + /** + * The user's name. + */ + private final String userName; + /** + * The user's password. + */ + private String password; + /** + * The user's permissions. + */ + private Set permissions; + + /** + * Creates a new instance of UserAccount. + * + * @param userName The user's name. + * @param password The user's password. + * @param perms The user's permissions. + */ + public UserAccount(String userName, String password, Set perms) { + this.userName = requireNonNull(userName, "userName"); + this.password = requireNonNull(password, "password"); + this.permissions = requireNonNull(perms, "perms"); + } + + /** + * Return the user's name. + * + * @return The user's name. + */ + public String getUserName() { + return userName; + } + + /** + * Return the user's password. + * + * @return The user's password. + */ + public String getPassword() { + return password; + } + + /** + * Set the user's password. + * + * @param pass The user's password. + */ + public void setPassword(String pass) { + password = pass; + } + + /** + * Returns the user's permissions. + * + * @return The user's permissions. + */ + public Set getPermissions() { + return permissions; + } + + /** + * Set the user's permissions. + * + * @param permissions The user's new permissions. + */ + public void setPermissions(Set permissions) { + this.permissions = requireNonNull(permissions, "permissions"); + } +} diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/UserAccountProvider.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/UserAccountProvider.java new file mode 100644 index 0000000..a8c4e93 --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/UserAccountProvider.java @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import java.util.Set; + +/** + * Provides user account data. + */ +public interface UserAccountProvider { + + Set getUserAccounts(); +} diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/UserManager.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/UserManager.java new file mode 100644 index 0000000..61ae620 --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/UserManager.java @@ -0,0 +1,434 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; +import static org.opentcs.util.Assertions.checkInRange; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import java.io.File; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import org.opentcs.access.CredentialsException; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.components.Lifecycle; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.customizations.ApplicationHome; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.event.EventSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages users allowed to connect/operate with the kernel and authenticated clients. + */ +public class UserManager + implements + EventHandler, + Lifecycle { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(UserManager.class); + /** + * Where we register for application events. + */ + private final EventSource eventSource; + /** + * The kernel's executor. + */ + private final ScheduledExecutorService kernelExecutor; + /** + * Provides configuration data. + */ + private final RmiKernelInterfaceConfiguration configuration; + /** + * Provides user account data. + */ + private final UserAccountProvider userAccountProvider; + /** + * The directory of users allowed to connect/operate with the kernel. + */ + private final Map knownUsers = new HashMap<>(); + /** + * The directory of authenticated clients (a mapping of ClientIDs to user names). + */ + private final Map knownClients = new HashMap<>(); + /** + * A handle for the task that periodically cleans up known clients and event buffers. + */ + private ScheduledFuture cleanerTaskFuture; + /** + * Whether this kernel extension is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param homeDirectory The kernel's home directory (for saving user account data). Will be + * created if it doesn't exist, yet. + * @param eventSource Where this instance registers for application events. + * @param kernelExecutor The kernel's executor. + * @param configuration This class' configuration. + * @param userAccountProvider Provides user account data. + */ + @Inject + public UserManager( + @ApplicationHome + File homeDirectory, + @ApplicationEventBus + EventSource eventSource, + @KernelExecutor + ScheduledExecutorService kernelExecutor, + RmiKernelInterfaceConfiguration configuration, + UserAccountProvider userAccountProvider + ) { + requireNonNull(homeDirectory, "homeDirectory"); + this.eventSource = requireNonNull(eventSource, "eventSource"); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + this.configuration = requireNonNull(configuration, "configuration"); + this.userAccountProvider = requireNonNull(userAccountProvider, "userAccountProvider"); + } + + @Override + public void initialize() { + if (isInitialized()) { + LOG.debug("Already initialized."); + return; + } + + // Register the user manager as an event listener so that the user manager can collect events + // and pass them to known clients polling events. + eventSource.subscribe(this); + + knownUsers.clear(); + for (UserAccount curAccount : userAccountProvider.getUserAccounts()) { + knownUsers.put(curAccount.getUserName(), curAccount); + } + + // Start the thread that periodically cleans up the list of known clients and event buffers. + LOG.debug("Starting cleaner task..."); + cleanerTaskFuture = kernelExecutor.scheduleWithFixedDelay( + new ClientCleanerTask(), + configuration.clientSweepInterval(), + configuration.clientSweepInterval(), + TimeUnit.MILLISECONDS + ); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + LOG.debug("Not initialized."); + return; + } + + LOG.debug("Terminating cleaner task..."); + cleanerTaskFuture.cancel(false); + cleanerTaskFuture = null; + + knownUsers.clear(); + + eventSource.unsubscribe(this); + + initialized = false; + } + + @Override + public void onEvent(Object event) { + // Forward the event to all clients' event buffers. + synchronized (knownClients) { + for (ClientEntry curEntry : knownClients.values()) { + curEntry.getEventBuffer().onEvent(event); + } + } + } + + /** + * Returns the directory of users allowed to connect/operate with the kernel. + * + * @return The directory of users allowed to connect/operate with the kernel. + */ + public Map getKnownUsers() { + return Collections.unmodifiableMap(knownUsers); + } + + /** + * Returns the directory of authenticated clients (a mapping of ClientIDs to user names). + * + * @return The directory of authenticated clients (a mapping of ClientIDs to user names). + */ + public Map getKnownClients() { + return Collections.unmodifiableMap(knownClients); + } + + /** + * Returns the {@link UserAccount} for the given user name. + * + * @param userName The user name to get the user account for. + * @return The user account or {@code null}, if there isn't an account associated to the given + * user name. + */ + @Nullable + public UserAccount getUser(String userName) { + return knownUsers.get(userName); + } + + /** + * Returns the {@link ClientEntry} for the given client id. + * + * @param clientID The client id to get the client entry for. + * @return The client entry or {@code null}, if there isn't an entry associated to the given + * client id. + */ + @Nullable + public ClientEntry getClient(ClientID clientID) { + return knownClients.get(clientID); + } + + /** + * Adds a new ClientEntry to the map of all known and authenticated clients. + * + * @param clientID The client id to identify the given ClientEntry. + * @param clientEntry The ClientEntry object to be registered. + */ + public void registerClient( + @Nonnull + ClientID clientID, + @Nonnull + ClientEntry clientEntry + ) { + requireNonNull(clientID, "clientID"); + requireNonNull(clientEntry, "clientEntry"); + + synchronized (knownClients) { + if (isClientRegistered(clientID)) { + return; + } + knownClients.put(clientID, clientEntry); + } + } + + /** + * Removes the given client from the map of known clients. + * + * @param clientID The client id to be removed. + */ + public void unregisterClient( + @Nonnull + ClientID clientID + ) { + requireNonNull(clientID, "clientID"); + + synchronized (knownClients) { + knownClients.remove(clientID); + } + } + + public List pollEvents(ClientID clientID, long timeout) { + requireNonNull(clientID, "clientID"); + checkInRange(timeout, 0, Long.MAX_VALUE, "timeout"); + + ClientEntry clientEntry; + EventBuffer eventBuffer; + synchronized (knownClients) { + clientEntry = getClient(clientID); + checkArgument(clientEntry != null, "Unknown client ID: %s", clientID); + eventBuffer = clientEntry.getEventBuffer(); + } + // Get events or wait for one to arrive if none is currently there. + List events = eventBuffer.getEvents(timeout); + // Set the client's 'alive' flag. + synchronized (knownClients) { + clientEntry.setAlive(true); + } + return events; + } + + /** + * Check whether the user described by the given credentials is granted permissions according to + * the specified user role. + *

+ * This method also sets the 'alive' flag of the client's entry to prevent it from being removed + * by the cleaner thread. + *

+ * + * @param clientID The client's identification object. + * @param requiredPermission The required role/permission. + * @return true if, and only if, the given client ID exists and the client has the + * given permission. + */ + private boolean checkCredentialsForRole(ClientID clientID, UserPermission requiredPermission) { + requireNonNull(clientID, "clientID"); + requireNonNull(requiredPermission, "requiredPermission"); + + synchronized (knownClients) { + ClientEntry clientEntry = getClient(clientID); + // Check if the client is known. + if (clientEntry == null) { + return false; + } + // Set the 'alive' flag for the cleaning thread. + clientEntry.setAlive(true); + // Check if the user's permissions are sufficient. + Set providedPerms = clientEntry.getPermissions(); + if (!providedPerms.contains(requiredPermission)) { + return false; + } + } + return true; + } + + /** + * Ensures the given client has the required permissions. + * + * @param clientID The client's identification object. + * @param requiredPermission The required role/permission. + * @throws CredentialsException If the client's permissions are insufficient. + */ + public void verifyCredentials(ClientID clientID, UserPermission requiredPermission) + throws CredentialsException { + requireNonNull(clientID, "clientID"); + requireNonNull(requiredPermission, "requiredPermission"); + + if (!checkCredentialsForRole(clientID, requiredPermission)) { + throw new CredentialsException("Client permissions insufficient."); + } + } + + private boolean isClientRegistered( + @Nonnull + ClientID clientID + ) { + return knownClients.containsKey(clientID); + } + + /** + * Instances of this class are used as containers for data kept about known clients. + */ + public static final class ClientEntry { + + /** + * The name of the user that connected with the client. + */ + private final String userName; + /** + * The client's permissions/privilege level. + */ + private final Set permissions; + /** + * The client's event buffer. + */ + private final EventBuffer eventBuffer = new EventBuffer(event -> false); + /** + * The client's alive flag. + */ + private boolean alive = true; + + /** + * Creates a new ClientEntry. + * + * @param name The client's name. + * @param perms The client's permissions. + */ + public ClientEntry(String name, Set perms) { + userName = requireNonNull(name, "name"); + permissions = requireNonNull(perms, "perms"); + } + + /** + * Checks whether the client has been seen since the last sweep of the cleaner task. + * + * @return true if, and only if, the client has been seen recently. + */ + public boolean isAlive() { + return alive; + } + + /** + * Sets this client's alive flag. + * + * @param isAlive The client's new alive flag. + */ + public void setAlive(boolean isAlive) { + alive = isAlive; + } + + public String getUserName() { + return userName; + } + + public EventBuffer getEventBuffer() { + return eventBuffer; + } + + public Set getPermissions() { + return permissions; + } + } + + /** + * A task for cleaning out stale client entries. + */ + private class ClientCleanerTask + implements + Runnable { + + /** + * Creates a new instance. + */ + private ClientCleanerTask() { + } + + @Override + public void run() { + LOG.debug("Sweeping client entries..."); + synchronized (knownClients) { + Iterator> clientIter = knownClients.entrySet().iterator(); + while (clientIter.hasNext()) { + Map.Entry curEntry = clientIter.next(); + ClientEntry clientEntry = curEntry.getValue(); + // Only touch the entry if the buffer not currently in use by a + // client. + if (!clientEntry.getEventBuffer().hasWaitingClient()) { + // If the client has been seen since the last run, reset the + // 'alive' flag. + if (clientEntry.isAlive()) { + clientEntry.setAlive(false); + } + // If the client hasn't been seen since the last run, remove its + // ID from the list of known clients - the client has been + // inactive for long enough. + else { + LOG.debug( + "Removing inactive client entry (client user: {})", + clientEntry.getUserName() + ); + clientIter.remove(); + } + } + } + } + } + } +} diff --git a/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/UserPermission.java b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/UserPermission.java new file mode 100644 index 0000000..d59e658 --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/main/java/org/opentcs/kernel/extensions/rmi/UserPermission.java @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +/** + * Defines the possible permission flags of kernel clients. + */ +public enum UserPermission { + + /** + * Indicates the client may retrieve any data from the kernel. + */ + READ_DATA, + /** + * Indicates the client may change the kernel's state. + */ + CHANGE_KERNEL_STATE, + /** + * Indicates the client may change the kernel's configuration items. + */ + CHANGE_CONFIGURATION, + /** + * Indicates the client may load another model. + */ + LOAD_MODEL, + /** + * Indicates the client may save the current model (under any name). + */ + SAVE_MODEL, + /** + * Indicates the client may modify any data of the current model. + */ + MODIFY_MODEL, + /** + * Indicates the client may add or remove temporary path locks. + */ + LOCK_PATH, + /** + * Indicates the client may move/place vehicles and modify their states + * explicitly. + */ + MODIFY_VEHICLES, + /** + * Indicates the client may create/modify transport orders. + */ + MODIFY_ORDER, + /** + * Indicates the client may modify peripheral states. + */ + MODIFY_PERIPHERALS, + /** + * Indicates the client may create/modify peripheral jobs. + */ + MODIFY_PERIPHERAL_JOBS, + /** + * Indicates the client may publish messages via the kernel. + */ + PUBLISH_MESSAGES +} diff --git a/opentcs-kernel-extension-rmi-services/src/test/java/org/opentcs/kernel/extensions/rmi/EventBufferTest.java b/opentcs-kernel-extension-rmi-services/src/test/java/org/opentcs/kernel/extensions/rmi/EventBufferTest.java new file mode 100644 index 0000000..d4d2e01 --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/test/java/org/opentcs/kernel/extensions/rmi/EventBufferTest.java @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.model.BoundingBox; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; + +/** + * Unit tests for {@link EventBuffer}. + */ +class EventBufferTest { + + private EventBuffer eventBuffer; + + @BeforeEach + void setUp() { + eventBuffer = new EventBuffer(event -> true); + } + + @Test + void checkGetEventsShouldReturnCorrectAmountOfEvents() { + eventBuffer.onEvent(new Object()); + eventBuffer.onEvent(new Object()); + eventBuffer.onEvent(new Object()); + + assertThat(eventBuffer.getEvents(0), hasSize(3)); + } + + @Test + void checkGetEventsShouldReturnEmptyList() { + eventBuffer.onEvent(new Object()); + eventBuffer.onEvent(new Object()); + eventBuffer.onEvent(new Object()); + + assertThat(eventBuffer.getEvents(0), hasSize(3)); + assertThat(eventBuffer.getEvents(0), is(empty())); + } + + @Test + void checkSetEventFilterShouldChangeEventFilter() { + eventBuffer.setEventFilter(i -> false); + + eventBuffer.onEvent(new Object()); + eventBuffer.onEvent(new Object()); + eventBuffer.onEvent(new Object()); + + assertThat(eventBuffer.getEvents(0), is(empty())); + } + + @Test + void checkGetEventsShouldWorkWhenTimeoutGreaterThanZero() { + eventBuffer.onEvent(new Object()); + + assertThat(eventBuffer.getEvents(1000), hasSize(1)); + assertFalse(eventBuffer.hasWaitingClient()); + } + + @Test + void aggregateConsecutiveTcsObjectEventsForSameObjects() { + Point point = new Point("point"); + Point pointA = point.withType(Point.Type.PARK_POSITION); + Point pointB = pointA.withProperty("some-key", "some-value"); + Point pointC = pointB.withProperty("some-other-key", "some-other-value"); + TCSObjectEvent event1 = new TCSObjectEvent( + pointA, + point, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + TCSObjectEvent event5 = new TCSObjectEvent( + pointB, + pointA, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + TCSObjectEvent event6 = new TCSObjectEvent( + pointC, + pointB, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + + Vehicle vehicle = new Vehicle("vehicle"); + Vehicle vehicleA = vehicle.withEnergyLevel(42); + Vehicle vehicleB = vehicleA.withIntegrationLevel(Vehicle.IntegrationLevel.TO_BE_UTILIZED); + Vehicle vehicleC = vehicleB.withBoundingBox(new BoundingBox(1382, 1000, 1000)); + + TCSObjectEvent event2 = new TCSObjectEvent( + vehicleA, + vehicle, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + TCSObjectEvent event3 = new TCSObjectEvent( + vehicleB, + vehicleA, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + TCSObjectEvent event4 = new TCSObjectEvent( + vehicleC, + vehicleB, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + + eventBuffer.onEvent(event1); + eventBuffer.onEvent(event2); + eventBuffer.onEvent(event3); + eventBuffer.onEvent(event4); + eventBuffer.onEvent(event5); + eventBuffer.onEvent(event6); + + List result = eventBuffer.getEvents(0); + assertThat(result, hasSize(3)); + assertThat(result.get(0), is(theInstance(event1))); + + assertThat( + ((TCSObjectEvent) result.get(1)).getPreviousObjectState(), + is(theInstance(vehicle)) + ); + assertThat( + ((TCSObjectEvent) result.get(1)).getCurrentObjectState(), + is(theInstance(vehicleC)) + ); + + assertThat( + ((TCSObjectEvent) result.get(2)).getPreviousObjectState(), + is(theInstance(pointA)) + ); + assertThat( + ((TCSObjectEvent) result.get(2)).getCurrentObjectState(), + is(theInstance(pointC)) + ); + } + + @Test + void dontAggregateEventsOfTypeCreateOrRemoved() { + Vehicle vehicle = new Vehicle("vehicle"); + Vehicle vehicleA = vehicle.withEnergyLevel(42); + + TCSObjectEvent event1 = new TCSObjectEvent( + vehicle, + null, + TCSObjectEvent.Type.OBJECT_CREATED + ); + TCSObjectEvent event2 = new TCSObjectEvent( + vehicleA, + vehicle, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + TCSObjectEvent event3 = new TCSObjectEvent( + null, + vehicleA, + TCSObjectEvent.Type.OBJECT_REMOVED + ); + + eventBuffer.onEvent(event1); + eventBuffer.onEvent(event2); + eventBuffer.onEvent(event3); + + List result = eventBuffer.getEvents(0); + assertThat(result, hasSize(3)); + assertThat(result.get(0), is(equalTo(event1))); + assertThat(result.get(1), is(equalTo(event2))); + assertThat(result.get(2), is(equalTo(event3))); + } +} diff --git a/opentcs-kernel-extension-rmi-services/src/test/java/org/opentcs/kernel/extensions/rmi/UserManagerTest.java b/opentcs-kernel-extension-rmi-services/src/test/java/org/opentcs/kernel/extensions/rmi/UserManagerTest.java new file mode 100644 index 0000000..b8d55fc --- /dev/null +++ b/opentcs-kernel-extension-rmi-services/src/test/java/org/opentcs/kernel/extensions/rmi/UserManagerTest.java @@ -0,0 +1,183 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.rmi; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.anEmptyMap; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +import java.io.File; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.access.CredentialsException; +import org.opentcs.access.rmi.ClientID; +import org.opentcs.util.event.EventSource; + +/** + * Unit tests for {@link UserManager}. + */ +class UserManagerTest { + + private File homedirectory; + private EventSource eventSource; + private ScheduledExecutorService kernelExecutor; + private RmiKernelInterfaceConfiguration configuration; + private UserAccountProvider userAccountProvider; + private UserAccount account1; + private UserManager.ClientEntry client1; + private ClientID id1; + private UserManager manager; + + @BeforeEach + void setUp() { + homedirectory = mock(); + eventSource = mock(); + kernelExecutor = Executors.newSingleThreadScheduledExecutor(); + configuration = mock(); + userAccountProvider = mock(); + + Set permissions = EnumSet.of(UserPermission.READ_DATA); + + account1 = new UserAccount("peter", "123", permissions); + Set userAccounts = Set.of(account1); + + client1 = new UserManager.ClientEntry("auto", permissions); + id1 = new ClientID("auto"); + + given(userAccountProvider.getUserAccounts()) + .willReturn(userAccounts); + + given(configuration.clientSweepInterval()) + .willReturn(1000L); + + manager = new UserManager( + homedirectory, + eventSource, + kernelExecutor, + configuration, + userAccountProvider + ); + manager.initialize(); + } + + @AfterEach + void tearDown() { + kernelExecutor.shutdown(); + } + + @Test + void testIfUserManagerIsInitialized() { + assertThat(manager.isInitialized(), is(true)); + } + + @Test + void testIfCleanerTaskIsTerminated() { + manager.terminate(); + + assertThat(manager.isInitialized(), is(false)); + then(eventSource).should().unsubscribe(manager); + } + + @Test + void checkGetKnownUsers() { + assertThat(manager.getKnownUsers(), is(aMapWithSize(1))); + assertThat(manager.getKnownUsers(), hasEntry("peter", account1)); + } + + @Test + void checkGetUserShouldReturnRightUser() { + assertThat(manager.getUser("peter"), is(account1)); + } + + @Test + void checkGetUserShouldReturnNullForUnknownUser() { + assertNull(manager.getUser("marie")); + } + + @Test + void checkGetClientShouldReturnRightClient() { + manager.registerClient(id1, client1); + + assertThat(manager.getClient(id1), is(client1)); + } + + @Test + void checkGetClientShouldReturnNull() { + manager.registerClient(id1, client1); + + assertNull(manager.getClient(new ClientID("jet"))); + } + + @Test + void checkIfPollEventsReturnsTheCorrectEventList() { + manager.registerClient(id1, client1); + + client1.getEventBuffer().setEventFilter(event -> true); + + Object event1 = new Object(); + manager.onEvent(event1); + + List eventList = manager.pollEvents(id1, 0); + + assertThat(eventList, hasSize(1)); + assertThat(eventList, contains(event1)); + } + + @Test + void checkVerifyCredentialsShouldThrowExceptionIfClientHasNoPermission() { + manager.registerClient(id1, client1); + assertThrows( + CredentialsException.class, + () -> manager.verifyCredentials(id1, UserPermission.SAVE_MODEL) + ); + } + + @Test + void checkVerifyCredentialsShouldThrowExceptionIfClientDoesNotExist() { + assertThrows( + CredentialsException.class, + () -> manager.verifyCredentials( + new ClientID("unknown-client"), + UserPermission.SAVE_MODEL + ) + ); + } + + @Test + void checkVerifyCredentialsShouldThrowNoException() { + manager.registerClient(id1, client1); + assertDoesNotThrow( + () -> manager.verifyCredentials(id1, UserPermission.READ_DATA) + ); + } + + @Test + void checkRegisterClient() { + manager.registerClient(id1, client1); + assertThat(manager.getKnownClients(), is(aMapWithSize(1))); + assertThat(manager.getKnownClients(), hasEntry(id1, client1)); + } + + @Test + void checkUnregisterClient() { + manager.registerClient(id1, client1); + manager.unregisterClient(id1); + assertThat(manager.getKnownClients(), is(anEmptyMap())); + } +} diff --git a/opentcs-kernel/build.gradle b/opentcs-kernel/build.gradle new file mode 100644 index 0000000..4b7f08c --- /dev/null +++ b/opentcs-kernel/build.gradle @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply from: "${rootDir}/gradle/java-project.gradle" +apply from: "${rootDir}/gradle/java-codequality.gradle" +apply from: "${rootDir}/gradle/guice-application.gradle" +apply from: "${rootDir}/gradle/publishing-java.gradle" + +if (!hasProperty('mainClass')) { + ext.mainClass = 'org.opentcs.kernel.RunKernel' +} +application.mainClass = ext.mainClass + +ext.collectableDistDir = new File(buildDir, 'install') + +dependencies { + api project(':opentcs-api-injection') + api project(':opentcs-common') + api project(':opentcs-commadapter-loopback') + api project(':opentcs-peripheralcommadapter-loopback') + api project(':opentcs-strategies-default') + api project(':opentcs-impl-configuration-gestalt') + api project(':opentcs-kernel-extension-http-services') + api project(':opentcs-kernel-extension-rmi-services') + + implementation group: 'de.huxhorn.sulky', name: 'de.huxhorn.sulky.ulid', version: '8.3.0' + + runtimeOnly group: 'org.slf4j', name: 'slf4j-jdk14', version: '2.0.16' +} + +distributions { + main { + contents { + from "${sourceSets.main.resources.srcDirs[0]}/org/opentcs/kernel/distribution" + } + } +} + +// For now, we're using hand-crafted start scripts, so disable the application +// plugin's start script generation. +startScripts.enabled = false + +distTar.enabled = false + +task release { + dependsOn build + dependsOn installDist +} + +run { + systemProperties(['java.util.logging.config.file':'./config/logging.config',\ + 'opentcs.base':'.',\ + 'opentcs.home':'.',\ + 'opentcs.configuration.reload.interval':'10000',\ + 'opentcs.configuration.provider':'gestalt']) + jvmArgs('-XX:-OmitStackTraceInFastThrow') +} diff --git a/opentcs-kernel/gradle.properties b/opentcs-kernel/gradle.properties new file mode 100644 index 0000000..14431ac --- /dev/null +++ b/opentcs-kernel/gradle.properties @@ -0,0 +1,40 @@ +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineMethodParams=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAfterDotInChainedMethodCalls=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineDisjunctiveCatchTypes=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineFor=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineImplements=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.sortMembersByVisibility=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.visibilityOrder=PUBLIC;PROTECTED;DEFAULT;PRIVATE +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.enable-indent=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineArrayInit=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineCallArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.keepGettersAndSettersTogether=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classMembersOrder=STATIC FIELD;FIELD;STATIC_INIT;CONSTRUCTOR;INSTANCE_INIT;STATIC METHOD;METHOD;STATIC CLASS;CLASS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapCommentText=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.importGroupsOrder=* +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize=4 +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineAnnotationArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineTryResources=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.preserveNewLinesInComments=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineParenthesized=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineThrows=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=100 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project diff --git a/opentcs-kernel/src/dist/bin/splash-image.gif b/opentcs-kernel/src/dist/bin/splash-image.gif new file mode 100644 index 0000000..9e6f131 Binary files /dev/null and b/opentcs-kernel/src/dist/bin/splash-image.gif differ diff --git a/opentcs-kernel/src/dist/bin/splash-image.gif.license b/opentcs-kernel/src/dist/bin/splash-image.gif.license new file mode 100644 index 0000000..777faa6 --- /dev/null +++ b/opentcs-kernel/src/dist/bin/splash-image.gif.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 diff --git a/opentcs-kernel/src/dist/config/logging.config b/opentcs-kernel/src/dist/config/logging.config new file mode 100644 index 0000000..3fdf2da --- /dev/null +++ b/opentcs-kernel/src/dist/config/logging.config @@ -0,0 +1,70 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +############################################################ +# Default Logging Configuration File +# +# You can use a different file by specifying a filename +# with the java.util.logging.config.file system property. +# For example java -Djava.util.logging.config.file=myfile +############################################################ + +############################################################ +# Global properties +############################################################ + +# "handlers" specifies a comma separated list of log Handler +# classes. These handlers will be installed during VM startup. +# Note that these classes must be on the system classpath. +# By default we only configure a ConsoleHandler, which will only +# show messages at the INFO and above levels. +#handlers= java.util.logging.ConsoleHandler + +# To also add the FileHandler, use the following line instead. +handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler + +# Default global logging level. +# This specifies which kinds of events are logged across +# all loggers. For any given facility this global level +# can be overriden by a facility specific level +# Note that the ConsoleHandler also has a separate level +# setting to limit messages printed to the console. +.level= INFO + +############################################################ +# Handler specific properties. +# Describes specific configuration info for Handlers. +############################################################ + +# default file output is in user's home directory. +java.util.logging.FileHandler.pattern = ./log/opentcs-kernel.%g.log +java.util.logging.FileHandler.limit = 500000 +java.util.logging.FileHandler.count = 10 +#java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.FileHandler.formatter = org.opentcs.util.logging.SingleLineFormatter +java.util.logging.FileHandler.append = true +java.util.logging.FileHandler.level = FINE + +# Limit the message that are printed on the console to INFO and above. +java.util.logging.ConsoleHandler.level = FINE +#java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.ConsoleHandler.formatter = org.opentcs.util.logging.SingleLineFormatter + +# Our own handler for the GUI: +#org.opentcs.kernel.controlcenter.ControlCenterInfoHandler.level = WARNING + + +############################################################ +# Facility specific properties. +# Provides extra control for each logger. +############################################################ + +# For example, set the com.xyz.foo logger to only log SEVERE +# messages: +#com.xyz.foo.level = SEVERE + +# Logging configuration for single classes. Remember that you might also have to +# adjust handler levels! + +#org.opentcs.strategies.basic.dispatching.DefaultDispatcher.level = FINE +#org.opentcs.kernel.KernelStateOperating.level = FINE \ No newline at end of file diff --git a/opentcs-kernel/src/dist/config/opentcs-kernel.properties b/opentcs-kernel/src/dist/config/opentcs-kernel.properties new file mode 100644 index 0000000..777faa6 --- /dev/null +++ b/opentcs-kernel/src/dist/config/opentcs-kernel.properties @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 diff --git a/opentcs-kernel/src/dist/generateKeystores.bat b/opentcs-kernel/src/dist/generateKeystores.bat new file mode 100644 index 0000000..2eb0d4b --- /dev/null +++ b/opentcs-kernel/src/dist/generateKeystores.bat @@ -0,0 +1,62 @@ +@echo off +rem SPDX-FileCopyrightText: The openTCS Authors +rem SPDX-License-Identifier: MIT +rem +rem Start SSL keystore and truststore generation. +rem + +rem Don't export variables to the parent shell. +setlocal + +rem Set the path to where keytool is located. Keytool comes with the Java JRE. +set KEYTOOL_PATH="keytool" + +rem Try to execute keytool +%KEYTOOL_PATH% 2>nul +if %ERRORLEVEL% neq 0 ( + if %KEYTOOL_PATH% equ "keytool" ( + echo Error: Could not find keytool in PATH. + ) else ( + echo Error: Could not find keytool in %KEYTOOL_PATH%. + ) + exit /B %ERRORLEVEL% +) + +rem Set base directory names. +set OPENTCS_HOME=. +set OPENTCS_CONFIGDIR=%OPENTCS_HOME%\config +set OUTPUTDIR=%OPENTCS_CONFIGDIR% + +rem Set paths to generate files at. +set KEYSTORE_FILEPATH=%OUTPUTDIR%\keystore.p12 +set TRUSTSTORE_FILEPATH=%OUTPUTDIR%\truststore.p12 +set CERTIFICATE_FILEPATH=%OUTPUTDIR%\certificate.cer + +rem Set the password used for generating the stores. +set PASSWORD=password + +echo Deleting previously generated keystore and truststore... +del %KEYSTORE_FILEPATH% 2>nul +del %TRUSTSTORE_FILEPATH% 2>nul +del %CERTIFICATE_FILEPATH% 2>nul + +rem Generates a keypair wrapped in a self-signed (X.509) certificate. +rem Some defaults of the -genkeypair command: -alias "mykey" -keyalg "DSA" -keysize 1024 -validity 90 +echo Generating a new keystore in %KEYSTORE_FILEPATH%... +%KEYTOOL_PATH% -genkeypair -alias openTCS -keyalg RSA -dname "c=DE" -storepass %PASSWORD% -keypass %PASSWORD% -validity 365 -storetype PKCS12 -keystore %KEYSTORE_FILEPATH% + +rem Exports the (wrapping) self-signed certificate from the generated keypair. +rem '-rfc' - Output the certificate in a printable encoding format (by default the -export command outputs a certificate in binary encoding) +rem '2>nul' - Suppress output of this command +%KEYTOOL_PATH% -exportcert -alias openTCS -file %CERTIFICATE_FILEPATH% -keystore %KEYSTORE_FILEPATH% -storepass %PASSWORD% -rfc 2>nul + +rem Adds the exported certificate to a new keystore and its trusted certificates. +echo Generating a new truststore in %TRUSTSTORE_FILEPATH%... +%KEYTOOL_PATH% -importcert -alias openTCS -file %CERTIFICATE_FILEPATH% -storepass %PASSWORD% -storetype PKCS12 -keystore %TRUSTSTORE_FILEPATH% -noprompt 2>nul + +rem Delete the exported certificate since it's not really needed. +del %CERTIFICATE_FILEPATH% + +echo Copy the generated truststore to the openTCS PlantOverview's \config folder or a corresponding location of your application. + +pause diff --git a/opentcs-kernel/src/dist/generateKeystores.sh b/opentcs-kernel/src/dist/generateKeystores.sh new file mode 100644 index 0000000..a12b542 --- /dev/null +++ b/opentcs-kernel/src/dist/generateKeystores.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT +# +# Start SSL keystore and truststore generation. +# + +# Set the path to where keytool is located. Keytool comes with the Java JRE. +export KEYTOOL_PATH="keytool" + +# Try to execute keytool +${KEYTOOL_PATH} 2>/dev/null +if [ $? -ne 0 ]; then + if [ "${KEYTOOL_PATH}" = "keytool" ]; then + echo Error: Could not find keytool in PATH. + else + echo Error: Could not find keytool in ${KEYTOOL_PATH}. + fi + exit $? +fi + +# Set base directory names. +export OPENTCS_HOME=. +export OPENTCS_CONFIGDIR="${OPENTCS_HOME}/config" +export OUTPUTDIR="${OPENTCS_CONFIGDIR}" + +# Set paths to generate files at. +export KEYSTORE_FILEPATH="${OUTPUTDIR}/keystore.p12" +export TRUSTSTORE_FILEPATH="${OUTPUTDIR}/truststore.p12" +export CERTIFICATE_FILEPATH="${OUTPUTDIR}/certificate.cer" + +# Set the password used for generating the stores. +export PASSWORD=password + +echo Deleting previously generated keystore and truststore... +rm ${KEYSTORE_FILEPATH} 2>/dev/null +rm ${TRUSTSTORE_FILEPATH} 2>/dev/null +rm ${CERTIFICATE_FILEPATH} 2>/dev/null + +# Generates a keypair wrapped in a self-signed (X.509) certificate. +# Some defaults of the -genkeypair command: -alias "mykey" -keyalg "DSA" -keysize 1024 -validity 90 +echo Generating a new keystore in ${KEYSTORE_FILEPATH}... +${KEYTOOL_PATH} -genkeypair -alias openTCS -keyalg RSA -dname "c=DE" -storepass ${PASSWORD} -keypass ${PASSWORD} -validity 365 -storetype PKCS12 -keystore ${KEYSTORE_FILEPATH} + +# Exports the (wrapping) self-signed certificate from the generated keypair. +# '-rfc' - Output the certificate in a printable encoding format (by default the -export command outputs a certificate in binary encoding) +# '2>nul' - Suppress output of this command +${KEYTOOL_PATH} -exportcert -alias openTCS -file ${CERTIFICATE_FILEPATH} -keystore ${KEYSTORE_FILEPATH} -storepass ${PASSWORD} -rfc 2>/dev/null + +# Adds the exported certificate to a new keystore and its trusted certificates. +echo Generating a new truststore in ${TRUSTSTORE_FILEPATH}... +${KEYTOOL_PATH} -importcert -alias openTCS -file ${CERTIFICATE_FILEPATH} -storepass ${PASSWORD} -storetype PKCS12 -keystore ${TRUSTSTORE_FILEPATH} -noprompt 2>/dev/null + +# Delete the exported certificate since it's not really needed. +rm ${CERTIFICATE_FILEPATH} + +echo Copy the generated truststore to the openTCS PlantOverview\'s /config folder or a corresponding location of your application. diff --git a/opentcs-kernel/src/dist/lib/openTCS-extensions/.keepme b/opentcs-kernel/src/dist/lib/openTCS-extensions/.keepme new file mode 100644 index 0000000..e69de29 diff --git a/opentcs-kernel/src/dist/log/statistics/.keepme b/opentcs-kernel/src/dist/log/statistics/.keepme new file mode 100644 index 0000000..e69de29 diff --git a/opentcs-kernel/src/dist/shutdownKernel.bat b/opentcs-kernel/src/dist/shutdownKernel.bat new file mode 100644 index 0000000..df1b7a0 --- /dev/null +++ b/opentcs-kernel/src/dist/shutdownKernel.bat @@ -0,0 +1,20 @@ +@echo off +rem SPDX-FileCopyrightText: The openTCS Authors +rem SPDX-License-Identifier: MIT +rem +rem Shut down the openTCS kernel. +rem + +rem Don't export variables to the parent shell +setlocal + +rem Set base directory names. +set OPENTCS_BASE=. +set OPENTCS_LIBDIR=%OPENTCS_BASE%\lib + +rem Set the class path +set OPENTCS_CP=%OPENTCS_LIBDIR%\*; +set OPENTCS_CP=%OPENTCS_CP%;%OPENTCS_LIBDIR%\openTCS-extensions\*; + +java -classpath "%OPENTCS_CP%" ^ + org.opentcs.kernel.ShutdownKernel localhost 55100 diff --git a/opentcs-kernel/src/dist/shutdownKernel.sh b/opentcs-kernel/src/dist/shutdownKernel.sh new file mode 100644 index 0000000..acc9a43 --- /dev/null +++ b/opentcs-kernel/src/dist/shutdownKernel.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT +# +# Shut down the openTCS kernel. +# + +# Set base directory names. +export OPENTCS_BASE=. +export OPENTCS_LIBDIR="${OPENTCS_BASE}/lib" + +# Set the class path +export OPENTCS_CP="${OPENTCS_LIBDIR}/*" +export OPENTCS_CP="${OPENTCS_CP}:${OPENTCS_LIBDIR}/openTCS-extensions/*" + +if [ -n "${OPENTCS_JAVAVM}" ]; then + export JAVA="${OPENTCS_JAVAVM}" +else + # XXX Be a bit more clever to find out the name of the JVM runtime. + export JAVA="java" +fi + +${JAVA} -classpath "${OPENTCS_CP}" \ + org.opentcs.kernel.ShutdownKernel localhost 55100 diff --git a/opentcs-kernel/src/dist/startKernel.bat b/opentcs-kernel/src/dist/startKernel.bat new file mode 100644 index 0000000..9408b95 --- /dev/null +++ b/opentcs-kernel/src/dist/startKernel.bat @@ -0,0 +1,36 @@ +@echo off +rem SPDX-FileCopyrightText: The openTCS Authors +rem SPDX-License-Identifier: MIT +rem +rem Start the openTCS kernel. +rem + +rem Set window title +title Kernel (openTCS) + +rem Don't export variables to the parent shell +setlocal + +rem Set base directory names. +set OPENTCS_BASE=. +set OPENTCS_HOME=. +set OPENTCS_CONFIGDIR=%OPENTCS_HOME%\config +set OPENTCS_LIBDIR=%OPENTCS_BASE%\lib + +rem Set the class path +set OPENTCS_CP=%OPENTCS_LIBDIR%\*; +set OPENTCS_CP=%OPENTCS_CP%;%OPENTCS_LIBDIR%\openTCS-extensions\*; + +rem XXX Be a bit more clever to find out the name of the JVM runtime. +set JAVA=java + +rem Start kernel +%JAVA% -enableassertions ^ + -Dopentcs.base="%OPENTCS_BASE%" ^ + -Dopentcs.home="%OPENTCS_HOME%" ^ + -Dopentcs.configuration.provider=gestalt ^ + -Dopentcs.configuration.reload.interval=10000 ^ + -Djava.util.logging.config.file="%OPENTCS_CONFIGDIR%\logging.config" ^ + -XX:-OmitStackTraceInFastThrow ^ + -classpath "%OPENTCS_CP%" ^ + org.opentcs.kernel.RunKernel diff --git a/opentcs-kernel/src/dist/startKernel.sh b/opentcs-kernel/src/dist/startKernel.sh new file mode 100644 index 0000000..ebecff5 --- /dev/null +++ b/opentcs-kernel/src/dist/startKernel.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT +# +# Start the openTCS kernel. +# + +# Set base directory names. +export OPENTCS_BASE=. +export OPENTCS_HOME=. +export OPENTCS_CONFIGDIR="${OPENTCS_HOME}/config" +export OPENTCS_LIBDIR="${OPENTCS_BASE}/lib" + +# Set the class path +export OPENTCS_CP="${OPENTCS_LIBDIR}/*" +export OPENTCS_CP="${OPENTCS_CP}:${OPENTCS_LIBDIR}/openTCS-extensions/*" + +if [ -n "${OPENTCS_JAVAVM}" ]; then + export JAVA="${OPENTCS_JAVAVM}" +else + # XXX Be a bit more clever to find out the name of the JVM runtime. + export JAVA="java" +fi + +# Start kernel +${JAVA} -enableassertions \ + -Dopentcs.base="${OPENTCS_BASE}" \ + -Dopentcs.home="${OPENTCS_HOME}" \ + -Dopentcs.configuration.provider=gestalt \ + -Dopentcs.configuration.reload.interval=10000 \ + -Djava.util.logging.config.file=${OPENTCS_CONFIGDIR}/logging.config \ + -XX:-OmitStackTraceInFastThrow \ + -classpath "${OPENTCS_CP}" \ + org.opentcs.kernel.RunKernel diff --git a/opentcs-kernel/src/guiceConfig/java/org/opentcs/kernel/DefaultKernelInjectionModule.java b/opentcs-kernel/src/guiceConfig/java/org/opentcs/kernel/DefaultKernelInjectionModule.java new file mode 100644 index 0000000..82bdee2 --- /dev/null +++ b/opentcs-kernel/src/guiceConfig/java/org/opentcs/kernel/DefaultKernelInjectionModule.java @@ -0,0 +1,338 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel; + +import com.google.inject.assistedinject.FactoryModuleBuilder; +import com.google.inject.multibindings.MapBinder; +import jakarta.inject.Singleton; +import java.io.File; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import org.opentcs.access.Kernel; +import org.opentcs.access.LocalKernel; +import org.opentcs.access.SslParameterSet; +import org.opentcs.common.LoggingScheduledThreadPoolExecutor; +import org.opentcs.components.kernel.ObjectNameProvider; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.components.kernel.services.InternalPeripheralJobService; +import org.opentcs.components.kernel.services.InternalPeripheralService; +import org.opentcs.components.kernel.services.InternalPlantModelService; +import org.opentcs.components.kernel.services.InternalQueryService; +import org.opentcs.components.kernel.services.InternalTransportOrderService; +import org.opentcs.components.kernel.services.InternalVehicleService; +import org.opentcs.components.kernel.services.NotificationService; +import org.opentcs.components.kernel.services.PeripheralDispatcherService; +import org.opentcs.components.kernel.services.PeripheralJobService; +import org.opentcs.components.kernel.services.PeripheralService; +import org.opentcs.components.kernel.services.PlantModelService; +import org.opentcs.components.kernel.services.QueryService; +import org.opentcs.components.kernel.services.RouterService; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.components.kernel.services.TransportOrderService; +import org.opentcs.components.kernel.services.VehicleService; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.customizations.ApplicationHome; +import org.opentcs.customizations.kernel.GlobalSyncObject; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.customizations.kernel.KernelInjectionModule; +import org.opentcs.drivers.peripherals.PeripheralControllerPool; +import org.opentcs.drivers.vehicle.VehicleControllerPool; +import org.opentcs.kernel.extensions.controlcenter.vehicles.AttachmentManager; +import org.opentcs.kernel.extensions.controlcenter.vehicles.VehicleEntryPool; +import org.opentcs.kernel.extensions.watchdog.Watchdog; +import org.opentcs.kernel.extensions.watchdog.WatchdogConfiguration; +import org.opentcs.kernel.peripherals.DefaultPeripheralControllerPool; +import org.opentcs.kernel.peripherals.LocalPeripheralControllerPool; +import org.opentcs.kernel.peripherals.PeripheralAttachmentManager; +import org.opentcs.kernel.peripherals.PeripheralCommAdapterRegistry; +import org.opentcs.kernel.peripherals.PeripheralControllerFactory; +import org.opentcs.kernel.peripherals.PeripheralEntryPool; +import org.opentcs.kernel.persistence.ModelPersister; +import org.opentcs.kernel.persistence.XMLFileModelPersister; +import org.opentcs.kernel.services.StandardDispatcherService; +import org.opentcs.kernel.services.StandardNotificationService; +import org.opentcs.kernel.services.StandardPeripheralDispatcherService; +import org.opentcs.kernel.services.StandardPeripheralJobService; +import org.opentcs.kernel.services.StandardPeripheralService; +import org.opentcs.kernel.services.StandardPlantModelService; +import org.opentcs.kernel.services.StandardQueryService; +import org.opentcs.kernel.services.StandardRouterService; +import org.opentcs.kernel.services.StandardTCSObjectService; +import org.opentcs.kernel.services.StandardTransportOrderService; +import org.opentcs.kernel.services.StandardVehicleService; +import org.opentcs.kernel.vehicles.DefaultVehicleControllerPool; +import org.opentcs.kernel.vehicles.LocalVehicleControllerPool; +import org.opentcs.kernel.vehicles.VehicleCommAdapterRegistry; +import org.opentcs.kernel.vehicles.VehicleControllerComponentsFactory; +import org.opentcs.kernel.vehicles.VehicleControllerFactory; +import org.opentcs.kernel.vehicles.transformers.CoordinateSystemTransformerFactory; +import org.opentcs.kernel.vehicles.transformers.DefaultVehicleDataTransformerFactory; +import org.opentcs.kernel.workingset.CreationTimeThreshold; +import org.opentcs.kernel.workingset.NotificationBuffer; +import org.opentcs.kernel.workingset.PeripheralJobPoolManager; +import org.opentcs.kernel.workingset.PlantModelManager; +import org.opentcs.kernel.workingset.PrefixedUlidObjectNameProvider; +import org.opentcs.kernel.workingset.TCSObjectManager; +import org.opentcs.kernel.workingset.TCSObjectRepository; +import org.opentcs.kernel.workingset.TransportOrderPoolManager; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.event.SimpleEventBus; +import org.opentcs.util.logging.UncaughtExceptionLogger; + +/** + * A Guice module for the openTCS kernel application. + */ +public class DefaultKernelInjectionModule + extends + KernelInjectionModule { + + /** + * Creates a new instance. + */ + public DefaultKernelInjectionModule() { + } + + @Override + protected void configure() { + configureEventHub(); + configureKernelExecutor(); + + // Ensure that the application's home directory can be used everywhere. + File applicationHome = new File(System.getProperty("opentcs.home", ".")); + bind(File.class) + .annotatedWith(ApplicationHome.class) + .toInstance(applicationHome); + + // A single global synchronization object for the kernel. + bind(Object.class) + .annotatedWith(GlobalSyncObject.class) + .to(Object.class) + .in(Singleton.class); + + // The kernel's data pool structures. + bind(TCSObjectRepository.class).in(Singleton.class); + bind(TCSObjectManager.class).in(Singleton.class); + bind(PlantModelManager.class).in(Singleton.class); + bind(TransportOrderPoolManager.class).in(Singleton.class); + bind(PeripheralJobPoolManager.class).in(Singleton.class); + bind(NotificationBuffer.class).in(Singleton.class); + + bind(ObjectNameProvider.class) + .to(PrefixedUlidObjectNameProvider.class) + .in(Singleton.class); + + configurePersistence(); + + bind(VehicleCommAdapterRegistry.class) + .in(Singleton.class); + + configureVehicleControllers(); + + bind(AttachmentManager.class) + .in(Singleton.class); + bind(VehicleEntryPool.class) + .in(Singleton.class); + + configurePeripheralControllers(); + + bind(PeripheralCommAdapterRegistry.class) + .in(Singleton.class); + bind(PeripheralAttachmentManager.class) + .in(Singleton.class); + bind(PeripheralEntryPool.class) + .in(Singleton.class); + + bind(StandardKernel.class) + .in(Singleton.class); + bind(LocalKernel.class) + .to(StandardKernel.class); + + vehicleDataTransformersBinder().addBinding().to(DefaultVehicleDataTransformerFactory.class); + // tag::documentation_registerTransformerFactory[] + vehicleDataTransformersBinder().addBinding().to(CoordinateSystemTransformerFactory.class); + // end::documentation_registerTransformerFactory[] + + configureKernelStatesDependencies(); + configureKernelStarterDependencies(); + configureSslParameters(); + configureKernelServicesDependencies(); + + // Ensure all of these binders are initialized. + extensionsBinderAllModes(); + extensionsBinderModelling(); + extensionsBinderOperating(); + vehicleCommAdaptersBinder(); + peripheralCommAdaptersBinder(); + + configureWatchdogExtension(); + } + + private void configureKernelServicesDependencies() { + bind(StandardPlantModelService.class).in(Singleton.class); + bind(PlantModelService.class).to(StandardPlantModelService.class); + bind(InternalPlantModelService.class).to(StandardPlantModelService.class); + + bind(StandardTransportOrderService.class).in(Singleton.class); + bind(TransportOrderService.class).to(StandardTransportOrderService.class); + bind(InternalTransportOrderService.class).to(StandardTransportOrderService.class); + + bind(StandardVehicleService.class).in(Singleton.class); + bind(VehicleService.class).to(StandardVehicleService.class); + bind(InternalVehicleService.class).to(StandardVehicleService.class); + + bind(StandardTCSObjectService.class).in(Singleton.class); + bind(TCSObjectService.class).to(StandardTCSObjectService.class); + + bind(StandardNotificationService.class).in(Singleton.class); + bind(NotificationService.class).to(StandardNotificationService.class); + + bind(StandardRouterService.class).in(Singleton.class); + bind(RouterService.class).to(StandardRouterService.class); + + bind(StandardDispatcherService.class).in(Singleton.class); + bind(DispatcherService.class).to(StandardDispatcherService.class); + + bind(StandardQueryService.class).in(Singleton.class); + bind(QueryService.class).to(StandardQueryService.class); + bind(InternalQueryService.class).to(StandardQueryService.class); + + bind(StandardPeripheralService.class).in(Singleton.class); + bind(PeripheralService.class).to(StandardPeripheralService.class); + bind(InternalPeripheralService.class).to(StandardPeripheralService.class); + + bind(StandardPeripheralJobService.class).in(Singleton.class); + bind(PeripheralJobService.class).to(StandardPeripheralJobService.class); + bind(InternalPeripheralJobService.class).to(StandardPeripheralJobService.class); + + bind(StandardPeripheralDispatcherService.class).in(Singleton.class); + bind(PeripheralDispatcherService.class).to(StandardPeripheralDispatcherService.class); + } + + private void configureVehicleControllers() { + install(new FactoryModuleBuilder().build(VehicleControllerFactory.class)); + install(new FactoryModuleBuilder().build(VehicleControllerComponentsFactory.class)); + + bind(DefaultVehicleControllerPool.class) + .in(Singleton.class); + bind(VehicleControllerPool.class) + .to(DefaultVehicleControllerPool.class); + bind(LocalVehicleControllerPool.class) + .to(DefaultVehicleControllerPool.class); + } + + private void configurePeripheralControllers() { + install(new FactoryModuleBuilder().build(PeripheralControllerFactory.class)); + + bind(DefaultPeripheralControllerPool.class) + .in(Singleton.class); + bind(PeripheralControllerPool.class) + .to(DefaultPeripheralControllerPool.class); + bind(LocalPeripheralControllerPool.class) + .to(DefaultPeripheralControllerPool.class); + } + + private void configurePersistence() { + bind(ModelPersister.class).to(XMLFileModelPersister.class); + } + + private void configureEventHub() { + EventBus newEventBus = new SimpleEventBus(); + bind(EventHandler.class) + .annotatedWith(ApplicationEventBus.class) + .toInstance(newEventBus); + bind(org.opentcs.util.event.EventSource.class) + .annotatedWith(ApplicationEventBus.class) + .toInstance(newEventBus); + bind(EventBus.class) + .annotatedWith(ApplicationEventBus.class) + .toInstance(newEventBus); + } + + private void configureKernelStatesDependencies() { + // A map for KernelState instances to be provided at runtime. + MapBinder stateMapBinder + = MapBinder.newMapBinder(binder(), Kernel.State.class, KernelState.class); + stateMapBinder.addBinding(Kernel.State.SHUTDOWN).to(KernelStateShutdown.class); + stateMapBinder.addBinding(Kernel.State.MODELLING).to(KernelStateModelling.class); + stateMapBinder.addBinding(Kernel.State.OPERATING).to(KernelStateOperating.class); + + bind(OrderPoolConfiguration.class) + .toInstance( + getConfigBindingProvider().get( + OrderPoolConfiguration.PREFIX, + OrderPoolConfiguration.class + ) + ); + + bind(CreationTimeThreshold.class) + .in(Singleton.class); + + transportOrderCleanupApprovalBinder(); + orderSequenceCleanupApprovalBinder(); + peripheralJobCleanupApprovalBinder(); + } + + private void configureKernelStarterDependencies() { + bind(KernelApplicationConfiguration.class) + .toInstance( + getConfigBindingProvider().get( + KernelApplicationConfiguration.PREFIX, + KernelApplicationConfiguration.class + ) + ); + } + + private void configureSslParameters() { + SslConfiguration configuration + = getConfigBindingProvider().get( + SslConfiguration.PREFIX, + SslConfiguration.class + ); + SslParameterSet sslParamSet = new SslParameterSet( + SslParameterSet.DEFAULT_KEYSTORE_TYPE, + new File(configuration.keystoreFile()), + configuration.keystorePassword(), + new File(configuration.truststoreFile()), + configuration.truststorePassword() + ); + bind(SslParameterSet.class).toInstance(sslParamSet); + } + + private void configureKernelExecutor() { + ScheduledExecutorService executor + = new LoggingScheduledThreadPoolExecutor( + 1, + runnable -> { + Thread thread = new Thread(runnable, "kernelExecutor"); + thread.setUncaughtExceptionHandler(new UncaughtExceptionLogger(false)); + return thread; + } + ); + bind(ScheduledExecutorService.class) + .annotatedWith(KernelExecutor.class) + .toInstance(executor); + bind(ExecutorService.class) + .annotatedWith(KernelExecutor.class) + .toInstance(executor); + bind(Executor.class) + .annotatedWith(KernelExecutor.class) + .toInstance(executor); + } + + private void configureWatchdogExtension() { + extensionsBinderOperating().addBinding() + .to(Watchdog.class) + .in(Singleton.class); + + bind(WatchdogConfiguration.class) + .toInstance( + getConfigBindingProvider().get( + WatchdogConfiguration.PREFIX, + WatchdogConfiguration.class + ) + ); + + } +} diff --git a/opentcs-kernel/src/guiceConfig/java/org/opentcs/kernel/RunKernel.java b/opentcs-kernel/src/guiceConfig/java/org/opentcs/kernel/RunKernel.java new file mode 100644 index 0000000..7c292b4 --- /dev/null +++ b/opentcs-kernel/src/guiceConfig/java/org/opentcs/kernel/RunKernel.java @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.util.Modules; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.ServiceLoader; +import org.opentcs.configuration.ConfigurationBindingProvider; +import org.opentcs.configuration.gestalt.GestaltConfigurationBindingProvider; +import org.opentcs.customizations.kernel.KernelInjectionModule; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherModule; +import org.opentcs.strategies.basic.peripherals.dispatching.DefaultPeripheralJobDispatcherModule; +import org.opentcs.strategies.basic.routing.DefaultRouterModule; +import org.opentcs.strategies.basic.scheduling.DefaultSchedulerModule; +import org.opentcs.util.Environment; +import org.opentcs.util.logging.UncaughtExceptionLogger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The kernel process's default entry point. + */ +public class RunKernel { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(RunKernel.class); + + /** + * Prevents external instantiation. + */ + private RunKernel() { + } + + /** + * Initializes the system and starts the openTCS kernel including modules. + * + * @param args The command line arguments. + * @throws Exception If there was a problem starting the kernel. + */ + public static void main(String[] args) + throws Exception { + Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionLogger(false)); + + Environment.logSystemInfo(); + + LOG.debug("Setting up openTCS kernel {}...", Environment.getBaselineVersion()); + Injector injector = Guice.createInjector(customConfigurationModule()); + injector.getInstance(KernelStarter.class).startKernel(); + } + + /** + * Builds and returns a Guice module containing the custom configuration for the kernel + * application, including additions and overrides by the user. + * + * @return The custom configuration module. + */ + private static Module customConfigurationModule() { + List defaultModules + = Arrays.asList( + new DefaultKernelInjectionModule(), + new DefaultDispatcherModule(), + new DefaultRouterModule(), + new DefaultSchedulerModule(), + new DefaultPeripheralJobDispatcherModule() + ); + + ConfigurationBindingProvider bindingProvider = configurationBindingProvider(); + for (KernelInjectionModule defaultModule : defaultModules) { + defaultModule.setConfigBindingProvider(bindingProvider); + } + + return Modules.override(defaultModules) + .with(findRegisteredModules(bindingProvider)); + } + + /** + * Finds and returns all Guice modules registered via ServiceLoader. + * + * @return The registered/found modules. + */ + private static List findRegisteredModules( + ConfigurationBindingProvider bindingProvider + ) { + List registeredModules = new ArrayList<>(); + for (KernelInjectionModule module : ServiceLoader.load(KernelInjectionModule.class)) { + LOG.info( + "Integrating injection module {} (source: {})", + module.getClass().getName(), + module.getClass().getProtectionDomain().getCodeSource() + ); + module.setConfigBindingProvider(bindingProvider); + registeredModules.add(module); + } + return registeredModules; + } + + private static ConfigurationBindingProvider configurationBindingProvider() { + String chosenProvider = System.getProperty("opentcs.configuration.provider", "gestalt"); + switch (chosenProvider) { + case "gestalt": + default: + LOG.info("Using gestalt as the configuration provider."); + return gestaltConfigurationBindingProvider(); + } + } + + private static ConfigurationBindingProvider gestaltConfigurationBindingProvider() { + return new GestaltConfigurationBindingProvider( + Paths.get( + System.getProperty("opentcs.base", "."), + "config", + "opentcs-kernel-defaults-baseline.properties" + ) + .toAbsolutePath(), + Paths.get( + System.getProperty("opentcs.base", "."), + "config", + "opentcs-kernel-defaults-custom.properties" + ) + .toAbsolutePath(), + Paths.get( + System.getProperty("opentcs.home", "."), + "config", + "opentcs-kernel.properties" + ) + .toAbsolutePath() + ); + } + +} diff --git a/opentcs-kernel/src/guiceConfig/java/org/opentcs/kernel/ShutdownKernel.java b/opentcs-kernel/src/guiceConfig/java/org/opentcs/kernel/ShutdownKernel.java new file mode 100644 index 0000000..10ec6a8 --- /dev/null +++ b/opentcs-kernel/src/guiceConfig/java/org/opentcs/kernel/ShutdownKernel.java @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +/** + * Shuts down a running kernel via its administration interface. + */ +public class ShutdownKernel { + + private ShutdownKernel() { + } + + /** + * Java main. + * + * @param args command line args + */ + public static void main(String[] args) { + if (args.length > 2) { + System.err.println("ShutdownKernel [] []"); + return; + } + + String hostName = args.length > 0 ? args[0] : "localhost"; + int port = args.length > 1 ? Integer.parseInt(args[1]) : 55001; + + try { + URL url = new URI("http://" + hostName + ":" + port + "/v1/kernel").toURL(); + System.err.println("Calling to " + url + "..."); + HttpURLConnection httpCon = (HttpURLConnection) url.openConnection(); + httpCon.setRequestMethod("DELETE"); + httpCon.connect(); + httpCon.getInputStream(); + } + catch (IOException | URISyntaxException exc) { + System.err.println("Exception accessing admin interface:"); + exc.printStackTrace(); + } + + } + +} diff --git a/opentcs-kernel/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.kernel.KernelInjectionModule b/opentcs-kernel/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.kernel.KernelInjectionModule new file mode 100644 index 0000000..6038618 --- /dev/null +++ b/opentcs-kernel/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.kernel.KernelInjectionModule @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelApplicationConfiguration.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelApplicationConfiguration.java new file mode 100644 index 0000000..b696921 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelApplicationConfiguration.java @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel; + +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; + +/** + * Provides common kernel configuration entries. + */ +@ConfigurationPrefix(KernelApplicationConfiguration.PREFIX) +public interface KernelApplicationConfiguration { + + /** + * This configuration's prefix. + */ + String PREFIX = "kernelapp"; + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to automatically enable drivers on startup.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_NEW_PLANT_MODEL, + orderKey = "1_startup_0" + ) + boolean autoEnableDriversOnStartup(); + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to automatically enable peripheral drivers on startup.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_NEW_PLANT_MODEL, + orderKey = "1_startup_1" + ) + boolean autoEnablePeripheralDriversOnStartup(); + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to implicitly save the model when leaving modelling state.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_NEW_PLANT_MODEL, + orderKey = "2_autosave" + ) + boolean saveModelOnTerminateModelling(); + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to implicitly save the model when leaving operating state.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_NEW_PLANT_MODEL, + orderKey = "2_autosave" + ) + boolean saveModelOnTerminateOperating(); + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to implicitly update the router's topology when a path is (un)locked.", + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "3_topologyUpdate" + ) + boolean updateRoutingTopologyOnPathLockChange(); + + @ConfigurationEntry( + type = "Boolean", + description = "Whether vehicles should be rerouted immediately on topology changes.", + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "4_reroute_1" + ) + boolean rerouteOnRoutingTopologyUpdate(); + + @ConfigurationEntry( + type = "Boolean", + description = "Whether vehicles should be rerouted as soon as they finish a drive order.", + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "4_reroute_2" + ) + boolean rerouteOnDriveOrderFinished(); + + @ConfigurationEntry( + type = "String", + description = { + "The type of how vehicle resources (i.e., paths, points and locations allocated by " + + "vehicles) are managed.", + "Possible values:", + "LENGTH_IGNORED: Resources are _always_ released up to (excluding) a vehicle's current" + + "position. This type can be useful when you primarily want to utilize vehicle " + + "envelopes for traffic management.", + """ + LENGTH_RESPECTED: Only resources that are no longer "covered" by a vehicle (according + to the length of the vehicle and the length of the paths behind it) are released. This + is the "classic" way resources were managed before vehicle envelopes were introduced. + """ + }, + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "5_resource_management_1" + ) + VehicleResourceManagementType vehicleResourceManagementType(); + + /** + * Defines the different types of how vehicle resources (i.e., paths, points and locations + * allocated by vehicles) are managed. + */ + enum VehicleResourceManagementType { + /** + * When releasing resources, the length of a vehicle is ignored. + *

+ * Resources are always released up to (excluding) a vehicle's current position. + *

+ *

+ * This type can be useful when you primarily want to utilize vehicle envelopes for traffic + * management. + *

+ */ + LENGTH_IGNORED, + /** + * When releasing resources, the length of a vehicle is respected. + *

+ * Only resources that are no longer "covered" by a vehicle (according to the length of the + * vehicle and the length of the paths behind it) are released. This is the "classic" way + * resources were managed before vehicle envelopes were introduced. + *

+ */ + LENGTH_RESPECTED; + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelStarter.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelStarter.java new file mode 100644 index 0000000..ea8acd5 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelStarter.java @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.io.IOException; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import org.opentcs.access.Kernel; +import org.opentcs.access.LocalKernel; +import org.opentcs.components.kernel.KernelExtension; +import org.opentcs.components.kernel.services.InternalPlantModelService; +import org.opentcs.customizations.kernel.ActiveInAllModes; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Initializes an openTCS kernel instance. + */ +public class KernelStarter { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(KernelStarter.class); + /** + * The kernel we're working with. + */ + private final LocalKernel kernel; + /** + * The plant model service. + */ + private final InternalPlantModelService plantModelService; + /** + * The kernel extensions to be registered. + */ + private final Set extensions; + /** + * The kernel's executor service. + */ + private final ScheduledExecutorService kernelExecutor; + + /** + * Creates a new instance. + * + * @param kernel The kernel we're working with. + * @param plantModelService The plant model service. + * @param extensions The kernel extensions to be registered. + * @param kernelExecutor The kernel's executor service. + */ + @Inject + protected KernelStarter( + LocalKernel kernel, + InternalPlantModelService plantModelService, + @ActiveInAllModes + Set extensions, + @KernelExecutor + ScheduledExecutorService kernelExecutor + ) { + this.kernel = requireNonNull(kernel, "kernel"); + this.plantModelService = requireNonNull(plantModelService, "plantModelService"); + this.extensions = requireNonNull(extensions, "extensions"); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + } + + /** + * Initializes the system and starts the openTCS kernel including modules. + * + * @throws IOException If there was a problem loading model data. + */ + public void startKernel() + throws IOException { + kernelExecutor.submit(() -> { + // Register kernel extensions. + for (KernelExtension extension : extensions) { + kernel.addKernelExtension(extension); + } + + // Start local kernel. + kernel.initialize(); + LOG.debug("Kernel initialized."); + + plantModelService.loadPlantModel(); + + kernel.setState(Kernel.State.OPERATING); + }); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelState.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelState.java new file mode 100644 index 0000000..33f7638 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelState.java @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel; + +import static java.util.Objects.requireNonNull; + +import org.opentcs.access.Kernel.State; +import org.opentcs.components.Lifecycle; +import org.opentcs.kernel.persistence.ModelPersister; +import org.opentcs.kernel.workingset.PlantModelManager; + +/** + * The abstract base class for classes that implement state specific kernel + * behaviour. + */ +public abstract class KernelState + implements + Lifecycle { + + /** + * A global object to be used for synchronization within the kernel. + */ + private final Object globalSyncObject; + /** + * The model facade to the object pool. + */ + private final PlantModelManager plantModelManager; + /** + * The persister loading and storing model data. + */ + private final ModelPersister modelPersister; + + /** + * Creates a new state. + * + * @param globalSyncObject The kernel threads' global synchronization object. + * @param plantModelManager The plant model manager to be used. + * @param modelPersister The model persister to be used. + */ + public KernelState( + Object globalSyncObject, + PlantModelManager plantModelManager, + ModelPersister modelPersister + ) { + this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject"); + this.plantModelManager = requireNonNull(plantModelManager, "plantModelManager"); + this.modelPersister = requireNonNull(modelPersister, "modelPersister"); + } + + /** + * Returns the current state. + * + * @return The current state. + */ + public abstract State getState(); + + protected Object getGlobalSyncObject() { + return globalSyncObject; + } + + protected ModelPersister getModelPersister() { + return modelPersister; + } + + protected PlantModelManager getPlantModelManager() { + return plantModelManager; + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelStateModelling.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelStateModelling.java new file mode 100644 index 0000000..fd1c3bf --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelStateModelling.java @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Set; +import org.opentcs.access.Kernel; +import org.opentcs.components.kernel.KernelExtension; +import org.opentcs.customizations.kernel.ActiveInModellingMode; +import org.opentcs.customizations.kernel.GlobalSyncObject; +import org.opentcs.kernel.persistence.ModelPersister; +import org.opentcs.kernel.workingset.PlantModelManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class implements the standard openTCS kernel in modelling mode. + */ +public class KernelStateModelling + extends + KernelStateOnline { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(KernelStateModelling.class); + /** + * This kernel state's local extensions. + */ + private final Set extensions; + /** + * This instance's initialized flag. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param globalSyncObject The kernel threads' global synchronization object. + * @param plantModelManager The plant model manager to be used. + * @param modelPersister The model persister to be used. + * @param configuration This class's configuration. + * @param extensions The kernel extensions to be used. + */ + @Inject + public KernelStateModelling( + @GlobalSyncObject + Object globalSyncObject, + PlantModelManager plantModelManager, + ModelPersister modelPersister, + KernelApplicationConfiguration configuration, + @ActiveInModellingMode + Set extensions + ) { + super( + globalSyncObject, + plantModelManager, + modelPersister, + configuration.saveModelOnTerminateModelling() + ); + this.extensions = requireNonNull(extensions, "extensions"); + } + + @Override + public void initialize() { + if (initialized) { + throw new IllegalStateException("Already initialized"); + } + LOG.debug("Initializing modelling state..."); + + // Start kernel extensions. + for (KernelExtension extension : extensions) { + LOG.debug("Initializing kernel extension '{}'...", extension); + extension.initialize(); + } + LOG.debug("Finished initializing kernel extensions."); + + initialized = true; + + LOG.debug("Modelling state initialized."); + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!initialized) { + throw new IllegalStateException("Not initialized, cannot terminate"); + } + LOG.debug("Terminating modelling state..."); + super.terminate(); + + // Terminate everything that may still use resources. + for (KernelExtension extension : extensions) { + LOG.debug("Terminating kernel extension '{}'...", extension); + extension.terminate(); + } + LOG.debug("Terminated kernel extensions."); + + initialized = false; + + LOG.debug("Modelling state terminated."); + } + + @Override + public Kernel.State getState() { + return Kernel.State.MODELLING; + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelStateOnline.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelStateOnline.java new file mode 100644 index 0000000..ea84ba5 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelStateOnline.java @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel; + +import org.opentcs.kernel.persistence.ModelPersister; +import org.opentcs.kernel.workingset.PlantModelManager; + +/** + * The base class for the kernel's online states. + */ +public abstract class KernelStateOnline + extends + KernelState { + + /** + * Whether to save the model when this state is terminated. + */ + private final boolean saveModelOnTerminate; + + /** + * Creates a new instance. + * + * @param globalSyncObject The kernel threads' global synchronization object. + * @param plantModelManager The plant model manager to be used. + * @param modelPersister The model persister to be used. + * @param saveModelOnTerminate Whether to save the model when this state is terminated. + */ + public KernelStateOnline( + Object globalSyncObject, + PlantModelManager plantModelManager, + ModelPersister modelPersister, + boolean saveModelOnTerminate + ) { + super(globalSyncObject, plantModelManager, modelPersister); + this.saveModelOnTerminate = saveModelOnTerminate; + } + + @Override + public void terminate() { + if (saveModelOnTerminate) { + savePlantModel(); + } + } + + private void savePlantModel() + throws IllegalStateException { + synchronized (getGlobalSyncObject()) { + getModelPersister().saveModel(getPlantModelManager().createPlantModelCreationTO()); + } + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelStateOperating.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelStateOperating.java new file mode 100644 index 0000000..a778bc7 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelStateOperating.java @@ -0,0 +1,340 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel; + +import static java.util.Objects.requireNonNull; + +import com.google.common.util.concurrent.Uninterruptibles; +import jakarta.inject.Inject; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import org.opentcs.access.Kernel; +import org.opentcs.components.kernel.Dispatcher; +import org.opentcs.components.kernel.KernelExtension; +import org.opentcs.components.kernel.PeripheralJobDispatcher; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.components.kernel.services.InternalVehicleService; +import org.opentcs.customizations.kernel.ActiveInOperatingMode; +import org.opentcs.customizations.kernel.GlobalSyncObject; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.data.model.Vehicle; +import org.opentcs.kernel.extensions.controlcenter.vehicles.AttachmentManager; +import org.opentcs.kernel.peripherals.LocalPeripheralControllerPool; +import org.opentcs.kernel.peripherals.PeripheralAttachmentManager; +import org.opentcs.kernel.persistence.ModelPersister; +import org.opentcs.kernel.vehicles.LocalVehicleControllerPool; +import org.opentcs.kernel.workingset.PeripheralJobPoolManager; +import org.opentcs.kernel.workingset.PlantModelManager; +import org.opentcs.kernel.workingset.TransportOrderPoolManager; +import org.opentcs.kernel.workingset.WorkingSetCleanupTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class implements the standard openTCS kernel in normal operation. + */ +public class KernelStateOperating + extends + KernelStateOnline { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(KernelStateOperating.class); + /** + * The order pool manager. + */ + private final TransportOrderPoolManager orderPoolManager; + /** + * The job pool manager. + */ + private final PeripheralJobPoolManager jobPoolManager; + /** + * This kernel's router. + */ + private final Router router; + /** + * This kernel's scheduler. + */ + private final Scheduler scheduler; + /** + * This kernel's dispatcher. + */ + private final Dispatcher dispatcher; + /** + * This kernel's peripheral job dispatcher. + */ + private final PeripheralJobDispatcher peripheralJobDispatcher; + /** + * A pool of vehicle controllers. + */ + private final LocalVehicleControllerPool vehicleControllerPool; + /** + * A pool of peripheral controllers. + */ + private final LocalPeripheralControllerPool peripheralControllerPool; + /** + * The kernel's executor. + */ + private final ScheduledExecutorService kernelExecutor; + /** + * A task for periodically getting rid of old orders, order sequences and peripheral jobs. + */ + private final WorkingSetCleanupTask workingSetCleanupTask; + /** + * This kernel state's local extensions. + */ + private final Set extensions; + /** + * The kernel's attachment manager. + */ + private final AttachmentManager attachmentManager; + /** + * The kernel's peripheral attachment manager. + */ + private final PeripheralAttachmentManager peripheralAttachmentManager; + /** + * The vehicle service. + */ + private final InternalVehicleService vehicleService; + /** + * Listens to path lock events and updates the routing topology. + */ + private final PathLockEventListener pathLockListener; + /** + * Triggers dispatching of vehicles and transport orders on certain events. + */ + private final VehicleDispatchTrigger vehicleDispatchTrigger; + /** + * A handle for the cleaner task. + */ + private ScheduledFuture cleanerTaskFuture; + /** + * This instance's initialized flag. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param globalSyncObject kernel threads' global synchronization object. + * @param plantModelManager The plant model manager to be used. + * @param orderPoolManager The order pool manager to be used. + * @param jobPoolManager The job pool manager to be used. + * @param modelPersister The model persister to be used. + * @param configuration This class's configuration. + * @param router The router to be used. + * @param scheduler The scheduler to be used. + * @param dispatcher The dispatcher to be used. + * @param peripheralJobDispatcher The peripheral job dispatcher to be used. + * @param controllerPool The vehicle controller pool to be used. + * @param peripheralControllerPool The peripheral controller pool to be used. + * @param kernelExecutor The kernel executer to be used. + * @param workingSetCleanupTask The workingset cleanup task to be used. + * @param extensions The kernel extensions to load. + * @param attachmentManager The attachment manager to be used. + * @param peripheralAttachmentManager The peripheral attachment manager to be used. + * @param vehicleService The vehicle service to be used. + * @param pathLockListener Listens to path lock events and updates the routing topology. + * @param vehicleDispatchTrigger Triggers dispatching of vehicles and transport orders on certain + * events. + */ + @Inject + public KernelStateOperating( + @GlobalSyncObject + Object globalSyncObject, + PlantModelManager plantModelManager, + TransportOrderPoolManager orderPoolManager, + PeripheralJobPoolManager jobPoolManager, + ModelPersister modelPersister, + KernelApplicationConfiguration configuration, + Router router, + Scheduler scheduler, + Dispatcher dispatcher, + PeripheralJobDispatcher peripheralJobDispatcher, + LocalVehicleControllerPool controllerPool, + LocalPeripheralControllerPool peripheralControllerPool, + @KernelExecutor + ScheduledExecutorService kernelExecutor, + WorkingSetCleanupTask workingSetCleanupTask, + @ActiveInOperatingMode + Set extensions, + AttachmentManager attachmentManager, + PeripheralAttachmentManager peripheralAttachmentManager, + InternalVehicleService vehicleService, + PathLockEventListener pathLockListener, + VehicleDispatchTrigger vehicleDispatchTrigger + ) { + super( + globalSyncObject, + plantModelManager, + modelPersister, + configuration.saveModelOnTerminateOperating() + ); + this.orderPoolManager = requireNonNull(orderPoolManager, "orderPoolManager"); + this.jobPoolManager = requireNonNull(jobPoolManager, "jobPoolManager"); + this.router = requireNonNull(router, "router"); + this.scheduler = requireNonNull(scheduler, "scheduler"); + this.dispatcher = requireNonNull(dispatcher, "dispatcher"); + this.peripheralJobDispatcher = requireNonNull( + peripheralJobDispatcher, + "peripheralJobDispatcher" + ); + this.vehicleControllerPool = requireNonNull(controllerPool, "controllerPool"); + this.peripheralControllerPool = requireNonNull( + peripheralControllerPool, + "peripheralControllerPool" + ); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + this.workingSetCleanupTask = requireNonNull(workingSetCleanupTask, "workingSetCleanupTask"); + this.extensions = requireNonNull(extensions, "extensions"); + this.attachmentManager = requireNonNull(attachmentManager, "attachmentManager"); + this.peripheralAttachmentManager = requireNonNull( + peripheralAttachmentManager, + "peripheralAttachmentManager" + ); + this.vehicleService = requireNonNull(vehicleService, "vehicleService"); + this.pathLockListener = requireNonNull(pathLockListener, "pathLockListener"); + this.vehicleDispatchTrigger = requireNonNull(vehicleDispatchTrigger, "vehicleDispatchTrigger"); + } + + // Implementation of interface Kernel starts here. + @Override + public void initialize() { + if (initialized) { + LOG.debug("Already initialized."); + return; + } + LOG.debug("Initializing operating state..."); + + // Reset vehicle states to ensure vehicles are not dispatchable initially. + for (Vehicle curVehicle : vehicleService.fetchObjects(Vehicle.class)) { + vehicleService.updateVehicleProcState(curVehicle.getReference(), Vehicle.ProcState.IDLE); + vehicleService.updateVehicleIntegrationLevel( + curVehicle.getReference(), + Vehicle.IntegrationLevel.TO_BE_RESPECTED + ); + vehicleService.updateVehicleState(curVehicle.getReference(), Vehicle.State.UNKNOWN); + vehicleService.updateVehicleTransportOrder(curVehicle.getReference(), null); + vehicleService.updateVehicleOrderSequence(curVehicle.getReference(), null); + } + + LOG.debug("Initializing scheduler '{}'...", scheduler); + scheduler.initialize(); + LOG.debug("Initializing router '{}'...", router); + router.initialize(); + LOG.debug("Initializing dispatcher '{}'...", dispatcher); + dispatcher.initialize(); + LOG.debug("Initializing peripheral job dispatcher '{}'...", peripheralJobDispatcher); + peripheralJobDispatcher.initialize(); + LOG.debug("Initializing vehicle controller pool '{}'...", vehicleControllerPool); + vehicleControllerPool.initialize(); + LOG.debug("Initializing peripheral controller pool '{}'...", peripheralControllerPool); + peripheralControllerPool.initialize(); + LOG.debug("Initializing attachment manager '{}'...", attachmentManager); + attachmentManager.initialize(); + LOG.debug("Initializing peripheral attachment manager '{}'...", peripheralAttachmentManager); + peripheralAttachmentManager.initialize(); + + pathLockListener.initialize(); + vehicleDispatchTrigger.initialize(); + + // Start a task for cleaning up old orders periodically. + cleanerTaskFuture = kernelExecutor.scheduleAtFixedRate( + workingSetCleanupTask, + workingSetCleanupTask.getSweepInterval(), + workingSetCleanupTask.getSweepInterval(), + TimeUnit.MILLISECONDS + ); + + // Start kernel extensions. + for (KernelExtension extension : extensions) { + LOG.debug("Initializing kernel extension '{}'...", extension); + extension.initialize(); + } + LOG.debug("Finished initializing kernel extensions."); + + initialized = true; + + LOG.debug("Operating state initialized."); + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!initialized) { + LOG.debug("Not initialized."); + return; + } + LOG.debug("Terminating operating state..."); + super.terminate(); + + // Terminate everything that may still use resources. + for (KernelExtension extension : extensions) { + LOG.debug("Terminating kernel extension '{}'...", extension); + extension.terminate(); + } + LOG.debug("Terminated kernel extensions."); + + // No need to clean up any more - it's all going to be cleaned up very soon. + cleanerTaskFuture.cancel(false); + cleanerTaskFuture = null; + + // Terminate strategies. + LOG.debug("Terminating peripheral job dispatcher '{}'...", peripheralJobDispatcher); + peripheralJobDispatcher.terminate(); + LOG.debug("Terminating dispatcher '{}'...", dispatcher); + dispatcher.terminate(); + LOG.debug("Terminating router '{}'...", router); + router.terminate(); + LOG.debug("Terminating scheduler '{}'...", scheduler); + scheduler.terminate(); + LOG.debug("Terminating peripheral controller pool '{}'...", peripheralControllerPool); + peripheralControllerPool.terminate(); + LOG.debug("Terminating vehicle controller pool '{}'...", vehicleControllerPool); + vehicleControllerPool.terminate(); + LOG.debug("Terminating attachment manager '{}'...", attachmentManager); + attachmentManager.terminate(); + LOG.debug("Terminating peripheral attachment manager '{}'...", peripheralAttachmentManager); + peripheralAttachmentManager.terminate(); + + pathLockListener.terminate(); + vehicleDispatchTrigger.terminate(); + + // Grant communication adapters etc. some time to settle things. + Uninterruptibles.sleepUninterruptibly(500, TimeUnit.MILLISECONDS); + + // Ensure that vehicles do not reference orders any more. + for (Vehicle curVehicle : vehicleService.fetchObjects(Vehicle.class)) { + vehicleService.updateVehicleProcState(curVehicle.getReference(), Vehicle.ProcState.IDLE); + vehicleService.updateVehicleIntegrationLevel( + curVehicle.getReference(), + Vehicle.IntegrationLevel.TO_BE_RESPECTED + ); + vehicleService.updateVehicleState(curVehicle.getReference(), Vehicle.State.UNKNOWN); + vehicleService.updateVehicleTransportOrder(curVehicle.getReference(), null); + vehicleService.updateVehicleOrderSequence(curVehicle.getReference(), null); + } + + // Remove all orders and order sequences from the pool. + orderPoolManager.clear(); + // Remove all peripheral jobs from the pool. + jobPoolManager.clear(); + + initialized = false; + + LOG.debug("Operating state terminated."); + } + + @Override + public Kernel.State getState() { + return Kernel.State.OPERATING; + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelStateShutdown.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelStateShutdown.java new file mode 100644 index 0000000..4ac84dd --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/KernelStateShutdown.java @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel; + +import jakarta.inject.Inject; +import org.opentcs.access.Kernel; +import org.opentcs.customizations.kernel.GlobalSyncObject; +import org.opentcs.kernel.persistence.ModelPersister; +import org.opentcs.kernel.workingset.PlantModelManager; + +/** + * This class implements the standard openTCS kernel when it's shut down. + */ +public class KernelStateShutdown + extends + KernelState { + + /** + * Indicates whether this component is enabled. + */ + private boolean initialized; + + /** + * Creates a new StandardKernelShutdownState. + * + * @param globalSyncObject The kernel threads' global synchronization object. + * @param plantModelManager The plant model manager to be used. + * @param modelPersister The model persister to be used. + */ + @Inject + public KernelStateShutdown( + @GlobalSyncObject + Object globalSyncObject, + PlantModelManager plantModelManager, + ModelPersister modelPersister + ) { + super( + globalSyncObject, + plantModelManager, + modelPersister + ); + } + + // Methods that HAVE to be implemented/overridden start here. + @Override + public void initialize() { + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + initialized = false; + } + + @Override + public Kernel.State getState() { + return Kernel.State.SHUTDOWN; + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/OrderPoolConfiguration.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/OrderPoolConfiguration.java new file mode 100644 index 0000000..5f99006 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/OrderPoolConfiguration.java @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel; + +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; +import org.opentcs.kernel.workingset.WorkingSetCleanupTask; + +/** + * Provides methods to configure the {@link WorkingSetCleanupTask}. + */ +@ConfigurationPrefix(OrderPoolConfiguration.PREFIX) +public interface OrderPoolConfiguration { + + /** + * This configuration's prefix. + */ + String PREFIX = "orderpool"; + + @ConfigurationEntry( + type = "Long", + description = "The interval between sweeps (in ms).", + changesApplied = ConfigurationEntry.ChangesApplied.ON_NEW_PLANT_MODEL + ) + long sweepInterval(); + + @ConfigurationEntry( + type = "Integer", + description = "The minimum age of orders or peripheral jobs to remove in a sweep (in ms).", + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY + ) + int sweepAge(); +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/PathLockEventListener.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/PathLockEventListener.java new file mode 100644 index 0000000..5c1ffdf --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/PathLockEventListener.java @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Set; +import org.opentcs.components.Lifecycle; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.components.kernel.services.RouterService; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.model.Path; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.EventHandler; + +/** + * Listens to path lock events and updates the routing topology. + */ +public class PathLockEventListener + implements + EventHandler, + Lifecycle { + + /** + * The kernel configuration. + */ + private final KernelApplicationConfiguration configuration; + /** + * The router service. + */ + private final RouterService routerService; + /** + * The event bus. + */ + private final EventBus eventBus; + /** + * The dispatcher. + */ + private final DispatcherService dispatcher; + /** + * This instance's initialized flag. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param configuration The kernel configuration. + * @param routerService The router service. + * @param eventBus The event bus. + * @param dispatcher The dispatcher. + */ + @Inject + public PathLockEventListener( + KernelApplicationConfiguration configuration, + RouterService routerService, + @ApplicationEventBus + EventBus eventBus, + DispatcherService dispatcher + ) { + this.configuration = requireNonNull(configuration, "configuration"); + this.routerService = requireNonNull(routerService, "routerService"); + this.eventBus = requireNonNull(eventBus, "eventBus"); + this.dispatcher = requireNonNull(dispatcher, "dispatcher"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + initialized = true; + eventBus.subscribe(this); + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + initialized = false; + eventBus.unsubscribe(this); + } + + @Override + public void onEvent(Object eventObject) { + if (!configuration.updateRoutingTopologyOnPathLockChange()) { + return; + } + + if (!(eventObject instanceof TCSObjectEvent)) { + return; + } + + TCSObjectEvent event = (TCSObjectEvent) eventObject; + if (hasPathLockChanged(event)) { + routerService.updateRoutingTopology( + Set.of(((Path) event.getCurrentObjectState()).getReference()) + ); + + if (configuration.rerouteOnRoutingTopologyUpdate()) { + dispatcher.rerouteAll(ReroutingType.REGULAR); + } + } + } + + private boolean hasPathLockChanged(TCSObjectEvent event) { + return event.getCurrentObjectState() instanceof Path + && event.getType() == TCSObjectEvent.Type.OBJECT_MODIFIED + && ((Path) event.getCurrentObjectState()).isLocked() != ((Path) event + .getPreviousObjectState()).isLocked(); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/SslConfiguration.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/SslConfiguration.java new file mode 100644 index 0000000..9add6b8 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/SslConfiguration.java @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel; + +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; + +/** + * Provides methods to configure the ssl connection. + */ +@ConfigurationPrefix(SslConfiguration.PREFIX) +public interface SslConfiguration { + + /** + * This configuration's prefix. + */ + String PREFIX = "ssl"; + + @ConfigurationEntry( + type = "String", + description = {"The file url of the keystore."}, + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_connection_0" + ) + String keystoreFile(); + + @ConfigurationEntry( + type = "String", + description = {"The password for the keystore."}, + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_connection_1" + ) + String keystorePassword(); + + @ConfigurationEntry( + type = "String", + description = {"The file url of the truststore."}, + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_connection_2" + ) + String truststoreFile(); + + @ConfigurationEntry( + type = "String", + description = {"The password for the truststore."}, + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_connection_3" + ) + String truststorePassword(); + +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/StandardKernel.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/StandardKernel.java new file mode 100644 index 0000000..5d62633 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/StandardKernel.java @@ -0,0 +1,229 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel; + +import static java.util.Objects.requireNonNull; + +import com.google.common.util.concurrent.Uninterruptibles; +import com.google.inject.Provider; +import jakarta.inject.Inject; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import org.opentcs.access.Kernel; +import org.opentcs.access.Kernel.State; +import org.opentcs.access.KernelStateTransitionEvent; +import org.opentcs.access.LocalKernel; +import org.opentcs.components.kernel.KernelExtension; +import org.opentcs.components.kernel.services.NotificationService; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.data.notification.UserNotification; +import org.opentcs.util.event.EventBus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class implements the standard openTCS kernel. + */ +public class StandardKernel + implements + LocalKernel, + Runnable { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(StandardKernel.class); + /** + * A map to state providers used when switching kernel states. + */ + private final Map> stateProviders; + /** + * The application's event bus. + */ + private final EventBus eventBus; + /** + * Our executor. + */ + private final ScheduledExecutorService kernelExecutor; + /** + * This kernel's order receivers. + */ + private final Set kernelExtensions = new HashSet<>(); + /** + * Functions as a barrier for the kernel's {@link #run() run()} method. + */ + private final Semaphore terminationSemaphore = new Semaphore(0); + /** + * The notification service. + */ + private final NotificationService notificationService; + /** + * This kernel's initialized flag. + */ + private volatile boolean initialized; + /** + * The kernel implementing the actual functionality for the current mode. + */ + private KernelState kernelState; + + /** + * Creates a new kernel. + * + * @param eventBus The central event bus to be used. + * @param kernelExecutor An executor for this kernel's tasks. + * @param stateProviders The state map to be used. + * @param notificationService The notification service to be used. + */ + @Inject + public StandardKernel( + @ApplicationEventBus + EventBus eventBus, + @KernelExecutor + ScheduledExecutorService kernelExecutor, + Map> stateProviders, + NotificationService notificationService + ) { + this.eventBus = requireNonNull(eventBus, "eventBus"); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + this.stateProviders = requireNonNull(stateProviders, "stateProviders"); + this.notificationService = requireNonNull(notificationService, "notificationService"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + // First of all, start all kernel extensions that are already registered. + for (KernelExtension extension : kernelExtensions) { + LOG.debug("Initializing extension: {}", extension.getClass().getName()); + extension.initialize(); + } + + // Initial state is modelling. + setState(State.MODELLING); + + initialized = true; + LOG.debug("Starting kernel thread"); + Thread kernelThread = new Thread(this, "kernelThread"); + kernelThread.start(); + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + // Note that the actual shutdown of extensions should happen when the kernel + // thread (see run()) finishes, not here. + // Set the terminated flag and wake up this kernel's thread for termination. + initialized = false; + terminationSemaphore.release(); + } + + @Override + public void run() { + // Wait until terminated. + terminationSemaphore.acquireUninterruptibly(); + LOG.info("Terminating..."); + // Sleep a bit so clients have some time to receive an event for the + // SHUTDOWN state change and shut down gracefully themselves. + Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); + // Shut down all kernel extensions. + LOG.debug("Shutting down kernel extensions..."); + for (KernelExtension extension : kernelExtensions) { + extension.terminate(); + } + kernelExecutor.shutdown(); + LOG.info("Kernel thread finished."); + } + + @Override + public State getState() { + return kernelState.getState(); + } + + @Override + public void setState(State newState) + throws IllegalArgumentException { + requireNonNull(newState, "newState"); + + final Kernel.State oldState; + if (kernelState != null) { + oldState = kernelState.getState(); + // Don't do anything if the new state is the same as the current one. + if (oldState == newState) { + LOG.debug("Already in state '{}', doing nothing.", newState.name()); + return; + } + // Let listeners know we're in transition. + emitStateEvent(oldState, newState, false); + // Terminate previous state. + kernelState.terminate(); + } + else { + oldState = null; + } + LOG.info("Switching kernel to state '{}'", newState.name()); + switch (newState) { + case SHUTDOWN: + kernelState = stateProviders.get(Kernel.State.SHUTDOWN).get(); + kernelState.initialize(); + terminate(); + break; + case MODELLING: + kernelState = stateProviders.get(Kernel.State.MODELLING).get(); + kernelState.initialize(); + break; + case OPERATING: + kernelState = stateProviders.get(Kernel.State.OPERATING).get(); + kernelState.initialize(); + break; + default: + throw new IllegalArgumentException("Unexpected state: " + newState); + } + emitStateEvent(oldState, newState, true); + notificationService.publishUserNotification( + new UserNotification( + "Kernel is now in state " + newState, + UserNotification.Level.INFORMATIONAL + ) + ); + } + + @Override + public void addKernelExtension(final KernelExtension newExtension) { + requireNonNull(newExtension, "newExtension"); + + kernelExtensions.add(newExtension); + } + + @Override + public void removeKernelExtension(final KernelExtension rmExtension) { + requireNonNull(rmExtension, "rmExtension"); + + kernelExtensions.remove(rmExtension); + } + + // Methods not declared in any interface start here. + /** + * Generates an event for a state change. + * + * @param leftState The state left. + * @param enteredState The state entered. + * @param transitionFinished Whether the transition is finished or not. + */ + private void emitStateEvent(State leftState, State enteredState, boolean transitionFinished) { + eventBus.onEvent(new KernelStateTransitionEvent(leftState, enteredState, transitionFinished)); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/VehicleDispatchTrigger.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/VehicleDispatchTrigger.java new file mode 100644 index 0000000..54bf2c7 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/VehicleDispatchTrigger.java @@ -0,0 +1,163 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.concurrent.Executor; +import org.opentcs.components.Lifecycle; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Triggers dispatching of vehicles and transport orders on certain events. + */ +public class VehicleDispatchTrigger + implements + EventHandler, + Lifecycle { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(VehicleDispatchTrigger.class); + /** + * The dispatcher in use. + */ + private final DispatcherService dispatcher; + /** + * The event bus. + */ + private final EventBus eventBus; + /** + * The app configuration. + */ + private final KernelApplicationConfiguration configuration; + /** + * The kernel executor. + */ + private final Executor kernelExecutor; + /** + * This instance's initialized flag. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param kernelExecutor The kernel executor to use. + * @param eventBus The event bus. + * @param dispatcher The dispatcher in use. + * @param configuration The application configuration. + */ + @Inject + public VehicleDispatchTrigger( + @KernelExecutor + Executor kernelExecutor, + @ApplicationEventBus + EventBus eventBus, + DispatcherService dispatcher, + KernelApplicationConfiguration configuration + ) { + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + this.eventBus = requireNonNull(eventBus, "eventBus"); + this.dispatcher = requireNonNull(dispatcher, "dispatcher"); + this.configuration = requireNonNull(configuration, "configuration"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + initialized = true; + eventBus.subscribe(this); + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + initialized = false; + eventBus.unsubscribe(this); + } + + @Override + public void onEvent(Object event) { + if (!(event instanceof TCSObjectEvent)) { + return; + } + TCSObjectEvent objectEvent = (TCSObjectEvent) event; + if (objectEvent.getCurrentOrPreviousObjectState() instanceof Vehicle) { + checkVehicleChange( + (Vehicle) objectEvent.getPreviousObjectState(), + (Vehicle) objectEvent.getCurrentObjectState() + ); + } + } + + private void checkVehicleChange(Vehicle oldVehicle, Vehicle newVehicle) { + if (driveOrderFinished(oldVehicle, newVehicle) + && configuration.rerouteOnDriveOrderFinished()) { + LOG.debug("Rerouting vehicle {}...", newVehicle); + dispatcher.reroute(newVehicle.getReference(), ReroutingType.REGULAR); + } + + if ((newVehicle.getIntegrationLevel() == Vehicle.IntegrationLevel.TO_BE_UTILIZED + || newVehicle.getIntegrationLevel() == Vehicle.IntegrationLevel.TO_BE_RESPECTED) + && (idleAndEnergyLevelChanged(oldVehicle, newVehicle) + || awaitingNextOrder(oldVehicle, newVehicle) + || orderSequenceNulled(oldVehicle, newVehicle))) { + LOG.debug("Dispatching for {}...", newVehicle); + // Dispatching may result in changes to the vehicle and thus trigger this code, which would + // then lead to a second dispatch run before the first one is completed. To avoid this, we + // ensure dispatching is done at some later point by scheduling it to be executed on the + // kernel executor (so it does not trigger itself in a loop). + kernelExecutor.execute(() -> dispatcher.dispatch()); + } + } + + private boolean idleAndEnergyLevelChanged(Vehicle oldVehicle, Vehicle newVehicle) { + // If the vehicle is idle and its energy level changed, we may want to order it to recharge. + return newVehicle.hasProcState(Vehicle.ProcState.IDLE) + && (newVehicle.hasState(Vehicle.State.IDLE) || newVehicle.hasState(Vehicle.State.CHARGING)) + && newVehicle.getEnergyLevel() != oldVehicle.getEnergyLevel(); + } + + private boolean awaitingNextOrder(Vehicle oldVehicle, Vehicle newVehicle) { + // If the vehicle's processing state changed to IDLE or AWAITING_ORDER, it is waiting for + // its next order, so look for one. + return newVehicle.getProcState() != oldVehicle.getProcState() + && (newVehicle.hasProcState(Vehicle.ProcState.IDLE) + || newVehicle.hasProcState(Vehicle.ProcState.AWAITING_ORDER)); + } + + private boolean orderSequenceNulled(Vehicle oldVehicle, Vehicle newVehicle) { + // If the vehicle's order sequence reference has become null, the vehicle has just been released + // from an order sequence, so we may look for new assignments. + return newVehicle.getOrderSequence() == null + && oldVehicle.getOrderSequence() != null; + } + + private boolean driveOrderFinished(Vehicle oldVehicle, Vehicle newVehicle) { + // If the vehicle's processing state changes to AWAITING_ORDER, it has finished its current + // drive order. + return newVehicle.getProcState() != oldVehicle.getProcState() + && newVehicle.hasProcState(Vehicle.ProcState.AWAITING_ORDER); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/controlcenter/vehicles/AttachmentManager.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/controlcenter/vehicles/AttachmentManager.java new file mode 100644 index 0000000..6f45481 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/controlcenter/vehicles/AttachmentManager.java @@ -0,0 +1,393 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.controlcenter.vehicles; + +import static java.util.Objects.requireNonNull; + +import com.google.common.base.Strings; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import org.opentcs.components.Lifecycle; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; +import org.opentcs.drivers.vehicle.VehicleCommAdapterFactory; +import org.opentcs.drivers.vehicle.management.ProcessModelEvent; +import org.opentcs.drivers.vehicle.management.VehicleAttachmentEvent; +import org.opentcs.drivers.vehicle.management.VehicleAttachmentInformation; +import org.opentcs.drivers.vehicle.management.VehicleProcessModelTO; +import org.opentcs.kernel.KernelApplicationConfiguration; +import org.opentcs.kernel.vehicles.LocalVehicleControllerPool; +import org.opentcs.kernel.vehicles.VehicleCommAdapterRegistry; +import org.opentcs.util.Assertions; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages attachment and detachment of communication adapters to vehicles. + */ +public class AttachmentManager + implements + Lifecycle { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(AttachmentManager.class); + /** + * This class's configuration. + */ + private final KernelApplicationConfiguration configuration; + /** + * The object service. + */ + private final TCSObjectService objectService; + /** + * The vehicle controller pool. + */ + private final LocalVehicleControllerPool controllerPool; + /** + * The comm adapter registry. + */ + private final VehicleCommAdapterRegistry commAdapterRegistry; + /** + * The pool of vehicle entries. + */ + private final VehicleEntryPool vehicleEntryPool; + /** + * The handler to send events to. + */ + private final EventHandler eventHandler; + /** + * The pool of comm adapter attachments. + */ + private final Map attachmentPool = new HashMap<>(); + /** + * Whether the attachment manager is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param objectService The object service. + * @param controllerPool The vehicle controller pool. + * @param commAdapterRegistry The comm adapter registry. + * @param vehicleEntryPool The pool of vehicle entries. + * @param eventHandler The handler to send events to. + * @param configuration This class's configuration. + */ + @Inject + public AttachmentManager( + @Nonnull + TCSObjectService objectService, + @Nonnull + LocalVehicleControllerPool controllerPool, + @Nonnull + VehicleCommAdapterRegistry commAdapterRegistry, + @Nonnull + VehicleEntryPool vehicleEntryPool, + @Nonnull + @ApplicationEventBus + EventHandler eventHandler, + @Nonnull + KernelApplicationConfiguration configuration + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.controllerPool = requireNonNull(controllerPool, "controllerPool"); + this.commAdapterRegistry = requireNonNull(commAdapterRegistry, "commAdapterRegistry"); + this.vehicleEntryPool = requireNonNull(vehicleEntryPool, "vehicleEntryPool"); + this.eventHandler = requireNonNull(eventHandler, "eventHandler"); + this.configuration = requireNonNull(configuration, "configuration"); + } + + @Override + public void initialize() { + if (isInitialized()) { + LOG.debug("Already initialized."); + return; + } + + commAdapterRegistry.initialize(); + vehicleEntryPool.initialize(); + + initAttachmentPool(); + + autoAttachAllAdapters(); + + if (configuration.autoEnableDriversOnStartup()) { + autoEnableAllAdapters(); + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + LOG.debug("Not initialized."); + return; + } + + // Detach all attached drivers to clean up. + detachAllAdapters(); + vehicleEntryPool.terminate(); + commAdapterRegistry.terminate(); + + initialized = false; + } + + /** + * Attaches an adapter to a vehicle. + * + * @param vehicleName The vehicle name. + * @param factory The factory that provides the adapter to be assigned. + */ + public void attachAdapterToVehicle( + @Nonnull + String vehicleName, + @Nonnull + VehicleCommAdapterFactory factory + ) { + requireNonNull(vehicleName, "vehicleName"); + requireNonNull(factory, "factory"); + + LOG.info( + "Attaching vehicle comm adapter: '{}' -- '{}'...", + vehicleName, + factory.getClass().getName() + ); + + VehicleEntry vehicleEntry = vehicleEntryPool.getEntryFor(vehicleName); + if (vehicleEntry == null) { + LOG.warn( + "No vehicle entry found for '{}'. Entries: {}", + vehicleName, + vehicleEntryPool + ); + return; + } + + VehicleCommAdapter commAdapter = factory.getAdapterFor(vehicleEntry.getVehicle()); + if (commAdapter == null) { + LOG.warn( + "Factory {} did not provide adapter for vehicle {}, ignoring.", + factory, + vehicleEntry.getVehicle().getName() + ); + return; + } + + // Perform a cleanup for the old adapter. + disableAndTerminateAdapter(vehicleEntry); + controllerPool.detachVehicleController(vehicleEntry.getVehicle().getName()); + + commAdapter.initialize(); + controllerPool.attachVehicleController(vehicleEntry.getVehicle().getName(), commAdapter); + + vehicleEntry.setCommAdapterFactory(factory); + vehicleEntry.setCommAdapter(commAdapter); + vehicleEntry.setProcessModel(commAdapter.getProcessModel()); + + objectService.updateObjectProperty( + vehicleEntry.getVehicle().getReference(), + Vehicle.PREFERRED_ADAPTER, + factory.getClass().getName() + ); + + updateAttachmentInformation(vehicleEntry); + } + + /** + * Automatically attach a vehicle to an adapter. + * + * @param vehicleName The name of the vehicle to attach. + */ + public void autoAttachAdapterToVehicle( + @Nonnull + String vehicleName + ) { + requireNonNull(vehicleName, "vehicleName"); + + VehicleEntry vehicleEntry = vehicleEntryPool.getEntryFor(vehicleName); + if (vehicleEntry == null) { + LOG.warn( + "No vehicle entry found for '{}'. Entries: {}", + vehicleName, + vehicleEntryPool + ); + return; + } + + // Do not auto-attach if there is already a comm adapter attached to the vehicle. + if (vehicleEntry.getCommAdapter() != null) { + return; + } + + Vehicle vehicle = getUpdatedVehicle(vehicleEntry.getVehicle()); + String prefAdapter = vehicle.getProperties().get(Vehicle.PREFERRED_ADAPTER); + VehicleCommAdapterFactory factory = findFactoryWithName(prefAdapter); + if (factory != null && factory.providesAdapterFor(vehicle)) { + attachAdapterToVehicle(vehicleName, factory); + } + else { + if (!Strings.isNullOrEmpty(prefAdapter)) { + LOG.warn( + "Couldn't attach preferred adapter {} to {}. Attaching first available adapter.", + prefAdapter, + vehicleEntry.getVehicle().getName() + ); + } + List factories + = commAdapterRegistry.findFactoriesFor(vehicleEntry.getVehicle()); + if (!factories.isEmpty()) { + attachAdapterToVehicle(vehicleName, factories.get(0)); + } + } + } + + /** + * Automatically attach all vehicles to an adapter. + */ + public void autoAttachAllAdapters() { + vehicleEntryPool.getEntries().forEach((vehicleName, entry) -> { + autoAttachAdapterToVehicle(vehicleName); + }); + } + + /** + * Returns the attachment information for a vehicle. + * + * @param vehicleName The name of the vehicle. + * @return Attachment information about the vehicle. + */ + public VehicleAttachmentInformation getAttachmentInformation(String vehicleName) { + requireNonNull(vehicleName, "vehicleName"); + Assertions.checkArgument( + attachmentPool.get(vehicleName) != null, + "No attachment information for vehicle %s", + vehicleName + ); + + return attachmentPool.get(vehicleName); + } + + public Map getAttachmentPool() { + return attachmentPool; + } + + private void disableAndTerminateAdapter( + @Nonnull + VehicleEntry vehicleEntry + ) { + requireNonNull(vehicleEntry, "vehicleEntry"); + + VehicleCommAdapter commAdapter = vehicleEntry.getCommAdapter(); + if (commAdapter != null) { + commAdapter.disable(); + // Let the adapter know cleanup time is here. + commAdapter.terminate(); + } + } + + private void initAttachmentPool() { + vehicleEntryPool.getEntries().forEach((vehicleName, entry) -> { + List availableCommAdapters + = commAdapterRegistry.getFactories().stream() + .filter(f -> f.providesAdapterFor(entry.getVehicle())) + .map(f -> f.getDescription()) + .collect(Collectors.toList()); + + attachmentPool.put( + vehicleName, + new VehicleAttachmentInformation( + entry.getVehicle().getReference(), + availableCommAdapters, + new NullVehicleCommAdapterDescription() + ) + ); + }); + } + + private void updateAttachmentInformation(VehicleEntry entry) { + String vehicleName = entry.getVehicleName(); + VehicleCommAdapterFactory factory = entry.getCommAdapterFactory(); + VehicleAttachmentInformation newAttachment = attachmentPool.get(vehicleName) + .withAttachedCommAdapter(factory.getDescription()); + + attachmentPool.put(vehicleName, newAttachment); + + eventHandler.onEvent(new VehicleAttachmentEvent(vehicleName, newAttachment)); + if (entry.getCommAdapter() == null) { + // In case we are detached + eventHandler.onEvent(new ProcessModelEvent(vehicleName, new VehicleProcessModelTO())); + } + else { + eventHandler.onEvent( + new ProcessModelEvent( + vehicleName, + entry.getCommAdapter() + .createTransferableProcessModel() + ) + ); + } + } + + /** + * Returns a fresh copy of a vehicle from the kernel. + * + * @param vehicle The old vehicle instance. + * @return The fresh vehicle instance. + */ + private Vehicle getUpdatedVehicle( + @Nonnull + Vehicle vehicle + ) { + requireNonNull(vehicle, "vehicle"); + + return objectService.fetchObjects(Vehicle.class).stream() + .filter(updatedVehicle -> Objects.equals(updatedVehicle.getName(), vehicle.getName())) + .findFirst().orElse(vehicle); + } + + private void autoEnableAllAdapters() { + vehicleEntryPool.getEntries().values().stream() + .map(entry -> entry.getCommAdapter()) + .filter(adapter -> adapter != null) + .filter(adapter -> !adapter.isEnabled()) + .forEach(adapter -> adapter.enable()); + } + + private void detachAllAdapters() { + LOG.debug("Detaching vehicle communication adapters..."); + vehicleEntryPool.getEntries().forEach((vehicleName, entry) -> { + disableAndTerminateAdapter(entry); + }); + LOG.debug("Detached vehicle communication adapters"); + } + + @Nullable + private VehicleCommAdapterFactory findFactoryWithName( + @Nullable + String name + ) { + return commAdapterRegistry.getFactories().stream() + .filter(factory -> factory.getClass().getName().equals(name)) + .findFirst() + .orElse(null); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/controlcenter/vehicles/NullVehicleCommAdapterDescription.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/controlcenter/vehicles/NullVehicleCommAdapterDescription.java new file mode 100644 index 0000000..f6f9287 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/controlcenter/vehicles/NullVehicleCommAdapterDescription.java @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.controlcenter.vehicles; + +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; + +/** + * A {@link VehicleCommAdapterDescription} for no comm adapter. + */ +public class NullVehicleCommAdapterDescription + extends + VehicleCommAdapterDescription { + + /** + * Creates a new instance. + */ + public NullVehicleCommAdapterDescription() { + } + + @Override + public String getDescription() { + return "-"; + } + + @Override + public boolean isSimVehicleCommAdapter() { + return false; + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/controlcenter/vehicles/NullVehicleCommAdapterFactory.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/controlcenter/vehicles/NullVehicleCommAdapterFactory.java new file mode 100644 index 0000000..4bb58d3 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/controlcenter/vehicles/NullVehicleCommAdapterFactory.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.controlcenter.vehicles; + +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; +import org.opentcs.drivers.vehicle.VehicleCommAdapterFactory; + +/** + * A Vehicle adapter factory that creates no vehicles adapters. + */ +public class NullVehicleCommAdapterFactory + implements + VehicleCommAdapterFactory { + + /** + * Creates a new instance. + */ + public NullVehicleCommAdapterFactory() { + } + + @Override + public VehicleCommAdapterDescription getDescription() { + return new NullVehicleCommAdapterDescription(); + } + + @Override + public boolean providesAdapterFor(Vehicle vehicle) { + return false; + } + + @Override + public VehicleCommAdapter getAdapterFor(Vehicle vehicle) { + return null; + } + + @Override + public void initialize() { + } + + @Override + public boolean isInitialized() { + return true; + } + + @Override + public void terminate() { + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/controlcenter/vehicles/VehicleEntry.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/controlcenter/vehicles/VehicleEntry.java new file mode 100644 index 0000000..cfa7635 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/controlcenter/vehicles/VehicleEntry.java @@ -0,0 +1,193 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.controlcenter.vehicles; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; +import org.opentcs.drivers.vehicle.VehicleCommAdapterFactory; +import org.opentcs.drivers.vehicle.VehicleProcessModel; + +/** + * Represents a {@link Vehicle} in the {@link VehicleEntryPool}. + */ +public class VehicleEntry + implements + PropertyChangeListener { + + /** + * The vehicle this entry represents. + */ + private final Vehicle vehicle; + /** + * Used for implementing property change events. + */ + @SuppressWarnings("this-escape") + private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); + /** + * The process model for the vehicle. + */ + private VehicleProcessModel processModel; + /** + * The comm adapter factory for this vehicle. + */ + private VehicleCommAdapterFactory commAdapterFactory = new NullVehicleCommAdapterFactory(); + /** + * The comm adapter that is attached to this vehicle. + */ + private VehicleCommAdapter commAdapter; + + /** + * Creates a vehicle entry. + * + * @param vehicle The vehicle this entry represents. + */ + public VehicleEntry(Vehicle vehicle) { + this.vehicle = requireNonNull(vehicle, "vehicle"); + this.processModel = new VehicleProcessModel(vehicle); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (!(evt.getSource() instanceof VehicleProcessModel)) { + return; + } + + pcs.firePropertyChange(evt.getPropertyName(), evt.getOldValue(), evt.getNewValue()); + } + + /** + * Add a property change listener. + * + * @param listener The listener to add. + */ + public void addPropertyChangeListener(PropertyChangeListener listener) { + pcs.addPropertyChangeListener(listener); + } + + /** + * Remove a property change listener. + * + * @param listener The listener to remove. + */ + public void removePropertyChangeListener(PropertyChangeListener listener) { + pcs.removePropertyChangeListener(listener); + } + + /** + * Returns the vehicle that is represented by this entry. + * + * @return The vehicle that is represented by this entry. + */ + @Nonnull + public Vehicle getVehicle() { + return vehicle; + } + + /** + * Returns the name of the vehicle that is represented by this entry. + * + * @return The name of the vehicle that is represented by this entry. + */ + @Nonnull + public String getVehicleName() { + return vehicle.getName(); + } + + /** + * Returns the process model of the vehicle that is represented by this entry. + * + * @return The process model of the vehicle that is represented by this entry. + */ + @Nonnull + public VehicleProcessModel getProcessModel() { + return processModel; + } + + /** + * Sets the process model for the vehicle represented by this entry. + * + * @param processModel The new process model for the vehicle. + */ + public void setProcessModel( + @Nonnull + VehicleProcessModel processModel + ) { + VehicleProcessModel oldProcessModel = this.processModel; + this.processModel = requireNonNull(processModel, "processModel"); + + oldProcessModel.removePropertyChangeListener(this); + processModel.addPropertyChangeListener(this); + + pcs.firePropertyChange(Attribute.PROCESS_MODEL.name(), oldProcessModel, processModel); + } + + @Nonnull + public VehicleCommAdapterFactory getCommAdapterFactory() { + return commAdapterFactory; + } + + /** + * Sets the comm adapter factory for this entry. + * + * @param commAdapterFactory The new comm adapter factory. + */ + public void setCommAdapterFactory( + @Nonnull + VehicleCommAdapterFactory commAdapterFactory + ) { + VehicleCommAdapterFactory oldValue = this.commAdapterFactory; + this.commAdapterFactory = commAdapterFactory; + + pcs.firePropertyChange(Attribute.COMM_ADAPTER_FACTORY.name(), oldValue, commAdapterFactory); + } + + /** + * Returns the comm adapter factory for this entry. + * + * @return The comm adapter factory for this entry + */ + @Nullable + public VehicleCommAdapter getCommAdapter() { + return commAdapter; + } + + /** + * Sets the comm adapter for this entry. + * + * @param commAdapter The new comm adapter. + */ + public void setCommAdapter( + @Nullable + VehicleCommAdapter commAdapter + ) { + VehicleCommAdapter oldValue = this.commAdapter; + this.commAdapter = commAdapter; + + pcs.firePropertyChange(Attribute.COMM_ADAPTER.name(), oldValue, commAdapter); + } + + /** + * Enum elements used as notification arguments to specify which argument changed. + */ + public enum Attribute { + /** + * Indicates a change of the process model reference. + */ + PROCESS_MODEL, + /** + * Indicates a change of the comm adapter factory reference. + */ + COMM_ADAPTER_FACTORY, + /** + * Indicates a change of the comm adapter reference. + */ + COMM_ADAPTER + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/controlcenter/vehicles/VehicleEntryPool.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/controlcenter/vehicles/VehicleEntryPool.java new file mode 100644 index 0000000..1ba5712 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/controlcenter/vehicles/VehicleEntryPool.java @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.controlcenter.vehicles; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import java.util.Map; +import java.util.TreeMap; +import org.opentcs.components.Lifecycle; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Vehicle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides a pool of {@link VehicleEntry}s with an entry for every {@link Vehicle} object in the + * kernel. + */ +public class VehicleEntryPool + implements + Lifecycle { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(VehicleEntryPool.class); + /** + * The object service. + */ + private final TCSObjectService objectService; + /** + * The entries of this pool. + */ + private final Map entries = new TreeMap<>(); + /** + * Whether the pool is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param objectService The object service. + */ + @Inject + public VehicleEntryPool( + @Nonnull + TCSObjectService objectService + ) { + this.objectService = requireNonNull(objectService, "objectService"); + } + + @Override + public void initialize() { + if (isInitialized()) { + LOG.debug("Already initialized."); + return; + } + + objectService.fetchObjects(Vehicle.class).stream() + .forEach(vehicle -> entries.put(vehicle.getName(), new VehicleEntry(vehicle))); + LOG.debug("Initialized vehicle entry pool: {}", entries); + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + LOG.debug("Not initialized."); + return; + } + + entries.clear(); + initialized = false; + } + + /** + * Returns all entries in the pool. + * + * @return Map of vehicle names to their vehicle entries. + */ + @Nonnull + public Map getEntries() { + return entries; + } + + /** + * Returns the {@link VehicleEntry} for the given vehicle name. + * + * @param vehicleName The vehicle name to get the entry for. + * @return the vehicle entry for the given vehicle name. + */ + @Nullable + public VehicleEntry getEntryFor( + @Nonnull + String vehicleName + ) { + requireNonNull(vehicleName, "vehicleName"); + return entries.get(vehicleName); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/controlcenter/vehicles/package-info.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/controlcenter/vehicles/package-info.java new file mode 100644 index 0000000..e1fbe58 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/controlcenter/vehicles/package-info.java @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Classes of the kernel control center concerning vehicles/vehicle drivers. + * The GUI parts of the driver framework. + */ +package org.opentcs.kernel.extensions.controlcenter.vehicles; diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/watchdog/BlockConsistencyCheck.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/watchdog/BlockConsistencyCheck.java new file mode 100644 index 0000000..845d30e --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/watchdog/BlockConsistencyCheck.java @@ -0,0 +1,232 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.watchdog; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.data.model.Vehicle.IntegrationLevel.TO_BE_RESPECTED; +import static org.opentcs.data.model.Vehicle.IntegrationLevel.TO_BE_UTILIZED; + +import jakarta.inject.Inject; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import org.opentcs.components.Lifecycle; +import org.opentcs.components.kernel.services.NotificationService; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Block; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.notification.UserNotification; + +/** + * Periodically checks the occupancy status of single-vehicle blocks. + * + * This check will publish a user notification if a single-vehicle block is occupied by more than + * one vehicle. + * A single notification will be published when the violation is first detected. + * A second notification will be published when the violation changed or is resolved. + * + * The exact rules for when a violation notification should be sent are: + * + *
    + *
  • A violation should trigger a notification if a block is occupied by more than one vehicle + * and the set of vehicles occupying the block is different to the one in the previous iteration. + * The order of the occupants does not matter. + * (V1, V2) is the same as (V2, V1).
  • + *
  • If a block was previously occupied by more than one vehicle and is now occupied by one or no + * vehicle, a notification about the resultion of the situation is sent.
  • + *
+ * + * Examples: + * + *
    + *
  • A block previously occupied by (V1, V2) that is now occupied by (V1, V2, V3) should + * trigger a new violation notification.
  • + *
  • A block previously occupied by (V1, V2) that is now occupied by (V1) should trigger a + * resolution notification.
  • + *
  • A block previously occupied by (V1, V2) that is now still occupied by (V1, V2) or (V2, V1) + * should not trigger a new notification.
  • + *
+ */ +public class BlockConsistencyCheck + implements + Runnable, + Lifecycle { + + /** + * Notification source. + */ + private static final String NOTIFICATION_SOURCE = "Watchdog - Block consistency check"; + /** + * Object service to access the model. + */ + private final TCSObjectService objectService; + /** + * The service to send out user notifications. + */ + private final NotificationService notificationService; + /** + * The kernel executor. + */ + private final ScheduledExecutorService kernelExecutor; + /** + * The configuration. + */ + private final WatchdogConfiguration configuration; + /** + * Whether this check is initialized. + */ + private boolean initialized; + /** + * The Future created for the block check task. + */ + private ScheduledFuture scheduledFuture; + /** + * Holds currently known block occupations. + * Maps a block reference to a set of vehicles contained in that block. + */ + private Map, Set>> occupations + = new HashMap<>(); + + /** + * Creates a new instance. + * + * @param kernelExecutor The kernel executor. + * @param objectService The object service. + * @param notificationService The notification service. + * @param configuration The watchdog configuration. + */ + @Inject + public BlockConsistencyCheck( + @KernelExecutor + ScheduledExecutorService kernelExecutor, + TCSObjectService objectService, + NotificationService notificationService, + WatchdogConfiguration configuration + ) { + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + this.objectService = requireNonNull(objectService, "objectService"); + this.notificationService = requireNonNull(notificationService, "notificationService"); + this.configuration = requireNonNull(configuration, "configuration"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + scheduledFuture = kernelExecutor.scheduleAtFixedRate( + this, + configuration.blockConsistencyCheckInterval(), + configuration.blockConsistencyCheckInterval(), + TimeUnit.MILLISECONDS + ); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + if (scheduledFuture != null) { + scheduledFuture.cancel(true); + scheduledFuture = null; + } + + initialized = false; + } + + @Override + public void run() { + Map, Set>> currentOccupations + = findCurrentOccupations(); + + // Find new violations. + currentOccupations.entrySet().stream() + .filter(entry -> entry.getValue().size() > 1) + .filter(entry -> { + return !occupations.containsKey(entry.getKey()) + || !occupations.get(entry.getKey()).equals(entry.getValue()); + }) + .forEach(entry -> { + notificationService.publishUserNotification( + new UserNotification( + NOTIFICATION_SOURCE, + String.format( + "Block %s is overfull. Occupied by vehicles: %s", + entry.getKey().getName(), + entry.getValue().stream() + .map(vehicle -> vehicle.getName()) + .collect(Collectors.joining(", ")) + ), + UserNotification.Level.IMPORTANT + ) + ); + }); + + // Find resolved violations + occupations.entrySet().stream() + .filter(entry -> entry.getValue().size() > 1) + .filter(entry -> { + return !currentOccupations.containsKey(entry.getKey()) + || currentOccupations.get(entry.getKey()).size() <= 1; + }) + .forEach(entry -> { + notificationService.publishUserNotification( + new UserNotification( + NOTIFICATION_SOURCE, + String.format("Block %s is not overfull any more.", entry.getKey().getName()), + UserNotification.Level.IMPORTANT + ) + ); + }); + + occupations = currentOccupations; + } + + private Map, Set>> + findCurrentOccupations() { + Map, Set>> currentOccupations + = new HashMap<>(); + + Set blocks = objectService.fetchObjects(Block.class); + + objectService.fetchObjects(Vehicle.class) + .stream() + .filter(vehicle -> { + return vehicle.getIntegrationLevel() == TO_BE_RESPECTED + || vehicle.getIntegrationLevel() == TO_BE_UTILIZED; + }) + .filter(vehicle -> vehicle.getCurrentPosition() != null) + .forEach(vehicle -> { + Point currentPoint = objectService.fetchObject(Point.class, vehicle.getCurrentPosition()); + + blocks.stream() + .filter(block -> block.getType() == Block.Type.SINGLE_VEHICLE_ONLY) + .filter(block -> block.getMembers().contains(currentPoint.getReference())) + .forEach(block -> { + currentOccupations.putIfAbsent(block.getReference(), new HashSet<>()); + currentOccupations.get(block.getReference()).add(vehicle.getReference()); + }); + }); + + return currentOccupations; + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/watchdog/StrandedVehicleCheck.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/watchdog/StrandedVehicleCheck.java new file mode 100644 index 0000000..e8b3358 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/watchdog/StrandedVehicleCheck.java @@ -0,0 +1,179 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.watchdog; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import org.opentcs.components.Lifecycle; +import org.opentcs.components.kernel.services.NotificationService; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.data.notification.UserNotification; +import org.opentcs.kernel.extensions.watchdog.StrandedVehicles.VehicleSnapshot; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Checks for vehicles that are stranded for too long. + *

+ * A vehicle is considered "stranded" if one of the following conditions applies and lasts longer + * than a configurable time period (see + * {@link WatchdogConfiguration#strandedVehicleDurationThreshold()}): + *

+ *
    + *
  • The vehicle is idle and at a position that is not a parking position.
  • + *
  • The vehicle is idle and has a transport order assigned to it.
  • + *
+ * This check publishes a user notification when a vehicle is considered stranded and another user + * notification once a vehicle is no longer considered stranded. + */ +public class StrandedVehicleCheck + implements + Runnable, + Lifecycle { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(StrandedVehicleCheck.class); + /** + * Source for notifications. + */ + private static final String NOTIFICATION_SOURCE = "Watchdog - Stranded vehicle check"; + /** + * A formatter for timestamps. + */ + private static final DateTimeFormatter TIMESTAMP_FORMATTER + = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault()); + /** + * The service to send out user notifications. + */ + private final NotificationService notificationService; + /** + * The kernel executor. + */ + private final ScheduledExecutorService kernelExecutor; + /** + * The configuration. + */ + private final WatchdogConfiguration configuration; + /** + * Keeps track of stranded vehicles. + */ + private final StrandedVehicles stranded; + /** + * Whether this check is initialized. + */ + private boolean initialized; + /** + * The Future created for the block check task. + */ + private ScheduledFuture scheduledFuture; + + /** + * Creates a new instance. + * + * @param kernelExecutor The kernel executor. + * @param notificationService The notification service. + * @param configuration The watchdog configuration. + * @param stranded Keeps track of stranded vehicles. + */ + @Inject + public StrandedVehicleCheck( + @KernelExecutor + ScheduledExecutorService kernelExecutor, + NotificationService notificationService, + WatchdogConfiguration configuration, + StrandedVehicles stranded + ) { + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + this.notificationService = requireNonNull(notificationService, "notificationService"); + this.configuration = requireNonNull(configuration, "configuration"); + this.stranded = requireNonNull(stranded, "stranded"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + scheduledFuture = kernelExecutor.scheduleAtFixedRate( + this, + configuration.strandedVehicleCheckInterval(), + configuration.strandedVehicleCheckInterval(), + TimeUnit.MILLISECONDS + ); + + stranded.initialize(); + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + if (scheduledFuture != null) { + scheduledFuture.cancel(true); + scheduledFuture = null; + } + + stranded.terminate(); + initialized = false; + } + + @Override + public void run() { + long currentTime = System.currentTimeMillis(); + long strandedTimeThreshold = configuration.strandedVehicleDurationThreshold(); + + stranded.identifyStrandedVehicles(currentTime, strandedTimeThreshold); + Set newlyStrandedVehicles = stranded.newlyStrandedVehicles(); + Set noLongerStrandedVehicles = stranded.noLongerStrandedVehicles(); + + newlyStrandedVehicles + .forEach(vehicleSnapshot -> { + notificationService.publishUserNotification( + new UserNotification( + NOTIFICATION_SOURCE, + String.format( + "Vehicle '%s' is stranded since: %s", + vehicleSnapshot.getVehicle().getName(), + TIMESTAMP_FORMATTER.format( + Instant.ofEpochMilli(vehicleSnapshot.getLastRelevantStateChange()) + ) + ), + UserNotification.Level.INFORMATIONAL + ) + ); + }); + + noLongerStrandedVehicles + .forEach(vehicleSnapshot -> { + notificationService.publishUserNotification( + new UserNotification( + NOTIFICATION_SOURCE, + String.format( + "Vehicle '%s' is no longer stranded.", + vehicleSnapshot.getVehicle().getName() + ), + UserNotification.Level.INFORMATIONAL + ) + ); + }); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/watchdog/StrandedVehicles.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/watchdog/StrandedVehicles.java new file mode 100644 index 0000000..336a22b --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/watchdog/StrandedVehicles.java @@ -0,0 +1,267 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.watchdog; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.components.Lifecycle; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class to find out stranded vehicles. + */ +public class StrandedVehicles + implements + Lifecycle { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(StrandedVehicles.class); + /** + * Object service to access the model. + */ + private final TCSObjectService objectService; + /** + * Provider to get the current time. + */ + private final TimeProvider timeProvider; + /** + * Map to store the current snapshot for each vehicle. + */ + private final Map currentSnapshots = new HashMap<>(); + /** + * Map to store the previous snapshot for each vehicle. + */ + private final Map previousSnapshots = new HashMap<>(); + /** + * Whether this instance is initialized. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param objectService The object service. + * @param timeProvider Provider to get the current time. + */ + @Inject + public StrandedVehicles( + TCSObjectService objectService, + TimeProvider timeProvider + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.timeProvider = requireNonNull(timeProvider, "timeProvider"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + long currentTime = timeProvider.getCurrentTime(); + objectService.fetchObjects(Vehicle.class).forEach(vehicle -> { + VehicleSnapshot vehicleSnapshot = new VehicleSnapshot(vehicle); + vehicleSnapshot.setLastRelevantStateChange(currentTime); + currentSnapshots.put(vehicle.getName(), vehicleSnapshot); + }); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + currentSnapshots.clear(); + previousSnapshots.clear(); + + initialized = false; + } + + /** + * Identifies stranded vehicles. + * + * @param currentTime The current time. + * @param strandedDurationThreshold The duration that a vehicle must be in a stranded state to + * actually be considered stranded. + */ + public void identifyStrandedVehicles( + long currentTime, + long strandedDurationThreshold + ) { + LOG.debug("Identifying stranded vehicles..."); + previousSnapshots.clear(); + previousSnapshots.putAll(currentSnapshots); + currentSnapshots.clear(); + + objectService.fetchObjects(Vehicle.class) + .stream() + .forEach(vehicle -> { + VehicleSnapshot previousSnapshot = previousSnapshots.get(vehicle.getName()); + VehicleSnapshot currentSnapshot = new VehicleSnapshot(vehicle); + + if (vehicle.getState() != Vehicle.State.IDLE + || relevantPropertiesChanged(previousSnapshot, currentSnapshot)) { + currentSnapshot.setLastRelevantStateChange(currentTime); + currentSnapshot.setStranded(false); + } + else if (isInStrandedState(vehicle)) { + LOG.debug("Checking if vehicle '{}' is stranded long enough...", vehicle); + long lastRelevantStateChange = previousSnapshot.getLastRelevantStateChange(); + currentSnapshot.setLastRelevantStateChange(lastRelevantStateChange); + + long strandedDuration = currentTime - lastRelevantStateChange; + currentSnapshot.setStranded(strandedDuration >= strandedDurationThreshold); + } + else { + // The state of the vehicle has not effectively changed. Therefore, apply values from + // the previous snapshot. + currentSnapshot.setLastRelevantStateChange( + previousSnapshot.getLastRelevantStateChange() + ); + currentSnapshot.setStranded(previousSnapshot.isStranded()); + } + + LOG.debug("Snapshot of vehicle '{}': {}", vehicle.getName(), currentSnapshot); + currentSnapshots.put(vehicle.getName(), currentSnapshot); + }); + } + + /** + * Returns vehicles that are currently considered newly stranded (i.e., vehicles that were + * previously not considered stranded, but are now considered stranded). + * + * @return The set of vehicles that are considered newly stranded. + */ + public Set newlyStrandedVehicles() { + return currentSnapshots.values().stream() + .filter( + vehicleSnapshot -> !previousSnapshots.get(vehicleSnapshot.getVehicle().getName()) + .isStranded() + && vehicleSnapshot.isStranded() + ) + .collect(Collectors.toSet()); + } + + /** + * Returns vehicles that are currently considered no longer stranded (i.e., vehicles that were + * previously considered stranded, but are now not considered stranded). + * + * @return The set of vehicles that are no longer considered stranded. + */ + public Set noLongerStrandedVehicles() { + return currentSnapshots.values().stream() + .filter( + vehicleSnapshot -> previousSnapshots.get(vehicleSnapshot.getVehicle().getName()) + .isStranded() + && !vehicleSnapshot.isStranded() + ) + .collect(Collectors.toSet()); + } + + private boolean isInStrandedState(Vehicle vehicle) { + return isIdleAtNoParkingPosition(vehicle) || isIdleWithTransportOrder(vehicle); + } + + private boolean isIdleAtNoParkingPosition(Vehicle vehicle) { + return vehicle.hasState(Vehicle.State.IDLE) + && vehicle.getCurrentPosition() != null + && objectService.fetchObject(Point.class, vehicle.getCurrentPosition()) + .getType() != Point.Type.PARK_POSITION; + } + + private boolean isIdleWithTransportOrder(Vehicle vehicle) { + return vehicle.hasState(Vehicle.State.IDLE) + && vehicle.getTransportOrder() != null; + } + + private boolean relevantPropertiesChanged( + VehicleSnapshot previousSnapshot, + VehicleSnapshot currentSnapshot + ) { + return previousSnapshot.getLastState() != currentSnapshot.getLastState() + || !Objects.equals(previousSnapshot.getLastPosition(), currentSnapshot.getLastPosition()) + || !Objects.equals( + previousSnapshot.getVehicle().getTransportOrder(), + currentSnapshot.getVehicle().getTransportOrder() + ); + } + + /** + * A snapshot of a vehicle's state with additional information on whether the given vehicle is + * considered stranded and when the last state change (with regard to the "stranded" state) + * occurred. + */ + public static class VehicleSnapshot { + + private final Vehicle vehicle; + private long lastRelevantStateChange; + private boolean stranded; + + /** + * Creates a new instance. + * + * @param vehicle Vehicle to be stored. + */ + public VehicleSnapshot(Vehicle vehicle) { + this.vehicle = requireNonNull(vehicle, "vehicle"); + } + + public boolean isStranded() { + return stranded; + } + + public void setStranded(boolean stranded) { + this.stranded = stranded; + } + + public Vehicle getVehicle() { + return vehicle; + } + + public long getLastRelevantStateChange() { + return lastRelevantStateChange; + } + + public void setLastRelevantStateChange(long lastRelevantStateChange) { + this.lastRelevantStateChange = lastRelevantStateChange; + } + + public TCSObjectReference getLastPosition() { + return vehicle.getCurrentPosition(); + } + + public Vehicle.State getLastState() { + return vehicle.getState(); + } + + @Override + public String toString() { + return "VehicleSnapshot{" + + "stranded=" + stranded + + ", lastRelevantStateChange=" + lastRelevantStateChange + + ", vehicle=" + vehicle + + '}'; + } + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/watchdog/TimeProvider.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/watchdog/TimeProvider.java new file mode 100644 index 0000000..ed629e7 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/watchdog/TimeProvider.java @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.watchdog; + +/** + * Provides the current time. + */ +public class TimeProvider { + + /** + * Creates a new instance. + */ + public TimeProvider() { + } + + /** + * Returns the current time. + * + * @return The current time. + */ + public long getCurrentTime() { + return System.currentTimeMillis(); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/watchdog/Watchdog.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/watchdog/Watchdog.java new file mode 100644 index 0000000..c9e9e63 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/watchdog/Watchdog.java @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.watchdog; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.KernelExtension; + +/** + * A kernel extension to periodicly monitor the state of the kernel with check tasks. + */ +public class Watchdog + implements + KernelExtension { + + /** + * Whether this kernel extension is initialized. + */ + private boolean initialized; + /** + * The task to check for consistency of blocks. + */ + private final BlockConsistencyCheck blockCheck; + /** + * The task to check for stranded vehicles. + */ + private final StrandedVehicleCheck strandedVehicleCheck; + + /** + * Creates a new instance. + * + * @param blockCheck The block check task. + * @param strandedVehicleCheck The stranded vehicle check task. + */ + @Inject + public Watchdog( + BlockConsistencyCheck blockCheck, + StrandedVehicleCheck strandedVehicleCheck + ) { + this.blockCheck = requireNonNull(blockCheck, "blockCheck"); + this.strandedVehicleCheck = requireNonNull(strandedVehicleCheck, "strandedVehicleCheck"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + blockCheck.initialize(); + strandedVehicleCheck.initialize(); + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + blockCheck.terminate(); + strandedVehicleCheck.terminate(); + initialized = false; + } + +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/watchdog/WatchdogConfiguration.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/watchdog/WatchdogConfiguration.java new file mode 100644 index 0000000..c575e3c --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/extensions/watchdog/WatchdogConfiguration.java @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.watchdog; + +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; + +/** + * Configuration for the watchdog extension. + */ +@ConfigurationPrefix(WatchdogConfiguration.PREFIX) +public interface WatchdogConfiguration { + + /** + * This configuration's prefix. + */ + String PREFIX = "watchdog"; + + @ConfigurationEntry( + type = "Integer", + description = "The interval (in milliseconds) in which to check for block consistency.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "1_block" + ) + int blockConsistencyCheckInterval(); + + @ConfigurationEntry( + type = "Integer", + description = "The interval (in milliseconds) in which to check for stranded vehicles.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "2_stranded_vehicle_0" + ) + int strandedVehicleCheckInterval(); + + @ConfigurationEntry( + type = "Integer", + description = "The duration (in milliseconds) that a vehicle must be in a _stranded_ state " + + "to actually be considered stranded.", + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "2_stranded_vehicle_1" + ) + int strandedVehicleDurationThreshold(); +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/package-info.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/package-info.java new file mode 100644 index 0000000..c1e7b2c --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/package-info.java @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * The openTCS kernel, its interface for client applications and closely related + * functionality. + */ +package org.opentcs.kernel; diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/DefaultPeripheralController.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/DefaultPeripheralController.java new file mode 100644 index 0000000..109918f --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/DefaultPeripheralController.java @@ -0,0 +1,178 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.peripherals; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkState; + +import com.google.inject.assistedinject.Assisted; +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.Objects; +import org.opentcs.components.kernel.services.InternalPeripheralService; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.PeripheralInformation; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.drivers.peripherals.PeripheralAdapterCommand; +import org.opentcs.drivers.peripherals.PeripheralCommAdapter; +import org.opentcs.drivers.peripherals.PeripheralController; +import org.opentcs.drivers.peripherals.PeripheralJobCallback; +import org.opentcs.drivers.peripherals.PeripheralProcessModel; +import org.opentcs.drivers.peripherals.management.PeripheralProcessModelEvent; +import org.opentcs.util.ExplainedBoolean; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Realizes a bidirectional connection between the kernel and a comm adapter controlling a + * peripheral device. + */ +public class DefaultPeripheralController + implements + PeripheralController, + EventHandler { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(DefaultPeripheralController.class); + /** + * The location representing the peripheral device controlled by this controller/the comm adapter. + */ + private final TCSResourceReference location; + /** + * The comm adapter controling the peripheral device. + */ + private final PeripheralCommAdapter commAdapter; + /** + * The peripheral service to use. + */ + private final InternalPeripheralService peripheralService; + /** + * The event bus we should register with and send events to. + */ + private final EventBus eventBus; + /** + * Indicates whether this controller is initialized. + */ + private boolean initialized; + + /** + * Creates a new DefaultPeripheralController. + * + * @param location The location representing the peripheral device. + * @param commAdapter The comm adapter that controls the peripheral device. + * @param peripheralService The peripheral service to be used. + * @param eventBus The event bus to be used. + */ + @Inject + public DefaultPeripheralController( + @Assisted + @Nonnull + TCSResourceReference location, + @Assisted + @Nonnull + PeripheralCommAdapter commAdapter, + @Nonnull + InternalPeripheralService peripheralService, + @Nonnull + @ApplicationEventBus + EventBus eventBus + ) { + this.location = requireNonNull(location, "location"); + this.commAdapter = requireNonNull(commAdapter, "commAdapter"); + this.peripheralService = requireNonNull(peripheralService, "peripheralService"); + this.eventBus = requireNonNull(eventBus, "eventBus"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + eventBus.subscribe(this); + + updatePeripheralState(commAdapter.getProcessModel().getState()); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + updatePeripheralState(PeripheralInformation.State.UNKNOWN); + + eventBus.unsubscribe(this); + + initialized = false; + } + + @Override + public void onEvent(Object event) { + if (!(event instanceof PeripheralProcessModelEvent)) { + return; + } + + PeripheralProcessModelEvent processModelEvent = (PeripheralProcessModelEvent) event; + if (Objects.equals( + processModelEvent.getAttributeChanged(), + PeripheralProcessModel.Attribute.STATE.name() + ) + && Objects.equals(processModelEvent.getLocation(), location)) { + updatePeripheralState(processModelEvent.getProcessModel().getState()); + } + } + + @Override + public void process(PeripheralJob job, PeripheralJobCallback callback) + throws IllegalStateException { + requireNonNull(job, "job"); + requireNonNull(callback, "callback"); + + ExplainedBoolean canProcess = canProcess(job); + checkState( + canProcess.getValue(), + "%s: Can't process job: %s", + location.getName(), + canProcess.getReason() + ); + + LOG.debug("{}: Handing job to comm adapter: {}", location.getName(), job); + commAdapter.process(job, callback); + } + + @Override + public void abortJob() { + commAdapter.abortJob(); + } + + @Override + public ExplainedBoolean canProcess(PeripheralJob job) { + requireNonNull(job, "job"); + return commAdapter.canProcess(job); + } + + @Override + public void sendCommAdapterCommand(PeripheralAdapterCommand command) { + requireNonNull(command, "command"); + commAdapter.execute(command); + } + + private void updatePeripheralState(PeripheralInformation.State newState) { + requireNonNull(newState, "newState"); + peripheralService.updatePeripheralState(location, newState); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/DefaultPeripheralControllerPool.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/DefaultPeripheralControllerPool.java new file mode 100644 index 0000000..ec0c870 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/DefaultPeripheralControllerPool.java @@ -0,0 +1,192 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.peripherals; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.inject.Inject; +import java.util.HashMap; +import java.util.Map; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.drivers.peripherals.PeripheralCommAdapter; +import org.opentcs.drivers.peripherals.PeripheralController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Maintains associations of {@link Location}, {@link PeripheralController} and + * {@link PeripheralCommAdapter}. + */ +public class DefaultPeripheralControllerPool + implements + LocalPeripheralControllerPool { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(DefaultPeripheralControllerPool.class); + /** + * The object service to use. + */ + private final TCSObjectService objectService; + /** + * A factory for peripheral controllers. + */ + private final PeripheralControllerFactory controllerFactory; + /** + * The entries of this pool mapped to the corresponding locations. + */ + private final Map, PoolEntry> poolEntries = new HashMap<>(); + /** + * Indicates whether this component is initialized. + */ + private boolean initialized; + + /** + * Creates a new DefaultPeripheralControllerPool. + * + * @param objectService The object service to be used. + * @param controllerFactory The controller factory to be used. + */ + @Inject + public DefaultPeripheralControllerPool( + TCSObjectService objectService, + PeripheralControllerFactory controllerFactory + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.controllerFactory = requireNonNull(controllerFactory, "controllerFactory"); + } + + @Override + public void initialize() { + if (isInitialized()) { + LOG.debug("Already initialized, doing nothing."); + return; + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + LOG.debug("Not initialized, doing nothing."); + return; + } + // Detach all peripherals. + for (PoolEntry curEntry : poolEntries.values()) { + curEntry.controller.terminate(); + } + poolEntries.clear(); + + initialized = false; + } + + @Override + public PeripheralController getPeripheralController(TCSResourceReference locationRef) + throws IllegalArgumentException { + requireNonNull(locationRef, "locationRef"); + checkArgument( + poolEntries.containsKey(locationRef), + "No controller present for %s", + locationRef.getName() + ); + + return poolEntries.get(locationRef).getController(); + } + + @Override + public void attachPeripheralController( + TCSResourceReference locationRef, + PeripheralCommAdapter commAdapter + ) + throws IllegalArgumentException { + requireNonNull(locationRef, "locationRef"); + requireNonNull(commAdapter, "commAdapter"); + + if (poolEntries.containsKey(locationRef)) { + LOG.warn("{}: Peripheral controller already attached, doing nothing.", locationRef.getName()); + return; + } + + Location location = objectService.fetchObject(Location.class, locationRef); + checkArgument(location != null, "No such location: %s", locationRef.getName()); + + LOG.debug("{}: Attaching controller...", locationRef.getName()); + PeripheralController controller = controllerFactory.createPeripheralController( + locationRef, + commAdapter + ); + poolEntries.put(locationRef, new PoolEntry(locationRef, controller, commAdapter)); + controller.initialize(); + } + + @Override + public void detachPeripheralController(TCSResourceReference locationRef) { + requireNonNull(locationRef, "locationRef"); + + if (!poolEntries.containsKey(locationRef)) { + LOG.debug("{}: No peripheral controller attached, doing nothing.", locationRef.getName()); + return; + } + + LOG.debug("{}: Detaching controller...", locationRef.getName()); + poolEntries.remove(locationRef).getController().terminate(); + } + + /** + * An entry in this controller pool. + */ + private static class PoolEntry { + + /** + * The location. + */ + private final TCSResourceReference location; + /** + * The peripheral controller associated with the location. + */ + private final PeripheralController controller; + /** + * The comm adapter associated with the location. + */ + private final PeripheralCommAdapter commAdapter; + + /** + * Creates a new pool entry. + * + * @param location The location. + * @param controller The peripheral controller associated with the location. + * @param cmmmAdapter The comm adapter associated with the location. + */ + private PoolEntry( + TCSResourceReference location, + PeripheralController controller, + PeripheralCommAdapter cmmmAdapter + ) { + this.location = requireNonNull(location, "location"); + this.controller = requireNonNull(controller, "controller"); + this.commAdapter = requireNonNull(cmmmAdapter, "cmmmAdapter"); + } + + public TCSResourceReference getLocation() { + return location; + } + + public PeripheralController getController() { + return controller; + } + + public PeripheralCommAdapter getCommAdapter() { + return commAdapter; + } + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/LocalPeripheralControllerPool.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/LocalPeripheralControllerPool.java new file mode 100644 index 0000000..35cf3ac --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/LocalPeripheralControllerPool.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.peripherals; + +import org.opentcs.components.Lifecycle; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.drivers.peripherals.PeripheralCommAdapter; +import org.opentcs.drivers.peripherals.PeripheralControllerPool; + +/** + * Manages the attachment of peripheral controllers to locations and peripheral comm adapters. + */ +public interface LocalPeripheralControllerPool + extends + PeripheralControllerPool, + Lifecycle { + + /** + * Associates a peripheral controller with a location and a comm adapter. + * + * @param location The reference to the location. + * @param commAdapter The comm adapter that is going to control the peripheral deivce. + * @throws IllegalArgumentException If the referenced location does not exist. + */ + void attachPeripheralController( + TCSResourceReference location, + PeripheralCommAdapter commAdapter + ) + throws IllegalArgumentException; + + /** + * Disassociates a peripheral controller and a comm adapter from a location. + * + * @param location The reference to the location. + */ + void detachPeripheralController(TCSResourceReference location); +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/NullPeripheralCommAdapter.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/NullPeripheralCommAdapter.java new file mode 100644 index 0000000..075e5ed --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/NullPeripheralCommAdapter.java @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.peripherals; + +import jakarta.annotation.Nonnull; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.drivers.peripherals.PeripheralAdapterCommand; +import org.opentcs.drivers.peripherals.PeripheralCommAdapter; +import org.opentcs.drivers.peripherals.PeripheralJobCallback; +import org.opentcs.drivers.peripherals.PeripheralProcessModel; +import org.opentcs.util.ExplainedBoolean; + +/** + * A {@link PeripheralCommAdapter} implementation that is doing nothing. + */ +public class NullPeripheralCommAdapter + implements + PeripheralCommAdapter { + + /** + * The process model. + */ + private final PeripheralProcessModel processModel; + + /** + * Creates a new instance. + * + * @param location The reference to the location this adapter is attached to. + */ + public NullPeripheralCommAdapter( + @Nonnull + TCSResourceReference location + ) { + this.processModel = new PeripheralProcessModel(location); + } + + @Override + public void initialize() { + } + + @Override + public boolean isInitialized() { + return true; + } + + @Override + public void terminate() { + } + + @Override + public void enable() { + } + + @Override + public void disable() { + } + + @Override + public boolean isEnabled() { + return false; + } + + @Override + public PeripheralProcessModel getProcessModel() { + return processModel; + } + + @Override + public ExplainedBoolean canProcess(PeripheralJob job) { + return new ExplainedBoolean(false, "Can't process any jobs."); + } + + @Override + public void process(PeripheralJob job, PeripheralJobCallback callback) { + } + + @Override + public void abortJob() { + } + + @Override + public void execute(PeripheralAdapterCommand command) { + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/NullPeripheralCommAdapterFactory.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/NullPeripheralCommAdapterFactory.java new file mode 100644 index 0000000..0faf66a --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/NullPeripheralCommAdapterFactory.java @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.peripherals; + +import org.opentcs.common.peripherals.NullPeripheralCommAdapterDescription; +import org.opentcs.data.model.Location; +import org.opentcs.drivers.peripherals.PeripheralCommAdapter; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterFactory; + +/** + * A factory for {@link NullPeripheralCommAdapter}s. + */ +public class NullPeripheralCommAdapterFactory + implements + PeripheralCommAdapterFactory { + + /** + * Creates a new NullPeripheralCommAdapterFactory. + */ + public NullPeripheralCommAdapterFactory() { + } + + @Override + public PeripheralCommAdapterDescription getDescription() { + return new NullPeripheralCommAdapterDescription(); + } + + @Override + public boolean providesAdapterFor(Location location) { + return true; + } + + @Override + public PeripheralCommAdapter getAdapterFor(Location location) { + return new NullPeripheralCommAdapter(location.getReference()); + } + + @Override + public void initialize() { + } + + @Override + public boolean isInitialized() { + return true; + } + + @Override + public void terminate() { + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/PeripheralAttachmentManager.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/PeripheralAttachmentManager.java new file mode 100644 index 0000000..7b8f13b --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/PeripheralAttachmentManager.java @@ -0,0 +1,282 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.peripherals; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.List; +import org.opentcs.components.Lifecycle; +import org.opentcs.components.kernel.services.InternalPeripheralService; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.drivers.peripherals.PeripheralCommAdapter; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterFactory; +import org.opentcs.drivers.peripherals.PeripheralProcessModel; +import org.opentcs.drivers.peripherals.management.PeripheralAttachmentEvent; +import org.opentcs.drivers.peripherals.management.PeripheralAttachmentInformation; +import org.opentcs.drivers.peripherals.management.PeripheralProcessModelEvent; +import org.opentcs.kernel.KernelApplicationConfiguration; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages attachment and detachment of peripheral communication adapters to location. + */ +public class PeripheralAttachmentManager + implements + Lifecycle { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PeripheralAttachmentManager.class); + /** + * This class's configuration. + */ + private final KernelApplicationConfiguration configuration; + /** + * The peripheral service. + */ + private final InternalPeripheralService peripheralService; + /** + * The peripheral controller pool. + */ + private final LocalPeripheralControllerPool controllerPool; + /** + * The peripheral comm adapter registry. + */ + private final PeripheralCommAdapterRegistry commAdapterRegistry; + /** + * The pool of peripheral entries. + */ + private final PeripheralEntryPool peripheralEntryPool; + /** + * The handler to send events to. + */ + private final EventHandler eventHandler; + /** + * Whether the attachment manager is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param peripheralService The peripheral service. + * @param controllerPool The peripheral controller pool. + * @param commAdapterRegistry The peripheral comm adapter registry. + * @param peripheralEntryPool The pool of peripheral entries. + * @param eventHandler The handler to send events to. + * @param configuration This class's configuration. + */ + @Inject + public PeripheralAttachmentManager( + @Nonnull + InternalPeripheralService peripheralService, + @Nonnull + LocalPeripheralControllerPool controllerPool, + @Nonnull + PeripheralCommAdapterRegistry commAdapterRegistry, + @Nonnull + PeripheralEntryPool peripheralEntryPool, + @Nonnull + @ApplicationEventBus + EventHandler eventHandler, + @Nonnull + KernelApplicationConfiguration configuration + ) { + this.peripheralService = requireNonNull(peripheralService, "peripheralService"); + this.controllerPool = requireNonNull(controllerPool, "controllerPool"); + this.commAdapterRegistry = requireNonNull(commAdapterRegistry, "commAdapterRegistry"); + this.peripheralEntryPool = requireNonNull(peripheralEntryPool, "peripheralEntryPool"); + this.eventHandler = requireNonNull(eventHandler, "eventHandler"); + this.configuration = requireNonNull(configuration, "configuration"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + commAdapterRegistry.initialize(); + peripheralEntryPool.initialize(); + + autoAttachAllAdapters(); + LOG.debug("Locations attached: {}", peripheralEntryPool.getEntries()); + + if (configuration.autoEnablePeripheralDriversOnStartup()) { + autoEnableAllAdapters(); + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + LOG.debug("Not initialized."); + return; + } + + // Disable and terminate all attached drivers to clean up. + disableAndTerminateAllAdapters(); + peripheralEntryPool.terminate(); + commAdapterRegistry.terminate(); + + initialized = false; + } + + /** + * Attaches a peripheral comm adapter to a location. + * + * @param location The location to attach to. + * @param description The description of the comm adapter to attach. + */ + public void attachAdapterToLocation( + @Nonnull + TCSResourceReference location, + @Nonnull + PeripheralCommAdapterDescription description + ) { + requireNonNull(location, "location"); + requireNonNull(description, "description"); + + attachAdapterToLocation( + peripheralEntryPool.getEntryFor(location), + commAdapterRegistry.findFactoryFor(description) + ); + } + + /** + * Returns the attachment information for a location. + * + * @param location The location to get attachment information about. + * @return The attachment information for a location. + */ + @Nonnull + public PeripheralAttachmentInformation getAttachmentInformation( + @Nonnull + TCSResourceReference location + ) { + requireNonNull(location, "location"); + + PeripheralEntry entry = peripheralEntryPool.getEntryFor(location); + return new PeripheralAttachmentInformation( + entry.getLocation(), + entry.getAvailableCommAdapters(), + entry.getCommAdapterFactory().getDescription() + ); + } + + private void attachAdapterToLocation( + PeripheralEntry entry, + PeripheralCommAdapterFactory factory + ) { + requireNonNull(entry, "entry"); + requireNonNull(factory, "factory"); + + LOG.info( + "Attaching peripheral comm adapter: '{}' -- '{}'...", + entry.getLocation().getName(), + factory.getClass().getName() + ); + + Location location = peripheralService.fetchObject(Location.class, entry.getLocation()); + PeripheralCommAdapter commAdapter = factory.getAdapterFor(location); + if (commAdapter == null) { + LOG.warn( + "Factory {} did not provide adapter for location {}, ignoring.", + factory, + entry.getLocation().getName() + ); + return; + } + + // Perform a cleanup for the old adapter. + disableAndTerminateAdapter(entry); + controllerPool.detachPeripheralController(entry.getLocation()); + + commAdapter.initialize(); + controllerPool.attachPeripheralController(entry.getLocation(), commAdapter); + + entry.setCommAdapterFactory(factory); + entry.setCommAdapter(commAdapter); + + // Publish events about the new attached adapter. + eventHandler.onEvent( + new PeripheralAttachmentEvent( + entry.getLocation(), + new PeripheralAttachmentInformation( + entry.getLocation(), + entry.getAvailableCommAdapters(), + entry.getCommAdapterFactory().getDescription() + ) + ) + ); + eventHandler.onEvent( + new PeripheralProcessModelEvent( + entry.getLocation(), + PeripheralProcessModel.Attribute.LOCATION.name(), + entry.getProcessModel() + ) + ); + } + + private void autoAttachAdapterToLocation(PeripheralEntry peripheralEntry) { + // Do not auto-attach if there is already a (real) comm adapter attached to the location. + if (!(peripheralEntry.getCommAdapter() instanceof NullPeripheralCommAdapter)) { + return; + } + + Location location = peripheralService.fetchObject( + Location.class, + peripheralEntry.getLocation() + ); + List factories = commAdapterRegistry.findFactoriesFor(location); + if (!factories.isEmpty()) { + LOG.debug( + "Attaching {} to first available adapter: {}.", + peripheralEntry.getLocation().getName(), + factories.get(0).getDescription().getDescription() + ); + attachAdapterToLocation(peripheralEntry, factories.get(0)); + } + } + + private void autoAttachAllAdapters() { + peripheralEntryPool.getEntries().forEach((location, entry) -> { + autoAttachAdapterToLocation(entry); + }); + } + + private void disableAndTerminateAdapter(PeripheralEntry peripheralEntry) { + peripheralEntry.getCommAdapter().disable(); + peripheralEntry.getCommAdapter().terminate(); + } + + private void autoEnableAllAdapters() { + peripheralEntryPool.getEntries().values().stream() + .map(entry -> entry.getCommAdapter()) + .filter(adapter -> !adapter.isEnabled()) + .forEach(adapter -> adapter.enable()); + } + + private void disableAndTerminateAllAdapters() { + LOG.debug("Detaching peripheral communication adapters..."); + peripheralEntryPool.getEntries().forEach((location, entry) -> { + disableAndTerminateAdapter(entry); + }); + LOG.debug("Detached peripheral communication adapters"); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/PeripheralCommAdapterRegistry.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/PeripheralCommAdapterRegistry.java new file mode 100644 index 0000000..57bb1ad --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/PeripheralCommAdapterRegistry.java @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.peripherals; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.components.Lifecycle; +import org.opentcs.data.model.Location; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A registry for all peripheral communication adapters in the system. + */ +public class PeripheralCommAdapterRegistry + implements + Lifecycle { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PeripheralCommAdapterRegistry.class); + /** + * The registered factories. + */ + private final Map factories + = new HashMap<>(); + /** + * Indicates whether this component is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new registry. + * + * @param factories The peripheral comm adapter factories. + */ + @Inject + public PeripheralCommAdapterRegistry(Set factories) { + requireNonNull(factories, "factories"); + for (PeripheralCommAdapterFactory factory : factories) { + LOG.info( + "Setting up peripheral communication adapter factory: {}", + factory.getClass().getName() + ); + this.factories.put(factory.getDescription(), factory); + } + } + + @Override + public void initialize() { + if (initialized) { + return; + } + + for (PeripheralCommAdapterFactory factory : factories.values()) { + factory.initialize(); + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!initialized) { + return; + } + + for (PeripheralCommAdapterFactory factory : factories.values()) { + factory.terminate(); + } + + initialized = false; + } + + /** + * Returns all registered factories that can provide peripheral communication adapters. + * + * @return All registered factories that can provide peripheral communication adapters. + */ + public List getFactories() { + return new ArrayList<>(factories.values()); + } + + /** + * Returns the factory for the given description. + * + * @param description The description to get the factory for. + * @return The factory for the given description. + */ + @Nonnull + public PeripheralCommAdapterFactory findFactoryFor( + @Nonnull + PeripheralCommAdapterDescription description + ) { + requireNonNull(description, "description"); + checkArgument( + factories.get(description) != null, + "No factory for description %s", + description + ); + + return factories.get(description); + } + + /** + * Returns a set of factories that can provide communication adapters for the given location. + * + * @param location The location to find communication adapters/factories for. + * @return A set of factories that can provide communication adapters for the given location. + */ + public List findFactoriesFor(Location location) { + requireNonNull(location, "location"); + + return factories.values().stream() + .filter(factory -> factory.providesAdapterFor(location)) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/PeripheralControllerFactory.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/PeripheralControllerFactory.java new file mode 100644 index 0000000..1489243 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/PeripheralControllerFactory.java @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.peripherals; + +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.drivers.peripherals.PeripheralCommAdapter; +import org.opentcs.drivers.peripherals.PeripheralController; + +/** + * A factory for {@link PeripheralController} instances. + */ +public interface PeripheralControllerFactory { + + /** + * Creates a new peripheral controller for the given location and communication adapter. + * + * @param location The location. + * @param commAdapter The communication adapter. + * @return A new peripheral controller. + */ + DefaultPeripheralController createPeripheralController( + TCSResourceReference location, + PeripheralCommAdapter commAdapter + ); +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/PeripheralEntry.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/PeripheralEntry.java new file mode 100644 index 0000000..87f4822 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/PeripheralEntry.java @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.peripherals; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.List; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.drivers.peripherals.PeripheralCommAdapter; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterFactory; +import org.opentcs.drivers.peripherals.PeripheralProcessModel; + +/** + * An entry for a peripheral device represented by a {@link Location}. + */ +public class PeripheralEntry { + + /** + * The available comm adapters for this entry. + */ + private final List availableCommAdapters; + /** + * The peripheral comm adapter factory that created this entry's comm adapter instance. + */ + private PeripheralCommAdapterFactory commAdapterFactory = new NullPeripheralCommAdapterFactory(); + /** + * The comm adapter instance for this entry. + */ + private PeripheralCommAdapter commAdapter; + + /** + * Creates a new instance. + * + * @param location The location representing the peripheral device. + * @param availableCommAdapters The available comm adapters for this entry. + */ + public PeripheralEntry( + @Nonnull + Location location, + @Nonnull + List availableCommAdapters + ) { + requireNonNull(location, "location"); + this.availableCommAdapters = requireNonNull(availableCommAdapters, "availableCommAdapters"); + this.commAdapter = commAdapterFactory.getAdapterFor(location); + } + + @Nonnull + public PeripheralProcessModel getProcessModel() { + return commAdapter.getProcessModel(); + } + + @Nonnull + public TCSResourceReference getLocation() { + return getProcessModel().getLocation(); + } + + @Nonnull + public List getAvailableCommAdapters() { + return availableCommAdapters; + } + + @Nonnull + public PeripheralCommAdapterFactory getCommAdapterFactory() { + return commAdapterFactory; + } + + public void setCommAdapterFactory( + @Nonnull + PeripheralCommAdapterFactory commAdapterFactory + ) { + this.commAdapterFactory = requireNonNull(commAdapterFactory, "commAdapterFactory"); + } + + @Nonnull + public PeripheralCommAdapter getCommAdapter() { + return commAdapter; + } + + public void setCommAdapter( + @Nonnull + PeripheralCommAdapter commAdapter + ) { + this.commAdapter = requireNonNull(commAdapter, "commAdapter"); + } + + @Override + public String toString() { + return "PeripheralEntry{" + + "availableCommAdapters=" + availableCommAdapters + ", " + + "commAdapterFactory=" + commAdapterFactory + ", " + + "commAdapter=" + commAdapter + '}'; + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/PeripheralEntryPool.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/PeripheralEntryPool.java new file mode 100644 index 0000000..289313a --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/peripherals/PeripheralEntryPool.java @@ -0,0 +1,129 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.peripherals; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import org.opentcs.components.Lifecycle; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides a pool of {@link PeripheralEntry}s with an entry for every {@link Location} object in + * the kernel. + */ +public class PeripheralEntryPool + implements + Lifecycle { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PeripheralEntryPool.class); + /** + * The object service. + */ + private final TCSObjectService objectService; + /** + * The peripheral comm adapter registry. + */ + private final PeripheralCommAdapterRegistry commAdapterRegistry; + /** + * The entries of this pool. + */ + private final Map, PeripheralEntry> entries = new HashMap<>(); + /** + * Whether the pool is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param objectService The object service. + * @param commAdapterRegistry The peripheral comm adapter registry. + */ + @Inject + public PeripheralEntryPool( + @Nonnull + TCSObjectService objectService, + @Nonnull + PeripheralCommAdapterRegistry commAdapterRegistry + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.commAdapterRegistry = requireNonNull(commAdapterRegistry, "commAdapterRegistry"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + for (Location location : objectService.fetchObjects(Location.class)) { + entries.put( + location.getReference(), + new PeripheralEntry( + location, + commAdapterRegistry.findFactoriesFor(location).stream() + .map(factory -> factory.getDescription()) + .collect(Collectors.toList()) + ) + ); + } + + LOG.debug("Initialized peripheral entry pool: {}", entries); + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + entries.clear(); + initialized = false; + } + + @Nonnull + public Map, PeripheralEntry> getEntries() { + return entries; + } + + /** + * Returns the {@link PeripheralEntry} for the given location. + * + * @param location The reference to the location. + * @return The entry for the given location. + * @throws IllegalArgumentException If no entry is present for the given location. + */ + @Nonnull + public PeripheralEntry getEntryFor( + @Nonnull + TCSResourceReference location + ) + throws IllegalArgumentException { + requireNonNull(location, "location"); + checkArgument( + entries.containsKey(location), + "No peripheral entry present for %s", + location.getName() + ); + return entries.get(location); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/persistence/ModelPersister.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/persistence/ModelPersister.java new file mode 100644 index 0000000..d830d3d --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/persistence/ModelPersister.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.persistence; + +import org.opentcs.access.to.model.PlantModelCreationTO; + +/** + * Provides methods to persist and load models. + * Only a single model is persisted at a time. + */ +public interface ModelPersister { + + /** + * Find out if there is a persisted model at the moment. + * + * @return True if a model is saved. + */ + boolean hasSavedModel(); + + /** + * Persists a model according to the actual implementation of this method. + * + * @param model The model to be persisted. + * @throws IllegalStateException If persisting the model is not possible for some reason. + */ + void saveModel(PlantModelCreationTO model) + throws IllegalStateException; + + /** + * Reads the model and returns it as a PlantModelCreationTO. + * + * @return The PlantModelCreationTO that contains the data that was read. + * @throws IllegalStateException If reading the model is not possible for some reason. + */ + PlantModelCreationTO readModel() + throws IllegalStateException; +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/persistence/XMLFileModelPersister.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/persistence/XMLFileModelPersister.java new file mode 100644 index 0000000..0260f99 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/persistence/XMLFileModelPersister.java @@ -0,0 +1,186 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.persistence; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkState; + +import jakarta.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.customizations.ApplicationHome; +import org.opentcs.util.persistence.ModelParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@link ModelPersister} implementation using an XML file. + */ +public class XMLFileModelPersister + implements + ModelPersister { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(XMLFileModelPersister.class); + /** + * The name of the model file in the model directory. + */ + private static final String MODEL_FILE_NAME = "model.xml"; + /** + * The directory path for the persisted model. + */ + private final File dataDirectory; + /** + * The model file. + */ + private final File modelFile; + /** + * Reads and writes models into xml files. + */ + private final ModelParser modelParser; + + /** + * Creates a new XMLFileModelPersister. + * + * @param directory The application's home directory. + * @param modelParser Reads and writes into the xml file. + */ + @Inject + public XMLFileModelPersister( + @ApplicationHome + File directory, + ModelParser modelParser + ) { + this.modelParser = requireNonNull(modelParser, "modelParser"); + this.dataDirectory = new File(requireNonNull(directory, "directory"), "data"); + + this.modelFile = new File(dataDirectory, MODEL_FILE_NAME); + } + + @Override + public void saveModel(PlantModelCreationTO model) + throws IllegalStateException { + requireNonNull(model, "model"); + + LOG.debug("Saving model '{}'.", model.getName()); + + // Check if writing the model is possible. + checkState( + dataDirectory.isDirectory() || dataDirectory.mkdirs(), + "%s is not an existing directory and could not be created, either.", + dataDirectory.getPath() + ); + checkState( + !modelFile.exists() || modelFile.isFile(), + "%s exists, but is not a regular file", + modelFile.getPath() + ); + try { + if (modelFile.exists()) { + createBackup(); + } + + modelParser.writeModel(model, modelFile); + } + catch (IOException exc) { + throw new IllegalStateException("Exception saving model", exc); + } + } + + @Override + public PlantModelCreationTO readModel() + throws IllegalStateException { + // Return empty model if there is no saved model + if (!hasSavedModel()) { + return new PlantModelCreationTO("empty model"); + } + + // Read the model from the file. + return readXMLModel(modelFile); + } + + @Override + public boolean hasSavedModel() { + return modelFileExists(); + } + + /** + * Creates a backup of the currently saved model file by copying it to the + * "backups" subdirectory. + * + * Assumes that the model file exists. + * + * @throws IOException If the backup directory is not accessible or copying + * the file fails. + */ + private void createBackup() + throws IOException { + // Generate backup file name + Calendar cal = Calendar.getInstance(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss-SSS"); + String time = sdf.format(cal.getTime()); + String modelBackupName = MODEL_FILE_NAME + "_backup_" + time; + // Make sure backup directory exists + File modelBackupDirectory = new File(dataDirectory, "backups"); + if (modelBackupDirectory.exists()) { + if (!modelBackupDirectory.isDirectory()) { + throw new IOException( + modelBackupDirectory.getPath() + " exists, but is not a directory" + ); + } + } + else { + if (!modelBackupDirectory.mkdir()) { + throw new IOException( + "Could not create model directory " + modelBackupDirectory.getPath() + ); + } + } + // Backup the model file + Files.copy( + modelFile.toPath(), + new File(modelBackupDirectory, modelBackupName).toPath() + ); + } + + /** + * Test if the data directory with a model file exist. If not, throw an + * exception. + * + * @throws IOException If check failed. + */ + private boolean modelFileExists() { + if (!modelFile.exists()) { + return false; + } + if (!modelFile.isFile()) { + return false; + } + return true; + } + + /** + * Reads a model from a given InputStream. + * + * @param modelFile The file containing the model. + * @param model The model to be built. + * @throws IOException If an exception occured while loading + */ + private PlantModelCreationTO readXMLModel(File modelFile) + throws IllegalStateException { + try { + return modelParser.readModel(modelFile); + } + catch (IOException exc) { + LOG.error("Exception parsing input", exc); + throw new IllegalStateException("Exception parsing input", exc); + } + } + +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/persistence/package-info.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/persistence/package-info.java new file mode 100644 index 0000000..4fbdfb8 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/persistence/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Classes for persisting and materializing openTCS data. + */ +package org.opentcs.kernel.persistence; diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/services/AbstractTCSObjectService.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/AbstractTCSObjectService.java new file mode 100644 index 0000000..31e3dea --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/AbstractTCSObjectService.java @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.services; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.Set; +import java.util.function.Predicate; +import org.opentcs.access.CredentialsException; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectReference; + +/** + * Delegate method calls to the {@link TCSObjectService} implementation. + */ +public abstract class AbstractTCSObjectService + implements + TCSObjectService { + + /** + * The tcs object service to delegate method calls to. + */ + private final TCSObjectService objectService; + + /** + * Creates a new instance. + * + * @param objectService The service to delegate method calls to. + */ + public AbstractTCSObjectService(TCSObjectService objectService) { + this.objectService = requireNonNull(objectService, "objectService"); + } + + @Override + public > T fetchObject(Class clazz, TCSObjectReference ref) + throws CredentialsException { + requireNonNull(clazz, "clazz"); + requireNonNull(ref, "ref"); + + return getObjectService().fetchObject(clazz, ref); + } + + @Override + public > T fetchObject(Class clazz, String name) + throws CredentialsException { + requireNonNull(clazz, "clazz"); + + return getObjectService().fetchObject(clazz, name); + } + + @Override + public > Set fetchObjects(Class clazz) + throws CredentialsException { + requireNonNull(clazz, "clazz"); + + return getObjectService().fetchObjects(clazz); + } + + @Override + public > Set fetchObjects( + @Nonnull + Class clazz, + @Nonnull + Predicate predicate + ) + throws CredentialsException { + requireNonNull(clazz, "clazz"); + requireNonNull(predicate, "predicate"); + + return getObjectService().fetchObjects(clazz, predicate); + } + + @Override + public void updateObjectProperty( + TCSObjectReference ref, + String key, + @Nullable + String value + ) + throws ObjectUnknownException, + CredentialsException { + requireNonNull(ref, "ref"); + requireNonNull(key, "key"); + + getObjectService().updateObjectProperty(ref, key, value); + } + + @Override + public void appendObjectHistoryEntry(TCSObjectReference ref, ObjectHistory.Entry entry) + throws ObjectUnknownException, + KernelRuntimeException { + requireNonNull(ref, "ref"); + requireNonNull(entry, "entry"); + + getObjectService().appendObjectHistoryEntry(ref, entry); + } + + /** + * Retruns the {@link TCSObjectService} implementation being used. + * + * @return The {@link TCSObjectService} implementation being used. + */ + public TCSObjectService getObjectService() { + return objectService; + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardDispatcherService.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardDispatcherService.java new file mode 100644 index 0000000..20b6d7d --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardDispatcherService.java @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.services; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.Dispatcher; +import org.opentcs.components.kernel.dipatching.TransportOrderAssignmentException; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.customizations.kernel.GlobalSyncObject; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.kernel.workingset.TCSObjectRepository; + +/** + * This class is the standard implementation of the {@link DispatcherService} interface. + */ +public class StandardDispatcherService + implements + DispatcherService { + + /** + * A global object to be used for synchronization within the kernel. + */ + private final Object globalSyncObject; + /** + * The container of all course model and transport order objects. + */ + private final TCSObjectRepository objectRepo; + /** + * The dispatcher. + */ + private final Dispatcher dispatcher; + + /** + * Creates a new instance. + * + * @param globalSyncObject The kernel threads' global synchronization object. + * @param objectRepo The object repo to be used. + * @param dispatcher The dispatcher. + */ + @Inject + public StandardDispatcherService( + @GlobalSyncObject + Object globalSyncObject, + TCSObjectRepository objectRepo, + Dispatcher dispatcher + ) { + this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject"); + this.objectRepo = requireNonNull(objectRepo, "objectRepo"); + this.dispatcher = requireNonNull(dispatcher, "dispatcher"); + } + + @Override + public void dispatch() { + synchronized (globalSyncObject) { + dispatcher.dispatch(); + } + } + + @Override + public void withdrawByVehicle(TCSObjectReference ref, boolean immediateAbort) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + dispatcher.withdrawOrder(objectRepo.getObject(Vehicle.class, ref), immediateAbort); + } + } + + @Override + public void withdrawByTransportOrder( + TCSObjectReference ref, + boolean immediateAbort + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + dispatcher.withdrawOrder( + objectRepo.getObject(TransportOrder.class, ref), + immediateAbort + ); + } + } + + @Override + public void reroute(TCSObjectReference ref, ReroutingType reroutingType) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(reroutingType, "reroutingType"); + + synchronized (globalSyncObject) { + dispatcher.reroute(objectRepo.getObject(Vehicle.class, ref), reroutingType); + } + } + + @Override + public void rerouteAll(ReroutingType reroutingType) { + synchronized (globalSyncObject) { + dispatcher.rerouteAll(reroutingType); + } + } + + @Override + public void assignNow(TCSObjectReference ref) + throws ObjectUnknownException, + TransportOrderAssignmentException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + dispatcher.assignNow(objectRepo.getObject(TransportOrder.class, ref)); + } + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardNotificationService.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardNotificationService.java new file mode 100644 index 0000000..4798ca1 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardNotificationService.java @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.services; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.List; +import java.util.function.Predicate; +import org.opentcs.components.kernel.services.NotificationService; +import org.opentcs.customizations.kernel.GlobalSyncObject; +import org.opentcs.data.notification.UserNotification; +import org.opentcs.kernel.workingset.NotificationBuffer; + +/** + * This class is the standard implementation of the {@link NotificationService} interface. + */ +public class StandardNotificationService + implements + NotificationService { + + /** + * A global object to be used for synchronization within the kernel. + */ + private final Object globalSyncObject; + /** + * The buffer for all messages published. + */ + private final NotificationBuffer notificationBuffer; + + /** + * Creates a new instance. + * + * @param globalSyncObject The kernel threads' global synchronization object. + * @param notificationBuffer The notification buffer to be used. + */ + @Inject + public StandardNotificationService( + @GlobalSyncObject + Object globalSyncObject, + NotificationBuffer notificationBuffer + ) { + this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject"); + this.notificationBuffer = requireNonNull(notificationBuffer, "notificationBuffer"); + } + + @Override + public List fetchUserNotifications(Predicate predicate) { + synchronized (globalSyncObject) { + return notificationBuffer.getNotifications(predicate); + } + } + + @Override + public void publishUserNotification(UserNotification notification) { + requireNonNull(notification, "notification"); + + synchronized (globalSyncObject) { + notificationBuffer.addNotification(notification); + } + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardPeripheralDispatcherService.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardPeripheralDispatcherService.java new file mode 100644 index 0000000..cbbf647 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardPeripheralDispatcherService.java @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.services; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.components.kernel.PeripheralJobDispatcher; +import org.opentcs.components.kernel.services.PeripheralDispatcherService; +import org.opentcs.customizations.kernel.GlobalSyncObject; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.kernel.workingset.TCSObjectRepository; + +/** + * This class is the standard implementation of the {@link PeripheralDispatcherService} interface. + */ +public class StandardPeripheralDispatcherService + implements + PeripheralDispatcherService { + + /** + * A global object to be used for synchronization within the kernel. + */ + private final Object globalSyncObject; + /** + * The container of all course model and transport order objects. + */ + private final TCSObjectRepository objectRepo; + /** + * The peripheral job dispatcher. + */ + private final PeripheralJobDispatcher dispatcher; + + /** + * Creates a new instance. + * + * @param globalSyncObject The kernel threads' global synchronization object. + * @param objectRepo The object repo to be used. + * @param dispatcher The peripheral job dispatcher. + */ + @Inject + public StandardPeripheralDispatcherService( + @GlobalSyncObject + Object globalSyncObject, + TCSObjectRepository objectRepo, + PeripheralJobDispatcher dispatcher + ) { + this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject"); + this.objectRepo = requireNonNull(objectRepo, "objectRepo"); + this.dispatcher = requireNonNull(dispatcher, "dispatcher"); + } + + @Override + public void dispatch() { + synchronized (globalSyncObject) { + dispatcher.dispatch(); + } + } + + @Override + public void withdrawByLocation(TCSResourceReference ref) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + dispatcher.withdrawJob(objectRepo.getObject(Location.class, ref)); + } + } + + @Override + public void withdrawByPeripheralJob(TCSObjectReference ref) + throws ObjectUnknownException, + KernelRuntimeException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + dispatcher.withdrawJob(objectRepo.getObject(PeripheralJob.class, ref)); + } + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardPeripheralJobService.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardPeripheralJobService.java new file mode 100644 index 0000000..8bd517c --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardPeripheralJobService.java @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.services; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.to.peripherals.PeripheralJobCreationTO; +import org.opentcs.components.kernel.services.InternalPeripheralJobService; +import org.opentcs.components.kernel.services.PeripheralJobService; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.customizations.kernel.GlobalSyncObject; +import org.opentcs.data.ObjectExistsException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.kernel.workingset.PeripheralJobPoolManager; + +/** + * This class is the standard implementation of the {@link PeripheralJobService} interface. + */ +public class StandardPeripheralJobService + extends + AbstractTCSObjectService + implements + InternalPeripheralJobService { + + /** + * A global object to be used for synchronization within the kernel. + */ + private final Object globalSyncObject; + /** + * The job pool manager. + */ + private final PeripheralJobPoolManager jobPoolManager; + + /** + * Creates a new instance. + * + * @param objectService The tcs obejct service. + * @param globalSyncObject The kernel threads' global synchronization object. + * @param jobPoolManager The job pool manager to be used. + */ + @Inject + public StandardPeripheralJobService( + TCSObjectService objectService, + @GlobalSyncObject + Object globalSyncObject, + PeripheralJobPoolManager jobPoolManager + ) { + super(objectService); + this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject"); + this.jobPoolManager = requireNonNull(jobPoolManager, "jobPoolManager"); + } + + @Override + public void updatePeripheralJobState( + TCSObjectReference ref, + PeripheralJob.State state + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(state, "state"); + + synchronized (globalSyncObject) { + jobPoolManager.setPeripheralJobState(ref, state); + } + } + + @Override + public PeripheralJob createPeripheralJob(PeripheralJobCreationTO to) + throws ObjectUnknownException, + ObjectExistsException, + KernelRuntimeException { + requireNonNull(to, "to"); + + synchronized (globalSyncObject) { + return jobPoolManager.createPeripheralJob(to); + } + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardPeripheralService.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardPeripheralService.java new file mode 100644 index 0000000..09ecfb1 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardPeripheralService.java @@ -0,0 +1,204 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.services; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.services.InternalPeripheralService; +import org.opentcs.components.kernel.services.PeripheralService; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.customizations.kernel.GlobalSyncObject; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.PeripheralInformation; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.drivers.peripherals.PeripheralAdapterCommand; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; +import org.opentcs.drivers.peripherals.PeripheralProcessModel; +import org.opentcs.drivers.peripherals.management.PeripheralAttachmentInformation; +import org.opentcs.kernel.peripherals.PeripheralAttachmentManager; +import org.opentcs.kernel.peripherals.PeripheralEntry; +import org.opentcs.kernel.peripherals.PeripheralEntryPool; +import org.opentcs.kernel.workingset.PlantModelManager; + +/** + * This class is the standard implementation of the {@link PeripheralService} interface. + */ +public class StandardPeripheralService + extends + AbstractTCSObjectService + implements + InternalPeripheralService { + + /** + * A global object to be used for synchronization within the kernel. + */ + private final Object globalSyncObject; + /** + * The attachment manager. + */ + private final PeripheralAttachmentManager attachmentManager; + /** + * The pool of peripheral entries. + */ + private final PeripheralEntryPool peripheralEntryPool; + /** + * The plant model manager. + */ + private final PlantModelManager plantModelManager; + + /** + * Creates a new instance. + * + * @param objectService The tcs object service. + * @param globalSyncObject The kernel threads' global synchronization object. + * @param attachmentManager The attachment manager. + * @param peripheralEntryPool The pool of peripheral entries. + * @param plantModelManager The plant model manager to be used. + */ + @Inject + public StandardPeripheralService( + TCSObjectService objectService, + @GlobalSyncObject + Object globalSyncObject, + PeripheralAttachmentManager attachmentManager, + PeripheralEntryPool peripheralEntryPool, + PlantModelManager plantModelManager + ) { + super(objectService); + this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject"); + this.attachmentManager = requireNonNull(attachmentManager, "attachmentManager"); + this.peripheralEntryPool = requireNonNull(peripheralEntryPool, "peripheralEntryPool"); + this.plantModelManager = requireNonNull(plantModelManager, "plantModelManager"); + } + + @Override + public void attachCommAdapter( + TCSResourceReference ref, + PeripheralCommAdapterDescription description + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(description, "description"); + + synchronized (globalSyncObject) { + attachmentManager.attachAdapterToLocation(ref, description); + } + } + + @Override + public void disableCommAdapter(TCSResourceReference ref) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + peripheralEntryPool.getEntryFor(ref).getCommAdapter().disable(); + } + } + + @Override + public void enableCommAdapter(TCSResourceReference ref) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + peripheralEntryPool.getEntryFor(ref).getCommAdapter().enable(); + } + } + + @Override + public PeripheralAttachmentInformation fetchAttachmentInformation( + TCSResourceReference ref + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + return attachmentManager.getAttachmentInformation(ref); + } + } + + @Override + public PeripheralProcessModel fetchProcessModel(TCSResourceReference ref) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + return peripheralEntryPool.getEntryFor(ref).getCommAdapter().getProcessModel(); + } + } + + @Override + public void sendCommAdapterCommand( + TCSResourceReference ref, + PeripheralAdapterCommand command + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(command, "command"); + + synchronized (globalSyncObject) { + PeripheralEntry entry = peripheralEntryPool.getEntryFor(ref); + synchronized (entry.getCommAdapter()) { + entry.getCommAdapter().execute(command); + } + } + } + + @Override + public void updatePeripheralProcState( + TCSResourceReference ref, + PeripheralInformation.ProcState state + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(state, "state"); + + synchronized (globalSyncObject) { + plantModelManager.setLocationProcState(ref, state); + } + } + + @Override + public void updatePeripheralReservationToken( + TCSResourceReference ref, + String reservationToken + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + plantModelManager.setLocationReservationToken(ref, reservationToken); + } + } + + @Override + public void updatePeripheralState( + TCSResourceReference ref, + PeripheralInformation.State state + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(state, "state"); + + synchronized (globalSyncObject) { + plantModelManager.setLocationState(ref, state); + } + } + + @Override + public void updatePeripheralJob( + TCSResourceReference ref, + TCSObjectReference peripheralJob + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + plantModelManager.setLocationPeripheralJob(ref, peripheralJob); + } + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardPlantModelService.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardPlantModelService.java new file mode 100644 index 0000000..de54793 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardPlantModelService.java @@ -0,0 +1,264 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.services; + +import static com.google.common.base.Strings.isNullOrEmpty; +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Map; +import java.util.Set; +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.LocalKernel; +import org.opentcs.access.ModelTransitionEvent; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.components.kernel.services.InternalPlantModelService; +import org.opentcs.components.kernel.services.NotificationService; +import org.opentcs.components.kernel.services.PlantModelService; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.customizations.kernel.GlobalSyncObject; +import org.opentcs.data.ObjectExistsException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Block; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.PlantModel; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResource; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.model.visualization.VisualLayout; +import org.opentcs.data.notification.UserNotification; +import org.opentcs.kernel.persistence.ModelPersister; +import org.opentcs.kernel.workingset.PlantModelManager; +import org.opentcs.util.event.EventHandler; + +/** + * This class is the standard implementation of the {@link PlantModelService} interface. + */ +public class StandardPlantModelService + extends + AbstractTCSObjectService + implements + InternalPlantModelService { + + /** + * The kernel. + */ + private final Kernel kernel; + /** + * A global object to be used for synchronization within the kernel. + */ + private final Object globalSyncObject; + /** + * The plant model manager. + */ + private final PlantModelManager plantModelManager; + /** + * The persister loading and storing model data. + */ + private final ModelPersister modelPersister; + /** + * Where we send events to. + */ + private final EventHandler eventHandler; + /** + * The notification service. + */ + private final NotificationService notificationService; + + /** + * Creates a new instance. + * + * @param kernel The kernel. + * @param objectService The tcs object service. + * @param globalSyncObject The kernel threads' global synchronization object. + * @param plantModelManager The plant model manager to be used. + * @param modelPersister The model persister to be used. + * @param eventHandler Where this instance sends events to. + * @param notificationService The notification service. + */ + @Inject + public StandardPlantModelService( + LocalKernel kernel, + TCSObjectService objectService, + @GlobalSyncObject + Object globalSyncObject, + PlantModelManager plantModelManager, + ModelPersister modelPersister, + @ApplicationEventBus + EventHandler eventHandler, + NotificationService notificationService + ) { + super(objectService); + this.kernel = requireNonNull(kernel, "kernel"); + this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject"); + this.plantModelManager = requireNonNull(plantModelManager, "plantModelManager"); + this.modelPersister = requireNonNull(modelPersister, "modelPersister"); + this.eventHandler = requireNonNull(eventHandler, "eventHandler"); + this.notificationService = requireNonNull(notificationService, "notificationService"); + } + + @Override + public Set> expandResources(Set> resources) + throws ObjectUnknownException { + requireNonNull(resources, "resources"); + + synchronized (globalSyncObject) { + return plantModelManager.expandResources(resources); + } + } + + @Override + public void loadPlantModel() + throws IllegalStateException { + synchronized (globalSyncObject) { + if (!modelPersister.hasSavedModel()) { + createPlantModel(new PlantModelCreationTO(Kernel.DEFAULT_MODEL_NAME)); + return; + } + + final String oldModelName = getModelName(); + // Load the new model + PlantModelCreationTO modelCreationTO = modelPersister.readModel(); + final String newModelName = isNullOrEmpty(modelCreationTO.getName()) + ? "" + : modelCreationTO.getName(); + // Let listeners know we're in transition. + emitModelEvent(oldModelName, newModelName, true, false); + plantModelManager.createPlantModelObjects(modelCreationTO); + // Let listeners know we're done with the transition. + emitModelEvent(oldModelName, newModelName, true, true); + notificationService.publishUserNotification( + new UserNotification( + "Kernel loaded model " + newModelName, + UserNotification.Level.INFORMATIONAL + ) + ); + } + } + + @Override + public void savePlantModel() + throws IllegalStateException { + synchronized (globalSyncObject) { + modelPersister.saveModel(plantModelManager.createPlantModelCreationTO()); + } + } + + @Override + public PlantModel getPlantModel() { + synchronized (globalSyncObject) { + return new PlantModel(plantModelManager.getName()) + .withProperties(getModelProperties()) + .withPoints(fetchObjects(Point.class)) + .withPaths(fetchObjects(Path.class)) + .withLocationTypes(fetchObjects(LocationType.class)) + .withLocations(fetchObjects(Location.class)) + .withBlocks(fetchObjects(Block.class)) + .withVehicles(fetchObjects(Vehicle.class)) + .withVisualLayout(fetchObjects(VisualLayout.class).stream().findFirst().get()); + } + } + + @Override + public void createPlantModel(PlantModelCreationTO to) + throws ObjectUnknownException, + ObjectExistsException, + IllegalStateException { + requireNonNull(to, "to"); + + boolean kernelInOperating = kernel.getState() == Kernel.State.OPERATING; + // If we are in state operating, change the kernel state before creating the plant model + if (kernelInOperating) { + kernel.setState(Kernel.State.MODELLING); + } + + String oldModelName = getModelName(); + emitModelEvent(oldModelName, to.getName(), true, false); + + // Create the plant model + synchronized (globalSyncObject) { + plantModelManager.createPlantModelObjects(to); + } + + savePlantModel(); + + // If we were in state operating before, change the kernel state back to operating + if (kernelInOperating) { + kernel.setState(Kernel.State.OPERATING); + } + + emitModelEvent(oldModelName, to.getName(), true, true); + notificationService.publishUserNotification( + new UserNotification( + "Kernel created model " + to.getName(), + UserNotification.Level.INFORMATIONAL + ) + ); + } + + @Override + public String getModelName() { + synchronized (globalSyncObject) { + return plantModelManager.getName(); + } + } + + @Override + public Map getModelProperties() + throws KernelRuntimeException { + synchronized (globalSyncObject) { + return plantModelManager.getProperties(); + } + } + + @Override + public void updateLocationLock(TCSObjectReference ref, boolean locked) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + plantModelManager.setLocationLocked(ref, locked); + } + } + + @Override + public void updatePathLock(TCSObjectReference ref, boolean locked) + throws ObjectUnknownException, + KernelRuntimeException { + synchronized (globalSyncObject) { + plantModelManager.setPathLocked(ref, locked); + } + } + + /** + * Generates an event for a Model change. + * + * @param oldModelName The old model name. + * @param newModelName The new model name. + * @param modelContentChanged Whether the model's content actually changed. + * @param transitionFinished Whether the transition is finished or not. + */ + private void emitModelEvent( + String oldModelName, + String newModelName, + boolean modelContentChanged, + boolean transitionFinished + ) { + requireNonNull(newModelName, "newModelName"); + + eventHandler.onEvent( + new ModelTransitionEvent( + oldModelName, + newModelName, + modelContentChanged, + transitionFinished + ) + ); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardQueryService.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardQueryService.java new file mode 100644 index 0000000..21517fd --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardQueryService.java @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.services; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.HashMap; +import java.util.Map; +import org.opentcs.components.kernel.Query; +import org.opentcs.components.kernel.QueryResponder; +import org.opentcs.components.kernel.services.InternalQueryService; +import org.opentcs.customizations.kernel.GlobalSyncObject; + +/** + * The default implementation of the {@link InternalQueryService} interface. + */ +public class StandardQueryService + implements + InternalQueryService { + + /** + * A global object to be used for synchronization within the kernel. + */ + private final Object globalSyncObject; + /** + * The responders, by query type. + */ + private final Map>, QueryResponder> respondersByQueryType + = new HashMap<>(); + + /** + * Creates a new instance. + * + * @param globalSyncObject The kernel threads' global synchronization object. + */ + @Inject + public StandardQueryService( + @GlobalSyncObject + Object globalSyncObject + ) { + this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject"); + } + + @Override + public T query(Query query) { + requireNonNull(query, "query"); + + synchronized (globalSyncObject) { + QueryResponder responder = respondersByQueryType.get(query.getClass()); + + checkArgument(responder != null, "Query class not taken: %s", query.getClass().getName()); + return responder.query(query); + } + } + + @Override + public void registerResponder( + @Nonnull + Class> clazz, + @Nonnull + QueryResponder responder + ) { + requireNonNull(clazz, "clazz"); + requireNonNull(responder, "responder"); + + synchronized (globalSyncObject) { + checkArgument( + !respondersByQueryType.containsKey(clazz), + "Query class already taken: %s", + clazz.getName() + ); + + respondersByQueryType.put(clazz, responder); + } + } + + @Override + public void unregisterResponder( + @Nonnull + Class> clazz + ) { + requireNonNull(clazz, "clazz"); + + synchronized (globalSyncObject) { + respondersByQueryType.remove(clazz); + } + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardRouterService.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardRouterService.java new file mode 100644 index 0000000..e52d543 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardRouterService.java @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.services; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.RouterService; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.customizations.kernel.GlobalSyncObject; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.Route; +import org.opentcs.kernel.workingset.PlantModelManager; + +/** + * This class is the standard implementation of the {@link RouterService} interface. + */ +public class StandardRouterService + implements + RouterService { + + /** + * A global object to be used for synchronization within the kernel. + */ + private final Object globalSyncObject; + /** + * The router. + */ + private final Router router; + /** + * The plant model manager. + */ + private final PlantModelManager plantModelManager; + /** + * The object service. + */ + private final TCSObjectService objectService; + + /** + * Creates a new instance. + * + * @param globalSyncObject The kernel threads' global synchronization object. + * @param router The scheduler. + * @param plantModelManager The plant model manager to be used. + * @param objectService The object service. + */ + @Inject + public StandardRouterService( + @GlobalSyncObject + Object globalSyncObject, + Router router, + PlantModelManager plantModelManager, + TCSObjectService objectService + ) { + this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject"); + this.router = requireNonNull(router, "router"); + this.plantModelManager = requireNonNull(plantModelManager, "plantModelManager"); + this.objectService = requireNonNull(objectService, "objectService"); + } + + @Override + public void updateRoutingTopology(Set> refs) + throws KernelRuntimeException { + synchronized (globalSyncObject) { + router.updateRoutingTopology( + refs.stream() + .map(ref -> plantModelManager.getObjectRepo().getObject(Path.class, ref)) + .collect(Collectors.toSet()) + ); + } + } + + @Override + public Map, Route> computeRoutes( + TCSObjectReference vehicleRef, + TCSObjectReference sourcePointRef, + Set> destinationPointRefs, + Set> resourcesToAvoid + ) { + requireNonNull(vehicleRef, "vehicleRef"); + requireNonNull(sourcePointRef, "sourcePointRef"); + requireNonNull(destinationPointRefs, "destinationPointRefs"); + requireNonNull(resourcesToAvoid, "resourcesToAvoid"); + + synchronized (globalSyncObject) { + Map, Route> result = new HashMap<>(); + Vehicle vehicle = objectService.fetchObject(Vehicle.class, vehicleRef); + if (vehicle == null) { + throw new ObjectUnknownException("Unknown vehicle: " + vehicleRef.getName()); + } + Point sourcePoint = objectService.fetchObject(Point.class, sourcePointRef); + if (sourcePoint == null) { + throw new ObjectUnknownException("Unknown source point: " + sourcePointRef.getName()); + } + for (TCSObjectReference dest : destinationPointRefs) { + Point destinationPoint = objectService.fetchObject(Point.class, dest); + if (destinationPoint == null) { + throw new ObjectUnknownException("Unknown destination point: " + dest.getName()); + } + result.put( + dest, + router.getRoute(vehicle, sourcePoint, destinationPoint, resourcesToAvoid) + .orElse(null) + ); + } + return result; + } + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardTCSObjectService.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardTCSObjectService.java new file mode 100644 index 0000000..bc662ef --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardTCSObjectService.java @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.services; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.customizations.kernel.GlobalSyncObject; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.kernel.workingset.TCSObjectManager; +import org.opentcs.kernel.workingset.TCSObjectRepository; + +/** + * This class is the standard implementation of the {@link TCSObjectService} interface. + */ +public class StandardTCSObjectService + implements + TCSObjectService { + + /** + * A global object to be used for synchronization within the kernel. + */ + private final Object globalSyncObject; + /** + * The object manager. + */ + private final TCSObjectManager objectManager; + + /** + * Creates a new instance. + * + * @param globalSyncObject The kernel threads' global synchronization object. + * @param objectManager The object manager. + */ + @Inject + public StandardTCSObjectService( + @GlobalSyncObject + Object globalSyncObject, + TCSObjectManager objectManager + ) { + this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject"); + this.objectManager = requireNonNull(objectManager, "objectManager"); + } + + @Override + public > T fetchObject(Class clazz, TCSObjectReference ref) { + requireNonNull(clazz, "clazz"); + requireNonNull(ref, "ref"); + + synchronized (getGlobalSyncObject()) { + return getObjectRepo().getObjectOrNull(clazz, ref); + } + } + + @Override + public > T fetchObject(Class clazz, String name) { + requireNonNull(clazz, "clazz"); + + synchronized (getGlobalSyncObject()) { + return getObjectRepo().getObjectOrNull(clazz, name); + } + } + + @Override + public > Set fetchObjects(Class clazz) { + requireNonNull(clazz, "clazz"); + + synchronized (getGlobalSyncObject()) { + Set objects = getObjectRepo().getObjects(clazz); + Set copies = new HashSet<>(); + for (T object : objects) { + copies.add(object); + } + return copies; + } + } + + @Override + public > Set fetchObjects( + @Nonnull + Class clazz, + @Nonnull + Predicate predicate + ) { + requireNonNull(clazz, "clazz"); + requireNonNull(predicate, "predicate"); + + synchronized (getGlobalSyncObject()) { + return getObjectRepo().getObjects(clazz, predicate); + } + } + + @Override + public void updateObjectProperty( + TCSObjectReference ref, + String key, + @Nullable + String value + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(key, "key"); + + synchronized (getGlobalSyncObject()) { + objectManager.setObjectProperty(ref, key, value); + } + } + + @Override + public void appendObjectHistoryEntry(TCSObjectReference ref, ObjectHistory.Entry entry) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(entry, "entry"); + + synchronized (getGlobalSyncObject()) { + objectManager.appendObjectHistoryEntry(ref, entry); + } + } + + protected Object getGlobalSyncObject() { + return globalSyncObject; + } + + protected TCSObjectRepository getObjectRepo() { + return objectManager.getObjectRepo(); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardTransportOrderService.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardTransportOrderService.java new file mode 100644 index 0000000..f028487 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardTransportOrderService.java @@ -0,0 +1,257 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.services; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.List; +import org.opentcs.access.to.order.OrderSequenceCreationTO; +import org.opentcs.access.to.order.TransportOrderCreationTO; +import org.opentcs.components.kernel.services.InternalTransportOrderService; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.components.kernel.services.TransportOrderService; +import org.opentcs.customizations.kernel.GlobalSyncObject; +import org.opentcs.data.ObjectExistsException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.kernel.workingset.PlantModelManager; +import org.opentcs.kernel.workingset.TCSObjectRepository; +import org.opentcs.kernel.workingset.TransportOrderPoolManager; + +/** + * This class is the standard implementation of the {@link TransportOrderService} interface. + */ +public class StandardTransportOrderService + extends + AbstractTCSObjectService + implements + InternalTransportOrderService { + + /** + * A global object to be used for synchronization within the kernel. + */ + private final Object globalSyncObject; + /** + * The container of all course model and transport order objects. + */ + private final TCSObjectRepository globalObjectPool; + /** + * The order pool manager. + */ + private final TransportOrderPoolManager orderPoolManager; + /** + * The plant model manager. + */ + private final PlantModelManager plantModelManager; + + /** + * Creates a new instance. + * + * @param objectService The tcs obejct service. + * @param globalSyncObject The kernel threads' global synchronization object. + * @param globalObjectPool The object pool to be used. + * @param orderPoolManager The order pool manager to be used. + * @param plantModelManager The plant model manager to be used. + */ + @Inject + public StandardTransportOrderService( + TCSObjectService objectService, + @GlobalSyncObject + Object globalSyncObject, + TCSObjectRepository globalObjectPool, + TransportOrderPoolManager orderPoolManager, + PlantModelManager plantModelManager + ) { + super(objectService); + this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject"); + this.globalObjectPool = requireNonNull(globalObjectPool, "globalObjectPool"); + this.orderPoolManager = requireNonNull(orderPoolManager, "orderPoolManager"); + this.plantModelManager = requireNonNull(plantModelManager, "plantModelManager"); + } + + @Override + public void markOrderSequenceFinished(TCSObjectReference ref) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + OrderSequence seq = globalObjectPool.getObject(OrderSequence.class, ref); + // Make sure we don't execute this if the sequence is already marked as finished, as that + // would make it possible to trigger disposition of a vehicle at any given moment. + if (seq.isFinished()) { + return; + } + + orderPoolManager.setOrderSequenceFinished(ref); + // If the sequence was being processed by a vehicle, clear its back reference to the sequence + // to make it available again and dispatch it. + if (seq.getProcessingVehicle() != null) { + Vehicle vehicle = globalObjectPool.getObject( + Vehicle.class, + seq.getProcessingVehicle() + ); + plantModelManager.setVehicleOrderSequence(vehicle.getReference(), null); + } + } + } + + @Override + public void updateOrderSequenceFinishedIndex(TCSObjectReference ref, int index) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + orderPoolManager.setOrderSequenceFinishedIndex(ref, index); + } + } + + @Override + public void updateOrderSequenceProcessingVehicle( + TCSObjectReference seqRef, + TCSObjectReference vehicleRef + ) + throws ObjectUnknownException { + requireNonNull(seqRef, "seqRef"); + + synchronized (globalSyncObject) { + orderPoolManager.setOrderSequenceProcessingVehicle(seqRef, vehicleRef); + } + } + + @Override + public void updateTransportOrderProcessingVehicle( + TCSObjectReference orderRef, + TCSObjectReference vehicleRef, + List driveOrders + ) + throws ObjectUnknownException, + IllegalArgumentException { + requireNonNull(orderRef, "orderRef"); + requireNonNull(driveOrders, "driveOrders"); + + synchronized (globalSyncObject) { + orderPoolManager.setTransportOrderProcessingVehicle(orderRef, vehicleRef, driveOrders); + } + } + + @Override + public void updateTransportOrderDriveOrders( + TCSObjectReference ref, + List driveOrders + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(driveOrders, "driveOrders"); + + synchronized (globalSyncObject) { + orderPoolManager.setTransportOrderDriveOrders(ref, driveOrders); + } + } + + @Override + public void updateTransportOrderNextDriveOrder(TCSObjectReference ref) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + orderPoolManager.setTransportOrderNextDriveOrder(ref); + } + } + + @Override + public void updateTransportOrderCurrentRouteStepIndex( + TCSObjectReference ref, + int index + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + orderPoolManager.setTransportOrderCurrentRouteStepIndex(ref, index); + } + } + + @Override + public void updateTransportOrderState( + TCSObjectReference ref, + TransportOrder.State state + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(state, "state"); + + synchronized (globalSyncObject) { + orderPoolManager.setTransportOrderState(ref, state); + } + } + + @Override + public OrderSequence createOrderSequence(OrderSequenceCreationTO to) { + requireNonNull(to, "to"); + + synchronized (globalSyncObject) { + return orderPoolManager.createOrderSequence(to); + } + } + + @Override + public TransportOrder createTransportOrder(TransportOrderCreationTO to) + throws ObjectUnknownException, + ObjectExistsException { + requireNonNull(to, "to"); + + synchronized (globalSyncObject) { + return orderPoolManager.createTransportOrder(to); + } + } + + @Override + public void markOrderSequenceComplete(TCSObjectReference ref) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + OrderSequence seq = globalObjectPool.getObject(OrderSequence.class, ref); + // Make sure we don't execute this if the sequence is already marked as finished, as that + // would make it possible to trigger disposition of a vehicle at any given moment. + if (seq.isComplete()) { + return; + } + orderPoolManager.setOrderSequenceComplete(ref); + // If there aren't any transport orders left to be processed as part of the sequence, mark + // it as finished, too. + if (seq.getNextUnfinishedOrder() == null) { + orderPoolManager.setOrderSequenceFinished(ref); + // If the sequence was being processed by a vehicle, clear its back reference to the + // sequence to make it available again and dispatch it. + if (seq.getProcessingVehicle() != null) { + Vehicle vehicle = globalObjectPool.getObject( + Vehicle.class, + seq.getProcessingVehicle() + ); + plantModelManager.setVehicleOrderSequence(vehicle.getReference(), null); + } + } + } + } + + @Override + public void updateTransportOrderIntendedVehicle( + TCSObjectReference orderRef, + TCSObjectReference vehicleRef + ) + throws ObjectUnknownException, + IllegalArgumentException { + requireNonNull(orderRef, "orderRef"); + + synchronized (globalSyncObject) { + orderPoolManager.setTransportOrderIntendedVehicle(orderRef, vehicleRef); + } + } + +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardVehicleService.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardVehicleService.java new file mode 100644 index 0000000..d37c7a1 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/services/StandardVehicleService.java @@ -0,0 +1,504 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.services; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.List; +import java.util.Set; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.components.kernel.services.InternalVehicleService; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.components.kernel.services.VehicleService; +import org.opentcs.customizations.kernel.GlobalSyncObject; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.BoundingBox; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Pose; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Triple; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.model.Vehicle.EnergyLevelThresholdSet; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.LoadHandlingDevice; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; +import org.opentcs.drivers.vehicle.management.VehicleAttachmentInformation; +import org.opentcs.drivers.vehicle.management.VehicleProcessModelTO; +import org.opentcs.kernel.extensions.controlcenter.vehicles.AttachmentManager; +import org.opentcs.kernel.extensions.controlcenter.vehicles.VehicleEntry; +import org.opentcs.kernel.extensions.controlcenter.vehicles.VehicleEntryPool; +import org.opentcs.kernel.vehicles.LocalVehicleControllerPool; +import org.opentcs.kernel.vehicles.VehicleCommAdapterRegistry; +import org.opentcs.kernel.workingset.PlantModelManager; +import org.opentcs.util.annotations.ScheduledApiChange; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is the standard implementation of the {@link VehicleService} interface. + */ +public class StandardVehicleService + extends + AbstractTCSObjectService + implements + InternalVehicleService { + + /** + * This class' logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(StandardVehicleService.class); + /** + * A global object to be used for synchronization within the kernel. + */ + private final Object globalSyncObject; + /** + * A pool of vehicle controllers. + */ + private final LocalVehicleControllerPool vehicleControllerPool; + /** + * A pool of vehicle entries. + */ + private final VehicleEntryPool vehicleEntryPool; + /** + * The attachment manager. + */ + private final AttachmentManager attachmentManager; + /** + * The registry for all communication adapters. + */ + private final VehicleCommAdapterRegistry commAdapterRegistry; + /** + * The plant model manager. + */ + private final PlantModelManager plantModelManager; + + /** + * Creates a new instance. + * + * @param objectService The tcs object service. + * @param globalSyncObject The kernel threads' global synchronization object. + * @param vehicleControllerPool The controller pool to be used. + * @param vehicleEntryPool The pool of vehicle entries to be used. + * @param attachmentManager The attachment manager. + * @param commAdapterRegistry The registry for all communication adapters. + * @param plantModelManager The plant model manager to be used. + */ + @Inject + public StandardVehicleService( + TCSObjectService objectService, + @GlobalSyncObject + Object globalSyncObject, + LocalVehicleControllerPool vehicleControllerPool, + VehicleEntryPool vehicleEntryPool, + AttachmentManager attachmentManager, + VehicleCommAdapterRegistry commAdapterRegistry, + PlantModelManager plantModelManager + ) { + super(objectService); + this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject"); + this.vehicleControllerPool = requireNonNull(vehicleControllerPool, "vehicleControllerPool"); + this.vehicleEntryPool = requireNonNull(vehicleEntryPool, "vehicleEntryPool"); + this.attachmentManager = requireNonNull(attachmentManager, "attachmentManager"); + this.commAdapterRegistry = requireNonNull(commAdapterRegistry, "commAdapterRegistry"); + this.plantModelManager = requireNonNull(plantModelManager, "plantModelManager"); + } + + @Override + public void updateVehicleEnergyLevel(TCSObjectReference ref, int energyLevel) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + plantModelManager.setVehicleEnergyLevel(ref, energyLevel); + } + } + + @Override + public void updateVehicleLoadHandlingDevices( + TCSObjectReference ref, + List devices + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(devices, "devices"); + + synchronized (globalSyncObject) { + plantModelManager.setVehicleLoadHandlingDevices(ref, devices); + } + } + + @Override + public void updateVehicleNextPosition( + TCSObjectReference vehicleRef, + TCSObjectReference pointRef + ) + throws ObjectUnknownException { + requireNonNull(vehicleRef, "vehicleRef"); + + synchronized (globalSyncObject) { + plantModelManager.setVehicleNextPosition(vehicleRef, pointRef); + } + } + + @Override + public void updateVehicleOrderSequence( + TCSObjectReference vehicleRef, + TCSObjectReference sequenceRef + ) + throws ObjectUnknownException { + requireNonNull(vehicleRef, "vehicleRef"); + + synchronized (globalSyncObject) { + plantModelManager.setVehicleOrderSequence(vehicleRef, sequenceRef); + } + } + + @Deprecated + @Override + public void updateVehicleOrientationAngle(TCSObjectReference ref, double angle) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + Vehicle previousState = plantModelManager.getObjectRepo().getObject(Vehicle.class, ref); + plantModelManager.setVehiclePose(ref, previousState.getPose().withOrientationAngle(angle)); + } + } + + @Override + public void updateVehiclePosition( + TCSObjectReference vehicleRef, + TCSObjectReference pointRef + ) + throws ObjectUnknownException { + requireNonNull(vehicleRef, "vehicleRef"); + + synchronized (globalSyncObject) { + LOG.debug("Vehicle {} has reached point {}.", vehicleRef, pointRef); + plantModelManager.setVehiclePosition(vehicleRef, pointRef); + } + } + + @Deprecated + @Override + public void updateVehiclePrecisePosition(TCSObjectReference ref, Triple position) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + Vehicle previousState = plantModelManager.getObjectRepo().getObject(Vehicle.class, ref); + plantModelManager.setVehiclePose(ref, previousState.getPose().withPosition(position)); + } + } + + @Override + public void updateVehiclePose(TCSObjectReference ref, Pose pose) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(pose, "pose"); + + synchronized (globalSyncObject) { + plantModelManager.setVehiclePose(ref, pose); + } + } + + @Override + public void updateVehicleProcState(TCSObjectReference ref, Vehicle.ProcState state) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(state, "state"); + + synchronized (globalSyncObject) { + LOG.debug("Updating procState of vehicle {} to {}...", ref.getName(), state); + plantModelManager.setVehicleProcState(ref, state); + } + } + + @Override + public void updateVehicleRechargeOperation( + TCSObjectReference ref, + String rechargeOperation + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(rechargeOperation, "rechargeOperation"); + + synchronized (globalSyncObject) { + plantModelManager.setVehicleRechargeOperation(ref, rechargeOperation); + } + } + + @Override + public void updateVehicleClaimedResources( + TCSObjectReference ref, + List>> resources + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(resources, "resources"); + + synchronized (globalSyncObject) { + plantModelManager.setVehicleClaimedResources(ref, resources); + } + } + + @Override + public void updateVehicleAllocatedResources( + TCSObjectReference ref, + List>> resources + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(resources, "resources"); + + synchronized (globalSyncObject) { + plantModelManager.setVehicleAllocatedResources(ref, resources); + } + } + + @Override + public void updateVehicleState(TCSObjectReference ref, Vehicle.State state) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(state, "state"); + + synchronized (globalSyncObject) { + plantModelManager.setVehicleState(ref, state); + } + } + + @Override + @Deprecated + public void updateVehicleLength(TCSObjectReference ref, int length) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + plantModelManager.setVehicleBoundingBox( + ref, + plantModelManager.getObjectRepo().getObject(Vehicle.class, ref) + .getBoundingBox() + .withLength(length) + ); + } + } + + @Override + public void updateVehicleTransportOrder( + TCSObjectReference vehicleRef, + TCSObjectReference orderRef + ) + throws ObjectUnknownException { + requireNonNull(vehicleRef, "vehicleRef"); + + synchronized (globalSyncObject) { + plantModelManager.setVehicleTransportOrder(vehicleRef, orderRef); + } + } + + @Override + public void attachCommAdapter( + TCSObjectReference ref, + VehicleCommAdapterDescription description + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(description, "description"); + + synchronized (globalSyncObject) { + attachmentManager.attachAdapterToVehicle( + ref.getName(), + commAdapterRegistry.findFactoryFor(description) + ); + } + } + + @Override + public void disableCommAdapter(TCSObjectReference ref) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + VehicleEntry entry = vehicleEntryPool.getEntryFor(ref.getName()); + if (entry == null) { + throw new IllegalArgumentException("No vehicle entry found for" + ref.getName()); + } + + entry.getCommAdapter().disable(); + } + } + + @Override + public void enableCommAdapter(TCSObjectReference ref) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + VehicleEntry entry = vehicleEntryPool.getEntryFor(ref.getName()); + if (entry == null) { + throw new IllegalArgumentException("No vehicle entry found for " + ref.getName()); + } + + entry.getCommAdapter().enable(); + } + } + + @Override + public VehicleAttachmentInformation fetchAttachmentInformation(TCSObjectReference ref) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + return attachmentManager.getAttachmentInformation(ref.getName()); + } + } + + @Override + public VehicleProcessModelTO fetchProcessModel(TCSObjectReference ref) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + VehicleEntry entry = vehicleEntryPool.getEntryFor(ref.getName()); + if (entry == null) { + throw new IllegalArgumentException("No vehicle entry found for " + ref.getName()); + } + + return entry.getCommAdapter().createTransferableProcessModel(); + } + } + + @Override + public void sendCommAdapterCommand(TCSObjectReference ref, AdapterCommand command) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(command, "command"); + + synchronized (globalSyncObject) { + vehicleControllerPool + .getVehicleController(ref.getName()) + .sendCommAdapterCommand(command); + } + } + + @Override + public void sendCommAdapterMessage(TCSObjectReference ref, Object message) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + vehicleControllerPool + .getVehicleController(ref.getName()) + .sendCommAdapterMessage(message); + } + } + + @Override + public void updateVehicleIntegrationLevel( + TCSObjectReference ref, + Vehicle.IntegrationLevel integrationLevel + ) + throws ObjectUnknownException, + KernelRuntimeException { + requireNonNull(ref, "ref"); + requireNonNull(integrationLevel, "integrationLevel"); + + synchronized (globalSyncObject) { + Vehicle vehicle = fetchObject(Vehicle.class, ref); + + if (vehicle.isProcessingOrder() + && (integrationLevel == Vehicle.IntegrationLevel.TO_BE_IGNORED + || integrationLevel == Vehicle.IntegrationLevel.TO_BE_NOTICED)) { + throw new IllegalArgumentException( + String.format( + "%s: Cannot change integration level to %s while processing orders.", + vehicle.getName(), + integrationLevel.name() + ) + ); + } + + plantModelManager.setVehicleIntegrationLevel(ref, integrationLevel); + } + } + + @Override + public void updateVehiclePaused(TCSObjectReference ref, boolean paused) + throws ObjectUnknownException, + KernelRuntimeException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + plantModelManager.setVehiclePaused(ref, paused); + + vehicleControllerPool.getVehicleController(ref.getName()).onVehiclePaused(paused); + } + } + + @Override + public void updateVehicleEnergyLevelThresholdSet( + TCSObjectReference ref, + EnergyLevelThresholdSet energyLevelThresholdSet + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(energyLevelThresholdSet, "energyLevelThresholdSet"); + + synchronized (globalSyncObject) { + plantModelManager.setVehicleEnergyLevelThresholdSet(ref, energyLevelThresholdSet); + } + } + + @Override + public void updateVehicleAllowedOrderTypes( + TCSObjectReference ref, + Set allowedOrderTypes + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(allowedOrderTypes, "allowedOrderTypes"); + + synchronized (globalSyncObject) { + plantModelManager.setVehicleAllowedOrderTypes(ref, allowedOrderTypes); + } + } + + @Override + @ScheduledApiChange(when = "7.0", details = "Envelope key will become non-null.") + public void updateVehicleEnvelopeKey(TCSObjectReference ref, String envelopeKey) + throws ObjectUnknownException, + IllegalArgumentException, + KernelRuntimeException { + requireNonNull(ref, "ref"); + + synchronized (globalSyncObject) { + Vehicle vehicle = fetchObject(Vehicle.class, ref); + if (vehicle.isProcessingOrder() + || !vehicle.getClaimedResources().isEmpty() + || !vehicle.getAllocatedResources().isEmpty()) { + throw new IllegalArgumentException( + "Updating a vehicle's envelope key while the vehicle is processing an order or " + + "claiming/allocating resources is currently not supported." + ); + } + + plantModelManager.setVehicleEnvelopeKey(ref, envelopeKey); + } + } + + @Override + public void updateVehicleBoundingBox(TCSObjectReference ref, BoundingBox boundingBox) + throws ObjectUnknownException, + KernelRuntimeException { + requireNonNull(ref, "ref"); + requireNonNull(boundingBox, "boundingBox"); + + synchronized (globalSyncObject) { + plantModelManager.setVehicleBoundingBox(ref, boundingBox); + } + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/CommandProcessingTracker.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/CommandProcessingTracker.java new file mode 100644 index 0000000..a507456 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/CommandProcessingTracker.java @@ -0,0 +1,772 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; +import static org.opentcs.util.Assertions.checkState; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.SequencedCollection; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResource; +import org.opentcs.drivers.vehicle.MovementCommand; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tracks processing of movement commands. + *

+ * After the movement commands for a new or updated drive order have been passed to + * {@link #driveOrderUpdated(SequencedCollection)}, movement commands and their corresponding + * resources are expected to be processed in the following order: + *

    + *
  1. {@link #allocationRequested(Set)}
  2. + *
  3. {@link #allocationConfirmed(Set)}
  4. + *
  5. {@link #commandSent(MovementCommand)}
  6. + *
  7. {@link #commandExecuted(MovementCommand)}
  8. + *
  9. {@link #allocationReleased(Set)}
  10. + *
+ */ +public class CommandProcessingTracker { + + private static final Logger LOG = LoggerFactory.getLogger(CommandProcessingTracker.class); + /** + * The queue of commands that still need to be sent to the communication adapter. + */ + private final Deque futureCommands = new ArrayDeque<>(); + /** + * A command (the next one for the current drive order) that has yet to be sent to the + * communication adapter. + */ + private CommandResourcePair pendingCommand; + /** + * The state the pending command is currently in. + */ + private PendingCommandState pendingCommandState = PendingCommandState.UNDEFINED; + /** + * The queue of commands that have been sent to the communication adapter. + */ + private final Deque sentCommands = new ArrayDeque<>(); + /** + * The command that was executed last. + */ + private MovementCommand lastCommandExecuted; + /** + * The queue of resource sets that the vehicle has already passed. + *

+ * For every movement command that is executed, two elements are added to this queue - one element + * containing only the path (if any) associated with the respective movement command, and another + * element containing the point and the location (if any) associated with the respective movement + * command. + *

+ */ + private final Deque>> passedResources = new ArrayDeque<>(); + + /** + * Creates a new instance. + */ + public CommandProcessingTracker() { + } + + /** + * Clears (i.e. resets) this instance. + */ + public void clear() { + futureCommands.clear(); + pendingCommand = null; + pendingCommandState = PendingCommandState.UNDEFINED; + sentCommands.clear(); + lastCommandExecuted = null; + passedResources.clear(); + } + + /** + * Called when a drive order was updated (either because a new one was assigned to a vehicle or + * the one being currently processed was updated due to rerouting). + * + * @param movementCommands The collection of movement commands that belong to the updated drive + * order. + */ + public void driveOrderUpdated( + @Nonnull + SequencedCollection movementCommands + ) { + requireNonNull(movementCommands, "movementCommands"); + + if (isDriveOrderFinished()) { + // The movement commands belong to a new drive order. + futureCommands.addAll(toCommandResourcePairs(movementCommands)); + } + else { + // The movement commands belong to the same drive order we are currently processing. + futureCommands.clear(); + if (pendingCommandState == PendingCommandState.ALLOCATION_PENDING) { + // With drive order updates, any pending resource allocation is reset. + pendingCommand = null; + pendingCommandState = PendingCommandState.UNDEFINED; + } + + futureCommands.addAll(toCommandResourcePairs(movementCommands)); + + // The current drive order got updated but our queue of future commands now contains commands + // that have already been processed, so discard these. + discardProcessedFutureCommands(); + } + } + + /** + * Called when a drive order was aborted. + * + * @param immediate Indicates whether the drive order was aborted immediately or regularly. + */ + public void driveOrderAborted(boolean immediate) { + if (immediate) { + futureCommands.clear(); + pendingCommand = null; + pendingCommandState = PendingCommandState.UNDEFINED; + sentCommands.clear(); + } + else { + futureCommands.clear(); + if (pendingCommandState != PendingCommandState.SENDING_PENDING) { + pendingCommand = null; + pendingCommandState = PendingCommandState.UNDEFINED; + } + } + } + + /** + * Checks if there are any movement commands that are yet to be sent to the communication adapter. + * + * @return {@code true} if there are commands to be sent, otherwise {@code false}. + */ + public boolean hasCommandsToBeSent() { + return !futureCommands.isEmpty() + || pendingCommandState == PendingCommandState.ALLOCATION_PENDING + || pendingCommandState == PendingCommandState.SENDING_PENDING; + } + + /** + * Checks if the current drive order is considered finished. + * + * @return {@code true}, if there are no more commands that need to be sent to the communication + * adapter and all commands already sent have been reported as executed. + */ + public boolean isDriveOrderFinished() { + return futureCommands.isEmpty() + && pendingCommand == null + && sentCommands.isEmpty(); + } + + /** + * Called when a vehicle's allocation was reset. + * + * @param resources The (only) set of resources that the vehicle now allocates. + */ + public void allocationReset( + @Nonnull + Set> resources + ) { + requireNonNull(resources, "resources"); + + // Clear resources that have been passed previously as they are no longer allocated. + passedResources.clear(); + + if (!resources.isEmpty()) { + // Now, the given resources are allocated and considered as the new passed resources. + passedResources.add(resources); + } + + // Discard the pending command since pending allocations are reset and resources that have + // already been allocated are freed when allocation is reset. + pendingCommand = null; + pendingCommandState = PendingCommandState.UNDEFINED; + + // Clear sent commands since we don't expect a vehicle to report these commands as executed + // after allocation has been reset. + sentCommands.clear(); + } + + /** + * Called when a resource allocation was requested. + * + * @param resources The resources for which allocation was requested. + */ + public void allocationRequested( + @Nonnull + Set> resources + ) { + requireNonNull(resources, "resources"); + checkArgument( + !futureCommands.isEmpty(), + "Allocation requested, but there are no future commands: %s", + resources + ); + checkArgument( + Objects.equals(futureCommands.peek().getResources(), resources), + "Resource set is not head of future commands: %s (futureCommands=%s)", + resources, + futureCommands + ); + checkArgument( + pendingCommandState == PendingCommandState.UNDEFINED, + "pendingCommandState is not '%s' but '%s'", + PendingCommandState.UNDEFINED, + pendingCommandState + ); + + pendingCommand = futureCommands.remove(); + pendingCommandState = PendingCommandState.ALLOCATION_PENDING; + } + + /** + * Called when a resource allocation was confirmed. + * + * @param resources The resources for which allocation was confirmed. + */ + public void allocationConfirmed( + @Nonnull + Set> resources + ) { + requireNonNull(resources, "resources"); + checkArgument( + pendingCommandState == PendingCommandState.ALLOCATION_PENDING, + "pendingCommandState is not '%s' but '%s'", + PendingCommandState.ALLOCATION_PENDING, + pendingCommandState + ); + checkArgument( + Objects.equals(pendingCommand.getResources(), resources), + "Resource set does not belong to pending command: %s (pendingCommand=%s)", + resources, + pendingCommand + ); + + pendingCommandState = PendingCommandState.SENDING_PENDING; + } + + /** + * Called when a resource allocation was revoked. + * + * @param resources The resources for which allocation was revoked. + */ + public void allocationRevoked( + @Nonnull + Set> resources + ) { + requireNonNull(resources, "resources"); + checkArgument( + pendingCommandState == PendingCommandState.SENDING_PENDING, + "pendingCommandState is not '%s' but '%s'", + PendingCommandState.SENDING_PENDING, + pendingCommandState + ); + checkArgument( + Objects.equals(pendingCommand.getResources(), resources), + "Resource set does not belong to pending command: %s (pendingCommand=%s)", + resources, + pendingCommand + ); + + pendingCommand = null; + pendingCommandState = PendingCommandState.UNDEFINED; + } + + /** + * Called when a movement command won't be sent to the communication adapter. + * + * @param movementCommand The movement command. + */ + public void commandSendingStopped( + @Nonnull + MovementCommand movementCommand + ) { + requireNonNull(movementCommand, "movementCommand"); + checkArgument( + pendingCommandState == PendingCommandState.SENDING_PENDING, + "pendingCommandState is not '%s' but '%s'", + PendingCommandState.SENDING_PENDING, + pendingCommandState + ); + checkArgument( + Objects.equals(pendingCommand.getMovementCommand(), movementCommand), + "Movement command does not belong to pending command: %s (pendingCommand=%s)", + movementCommand, + pendingCommand + ); + + pendingCommandState = PendingCommandState.WONT_SEND; + } + + /** + * Called when a movement command was sent to the communication adapter. + * + * @param movementCommand The movement command. + */ + public void commandSent( + @Nonnull + MovementCommand movementCommand + ) { + requireNonNull(movementCommand, "movementCommand"); + checkArgument( + pendingCommandState == PendingCommandState.SENDING_PENDING, + "pendingCommandState is not '%s' but '%s'", + PendingCommandState.SENDING_PENDING, + pendingCommandState + ); + checkArgument( + Objects.equals(pendingCommand.getMovementCommand(), movementCommand), + "Movement command does not belong to pending command: %s (pendingCommand=%s)", + movementCommand, + pendingCommand + ); + + sentCommands.add(pendingCommand); + pendingCommand = null; + pendingCommandState = PendingCommandState.UNDEFINED; + } + + /** + * Called when a movement command was reported as executed. + * + * @param movementCommand The movement command. + */ + public void commandExecuted( + @Nonnull + MovementCommand movementCommand + ) { + requireNonNull(movementCommand, "movementCommand"); + checkArgument( + !sentCommands.isEmpty(), + "Movement command reported as executed, but no commands have been sent: %s", + movementCommand + ); + MovementCommand expectedCommand = sentCommands.peek().getMovementCommand(); + checkArgument( + Objects.equals(expectedCommand, movementCommand), + "%s: Unexpected movement command executed: %s != %s", + movementCommand.getTransportOrder().getProcessingVehicle().getName(), + movementCommand, + expectedCommand + ); + + CommandResourcePair executedCommand = sentCommands.remove(); + lastCommandExecuted = executedCommand.getMovementCommand(); + passedResources.add(extractPath(executedCommand.getResources())); + passedResources.add(extractPointAndLocation(executedCommand.getResources())); + } + + /** + * Called when a resource allocation was released. + * + * @param resources The resources for which allocation was released. + */ + public void allocationReleased( + @Nonnull + Set> resources + ) { + requireNonNull(resources, "resources"); + checkArgument( + Objects.equals(passedResources.peek(), resources), + "Resource set is not head of passed resources: %s (passedResources=%s)", + resources, + passedResources + ); + + passedResources.remove(); + } + + /** + * Returns the queue of resources claimed by the vehicle. + *

+ * The order of the elements in this queue corresponds to the order in which they will be + * allocated, with the first element in the queue (i.e. its head) corresponding to the resources + * that will be allocated next. + *

+ * + * @return The queue of resources claimed by the vehicle. + */ + @Nonnull + public Deque>> getClaimedResources() { + Deque>> claimedResources = new ArrayDeque<>(); + + if (pendingCommandState == PendingCommandState.ALLOCATION_PENDING) { + claimedResources.add(pendingCommand.getResources()); + } + + futureCommands.stream() + .map(CommandResourcePair::getResources) + .forEach(claimedResources::add); + + return claimedResources; + } + + /** + * Returns the queue of resources allocated by the vehicle. + *

+ * The order of the elements in this queue corresponds to the order in which they were allocated, + * with the first element in the queue (i.e. its head) corresponding to the oldest resources. + *

+ * + * @return The queue of resources allocated by the vehicle. + */ + @Nonnull + public Deque>> getAllocatedResources() { + Deque>> allocatedResources = new ArrayDeque<>(); + + allocatedResources.addAll(passedResources); + allocatedResources.addAll(getAllocatedResourcesAhead()); + + return allocatedResources; + } + + /** + * Returns the queue of resources allocated by the vehicle that lie in front of it. + *

+ * The order of the elements in this queue corresponds to the order in which they were allocated, + * with the first element in the queue (i.e. its head) corresponding to the resources right in + * front of the vehicle. + *

+ * + * @return The queue of allocated resources in front of the vehicle. + */ + @Nonnull + public Deque>> getAllocatedResourcesAhead() { + Deque>> allocatedResourcesAhead = new ArrayDeque<>(); + + sentCommands.stream() + .map(CommandResourcePair::getResources) + .forEach(allocatedResourcesAhead::add); + + if (pendingCommandState == PendingCommandState.SENDING_PENDING + || pendingCommandState == PendingCommandState.WONT_SEND) { + allocatedResourcesAhead.add(pendingCommand.getResources()); + } + + return allocatedResourcesAhead; + } + + /** + * Returns the movement command for which resource allocation is currently pending. + * + * @return An optional containing the movement command for which resource allocation is currently + * pending or {@link Optional#empty()} if there is no such command. + * @see #getAllocationPendingResources() + */ + public Optional getAllocationPendingCommand() { + if (pendingCommandState == PendingCommandState.ALLOCATION_PENDING) { + return Optional.of(pendingCommand.getMovementCommand()); + } + + return Optional.empty(); + } + + /** + * Returns the resources for which allocation is currently pending. + * + * @return An optional containing the resources for which allocation is currently pending or + * {@link Optional#empty()} if there are no such resources. + * @see #getAllocationPendingCommand() + */ + public Optional>> getAllocationPendingResources() { + if (pendingCommandState == PendingCommandState.ALLOCATION_PENDING) { + return Optional.of(pendingCommand.getResources()); + } + + return Optional.empty(); + } + + /** + * Returns the movement command for which resources have already been allocated but which is yet + * to be sent to the communication adapter. + * + * @return An optional containing the movement command for which resources have already been + * allocated but which is yet to be sent to the communication adapter or {@link Optional#empty()} + * if there is no such command. + */ + public Optional getSendingPendingCommand() { + if (pendingCommandState == PendingCommandState.SENDING_PENDING) { + return Optional.of(pendingCommand.getMovementCommand()); + } + + return Optional.empty(); + } + + /** + * Returns the queue of movement commands that have been sent to the communication adapter but + * have not yet been reported as executed. + * + * @return The queue of movement commands that have been sent to the communication adapter but + * have not yet been reported as executed. + */ + public Deque getSentCommands() { + return sentCommands.stream() + .map(CommandResourcePair::getMovementCommand) + .collect(Collectors.toCollection(ArrayDeque::new)); + } + + /** + * Returns the movement command that was executed last. + * + * @return The movement command that was executed last. + */ + public Optional getLastCommandExecuted() { + return Optional.ofNullable(lastCommandExecuted); + } + + /** + * Returns the movement command for which resources are to be allocated next. + * + * @return The movement command for which resources are to be allocated next. + * @see #getNextAllocationResources() + */ + public Optional getNextAllocationCommand() { + return Optional.ofNullable(futureCommands.peek()) + .map(CommandResourcePair::getMovementCommand); + } + + /** + * Returns the resources that are to be allocated next. + * + * @return The resources that are to be allocated next. + * @see #getNextAllocationCommand() + */ + public Optional>> getNextAllocationResources() { + return Optional.ofNullable(futureCommands.peek()) + .map(CommandResourcePair::getResources); + } + + /** + * Checks if there are resources for which allocation was requested but is yet to be confirmed. + * + * @return {@code true} if there are resources for which allocation was requested but is yet to + * be confirmed, otherwise {@code false}. + */ + public boolean isWaitingForAllocation() { + return pendingCommandState == PendingCommandState.ALLOCATION_PENDING; + } + + private SequencedCollection toCommandResourcePairs( + SequencedCollection movementCommands + ) { + return movementCommands.stream() + .map(command -> new CommandResourcePair(command, getNeededResources(command))) + .toList(); + } + + private void discardProcessedFutureCommands() { + MovementCommand lastCommandProcessed = lastCommandProcessed(); + if (futureCommands.isEmpty()) { + // There are no commands to be discarded. + return; + } + + if (!fromSameDriveOrder(lastCommandProcessed, futureCommands.peek().getMovementCommand())) { + // If the last processed command is from a different drive order, there is nothing to be + // discarded. This is the case, for example, if the vehicle didn't yet process the very first + // movement command of a new drive order. + return; + } + + LOG.debug( + "{}: Discarding future commands up to '{}' (inclusively): {}", + lastCommandProcessed.getTransportOrder().getProcessingVehicle().getName(), + lastCommandProcessed, + futureCommands + ); + // Discard commands up to lastCommandProcessed... + while (!futureCommands.isEmpty() + && !lastCommandProcessed.equalsInMovement(futureCommands.peek().getMovementCommand())) { + futureCommands.remove(); + } + + checkState( + !futureCommands.isEmpty(), + "%s: Future commands should not be empty.", + lastCommandProcessed.getTransportOrder().getProcessingVehicle().getName(), + lastCommandProcessed + ); + checkState( + lastCommandProcessed.equalsInMovement(futureCommands.peek().getMovementCommand()), + "%s: Last command processed is not head of future commands: %s (futureCommands=%s)", + lastCommandProcessed.getTransportOrder().getProcessingVehicle().getName(), + lastCommandProcessed, + futureCommands + ); + + // ...and also discard lastCommandProcessed itself. + futureCommands.remove(); + } + + /** + * Returns the last movement command that has been processed in a way that is relevant in the + * context of rerouting. + *

+ * Generally, a movement command is processed in multiple stages. It is: + *

    + *
  1. Added to the futureCommands queue (when a transport order for the vehicle + * is set or updated).
  2. + *
  3. Removed from the futureCommands queue and set as the + * pendingCommand (when allocation for the resources needed for executing the next + * command has been requested).
  4. + *
  5. Unset as the pendingCommand and added to the + * commandsSent queue (when the command has been handed over to the vehicle driver). + *
  6. + *
  7. Removed from the commandsSent queue and set as the + * lastCommandExecuted (when the driver reports that the command has been executed) + *
  8. + *
+ *

+ *

+ * The earliest stage a movement command can be in that is relevant in the context of rerouting is + * when it is set as the pendingCommand with the state + * {@link PendingCommandState#SENDING_PENDING}. At this stage, the resources for the command have + * already been (successfully) allocated, and it will either be handed over to the vehicle driver + * or discarded. Rerouting should therefore take place from this command (or rather the respective + * step) at the earliest. + *

+ *

+ * pendingCommand with the state {@link PendingCommandState#ALLOCATION_PENDING} + * (as well as everything prior to that) is not relevant here, as the allocation for corresponding + * resources is still pending at this stage, and all pending allocations are cleared upon + * rerouting. + *

+ * + * @return A movement command or {@code null} if there is no movement command that has been + * processed by this vehicle controller in a way that is relevant in the context of rerouting. + */ + @Nullable + private MovementCommand lastCommandProcessed() { + return Optional.ofNullable(pendingCommand) + .or(() -> Optional.ofNullable(sentCommands.peekLast())) + .map(CommandResourcePair::getMovementCommand) + .orElse(lastCommandExecuted); + } + + private boolean fromSameDriveOrder( + @Nullable + MovementCommand commandA, + @Nullable + MovementCommand commandB + ) { + return commandA != null + && commandB != null + && Objects.equals(commandA.getDriveOrder(), commandB.getDriveOrder()); + } + + /** + * Returns a set of resources needed for executing the given command. + * + * @param cmd The command for which to return the needed resources. + * @return A set of resources needed for executing the given command. + */ + @Nonnull + private Set> getNeededResources(MovementCommand cmd) { + requireNonNull(cmd, "cmd"); + + Set> result = new HashSet<>(); + result.add(cmd.getStep().getDestinationPoint()); + if (cmd.getStep().getPath() != null) { + result.add(cmd.getStep().getPath()); + } + if (cmd.getOpLocation() != null) { + result.add(cmd.getOpLocation()); + } + + return result; + } + + private Set> extractPointAndLocation(Set> resources) { + return resources.stream() + .filter(resource -> resource instanceof Point || resource instanceof Location) + .collect(Collectors.toSet()); + } + + private Set> extractPath(Set> resources) { + return resources.stream() + .filter(resource -> resource instanceof Path) + .collect(Collectors.toSet()); + } + + /** + * Defines the states the pending command can be in. + */ + private enum PendingCommandState { + /** + * The state is undefined. This is the case when the pending command is {@code null}. + */ + UNDEFINED, + /** + * Allocation of the resources for the pending command was requested but is yet to be confirmed. + */ + ALLOCATION_PENDING, + /** + * Allocation of the resources for the pending command was confirmed but the command is yet to + * be sent to the communication adapter. + */ + SENDING_PENDING, + /** + * The pending command won't be sent to the communication adapter but the corresponding + * resources are still allocated. + */ + WONT_SEND; + } + + /** + * A wrapper for a {@link MovementCommand} and the {@link TCSResource}s that are associated with + * it. + */ + private static class CommandResourcePair { + + private final MovementCommand movementCommand; + private final Set> resources; + + /** + * Creates a new instance. + * + * @param movementCommand The movement command. + * @param resources The set of resources associated with the movement command. + */ + CommandResourcePair(MovementCommand movementCommand, Set> resources) { + this.movementCommand = requireNonNull(movementCommand, "movementCommand"); + this.resources = requireNonNull(resources, "resources"); + } + + /** + * Returns the movement command. + * + * @return The movement command. + */ + public MovementCommand getMovementCommand() { + return movementCommand; + } + + /** + * Returns the set of resources associated with the movement command. + * + * @return The set of resources associated with the movement command. + */ + public Set> getResources() { + return resources; + } + + @Override + public String toString() { + return "CommandResourcePair{" + + "movementCommand=" + movementCommand + + ", resources=" + resources + + '}'; + } + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/DefaultVehicleController.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/DefaultVehicleController.java new file mode 100644 index 0000000..2df4348 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/DefaultVehicleController.java @@ -0,0 +1,1533 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.kernel.vehicles.MovementComparisons.equalsInMovement; +import static org.opentcs.util.Assertions.checkArgument; +import static org.opentcs.util.Assertions.checkState; + +import com.google.inject.assistedinject.Assisted; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.components.kernel.ResourceAllocationException; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.components.kernel.services.InternalTransportOrderService; +import org.opentcs.components.kernel.services.InternalVehicleService; +import org.opentcs.components.kernel.services.NotificationService; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.BoundingBox; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Pose; +import org.opentcs.data.model.TCSResource; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.notification.UserNotification; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.data.order.Route.Step; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.IncomingPoseTransformer; +import org.opentcs.drivers.vehicle.LoadHandlingDevice; +import org.opentcs.drivers.vehicle.MovementCommand; +import org.opentcs.drivers.vehicle.MovementCommandTransformer; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; +import org.opentcs.drivers.vehicle.VehicleController; +import org.opentcs.drivers.vehicle.VehicleProcessModel; +import org.opentcs.drivers.vehicle.management.ProcessModelEvent; +import org.opentcs.kernel.KernelApplicationConfiguration; +import org.opentcs.kernel.vehicles.transformers.VehicleDataTransformerRegistry; +import org.opentcs.util.ExplainedBoolean; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Realizes a bidirectional connection between the kernel and a communication adapter controlling a + * vehicle. + */ +public class DefaultVehicleController + implements + VehicleController, + Scheduler.Client, + PropertyChangeListener, + EventHandler { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(DefaultVehicleController.class); + /** + * The kernel's vehicle service. + */ + private final InternalVehicleService vehicleService; + /** + * The kernel's transport order service. + */ + private final InternalTransportOrderService transportOrderService; + /** + * The kernel's notification service. + */ + private final NotificationService notificationService; + /** + * The kernel's dispatcher service. + */ + private final DispatcherService dispatcherService; + /** + * The scheduler maintaining the resources. + */ + private final Scheduler scheduler; + /** + * The event bus we should register with and send events to. + */ + private final EventBus eventBus; + /** + * The vehicle controlled by this controller/the communication adapter. + */ + private final Vehicle vehicle; + /** + * The communication adapter controlling the physical vehicle. + */ + private final VehicleCommAdapter commAdapter; + /** + * This controller's enabled flag. + */ + private volatile boolean initialized; + /** + * Manages interactions with peripheral devices that are to be performed before or after the + * execution of movement commands. + */ + private final PeripheralInteractor peripheralInteractor; + /** + * Maps drive orders to movement commands. + */ + private final MovementCommandMapper movementCommandMapper; + /** + * The configuration to use. + */ + private final KernelApplicationConfiguration configuration; + /** + * The transport order that the vehicle is currently processing. + */ + private volatile TransportOrder transportOrder; + /** + * The drive order that the vehicle currently has to process. + */ + private volatile DriveOrder currentDriveOrder; + /** + * A flag indicating if the vehicle controller is allowed to send commands to the vehicle driver. + */ + private boolean sendingCommandsAllowed; + /** + * Tracks processing of movement commands. + */ + private final CommandProcessingTracker commandProcessingTracker; + /** + * A transformer transforming movement commands. + */ + private final MovementCommandTransformer movementCommandTransformer; + /** + * A transformer transforming incoming poses. + */ + private final IncomingPoseTransformer incomingPoseTransformer; + /** + * A map of transformed movement commands to their corresponding original ones. + */ + private final Map transformedToOriginalCommands + = new HashMap<>(); + + /** + * Creates a new instance associated with the given vehicle. + * + * @param vehicle The vehicle this vehicle controller will be associated with. + * @param adapter The communication adapter of the associated vehicle. + * @param vehicleService The kernel's vehicle service. + * @param transportOrderService The kernel's transport order service. + * @param notificationService The kernel's notification service. + * @param dispatcherService The kernel's dispatcher service. + * @param scheduler The scheduler managing resource allocations. + * @param eventBus The event bus this instance should register with and send events to. + * @param componentsFactory A factory for various components related to a vehicle controller. + * @param movementCommandMapper Maps drive orders to movement commands. + * @param configuration The configuration to use. + * @param commandProcessingTracker Track processing of movement commands. + * @param dataTransformerRegistry A registry for data transformer factories. + */ + @Inject + public DefaultVehicleController( + @Assisted + @Nonnull + Vehicle vehicle, + @Assisted + @Nonnull + VehicleCommAdapter adapter, + @Nonnull + InternalVehicleService vehicleService, + @Nonnull + InternalTransportOrderService transportOrderService, + @Nonnull + NotificationService notificationService, + @Nonnull + DispatcherService dispatcherService, + @Nonnull + Scheduler scheduler, + @Nonnull + @ApplicationEventBus + EventBus eventBus, + @Nonnull + VehicleControllerComponentsFactory componentsFactory, + @Nonnull + MovementCommandMapper movementCommandMapper, + @Nonnull + KernelApplicationConfiguration configuration, + @Nonnull + CommandProcessingTracker commandProcessingTracker, + @Nonnull + VehicleDataTransformerRegistry dataTransformerRegistry + ) { + this.vehicle = requireNonNull(vehicle, "vehicle"); + this.commAdapter = requireNonNull(adapter, "adapter"); + this.vehicleService = requireNonNull(vehicleService, "vehicleService"); + this.transportOrderService = requireNonNull(transportOrderService, "transportOrderService"); + this.notificationService = requireNonNull(notificationService, "notificationService"); + this.dispatcherService = requireNonNull(dispatcherService, "dispatcherService"); + this.scheduler = requireNonNull(scheduler, "scheduler"); + this.eventBus = requireNonNull(eventBus, "eventBus"); + requireNonNull(componentsFactory, "componentsFactory"); + this.peripheralInteractor + = componentsFactory.createPeripheralInteractor(vehicle.getReference()); + this.movementCommandMapper = requireNonNull(movementCommandMapper, "movementCommandMapper"); + this.configuration = requireNonNull(configuration, "configuration"); + this.commandProcessingTracker + = requireNonNull(commandProcessingTracker, "commandProcessingTracker"); + requireNonNull(dataTransformerRegistry, "dataTransformerRegistry"); + this.movementCommandTransformer + = dataTransformerRegistry + .findFactoryFor(vehicle) + .createMovementCommandTransformer(vehicle); + this.incomingPoseTransformer + = dataTransformerRegistry + .findFactoryFor(vehicle) + .createIncomingPoseTransformer(vehicle); + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + eventBus.subscribe(this); + + vehicleService.updateVehicleRechargeOperation( + vehicle.getReference(), + commAdapter.getRechargeOperation() + ); + commAdapter.getProcessModel().addPropertyChangeListener(this); + + // Initialize standard attributes once. + setVehiclePosition(commAdapter.getProcessModel().getPosition()); + updateVehiclePose(commAdapter.getProcessModel().getPose()); + vehicleService.updateVehicleEnergyLevel( + vehicle.getReference(), + commAdapter.getProcessModel().getEnergyLevel() + ); + vehicleService.updateVehicleLoadHandlingDevices( + vehicle.getReference(), + commAdapter.getProcessModel().getLoadHandlingDevices() + ); + updateVehicleState(commAdapter.getProcessModel().getState()); + updateVehicleBoundingBox(commAdapter.getProcessModel().getBoundingBox()); + + commandProcessingTracker.clear(); + + peripheralInteractor.initialize(); + + sendingCommandsAllowed = true; + + initialized = true; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + peripheralInteractor.terminate(); + + commAdapter.getProcessModel().removePropertyChangeListener(this); + // Reset the vehicle's position. + updatePosition(null, null); + updateVehiclePose(new Pose(null, Double.NaN)); + // Free all allocated resources. + freeAllResources(); + + updateVehicleState(Vehicle.State.UNKNOWN); + + eventBus.unsubscribe(this); + + sendingCommandsAllowed = false; + + initialized = false; + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getSource() != commAdapter.getProcessModel()) { + return; + } + + handleProcessModelEvent(evt); + } + + @Override + public void onEvent(Object event) { + if (!(event instanceof TCSObjectEvent)) { + return; + } + + TCSObjectEvent objectEvent = (TCSObjectEvent) event; + if (objectEvent.getType() != TCSObjectEvent.Type.OBJECT_MODIFIED) { + return; + } + + if (!(objectEvent.getCurrentOrPreviousObjectState() instanceof Vehicle)) { + return; + } + + if (!(Objects.equals( + objectEvent.getCurrentOrPreviousObjectState().getName(), + vehicle.getName() + ))) { + return; + } + + Vehicle prevVehicleState = (Vehicle) objectEvent.getPreviousObjectState(); + Vehicle currVehicleState = (Vehicle) objectEvent.getCurrentObjectState(); + + if (prevVehicleState.getIntegrationLevel() != currVehicleState.getIntegrationLevel()) { + onIntegrationLevelChange(prevVehicleState, currVehicleState); + } + } + + @Override + public void setTransportOrder( + @Nonnull + TransportOrder newOrder + ) + throws IllegalArgumentException { + requireNonNull(newOrder, "newOrder"); + requireNonNull(newOrder.getCurrentDriveOrder(), "newOrder.getCurrentDriveOrder()"); + + if (transportOrder == null + || !Objects.equals(newOrder.getName(), transportOrder.getName()) + || newOrder.getCurrentDriveOrderIndex() != transportOrder.getCurrentDriveOrderIndex()) { + // We received either a new transport order or the same transport order for its next drive + // order. + sendingCommandsAllowed = true; + transformedToOriginalCommands.clear(); + transportOrder = newOrder; + setDriveOrder(transportOrder.getCurrentDriveOrder(), transportOrder.getProperties()); + } + else { + // We received an update for a drive order we're already processing. + transportOrder = newOrder; + + checkArgument( + driveOrdersContinual(currentDriveOrder, transportOrder.getCurrentDriveOrder()), + "The new and old drive orders are not considered continual." + ); + + if (isForcedRerouting(transportOrder.getCurrentDriveOrder())) { + Vehicle currVehicle = vehicleService.fetchObject(Vehicle.class, vehicle.getReference()); + if (currVehicle.getCurrentPosition() == null) { + throw new IllegalArgumentException("The vehicle's current position is unknown."); + } + + sendingCommandsAllowed = true; + transformedToOriginalCommands.clear(); + + Point currPosition = vehicleService.fetchObject( + Point.class, + currVehicle.getCurrentPosition() + ); + // Before interacting with the scheduler in any way, ensure that we will be able to + // allocate the required resources. + if (!mayAllocateNow(Set.of(currPosition))) { + throw new IllegalArgumentException( + "Resources for the vehicle's current position may not be allocated now." + ); + } + + freeAllResources(); + try { + // Allocate the resources for the vehicle's current position. + scheduler.allocateNow(this, Set.of(currPosition)); + commandProcessingTracker.allocationReset(Set.of(currPosition)); + vehicleService.updateVehicleAllocatedResources( + vehicle.getReference(), + toListOfResourceSets(commandProcessingTracker.getAllocatedResources()) + ); + } + catch (ResourceAllocationException ex) { + // May never happen. The caller is expected to call mayAllocateNow() first before applying + // forced rerouting. + throw new IllegalArgumentException( + "Unable to allocate resources for the vehicle's current position.", + ex + ); + } + } + + updateDriveOrder(transportOrder.getCurrentDriveOrder(), transportOrder.getProperties()); + } + } + + private void setDriveOrder( + @Nonnull + DriveOrder newOrder, + @Nonnull + Map orderProperties + ) + throws IllegalArgumentException { + synchronized (commAdapter) { + requireNonNull(newOrder, "newOrder"); + requireNonNull(orderProperties, "orderProperties"); + requireNonNull(newOrder.getRoute(), "newOrder.getRoute()"); + // Assert that there isn't still is a drive order that hasn't been finished/removed, yet. + checkArgument( + currentDriveOrder == null, + "%s still has an order! Current order: %s, new order: %s", + vehicle.getName(), + currentDriveOrder, + newOrder + ); + + LOG.debug("{}: Setting drive order: {}", vehicle.getName(), newOrder); + + currentDriveOrder = newOrder; + + commandProcessingTracker.driveOrderUpdated( + movementCommandMapper.toMovementCommands(newOrder, transportOrder) + ); + + // Set the claim for (the remainder of) this transport order. + List>> claim = currentClaim(transportOrder); + scheduler.claim(this, claim); + + vehicleService.updateVehicleClaimedResources( + vehicle.getReference(), + toListOfResourceSets(claim) + ); + + if (canSendNextCommand()) { + allocateForNextCommand(); + } + + // Set the vehicle's next expected position. + Point nextPoint = newOrder.getRoute().getSteps().get(0).getDestinationPoint(); + vehicleService.updateVehicleNextPosition( + vehicle.getReference(), + nextPoint.getReference() + ); + } + } + + private void updateDriveOrder( + @Nonnull + DriveOrder newOrder, + @Nonnull + Map orderProperties + ) + throws IllegalArgumentException { + synchronized (commAdapter) { + requireNonNull(newOrder, "newOrder"); + checkArgument(currentDriveOrder != null, "There's no drive order to be updated"); + + LOG.debug("{}: Updating drive order: {}", vehicle.getName(), newOrder); + // Update the current drive order and future commands + currentDriveOrder = newOrder; + // There is a new drive order, so discard all the future/scheduled commands of the old one. + discardFutureCommands(); + + commandProcessingTracker.driveOrderUpdated( + movementCommandMapper.toMovementCommands(newOrder, transportOrder) + ); + + // Update the claim. + List>> claim = currentClaim(transportOrder); + scheduler.claim(this, claim); + + vehicleService.updateVehicleClaimedResources( + vehicle.getReference(), + toListOfResourceSets(claim) + ); + + // The vehicle may now process previously restricted steps. + if (canSendNextCommand()) { + allocateForNextCommand(); + } + } + } + + private boolean driveOrdersContinual(DriveOrder oldOrder, DriveOrder newOrder) { + LOG.debug( + "{}: Checking drive order continuity for {} (old) and {} (new).", + vehicle.getName(), oldOrder, newOrder + ); + + if (getLastCommandExecutedRouteIndex() == TransportOrder.ROUTE_STEP_INDEX_DEFAULT) { + LOG.debug("{}: Drive orders continuous: No route progress, yet.", vehicle.getName()); + return true; + } + + List oldSteps = oldOrder.getRoute().getSteps(); + List newSteps = newOrder.getRoute().getSteps(); + + // Compare already processed steps (up to and including the last executed command) for equality. + List oldProcessedSteps = oldSteps.subList(0, getLastCommandExecutedRouteIndex() + 1); + List newProcessedSteps = newSteps.subList(0, getLastCommandExecutedRouteIndex() + 1); + LOG.debug( + "{}: Comparing already processed steps for equality: {} (old) and {} (new)", + vehicle.getName(), + oldProcessedSteps, + newProcessedSteps + ); + if (!equalsInMovement(oldProcessedSteps, newProcessedSteps)) { + LOG.debug( + "{}: Drive orders not continuous: Mismatching old and new processed steps.", + vehicle.getName() + ); + return false; + } + + if (isForcedRerouting(newOrder)) { + LOG.debug("{}: Drive orders continuous: New order with forced rerouting.", vehicle.getName()); + return true; + } + + // Compare pending steps (after the last executed command) for equality. + int futureOrCurrentPositionIndex = getFutureOrCurrentPositionIndex(); + List oldPendingSteps = oldSteps.subList( + getLastCommandExecutedRouteIndex() + 1, + futureOrCurrentPositionIndex + 1 + ); + List newPendingSteps = newSteps.subList( + getLastCommandExecutedRouteIndex() + 1, + futureOrCurrentPositionIndex + 1 + ); + LOG.debug( + "{}: Comparing pending steps for equality: {} (old) and {} (new)", + vehicle.getName(), + oldPendingSteps, + newPendingSteps + ); + if (!equalsInMovement(oldPendingSteps, newPendingSteps)) { + LOG.debug( + "{}: Drive orders not continuous: Mismatching old and new pending steps.", + vehicle.getName() + ); + return false; + } + + LOG.debug("{}: Drive orders continuous.", vehicle.getName()); + return true; + } + + private int getFutureOrCurrentPositionIndex() { + if (commandProcessingTracker.getSentCommands().isEmpty() + && getInteractionsPendingCommand().isEmpty()) { + LOG.debug( + "{}: No commands expected to be executed. Last executed command route index: {}", + vehicle.getName(), + getLastCommandExecutedRouteIndex() + ); + return getLastCommandExecutedRouteIndex(); + } + + if (getInteractionsPendingCommand().isPresent()) { + LOG.debug( + "{}: Command with pending peripheral operations present. Route index: {}", + vehicle.getName(), + getInteractionsPendingCommand().orElseThrow().getStep().getRouteIndex() + ); + return getInteractionsPendingCommand().orElseThrow().getStep().getRouteIndex(); + } + + MovementCommand lastCommandSent = commandProcessingTracker.getSentCommands().getLast(); + LOG.debug( + "{}: Using the last command sent to the communication adapter. Route index: {}", + vehicle.getName(), + lastCommandSent.getStep().getRouteIndex() + ); + return lastCommandSent.getStep().getRouteIndex(); + } + + private void discardFutureCommands() { + withdrawPendingResourceAllocations(); + } + + @Override + public void abortTransportOrder(boolean immediate) { + synchronized (commAdapter) { + if (immediate) { + clearDriveOrder(); + + withdrawPendingResourceAllocations(); + + scheduler.claim(this, List.of()); + } + else { + abortDriveOrder(); + + withdrawPendingResourceAllocations(); + + commandProcessingTracker.driveOrderAborted(false); + + scheduler.claim(this, List.of()); + + checkForPendingCommands(); + } + + vehicleService.updateVehicleClaimedResources(vehicle.getReference(), List.of()); + vehicleService.updateVehicleAllocatedResources( + vehicle.getReference(), + toListOfResourceSets(commandProcessingTracker.getAllocatedResources()) + ); + } + } + + private void clearDriveOrder() { + synchronized (commAdapter) { + currentDriveOrder = null; + + clearCommandQueue(); + } + } + + private void abortDriveOrder() { + synchronized (commAdapter) { + if (currentDriveOrder == null) { + LOG.debug("{}: No drive order to be aborted", vehicle.getName()); + return; + } + } + } + + private void clearCommandQueue() { + synchronized (commAdapter) { + commAdapter.clearCommandQueue(); + + Collection>> resourceToBeFreed + = commandProcessingTracker.getAllocatedResourcesAhead(); + commandProcessingTracker.driveOrderAborted(true); + + peripheralInteractor.clear(); + + for (Set> resSet : resourceToBeFreed) { + scheduler.free(this, resSet); + } + } + } + + @Override + @Nonnull + public ExplainedBoolean canProcess(TransportOrder order) { + requireNonNull(order, "order"); + + synchronized (commAdapter) { + return commAdapter.canProcess(order); + } + } + + @Override + public void onVehiclePaused(boolean paused) { + synchronized (commAdapter) { + commAdapter.onVehiclePaused(paused); + } + } + + @Override + public void sendCommAdapterMessage( + @Nullable + Object message + ) { + synchronized (commAdapter) { + commAdapter.processMessage(message); + } + } + + @Override + public void sendCommAdapterCommand(AdapterCommand command) { + synchronized (commAdapter) { + commAdapter.execute(command); + } + } + + @Override + public Queue getCommandsSent() { + return commandProcessingTracker.getSentCommands(); + } + + @Override + public Optional getInteractionsPendingCommand() { + return commandProcessingTracker.getSendingPendingCommand(); + } + + @Override + public boolean mayAllocateNow(Set> resources) { + return scheduler.mayAllocateNow(this, resources); + } + + @Override + @Nonnull + public String getId() { + return vehicle.getName(); + } + + @Override + public TCSObjectReference getRelatedVehicle() { + return vehicle.getReference(); + } + + @Override + public boolean allocationSuccessful( + @Nonnull + Set> resources + ) { + requireNonNull(resources, "resources"); + + synchronized (commAdapter) { + // Check if we've actually been waiting for these resources now. If not, + // let the scheduler know that we don't want them. + if (!Objects.equals( + resources, + commandProcessingTracker.getAllocationPendingResources().orElse(null) + )) { + LOG.warn( + "{}: Allocated resources ({}) != pending resources ({}), refusing them", + vehicle.getName(), + resources, + commandProcessingTracker.getAllocationPendingResources() + ); + return false; + } + + LOG.debug("{}: Accepting allocated resources: {}", vehicle.getName(), resources); + + commandProcessingTracker.allocationConfirmed(resources); + + MovementCommand command = commandProcessingTracker.getSendingPendingCommand().orElseThrow(); + + vehicleService.updateVehicleClaimedResources( + vehicle.getReference(), + toListOfResourceSets(currentClaim(transportOrder)) + ); + vehicleService.updateVehicleAllocatedResources( + vehicle.getReference(), + toListOfResourceSets(commandProcessingTracker.getAllocatedResources()) + ); + + peripheralInteractor.prepareInteractions(transportOrder.getReference(), command); + peripheralInteractor.startPreMovementInteractions( + command, + () -> sendCommandOrStopSending(command), + this::onPreMovementInteractionFailed + ); + } + // Let the scheduler know we've accepted the resources given. + return true; + } + + @Override + public void allocationFailed( + @Nonnull + Set> resources + ) { + requireNonNull(resources, "resources"); + throw new IllegalStateException("Failed to allocate: " + resources); + } + + @Override + public String toString() { + return "DefaultVehicleController{" + "vehicleName=" + vehicle.getName() + '}'; + } + + private void sendCommandOrStopSending(MovementCommand command) { + if (sendingCommandsAllowed) { + sendCommand(command); + } + else { + LOG.debug( + "{}: Sending commands not allowed. Discarding movement command: {}", + vehicle.getName(), + command + ); + commandProcessingTracker.commandSendingStopped(command); + } + } + + private void sendCommand(MovementCommand command) + throws IllegalStateException { + LOG.debug("{}: Enqueuing movement command with comm adapter: {}", vehicle.getName(), command); + + MovementCommand transformedCommand = movementCommandTransformer.apply(command); + // Send the command to the communication adapter. + checkState( + commAdapter.enqueueCommand(transformedCommand), + "Comm adapter did not accept command" + ); + transformedToOriginalCommands.put(transformedCommand, command); + commandProcessingTracker.commandSent(command); + + // Check if the communication adapter has capacity for another command. + if (canSendNextCommand()) { + allocateForNextCommand(); + } + } + + private void onPreMovementInteractionFailed() { + // Implementation remark: This method is called only for interactions where a peripheral job + // with the completion required flag set has failed. + LOG.warn("{}: Pre-movement interaction failed.", vehicle.getName()); + + // With a failed pre-movement interaction, the movement command for the latest allocated + // resources will not be sent to the vehicle. Therefore, free these resources. + Set> res = commandProcessingTracker.getAllocatedResources().peekLast(); + scheduler.free(this, res); + commandProcessingTracker.allocationRevoked(res); + vehicleService.updateVehicleAllocatedResources( + vehicle.getReference(), + toListOfResourceSets(commandProcessingTracker.getAllocatedResources()) + ); + + dispatcherService.withdrawByVehicle(vehicle.getReference(), false); + } + + private void onPostMovementInteractionFailed() { + // Implementation remark: This method is called only for interactions where a peripheral job + // with the completion required flag set has failed. + LOG.warn("{}: Post-movement interaction failed.", vehicle.getName()); + + dispatcherService.withdrawByVehicle(vehicle.getReference(), false); + } + + @SuppressWarnings("unchecked") + private void handleProcessModelEvent(PropertyChangeEvent evt) { + eventBus.onEvent( + new ProcessModelEvent( + evt.getPropertyName(), + commAdapter.createTransferableProcessModel() + ) + ); + + if (Objects.equals(evt.getPropertyName(), VehicleProcessModel.Attribute.POSITION.name())) { + updateVehiclePosition((String) evt.getNewValue()); + } + else if (Objects.equals( + evt.getPropertyName(), + VehicleProcessModel.Attribute.POSE.name() + )) { + if (vehicleService.fetchObject(Vehicle.class, vehicle.getReference()).getIntegrationLevel() + != Vehicle.IntegrationLevel.TO_BE_IGNORED) { + updateVehiclePose((Pose) evt.getNewValue()); + } + } + else if (Objects.equals( + evt.getPropertyName(), + VehicleProcessModel.Attribute.ENERGY_LEVEL.name() + )) { + vehicleService.updateVehicleEnergyLevel(vehicle.getReference(), (Integer) evt.getNewValue()); + } + else if (Objects.equals( + evt.getPropertyName(), + VehicleProcessModel.Attribute.LOAD_HANDLING_DEVICES.name() + )) { + vehicleService.updateVehicleLoadHandlingDevices( + vehicle.getReference(), + (List) evt.getNewValue() + ); + } + else if (Objects.equals(evt.getPropertyName(), VehicleProcessModel.Attribute.STATE.name())) { + updateVehicleState((Vehicle.State) evt.getNewValue()); + } + else if (Objects.equals( + evt.getPropertyName(), + VehicleProcessModel.Attribute.BOUNDING_BOX.name() + )) { + updateVehicleBoundingBox((BoundingBox) evt.getNewValue()); + } + else if (Objects.equals( + evt.getPropertyName(), + VehicleProcessModel.Attribute.COMMAND_EXECUTED.name() + )) { + commandExecuted((MovementCommand) evt.getNewValue()); + } + else if (Objects.equals( + evt.getPropertyName(), + VehicleProcessModel.Attribute.COMMAND_FAILED.name() + )) { + commandFailed((MovementCommand) evt.getNewValue()); + } + else if (Objects.equals( + evt.getPropertyName(), + VehicleProcessModel.Attribute.USER_NOTIFICATION.name() + )) { + notificationService.publishUserNotification((UserNotification) evt.getNewValue()); + } + else if (Objects.equals( + evt.getPropertyName(), + VehicleProcessModel.Attribute.COMM_ADAPTER_EVENT.name() + )) { + eventBus.onEvent(evt.getNewValue()); + } + else if (Objects.equals( + evt.getPropertyName(), + VehicleProcessModel.Attribute.VEHICLE_PROPERTY.name() + )) { + VehicleProcessModel.VehiclePropertyUpdate propUpdate + = (VehicleProcessModel.VehiclePropertyUpdate) evt.getNewValue(); + vehicleService.updateObjectProperty( + vehicle.getReference(), + propUpdate.getKey(), + propUpdate.getValue() + ); + } + else if (Objects.equals( + evt.getPropertyName(), + VehicleProcessModel.Attribute.TRANSPORT_ORDER_PROPERTY.name() + )) { + VehicleProcessModel.TransportOrderPropertyUpdate propUpdate + = (VehicleProcessModel.TransportOrderPropertyUpdate) evt.getNewValue(); + if (currentDriveOrder != null) { + vehicleService.updateObjectProperty( + currentDriveOrder.getTransportOrder(), + propUpdate.getKey(), + propUpdate.getValue() + ); + } + } + else if (Objects.equals( + evt.getPropertyName(), + VehicleProcessModel.Attribute.INTEGRATION_LEVEL_CHANGE_REQUESTED.name() + )) { + vehicleService.updateVehicleIntegrationLevel( + vehicle.getReference(), + (Vehicle.IntegrationLevel) evt.getNewValue() + ); + } + else if (Objects.equals( + evt.getPropertyName(), + VehicleProcessModel.Attribute.TRANSPORT_ORDER_WITHDRAWAL_REQUESTED.name() + )) { + dispatcherService.withdrawByVehicle(vehicle.getReference(), (Boolean) evt.getNewValue()); + } + } + + private void withdrawPendingResourceAllocations() { + scheduler.clearPendingAllocations(this); + } + + private void updateVehiclePose( + @Nonnull + Pose pose + ) + throws ObjectUnknownException { + requireNonNull(pose, "pose"); + vehicleService.updateVehiclePose(vehicle.getReference(), incomingPoseTransformer.apply(pose)); + } + + private void updateVehiclePosition(String position) { + // Get an up-to-date copy of the vehicle + Vehicle currVehicle = vehicleService.fetchObject(Vehicle.class, vehicle.getReference()); + + if (currVehicle.getIntegrationLevel() == Vehicle.IntegrationLevel.TO_BE_RESPECTED + || currVehicle.getIntegrationLevel() == Vehicle.IntegrationLevel.TO_BE_UTILIZED + || currVehicle.getIntegrationLevel() == Vehicle.IntegrationLevel.TO_BE_NOTICED) { + setVehiclePosition(position); + } + } + + private void setVehiclePosition(String position) { + // Place the vehicle on the given position, regardless of what the kernel + // might expect. The vehicle is physically there, even if it shouldn't be. + // The same is true for null values - if the vehicle says it's not on any + // known position, it has to be treated as a fact. + Point point; + if (position == null) { + point = null; + } + else { + point = vehicleService.fetchObject(Point.class, position); + // If the new position is not in the model, ignore it. (Some vehicles/drivers send + // intermediate positions that cannot be order destinations and thus do not exist in + // the model. + if (point == null) { + LOG.warn("{}: At unknown position {}", vehicle.getName(), position); + return; + } + } + synchronized (commAdapter) { + // If the current drive order is null, just set the vehicle's position. + if (currentDriveOrder == null) { + LOG.debug( + "{}: Reported new position {} and we do not have a drive order.", + vehicle.getName(), + point + ); + updatePositionWithoutOrder(point); + } + else { + updatePositionWithOrder(point); + } + } + } + + private void commandExecuted(MovementCommand executedCommand) { + requireNonNull(executedCommand, "executedCommand"); + + synchronized (commAdapter) { + checkArgument( + transformedToOriginalCommands.containsKey(executedCommand), + "Unknown command reported as executed: %s", + executedCommand + ); + MovementCommand originalCommand = transformedToOriginalCommands.remove(executedCommand); + + LOG.debug( + "{}: Communication adapter reports movement command as executed: {}", + vehicle.getName(), + originalCommand + ); + + commandProcessingTracker.commandExecuted(originalCommand); + + Point currentVehiclePosition = originalCommand.getStep().getDestinationPoint(); + Deque>> allocatedResources + = commandProcessingTracker.getAllocatedResources(); + switch (configuration.vehicleResourceManagementType()) { + case LENGTH_IGNORED: + while (!allocatedResources.peek().contains(currentVehiclePosition)) { + Set> oldResources = allocatedResources.poll(); + LOG.debug("{}: Freeing resources: {}", vehicle.getName(), oldResources); + scheduler.free(this, oldResources); + commandProcessingTracker.allocationReleased(oldResources); + } + break; + case LENGTH_RESPECTED: + // Free resources allocated for executed commands, but keep as many as needed for the + // vehicle's current length. + int freeableResourceSetCount + = ResourceMath.freeableResourceSetCount( + SplitResources.from(allocatedResources, Set.of(currentVehiclePosition)) + .getResourcesPassed(), + commAdapter.getProcessModel().getBoundingBox().getLength() + ); + for (int i = 0; i < freeableResourceSetCount; i++) { + Set> oldResources = allocatedResources.poll(); + LOG.debug("{}: Freeing resources: {}", vehicle.getName(), oldResources); + scheduler.free(this, oldResources); + commandProcessingTracker.allocationReleased(oldResources); + } + break; + default: + throw new IllegalArgumentException( + "Unhandled resource deallocation method: " + + configuration.vehicleResourceManagementType().name() + ); + } + + vehicleService.updateVehicleAllocatedResources( + vehicle.getReference(), + toListOfResourceSets(commandProcessingTracker.getAllocatedResources()) + ); + + transportOrderService.updateTransportOrderCurrentRouteStepIndex( + transportOrder.getReference(), + originalCommand.getStep().getRouteIndex() + ); + + peripheralInteractor.startPostMovementInteractions( + originalCommand, + this::checkForPendingCommands, + this::onPostMovementInteractionFailed + ); + } + } + + private void commandFailed(MovementCommand failedCommand) { + LOG.debug( + "{}: Communication adapter reports movement command as failed: {}", + vehicle.getName(), + failedCommand + ); + dispatcherService.withdrawByVehicle(vehicle.getReference(), true); + } + + private void checkForPendingCommands() { + // Check if there are more commands to be processed for the current drive order. + if (!commandProcessingTracker.hasCommandsToBeSent()) { + LOG.debug("{}: No more commands in current drive order", vehicle.getName()); + // Check if there are still commands that have been sent to the communication adapter but + // not yet executed. If not, the whole order has been executed completely - let the kernel + // know about that so it can give us the next drive order. + if (commandProcessingTracker.isDriveOrderFinished()) { + LOG.debug("{}: Current drive order processed", vehicle.getName()); + currentDriveOrder = null; + // Let the kernel/dispatcher know that the drive order has been processed completely (by + // setting its state to AWAITING_ORDER). + vehicleService.updateVehicleProcState( + vehicle.getReference(), + Vehicle.ProcState.AWAITING_ORDER + ); + } + } + // There are more commands to be processed. + // Check if we can send another command to the comm adapter. + else if (canSendNextCommand()) { + allocateForNextCommand(); + } + } + + private void updateVehicleState(Vehicle.State newState) { + requireNonNull(newState, "newState"); + vehicleService.updateVehicleState(vehicle.getReference(), newState); + } + + private void updateVehicleBoundingBox(BoundingBox newBoundingBox) { + requireNonNull(newBoundingBox, "newBoundingBox"); + vehicleService.updateVehicleBoundingBox(vehicle.getReference(), newBoundingBox); + } + + /** + * Checks if we can send another command to the communication adapter without + * overflowing its capacity and with respect to the number of commands still + * in our queue and allocation requests to the scheduler in progress. + * + * @return true if, and only if, we can send another command. + */ + private boolean canSendNextCommand() { + if (!commAdapter.canAcceptNextCommand()) { + LOG.debug( + "{}: Cannot send, comm adapter cannot accept any further commands.", + vehicle.getName() + ); + return false; + } + if (commandProcessingTracker.isWaitingForAllocation()) { + LOG.debug( + "{}: Cannot send, resource allocation is pending for: {}", + vehicle.getName(), + commandProcessingTracker.getAllocationPendingResources().orElse(null) + ); + return false; + } + if (commandProcessingTracker.getNextAllocationCommand().isEmpty()) { + LOG.debug("{}: Cannot send, no commands to be sent.", vehicle.getName()); + return false; + } + else { + if (!commandProcessingTracker.getNextAllocationCommand().orElseThrow() + .getStep().isExecutionAllowed()) { + LOG.debug("{}: Cannot send, movement execution is not allowed", vehicle.getName()); + return false; + } + } + if (peripheralInteractor.isWaitingForMovementInteractionsToFinish()) { + LOG.debug( + "{}: Cannot send, waiting for peripheral operations to be completed: {}", + vehicle.getName(), + peripheralInteractor.pendingRequiredInteractionsByDestination() + ); + return false; + } + if (!sendingCommandsAllowed) { + LOG.debug( + "{}: Cannot send, unresolved report of an unexpected position.", + vehicle.getName() + ); + return false; + } + return true; + } + + /** + * Allocate the resources needed for executing the next command. + */ + private void allocateForNextCommand() { + checkState( + !commandProcessingTracker.isWaitingForAllocation(), + "%s: Already waiting for allocation: %s", + vehicle.getName(), + commandProcessingTracker.getAllocationPendingResources().orElse(null) + ); + + // Find out which resources are actually needed for the next command. + Set> nextAllocation + = commandProcessingTracker.getNextAllocationResources().orElseThrow(); + LOG.debug("{}: Requesting allocation of resources: {}", vehicle.getName(), nextAllocation); + scheduler.allocate(this, nextAllocation); + commandProcessingTracker.allocationRequested(nextAllocation); + } + + /** + * Returns a set of resources needed for executing the given command. + * + * @param cmd The command for which to return the needed resources. + * @return A set of resources needed for executing the given command. + */ + @Nonnull + private Set> getNeededResources(MovementCommand cmd) { + requireNonNull(cmd, "cmd"); + + Set> result = new HashSet<>(); + result.add(cmd.getStep().getDestinationPoint()); + if (cmd.getStep().getPath() != null) { + result.add(cmd.getStep().getPath()); + } + if (cmd.getOpLocation() != null) { + result.add(cmd.getOpLocation()); + } + + return result; + } + + /** + * Frees all resources allocated for the vehicle. + */ + private void freeAllResources() { + scheduler.freeAll(this); + commandProcessingTracker.allocationReset(Set.of()); + vehicleService.updateVehicleAllocatedResources(vehicle.getReference(), List.of()); + } + + /** + * Returns the next command expected to be executed by the vehicle, skipping the current one. + * + * @return The next command expected to be executed by the vehicle. + */ + private MovementCommand findNextCommand() { + return commandProcessingTracker.getSentCommands().stream() + .skip(1) + .findFirst() + .or(commandProcessingTracker::getSendingPendingCommand) + .or(commandProcessingTracker::getAllocationPendingCommand) + .or(commandProcessingTracker::getNextAllocationCommand) + .orElse(null); + } + + private void updatePositionWithoutOrder(Point point) + throws IllegalArgumentException { + if (point == null) { + freeAllResources(); + } + else { + Set> requiredResource = Set.of(point); + + // Before giving up the resources allocated, ensure that we will be able to allocate the + // newly required resources. + checkArgument( + mayAllocateNow(requiredResource), + "%s: Current position '%s' may not be allocated now - check other vehicles' allocations!", + vehicle.getName(), + point.getName() + ); + freeAllResources(); + try { + scheduler.allocateNow(this, requiredResource); + commandProcessingTracker.allocationReset(requiredResource); + } + catch (ResourceAllocationException exc) { + // May never happen. After a successful call to `mayAllocateNow` the allocation should + // always succeed. + LOG.error( + "{}: Could not allocate now although permission previously granted: {}", + vehicle.getName(), + point.getName(), + exc + ); + throw new IllegalArgumentException( + vehicle.getName() + + ": Could not allocate now although permission previously granted: " + + point.getName() + ); + } + vehicleService.updateVehicleAllocatedResources( + vehicle.getReference(), + toListOfResourceSets(commandProcessingTracker.getAllocatedResources()) + ); + } + + updatePosition(toReference(point), null); + } + + private void updatePositionWithOrder(Point point) { + if (commandProcessingTracker.getSentCommands().isEmpty()) { + if (commandProcessingTracker.getAllocationPendingCommand().isPresent()) { + LOG.warn( + "{}: Reported new position {} but we are waiting for resource allocation for: {}", + vehicle.getName(), + point, + commandProcessingTracker.getAllocationPendingCommand().orElse(null) + ); + } + else if (commandProcessingTracker.getSendingPendingCommand().isPresent()) { + LOG.warn( + "{}: Reported new position {} but we are waiting for command to be sent: {}", + vehicle.getName(), + point, + commandProcessingTracker.getSendingPendingCommand().orElse(null) + ); + } + else { + LOG.warn( + "{}: Reported new position {} but we didn't send any commands of the drive order.", + vehicle.getName(), + point + ); + } + + onUnexpectedPositionReported(point); + + // We have a drive order, but can't remember sending a command to the vehicle. Just set the + // position without touching the resources, as that might cause even more damage when we + // actually send commands to the vehicle. + updatePosition(toReference(point), null); + } + else { + if (point == null) { + LOG.info("{}: Resetting position for vehicle", vehicle.getName()); + } + else { + // Check if the reported position belongs to any of the commands we sent. + List expectedPoints = commandProcessingTracker.getSentCommands().stream() + .map(cmd -> cmd.getStep().getDestinationPoint()) + .collect(Collectors.toList()); + + if (!expectedPoints.contains(point)) { + LOG.warn( + "{}: Reported position: {}, expected one of: {}", + vehicle.getName(), + point.getName(), + expectedPoints + ); + onUnexpectedPositionReported(point); + } + } + + updatePosition(toReference(point), extractNextPosition(findNextCommand())); + } + } + + private void updatePosition( + TCSObjectReference posRef, + TCSObjectReference nextPosRef + ) { + vehicleService.updateVehiclePosition(vehicle.getReference(), posRef); + vehicleService.updateVehicleNextPosition(vehicle.getReference(), nextPosRef); + } + + private void onIntegrationLevelChange( + Vehicle prevVehicleState, + Vehicle currVehicleState + ) { + Vehicle.IntegrationLevel prevIntegrationLevel = prevVehicleState.getIntegrationLevel(); + Vehicle.IntegrationLevel currIntegrationLevel = currVehicleState.getIntegrationLevel(); + + synchronized (commAdapter) { + if (currIntegrationLevel == Vehicle.IntegrationLevel.TO_BE_IGNORED) { + // Reset the vehicle's position to free all allocated resources + resetVehiclePosition(); + updateVehiclePose(new Pose(null, Double.NaN)); + } + else if (currIntegrationLevel == Vehicle.IntegrationLevel.TO_BE_NOTICED) { + // Reset the vehicle's position to free all allocated resources + resetVehiclePosition(); + + // Update the vehicle's position in its model, but don't allocate any resources + VehicleProcessModel processModel = commAdapter.getProcessModel(); + if (processModel.getPosition() != null) { + Point point = vehicleService.fetchObject(Point.class, processModel.getPosition()); + vehicleService.updateVehiclePosition(vehicle.getReference(), point.getReference()); + } + updateVehiclePose(processModel.getPose()); + } + else if ((currIntegrationLevel == Vehicle.IntegrationLevel.TO_BE_RESPECTED + || currIntegrationLevel == Vehicle.IntegrationLevel.TO_BE_UTILIZED) + && (prevIntegrationLevel == Vehicle.IntegrationLevel.TO_BE_IGNORED + || prevIntegrationLevel == Vehicle.IntegrationLevel.TO_BE_NOTICED)) { + // Allocate the vehicle's current position and implicitly update its model's + // position + allocateVehiclePosition(); + } + } + } + + private void resetVehiclePosition() { + synchronized (commAdapter) { + checkState(currentDriveOrder == null, "%s: Vehicle has a drive order", vehicle.getName()); + checkState( + !commandProcessingTracker.isWaitingForAllocation(), + "%s: Vehicle is waiting for resource allocation (%s)", + vehicle.getName(), + commandProcessingTracker.getAllocationPendingResources() + ); + + setVehiclePosition(null); + } + } + + private void allocateVehiclePosition() { + VehicleProcessModel processModel = commAdapter.getProcessModel(); + // We don't want to set the vehicle position right away, since the vehicle's currently + // allocated resources would be freed in the first place. We need to check, if the vehicle's + // current position is already part of it's allocated resources. + if (!alreadyAllocated(processModel.getPosition())) { + // Set vehicle's position to allocate the resources + setVehiclePosition(processModel.getPosition()); + updateVehiclePose(processModel.getPose()); + } + } + + private boolean alreadyAllocated(String position) { + return commandProcessingTracker.getAllocatedResources().stream() + .filter(resources -> resources != null) + .flatMap(resources -> resources.stream()) + .anyMatch(resource -> resource.getName().equals(position)); + } + + private static TCSObjectReference toReference(Point point) { + return point == null ? null : point.getReference(); + } + + private static TCSObjectReference extractNextPosition(MovementCommand nextCommand) { + if (nextCommand == null) { + return null; + } + else { + return nextCommand.getStep().getDestinationPoint().getReference(); + } + } + + private static List>> toListOfResourceSets( + Queue>> resources + ) { + List>> result = new ArrayList<>(resources.size()); + + for (Set> resourceSet : resources) { + result.add( + resourceSet.stream() + .map(resource -> resource.getReference()) + .collect(Collectors.toSet()) + ); + } + + return result; + } + + private static List>> toListOfResourceSets( + List>> resources + ) { + List>> result = new ArrayList<>(resources.size()); + + for (Set> resourceSet : resources) { + result.add( + resourceSet.stream() + .map(TCSResource::getReference) + .collect(Collectors.toSet()) + ); + } + + return result; + } + + private List>> currentClaim(TransportOrder order) { + List>> claim = new ArrayList<>(); + claim.addAll(commandProcessingTracker.getClaimedResources()); + claim.addAll(requiredClaimForFutureDriveOrders(transportOrder)); + return claim; + } + + private List>> requiredClaimForFutureDriveOrders(TransportOrder order) { + return order.getFutureDriveOrders().stream() + .map(driveOrder -> movementCommandMapper.toMovementCommands(driveOrder, order)) + .flatMap(Collection::stream) + .map(this::getNeededResources) + .toList(); + } + + private boolean isForcedRerouting(DriveOrder newOrder) { + // If it's a forced rerouting, the step after the one the vehicle executed last should be marked + // accordingly. + Step nextPendingStep + = newOrder.getRoute().getSteps().get(getLastCommandExecutedRouteIndex() + 1); + if (nextPendingStep.getReroutingType() == ReroutingType.FORCED) { + return true; + } + + return false; + } + + private int getLastCommandExecutedRouteIndex() { + if (commandProcessingTracker.getLastCommandExecuted().isEmpty()) { + return TransportOrder.ROUTE_STEP_INDEX_DEFAULT; + } + + if (!Objects.equals( + currentDriveOrder, + commandProcessingTracker.getLastCommandExecuted().orElseThrow().getDriveOrder() + )) { + return TransportOrder.ROUTE_STEP_INDEX_DEFAULT; + } + + return commandProcessingTracker.getLastCommandExecuted().orElseThrow() + .getStep().getRouteIndex(); + } + + private void onUnexpectedPositionReported( + @Nullable + Point point + ) { + sendingCommandsAllowed = false; + + notificationService.publishUserNotification( + new UserNotification( + vehicle.getName(), + String.format( + "Vehicle reported an unexpected position ('%s') while processing a transport order." + + " Its vehicle driver won't receive further movement commands until the" + + " vehicle is forcefully rerouted.", + point == null ? "null" : point.getName() + ), + UserNotification.Level.IMPORTANT + ) + ); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/DefaultVehicleControllerPool.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/DefaultVehicleControllerPool.java new file mode 100644 index 0000000..caabf24 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/DefaultVehicleControllerPool.java @@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.HashMap; +import java.util.Map; +import org.opentcs.components.kernel.services.InternalVehicleService; +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; +import org.opentcs.drivers.vehicle.VehicleController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Maintains associations of {@link Vehicle}, {@link VehicleController} and + * {@link VehicleCommAdapter}. + */ +public final class DefaultVehicleControllerPool + implements + LocalVehicleControllerPool { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(DefaultVehicleControllerPool.class); + /** + * The vehicle service. + */ + private final InternalVehicleService vehicleService; + /** + * A factory for vehicle managers. + */ + private final VehicleControllerFactory vehicleManagerFactory; + /** + * The currently existing/assigned managers, mapped by the names of the + * corresponding vehicles. + */ + private final Map poolEntries = new HashMap<>(); + /** + * Indicates whether this components is initialized. + */ + private boolean initialized; + + /** + * Creates a new StandardVehicleManagerPool. + * + * @param vehicleService The vehicle service. + * @param vehicleManagerFactory A factory for vehicle managers. + */ + @Inject + public DefaultVehicleControllerPool( + InternalVehicleService vehicleService, + VehicleControllerFactory vehicleManagerFactory + ) { + this.vehicleService = requireNonNull(vehicleService, "vehicleService"); + this.vehicleManagerFactory = requireNonNull(vehicleManagerFactory, "vehicleManagerFactory"); + } + + @Override + public void initialize() { + if (initialized) { + LOG.debug("Already initialized, doing nothing."); + return; + } + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!initialized) { + LOG.debug("Not initialized, doing nothing."); + return; + } + // Detach all vehicles and reset their positions. + for (PoolEntry curEntry : poolEntries.values()) { + curEntry.vehicleController.terminate(); + Vehicle vehicle = vehicleService.fetchObject(Vehicle.class, curEntry.vehicleName); + vehicleService.updateVehiclePosition(vehicle.getReference(), null); + } + poolEntries.clear(); + initialized = false; + } + + @Override + public synchronized void attachVehicleController( + String vehicleName, + VehicleCommAdapter commAdapter + ) { + requireNonNull(vehicleName, "vehicleName"); + requireNonNull(commAdapter, "commAdapter"); + + if (poolEntries.containsKey(vehicleName)) { + LOG.warn("manager already attached, doing nothing"); + return; + } + + Vehicle vehicle = vehicleService.fetchObject(Vehicle.class, vehicleName); + checkArgument(vehicle != null, "No such vehicle: %s", vehicleName); + + VehicleController controller = vehicleManagerFactory.createVehicleController( + vehicle, + commAdapter + ); + PoolEntry poolEntry = new PoolEntry(vehicleName, controller); + poolEntries.put(vehicleName, poolEntry); + controller.initialize(); + } + + @Override + public synchronized void detachVehicleController(String vehicleName) { + requireNonNull(vehicleName, "vehicleName"); + + LOG.debug("Detaching controller for vehicle {}...", vehicleName); + PoolEntry poolEntry = poolEntries.remove(vehicleName); + if (poolEntry == null) { + LOG.debug("A vehicle named '{}' is not attached to a controller.", vehicleName); + return; + } + // Clean up - mark vehicle state and adapter state as unknown. + poolEntry.vehicleController.terminate(); + } + + @Override + public VehicleController getVehicleController(String vehicleName) { + requireNonNull(vehicleName, "vehicleName"); + + PoolEntry poolEntry = poolEntries.get(vehicleName); + return poolEntry == null ? new NullVehicleController(vehicleName) : poolEntry.vehicleController; + } + + /** + * An entry in this vehicle manager pool. + */ + private static final class PoolEntry { + + /** + * The name of the vehicle managed. + */ + private final String vehicleName; + /** + * The vehicle controller associated with the vehicle. + */ + private final VehicleController vehicleController; + + /** + * Creates a new pool entry. + * + * @param name The name of the vehicle managed. + * @param manager The vehicle manager associated with the vehicle. + * @param controller The VehicleController + */ + private PoolEntry(String name, VehicleController controller) { + vehicleName = requireNonNull(name, "name"); + vehicleController = requireNonNull(controller, "controller"); + } + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/LocalVehicleControllerPool.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/LocalVehicleControllerPool.java new file mode 100644 index 0000000..38a12f7 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/LocalVehicleControllerPool.java @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import org.opentcs.components.Lifecycle; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; +import org.opentcs.drivers.vehicle.VehicleControllerPool; + +/** + * Manages the attachment of vehicle controllers to vehicles and comm adapters. + */ +public interface LocalVehicleControllerPool + extends + VehicleControllerPool, + Lifecycle { + + /** + * Associates a vehicle controller with a named vehicle and a comm adapter. + * + * @param vehicleName The name of the vehicle. + * @param commAdapter The communication adapter that is going to control the physical vehicle. + */ + void attachVehicleController(String vehicleName, VehicleCommAdapter commAdapter) + throws IllegalArgumentException; + + /** + * Disassociates a vehicle control and a comm adapter from a vehicle. + * + * @param vehicleName The name of the vehicle from which to detach. + */ + void detachVehicleController(String vehicleName); +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/MovementCommandMapper.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/MovementCommandMapper.java new file mode 100644 index 0000000..c735454 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/MovementCommandMapper.java @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Point; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.drivers.vehicle.MovementCommand; + +/** + * Provides methods for mapping {@link DriveOrder}s to a list of {@link MovementCommand}s. + */ +public class MovementCommandMapper { + + private final TCSObjectService objectService; + + /** + * Creates a new instance. + * + * @param objectService The object service to use. + */ + @Inject + public MovementCommandMapper(TCSObjectService objectService) { + this.objectService = requireNonNull(objectService, "objectService"); + } + + /** + * Maps the given {@link DriveOrder} to a corresponding list of {@link MovementCommand}s. + * + * @param driveOrder The {@link DriveOrder} to map. + * @param transportOrder The {@link TransportOrder} the drive order belongs to. + * @return A list of {@link MovementCommand}s. + */ + public List toMovementCommands( + DriveOrder driveOrder, + TransportOrder transportOrder + ) { + requireNonNull(driveOrder, "driveOrder"); + requireNonNull(transportOrder, "transportOrder"); + + // Gather information from the drive order. + String op = driveOrder.getDestination().getOperation(); + Route orderRoute = driveOrder.getRoute(); + Point finalDestination = orderRoute.getFinalDestinationPoint(); + Location finalDestinationLocation + = objectService.fetchObject( + Location.class, + driveOrder.getDestination().getDestination().getName() + ); + Map destProperties = driveOrder.getDestination().getProperties(); + + List result = new ArrayList<>(orderRoute.getSteps().size()); + // Create a movement command for every step in the drive order's route. + Iterator stepIter = orderRoute.getSteps().iterator(); + while (stepIter.hasNext()) { + Route.Step curStep = stepIter.next(); + boolean isFinalMovement = !stepIter.hasNext(); + + String operation = isFinalMovement ? op : MovementCommand.NO_OPERATION; + Location location = isFinalMovement ? finalDestinationLocation : null; + + result.add( + new MovementCommand( + transportOrder, + driveOrder, + curStep, + operation, + location, + isFinalMovement, + finalDestinationLocation, + finalDestination, + op, + mergeProperties(transportOrder.getProperties(), destProperties) + ) + ); + } + + return result; + } + + /** + * Merges the properties of a transport order and those of a drive order. + * + * @param orderProps The properties of a transport order. + * @param destProps The properties of a drive order destination. + * @return The merged properties. + */ + private Map mergeProperties( + Map orderProps, + Map destProps + ) { + requireNonNull(orderProps, "orderProps"); + requireNonNull(destProps, "destProps"); + + Map result = new HashMap<>(); + result.putAll(orderProps); + result.putAll(destProps); + return result; + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/MovementComparisons.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/MovementComparisons.java new file mode 100644 index 0000000..70ce38d --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/MovementComparisons.java @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import static java.util.Objects.requireNonNull; + +import java.util.Iterator; +import java.util.List; +import org.opentcs.data.order.Route.Step; +import org.opentcs.drivers.vehicle.MovementCommand; + +/** + * Provides methods for comparing movements represented by {@link MovementCommand}s and + * {@link Step}s. + */ +public class MovementComparisons { + + private MovementComparisons() { + } + + /** + * Compares the two given lists of steps, ignoring rerouting-related properties. + * + * @param stepsA The first list of steps. + * @param stepsB The second list of steps. + * @return {@code true}, if the given lists of steps are equal (ignoring rerouting-related + * properties), otherwise {@code false}. + */ + public static boolean equalsInMovement(List stepsA, List stepsB) { + requireNonNull(stepsA, "stepsA"); + requireNonNull(stepsB, "stepsB"); + + if (stepsA.size() != stepsB.size()) { + return false; + } + + Iterator itStepsA = stepsA.iterator(); + Iterator itStepsB = stepsB.iterator(); + + while (itStepsA.hasNext()) { + if (!itStepsA.next().equalsInMovement(itStepsB.next())) { + return false; + } + } + + return true; + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/NullVehicleController.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/NullVehicleController.java new file mode 100644 index 0000000..d987f88 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/NullVehicleController.java @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayDeque; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import org.opentcs.data.model.TCSResource; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.drivers.vehicle.AdapterCommand; +import org.opentcs.drivers.vehicle.MovementCommand; +import org.opentcs.drivers.vehicle.VehicleController; +import org.opentcs.util.ExplainedBoolean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Null-object implementation of {@link VehicleController}. + */ +public class NullVehicleController + implements + VehicleController { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(NullVehicleController.class); + /** + * The associated vehicle's name. + */ + private final String vehicleName; + + /** + * Creates a new instance. + * + * @param vehicleName The associated vehicle's name. + */ + public NullVehicleController( + @Nonnull + String vehicleName + ) { + this.vehicleName = requireNonNull(vehicleName, "vehicleName"); + } + + @Override + public void initialize() { + } + + @Override + public boolean isInitialized() { + return true; + } + + @Override + public void terminate() { + } + + @Override + public void setTransportOrder(TransportOrder newOrder) { + LOG.warn("No comm adapter attached to vehicle {}", vehicleName); + } + + @Override + public void abortTransportOrder(boolean immediate) { + LOG.warn("No comm adapter attached to vehicle {}", vehicleName); + } + + @Override + public ExplainedBoolean canProcess(TransportOrder order) { + return new ExplainedBoolean(false, "NullVehicleController"); + } + + @Override + public void sendCommAdapterMessage(Object message) { + LOG.warn("No comm adapter attached to vehicle {}", vehicleName); + } + + @Override + public void sendCommAdapterCommand(AdapterCommand command) { + LOG.warn("No comm adapter attached to vehicle {}", vehicleName); + } + + @Override + public Queue getCommandsSent() { + LOG.warn("No comm adapter attached to vehicle {}", vehicleName); + return new ArrayDeque<>(); + } + + @Override + public void onVehiclePaused(boolean paused) { + } + + @Override + public Optional getInteractionsPendingCommand() { + return Optional.empty(); + } + + @Override + public boolean mayAllocateNow(Set> resources) { + return false; + } + +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/PeripheralInteraction.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/PeripheralInteraction.java new file mode 100644 index 0000000..ac648a3 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/PeripheralInteraction.java @@ -0,0 +1,347 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkState; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import org.opentcs.access.to.peripherals.PeripheralJobCreationTO; +import org.opentcs.access.to.peripherals.PeripheralOperationCreationTO; +import org.opentcs.components.kernel.services.PeripheralJobService; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.data.peripherals.PeripheralOperation; +import org.opentcs.drivers.vehicle.MovementCommand; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Describes an interaction with any number of peripheral devices. + *

+ * An interaction is always associated with a {@link MovementCommand} and contains + * {@link PeripheralOperation}s for which {@link PeripheralJob}s are created once the interaction + * is started. + *

+ *

+ * In case there are no operations with the completion required flag set, the interaction is marked + * as finished immediately after it was started. + * In case there are operations with the completion required flag set, the interaction is only + * marked as finished after all corresponding jobs have been processed by the respective peripheral + * device. + *

+ *

+ * Once the interaction is finished, an interaction-specific callback is executed. + *

+ */ +public class PeripheralInteraction { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PeripheralInteraction.class); + /** + * The reference to the vehicle that's interacting with peripheral devices. + */ + private final TCSObjectReference vehicleRef; + /** + * A reference to the transport order this interaction is related to. + */ + private final TCSObjectReference orderRef; + /** + * The movement command this interaction is associated with. + */ + private final MovementCommand movementCommand; + /** + * The operations that jobs have to be created for. + */ + private final List operations; + /** + * The jobs that are required to be finished in order for this interaction itself to be marked as + * finished. + */ + private final List pendingJobsWithCompletionRequired = new ArrayList<>(); + /** + * The peripheral job service to use for creating jobs. + */ + private final PeripheralJobService peripheralJobService; + /** + * The reservation token to use for creating jobs. + */ + private final String reservationToken; + /** + * The callback that is to be executed if the interaction succeeds. + */ + private Runnable interactionSucceededCallback; + /** + * The callback that is executed if the interaction fails. + */ + private Runnable interactionFailedCallback; + /** + * The state of the interaction. + */ + private State state = State.PRISTINE; + + /** + * Creates a new instance. + * + * @param vehicleRef The reference to the vehicle that's interacting with peripheral devices. + * @param orderRef A reference to the transport order that this interaction is related to. + * @param movementCommand The movement command this interaction is associated with. + * @param operations The operations that jobs have to be created for. + * @param peripheralJobService The peripheral job service to use for creating jobs. + * @param reservationToken The reservation token to use for creating jobs. + */ + public PeripheralInteraction( + @Nonnull + TCSObjectReference vehicleRef, + @Nonnull + TCSObjectReference orderRef, + @Nonnull + MovementCommand movementCommand, + @Nonnull + List operations, + @Nonnull + PeripheralJobService peripheralJobService, + @Nonnull + String reservationToken + ) { + this.vehicleRef = requireNonNull(vehicleRef, "vehicleRef"); + this.orderRef = requireNonNull(orderRef, "orderRef"); + this.movementCommand = requireNonNull(movementCommand, "movementCommand"); + this.operations = requireNonNull(operations, "operations"); + this.peripheralJobService = requireNonNull(peripheralJobService, "peripheralJobService"); + this.reservationToken = requireNonNull(reservationToken, "reservationToken"); + } + + /** + * Starts this peripheral interaction. + * + * @param interactionSucceededCallback The callback that is to be executed if the interaction + * succeeds. + * @param interactionFailedCallback The callback that is to be executed if the interaction fails. + */ + public void start( + @Nonnull + Runnable interactionSucceededCallback, + @Nonnull + Runnable interactionFailedCallback + ) { + this.interactionSucceededCallback = requireNonNull( + interactionSucceededCallback, + "interactionSucceededCallback" + ); + this.interactionFailedCallback = requireNonNull( + interactionFailedCallback, + "interactionFailedCallback" + ); + + LOG.debug( + "{}: Starting peripheral interaction for movement to {}", + vehicleRef.getName(), + movementCommand.getStep().getDestinationPoint().getName() + ); + for (PeripheralOperation operation : operations) { + PeripheralJob job = createPeripheralJob(operation); + if (operation.isCompletionRequired()) { + pendingJobsWithCompletionRequired.add(job); + } + } + + state = State.STARTED; + + if (pendingJobsWithCompletionRequired.isEmpty()) { + onInteractionFinished(); + } + } + + /** + * Informs this interaction that a peripheral job (that might be of interest for this interaction) + * has been finished. + * + * @param job The peripheral job that has been finished. + */ + public void onPeripheralJobFinished( + @Nonnull + PeripheralJob job + ) { + requireNonNull(job, "job"); + if (pendingJobsWithCompletionRequired.remove(job) + && pendingJobsWithCompletionRequired.isEmpty()) { + // The last pending job has been finished. + onInteractionFinished(); + } + } + + /** + * Informs this interaction that a peripheral job (that might be of interest for this interaction) + * has failed. + * + * @param job The peripheral job that has failed. + */ + public void onPeripheralJobFailed( + @Nonnull + PeripheralJob job + ) { + requireNonNull(job, "job"); + if (pendingJobsWithCompletionRequired.contains(job)) { + // As soon as one of the jobs for this interaction fails, the entire interaction itself is + // considered failed. At this point, we're no longer interested in any of the pending jobs and + // don't expect any other job to be reported as finished or failed. Therefore, simply forget + // all pending jobs and mark the interaction as failed. This also ensures that the callback + // for the failed interaction is called only once. + pendingJobsWithCompletionRequired.clear(); + onInteractionFailed(); + } + } + + /** + * Returns the movement command this interaction is associated with. + * + * @return The movement command this interaction is associated with. + */ + public MovementCommand getMovementCommand() { + return movementCommand; + } + + /** + * Returns whether the interaction is finished. + * + * @return Whether the interaction is finished. + */ + public boolean isFinished() { + return hasState(State.FINISHED); + } + + /** + * Returns whether the interaction has failed. + * + * @return Whether the interaction has failed. + */ + public boolean isFailed() { + return hasState(State.FAILED); + } + + /** + * Returns whether the interaction is in the given state. + * + * @param state The state. + * @return Whether the interaction is in the given state. + */ + public boolean hasState(State state) { + return this.state == state; + } + + /** + * Returns whether this interaction has some operations that are required to be completed. + * + * @return Whether this interaction has some operations that are required to be completed. + */ + public boolean hasRequiredOperations() { + return operations.stream() + .anyMatch(PeripheralOperation::isCompletionRequired); + } + + /** + * Returns the list of operations that are required to be completed and that haven't been + * completed yet. + * + * @return A list of operations. + */ + public List getPendingRequiredOperations() { + // If we're already done interacting with the peripheral device, there cannot be any pending + // operations. + if (hasState(State.FINISHED)) { + return new ArrayList<>(); + } + + if (!hasRequiredOperations()) { + return new ArrayList<>(); + } + + if (!pendingJobsWithCompletionRequired.isEmpty()) { + // The interaction is still ongoing. Jobs are not yet finished or have even failed. + return pendingJobsWithCompletionRequired.stream() + .map(job -> job.getPeripheralOperation()) + .collect(Collectors.toList()); + } + + // The interaction is still ongoing but no jobs have been created (yet) for the required + // operations. + return operations.stream() + .filter(PeripheralOperation::isCompletionRequired) + .collect(Collectors.toList()); + } + + private void onInteractionFinished() { + checkState(interactionSucceededCallback != null, "The interaction hasn't been started yet."); + checkState(!hasState(State.FAILED), "The interaction has already been marked as failed."); + checkState(!hasState(State.FINISHED), "The interaction has already been marked as finished."); + state = State.FINISHED; + + LOG.debug( + "{}: Peripheral interaction finished for movement to {}", + vehicleRef.getName(), + movementCommand.getStep().getDestinationPoint().getName() + ); + interactionSucceededCallback.run(); + } + + private PeripheralJob createPeripheralJob(PeripheralOperation operation) { + return peripheralJobService.createPeripheralJob( + new PeripheralJobCreationTO( + "Job-", + reservationToken, + new PeripheralOperationCreationTO( + operation.getOperation(), + operation.getLocation().getName() + ) + .withExecutionTrigger(operation.getExecutionTrigger()) + .withCompletionRequired(operation.isCompletionRequired()) + ) + .withIncompleteName(true) + .withRelatedVehicleName(vehicleRef.getName()) + .withRelatedTransportOrderName(orderRef.getName()) + ); + } + + private void onInteractionFailed() { + checkState(interactionFailedCallback != null, "The interaction hasn't been started yet."); + checkState(!hasState(State.FINISHED), "The interaction has already been marked as finished."); + checkState(!hasState(State.FAILED), "The interaction has already been marked as failed."); + state = State.FAILED; + + LOG.debug( + "{}: Peripheral interaction failed for movement to {}", + vehicleRef.getName(), + movementCommand.getStep().getDestinationPoint().getName() + ); + interactionFailedCallback.run(); + } + + public enum State { + /** + * The interaction is initialized and yet to be started. + */ + PRISTINE, + /** + * The interaction was started. + */ + STARTED, + /** + * The interaction was finished. + * All the required operations (if any) have been finished successfully. + */ + FINISHED, + /** + * The interaction has failed. + * At least one of the required operations failed. + */ + FAILED; + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/PeripheralInteractor.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/PeripheralInteractor.java new file mode 100644 index 0000000..ecb5713 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/PeripheralInteractor.java @@ -0,0 +1,461 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.opentcs.components.Lifecycle; +import org.opentcs.components.kernel.services.PeripheralDispatcherService; +import org.opentcs.components.kernel.services.PeripheralJobService; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.data.peripherals.PeripheralOperation; +import org.opentcs.drivers.vehicle.MovementCommand; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.event.EventSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages interactions with peripheral devices that are to be performed before or after the + * execution of movement commands. + */ +public class PeripheralInteractor + implements + EventHandler, + Lifecycle { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PeripheralInteractor.class); + /** + * The reference to the vehicle that's interacting with peripheral devices. + */ + private final TCSObjectReference vehicleRef; + /** + * The peripheral job service to use. + */ + private final PeripheralJobService peripheralJobService; + /** + * The peripheral dispatcher service to use. + */ + private final PeripheralDispatcherService peripheralDispatcherService; + /** + * The event source to register with. + */ + private final EventSource eventSource; + /** + * The peripheral interactions to be performed BEFORE the execution of a movement command mapped + * to the corresponding movement command. + */ + private final Map preMovementInteractions + = new HashMap<>(); + /** + * The peripheral interactions to be performed AFTER the execution of a movement command mapped + * to the corresponding movement command. + */ + private final Map postMovementInteractions + = new HashMap<>(); + /** + * Indicates whether this instance is initialized. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param vehicleRef The reference to the vehicle that's interacting with peripheral devices. + * @param peripheralJobService The peripheral job service to use. + * @param peripheralDispatcherService The peripheral dispatcher service to use. + * @param eventSource The event source to register with. + */ + @Inject + public PeripheralInteractor( + @Assisted + @Nonnull + TCSObjectReference vehicleRef, + @Nonnull + PeripheralJobService peripheralJobService, + @Nonnull + PeripheralDispatcherService peripheralDispatcherService, + @Nonnull + @ApplicationEventBus + EventSource eventSource + ) { + this.vehicleRef = requireNonNull(vehicleRef, "vehicleRef"); + this.peripheralJobService = requireNonNull(peripheralJobService, "peripheralJobService"); + this.peripheralDispatcherService = requireNonNull( + peripheralDispatcherService, + "peripheralDispatcherService" + ); + this.eventSource = requireNonNull(eventSource, "eventSource"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + eventSource.subscribe(this); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + eventSource.unsubscribe(this); + + initialized = false; + } + + @Override + public void onEvent(Object event) { + if (!(event instanceof TCSObjectEvent)) { + return; + } + + TCSObjectEvent objectEvent = (TCSObjectEvent) event; + if (objectEvent.getType() != TCSObjectEvent.Type.OBJECT_MODIFIED) { + return; + } + + TCSObject currentOrPreviousObjectState = objectEvent.getCurrentOrPreviousObjectState(); + if (!(currentOrPreviousObjectState instanceof PeripheralJob)) { + return; + } + + // Since a PeripheralInteraction only keeps track of peripheral jobs where the completion + // required flag is set, we can ignore all peripheral jobs where this is not the case. + if (!hasCompletionRequiredFlagSet((PeripheralJob) currentOrPreviousObjectState)) { + return; + } + + onPeripheralJobChange(objectEvent); + } + + /** + * Prepares for peripheral interactions in the context of the given movement command by + * determining the interactions that have to be performed before and after the movement command + * is executed. + * + * @param orderRef A reference to the transport order that the movement command belongs to. + * @param movementCommand The movement command to prepare peripheral interactions for. + */ + public void prepareInteractions( + TCSObjectReference orderRef, + MovementCommand movementCommand + ) { + Path path = movementCommand.getStep().getPath(); + if (path == null) { + return; + } + + Map> operations + = path.getPeripheralOperations().stream() + .collect(Collectors.groupingBy(t -> t.getExecutionTrigger())); + operations.computeIfAbsent( + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + executionTrigger -> new ArrayList<>() + ); + operations.computeIfAbsent( + PeripheralOperation.ExecutionTrigger.AFTER_MOVEMENT, + executionTrigger -> new ArrayList<>() + ); + String reservationToken = determineReservationToken(); + + List preMovementOperations + = operations.get(PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION); + if (!preMovementOperations.isEmpty()) { + preMovementInteractions.put( + movementCommand, + new PeripheralInteraction( + vehicleRef, + orderRef, + movementCommand, + preMovementOperations, + peripheralJobService, + reservationToken + ) + ); + } + + List postMovementOperations + = operations.get(PeripheralOperation.ExecutionTrigger.AFTER_MOVEMENT); + if (!postMovementOperations.isEmpty()) { + postMovementInteractions.put( + movementCommand, + new PeripheralInteraction( + vehicleRef, + orderRef, + movementCommand, + postMovementOperations, + peripheralJobService, + reservationToken + ) + ); + } + } + + /** + * Starts the peripheral interactions that have to be performed before the given movement command + * is executed. + * + * @param movementCommand The movement command. + * @param succeededCallback The callback that is executed if the interactions succeeds (i.e. once + * all required interactions are finished). + * @param failedCallback The callback that is executed if the interactions fails (i.e. if a + * single interaction failed). + */ + public void startPreMovementInteractions( + @Nonnull + MovementCommand movementCommand, + @Nonnull + Runnable succeededCallback, + @Nonnull + Runnable failedCallback + ) { + requireNonNull(movementCommand, "movementCommand"); + requireNonNull(succeededCallback, "succeededCallback"); + requireNonNull(failedCallback, "failedCallback"); + if (!preMovementInteractions.containsKey(movementCommand)) { + LOG.debug( + "{}: No interactions to be performed before movement to {}...", + vehicleRef.getName(), + movementCommand.getStep().getDestinationPoint().getName() + ); + succeededCallback.run(); + return; + } + + LOG.debug( + "{}: There are interactions to be performed before movement to {}...", + vehicleRef.getName(), + movementCommand.getStep().getDestinationPoint().getName() + ); + preMovementInteractions.get(movementCommand).start(succeededCallback, failedCallback); + + // In case there are only operations with the completion required flag not set, the interaction + // is immediately finished and we can remove it right away. + if (preMovementInteractions.get(movementCommand).isFinished()) { + preMovementInteractions.remove(movementCommand); + } + + // Peripheral jobs have been created. Dispatch them. + peripheralDispatcherService.dispatch(); + } + + /** + * Starts the peripheral interactions that have to be performed after the given movement command + * is executed. + * + * @param movementCommand The movement command. + * @param succeededCallback The callback that is executed if the interactions succeeds (i.e. once + * all required interactions are finished). + * @param failedCallback The callback that is executed if the interactions fails (i.e. if a + * single interaction failed). + */ + public void startPostMovementInteractions( + @Nonnull + MovementCommand movementCommand, + @Nonnull + Runnable succeededCallback, + @Nonnull + Runnable failedCallback + ) { + requireNonNull(movementCommand, "movementCommand"); + requireNonNull(succeededCallback, "succeededCallback"); + requireNonNull(failedCallback, "failedCallback"); + if (!postMovementInteractions.containsKey(movementCommand)) { + LOG.debug( + "{}: No interactions to be performed after movement to {}...", + vehicleRef.getName(), + movementCommand.getStep().getDestinationPoint().getName() + ); + succeededCallback.run(); + return; + } + + LOG.debug( + "{}: There are interactions to be performed after movement to {}...", + vehicleRef.getName(), + movementCommand.getStep().getDestinationPoint().getName() + ); + postMovementInteractions.get(movementCommand).start(succeededCallback, failedCallback); + + // In case there are only operations with the completion required flag not set, the interaction + // is immediately finished and we can remove it right away. + if (postMovementInteractions.get(movementCommand).isFinished()) { + postMovementInteractions.remove(movementCommand); + } + + // Peripheral jobs have been created. Dispatch them. + peripheralDispatcherService.dispatch(); + } + + /** + * Returns whether there are any required (pre or post movement) interactions that have not been + * finished yet. + * In case there's a required interaction that has failed (and therefore not finished), this + * method returns {@code true}. + * + * @return Whether there are any required interactions that have not been finished yet. + */ + public boolean isWaitingForMovementInteractionsToFinish() { + return isWaitingForPreMovementInteractionsToFinish() + || isWaitingForPostMovementInteractionsToFinish(); + } + + /** + * Returns whether there are any required (pre movement) interactions that have not been finished + * yet. + * In case there's a required interaction that has failed (and therefore not finished), this + * method returns {@code true}. + * + * @return Whether there are any required (pre movement) interactions that have not been finished + * yet. + */ + public boolean isWaitingForPreMovementInteractionsToFinish() { + return !preMovementInteractions.values().stream() + .filter(PeripheralInteraction::hasRequiredOperations) + .allMatch(PeripheralInteraction::isFinished); + } + + /** + * Returns whether there are any required (post movement) interactions that have not been finished + * yet. + * In case there's a required interaction that has failed (and therefore not finished), this + * method returns {@code true}. + * + * @return Whether there are any required (post movement) interactions that have not been finished + * yet. + */ + public boolean isWaitingForPostMovementInteractionsToFinish() { + return !postMovementInteractions.values().stream() + .filter(PeripheralInteraction::hasRequiredOperations) + .allMatch(PeripheralInteraction::isFinished); + } + + /** + * Returns a list of required operations that are still to be completed mapped to the associated + * movement command's destination point. + * + * @return A list of required operations that are still to be completed mapped to the associated + * movement command's destination point. + */ + public Map> pendingRequiredInteractionsByDestination() { + return Stream.concat( + preMovementInteractions.entrySet().stream(), + postMovementInteractions.entrySet().stream() + ) + .map(entry -> entry.getValue()) + .filter(interaction -> interaction.hasRequiredOperations()) + // We're working with two streams from two maps which can each contain the same keys. + // Therefore we have to use the groupingBy collector and need to flat map each interaction's + // pending required operations. + .collect( + Collectors.groupingBy( + interact -> interact.getMovementCommand().getStep().getDestinationPoint().getName(), + Collectors.flatMapping( + interaction -> interaction.getPendingRequiredOperations().stream(), + Collectors.toList() + ) + ) + ); + } + + private void onPeripheralJobChange(TCSObjectEvent event) { + PeripheralJob prevJobState = (PeripheralJob) event.getPreviousObjectState(); + PeripheralJob currJobState = (PeripheralJob) event.getCurrentObjectState(); + + if (prevJobState.getState() != currJobState.getState()) { + switch (currJobState.getState()) { + case FINISHED: + onPeripheralJobFinished(currJobState); + break; + case FAILED: + onPeripheralJobFailed(currJobState); + break; + default: // Do nothing + } + } + } + + private void onPeripheralJobFinished(PeripheralJob job) { + Stream.concat( + preMovementInteractions.values().stream(), + postMovementInteractions.values().stream() + ) + .forEach(interaction -> interaction.onPeripheralJobFinished(job)); + + // With a peripheral job finished, an associated interaction might now be finished as well. If + // that's the case, forget the interaction. + preMovementInteractions.entrySet().removeIf(entry -> entry.getValue().isFinished()); + postMovementInteractions.entrySet().removeIf(entry -> entry.getValue().isFinished()); + } + + private void onPeripheralJobFailed(PeripheralJob job) { + Stream.concat( + preMovementInteractions.values().stream(), + postMovementInteractions.values().stream() + ) + .forEach(interaction -> interaction.onPeripheralJobFailed(job)); + + // With a peripheral job failed, an associated interaction might now be failed as well. If + // that's the case, forget the interaction. + preMovementInteractions.entrySet().removeIf(entry -> entry.getValue().isFailed()); + postMovementInteractions.entrySet().removeIf(entry -> entry.getValue().isFailed()); + } + + /** + * Clears the interactions. + */ + public void clear() { + preMovementInteractions.clear(); + postMovementInteractions.clear(); + } + + private String determineReservationToken() { + Vehicle vehicle = peripheralJobService.fetchObject(Vehicle.class, vehicleRef); + if (vehicle.getTransportOrder() != null) { + TransportOrder transportOrder = peripheralJobService.fetchObject( + TransportOrder.class, + vehicle.getTransportOrder() + ); + if (transportOrder.getPeripheralReservationToken() != null) { + return transportOrder.getPeripheralReservationToken(); + } + } + + return vehicle.getName(); + } + + private boolean hasCompletionRequiredFlagSet(PeripheralJob job) { + return job.getPeripheralOperation().isCompletionRequired(); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/ResourceMath.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/ResourceMath.java new file mode 100644 index 0000000..06a1daf --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/ResourceMath.java @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.TCSResource; + +/** + * Utility methods for resource-related computations. + */ +public class ResourceMath { + + /** + * Prevents instantiation. + */ + private ResourceMath() { + } + + /** + * Returns the number of resource sets that could be freed based on the given vehicle length. + * + * @param resourcesPassed A list of passed resource sets still allocated for a vehicle, from + * oldest to youngest, including those for the vehicle's current position (= the last set in the + * list). + * @param vehicleLength The vehicle's length. Must be a positive value. + * @return The number of resource sets from {@code resourcesPassed} that could be freed because + * they are not covered by the vehicle's length any more. + */ + public static int freeableResourceSetCount( + @Nonnull + List>> resourcesPassed, + long vehicleLength + ) { + requireNonNull(resourcesPassed, "resourcesPassed"); + checkArgument(vehicleLength > 0, "vehicleLength <= 0"); + + // We want to iterate over the passed resources from youngest (= current position) to oldest, so + // we reverse the list here. + List>> reversedPassedResources = new ArrayList<>(resourcesPassed); + Collections.reverse(reversedPassedResources); + + long remainingRequiredLength = vehicleLength; + int result = 0; + for (Set> curSet : reversedPassedResources) { + if (remainingRequiredLength > 0) { + remainingRequiredLength -= requiredLength(curSet); + } + else { + result++; + } + } + + return result; + } + + private static long requiredLength(Set> resources) { + return resources.stream() + .filter(resource -> resource instanceof Path) + .mapToLong(resource -> ((Path) resource).getLength()) + .sum(); + } + +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/SplitResources.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/SplitResources.java new file mode 100644 index 0000000..8d4948c --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/SplitResources.java @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.opentcs.data.model.TCSResource; + +/** + * A vehicle's resources, split into resources the vehicle has already passed (including the + * resources for the vehicle's current position) and resources that still lay ahead of it. + */ +public class SplitResources { + + private final List>> resourcesPassed; + private final List>> resourcesAhead; + + /** + * Creates a new instance. + * + * @param resourcesPassed The passed resources, including the ones for the vehicle's current + * position. + * @param resourcesAhead The resources ahead of the vehicle. + */ + public SplitResources( + @Nonnull + List>> resourcesPassed, + @Nonnull + List>> resourcesAhead + ) { + this.resourcesPassed = requireNonNull(resourcesPassed, "resourcesPassed"); + this.resourcesAhead = requireNonNull(resourcesAhead, "resourcesAhead"); + } + + /** + * Returns the resources the vehicle has already passed, from oldest to youngest, with the + * youngest being the resources for the vehicle's current position. + * + * @return The resources the vehicle has already passed, from oldest to youngest, with the + * youngest being the resources for the vehicle's current position. + */ + public List>> getResourcesPassed() { + return resourcesPassed; + } + + /** + * Returns the resources ahead of the vehicle, from oldest to youngest. + * + * @return The resources ahead of the vehicle, from oldest to youngest. + */ + public List>> getResourcesAhead() { + return resourcesAhead; + } + + /** + * Returns a new instance created from the given iterable of resources, split at the element that + * contains the given delimiter (resources). + * + * @param resourceSets The iterable of resources to be split, from oldest to youngest. + * @param delimiter The delimiter / resources for the vehicle's current position. + * @return A new instance created from the given iterable of resources, split at the element that + * contains the given delimiter (resources). + */ + public static SplitResources from( + @Nonnull + Iterable>> resourceSets, + @Nonnull + Set> delimiter + ) { + requireNonNull(resourceSets, "resourceSets"); + requireNonNull(delimiter, "delimiter"); + + List>> resourcesPassed = new ArrayList<>(); + List>> resourcesAhead = new ArrayList<>(); + List>> resourcesToPutIn = resourcesPassed; + + for (Set> curSet : resourceSets) { + resourcesToPutIn.add(curSet); + + if (!delimiter.isEmpty() && curSet.containsAll(delimiter)) { + resourcesToPutIn = resourcesAhead; + } + } + + return new SplitResources(resourcesPassed, resourcesAhead); + } + +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/VehicleCommAdapterRegistry.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/VehicleCommAdapterRegistry.java new file mode 100644 index 0000000..0c74549 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/VehicleCommAdapterRegistry.java @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; +import org.opentcs.access.LocalKernel; +import org.opentcs.components.Lifecycle; +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; +import org.opentcs.drivers.vehicle.VehicleCommAdapterFactory; +import org.opentcs.virtualvehicle.LoopbackCommunicationAdapterDescription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A registry for all communication adapters in the system. + */ +public class VehicleCommAdapterRegistry + implements + Lifecycle { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(VehicleCommAdapterRegistry.class); + /** + * The registered factories. Uses a comparator to sort the loopback driver to the end. + */ + private final Map factories + = new TreeMap<>((f1, f2) -> { + if (f1 instanceof LoopbackCommunicationAdapterDescription + && f2 instanceof LoopbackCommunicationAdapterDescription) { + return 0; + } + if (f1 instanceof LoopbackCommunicationAdapterDescription) { + return 1; + } + else if (f2 instanceof LoopbackCommunicationAdapterDescription) { + return -1; + } + return f1.getDescription().compareTo(f2.getDescription()); + }); + /** + * Indicates whether this component is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new registry. + * + * @param kernel A reference to the local kernel. + * @param factories The comm adapter factories. + */ + @Inject + public VehicleCommAdapterRegistry(LocalKernel kernel, Set factories) { + requireNonNull(kernel, "kernel"); + + for (VehicleCommAdapterFactory factory : factories) { + LOG.info("Setting up communication adapter factory: {}", factory.getClass().getName()); + this.factories.put(factory.getDescription(), factory); + } + + checkState(!factories.isEmpty(), "No adapter factories found."); + } + + @Override + public void initialize() { + if (initialized) { + LOG.debug("Already initialized."); + return; + } + for (VehicleCommAdapterFactory factory : factories.values()) { + factory.initialize(); + } + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!initialized) { + LOG.debug("Not initialized."); + return; + } + for (VehicleCommAdapterFactory factory : factories.values()) { + factory.terminate(); + } + initialized = false; + } + + /** + * Returns all registered factories that can provide communication adapters. + * + * @return All registered factories that can provide communication adapters. + */ + public List getFactories() { + return new ArrayList<>(factories.values()); + } + + /** + * Returns the factory for the given description. + * + * @param description The description to get the factory for. + * @return The factory for the given description. + */ + public VehicleCommAdapterFactory findFactoryFor(VehicleCommAdapterDescription description) { + requireNonNull(description, "description"); + checkArgument( + factories.get(description) != null, + "No factory for description %s", + description + ); + + return factories.get(description); + } + + /** + * Returns a set of factories that can provide communication adapters for the + * given vehicle. + * + * @param vehicle The vehicle to find communication adapters/factories for. + * @return A set of factories that can provide communication adapters for the + * given vehicle. + */ + public List findFactoriesFor(Vehicle vehicle) { + requireNonNull(vehicle, "vehicle"); + + return factories.values().stream() + .filter(factory -> factory.providesAdapterFor(vehicle)) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/VehicleControllerComponentsFactory.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/VehicleControllerComponentsFactory.java new file mode 100644 index 0000000..0a12db2 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/VehicleControllerComponentsFactory.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; + +/** + * A factory for various components related to a vehicle controller. + */ +public interface VehicleControllerComponentsFactory { + + /** + * Creates a new {@link PeripheralInteractor} instance for the given vehicle. + * + * @param vehicleRef The vehicle. + * @return A new peripheral interactor. + */ + PeripheralInteractor createPeripheralInteractor(TCSObjectReference vehicleRef); +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/VehicleControllerFactory.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/VehicleControllerFactory.java new file mode 100644 index 0000000..9e6a3fd --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/VehicleControllerFactory.java @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; + +/** + * A factory for VehicleManager instances. + */ +public interface VehicleControllerFactory { + + /** + * Creates a new vehicle controller for the given vehicle and communication adapter. + * + * @param vehicle The vehicle. + * @param commAdapter The communication adapter. + * @return A new vehicle controller. + */ + DefaultVehicleController createVehicleController(Vehicle vehicle, VehicleCommAdapter commAdapter); +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemIncomingPoseTransformer.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemIncomingPoseTransformer.java new file mode 100644 index 0000000..f5f70ee --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemIncomingPoseTransformer.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles.transformers; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.Optional; +import org.opentcs.data.model.Pose; +import org.opentcs.data.model.Triple; +import org.opentcs.drivers.vehicle.IncomingPoseTransformer; + +/** + * Transforms {@link Pose}s by subtracting offsets in a given + * {@link CoordinateSystemTransformation}. + */ +public class CoordinateSystemIncomingPoseTransformer + implements + IncomingPoseTransformer { + + private final CoordinateSystemTransformation transformation; + + public CoordinateSystemIncomingPoseTransformer( + @Nonnull + CoordinateSystemTransformation transformation + ) { + this.transformation = requireNonNull(transformation, "transformation"); + } + + @Override + public Pose apply( + @Nonnull + Pose pose + ) { + requireNonNull(pose, "pose"); + + return pose + .withPosition(transformTriple(pose.getPosition())) + .withOrientationAngle( + (pose.getOrientationAngle() - transformation.getOffsetOrientation()) % 360.0 + ); + } + + private Triple transformTriple(Triple triple) { + return Optional.ofNullable(triple) + .map( + originalTriple -> new Triple( + originalTriple.getX() - transformation.getOffsetX(), + originalTriple.getY() - transformation.getOffsetY(), + originalTriple.getZ() - transformation.getOffsetZ() + ) + ) + .orElse(null); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemMovementCommandTransformer.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemMovementCommandTransformer.java new file mode 100644 index 0000000..43c1992 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemMovementCommandTransformer.java @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles.transformers; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Pose; +import org.opentcs.data.model.Triple; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.Route.Step; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.drivers.vehicle.MovementCommand; +import org.opentcs.drivers.vehicle.MovementCommandTransformer; + +/** + * Transforms coordinates in {@link MovementCommand}s by adding offsets in a given + * {@link CoordinateSystemTransformation}. + */ +public class CoordinateSystemMovementCommandTransformer + implements + MovementCommandTransformer { + + private final CoordinateSystemTransformation transformation; + + public CoordinateSystemMovementCommandTransformer( + @Nonnull + CoordinateSystemTransformation transformation + ) { + this.transformation = requireNonNull(transformation, "transformation"); + } + + @Override + public MovementCommand apply(MovementCommand command) { + return command + .withTransportOrder(transformTransportOrder(command.getTransportOrder())) + .withDriveOrder(transformDriveOrder(command.getDriveOrder())) + .withFinalDestination(transformPoint(command.getFinalDestination())) + .withFinalDestinationLocation(transformLocation(command.getFinalDestinationLocation())) + .withOpLocation(transformLocation(command.getOpLocation())) + .withStep(transformStep(command.getStep())); + } + + private TransportOrder transformTransportOrder(TransportOrder oldTransportOrder) { + return oldTransportOrder.withDriveOrders( + transformDriveOrders(oldTransportOrder.getAllDriveOrders()) + ); + } + + private List transformDriveOrders(List oldDriveOrders) { + return oldDriveOrders.stream() + .map(this::transformDriveOrder) + .toList(); + } + + private DriveOrder transformDriveOrder(DriveOrder oldOrder) { + return oldOrder.withRoute(transformRoute(oldOrder.getRoute())); + } + + private Route transformRoute(Route route) { + return Optional.ofNullable(route) + .map( + originalRoute -> new Route( + originalRoute.getSteps().stream() + .map(step -> transformStep(step)) + .collect(Collectors.toList()), + originalRoute.getCosts() + ) + ) + .orElse(null); + } + + private Step transformStep(Step oldStep) { + return new Step( + oldStep.getPath(), + transformPoint(oldStep.getSourcePoint()), + transformPoint(oldStep.getDestinationPoint()), + oldStep.getVehicleOrientation(), + oldStep.getRouteIndex(), + oldStep.isExecutionAllowed(), + oldStep.getReroutingType() + ); + } + + private Point transformPoint(Point point) { + return Optional.ofNullable(point) + .map( + originalPoint -> originalPoint.withPose( + new Pose( + transformTriple(originalPoint.getPose().getPosition()), + (originalPoint.getPose().getOrientationAngle() + transformation + .getOffsetOrientation()) % 360.0 + ) + ) + ) + .orElse(null); + } + + private Location transformLocation(Location location) { + return Optional.ofNullable(location) + .map( + originalLocation -> originalLocation.withPosition( + transformTriple(originalLocation.getPosition()) + ) + ) + .orElse(null); + } + + private Triple transformTriple(Triple triple) { + return Optional.ofNullable(triple) + .map( + originalTriple -> new Triple( + originalTriple.getX() + transformation.getOffsetX(), + originalTriple.getY() + transformation.getOffsetY(), + originalTriple.getZ() + transformation.getOffsetZ() + ) + ) + .orElse(null); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemTransformation.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemTransformation.java new file mode 100644 index 0000000..f1ef87a --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemTransformation.java @@ -0,0 +1,177 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles.transformers; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.Optional; +import org.opentcs.data.model.Vehicle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The data used by coordinate system transformer classes. + *

+ * The data consists of offset values that are ... + *

+ *
    + *
  • added to coordinate and orientation angle values sent to the vehicle driver.
  • + *
  • subtracted from coordinate and orientation angle values reported by the vehicle driver.
  • + *
+ */ +public class CoordinateSystemTransformation { + + /** + * The key of the property for the x offset. + */ + public static final String PROPKEY_OFFSET_X = "tcs:offsetTransformer.x"; + /** + * The key of the property for the y offset. + */ + public static final String PROPKEY_OFFSET_Y = "tcs:offsetTransformer.y"; + /** + * The key of the property for the z offset. + */ + public static final String PROPKEY_OFFSET_Z = "tcs:offsetTransformer.z"; + /** + * The key of the property for the orientation offset. + */ + public static final String PROPKEY_OFFSET_ORIENTATION = "tcs:offsetTransformer.orientation"; + + private static final Logger LOG = LoggerFactory.getLogger(CoordinateSystemTransformation.class); + private final int offsetX; + private final int offsetY; + private final int offsetZ; + private final double offsetOrientation; + + /** + * Creates a new instance. + * + * @param offsetX The x offset. + * @param offsetY The y offset. + * @param offsetZ The z offset. + * @param offsetOrientation The orientation offset. + */ + public CoordinateSystemTransformation( + int offsetX, int offsetY, int offsetZ, double offsetOrientation + ) { + this.offsetX = offsetX; + this.offsetY = offsetY; + this.offsetZ = offsetZ; + this.offsetOrientation = offsetOrientation; + } + + /** + * Returns the x offset. + * + * @return The x offset. + */ + public int getOffsetX() { + return offsetX; + } + + /** + * Returns the y offset. + * + * @return The y offset. + */ + public int getOffsetY() { + return offsetY; + } + + /** + * Returns the z offset. + * + * @return The z offset. + */ + public int getOffsetZ() { + return offsetZ; + } + + /** + * Returns the orientation offset. + * + * @return The orientation offset. + */ + public double getOffsetOrientation() { + return offsetOrientation; + } + + /** + * Creates a {@link CoordinateSystemTransformation} based on offsets given via vehicle properties. + *

+ * The property keys used are the values of the following constants: + *

+ *
    + *
  • {@link #PROPKEY_OFFSET_X}
  • + *
  • {@link #PROPKEY_OFFSET_Y}
  • + *
  • {@link #PROPKEY_OFFSET_Z}
  • + *
  • {@link #PROPKEY_OFFSET_ORIENTATION}
  • + *
+ * + * @param vehicle The vehicle. + * @return An {@link Optional} containing the {@link CoordinateSystemTransformation} or + * {@link Optional#empty()}, if one of the corresponding property values could not be parsed. + */ + public static Optional fromVehicle( + @Nonnull + Vehicle vehicle + ) { + requireNonNull(vehicle, "vehicle"); + + try { + return Optional.of( + new CoordinateSystemTransformation( + getPropertyInteger(vehicle, PROPKEY_OFFSET_X, 0), + getPropertyInteger(vehicle, PROPKEY_OFFSET_Y, 0), + getPropertyInteger(vehicle, PROPKEY_OFFSET_Z, 0), + getPropertyDouble(vehicle, PROPKEY_OFFSET_ORIENTATION, 0.0) + ) + ); + } + catch (NumberFormatException e) { + LOG.warn( + "Could not create coordinate system transformation for vehicle '{}'.", + vehicle.getName(), + e + ); + return Optional.empty(); + } + } + + private static int getPropertyInteger(Vehicle vehicle, String propertyKey, int defaultValue) + throws NumberFormatException { + String property = vehicle.getProperty(propertyKey); + if (property != null) { + return Integer.parseInt(property); + } + else { + LOG.debug( + "{}: Property '{}' is not set. Using default value {}.", + vehicle.getName(), + propertyKey, + defaultValue + ); + return defaultValue; + } + } + + + private static double getPropertyDouble(Vehicle vehicle, String propertyKey, double defaultValue) + throws NumberFormatException { + String property = vehicle.getProperty(propertyKey); + if (property != null) { + return Double.parseDouble(property); + } + else { + LOG.debug( + "{}: Property '{}' is not set. Using default value {}.", + vehicle.getName(), + propertyKey, + defaultValue + ); + return defaultValue; + } + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemTransformerFactory.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemTransformerFactory.java new file mode 100644 index 0000000..99ef19c --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemTransformerFactory.java @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles.transformers; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.IncomingPoseTransformer; +import org.opentcs.drivers.vehicle.MovementCommandTransformer; +import org.opentcs.drivers.vehicle.VehicleDataTransformerFactory; + +/** + * Provides instances of {@link CoordinateSystemMovementCommandTransformer} and + * {@link CoordinateSystemIncomingPoseTransformer}. + */ +public class CoordinateSystemTransformerFactory + implements + VehicleDataTransformerFactory { + + public CoordinateSystemTransformerFactory() { + } + + @Override + @Nonnull + public String getName() { + return "OFFSET_TRANSFORMER"; + } + + @Override + @Nonnull + public MovementCommandTransformer createMovementCommandTransformer( + @Nonnull + Vehicle vehicle + ) { + requireNonNull(vehicle); + + return new CoordinateSystemMovementCommandTransformer( + CoordinateSystemTransformation.fromVehicle(vehicle) + .orElseThrow( + () -> new IllegalArgumentException( + "Cannot create transformer without transformation data." + ) + ) + ); + } + + @Override + @Nonnull + public IncomingPoseTransformer createIncomingPoseTransformer( + @Nonnull + Vehicle vehicle + ) { + requireNonNull(vehicle); + + return new CoordinateSystemIncomingPoseTransformer( + CoordinateSystemTransformation.fromVehicle(vehicle) + .orElseThrow( + () -> new IllegalArgumentException( + "Cannot create transformer without transformation data." + ) + ) + ); + } + + @Override + public boolean providesTransformersFor( + @Nonnull + Vehicle vehicle + ) { + requireNonNull(vehicle, "vehicle"); + + return CoordinateSystemTransformation.fromVehicle(vehicle).isPresent(); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/transformers/DefaultVehicleDataTransformerFactory.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/transformers/DefaultVehicleDataTransformerFactory.java new file mode 100644 index 0000000..35c701f --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/transformers/DefaultVehicleDataTransformerFactory.java @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles.transformers; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.IncomingPoseTransformer; +import org.opentcs.drivers.vehicle.MovementCommandTransformer; +import org.opentcs.drivers.vehicle.VehicleDataTransformerFactory; + +/** + * Provides transformers that do not modify their inputs in any way. + */ +public class DefaultVehicleDataTransformerFactory + implements + VehicleDataTransformerFactory { + + public DefaultVehicleDataTransformerFactory() { + } + + @Override + @Nonnull + public String getName() { + return "DEFAULT_TRANSFORMER"; + } + + @Override + @Nonnull + public MovementCommandTransformer createMovementCommandTransformer( + @Nonnull + Vehicle vehicle + ) { + requireNonNull(vehicle, "vehicle"); + + return command -> command; + } + + @Override + @Nonnull + public IncomingPoseTransformer createIncomingPoseTransformer( + @Nonnull + Vehicle vehicle + ) { + requireNonNull(vehicle, "vehicle"); + + return pose -> pose; + } + + @Override + public boolean providesTransformersFor( + @Nonnull + Vehicle vehicle + ) { + requireNonNull(vehicle, "vehicle"); + + return true; + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/transformers/VehicleDataTransformerRegistry.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/transformers/VehicleDataTransformerRegistry.java new file mode 100644 index 0000000..0f35d9c --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/vehicles/transformers/VehicleDataTransformerRegistry.java @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles.transformers; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkState; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.Optional; +import java.util.Set; +import org.opentcs.components.Lifecycle; +import org.opentcs.data.ObjectPropConstants; +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.VehicleDataTransformerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A registry for all vehicle data transformers in the system. + */ +public class VehicleDataTransformerRegistry + implements + Lifecycle { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(VehicleDataTransformerRegistry.class); + /** + * The registered factories. + */ + private final Set factories; + /** + * Indicates whether this component is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new registry. + * + * @param factories The data transformer factories. + */ + @Inject + public VehicleDataTransformerRegistry( + @Nonnull + Set factories + ) { + this.factories = requireNonNull(factories, "factories"); + + checkState(!factories.isEmpty(), "No adapter factories found."); + } + + @Override + public void initialize() { + if (isInitialized()) { + LOG.debug("Already initialized."); + return; + } + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + LOG.debug("Not initialized."); + return; + } + initialized = false; + } + + /** + * Returns a factory for data transformers for the given vehicle. + * + * @param vehicle The vehicle to find a data transformer factory for. + * @return A factory for data transformers for the given vehicle. + */ + public VehicleDataTransformerFactory findFactoryFor( + @Nonnull + Vehicle vehicle + ) { + requireNonNull(vehicle, "vehicle"); + + return Optional.ofNullable(vehicle.getProperty(ObjectPropConstants.VEHICLE_DATA_TRANSFORMER)) + .flatMap( + name -> factories.stream() + .filter(factory -> name.equals(factory.getName())) + .filter(factory -> factory.providesTransformersFor(vehicle)) + .findAny() + ) + .orElseGet(() -> { + LOG.debug("Falling back to default transformer for vehicle '{}'", vehicle.getName()); + return new DefaultVehicleDataTransformerFactory(); + }); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/CompositeOrderSequenceCleanupApproval.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/CompositeOrderSequenceCleanupApproval.java new file mode 100644 index 0000000..df6e1f7 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/CompositeOrderSequenceCleanupApproval.java @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Set; +import org.opentcs.components.kernel.OrderSequenceCleanupApproval; +import org.opentcs.data.order.OrderSequence; + +/** + * A collection of {@link OrderSequenceCleanupApproval}s. + */ +public class CompositeOrderSequenceCleanupApproval + implements + OrderSequenceCleanupApproval { + + private final Set sequenceCleanupApprovals; + private final DefaultOrderSequenceCleanupApproval defaultOrderSequenceCleanupApproval; + + /** + * Creates a new instance. + * + * @param sequenceCleanupApprovals The {@link OrderSequenceCleanupApproval}s. + * @param defaultOrderSequenceCleanupApproval The {@link OrderSequenceCleanupApproval}, which + * should always be applied by default. + */ + @Inject + public CompositeOrderSequenceCleanupApproval( + Set sequenceCleanupApprovals, + DefaultOrderSequenceCleanupApproval defaultOrderSequenceCleanupApproval + ) { + this.sequenceCleanupApprovals = requireNonNull( + sequenceCleanupApprovals, + "sequenceCleanupApprovals" + ); + this.defaultOrderSequenceCleanupApproval + = requireNonNull( + defaultOrderSequenceCleanupApproval, + "defaultOrderSequenceCleanupApproval" + ); + } + + @Override + public boolean test(OrderSequence seq) { + if (!defaultOrderSequenceCleanupApproval.test(seq)) { + return false; + } + for (OrderSequenceCleanupApproval approval : sequenceCleanupApprovals) { + if (!approval.test(seq)) { + return false; + } + } + return true; + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/CompositePeripheralJobCleanupApproval.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/CompositePeripheralJobCleanupApproval.java new file mode 100644 index 0000000..b888643 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/CompositePeripheralJobCleanupApproval.java @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Set; +import org.opentcs.components.kernel.PeripheralJobCleanupApproval; +import org.opentcs.data.peripherals.PeripheralJob; + +/** + * A collection of {@link PeripheralJobCleanupApproval}s. + */ +public class CompositePeripheralJobCleanupApproval + implements + PeripheralJobCleanupApproval { + + private final Set peripheralJobCleanupApprovals; + private final DefaultPeripheralJobCleanupApproval defaultPeripheralJobCleanupApproval; + + /** + * Creates a new instance. + * + * @param peripheralJobCleanupApprovals The {@link PeripheralJobCleanupApproval}s. + * @param defaultPeripheralJobCleanupApproval The {@link PeripheralJobCleanupApproval}, which + * should always be applied by default. + */ + @Inject + public CompositePeripheralJobCleanupApproval( + Set peripheralJobCleanupApprovals, + DefaultPeripheralJobCleanupApproval defaultPeripheralJobCleanupApproval + ) { + this.peripheralJobCleanupApprovals = requireNonNull( + peripheralJobCleanupApprovals, + "peripheralJobCleanupApprovals" + ); + this.defaultPeripheralJobCleanupApproval + = requireNonNull( + defaultPeripheralJobCleanupApproval, + "defaultPeripheralJobCleanupApproval" + ); + } + + @Override + public boolean test(PeripheralJob job) { + if (!defaultPeripheralJobCleanupApproval.test(job)) { + return false; + } + for (PeripheralJobCleanupApproval approval : peripheralJobCleanupApprovals) { + if (!approval.test(job)) { + return false; + } + } + return true; + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/CompositeTransportOrderCleanupApproval.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/CompositeTransportOrderCleanupApproval.java new file mode 100644 index 0000000..f17c160 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/CompositeTransportOrderCleanupApproval.java @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Set; +import org.opentcs.components.kernel.TransportOrderCleanupApproval; +import org.opentcs.data.order.TransportOrder; + +/** + * A collection of {@link TransportOrderCleanupApproval}s. + */ +public class CompositeTransportOrderCleanupApproval + implements + TransportOrderCleanupApproval { + + private final Set orderCleanupApprovals; + private final DefaultTransportOrderCleanupApproval defaultTransportOrderCleanupApproval; + + /** + * Creates a new instance. + * + * @param orderCleanupApprovals The {@link TransportOrderCleanupApproval}s. + * @param defaultTransportOrderCleanupApproval The {@link TransportOrderCleanupApproval}, which + * should always be applied by default. + */ + @Inject + public CompositeTransportOrderCleanupApproval( + Set orderCleanupApprovals, + DefaultTransportOrderCleanupApproval defaultTransportOrderCleanupApproval + ) { + this.orderCleanupApprovals = requireNonNull(orderCleanupApprovals, "orderCleanupApprovals"); + this.defaultTransportOrderCleanupApproval + = requireNonNull( + defaultTransportOrderCleanupApproval, + "defaultTransportOrderCleanupApproval" + ); + } + + @Override + public boolean test(TransportOrder order) { + if (!defaultTransportOrderCleanupApproval.test(order)) { + return false; + } + for (TransportOrderCleanupApproval approval : orderCleanupApprovals) { + if (!approval.test(order)) { + return false; + } + } + return true; + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/CreationTimeThreshold.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/CreationTimeThreshold.java new file mode 100644 index 0000000..d79626c --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/CreationTimeThreshold.java @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import jakarta.inject.Inject; +import java.time.Instant; + +/** + * Keeps track of the time used to determine whether a working set item should be removed (according + * to its creation time). + */ +public class CreationTimeThreshold { + + private Instant currentThreshold; + + /** + * Creates a new instance. + */ + @Inject + public CreationTimeThreshold() { + } + + /** + * Updates the current threshold by subtracting the given amount of milliseconds from the current + * time. + * + * @param millis The amount of milliseconds to subtract from the current time. + */ + public void updateCurrentThreshold(long millis) { + currentThreshold = Instant.now().minusMillis(millis); + } + + /** + * Returns the current threshold. + *

+ * Working set items that are created before this point of time should be removed. + * + * @return The current threshold. + */ + public Instant getCurrentThreshold() { + return currentThreshold; + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/DefaultOrderSequenceCleanupApproval.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/DefaultOrderSequenceCleanupApproval.java new file mode 100644 index 0000000..575f576 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/DefaultOrderSequenceCleanupApproval.java @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.OrderSequenceCleanupApproval; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; + +/** + * Checks whether an order sequence may be removed. + */ +public class DefaultOrderSequenceCleanupApproval + implements + OrderSequenceCleanupApproval { + + private final TransportOrderPoolManager orderPoolManager; + private final DefaultTransportOrderCleanupApproval defaultTransportOrderCleanupApproval; + + /** + * Creates a new instance. + * + * @param orderPoolManager The order pool manager to be used. + * @param defaultTransportOrderCleanupApproval Checks whether a transport order may be removed. + */ + @Inject + public DefaultOrderSequenceCleanupApproval( + TransportOrderPoolManager orderPoolManager, + DefaultTransportOrderCleanupApproval defaultTransportOrderCleanupApproval + ) { + this.orderPoolManager = requireNonNull(orderPoolManager, "orderPoolManager"); + this.defaultTransportOrderCleanupApproval + = requireNonNull( + defaultTransportOrderCleanupApproval, + "defaultTransportOrderCleanupApproval" + ); + } + + @Override + public boolean test(OrderSequence seq) { + if (!seq.isFinished()) { + return false; + } + if (hasUnapprovedOrder(seq)) { + return false; + } + return true; + } + + private boolean hasUnapprovedOrder(OrderSequence seq) { + return !(seq.getOrders() + .stream() + .map( + reference -> orderPoolManager.getObjectRepo() + .getObject(TransportOrder.class, reference) + ) + .allMatch(defaultTransportOrderCleanupApproval)); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/DefaultPeripheralJobCleanupApproval.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/DefaultPeripheralJobCleanupApproval.java new file mode 100644 index 0000000..36992ba --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/DefaultPeripheralJobCleanupApproval.java @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.PeripheralJobCleanupApproval; +import org.opentcs.data.peripherals.PeripheralJob; + +/** + * Checks whether a peripheral job may be removed. + */ +public class DefaultPeripheralJobCleanupApproval + implements + PeripheralJobCleanupApproval { + + private final CreationTimeThreshold creationTimeThreshold; + + /** + * Creates a new instance. + * + * @param creationTimeThreshold Keeps track of the time used to determine whether a peripheral + * job should be removed (according to its creation time). + */ + @Inject + public DefaultPeripheralJobCleanupApproval(CreationTimeThreshold creationTimeThreshold) { + this.creationTimeThreshold = requireNonNull(creationTimeThreshold, "creationTimeThreshold"); + } + + @Override + public boolean test(PeripheralJob job) { + if (!job.getState().isFinalState()) { + return false; + } + if (job.getCreationTime().isAfter(creationTimeThreshold.getCurrentThreshold())) { + return false; + } + return true; + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/DefaultTransportOrderCleanupApproval.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/DefaultTransportOrderCleanupApproval.java new file mode 100644 index 0000000..131cf78 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/DefaultTransportOrderCleanupApproval.java @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Objects; +import org.opentcs.components.kernel.TransportOrderCleanupApproval; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.peripherals.PeripheralJob; + +/** + * Checks whether a transport order may be removed. + */ +public class DefaultTransportOrderCleanupApproval + implements + TransportOrderCleanupApproval { + + private final PeripheralJobPoolManager peripheralJobPoolManager; + private final DefaultPeripheralJobCleanupApproval defaultPeripheralJobCleanupApproval; + private final CreationTimeThreshold creationTimeThreshold; + + /** + * Creates a new instance. + * + * @param peripheralJobPoolManager The peripheral job pool manager to be used. + * @param defaultPeripheralJobCleanupApproval Checks whether a peripheral job may be removed. + * @param creationTimeThreshold Keeps track of the time used to determine whether a transport + * order should be removed (according to its creation time). + */ + @Inject + public DefaultTransportOrderCleanupApproval( + PeripheralJobPoolManager peripheralJobPoolManager, + DefaultPeripheralJobCleanupApproval defaultPeripheralJobCleanupApproval, + CreationTimeThreshold creationTimeThreshold + ) { + this.peripheralJobPoolManager = requireNonNull( + peripheralJobPoolManager, + "peripheralJobPoolManager" + ); + this.defaultPeripheralJobCleanupApproval + = requireNonNull( + defaultPeripheralJobCleanupApproval, + "defaultPeripheralJobCleanupApproval" + ); + this.creationTimeThreshold = requireNonNull(creationTimeThreshold, "creationTimeThreshold"); + } + + @Override + public boolean test(TransportOrder order) { + if (!order.getState().isFinalState()) { + return false; + } + if (isRelatedToJobWithNonFinalState(order)) { + return false; + } + if (isRelatedToUnapprovedJob(order)) { + return false; + } + if (order.getCreationTime().isAfter(creationTimeThreshold.getCurrentThreshold())) { + return false; + } + return true; + } + + private boolean isRelatedToJobWithNonFinalState(TransportOrder order) { + return peripheralJobPoolManager.getObjectRepo() + .getObjects( + PeripheralJob.class, + job -> Objects.equals(job.getRelatedTransportOrder(), order.getReference()) + ) + .stream() + .filter(job -> !job.getState().isFinalState()) + .findAny() + .isPresent(); + } + + private boolean isRelatedToUnapprovedJob(TransportOrder order) { + return !(peripheralJobPoolManager.getObjectRepo().getObjects( + PeripheralJob.class, + job -> Objects.equals(job.getRelatedTransportOrder(), order.getReference()) + ) + .stream() + .allMatch(defaultPeripheralJobCleanupApproval)); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/NotificationBuffer.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/NotificationBuffer.java new file mode 100644 index 0000000..7f58e23 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/NotificationBuffer.java @@ -0,0 +1,151 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkInRange; + +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import org.opentcs.access.NotificationPublicationEvent; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.notification.UserNotification; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A buffer which can store a (configurable) limited number of + * {@link UserNotification UserNotification} objects. + *

+ * When a new message is added to the buffer and the number of messages in the buffer exceeds its + * capacity, messages are removed from the buffer until it contains not more than + * capacity. + *

+ *

+ * Note that no synchronization is done inside this class. Concurrent access of + * instances of this class must be synchronized externally. + *

+ */ +public class NotificationBuffer { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(NotificationBuffer.class); + /** + * The actual messages. + */ + private final Queue notifications = new ArrayDeque<>(); + /** + * The maximum number of messages that should be kept in this buffer. + */ + private int capacity = 500; + /** + * A listener for events concerning the stored messages. + */ + private final EventHandler messageEventListener; + + /** + * Creates a new instance that uses the given event listener. + * + * @param eventListener The event listener to be used. + */ + @Inject + public NotificationBuffer( + @ApplicationEventBus + EventHandler eventListener + ) { + messageEventListener = requireNonNull(eventListener, "eventListener"); + } + + /** + * Returns this buffer's capacity. + * + * @return This buffer's capacity. + */ + public int getCapacity() { + return capacity; + } + + /** + * Adjusts this buffer's capacity. + * If the new capacity is less than the current number of messages in this + * buffer, messages are removed until the number of messages equals the + * buffer's capacity. + * + * @param capacity The buffer's new capacity. Must be at least 1. + * @throws IllegalArgumentException If newCapacity is less than 1. + */ + public void setCapacity(int capacity) { + this.capacity = checkInRange(capacity, 1, Integer.MAX_VALUE, "capacity"); + cutBackMessages(); + } + + /** + * Adds a notification to the buffer. + * + * @param notification The notification to be added. + */ + public void addNotification(UserNotification notification) { + requireNonNull(notification, "notification"); + + notifications.add(notification); + LOG.info("User notification added: {}", notification); + + // Make sure we don't have too many messages now. + cutBackMessages(); + // Emit an event for this message. + emitMessageEvent(notification); + } + + /** + * Returns all notifications that are accepted by the given filter, or all notifications, if no + * filter is given. + * + * @param predicate The predicate used to filter. May be null to return all + * notifications. + * @return A list of notifications accepted by the given filter. + */ + public List getNotifications( + @Nullable + Predicate predicate + ) { + Predicate filterPredicate + = predicate == null + ? (notification) -> true + : predicate; + + return notifications.stream() + .filter(filterPredicate) + .collect(Collectors.toCollection(ArrayList::new)); + } + + /** + * Removes all messages from this buffer. + */ + public void clear() { + notifications.clear(); + } + + /** + * Emits an event for the given message. + * + * @param message The message to emit an event for. + */ + public void emitMessageEvent(UserNotification message) { + messageEventListener.onEvent(new NotificationPublicationEvent(message)); + } + + private void cutBackMessages() { + while (notifications.size() > capacity) { + notifications.remove(); + } + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/PeripheralJobPoolManager.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/PeripheralJobPoolManager.java new file mode 100644 index 0000000..1a3eea7 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/PeripheralJobPoolManager.java @@ -0,0 +1,239 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import org.opentcs.access.to.peripherals.PeripheralJobCreationTO; +import org.opentcs.access.to.peripherals.PeripheralOperationCreationTO; +import org.opentcs.components.kernel.ObjectNameProvider; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.ObjectExistsException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.data.peripherals.PeripheralOperation; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Keeps all {@code PeripheralJobs}s and provides methods to create and manipulate them. + *

+ * Note that no synchronization is done inside this class. Concurrent access of instances of this + * class must be synchronized externally. + *

+ */ +public class PeripheralJobPoolManager + extends + TCSObjectManager { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PeripheralJobPoolManager.class); + /** + * Provides names for peripheral jobs. + */ + private final ObjectNameProvider objectNameProvider; + + /** + * Creates a new instance. + * + * @param objectRepo The object repo. + * @param eventHandler The event handler to publish events to. + * @param orderNameProvider Provides names for peripheral jobs. + */ + @Inject + public PeripheralJobPoolManager( + @Nonnull + TCSObjectRepository objectRepo, + @Nonnull + @ApplicationEventBus + EventHandler eventHandler, + @Nonnull + ObjectNameProvider orderNameProvider + ) { + super(objectRepo, eventHandler); + this.objectNameProvider = requireNonNull(orderNameProvider, "orderNameProvider"); + } + + /** + * Removes all peripheral jobs from this pool. + */ + public void clear() { + for (PeripheralJob job : getObjectRepo().getObjects(PeripheralJob.class)) { + getObjectRepo().removeObject(job.getReference()); + emitObjectEvent( + null, + job, + TCSObjectEvent.Type.OBJECT_REMOVED + ); + } + } + + /** + * Adds a new peripheral job to the pool. + * + * @param to The transfer object from which to create the new peripheral job. + * @return The newly created peripheral job. + * @throws ObjectExistsException If an object with the new object's name already exists. + * @throws ObjectUnknownException If any object referenced in the TO does not exist. + * @throws IllegalArgumentException If the transfer object's combination of parameters is invalid. + */ + public PeripheralJob createPeripheralJob(PeripheralJobCreationTO to) + throws ObjectUnknownException, + ObjectExistsException, + IllegalArgumentException { + checkArgument( + !hasCompletionRequiredAndExecutionTriggerImmediate(to), + "Peripheral job's operation has executionTrigger 'immediate' and completionRequired set." + ); + + PeripheralJob job = new PeripheralJob( + nameFor(to), + to.getReservationToken(), + toPeripheralOperation(to.getPeripheralOperation()) + ) + .withRelatedVehicle(toVehicleReference(to.getRelatedVehicleName())) + .withRelatedTransportOrder(toTransportOrderReference(to.getRelatedTransportOrderName())) + .withProperties(to.getProperties()); + + LOG.info( + "Peripheral job is being created: {} -- {}", + job.getName(), + job.getPeripheralOperation() + ); + + getObjectRepo().addObject(job); + emitObjectEvent(job, null, TCSObjectEvent.Type.OBJECT_CREATED); + + return job; + } + + /** + * Sets a peripheral jobs's state. + * + * @param ref A reference to the peripheral job to be modified. + * @param newState The peripheral job's new state. + * @return The modified peripheral job. + * @throws ObjectUnknownException If the referenced peripheral job is not in this pool. + */ + public PeripheralJob setPeripheralJobState( + TCSObjectReference ref, + PeripheralJob.State newState + ) + throws ObjectUnknownException { + PeripheralJob previousState = getObjectRepo().getObject(PeripheralJob.class, ref); + + checkArgument( + !previousState.getState().isFinalState(), + "Peripheral job %s already in a final state, not changing %s -> %s.", + ref.getName(), + previousState.getState(), + newState + ); + + LOG.info( + "Peripheral job's state changes: {} -- {} -> {}", + previousState.getName(), + previousState.getState(), + newState + ); + + PeripheralJob job = previousState.withState(newState); + getObjectRepo().replaceObject(job); + emitObjectEvent( + job, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return job; + } + + /** + * Removes the referenced peripheral job from the pool. + * + * @param ref A reference to the peripheral job to be removed. + * @return The removed peripheral job. + * @throws ObjectUnknownException If the referenced peripheral job is not in the pool. + */ + public PeripheralJob removePeripheralJob(TCSObjectReference ref) + throws ObjectUnknownException { + PeripheralJob job = getObjectRepo().getObject(PeripheralJob.class, ref); + // Make sure only jobs in a final state are removed. + checkArgument( + job.getState().isFinalState(), + "Peripheral job %s is not in a final state.", + job.getName() + ); + getObjectRepo().removeObject(ref); + emitObjectEvent( + null, + job, + TCSObjectEvent.Type.OBJECT_REMOVED + ); + return job; + } + + private PeripheralOperation toPeripheralOperation(PeripheralOperationCreationTO to) + throws ObjectUnknownException { + return new PeripheralOperation( + toLocationReference(to.getLocationName()), + to.getOperation(), + to.getExecutionTrigger(), + to.isCompletionRequired() + ); + } + + private TCSResourceReference toLocationReference(String locationName) + throws ObjectUnknownException { + Location location = getObjectRepo().getObject(Location.class, locationName); + return location.getReference(); + } + + private TCSObjectReference toVehicleReference(String vehicleName) + throws ObjectUnknownException { + if (vehicleName == null) { + return null; + } + Vehicle vehicle = getObjectRepo().getObject(Vehicle.class, vehicleName); + return vehicle.getReference(); + } + + private TCSObjectReference toTransportOrderReference(String transportOrderName) + throws ObjectUnknownException { + if (transportOrderName == null) { + return null; + } + TransportOrder order = getObjectRepo().getObject(TransportOrder.class, transportOrderName); + return order.getReference(); + } + + @Nonnull + private String nameFor( + @Nonnull + PeripheralJobCreationTO to + ) { + if (to.hasIncompleteName()) { + return objectNameProvider.apply(to); + } + else { + return to.getName(); + } + } + + private boolean hasCompletionRequiredAndExecutionTriggerImmediate(PeripheralJobCreationTO to) { + PeripheralOperationCreationTO opTo = to.getPeripheralOperation(); + return opTo.isCompletionRequired() + && opTo.getExecutionTrigger() == PeripheralOperation.ExecutionTrigger.IMMEDIATE; + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/PlantModelManager.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/PlantModelManager.java new file mode 100644 index 0000000..c61e760 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/PlantModelManager.java @@ -0,0 +1,1711 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkState; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.access.to.model.BlockCreationTO; +import org.opentcs.access.to.model.BoundingBoxCreationTO; +import org.opentcs.access.to.model.CoupleCreationTO; +import org.opentcs.access.to.model.LocationCreationTO; +import org.opentcs.access.to.model.LocationTypeCreationTO; +import org.opentcs.access.to.model.PathCreationTO; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.access.to.model.PointCreationTO; +import org.opentcs.access.to.model.VehicleCreationTO; +import org.opentcs.access.to.model.VisualLayoutCreationTO; +import org.opentcs.access.to.peripherals.PeripheralOperationCreationTO; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.ObjectExistsException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Block; +import org.opentcs.data.model.BoundingBox; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.PeripheralInformation; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Pose; +import org.opentcs.data.model.TCSResource; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.model.Vehicle.EnergyLevelThresholdSet; +import org.opentcs.data.model.visualization.VisualLayout; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.data.peripherals.PeripheralOperation; +import org.opentcs.drivers.vehicle.LoadHandlingDevice; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Presents a view on the topology of a plant model contained in a + * {@link TCSObjectRepository}. + *

+ * Note that no synchronization is done inside this class. Concurrent access of instances of this + * class must be synchronized externally. + *

+ */ +public class PlantModelManager + extends + TCSObjectManager { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PlantModelManager.class); + /** + * This model's name. + */ + private String name = ""; + /** + * This model's properties. + */ + private Map properties = new HashMap<>(); + + /** + * Creates a new model. + * + * @param objectRepo The object repo. + * @param eventHandler The event handler to publish events to. + */ + @Inject + public PlantModelManager( + @Nonnull + TCSObjectRepository objectRepo, + @Nonnull + @ApplicationEventBus + EventHandler eventHandler + ) { + super(objectRepo, eventHandler); + } + + /** + * Returns this model's name. + * + * @return This model's name. + */ + public String getName() { + return name; + } + + /** + * Sets this model's name. + * + * @param name This model's new name. + */ + public void setName(String name) { + this.name = requireNonNull(name, "name"); + } + + /** + * Returns this model's properties. + * + * @return This model's properties. + */ + public Map getProperties() { + return properties; + } + + /** + * Sets this model's properties. + * + * @param properties The properties. + */ + public void setProperties(Map properties) { + this.properties = requireNonNull(properties, "properties"); + } + + /** + * Removes all model objects from this model and the object pool by which it is backed. + */ + public void clear() { + List> objects = new ArrayList<>(); + objects.addAll(getObjectRepo().getObjects(VisualLayout.class)); + objects.addAll(getObjectRepo().getObjects(Vehicle.class)); + objects.addAll(getObjectRepo().getObjects(Block.class)); + objects.addAll(getObjectRepo().getObjects(Path.class)); + objects.addAll(getObjectRepo().getObjects(Location.class)); + objects.addAll(getObjectRepo().getObjects(LocationType.class)); + objects.addAll(getObjectRepo().getObjects(Point.class)); + + for (TCSObject curObject : objects) { + getObjectRepo().removeObject(curObject.getReference()); + emitObjectEvent( + null, + curObject, + TCSObjectEvent.Type.OBJECT_REMOVED + ); + } + } + + /** + * Creates new plant model objects with unique IDs and all other attributes taken from the given + * transfer object. + * + * @param to The transfer object from which to create the new objects. + * @throws ObjectExistsException If an object with a new object's name already exists. + * @throws ObjectUnknownException If any object referenced in the TO does not exist. + */ + public void createPlantModelObjects(PlantModelCreationTO to) + throws ObjectExistsException, + ObjectUnknownException { + LOG.info("Plant model is being created: {}", to.getName()); + + clear(); + setName(to.getName()); + setProperties(to.getProperties()); + + for (PointCreationTO point : to.getPoints()) { + createPoint(point); + } + for (LocationTypeCreationTO locType : to.getLocationTypes()) { + createLocationType(locType); + } + for (LocationCreationTO loc : to.getLocations()) { + createLocation(loc); + } + for (PathCreationTO path : to.getPaths()) { + createPath(path); + } + + for (BlockCreationTO block : to.getBlocks()) { + createBlock(block); + } + for (VehicleCreationTO vehicle : to.getVehicles()) { + createVehicle(vehicle); + } + + createVisualLayout(to.getVisualLayout()); + } + + /** + * Locks/Unlocks a path. + * + * @param ref A reference to the path to be modified. + * @param newLocked If true, this path will be locked when the + * method call returns; if false, this path will be unlocked. + * @return The modified path. + * @throws ObjectUnknownException If the referenced path does not exist. + */ + public Path setPathLocked(TCSObjectReference ref, boolean newLocked) + throws ObjectUnknownException { + Path previousState = getObjectRepo().getObject(Path.class, ref); + + LOG.debug( + "Path's locked state changes: {} -- {} -> {}", + previousState.getName(), + previousState.isLocked(), + newLocked + ); + + Path path = previousState.withLocked(newLocked); + getObjectRepo().replaceObject(path.withLocked(newLocked)); + emitObjectEvent( + path, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return path; + } + + /** + * Locks/Unlocks a location. + * + * @param ref A reference to the location to be modified. + * @param newLocked If {@code true}, this path will be locked when the method call returns; + * if {@code false}, this path will be unlocked. + * @return The modified location. + * @throws ObjectUnknownException If the referenced location does not exist. + */ + public Location setLocationLocked(TCSObjectReference ref, boolean newLocked) + throws ObjectUnknownException { + Location previousState = getObjectRepo().getObject(Location.class, ref); + + LOG.debug( + "Location's locked state changes: {} -- {} -> {}", + previousState.getName(), + previousState.isLocked(), + newLocked + ); + + Location location = previousState.withLocked(newLocked); + getObjectRepo().replaceObject(location); + emitObjectEvent( + location, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return location; + } + + /** + * Sets a location's reservation token. + * + * @param ref A reference to the location to be modified. + * @param newToken The new reservation token. + * @return The modified location. + * @throws ObjectUnknownException If the referenced location does not exist. + */ + public Location setLocationReservationToken(TCSObjectReference ref, String newToken) + throws ObjectUnknownException { + Location previousState = getObjectRepo().getObject(Location.class, ref); + + LOG.debug( + "Location's reservation token changes: {} -- {} -> {}", + previousState.getName(), + previousState.getPeripheralInformation().getReservationToken(), + newToken + ); + + Location location = previousState.withPeripheralInformation( + previousState.getPeripheralInformation().withReservationToken(newToken) + ); + getObjectRepo().replaceObject(location); + emitObjectEvent( + location, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return location; + } + + /** + * Sets a location's processing state. + * + * @param ref A reference to the location to be modified. + * @param newState The new processing state. + * @return The modified location. + * @throws ObjectUnknownException If the referenced location does not exist. + */ + public Location setLocationProcState( + TCSObjectReference ref, + PeripheralInformation.ProcState newState + ) + throws ObjectUnknownException { + Location previousState = getObjectRepo().getObject(Location.class, ref); + + LOG.debug( + "Location's proc state changes: {} -- {} -> {}", + previousState.getName(), + previousState.getPeripheralInformation().getProcState(), + newState + ); + + Location location = previousState.withPeripheralInformation( + previousState.getPeripheralInformation().withProcState(newState) + ); + getObjectRepo().replaceObject(location); + emitObjectEvent( + location, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return location; + } + + /** + * Sets a location's state. + * + * @param ref A reference to the location to be modified. + * @param newState The new state. + * @return The modified location. + * @throws ObjectUnknownException If the referenced location does not exist. + */ + public Location setLocationState( + TCSObjectReference ref, + PeripheralInformation.State newState + ) + throws ObjectUnknownException { + Location previousState = getObjectRepo().getObject(Location.class, ref); + + LOG.debug( + "Location's state changes: {} -- {} -> {}", + previousState.getName(), + previousState.getPeripheralInformation().getState(), + newState + ); + + Location location = previousState.withPeripheralInformation( + previousState.getPeripheralInformation().withState(newState) + ); + getObjectRepo().replaceObject(location); + emitObjectEvent( + location, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return location; + } + + /** + * Sets a location's peripheral job. + * + * @param ref A reference to the location to be modified. + * @param newJob The new peripheral job. + * @return The modified location. + * @throws ObjectUnknownException If the referenced location does not exist. + */ + public Location setLocationPeripheralJob( + TCSObjectReference ref, + TCSObjectReference newJob + ) + throws ObjectUnknownException { + Location previousState = getObjectRepo().getObject(Location.class, ref); + + LOG.debug( + "Location's peripheral job changes: {} -- {} -> {}", + previousState.getName(), + previousState.getPeripheralInformation().getPeripheralJob(), + newJob + ); + + Location location = previousState.withPeripheralInformation( + previousState.getPeripheralInformation().withPeripheralJob(newJob) + ); + getObjectRepo().replaceObject(location); + emitObjectEvent( + location, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return location; + } + + /** + * Sets a vehicle's energy level. + * + * @param ref A reference to the vehicle to be modified. + * @param energyLevel The vehicle's new energy level. + * @return The modified vehicle. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + public Vehicle setVehicleEnergyLevel( + TCSObjectReference ref, + int energyLevel + ) + throws ObjectUnknownException { + Vehicle previousState = getObjectRepo().getObject(Vehicle.class, ref); + + LOG.debug( + "Vehicle's energy level changes: {} -- {} -> {}", + previousState.getName(), + previousState.getEnergyLevel(), + energyLevel + ); + + Vehicle vehicle = previousState.withEnergyLevel(energyLevel); + getObjectRepo().replaceObject(vehicle); + emitObjectEvent( + vehicle, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return vehicle; + } + + /** + * Sets the energy level threshold set for a given vehicle. + * + * @param ref Reference to the vehicle. + * @param energyLevelThresholdSet The energy level threshold set. + * @return The modified vehicle. + * @throws ObjectUnknownException The vehicle reference is not known. + */ + public Vehicle setVehicleEnergyLevelThresholdSet( + TCSObjectReference ref, + EnergyLevelThresholdSet energyLevelThresholdSet + ) + throws ObjectUnknownException { + Vehicle previousState = getObjectRepo().getObject(Vehicle.class, ref); + + LOG.info( + "Vehicle's energy level threshold set changes: {} -- {} -> {}", + previousState.getName(), + previousState.getEnergyLevelThresholdSet(), + energyLevelThresholdSet + ); + + Vehicle vehicle = previousState.withEnergyLevelThresholdSet(energyLevelThresholdSet); + getObjectRepo().replaceObject(vehicle); + emitObjectEvent( + vehicle, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return vehicle; + } + + /** + * Sets a vehicle's recharge operation. + * + * @param ref A reference to the vehicle to be modified. + * @param rechargeOperation The vehicle's new recharge operation. + * @return The modified vehicle. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + public Vehicle setVehicleRechargeOperation( + TCSObjectReference ref, + String rechargeOperation + ) + throws ObjectUnknownException { + Vehicle previousState = getObjectRepo().getObject(Vehicle.class, ref); + + LOG.info( + "Vehicle's recharge operation changes: {} -- {} -> {}", + previousState.getName(), + previousState.getRechargeOperation(), + rechargeOperation + ); + + Vehicle vehicle = previousState.withRechargeOperation(rechargeOperation); + getObjectRepo().replaceObject(vehicle); + emitObjectEvent( + vehicle, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return vehicle; + } + + /** + * Sets a vehicle's load handling devices. + * + * @param ref A reference to the vehicle to be modified. + * @param devices The vehicle's new load handling devices. + * @return The modified vehicle. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + public Vehicle setVehicleLoadHandlingDevices( + TCSObjectReference ref, + List devices + ) + throws ObjectUnknownException { + Vehicle previousState = getObjectRepo().getObject(Vehicle.class, ref); + + LOG.debug( + "Vehicle's load handling devices change: {} -- {} -> {}", + previousState.getName(), + previousState.getLoadHandlingDevices(), + devices + ); + + Vehicle vehicle = previousState.withLoadHandlingDevices(devices); + getObjectRepo().replaceObject(vehicle); + emitObjectEvent( + vehicle, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return vehicle; + } + + /** + * Sets a vehicle's state. + * + * @param ref A reference to the vehicle to be modified. + * @param newState The vehicle's new state. + * @return The modified vehicle. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + public Vehicle setVehicleState( + TCSObjectReference ref, + Vehicle.State newState + ) + throws ObjectUnknownException { + Vehicle previousState = getObjectRepo().getObject(Vehicle.class, ref); + + LOG.debug( + "Vehicle's state changes: {} -- {} -> {}", + previousState.getName(), + previousState.getState(), + newState + ); + + Vehicle vehicle = previousState.withState(newState); + getObjectRepo().replaceObject(vehicle); + emitObjectEvent( + vehicle, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return vehicle; + } + + /** + * Sets a vehicle integration level. + * + * @param ref A reference to the vehicle to be modified. + * @param integrationLevel The vehicle's new integration level. + * @return The modified vehicle. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + public Vehicle setVehicleIntegrationLevel( + TCSObjectReference ref, + Vehicle.IntegrationLevel integrationLevel + ) + throws ObjectUnknownException { + Vehicle previousState = getObjectRepo().getObject(Vehicle.class, ref); + + LOG.info( + "Vehicle's integration level changes: {} -- {} -> {}", + previousState.getName(), + previousState.getIntegrationLevel(), + integrationLevel + ); + + Vehicle vehicle = previousState.withIntegrationLevel(integrationLevel); + getObjectRepo().replaceObject(vehicle); + emitObjectEvent( + vehicle, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return vehicle; + } + + /** + * Sets a vehicle's paused state. + * + * @param ref A reference to the vehicle to be modified. + * @param paused The vehicle's new paused state. + * @return The modified vehicle. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + public Vehicle setVehiclePaused( + TCSObjectReference ref, + boolean paused + ) + throws ObjectUnknownException { + Vehicle previousState = getObjectRepo().getObject(Vehicle.class, ref); + + LOG.info( + "Vehicle's paused state changes: {} -- {} -> {}", + previousState.getName(), + previousState.isPaused(), + paused + ); + + Vehicle vehicle = previousState.withPaused(paused); + getObjectRepo().replaceObject(vehicle); + emitObjectEvent( + vehicle, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return vehicle; + } + + /** + * Sets a vehicle's processing state. + * + * @param ref A reference to the vehicle to be modified. + * @param newState The vehicle's new processing state. + * @return The modified vehicle. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + public Vehicle setVehicleProcState( + TCSObjectReference ref, + Vehicle.ProcState newState + ) + throws ObjectUnknownException { + Vehicle previousState = getObjectRepo().getObject(Vehicle.class, ref); + + LOG.debug( + "Vehicle's proc state changes: {} -- {} -> {}", + previousState.getName(), + previousState.getProcState(), + newState + ); + + Vehicle vehicle = previousState.withProcState(newState); + getObjectRepo().replaceObject(vehicle); + emitObjectEvent( + vehicle, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return vehicle; + } + + /** + * Sets the allowed order types for a given vehicle. + * + * @param ref Reference to the vehicle. + * @param allowedOrderTypes Set of allowed order types. + * @return The vehicle with the allowed order types. + * @throws ObjectUnknownException The vehicle reference is not known. + */ + public Vehicle setVehicleAllowedOrderTypes( + TCSObjectReference ref, + Set allowedOrderTypes + ) + throws ObjectUnknownException { + Vehicle previousState = getObjectRepo().getObject(Vehicle.class, ref); + + LOG.info( + "Vehicle's allowed order types change: {} -- {} -> {}", + previousState.getName(), + previousState.getAllowedOrderTypes(), + allowedOrderTypes + ); + + Vehicle vehicle = previousState.withAllowedOrderTypes(allowedOrderTypes); + getObjectRepo().replaceObject(vehicle); + emitObjectEvent( + vehicle, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return vehicle; + } + + /** + * Sets a vehicle's envelope key. + * + * @param ref A reference to the vehicle to be modified. + * @param envelopeKey The vehicle's new envelope key. + * @return The modified vehicle. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + public Vehicle setVehicleEnvelopeKey( + TCSObjectReference ref, + String envelopeKey + ) + throws ObjectUnknownException { + Vehicle previousState = getObjectRepo().getObject(Vehicle.class, ref); + + LOG.info( + "Vehicle's envelope key change: {} -- {} -> {}", + previousState.getName(), + previousState.getEnvelopeKey(), + envelopeKey + ); + + Vehicle vehicle = previousState.withEnvelopeKey(envelopeKey); + getObjectRepo().replaceObject(vehicle); + emitObjectEvent( + vehicle, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return vehicle; + } + + /** + * Sets a vehicle's bounding box. + * + * @param ref A reference to the vehicle to be modified. + * @param boundingBox The vehicle's new bounding box. + * @return The modified vehicle. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + public Vehicle setVehicleBoundingBox( + TCSObjectReference ref, + BoundingBox boundingBox + ) + throws ObjectUnknownException { + Vehicle previousState = getObjectRepo().getObject(Vehicle.class, ref); + + LOG.debug( + "Vehicle's bounding box change: {} -- {} -> {}", + previousState.getName(), + previousState.getBoundingBox(), + boundingBox + ); + + Vehicle vehicle = previousState.withBoundingBox(boundingBox); + getObjectRepo().replaceObject(vehicle); + emitObjectEvent( + vehicle, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return vehicle; + } + + /** + * Sets a vehicle's position. + * + * @param ref A reference to the vehicle to be modified. + * @param newPosRef A reference to the point the vehicle is occupying. + * @return The modified vehicle. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + public Vehicle setVehiclePosition( + TCSObjectReference ref, + TCSObjectReference newPosRef + ) + throws ObjectUnknownException { + Vehicle vehicle = getObjectRepo().getObject(Vehicle.class, ref); + + LOG.debug( + "Vehicle's position changes: {} -- {} -> {}", + vehicle.getName(), + vehicle.getCurrentPosition() == null ? null : vehicle.getCurrentPosition().getName(), + newPosRef == null ? null : newPosRef.getName() + ); + + Vehicle previousVehicleState = vehicle; + // If the vehicle was occupying a point before, clear it and send an event. + if (vehicle.getCurrentPosition() != null) { + Point oldVehiclePos = getObjectRepo().getObject(Point.class, vehicle.getCurrentPosition()); + Point previousPointState = oldVehiclePos; + oldVehiclePos = oldVehiclePos.withOccupyingVehicle(null); + getObjectRepo().replaceObject(oldVehiclePos); + emitObjectEvent( + oldVehiclePos, + previousPointState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + } + // If the vehicle is occupying a point now, set that and send an event. + if (newPosRef != null) { + Point newVehiclePos = getObjectRepo().getObject(Point.class, newPosRef); + Point previousPointState = newVehiclePos; + newVehiclePos = newVehiclePos.withOccupyingVehicle(ref); + getObjectRepo().replaceObject(newVehiclePos); + emitObjectEvent( + newVehiclePos, + previousPointState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + } + vehicle = vehicle.withCurrentPosition(newPosRef); + getObjectRepo().replaceObject(vehicle); + emitObjectEvent( + vehicle, + previousVehicleState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + + return vehicle; + } + + /** + * Sets a vehicle's next position. + * + * @param ref A reference to the vehicle to be modified. + * @param newPosition A reference to the point the vehicle is expected to + * occupy next. + * @return The modified vehicle. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + public Vehicle setVehicleNextPosition( + TCSObjectReference ref, + TCSObjectReference newPosition + ) + throws ObjectUnknownException { + Vehicle previousState = getObjectRepo().getObject(Vehicle.class, ref); + + LOG.debug( + "Vehicle's next position changes: {} -- {} -> {}", + previousState.getName(), + previousState.getNextPosition(), + newPosition + ); + + Vehicle vehicle = previousState.withNextPosition(newPosition); + getObjectRepo().replaceObject(vehicle); + emitObjectEvent( + vehicle, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return vehicle; + } + + /** + * Sets a vehicle's pose. + * + * @param ref A reference to the vehicle to be modified. + * @param pose The vehicle's pose. + * @return The modified vehicle. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + public Vehicle setVehiclePose( + TCSObjectReference ref, + @Nonnull + Pose pose + ) + throws ObjectUnknownException { + requireNonNull(pose, "pose"); + + Vehicle previousState = getObjectRepo().getObject(Vehicle.class, ref); + + LOG.trace( + "Vehicle's pose changes: {} -- {} -> {}", + previousState.getName(), + previousState.getPose(), + pose + ); + + Vehicle vehicle = previousState.withPose(pose); + getObjectRepo().replaceObject(vehicle); + emitObjectEvent( + vehicle, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return vehicle; + } + + /** + * Sets a vehicle's transport order. + * + * @param vehicleRef A reference to the vehicle to be modified. + * @param orderRef A reference to the transport order the vehicle processes. + * @return The modified vehicle. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + public Vehicle setVehicleTransportOrder( + TCSObjectReference vehicleRef, + TCSObjectReference orderRef + ) + throws ObjectUnknownException { + Vehicle vehicle = getObjectRepo().getObject(Vehicle.class, vehicleRef); + Vehicle previousState = vehicle; + + LOG.debug( + "Vehicle's transport order changes: {} -- {} -> {}", + previousState.getName(), + previousState.getTransportOrder(), + orderRef + ); + + if (orderRef == null) { + vehicle = vehicle.withTransportOrder(null); + getObjectRepo().replaceObject(vehicle); + } + else { + TransportOrder order = getObjectRepo().getObject(TransportOrder.class, orderRef); + vehicle = vehicle.withTransportOrder(order.getReference()); + getObjectRepo().replaceObject(vehicle); + } + emitObjectEvent( + vehicle, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return vehicle; + } + + /** + * Sets a vehicle's order sequence. + * + * @param vehicleRef A reference to the vehicle to be modified. + * @param seqRef A reference to the order sequence the vehicle processes. + * @return The modified vehicle. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + public Vehicle setVehicleOrderSequence( + TCSObjectReference vehicleRef, + TCSObjectReference seqRef + ) + throws ObjectUnknownException { + Vehicle vehicle = getObjectRepo().getObject(Vehicle.class, vehicleRef); + Vehicle previousState = vehicle; + + LOG.debug( + "Vehicle's order sequence changes: {} -- {} -> {}", + previousState.getName(), + previousState.getOrderSequence(), + seqRef + ); + + if (seqRef == null) { + vehicle = vehicle.withOrderSequence(null); + getObjectRepo().replaceObject(vehicle); + } + else { + OrderSequence seq = getObjectRepo().getObject(OrderSequence.class, seqRef); + vehicle = vehicle.withOrderSequence(seq.getReference()); + getObjectRepo().replaceObject(vehicle); + } + emitObjectEvent( + vehicle, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return vehicle; + } + + /** + * Sets a vehicle's claimed resources. + * + * @param vehicleRef A reference to the vehicle to be modified. + * @param resources The new resources. + * @return The modified vehicle. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + public Vehicle setVehicleClaimedResources( + TCSObjectReference vehicleRef, + List>> resources + ) + throws ObjectUnknownException { + Vehicle previousState = getObjectRepo().getObject(Vehicle.class, vehicleRef); + + LOG.debug( + "Vehicle's claimed resources change: {} -- {} -> {}", + previousState.getName(), + previousState.getClaimedResources(), + resources + ); + + Vehicle vehicle = previousState.withClaimedResources(unmodifiableCopy(resources)); + getObjectRepo().replaceObject(vehicle); + emitObjectEvent( + vehicle, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return vehicle; + } + + /** + * Sets a vehicle's allocated resources. + * + * @param vehicleRef A reference to the vehicle to be modified. + * @param resources The new resources. + * @return The modified vehicle. + * @throws ObjectUnknownException If the referenced vehicle does not exist. + */ + public Vehicle setVehicleAllocatedResources( + TCSObjectReference vehicleRef, + List>> resources + ) + throws ObjectUnknownException { + Vehicle previousState = getObjectRepo().getObject(Vehicle.class, vehicleRef); + + LOG.debug( + "Vehicle's allocated resources change: {} -- {} -> {}", + previousState.getName(), + previousState.getAllocatedResources(), + resources + ); + + Vehicle vehicle = previousState.withAllocatedResources(unmodifiableCopy(resources)); + getObjectRepo().replaceObject(vehicle); + emitObjectEvent( + vehicle, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return vehicle; + } + + /** + * Returns a PlantModelCreationTO for this model. + * + * @return A PlantModelCreationTO for this model. + */ + public PlantModelCreationTO createPlantModelCreationTO() { + return new PlantModelCreationTO(name) + .withProperties(getProperties()) + .withPoints(getPoints()) + .withPaths(getPaths()) + .withVehicles(getVehicles()) + .withLocationTypes(getLocationTypes()) + .withLocations(getLocations()) + .withBlocks(getBlocks()) + .withVisualLayout(getVisualLayout()); + } + + /** + * Expands a set of resources A to a set of resources B. + * B contains the resources in A with blocks expanded to + * their actual members. + * The given set is not modified. + * + * @param resources The set of resources to be expanded. + * @return The given set with resources expanded. + * @throws ObjectUnknownException If an object referenced in the given set + * does not exist. + */ + public Set> expandResources( + @Nonnull + Set> resources + ) + throws ObjectUnknownException { + requireNonNull(resources, "resources"); + + Set blocks = getObjectRepo().getObjects(Block.class); + + // First, collect the given references plus references to all members of blocks that contain the + // given references in a set. + // We could look up all resources and add them to the result immediately, but by first + // collecting all references, we ensure that we look up each resource only once. + Set> refsToLookUp = new HashSet<>(); + for (TCSResourceReference resourceRef : resources) { + refsToLookUp.add(resourceRef); + + blocks.stream() + .filter(block -> block.getMembers().contains(resourceRef)) + .flatMap(block -> block.getMembers().stream()) + .forEach(memberRef -> refsToLookUp.add(memberRef)); + } + + // Look up and return the actual resources. + return refsToLookUp.stream() + .map(memberRef -> (TCSResource) getObjectRepo().getObject(memberRef)) + .collect(Collectors.toSet()); + } + + private List mapPeripheralOperationTOs( + List creationTOs + ) { + return creationTOs.stream() + .map( + operationTO -> new PeripheralOperation( + getObjectRepo().getObject( + Location.class, + operationTO.getLocationName() + ).getReference(), + operationTO.getOperation(), + operationTO.getExecutionTrigger(), + operationTO.isCompletionRequired() + ) + ) + .collect(Collectors.toList()); + } + + /** + * Returns a list of {@link PointCreationTO Points} for all points in a model. + * + * @return A list of {@link PointCreationTO Points} for all points in a model. + */ + private List getPoints() { + Set points = getObjectRepo().getObjects(Point.class); + List result = new ArrayList<>(); + + for (Point curPoint : points) { + result.add( + new PointCreationTO(curPoint.getName()) + .withPose( + new Pose( + curPoint.getPose().getPosition(), + curPoint.getPose().getOrientationAngle() + ) + ) + .withType(curPoint.getType()) + .withVehicleEnvelopes(curPoint.getVehicleEnvelopes()) + .withMaxVehicleBoundingBox( + new BoundingBoxCreationTO( + curPoint.getMaxVehicleBoundingBox().getLength(), + curPoint.getMaxVehicleBoundingBox().getWidth(), + curPoint.getMaxVehicleBoundingBox().getHeight() + ) + .withReferenceOffset( + new CoupleCreationTO( + curPoint.getMaxVehicleBoundingBox().getReferenceOffset().getX(), + curPoint.getMaxVehicleBoundingBox().getReferenceOffset().getY() + ) + ) + ) + .withProperties(curPoint.getProperties()) + .withLayout( + new PointCreationTO.Layout( + curPoint.getLayout().getPosition(), + curPoint.getLayout().getLabelOffset(), + curPoint.getLayout().getLayerId() + ) + ) + ); + } + + return result; + } + + /** + * Returns a list of {@link PathCreationTO Paths} for all paths in a model. + * + * @param model The model data. + * @return A list of {@link PathCreationTO Paths} for all paths in a model. + */ + private List getPaths() { + Set paths = getObjectRepo().getObjects(Path.class); + List result = new ArrayList<>(); + + for (Path curPath : paths) { + result.add( + new PathCreationTO( + curPath.getName(), + curPath.getSourcePoint().getName(), + curPath.getDestinationPoint().getName() + ) + .withLength(curPath.getLength()) + .withMaxVelocity(curPath.getMaxVelocity()) + .withMaxReverseVelocity(curPath.getMaxReverseVelocity()) + .withLocked(curPath.isLocked()) + .withPeripheralOperations(getPeripheralOperations(curPath)) + .withVehicleEnvelopes(curPath.getVehicleEnvelopes()) + .withProperties(curPath.getProperties()) + .withLayout( + new PathCreationTO.Layout( + curPath.getLayout().getConnectionType(), + curPath.getLayout().getControlPoints(), + curPath.getLayout().getLayerId() + ) + ) + ); + } + + return result; + } + + private List getPeripheralOperations(Path path) { + return path.getPeripheralOperations().stream() + .map( + op -> new PeripheralOperationCreationTO(op.getOperation(), op.getLocation().getName()) + .withExecutionTrigger(op.getExecutionTrigger()) + .withCompletionRequired(op.isCompletionRequired()) + ) + .collect(Collectors.toList()); + } + + /** + * Returns a list of {@link VehicleCreationTO Vehicles} for all vehicles in a model. + * + * @param model The model data. + * @return A list of {@link VehicleCreationTO Vehicles} for all vehicles in a model. + */ + private List getVehicles() { + Set vehicles = getObjectRepo().getObjects(Vehicle.class); + List result = new ArrayList<>(); + + for (Vehicle vehicle : vehicles) { + result.add( + new VehicleCreationTO(vehicle.getName()) + .withBoundingBox( + new BoundingBoxCreationTO( + vehicle.getBoundingBox().getLength(), + vehicle.getBoundingBox().getWidth(), + vehicle.getBoundingBox().getHeight() + ) + .withReferenceOffset( + new CoupleCreationTO( + vehicle.getBoundingBox().getReferenceOffset().getX(), + vehicle.getBoundingBox().getReferenceOffset().getY() + ) + ) + ) + .withEnergyLevelThresholdSet( + new VehicleCreationTO.EnergyLevelThresholdSet( + vehicle.getEnergyLevelThresholdSet().getEnergyLevelCritical(), + vehicle.getEnergyLevelThresholdSet().getEnergyLevelGood(), + vehicle.getEnergyLevelThresholdSet().getEnergyLevelSufficientlyRecharged(), + vehicle.getEnergyLevelThresholdSet().getEnergyLevelFullyRecharged() + ) + ) + .withMaxVelocity(vehicle.getMaxVelocity()) + .withMaxReverseVelocity(vehicle.getMaxReverseVelocity()) + .withEnvelopeKey(vehicle.getEnvelopeKey()) + .withProperties(vehicle.getProperties()) + .withLayout(new VehicleCreationTO.Layout(vehicle.getLayout().getRouteColor())) + ); + } + + return result; + } + + /** + * Returns a list of {@link LocationTypeCreationTO LocationTypes} for all location types in a + * model. + * + * @param model The model data. + * @return A list of {@link LocationTypeCreationTO LocationTypes} for all location types in a + * model. + */ + private List getLocationTypes() { + Set locTypes = getObjectRepo().getObjects(LocationType.class); + List result = new ArrayList<>(); + + for (LocationType curType : locTypes) { + result.add( + new LocationTypeCreationTO(curType.getName()) + .withAllowedOperations(curType.getAllowedOperations()) + .withAllowedPeripheralOperations(curType.getAllowedPeripheralOperations()) + .withProperties(curType.getProperties()) + .withLayout( + new LocationTypeCreationTO.Layout(curType.getLayout().getLocationRepresentation()) + ) + ); + } + + return result; + } + + /** + * Returns a list of {@link LocationCreationTO Locations} for all locations in a model. + * + * @param model The model data. + * @return A list of {@link LocationCreationTO Locations} for all locations in a model. + */ + private List getLocations() { + Set locations = getObjectRepo().getObjects(Location.class); + List result = new ArrayList<>(); + + for (Location curLoc : locations) { + result.add( + new LocationCreationTO( + curLoc.getName(), + curLoc.getType().getName(), + curLoc.getPosition() + ) + .withLinks( + curLoc.getAttachedLinks().stream() + .collect( + Collectors.toMap( + link -> link.getPoint().getName(), + Location.Link::getAllowedOperations + ) + ) + ) + .withLocked(curLoc.isLocked()) + .withProperties(curLoc.getProperties()) + .withLayout( + new LocationCreationTO.Layout( + curLoc.getLayout().getPosition(), + curLoc.getLayout().getLabelOffset(), + curLoc.getLayout().getLocationRepresentation(), + curLoc.getLayout().getLayerId() + ) + ) + ); + } + + return result; + } + + /** + * Returns a list of {@link BlockCreationTO Blocks} for all blocks in a model. + * + * @param model The model data. + * @return A list of {@link BlockCreationTO Blocks} for all blocks in a model. + */ + private List getBlocks() { + Set blocks = getObjectRepo().getObjects(Block.class); + List result = new ArrayList<>(); + + for (Block curBlock : blocks) { + result.add( + new BlockCreationTO(curBlock.getName()) + .withMemberNames( + curBlock.getMembers().stream() + .map(member -> member.getName()) + .collect(Collectors.toSet()) + ) + .withType(curBlock.getType()) + .withProperties(curBlock.getProperties()) + .withLayout(new BlockCreationTO.Layout(curBlock.getLayout().getColor())) + ); + } + + return result; + } + + /** + * Returns a {@link VisualLayoutCreationTO} for the visual layouts in a model. + * + * @param model The model data. + * @return A {@link VisualLayoutCreationTO} for the visual layouts in a model. + */ + private VisualLayoutCreationTO getVisualLayout() { + Set layouts = getObjectRepo().getObjects(VisualLayout.class); + checkState( + layouts.size() == 1, + "There has to be one, and only one, visual layout. Number of visual layouts: %d", + layouts.size() + ); + VisualLayout layout = layouts.iterator().next(); + + return new VisualLayoutCreationTO(layout.getName()) + .withScaleX(layout.getScaleX()) + .withScaleY(layout.getScaleY()) + .withProperties(layout.getProperties()) + .withLayers(layout.getLayers()) + .withLayerGroups(layout.getLayerGroups()); + } + + /** + * Creates a new visual layout with a unique name and all other attributes set + * to default values. + * + * @param to The transfer object from which to create the new layout. + * @return The newly created layout. + * @throws ObjectExistsException If an object with the new object's name already exists. + * @throws ObjectUnknownException If any object referenced in the TO does not exist. + */ + private VisualLayout createVisualLayout(VisualLayoutCreationTO to) + throws ObjectUnknownException, + ObjectExistsException { + VisualLayout newLayout = new VisualLayout(to.getName()) + .withScaleX(to.getScaleX()) + .withScaleY(to.getScaleY()) + .withLayers(to.getLayers()) + .withLayerGroups(to.getLayerGroups()); + + getObjectRepo().addObject(newLayout); + emitObjectEvent( + newLayout, + null, + TCSObjectEvent.Type.OBJECT_CREATED + ); + // Return the newly created layout. + return newLayout; + } + + /** + * Creates a new point with a unique name and all other attributes set to + * default values. + * + * @param to The transfer object from which to create the new point. + * @return The newly created point. + * @throws ObjectExistsException If an object with the point's name already exists. + */ + private Point createPoint(PointCreationTO to) + throws ObjectExistsException { + // Get a unique ID for the new point and create an instance. + Point newPoint = new Point(to.getName()) + .withPose(new Pose(to.getPose().getPosition(), to.getPose().getOrientationAngle())) + .withType(to.getType()) + .withVehicleEnvelopes(to.getVehicleEnvelopes()) + .withMaxVehicleBoundingBox( + new BoundingBox( + to.getMaxVehicleBoundingBox().getLength(), + to.getMaxVehicleBoundingBox().getWidth(), + to.getMaxVehicleBoundingBox().getHeight() + ) + .withReferenceOffset( + new Couple( + to.getMaxVehicleBoundingBox().getReferenceOffset().getX(), + to.getMaxVehicleBoundingBox().getReferenceOffset().getY() + ) + ) + ) + .withProperties(to.getProperties()) + .withLayout( + new Point.Layout( + to.getLayout().getPosition(), + to.getLayout().getLabelOffset(), + to.getLayout().getLayerId() + ) + ); + getObjectRepo().addObject(newPoint); + emitObjectEvent(newPoint, null, TCSObjectEvent.Type.OBJECT_CREATED); + // Return the newly created point. + return newPoint; + } + + /** + * Creates a new path from the given transfer object. + * + * @param to The transfer object from which to create the new path. + * @return The newly created path. + * @throws ObjectUnknownException If the referenced point does not exist. + * @throws ObjectExistsException If an object with the same name as the path already exists. + */ + private Path createPath(PathCreationTO to) + throws ObjectUnknownException, + ObjectExistsException { + requireNonNull(to, "to"); + + Point srcPoint = getObjectRepo().getObject(Point.class, to.getSrcPointName()); + Point destPoint = getObjectRepo().getObject(Point.class, to.getDestPointName()); + Path newPath = new Path( + to.getName(), + srcPoint.getReference(), + destPoint.getReference() + ) + .withLength(to.getLength()) + .withMaxVelocity(to.getMaxVelocity()) + .withMaxReverseVelocity(to.getMaxReverseVelocity()) + .withPeripheralOperations(mapPeripheralOperationTOs(to.getPeripheralOperations())) + .withVehicleEnvelopes(to.getVehicleEnvelopes()) + .withProperties(to.getProperties()) + .withLocked(to.isLocked()) + .withLayout( + new Path.Layout( + to.getLayout().getConnectionType(), + to.getLayout().getControlPoints(), + to.getLayout().getLayerId() + ) + ); + + getObjectRepo().addObject(newPath); + + emitObjectEvent( + newPath, + null, + TCSObjectEvent.Type.OBJECT_CREATED + ); + + addPointOutgoingPath(srcPoint.getReference(), newPath.getReference()); + addPointIncomingPath(destPoint.getReference(), newPath.getReference()); + + return newPath; + } + + /** + * Creates a new location type with a unique name and all other attributes set + * to their default values. + * + * @param to The transfer object from which to create the new location type. + * @return The newly created location type. + * @throws ObjectExistsException If an object with the new object's name already exists. + */ + private LocationType createLocationType(LocationTypeCreationTO to) + throws ObjectExistsException { + LocationType newType = new LocationType(to.getName()) + .withAllowedOperations(to.getAllowedOperations()) + .withAllowedPeripheralOperations(to.getAllowedPeripheralOperations()) + .withProperties(to.getProperties()) + .withLayout(new LocationType.Layout(to.getLayout().getLocationRepresentation())); + getObjectRepo().addObject(newType); + emitObjectEvent( + newType, + null, + TCSObjectEvent.Type.OBJECT_CREATED + ); + return newType; + } + + /** + * Creates a new location with a unique name and all other attributes set to + * default values. + * + * @param to The transfer object from which to create the new location type. + * @return The newly created location. + * @throws ObjectExistsException If an object with the new object's name already exists. + * @throws ObjectUnknownException If any object referenced in the TO does not exist. + */ + private Location createLocation(LocationCreationTO to) + throws ObjectUnknownException, + ObjectExistsException { + LocationType type = getObjectRepo().getObject(LocationType.class, to.getTypeName()); + Location newLocation = new Location(to.getName(), type.getReference()) + .withPosition(to.getPosition()) + .withLocked(to.isLocked()) + .withProperties(to.getProperties()) + .withLayout( + new Location.Layout( + to.getLayout().getPosition(), + to.getLayout().getLabelOffset(), + to.getLayout().getLocationRepresentation(), + to.getLayout().getLayerId() + ) + ); + + Set locationLinks = new HashSet<>(); + for (Map.Entry> linkEntry : to.getLinks().entrySet()) { + Point point = getObjectRepo().getObject(Point.class, linkEntry.getKey()); + Location.Link link = new Location.Link(newLocation.getReference(), point.getReference()) + .withAllowedOperations(linkEntry.getValue()); + locationLinks.add(link); + } + newLocation = newLocation.withAttachedLinks(locationLinks); + + getObjectRepo().addObject(newLocation); + emitObjectEvent( + newLocation, + null, + TCSObjectEvent.Type.OBJECT_CREATED + ); + + // Add the location's links to the respective points, too. + for (Location.Link link : locationLinks) { + Point point = getObjectRepo().getObject(Point.class, link.getPoint()); + + Set pointLinks = new HashSet<>(point.getAttachedLinks()); + pointLinks.add(link); + + Point previousPointState = point; + point = point.withAttachedLinks(pointLinks); + getObjectRepo().replaceObject(point); + + emitObjectEvent( + point, + previousPointState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + } + + return newLocation; + } + + /** + * Creates a new vehicle with a unique name and all other attributes set to + * their default values. + * + * @param to The transfer object from which to create the new group. + * @return The newly created group. + * @throws ObjectExistsException If an object with the new object's name already exists. + */ + private Vehicle createVehicle(VehicleCreationTO to) + throws ObjectExistsException { + Vehicle newVehicle = new Vehicle(to.getName()) + .withBoundingBox( + new BoundingBox( + to.getBoundingBox().getLength(), + to.getBoundingBox().getWidth(), + to.getBoundingBox().getHeight() + ) + .withReferenceOffset( + new Couple( + to.getBoundingBox().getReferenceOffset().getX(), + to.getBoundingBox().getReferenceOffset().getY() + ) + ) + ) + .withEnergyLevelThresholdSet( + new EnergyLevelThresholdSet( + to.getEnergyLevelThresholdSet().getEnergyLevelCritical(), + to.getEnergyLevelThresholdSet().getEnergyLevelGood(), + to.getEnergyLevelThresholdSet().getEnergyLevelSufficientlyRecharged(), + to.getEnergyLevelThresholdSet().getEnergyLevelFullyRecharged() + ) + ) + .withMaxVelocity(to.getMaxVelocity()) + .withMaxReverseVelocity(to.getMaxReverseVelocity()) + .withEnvelopeKey(to.getEnvelopeKey()) + .withProperties(to.getProperties()) + .withLayout(new Vehicle.Layout(to.getLayout().getRouteColor())); + getObjectRepo().addObject(newVehicle); + emitObjectEvent( + newVehicle, + null, + TCSObjectEvent.Type.OBJECT_CREATED + ); + return newVehicle; + } + + /** + * Creates a new block with a unique name and all other attributes set to + * default values. + * + * @param to The transfer object from which to create the new block. + * @return The newly created block. + * @throws ObjectExistsException If an object with the new object's name already exists. + * @throws ObjectUnknownException If any object referenced in the TO does not exist. + */ + private Block createBlock(BlockCreationTO to) + throws ObjectExistsException, + ObjectUnknownException { + Set> members = new HashSet<>(); + for (String memberName : to.getMemberNames()) { + TCSObject object = getObjectRepo().getObject(memberName); + if (!(object instanceof TCSResource)) { + throw new ObjectUnknownException(memberName); + } + members.add(((TCSResource) object).getReference()); + } + Block newBlock = new Block(to.getName()) + .withType(to.getType()) + .withMembers(members) + .withProperties(to.getProperties()) + .withLayout(new Block.Layout(to.getLayout().getColor())); + getObjectRepo().addObject(newBlock); + emitObjectEvent( + newBlock, + null, + TCSObjectEvent.Type.OBJECT_CREATED + ); + // Return the newly created block. + return newBlock; + } + + /** + * Adds an incoming path to a point. + * + * @param pointRef A reference to the point to be modified. + * @param pathRef A reference to the path. + * @return The modified point. + * @throws ObjectUnknownException If the referenced point or path do not + * exist. + */ + private Point addPointIncomingPath( + TCSObjectReference pointRef, + TCSObjectReference pathRef + ) + throws ObjectUnknownException { + Point point = getObjectRepo().getObject(Point.class, pointRef); + Path path = getObjectRepo().getObject(Path.class, pathRef); + // Check if the point really is the path's destination point. + if (!path.getDestinationPoint().equals(point.getReference())) { + throw new IllegalArgumentException("Point is not the path's destination."); + } + Path previousState = path; + Set> incomingPaths = new HashSet<>(point.getIncomingPaths()); + incomingPaths.add(path.getReference()); + point = point.withIncomingPaths(incomingPaths); + getObjectRepo().replaceObject(point); + emitObjectEvent( + point, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return point; + } + + /** + * Adds an outgoing path to a point. + * + * @param pointRef A reference to the point to be modified. + * @param pathRef A reference to the path. + * @return The modified point. + * @throws ObjectUnknownException If the referenced point or path do not + * exist. + */ + private Point addPointOutgoingPath( + TCSObjectReference pointRef, + TCSObjectReference pathRef + ) + throws ObjectUnknownException { + Point point = getObjectRepo().getObject(Point.class, pointRef); + Path path = getObjectRepo().getObject(Path.class, pathRef); + // Check if the point really is the path's source. + if (!path.getSourcePoint().equals(point.getReference())) { + throw new IllegalArgumentException("Point is not the path's source."); + } + Path previousState = path; + Set> outgoingPaths = new HashSet<>(point.getOutgoingPaths()); + outgoingPaths.add(path.getReference()); + point = point.withOutgoingPaths(outgoingPaths); + getObjectRepo().replaceObject(point); + emitObjectEvent( + point, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return point; + } + + private static List>> unmodifiableCopy( + List>> resources + ) { + List>> result = new ArrayList<>(); + + for (Set> resSet : resources) { + result.add(Set.copyOf(resSet)); + } + + return Collections.unmodifiableList(result); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/PrefixedUlidObjectNameProvider.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/PrefixedUlidObjectNameProvider.java new file mode 100644 index 0000000..3bdbc3e --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/PrefixedUlidObjectNameProvider.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static java.util.Objects.requireNonNull; + +import de.huxhorn.sulky.ulid.ULID; +import java.util.Optional; +import org.opentcs.access.to.CreationTO; +import org.opentcs.components.kernel.ObjectNameProvider; + +/** + * Provides names for objects based on ULIDs, prefixed with the name taken from a given + * {@link CreationTO}. + */ +public class PrefixedUlidObjectNameProvider + implements + ObjectNameProvider { + + /** + * Generates ULIDs for us. + */ + private final ULID ulid = new ULID(); + /** + * The previously generated ULID value. + */ + private ULID.Value previousUlid = ulid.nextValue(); + + /** + * Creates a new instance. + */ + public PrefixedUlidObjectNameProvider() { + } + + @Override + public String apply(CreationTO to) { + requireNonNull(to, "to"); + + Optional newValue = ulid.nextStrictlyMonotonicValue(previousUlid); + while (newValue.isEmpty()) { + newValue = ulid.nextStrictlyMonotonicValue(previousUlid); + } + previousUlid = newValue.get(); + return to.getName() + newValue.get().toString(); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/TCSObjectManager.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/TCSObjectManager.java new file mode 100644 index 0000000..fb90f14 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/TCSObjectManager.java @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handles generic modifications of objects contained in a {@link TCSObjectRepository}. + *

+ * Note that no synchronization is done inside this class. Concurrent access of instances of this + * class must be synchronized externally. + *

+ */ +public class TCSObjectManager { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(TCSObjectManager.class); + /** + * The object repo. + */ + private final TCSObjectRepository objectRepo; + /** + * A handler we should emit object events to. + */ + private final EventHandler eventHandler; + + /** + * Creates a new instance. + * + * @param objectRepo The object repo. + * @param eventHandler The event handler to publish events to. + */ + @Inject + public TCSObjectManager( + @Nonnull + TCSObjectRepository objectRepo, + @Nonnull + @ApplicationEventBus + EventHandler eventHandler + ) { + this.objectRepo = requireNonNull(objectRepo, "objectRepo"); + this.eventHandler = requireNonNull(eventHandler, "eventHandler"); + } + + /** + * Returns the underlying object repo. + * + * @return The underlying object repo. + */ + @Nonnull + public TCSObjectRepository getObjectRepo() { + return objectRepo; + } + + /** + * Sets a property for the referenced object. + * + * @param ref A reference to the object to be modified. + * @param key The property's key/name. + * @param value The property's value. If null, removes the + * property from the object. + * @throws ObjectUnknownException If the referenced object does not exist. + */ + public void setObjectProperty( + @Nonnull + TCSObjectReference ref, + @Nonnull + String key, + @Nullable + String value + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(key, "key"); + + TCSObject object = objectRepo.getObject(ref); + TCSObject previousState = object; + LOG.debug( + "Setting property on object named '{}': key='{}', value='{}'", + ref.getName(), + key, + value + ); + object = object.withProperty(key, value); + objectRepo.replaceObject(object); + emitObjectEvent(object, previousState, TCSObjectEvent.Type.OBJECT_MODIFIED); + } + + /** + * Appends a history entry to the referenced object. + * + * @param ref A reference to the object to be modified. + * @param entry The history entry to be appended. + * @throws ObjectUnknownException If the referenced object does not exist. + */ + public void appendObjectHistoryEntry( + @Nonnull + TCSObjectReference ref, + @Nonnull + ObjectHistory.Entry entry + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + requireNonNull(entry, "entry"); + + TCSObject object = objectRepo.getObject(ref); + TCSObject previousState = object; + LOG.debug("Appending history entry to object named '{}': {}", ref.getName(), entry); + object = object.withHistoryEntry(entry); + objectRepo.replaceObject(object); + emitObjectEvent(object, previousState, TCSObjectEvent.Type.OBJECT_MODIFIED); + } + + /** + * Emits an event for the given object with the given type. + * + * @param currentObjectState The current state of the object to emit an event + * for. + * @param previousObjectState The previous state of the object to emit an + * event for. + * @param evtType The type of event to emit. + */ + public void emitObjectEvent( + TCSObject currentObjectState, + TCSObject previousObjectState, + TCSObjectEvent.Type evtType + ) { + eventHandler.onEvent(new TCSObjectEvent(currentObjectState, previousObjectState, evtType)); + } + +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/TCSObjectRepository.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/TCSObjectRepository.java new file mode 100644 index 0000000..d84fec0 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/TCSObjectRepository.java @@ -0,0 +1,350 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import org.opentcs.data.ObjectExistsException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectReference; + +/** + * A container for TCSObjects belonging together. + *

+ * Provides access to a set of data objects and ensures they have unique names. + *

+ */ +public class TCSObjectRepository { + + /** + * The objects contained in this pool, mapped by their names, grouped by their classes. + */ + private final Map, Map>> objects = new HashMap<>(); + + /** + * Creates a new instance. + */ + public TCSObjectRepository() { + } + + /** + * Adds a new object to the pool. + * + * @param newObject The object to be added to the pool. + * @throws ObjectExistsException If an object with the same ID or the same + * name as the new one already exists in this pool. + */ + public void addObject( + @Nonnull + TCSObject newObject + ) + throws ObjectExistsException { + requireNonNull(newObject, "newObject"); + + if (containsName(newObject.getName())) { + throw new ObjectExistsException("Object name already exists: " + newObject.getName()); + } + + Map> objectsByName = objects.get(newObject.getClass()); + if (objectsByName == null) { + objectsByName = new HashMap<>(); + objects.put(newObject.getClass(), objectsByName); + } + objectsByName.put(newObject.getName(), newObject); + } + + /** + * Uses the given object to replace an object in the pool with same name. + * + * @param object The replacing object. + * @throws IllegalArgumentException If an object with the same name as the given object does not + * exist in this repository, yet, or if an object with the same name does exist but is an instance + * of a different class. + */ + @Nonnull + public void replaceObject( + @Nonnull + TCSObject object + ) + throws IllegalArgumentException { + requireNonNull(object, "object"); + TCSObject oldObject = getObjectOrNull(object.getName()); + checkArgument( + oldObject != null, + "Object named '%s' does not exist", + object.getName() + ); + checkArgument( + object.getClass() == oldObject.getClass(), + "Object named '%s' not an instance of the same class: '%s' != '%s'", + object.getName(), + object.getClass().getName(), + oldObject.getClass().getName() + ); + + objects.get(object.getClass()).put(object.getName(), object); + } + + /** + * Returns an object from the pool. + * + * @param ref A reference to the object to return. + * @return The referenced object, or null, if no such object exists in this pool. + */ + @Nullable + public TCSObject getObjectOrNull( + @Nonnull + TCSObjectReference ref + ) { + requireNonNull(ref); + + return objects.getOrDefault(ref.getReferentClass(), Map.of()).get(ref.getName()); + } + + /** + * Returns an object from the pool. + * + * @param ref A reference to the object to return. + * @return The referenced object. + * @throws ObjectUnknownException If the referenced object does not exist. + */ + @Nonnull + public TCSObject getObject( + @Nonnull + TCSObjectReference ref + ) + throws ObjectUnknownException { + TCSObject result = getObjectOrNull(ref); + if (result == null) { + throw new ObjectUnknownException(ref); + } + return result; + } + + /** + * Returns an object from the pool. + * + * @param The object's type. + * @param clazz The class of the object to be returned. + * @param ref A reference to the object to be returned. + * @return The referenced object, or null, if no such object + * exists in this pool or if an object exists but is not an instance of the + * given class. + */ + @Nullable + public > T getObjectOrNull( + @Nonnull + Class clazz, + @Nonnull + TCSObjectReference ref + ) { + requireNonNull(clazz, "clazz"); + requireNonNull(ref, "ref"); + + TCSObject result = objects.getOrDefault(clazz, Map.of()).get(ref.getName()); + if (clazz.isInstance(result)) { + return clazz.cast(result); + } + else { + return null; + } + } + + /** + * Returns an object from the pool. + * + * @param The object's type. + * @param clazz The class of the object to be returned. + * @param ref A reference to the object to be returned. + * @return The referenced object. + * @throws ObjectUnknownException If the referenced object does not exist, or if an object exists + * but is not an instance of the given class. + */ + @Nonnull + public > T getObject( + @Nonnull + Class clazz, + @Nonnull + TCSObjectReference ref + ) + throws ObjectUnknownException { + T result = getObjectOrNull(clazz, ref); + if (result == null) { + throw new ObjectUnknownException(ref); + } + return result; + } + + /** + * Returns an object from the pool. + * + * @param name The name of the object to return. + * @return The object with the given name, or null, if no such + * object exists in this pool. + */ + @Nullable + public TCSObject getObjectOrNull( + @Nonnull + String name + ) { + requireNonNull(name, "name"); + + return objects.values().stream() + .map(objectsByName -> objectsByName.get(name)) + .filter(object -> object != null) + .findAny() + .orElse(null); + } + + /** + * Returns an object from the pool. + * + * @param name The name of the object to return. + * @return The object with the given name. + * @throws ObjectUnknownException If the referenced object does not exist. + */ + @Nonnull + public TCSObject getObject( + @Nonnull + String name + ) + throws ObjectUnknownException { + TCSObject result = getObjectOrNull(name); + if (result == null) { + throw new ObjectUnknownException(name); + } + return result; + } + + /** + * Returns an object from the pool. + * + * @param The object's type. + * @param clazz The class of the object to be returned. + * @param name The name of the object to be returned. + * @return The named object, or null, if no such object + * exists in this pool or if an object exists but is not an instance of the + * given class. + */ + @Nullable + public > T getObjectOrNull( + @Nonnull + Class clazz, + @Nonnull + String name + ) { + requireNonNull(clazz, "clazz"); + requireNonNull(name, "name"); + + TCSObject result = objects.getOrDefault(clazz, Map.of()).get(name); + if (clazz.isInstance(result)) { + return clazz.cast(result); + } + else { + return null; + } + } + + /** + * Returns an object from the pool. + * + * @param The object's type. + * @param clazz The class of the object to be returned. + * @param name The name of the object to be returned. + * @return The named object. + * @throws ObjectUnknownException If no object with the given name exists in this pool or if an + * object exists but is not an instance of the given class. + */ + @Nonnull + public > T getObject( + @Nonnull + Class clazz, + @Nonnull + String name + ) + throws ObjectUnknownException { + T result = getObjectOrNull(clazz, name); + if (result == null) { + throw new ObjectUnknownException(name); + } + return result; + } + + /** + * Returns a set of objects belonging to the given class. + * + * @param The objects' type. + * @param clazz The class of the objects to be returned. + * @return A set of objects belonging to the given class. + */ + @Nonnull + public > Set getObjects( + @Nonnull + Class clazz + ) { + return objects.getOrDefault(clazz, Map.of()).values().stream() + .map(object -> clazz.cast(object)) + .collect(Collectors.toSet()); + } + + /** + * Returns a set of objects of the given class for which the given predicate is true. + * + * @param The objects' type. + * @param clazz The class of the objects to be returned. + * @param predicate The predicate that must be true for returned objects. + * @return A set of objects of the given class for which the given predicate is true. If no such + * objects exist, the returned set is empty. + */ + @Nonnull + public > Set getObjects( + @Nonnull + Class clazz, + @Nonnull + Predicate predicate + ) { + requireNonNull(clazz, "clazz"); + requireNonNull(predicate, "predicate"); + + return objects.getOrDefault(clazz, Map.of()).values().stream() + .map(object -> clazz.cast(object)) + .filter(predicate) + .collect(Collectors.toSet()); + } + + /** + * Removes a referenced object from this pool. + * + * @param ref A reference to the object to be removed. + * @return The object that was removed from the pool. + * @throws ObjectUnknownException If the referenced object does not exist. + */ + @Nonnull + public TCSObject removeObject( + @Nonnull + TCSObjectReference ref + ) + throws ObjectUnknownException { + requireNonNull(ref, "ref"); + + Map> map = objects.get(ref.getReferentClass()); + TCSObject obj = (map == null) ? null : map.remove(ref.getName()); + if (obj == null) { + throw new ObjectUnknownException(ref); + } + return obj; + } + + private boolean containsName(String name) { + return objects.values().stream().anyMatch(objectsByName -> objectsByName.containsKey(name)); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/TransportOrderPoolManager.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/TransportOrderPoolManager.java new file mode 100644 index 0000000..e08d9fe --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/TransportOrderPoolManager.java @@ -0,0 +1,817 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import org.opentcs.access.to.order.DestinationCreationTO; +import org.opentcs.access.to.order.OrderSequenceCreationTO; +import org.opentcs.access.to.order.TransportOrderCreationTO; +import org.opentcs.components.kernel.ObjectNameProvider; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.ObjectExistsException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Location.Link; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.DriveOrder.Destination; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Keeps all {@code TransportOrder}s and provides methods to create and manipulate them. + *

+ * Note that no synchronization is done inside this class. Concurrent access of instances of this + * class must be synchronized externally. + *

+ */ +public class TransportOrderPoolManager + extends + TCSObjectManager { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(TransportOrderPoolManager.class); + /** + * Provides names for transport orders and order sequences. + */ + private final ObjectNameProvider objectNameProvider; + + /** + * Creates a new instance. + * + * @param objectRepo The object repo. + * @param eventHandler The event handler to publish events to. + * @param orderNameProvider Provides names for transport orders. + */ + @Inject + public TransportOrderPoolManager( + @Nonnull + TCSObjectRepository objectRepo, + @Nonnull + @ApplicationEventBus + EventHandler eventHandler, + @Nonnull + ObjectNameProvider orderNameProvider + ) { + super(objectRepo, eventHandler); + this.objectNameProvider = requireNonNull(orderNameProvider, "orderNameProvider"); + } + + /** + * Removes all transport orders from this pool. + */ + public void clear() { + List> objects = new ArrayList<>(); + objects.addAll(getObjectRepo().getObjects(OrderSequence.class)); + objects.addAll(getObjectRepo().getObjects(TransportOrder.class)); + + for (TCSObject curObject : objects) { + getObjectRepo().removeObject(curObject.getReference()); + emitObjectEvent( + null, + curObject, + TCSObjectEvent.Type.OBJECT_REMOVED + ); + } + } + + /** + * Adds a new transport order to the pool. + * This method implicitly adds the transport order to its wrapping sequence, if any. + * + * @param to The transfer object from which to create the new transport order. + * @return The newly created transport order. + * @throws ObjectExistsException If an object with the new object's name already exists. + * @throws ObjectUnknownException If any object referenced in the TO does not exist. + * @throws IllegalArgumentException One of: + *
    + *
  1. The order is supposed to be part of an order sequence, but + * the sequence is already complete.
  2. + *
  3. The type of the transport order and the order sequence differ.
  4. + *
  5. The intended vehicle of the transport order and the order sequence differ.
  6. + *
  7. A destination operation is not a valid operation on the destination object.
  8. + *
+ */ + public TransportOrder createTransportOrder(TransportOrderCreationTO to) + throws ObjectUnknownException, + ObjectExistsException, + IllegalArgumentException { + TransportOrder newOrder = new TransportOrder( + nameFor(to), + toDriveOrders(to.getDestinations()) + ) + .withCreationTime(Instant.now()) + .withPeripheralReservationToken(to.getPeripheralReservationToken()) + .withIntendedVehicle(toVehicleReference(to.getIntendedVehicleName())) + .withType(to.getType()) + .withDeadline(to.getDeadline()) + .withDispensable(to.isDispensable()) + .withWrappingSequence(getWrappingSequence(to)) + .withDependencies(getDependencies(to)) + .withProperties(to.getProperties()); + + LOG.info( + "Transport order is being created: {} -- {}", + newOrder.getName(), + newOrder.getAllDriveOrders() + ); + + getObjectRepo().addObject(newOrder); + emitObjectEvent(newOrder, null, TCSObjectEvent.Type.OBJECT_CREATED); + + if (newOrder.getWrappingSequence() != null) { + OrderSequence sequence = getObjectRepo().getObject( + OrderSequence.class, + newOrder.getWrappingSequence() + ); + OrderSequence prevSeq = sequence; + sequence = sequence.withOrder(newOrder.getReference()); + getObjectRepo().replaceObject(sequence); + emitObjectEvent(sequence, prevSeq, TCSObjectEvent.Type.OBJECT_MODIFIED); + } + + // Return the newly created transport order. + return newOrder; + } + + /** + * Sets a transport order's state. + * + * @param ref A reference to the transport order to be modified. + * @param newState The transport order's new state. + * @return The modified transport order. + * @throws ObjectUnknownException If the referenced transport order is not + * in this pool. + */ + public TransportOrder setTransportOrderState( + TCSObjectReference ref, + TransportOrder.State newState + ) + throws ObjectUnknownException { + TransportOrder previousState = getObjectRepo().getObject(TransportOrder.class, ref); + + checkArgument( + !previousState.getState().isFinalState(), + "Transport order %s already in a final state, not changing %s -> %s.", + ref.getName(), + previousState.getState(), + newState + ); + + LOG.info( + "Transport order's state changes: {} -- {} -> {}", + previousState.getName(), + previousState.getState(), + newState + ); + + TransportOrder order = previousState.withState(newState); + getObjectRepo().replaceObject(order); + emitObjectEvent( + order, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return order; + } + + /** + * Sets a transport order's processing vehicle. + * + * @param orderRef A reference to the transport order to be modified. + * @param vehicleRef A reference to the vehicle processing the order. + * @param driveOrders The drive orders containing the data to be copied into this transport + * order's drive orders. + * @return The modified transport order. + * @throws ObjectUnknownException If the referenced transport order is not + * in this pool. + * @throws IllegalArgumentException If the destinations of the given drive + * orders do not match the destinations of the drive orders in this transport + * order. + */ + public TransportOrder setTransportOrderProcessingVehicle( + TCSObjectReference orderRef, + TCSObjectReference vehicleRef, + List driveOrders + ) + throws ObjectUnknownException, + IllegalArgumentException { + TransportOrder order = getObjectRepo().getObject(TransportOrder.class, orderRef); + + LOG.info( + "Transport order's processing vehicle changes: {} -- {} -> {}", + order.getName(), + toObjectName(order.getProcessingVehicle()), + toObjectName(vehicleRef) + ); + + TransportOrder previousState = order; + if (vehicleRef == null) { + order = order.withProcessingVehicle(null); + getObjectRepo().replaceObject(order); + } + else { + Vehicle vehicle = getObjectRepo().getObject(Vehicle.class, vehicleRef); + order = order.withProcessingVehicle(vehicle.getReference()) + .withDriveOrders(driveOrders) + .withCurrentDriveOrderIndex(0); + getObjectRepo().replaceObject(order); + if (order.getCurrentDriveOrder() != null) { + order = order.withCurrentDriveOrderState(DriveOrder.State.TRAVELLING); + getObjectRepo().replaceObject(order); + } + } + emitObjectEvent( + order, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return order; + } + + /** + * Copies drive order data from a list of drive orders to the given transport + * order's future drive orders. + * + * @param orderRef A reference to the transport order to be modified. + * @param newOrders The drive orders containing the data to be copied into + * this transport order's drive orders. + * @return The modified transport order. + * @throws ObjectUnknownException If the referenced transport order is not + * in this pool. + * @throws IllegalArgumentException If the destinations of the given drive + * orders do not match the destinations of the drive orders in this transport + * order. + */ + public TransportOrder setTransportOrderDriveOrders( + TCSObjectReference orderRef, + List newOrders + ) + throws ObjectUnknownException, + IllegalArgumentException { + TransportOrder previousState = getObjectRepo().getObject(TransportOrder.class, orderRef); + + LOG.debug( + "Transport order's drive orders change: {} -- {} -> {}", + previousState.getName(), + previousState.getAllDriveOrders(), + newOrders + ); + + TransportOrder order = previousState.withDriveOrders(newOrders); + getObjectRepo().replaceObject(order); + emitObjectEvent( + order, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return order; + } + + /** + * Updates a transport order's current drive order. + * Marks the current drive order as finished, adds it to the list of past + * drive orders and sets the current drive order to the next one of the list + * of future drive orders (or null, if that list is empty). + * If the current drive order is null because all drive orders + * have been finished already or none has been started, yet, nothing happens. + * + * @param ref A reference to the transport order to be modified. + * @return The modified transport order. + * @throws ObjectUnknownException If the referenced transport order is not + * in this pool. + */ + public TransportOrder setTransportOrderNextDriveOrder(TCSObjectReference ref) + throws ObjectUnknownException { + TransportOrder previousState = getObjectRepo().getObject(TransportOrder.class, ref); + TransportOrder order = previousState; + // First, mark the current drive order as FINISHED and send an event. + // Then, shift drive orders and send a second event. + // Then, mark the current drive order as TRAVELLING and send another event. + if (order.getCurrentDriveOrder() != null) { + LOG.info( + "Transport order's drive order finished: {} -- {}", + order.getName(), + order.getCurrentDriveOrder().getDestination() + ); + + order = order.withCurrentDriveOrderState(DriveOrder.State.FINISHED); + getObjectRepo().replaceObject(order); + TransportOrder newState = order; + emitObjectEvent( + newState, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + previousState = newState; + order = order.withCurrentDriveOrderIndex(order.getCurrentDriveOrderIndex() + 1) + .withCurrentRouteStepIndex(TransportOrder.ROUTE_STEP_INDEX_DEFAULT); + getObjectRepo().replaceObject(order); + newState = order; + emitObjectEvent( + newState, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + previousState = newState; + if (order.getCurrentDriveOrder() != null) { + order = order.withCurrentDriveOrderState(DriveOrder.State.TRAVELLING); + getObjectRepo().replaceObject(order); + newState = order; + emitObjectEvent( + newState, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + previousState = newState; + } + } + emitObjectEvent( + order, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return order; + } + + /** + * Sets a transport order's index of the last route step travelled for the currently processed + * drive order. + * + * @param ref A reference to the transport order to be modified. + * @param index The new index. + * @return The modified transport order. + * @throws ObjectUnknownException If the referenced transport order does not exist. + */ + public TransportOrder setTransportOrderCurrentRouteStepIndex( + TCSObjectReference ref, + int index + ) + throws ObjectUnknownException { + TransportOrder previousState = getObjectRepo().getObject(TransportOrder.class, ref); + + LOG.debug( + "Transport order's route step index changes: {} -- {} -> {}", + previousState.getName(), + previousState.getCurrentRouteStepIndex(), + index + ); + + TransportOrder order = previousState.withCurrentRouteStepIndex(index); + getObjectRepo().replaceObject(order); + emitObjectEvent( + order, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return order; + } + + /** + * Set a transport order's intended vehicle. + * + * @param orderRef A reference to the transport order to be modified. + * @param vehicleRef A reference to the vehicle intended for the transport order. + * @return The modified transport order. + * @throws ObjectUnknownException If the referenced transport order is not + * in this pool or if the intended vehicle is not null and not in this this pool. + * @throws IllegalArgumentException If the transport order is not in the dispatchable state. + */ + public TransportOrder setTransportOrderIntendedVehicle( + TCSObjectReference orderRef, + TCSObjectReference vehicleRef + ) + throws ObjectUnknownException, + IllegalArgumentException { + TransportOrder order = getObjectRepo().getObject(TransportOrder.class, orderRef); + + if (!canSetIntendedVehicle(order)) { + throw new IllegalArgumentException( + String.format( + "Cannot set intended vehicle '%s' for transport order '%s' in state '%s'", + toObjectName(vehicleRef), + order.getName(), + order.getState() + ) + ); + } + + if (vehicleRef != null && getObjectRepo().getObjectOrNull(Vehicle.class, vehicleRef) == null) { + throw new ObjectUnknownException("Unknown vehicle: " + vehicleRef.getName()); + } + + LOG.info( + "Transport order's intended vehicle changes: {} -- {} -> {}", + order.getName(), + toObjectName(order.getIntendedVehicle()), + toObjectName(vehicleRef) + ); + + TransportOrder previousState = order; + order = order.withIntendedVehicle(vehicleRef); + getObjectRepo().replaceObject(order); + emitObjectEvent( + order, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return order; + } + + /** + * Removes the referenced transport order from this pool. + * + * @param ref A reference to the transport order to be removed. + * @return The removed transport order. + * @throws ObjectUnknownException If the referenced transport order is not + * in this pool. + */ + public TransportOrder removeTransportOrder(TCSObjectReference ref) + throws ObjectUnknownException { + TransportOrder order = getObjectRepo().getObject(TransportOrder.class, ref); + // Make sure only orders in a final state are removed. + checkArgument( + order.getState().isFinalState(), + "Transport order %s is not in a final state.", + order.getName() + ); + getObjectRepo().removeObject(ref); + emitObjectEvent( + null, + order, + TCSObjectEvent.Type.OBJECT_REMOVED + ); + return order; + } + + /** + * Adds a new order sequence to the pool. + * + * @param to The transfer object from which to create the new order sequence. + * @return The newly created order sequence. + * @throws ObjectExistsException If an object with the new object's name already exists. + * @throws ObjectUnknownException If any object referenced in the TO does not exist. + */ + public OrderSequence createOrderSequence(OrderSequenceCreationTO to) + throws ObjectExistsException, + ObjectUnknownException { + OrderSequence newSequence = new OrderSequence(nameFor(to)) + .withType(to.getType()) + .withIntendedVehicle(toVehicleReference(to.getIntendedVehicleName())) + .withFailureFatal(to.isFailureFatal()) + .withProperties(to.getProperties()); + + LOG.info("Order sequence is being created: {}", newSequence.getName()); + + getObjectRepo().addObject(newSequence); + emitObjectEvent( + newSequence, + null, + TCSObjectEvent.Type.OBJECT_CREATED + ); + // Return the newly created transport order. + return newSequence; + } + + /** + * Sets an order sequence's finished index. + * + * @param seqRef A reference to the order sequence to be modified. + * @param index The sequence's new finished index. + * @return The modified order sequence. + * @throws ObjectUnknownException If the referenced transport order is not + * in this pool. + */ + public OrderSequence setOrderSequenceFinishedIndex( + TCSObjectReference seqRef, + int index + ) + throws ObjectUnknownException { + OrderSequence previousState = getObjectRepo().getObject(OrderSequence.class, seqRef); + + LOG.debug( + "Order sequence's finished index changes: {} -- {} -> {}", + previousState.getName(), + previousState.getFinishedIndex(), + index + ); + + OrderSequence sequence = previousState.withFinishedIndex(index); + getObjectRepo().replaceObject(sequence); + emitObjectEvent( + sequence, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return sequence; + } + + /** + * Sets an order sequence's complete flag. + * + * @param seqRef A reference to the order sequence to be modified. + * @return The modified order sequence. + * @throws ObjectUnknownException If the referenced transport order is not + * in this pool. + */ + public OrderSequence setOrderSequenceComplete(TCSObjectReference seqRef) + throws ObjectUnknownException { + OrderSequence previousState = getObjectRepo().getObject(OrderSequence.class, seqRef); + + LOG.info("Order sequence being marked as complete: {}", previousState.getName()); + + OrderSequence sequence = previousState.withComplete(true); + getObjectRepo().replaceObject(sequence); + emitObjectEvent( + sequence, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return sequence; + } + + /** + * Sets an order sequence's finished flag. + * + * @param seqRef A reference to the order sequence to be modified. + * @return The modified order sequence. + * @throws ObjectUnknownException If the referenced transport order is not + * in this pool. + */ + public OrderSequence setOrderSequenceFinished(TCSObjectReference seqRef) + throws ObjectUnknownException { + OrderSequence previousState = getObjectRepo().getObject(OrderSequence.class, seqRef); + + LOG.info("Order sequence being marked as finished: {}", previousState.getName()); + + OrderSequence sequence = previousState.withFinished(true); + getObjectRepo().replaceObject(sequence); + emitObjectEvent( + sequence, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return sequence; + } + + /** + * Sets an order sequence's processing vehicle. + * + * @param seqRef A reference to the order sequence to be modified. + * @param vehicleRef A reference to the vehicle processing the order sequence. + * @return The modified order sequence. + * @throws ObjectUnknownException If the referenced transport order is not + * in this pool. + */ + public OrderSequence setOrderSequenceProcessingVehicle( + TCSObjectReference seqRef, + TCSObjectReference vehicleRef + ) + throws ObjectUnknownException { + OrderSequence previousState = getObjectRepo().getObject(OrderSequence.class, seqRef); + + LOG.info( + "Order sequence's processing vehicle changes: {} -- {} -> {}", + previousState.getName(), + toObjectName(previousState.getProcessingVehicle()), + toObjectName(vehicleRef) + ); + + OrderSequence sequence = previousState; + if (vehicleRef == null) { + sequence = sequence.withProcessingVehicle(null); + getObjectRepo().replaceObject(sequence); + } + else { + Vehicle vehicle = getObjectRepo().getObject(Vehicle.class, vehicleRef); + sequence = sequence.withProcessingVehicle(vehicle.getReference()); + getObjectRepo().replaceObject(sequence); + } + emitObjectEvent( + sequence, + previousState, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + return sequence; + } + + /** + * Removes the referenced order sequence from this pool. + * + * @param ref A reference to the order sequence to be removed. + * @return The removed order sequence. + * @throws ObjectUnknownException If the referenced order sequence is not in this pool. + */ + public OrderSequence removeOrderSequence(TCSObjectReference ref) + throws ObjectUnknownException { + OrderSequence previousState = getObjectRepo().getObject(OrderSequence.class, ref); + OrderSequence sequence = previousState; + // XXX Any sanity checks here? + getObjectRepo().removeObject(ref); + emitObjectEvent( + null, + previousState, + TCSObjectEvent.Type.OBJECT_REMOVED + ); + return sequence; + } + + /** + * Removes a completed order sequence including its transport orders. + * + * @param ref A reference to the order sequence. + * @throws ObjectUnknownException If the referenced order sequence is not in this pool. + * @throws IllegalArgumentException If the order sequence is not finished, yet. + */ + public void removeFinishedOrderSequenceAndOrders(TCSObjectReference ref) + throws ObjectUnknownException, + IllegalArgumentException { + OrderSequence previousState = getObjectRepo().getObject(OrderSequence.class, ref); + checkArgument( + previousState.isFinished(), + "Order sequence %s is not finished", + previousState.getName() + ); + OrderSequence sequence = previousState; + getObjectRepo().removeObject(ref); + emitObjectEvent(null, previousState, TCSObjectEvent.Type.OBJECT_REMOVED); + // Also remove all orders in the sequence. + for (TCSObjectReference orderRef : sequence.getOrders()) { + removeTransportOrder(orderRef); + } + } + + private Set> getDependencies(TransportOrderCreationTO to) + throws ObjectUnknownException { + Set> result = new HashSet<>(); + for (String dependencyName : to.getDependencyNames()) { + result.add(getObjectRepo().getObject(TransportOrder.class, dependencyName).getReference()); + } + return result; + } + + private TCSObjectReference getWrappingSequence(TransportOrderCreationTO to) + throws ObjectUnknownException, + IllegalArgumentException { + if (to.getWrappingSequence() == null) { + return null; + } + OrderSequence sequence = getObjectRepo().getObject( + OrderSequence.class, + to.getWrappingSequence() + ); + checkArgument(!sequence.isComplete(), "Order sequence %s is already complete", sequence); + checkArgument( + Objects.equals(to.getType(), sequence.getType()), + "Order sequence %s has different type than order %s: %s != %s", + sequence, + to.getName(), + sequence.getType(), + to.getType() + ); + checkArgument( + Objects.equals(to.getIntendedVehicleName(), getIntendedVehicleName(sequence)), + "Order sequence %s has different intended vehicle than order %s: %s != %s", + sequence, + to.getName(), + sequence.getIntendedVehicle(), + to.getIntendedVehicleName() + ); + return sequence.getReference(); + } + + private TCSObjectReference toVehicleReference(String vehicleName) + throws ObjectUnknownException { + if (vehicleName == null) { + return null; + } + Vehicle vehicle = getObjectRepo().getObject(Vehicle.class, vehicleName); + return vehicle.getReference(); + } + + private List toDriveOrders(List dests) + throws ObjectUnknownException, + IllegalArgumentException { + List result = new ArrayList<>(dests.size()); + for (DestinationCreationTO destTo : dests) { + TCSObject destObject = getObjectRepo().getObjectOrNull(destTo.getDestLocationName()); + + if (destObject instanceof Point) { + if (!isValidOperationOnPoint(destTo.getDestOperation())) { + throw new IllegalArgumentException( + destTo.getDestOperation() + + " is not a valid operation for point destination " + destObject.getName() + ); + } + } + else if (destObject instanceof Location) { + if (!isValidLocationDestination(destTo, (Location) destObject)) { + throw new IllegalArgumentException( + destTo.getDestOperation() + + " is not a valid operation for location destination " + destObject.getName() + ); + } + } + else { + throw new ObjectUnknownException(destTo.getDestLocationName()); + } + result.add( + new DriveOrder( + new DriveOrder.Destination(destObject.getReference()) + .withOperation(destTo.getDestOperation()) + .withProperties(destTo.getProperties()) + ) + ); + } + return result; + } + + private boolean isValidOperationOnPoint(String operation) { + return operation.equals(Destination.OP_MOVE) + || operation.equals(Destination.OP_PARK); + } + + private boolean isValidLocationDestination(DestinationCreationTO dest, Location location) { + LocationType type = getObjectRepo() + .getObjectOrNull(LocationType.class, location.getType().getName()); + + return type != null + && isValidOperationOnLocationType(dest.getDestOperation(), type) + && location.getAttachedLinks().stream() + .anyMatch(link -> isValidOperationOnLink(dest.getDestOperation(), link)); + } + + private boolean isValidOperationOnLink(String operation, Link link) { + return link.getAllowedOperations().isEmpty() + || link.getAllowedOperations().contains(operation) + || operation.equals(Destination.OP_NOP); + } + + private boolean isValidOperationOnLocationType(String operation, LocationType type) { + return type.isAllowedOperation(operation) + || operation.equals(Destination.OP_NOP); + } + + @Nullable + private String getIntendedVehicleName(OrderSequence sequence) { + return sequence.getIntendedVehicle() == null ? null : sequence.getIntendedVehicle().getName(); + } + + @Nonnull + private String nameFor( + @Nonnull + TransportOrderCreationTO to + ) { + if (to.hasIncompleteName()) { + return objectNameProvider.apply(to); + } + else { + return to.getName(); + } + } + + @Nonnull + private String nameFor( + @Nonnull + OrderSequenceCreationTO to + ) { + if (to.hasIncompleteName()) { + return objectNameProvider.apply(to); + } + else { + return to.getName(); + } + } + + @Nonnull + private String toObjectName(TCSObjectReference ref) { + return ref == null ? "null" : ref.getName(); + } + + private boolean canSetIntendedVehicle(TransportOrder order) { + return order.hasState(TransportOrder.State.RAW) + || order.hasState(TransportOrder.State.ACTIVE) + || order.hasState(TransportOrder.State.DISPATCHABLE); + } +} diff --git a/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/WorkingSetCleanupTask.java b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/WorkingSetCleanupTask.java new file mode 100644 index 0000000..878af65 --- /dev/null +++ b/opentcs-kernel/src/main/java/org/opentcs/kernel/workingset/WorkingSetCleanupTask.java @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Objects; +import java.util.function.Predicate; +import org.opentcs.customizations.kernel.GlobalSyncObject; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.kernel.OrderPoolConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A task that periodically removes orders, order sequences and peripheral jobs in a final state. + */ +public class WorkingSetCleanupTask + implements + Runnable { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(WorkingSetCleanupTask.class); + /** + * A global object to be used for synchronization within the kernel. + */ + private final Object globalSyncObject; + /** + * Keeps all the transport orders. + */ + private final TransportOrderPoolManager orderPoolManager; + /** + * Keeps all peripheral jobs. + */ + private final PeripheralJobPoolManager peripheralJobPoolManager; + /** + * This class's configuration. + */ + private final OrderPoolConfiguration configuration; + /** + * Checks whether an order sequence may be removed. + */ + private final CompositeOrderSequenceCleanupApproval compositeOrderSequenceCleanupApproval; + /** + * Checks whether a transport order may be removed. + */ + private final CompositeTransportOrderCleanupApproval compositeTransportOrderCleanupApproval; + /** + * Checks whether a peripheral job may be removed. + */ + private final CompositePeripheralJobCleanupApproval compositePeripheralJobCleanupApproval; + /** + * Keeps track of the time used to determine whether a working set item should be removed + * (according to its creation time). + */ + private final CreationTimeThreshold creationTimeThreshold; + + /** + * Creates a new instance. + * + * @param globalSyncObject The kernel threads' global synchronization object. + * @param orderPoolManager The order pool manager to be used. + * @param peripheralJobPoolManager The peripheral job pool manager to be used. + * @param compositeOrderSequenceCleanupApproval Checks whether an order sequence may be removed. + * @param compositeTransportOrderCleanupApproval Checks whether a transport order may be removed. + * @param compositePeripheralJobCleanupApproval Checks whether a peripheral job may be removed. + * @param creationTimeThreshold Keeps track of the time used to determine whether a working set + * item should be removed (according to its creation time). + * @param configuration This class's configuration. + */ + @Inject + public WorkingSetCleanupTask( + @GlobalSyncObject + Object globalSyncObject, + TransportOrderPoolManager orderPoolManager, + PeripheralJobPoolManager peripheralJobPoolManager, + OrderPoolConfiguration configuration, + CompositeOrderSequenceCleanupApproval compositeOrderSequenceCleanupApproval, + CompositeTransportOrderCleanupApproval compositeTransportOrderCleanupApproval, + CompositePeripheralJobCleanupApproval compositePeripheralJobCleanupApproval, + CreationTimeThreshold creationTimeThreshold + ) { + this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject"); + this.orderPoolManager = requireNonNull(orderPoolManager, "orderPoolManager"); + this.peripheralJobPoolManager = requireNonNull( + peripheralJobPoolManager, + "peripheralJobPoolManager" + ); + this.compositeOrderSequenceCleanupApproval + = requireNonNull(compositeOrderSequenceCleanupApproval); + this.compositeTransportOrderCleanupApproval + = requireNonNull(compositeTransportOrderCleanupApproval); + this.compositePeripheralJobCleanupApproval + = requireNonNull(compositePeripheralJobCleanupApproval); + this.creationTimeThreshold = requireNonNull(creationTimeThreshold, "creationTimeThreshold"); + this.configuration = requireNonNull(configuration, "configuration"); + } + + public long getSweepInterval() { + return configuration.sweepInterval(); + } + + @Override + public void run() { + synchronized (globalSyncObject) { + LOG.debug("Sweeping working set..."); + + // Update the creation time threshold for this cleanup run. + creationTimeThreshold.updateCurrentThreshold(configuration.sweepAge()); + + // Remove all peripheral jobs in a final state that do not belong to a transport order and + // that are older than the threshold. + Predicate noRelatedTransportOrder = job -> job + .getRelatedTransportOrder() == null; + for (PeripheralJob peripheralJob : peripheralJobPoolManager.getObjectRepo().getObjects( + PeripheralJob.class, + noRelatedTransportOrder.and(compositePeripheralJobCleanupApproval) + )) { + peripheralJobPoolManager.removePeripheralJob(peripheralJob.getReference()); + } + + // Remove all transport orders in a final state that do NOT belong to a sequence and that are + // older than the threshold, including their related peripheral jobs. + Predicate noWrappingSequence = order -> order.getWrappingSequence() == null; + for (TransportOrder transportOrder : orderPoolManager.getObjectRepo().getObjects( + TransportOrder.class, + noWrappingSequence.and(compositeTransportOrderCleanupApproval) + )) { + removeRelatedPeripheralJobs(transportOrder.getReference()); + orderPoolManager.removeTransportOrder(transportOrder.getReference()); + } + + // Remove all order sequences that have been finished, including their transport orders and + // the transport orders' related peripheral jobs. + for (OrderSequence orderSequence : orderPoolManager.getObjectRepo().getObjects( + OrderSequence.class, + compositeOrderSequenceCleanupApproval + )) { + for (TCSObjectReference transportOrderRef : orderSequence.getOrders()) { + removeRelatedPeripheralJobs(transportOrderRef); + } + orderPoolManager.removeFinishedOrderSequenceAndOrders(orderSequence.getReference()); + } + } + } + + private void removeRelatedPeripheralJobs(TCSObjectReference transportOrderRef) { + for (PeripheralJob peripheralJob : peripheralJobPoolManager.getObjectRepo().getObjects( + PeripheralJob.class, + job -> Objects.equals(job.getRelatedTransportOrder(), transportOrderRef) + )) { + peripheralJobPoolManager.removePeripheralJob(peripheralJob.getReference()); + } + } +} diff --git a/opentcs-kernel/src/main/resources/org/opentcs/kernel/distribution/config/opentcs-kernel-defaults-baseline.properties b/opentcs-kernel/src/main/resources/org/opentcs/kernel/distribution/config/opentcs-kernel-defaults-baseline.properties new file mode 100644 index 0000000..5895118 --- /dev/null +++ b/opentcs-kernel/src/main/resources/org/opentcs/kernel/distribution/config/opentcs-kernel-defaults-baseline.properties @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +# This file contains default configuration values and should not be modified. +# To adjust the application configuration, override values in a separate file. + +kernelapp.autoEnableDriversOnStartup = false +kernelapp.autoEnablePeripheralDriversOnStartup = false +kernelapp.saveModelOnTerminateModelling = false +kernelapp.saveModelOnTerminateOperating = false +kernelapp.updateRoutingTopologyOnPathLockChange = false +kernelapp.rerouteOnRoutingTopologyUpdate = false +kernelapp.rerouteOnDriveOrderFinished = false +kernelapp.vehicleResourceManagementType = LENGTH_RESPECTED + +orderpool.sweepInterval = 60000 +orderpool.sweepAge = 86400000 + +rmikernelinterface.enable = true +rmikernelinterface.useSsl = false +rmikernelinterface.clientSweepInterval = 300000 +rmikernelinterface.registryPort = 1099 +rmikernelinterface.remoteKernelServicePortalPort = 55000 +rmikernelinterface.remotePlantModelServicePort = 55001 +rmikernelinterface.remoteTransportOrderServicePort = 55002 +rmikernelinterface.remoteVehicleServicePort = 55003 +rmikernelinterface.remoteNotificationServicePort = 55004 +rmikernelinterface.remoteSchedulerServicePort = 55005 +rmikernelinterface.remoteRouterServicePort = 55006 +rmikernelinterface.remoteDispatcherServicePort = 55007 +rmikernelinterface.remoteQueryServicePort = 55008 +rmikernelinterface.remotePeripheralServicePort = 55009 +rmikernelinterface.remotePeripheralJobServicePort = 55010 + +ssl.keystoreFile = ./config/keystore.p12 +ssl.truststoreFile = ./config/truststore.p12 +ssl.keystorePassword = password +ssl.truststorePassword = password + +adminwebapi.enable = true +adminwebapi.bindAddress = 127.0.0.1 +adminwebapi.bindPort = 55100 + +servicewebapi.enable = true +servicewebapi.useSsl = false +servicewebapi.bindAddress = 0.0.0.0 +servicewebapi.bindPort = 55200 +servicewebapi.accessKey = +servicewebapi.statusEventsCapacity = 1000 + +defaultdispatcher.dismissUnroutableTransportOrders = true +defaultdispatcher.assignRedundantOrders = false +defaultdispatcher.reroutingImpossibleStrategy = IGNORE_PATH_LOCKS +defaultdispatcher.parkIdleVehicles = false +defaultdispatcher.considerParkingPositionPriorities = false +defaultdispatcher.reparkVehiclesToHigherPriorityPositions = false +defaultdispatcher.rechargeIdleVehicles = false +defaultdispatcher.keepRechargingUntilFullyCharged = true +defaultdispatcher.idleVehicleRedispatchingInterval = 10000 +defaultdispatcher.orderPriorities = BY_DEADLINE +defaultdispatcher.orderCandidatePriorities = BY_DEADLINE +defaultdispatcher.vehiclePriorities = IDLE_FIRST,BY_ENERGY_LEVEL +defaultdispatcher.vehicleCandidatePriorities = IDLE_FIRST,BY_ENERGY_LEVEL +defaultdispatcher.deadlineAtRiskPeriod = 60000 + +defaultrouter.routeToCurrentPosition = false + +defaultrouter.shortestpath.algorithm = DIJKSTRA +defaultrouter.shortestpath.edgeEvaluators = DISTANCE + +defaultrouter.edgeevaluator.explicitproperties.defaultValue = 1000000 + +defaultperipheraljobdispatcher.idlePeripheralRedispatchingInterval = 10000 + +virtualvehicle.enable = true +virtualvehicle.commandQueueCapacity = 2 +virtualvehicle.rechargeOperation = CHARGE +virtualvehicle.rechargePercentagePerSecond = 1.0 +virtualvehicle.simulationTimeFactor = 1.0 +virtualvehicle.vehicleLengthLoaded = 1000 +virtualvehicle.vehicleLengthUnloaded = 1000 + +virtualperipheral.enable = true + +statisticscollector.enable = true + +watchdog.blockConsistencyCheckInterval = 10000 +watchdog.strandedVehicleCheckInterval = 10000 +watchdog.strandedVehicleDurationThreshold = 60000 diff --git a/opentcs-kernel/src/test/java/org/opentcs/DataObjectFactory.java b/opentcs-kernel/src/test/java/org/opentcs/DataObjectFactory.java new file mode 100644 index 0000000..2fb4b3c --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/DataObjectFactory.java @@ -0,0 +1,184 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs; + +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; + +/** + * Creates model elements that can be used in tests. + */ +public class DataObjectFactory { + + /** + * Prefix to use for points. + */ + private String pointNamePrefix = "MyPoint-"; + /** + * Prefix to use for paths. + */ + private String pathNamePrefix = "MyPath-"; + /** + * Prefix to use for vehicles. + */ + private String vehicleNamePrefix = "MyVehicle-"; + /** + * Prefix to use for location types. + */ + private String locTypeNamePrefix = "MyLocType-"; + /** + * Prefix to use for locations. + */ + private String locationNamePrefix = "MyLocation-"; + /** + * counter to create unique names. + */ + private int uniqueIdCounter; + + /** + * Creates a new instance. + */ + public DataObjectFactory() { + } + + /** + * Sets the suffix used for points. + * + * @param pointNamePrefix The suffix used for points. + */ + void setPointNamePrefix(String pointNamePrefix) { + this.pointNamePrefix = pointNamePrefix; + } + + /** + * Sets the prefix used for paths. + * + * @param pathNamePrefix The prefix used for paths. + */ + void setPathNamePrefix(String pathNamePrefix) { + this.pathNamePrefix = pathNamePrefix; + } + + /** + * Sets the prefix used for vehicles. + * + * @param vehicleNamePrefix The prefix used for vehicles. + */ + void setVehicleNamePrefix(String vehicleNamePrefix) { + this.vehicleNamePrefix = vehicleNamePrefix; + } + + /** + * Sets the prefix used for location types. + * + * @param locTypeNamePrefix The prefix used for location types. + */ + void setLocTypeNamePrefix(String locTypeNamePrefix) { + this.locTypeNamePrefix = locTypeNamePrefix; + } + + /** + * Sets the prefix used for locations. + * + * @param locationNamePrefix The prefix used for locations. + */ + void setLocationNamePrefix(String locationNamePrefix) { + this.locationNamePrefix = locationNamePrefix; + } + + /** + * Creates a point. + * + * @return A new point. + */ + public Point createPoint() { + ++uniqueIdCounter; + return new Point(pointNamePrefix + uniqueIdCounter); + } + + /** + * Creates a path from a start point to an end point. + * + * @param srcRef Reference to the start point. + * @param dstRef Reference to the end point. + * @return A new path from the start point to the end point. + */ + Path createPath( + TCSObjectReference srcRef, + TCSObjectReference dstRef + ) { + ++uniqueIdCounter; + return new Path(pathNamePrefix + uniqueIdCounter, srcRef, dstRef); + } + + /** + * Creates a path leading to a destination point. + * Creates an anonymous start point. + * + * @param dstRef Reference to the destination point. + * @return A new path from an anonymous start point to the end point. + */ + public Path createPath(TCSObjectReference dstRef) { + Point srcPoint = createPoint(); + ++uniqueIdCounter; + return createPath(srcPoint.getReference(), dstRef); + } + + /** + * Creates a path with anonymous start and destination points. + * + * @return A new path with anonymous start and end points. + */ + Path createPath() { + Point dstPoint = createPoint(); + ++uniqueIdCounter; + return createPath(dstPoint.getReference()); + } + + /** + * Creates a vehicle. + * + * @return A new vehicle. + */ + public Vehicle createVehicle() { + ++uniqueIdCounter; + return new Vehicle(vehicleNamePrefix + uniqueIdCounter); + } + + /** + * Creates a location type. + * + * @return A new location type. + */ + LocationType createLocationType() { + ++uniqueIdCounter; + return new LocationType(locTypeNamePrefix + uniqueIdCounter); + } + + /** + * Creates a location. + * + * @param locTypeRef Reference to the location type to use for the new location. + * @return A new location with the location type. + */ + public Location createLocation(TCSObjectReference locTypeRef) { + ++uniqueIdCounter; + return new Location(locationNamePrefix + uniqueIdCounter, locTypeRef); + } + + /** + * Creates a location with an anonymous location type. + * + * @return A new location with an anonymous location type. + */ + public Location createLocation() { + LocationType locType = createLocationType(); + ++uniqueIdCounter; + return new Location(locationNamePrefix + uniqueIdCounter, locType.getReference()); + } + +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/TestEnvironment.java b/opentcs-kernel/src/test/java/org/opentcs/TestEnvironment.java new file mode 100644 index 0000000..3e6dd6e --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/TestEnvironment.java @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs; + +import java.io.File; +import org.opentcs.util.FileSystems; + +/** + * A class that keeps/provides commonly used data about the test environment. + */ +public final class TestEnvironment { + + /** + * The home directory for the openTCS system during tests. + */ + private static final File KERNEL_HOME_DIRECTORY; + /** + * The directory in which persistent data of the openTCS kernel is stored. + */ + private static final File KERNEL_DATA_DIRECTORY; + /** + * The directory in which log files are kept. + */ + private static final File LOG_FILE_DIRECTORY; + + static { + KERNEL_HOME_DIRECTORY + = new File(System.getProperty("java.io.tmpdir"), "openTCS-Tests"); + KERNEL_DATA_DIRECTORY = new File(KERNEL_HOME_DIRECTORY, "data"); + LOG_FILE_DIRECTORY = new File(System.getProperty("java.io.tmpdir"), "log"); + } + + /** + * Prevents creation of instances. + */ + private TestEnvironment() { + } + + /** + * Returns the home directory for the openTCS system during tests. + * + * @return The home directory for the openTCS system during tests. + */ + public static File getKernelHomeDirectory() { + return KERNEL_HOME_DIRECTORY; + } + + /** + * Initializes the test environment. + */ + private static void init() { + // Clean and recreate the home directory. + FileSystems.deleteRecursively(KERNEL_HOME_DIRECTORY); + KERNEL_HOME_DIRECTORY.mkdirs(); + KERNEL_DATA_DIRECTORY.mkdirs(); + LOG_FILE_DIRECTORY.mkdirs(); + } + + /** + * Initializes the test environment. + * + * @param args The command line arguments. + */ + public static void main(String[] args) { + System.out.println("Initializing the test environment..."); + init(); + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/VehicleDispatchTriggerTest.java b/opentcs-kernel/src/test/java/org/opentcs/VehicleDispatchTriggerTest.java new file mode 100644 index 0000000..cb9d281 --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/VehicleDispatchTriggerTest.java @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.common.SameThreadExecutorService; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.kernel.KernelApplicationConfiguration; +import org.opentcs.kernel.VehicleDispatchTrigger; +import org.opentcs.util.event.EventBus; + +/** + * Unit tests for {@link VehicleDispatchTrigger}. + */ +public class VehicleDispatchTriggerTest { + + private EventBus eventBus; + private KernelApplicationConfiguration config; + private DispatcherService dispatcher; + + private VehicleDispatchTrigger trigger; + + @BeforeEach + public void setUp() { + eventBus = mock(EventBus.class); + dispatcher = mock(DispatcherService.class); + config = mock(KernelApplicationConfiguration.class); + when(config.rerouteOnDriveOrderFinished()).thenReturn(false); + trigger = new VehicleDispatchTrigger( + new SameThreadExecutorService(), + eventBus, + dispatcher, + config + ); + } + + @Test + void dispatchWhenIdleAndEnergyLevelChanged() { + Vehicle vehicleOld = new Vehicle("someVehicle") + .withIntegrationLevel(Vehicle.IntegrationLevel.TO_BE_UTILIZED) + .withProcState(Vehicle.ProcState.IDLE) + .withState(Vehicle.State.IDLE) + .withEnergyLevel(100); + Vehicle vehicleNew = vehicleOld.withEnergyLevel(99); + + trigger.onEvent( + new TCSObjectEvent( + vehicleNew, + vehicleOld, + TCSObjectEvent.Type.OBJECT_MODIFIED + ) + ); + + verify(dispatcher).dispatch(); + } + + @Test + void noDispatchWhenNotIdleAndEnergyLevelChanged() { + Vehicle vehicleOld = new Vehicle("someVehicle") + .withIntegrationLevel(Vehicle.IntegrationLevel.TO_BE_UTILIZED) + .withProcState(Vehicle.ProcState.PROCESSING_ORDER) + .withState(Vehicle.State.EXECUTING) + .withEnergyLevel(100); + Vehicle vehicleNew = vehicleOld.withEnergyLevel(99); + + trigger.onEvent( + new TCSObjectEvent( + vehicleNew, + vehicleOld, + TCSObjectEvent.Type.OBJECT_MODIFIED + ) + ); + + verify(dispatcher, never()).dispatch(); + } + + @Test + void dispatchWhenProcStateBecameIdle() { + Vehicle vehicleOld = new Vehicle("someVehicle") + .withIntegrationLevel(Vehicle.IntegrationLevel.TO_BE_UTILIZED) + .withProcState(Vehicle.ProcState.PROCESSING_ORDER) + .withState(Vehicle.State.EXECUTING); + Vehicle vehicleNew = vehicleOld.withProcState(Vehicle.ProcState.IDLE) + .withState(Vehicle.State.IDLE); + + trigger.onEvent( + new TCSObjectEvent( + vehicleNew, + vehicleOld, + TCSObjectEvent.Type.OBJECT_MODIFIED + ) + ); + + verify(dispatcher).dispatch(); + } + + @Test + void dispatchWhenProcStateBecameAwaitingOrder() { + Vehicle vehicleOld = new Vehicle("someVehicle") + .withIntegrationLevel(Vehicle.IntegrationLevel.TO_BE_UTILIZED) + .withProcState(Vehicle.ProcState.PROCESSING_ORDER); + Vehicle vehicleNew = vehicleOld.withProcState(Vehicle.ProcState.AWAITING_ORDER); + + trigger.onEvent( + new TCSObjectEvent( + vehicleNew, + vehicleOld, + TCSObjectEvent.Type.OBJECT_MODIFIED + ) + ); + + verify(dispatcher).dispatch(); + } + + @Test + void dispatchWhenOrderSequenceNulled() { + Vehicle vehicleOld = new Vehicle("someVehicle") + .withIntegrationLevel(Vehicle.IntegrationLevel.TO_BE_UTILIZED) + .withOrderSequence(new OrderSequence("someSequence").getReference()); + Vehicle vehicleNew = vehicleOld.withOrderSequence(null); + + trigger.onEvent( + new TCSObjectEvent( + vehicleNew, + vehicleOld, + TCSObjectEvent.Type.OBJECT_MODIFIED + ) + ); + + verify(dispatcher).dispatch(); + } + + @Test + public void rerouteWhenProcStateBecameAwaitingOrder() { + Vehicle vehicleOld = new Vehicle("someVehicle") + .withIntegrationLevel(Vehicle.IntegrationLevel.TO_BE_UTILIZED) + .withProcState(Vehicle.ProcState.PROCESSING_ORDER); + Vehicle vehicleNew = vehicleOld.withProcState(Vehicle.ProcState.AWAITING_ORDER); + + when(config.rerouteOnDriveOrderFinished()).thenReturn(true); + + trigger.onEvent( + new TCSObjectEvent( + vehicleNew, + vehicleOld, + TCSObjectEvent.Type.OBJECT_MODIFIED + ) + ); + + verify(dispatcher).dispatch(); + verify(dispatcher).reroute(vehicleNew.getReference(), ReroutingType.REGULAR); + } + +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/KernelStateOperatingTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/KernelStateOperatingTest.java new file mode 100644 index 0000000..5dccf23 --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/KernelStateOperatingTest.java @@ -0,0 +1,197 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import jakarta.annotation.Nonnull; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.Dispatcher; +import org.opentcs.components.kernel.KernelExtension; +import org.opentcs.components.kernel.PeripheralJobDispatcher; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.components.kernel.services.InternalVehicleService; +import org.opentcs.data.model.Vehicle; +import org.opentcs.kernel.extensions.controlcenter.vehicles.AttachmentManager; +import org.opentcs.kernel.peripherals.LocalPeripheralControllerPool; +import org.opentcs.kernel.peripherals.PeripheralAttachmentManager; +import org.opentcs.kernel.persistence.ModelPersister; +import org.opentcs.kernel.vehicles.LocalVehicleControllerPool; +import org.opentcs.kernel.workingset.PeripheralJobPoolManager; +import org.opentcs.kernel.workingset.PlantModelManager; +import org.opentcs.kernel.workingset.PrefixedUlidObjectNameProvider; +import org.opentcs.kernel.workingset.TCSObjectRepository; +import org.opentcs.kernel.workingset.TransportOrderPoolManager; +import org.opentcs.kernel.workingset.WorkingSetCleanupTask; +import org.opentcs.util.event.SimpleEventBus; + +/** + * Tests the operating state of the kernel. + */ +class KernelStateOperatingTest { + + private final Set vehicles = new HashSet<>(); + + private int objectID; + + private KernelState operating; + + private KernelApplicationConfiguration configuration; + + private TCSObjectRepository objectPool; + + private Router router; + + private Scheduler scheduler; + + private Dispatcher dispatcher; + + private PeripheralJobDispatcher peripheralJobDispatcher; + + private LocalVehicleControllerPool controllerPool; + + private AttachmentManager attachmentManager; + + private InternalVehicleService vehicleService; + + @BeforeEach + void setUp() { + objectID = 0; + objectPool = mock(TCSObjectRepository.class); + configuration = mock(KernelApplicationConfiguration.class); + router = mock(Router.class); + scheduler = mock(Scheduler.class); + dispatcher = mock(Dispatcher.class); + peripheralJobDispatcher = mock(PeripheralJobDispatcher.class); + controllerPool = mock(LocalVehicleControllerPool.class); + attachmentManager = mock(AttachmentManager.class); + vehicleService = mock(InternalVehicleService.class); + when(vehicleService.fetchObjects(Vehicle.class)).thenReturn(vehicles); + } + + @Test + void shouldInitializeExtensionsAndComponents() { + KernelExtension extension = mock(KernelExtension.class); + operating = createKernel(Collections.singleton(extension)); + operating.initialize(); + verify(router, times(1)).initialize(); + verify(scheduler, times(1)).initialize(); + verify(dispatcher, times(1)).initialize(); + verify(peripheralJobDispatcher, times(1)).initialize(); + verify(controllerPool, times(1)).initialize(); + verify(extension, times(1)).initialize(); + } + + @Test + void shouldTerminateExtensionsAndComponents() { + KernelExtension extension = mock(KernelExtension.class); + operating = createKernel(Collections.singleton(extension)); + operating.initialize(); + operating.terminate(); + verify(router, times(1)).terminate(); + verify(scheduler, times(1)).terminate(); + verify(dispatcher, times(1)).terminate(); + verify(peripheralJobDispatcher, times(1)).terminate(); + verify(controllerPool, times(1)).terminate(); + verify(extension, times(1)).terminate(); + } + + @Test + void initializeKernelWithVehiclesAsUnavailable() { + Vehicle vehicle = new Vehicle("Vehicle-" + objectID++); + vehicles.add(vehicle); + operating = createKernel(new HashSet<>()); + operating.initialize(); + verify(vehicleService, times(1)).updateVehicleProcState( + vehicle.getReference(), + Vehicle.ProcState.IDLE + ); + verify(vehicleService, times(1)).updateVehicleState( + vehicle.getReference(), + Vehicle.State.UNKNOWN + ); + verify(vehicleService, times(1)).updateVehicleTransportOrder(vehicle.getReference(), null); + verify(vehicleService, times(1)).updateVehicleOrderSequence(vehicle.getReference(), null); + } + + @Test + void terminateKernelWithVehiclesAsUnavailable() { + Vehicle vehicle = new Vehicle("Vehicle-" + objectID++); + vehicles.add(vehicle); + operating = createKernel(new HashSet<>()); + operating.initialize(); + operating.terminate(); + verify(vehicleService, times(2)).updateVehicleProcState( + vehicle.getReference(), + Vehicle.ProcState.IDLE + ); + verify(vehicleService, times(2)).updateVehicleState( + vehicle.getReference(), + Vehicle.State.UNKNOWN + ); + verify(vehicleService, times(2)).updateVehicleTransportOrder(vehicle.getReference(), null); + verify(vehicleService, times(2)).updateVehicleOrderSequence(vehicle.getReference(), null); + } + + /** + * Creates the kernel to test. + * + * @param extensions The kernel extensions + * @return The kernel to test + */ + @SuppressWarnings("unchecked") + private KernelStateOperating createKernel( + @Nonnull + Set extensions + ) { + ScheduledExecutorService executorMock = mock(ScheduledExecutorService.class); + when(executorMock.scheduleAtFixedRate(any(), anyLong(), anyLong(), any())) + .thenReturn(mock(ScheduledFuture.class)); + + return spy( + new KernelStateOperating( + new Object(), + mock(PlantModelManager.class), + new TransportOrderPoolManager( + objectPool, + new SimpleEventBus(), + new PrefixedUlidObjectNameProvider() + ), + new PeripheralJobPoolManager( + objectPool, + new SimpleEventBus(), + new PrefixedUlidObjectNameProvider() + ), + mock(ModelPersister.class), + configuration, + router, + scheduler, + dispatcher, + peripheralJobDispatcher, + controllerPool, + mock(LocalPeripheralControllerPool.class), + executorMock, + mock(WorkingSetCleanupTask.class), + extensions, + attachmentManager, + mock(PeripheralAttachmentManager.class), + vehicleService, + mock(PathLockEventListener.class), + mock(VehicleDispatchTrigger.class) + ) + ); + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/StandardKernelTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/StandardKernelTest.java new file mode 100644 index 0000000..888c250 --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/StandardKernelTest.java @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.inject.Provider; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ScheduledExecutorService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.access.Kernel; +import org.opentcs.access.LocalKernel; +import org.opentcs.components.kernel.services.NotificationService; +import org.opentcs.util.event.SimpleEventBus; + +/** + * A test case for StandardKernel. + */ +class StandardKernelTest { + + /** + * The kernel instance to be tested. + */ + private LocalKernel kernel; + + private KernelState kernelStateShutdown; + private KernelState kernelStateModelling; + private KernelState kernelStateOperating; + + @BeforeEach + void setUp() { + // Build a map of providers for our mocked state objects. + Map> stateMap = new HashMap<>(); + kernelStateShutdown = mock(KernelState.class); + when(kernelStateShutdown.getState()).thenReturn(Kernel.State.SHUTDOWN); + stateMap.put( + Kernel.State.SHUTDOWN, + new KernelStateProvider(kernelStateShutdown) + ); + + kernelStateModelling = mock(KernelState.class); + when(kernelStateModelling.getState()).thenReturn(Kernel.State.MODELLING); + stateMap.put( + Kernel.State.MODELLING, + new KernelStateProvider(kernelStateModelling) + ); + + kernelStateOperating = mock(KernelState.class); + when(kernelStateOperating.getState()).thenReturn(Kernel.State.OPERATING); + stateMap.put( + Kernel.State.OPERATING, + new KernelStateProvider(kernelStateOperating) + ); + + kernel = new StandardKernel( + new SimpleEventBus(), + mock(ScheduledExecutorService.class), + stateMap, + mock(NotificationService.class) + ); + } + + @Test + void testStateSwitchToModelling() { + kernel.setState(Kernel.State.MODELLING); + assertEquals(Kernel.State.MODELLING, kernel.getState()); + // Verify that the selected state has been initialized. + verify(kernelStateModelling, times(1)).initialize(); + } + + @Test + void testStateSwitchToOperating() { + kernel.setState(Kernel.State.OPERATING); + assertEquals(Kernel.State.OPERATING, kernel.getState()); + // Verify that the selected state has been initialized. + verify(kernelStateOperating, times(1)).initialize(); + } + + private static class KernelStateProvider + implements + Provider { + + private final KernelState state; + + private KernelStateProvider(KernelState providedState) { + this.state = Objects.requireNonNull(providedState, "providedState is null"); + } + + @Override + public KernelState get() { + return state; + } + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/extensions/controlcenter/vehicles/AttachmentManagerTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/extensions/controlcenter/vehicles/AttachmentManagerTest.java new file mode 100644 index 0000000..1b9a7b0 --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/extensions/controlcenter/vehicles/AttachmentManagerTest.java @@ -0,0 +1,317 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.controlcenter.vehicles; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.drivers.vehicle.BasicVehicleCommAdapter; +import org.opentcs.drivers.vehicle.DefaultVehicleCommAdapterDescription; +import org.opentcs.drivers.vehicle.MovementCommand; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; +import org.opentcs.drivers.vehicle.VehicleCommAdapterFactory; +import org.opentcs.drivers.vehicle.VehicleProcessModel; +import org.opentcs.kernel.KernelApplicationConfiguration; +import org.opentcs.kernel.vehicles.LocalVehicleControllerPool; +import org.opentcs.kernel.vehicles.VehicleCommAdapterRegistry; +import org.opentcs.util.ExplainedBoolean; +import org.opentcs.util.event.EventHandler; +import org.slf4j.LoggerFactory; + +/** + * Tests for the {@link AttachmentManager}. + */ +class AttachmentManagerTest { + + private static final String VEHICLE_1_NAME = "Vehicle1"; + private static final String VEHICLE_2_NAME = "Vehicle2"; + private static final String VEHICLE_3_NAME = "Vehicle3"; + + private final AttachmentManager attachmentManager; + private final TCSObjectService objectService; + private final LocalVehicleControllerPool vehicleControllerPool; + private final VehicleCommAdapterRegistry commAdapterRegistry; + private final VehicleEntryPool vehicleEntryPool; + private final VehicleCommAdapterFactory commAdapterFactory; + private final EventHandler eventHandler; + + private final Vehicle vehicle1; + private final Vehicle vehicle2; + private final Vehicle vehicle3; + + AttachmentManagerTest() { + objectService = mock(TCSObjectService.class); + vehicleControllerPool = mock(LocalVehicleControllerPool.class); + commAdapterRegistry = mock(VehicleCommAdapterRegistry.class); + commAdapterFactory = mock(VehicleCommAdapterFactory.class); + vehicleEntryPool = new VehicleEntryPool(objectService); + eventHandler = mock(EventHandler.class); + attachmentManager = spy( + new AttachmentManager( + objectService, + vehicleControllerPool, + commAdapterRegistry, + vehicleEntryPool, + eventHandler, + mock(KernelApplicationConfiguration.class) + ) + ); + + vehicle1 = new Vehicle(VEHICLE_1_NAME); + vehicle2 = new Vehicle(VEHICLE_2_NAME) + .withProperty( + Vehicle.PREFERRED_ADAPTER, + SimpleCommAdapterFactory.class.getName() + ); + vehicle3 = new Vehicle(VEHICLE_3_NAME) + .withProperty( + Vehicle.PREFERRED_ADAPTER, + RefusingCommAdapterFactory.class.getName() + ); + } + + @BeforeEach + void setUp() { + Set vehicles = new HashSet<>(); + vehicles.add(vehicle1); + vehicles.add(vehicle2); + vehicles.add(vehicle3); + when(objectService.fetchObjects(Vehicle.class)).thenReturn(vehicles); + attachmentManager.initialize(); + for (VehicleEntry entry : vehicleEntryPool.getEntries().values()) { + LoggerFactory.getLogger(getClass()).info("{}", entry.getVehicle()); + } + } + + @AfterEach + void tearDown() { + attachmentManager.terminate(); + } + + @Test + void shouldNotAttachUnknownVehicle() { + attachmentManager.attachAdapterToVehicle("UnknownVehicle", commAdapterFactory); + + verify(commAdapterFactory, times(0)).getAdapterFor(any(Vehicle.class)); + verify(vehicleControllerPool, times(0)).attachVehicleController( + any(String.class), + any(VehicleCommAdapter.class) + ); + } + + @Test + void shouldAttachAdapterToVehicle() { + VehicleCommAdapter commAdapter = new SimpleCommAdapter(vehicle1); + when(commAdapterFactory.getAdapterFor(vehicle1)).thenReturn(commAdapter); + when(commAdapterFactory.getDescription()).thenReturn(new SimpleVehicleCommAdapterDescription()); + + attachmentManager.attachAdapterToVehicle(VEHICLE_1_NAME, commAdapterFactory); + + verify(vehicleControllerPool, times(1)).detachVehicleController(VEHICLE_1_NAME); + verify(vehicleControllerPool, times(1)).attachVehicleController(VEHICLE_1_NAME, commAdapter); + assertNotNull(vehicleEntryPool.getEntryFor(VEHICLE_1_NAME)); + assertThat( + vehicleEntryPool.getEntryFor(VEHICLE_1_NAME).getCommAdapter(), + is(commAdapter) + ); + assertThat( + vehicleEntryPool.getEntryFor(VEHICLE_1_NAME).getCommAdapterFactory(), + is(commAdapterFactory) + ); + assertThat( + vehicleEntryPool.getEntryFor(VEHICLE_1_NAME).getProcessModel(), + is(commAdapter.getProcessModel()) + ); + } + + @Test + void shouldAutoAttachAdapterToVehicle() { + List factories = Arrays.asList( + new NullVehicleCommAdapterFactory(), + new SimpleCommAdapterFactory() + ); + when(commAdapterRegistry.getFactories()).thenReturn(factories); + + attachmentManager.autoAttachAdapterToVehicle(VEHICLE_2_NAME); + + verify(attachmentManager, times(1)).attachAdapterToVehicle(VEHICLE_2_NAME, factories.get(1)); + } + + @Test + void shouldAutoAttachToFirstAvailableAdapter() { + List factories = Arrays.asList( + new SimpleCommAdapterFactory(), + new NullVehicleCommAdapterFactory() + ); + when(commAdapterRegistry.getFactories()).thenReturn(factories); + when(commAdapterRegistry.findFactoriesFor(vehicle1)).thenReturn(factories); + + attachmentManager.autoAttachAdapterToVehicle(VEHICLE_1_NAME); + + verify(attachmentManager, times(1)).attachAdapterToVehicle(VEHICLE_1_NAME, factories.get(0)); + } + + @Test + void shouldFallBackToFirstAvailableAdapterIfPreferredAdapterIsNotProvided() { + SimpleCommAdapterFactory simpleCommAdapterFactory = new SimpleCommAdapterFactory(); + when(commAdapterRegistry.getFactories()) + .thenReturn(Arrays.asList(new RefusingCommAdapterFactory(), simpleCommAdapterFactory)); + when(commAdapterRegistry.findFactoriesFor(vehicle3)) + .thenReturn(Arrays.asList(simpleCommAdapterFactory)); + + attachmentManager.autoAttachAdapterToVehicle(VEHICLE_3_NAME); + + verify(attachmentManager, times(1)) + .attachAdapterToVehicle(VEHICLE_3_NAME, simpleCommAdapterFactory); + } + + private class SimpleCommAdapter + extends + BasicVehicleCommAdapter { + + SimpleCommAdapter(Vehicle vehicle) { + super( + new VehicleProcessModel(vehicle), + 1, + "", + Executors.newSingleThreadScheduledExecutor() + ); + } + + @Override + public void sendCommand(MovementCommand cmd) + throws IllegalArgumentException { + } + + @Override + protected void connectVehicle() { + } + + @Override + protected void disconnectVehicle() { + } + + @Override + protected boolean isVehicleConnected() { + return true; + } + + @Override + public ExplainedBoolean canProcess(TransportOrder order) { + return new ExplainedBoolean(true, ""); + } + + @Override + public void processMessage(Object o) { + } + + @Override + public void onVehiclePaused(boolean paused) { + } + } + + private class SimpleCommAdapterFactory + implements + VehicleCommAdapterFactory { + + @Override + public boolean providesAdapterFor(Vehicle vehicle) { + return vehicle.equals(vehicle1) || vehicle.equals(vehicle2) || vehicle.equals(vehicle3); + } + + @Override + public VehicleCommAdapter getAdapterFor(Vehicle vehicle) { + if (vehicle.equals(vehicle1) || vehicle.equals(vehicle2) || vehicle.equals(vehicle3)) { + return new SimpleCommAdapter(vehicle); + } + else { + return null; + } + } + + @Override + public VehicleCommAdapterDescription getDescription() { + return new DefaultVehicleCommAdapterDescription("simpleCommAdapter", false); + } + + @Override + public void initialize() { + } + + @Override + public boolean isInitialized() { + return true; + } + + @Override + public void terminate() { + } + } + + private class SimpleVehicleCommAdapterDescription + extends + VehicleCommAdapterDescription { + + @Override + public String getDescription() { + return getClass().getName(); + } + + @Override + public boolean isSimVehicleCommAdapter() { + return false; + } + } + + private class RefusingCommAdapterFactory + implements + VehicleCommAdapterFactory { + + @Override + public boolean providesAdapterFor(Vehicle vehicle) { + return false; + } + + @Override + public VehicleCommAdapter getAdapterFor(Vehicle vehicle) { + return null; + } + + @Override + public VehicleCommAdapterDescription getDescription() { + return new DefaultVehicleCommAdapterDescription("refusingCommAdapter", false); + } + + @Override + public void initialize() { + } + + @Override + public boolean isInitialized() { + return true; + } + + @Override + public void terminate() { + } + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/extensions/watchdog/BlockConsistencyCheckTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/extensions/watchdog/BlockConsistencyCheckTest.java new file mode 100644 index 0000000..e3eb5aa --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/extensions/watchdog/BlockConsistencyCheckTest.java @@ -0,0 +1,218 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.watchdog; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.opentcs.components.kernel.services.NotificationService; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Block; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.model.Vehicle.IntegrationLevel; + +/** + * Tests the {@link BlockConsistencyCheck} watchdog task. + */ +public class BlockConsistencyCheckTest { + + private BlockConsistencyCheck blockCheck; + private NotificationService notificationService; + private TCSObjectService objectService; + private Vehicle vehicle1; + private Vehicle vehicle2; + private Vehicle vehicle3; + private Block block; + private Point point; + private Point pointOutSideBlock; + + @BeforeEach + void setup() { + notificationService = mock(NotificationService.class); + objectService = mock(TCSObjectService.class); + // Setup the object service with 1 block with 2 points and a third point outside of the block. + vehicle1 = new Vehicle("vehicle 1") + .withIntegrationLevel(Vehicle.IntegrationLevel.TO_BE_UTILIZED); + vehicle2 = new Vehicle("vehicle 2") + .withIntegrationLevel(Vehicle.IntegrationLevel.TO_BE_UTILIZED); + vehicle3 = new Vehicle("vehicle 3") + .withIntegrationLevel(Vehicle.IntegrationLevel.TO_BE_UTILIZED); + point = new Point("Point 1"); + pointOutSideBlock = new Point("Point outside block"); + block = new Block("block 1").withMembers(Set.of(point.getReference())); + rebuildObjectService(); + + blockCheck = new BlockConsistencyCheck( + mock(ScheduledExecutorService.class), + objectService, + notificationService, + mock(WatchdogConfiguration.class) + ); + } + + void rebuildObjectService() { + when(objectService.fetchObjects(Vehicle.class)) + .thenReturn(Set.of(vehicle1, vehicle2, vehicle3)); + when(objectService.fetchObject(Point.class, point.getReference())) + .thenReturn(point); + when(objectService.fetchObject(Point.class, pointOutSideBlock.getReference())) + .thenReturn(pointOutSideBlock); + when(objectService.fetchObjects(Block.class)).thenReturn(Set.of(block)); + } + + @Test + void shouldReportViolation() { + vehicle1 = vehicle1.withCurrentPosition(point.getReference()); + vehicle2 = vehicle2.withCurrentPosition(point.getReference()); + rebuildObjectService(); + + blockCheck.run(); + + verify(notificationService).publishUserNotification(any()); + } + + @Test + void singleVehicleInBlockShouldNotReportViolation() { + vehicle1 = vehicle1.withCurrentPosition(point.getReference()); + rebuildObjectService(); + + blockCheck.run(); + + verify(notificationService, never()).publishUserNotification(any()); + } + + @ParameterizedTest + @EnumSource(value = Vehicle.IntegrationLevel.class, names = {"TO_BE_UTILIZED", "TO_BE_RESPECTED"}) + void onlyIntegratedVehiclesReportViolations(IntegrationLevel integrationLevel) { + vehicle1 = vehicle1.withCurrentPosition(point.getReference()) + .withIntegrationLevel(integrationLevel); + vehicle2 = vehicle2.withCurrentPosition(point.getReference()) + .withIntegrationLevel(integrationLevel); + rebuildObjectService(); + + blockCheck.run(); + + verify(notificationService).publishUserNotification(any()); + } + + @ParameterizedTest + @EnumSource(value = Vehicle.IntegrationLevel.class, names = {"TO_BE_NOTICED", "TO_BE_IGNORED"}) + void nonIntegratedVehiclesDontReportViolations(IntegrationLevel integrationLevel) { + vehicle1 = vehicle1.withCurrentPosition(point.getReference()) + .withIntegrationLevel(integrationLevel); + vehicle2 = vehicle2.withCurrentPosition(point.getReference()) + .withIntegrationLevel(integrationLevel); + rebuildObjectService(); + + blockCheck.run(); + + verify(notificationService, never()).publishUserNotification(any()); + } + + @Test + void aNewVehicleInTheBlockShouldCauseANewViolation() { + vehicle1 = vehicle1.withCurrentPosition(point.getReference()); + vehicle2 = vehicle2.withCurrentPosition(point.getReference()); + rebuildObjectService(); + + blockCheck.run(); + verify(notificationService, times(1)).publishUserNotification(any()); + + vehicle3 = vehicle3.withCurrentPosition(point.getReference()); + rebuildObjectService(); + + blockCheck.run(); + verify(notificationService, times(2)).publishUserNotification(any()); + } + + @Test + void vehicleLeavingTheBlockShouldCauseANewMessage() { + vehicle1 = vehicle1.withCurrentPosition(point.getReference()); + vehicle2 = vehicle2.withCurrentPosition(point.getReference()); + vehicle3 = vehicle3.withCurrentPosition(point.getReference()); + rebuildObjectService(); + + blockCheck.run(); + verify(notificationService, times(1)).publishUserNotification(any()); + + vehicle3 = vehicle3.withCurrentPosition(null); + rebuildObjectService(); + + blockCheck.run(); + verify(notificationService, times(2)).publishUserNotification(any()); + } + + @Test + void vehicleLeavingBlockShouldCausesAViolationResolution() { + vehicle1 = vehicle1.withCurrentPosition(point.getReference()); + vehicle2 = vehicle2.withCurrentPosition(point.getReference()); + rebuildObjectService(); + + blockCheck.run(); + verify(notificationService, times(1)).publishUserNotification(any()); + + vehicle2 = vehicle2.withCurrentPosition(null); + rebuildObjectService(); + + blockCheck.run(); + verify(notificationService, times(2)).publishUserNotification(any()); + } + + @Test + void singleVehicleLeavingBlockShouldNotCauseAResolution() { + vehicle1 = vehicle1.withCurrentPosition(point.getReference()); + rebuildObjectService(); + + blockCheck.run(); + verify(notificationService, never()).publishUserNotification(any()); + + vehicle1 = vehicle1.withCurrentPosition(null); + rebuildObjectService(); + + blockCheck.run(); + verify(notificationService, never()).publishUserNotification(any()); + } + + @Test + void dontSendNotificationIfBlockStaysTheSame() { + vehicle1 = vehicle1.withCurrentPosition(point.getReference()); + vehicle2 = vehicle2.withCurrentPosition(point.getReference()); + rebuildObjectService(); + + blockCheck.run(); + verify(notificationService, times(1)).publishUserNotification(any()); + + blockCheck.run(); + verify(notificationService, times(1)).publishUserNotification(any()); + } + + @Test + void sendNotificationIfVehiclesInBlockChanged() { + vehicle1 = vehicle1.withCurrentPosition(point.getReference()); + vehicle2 = vehicle2.withCurrentPosition(point.getReference()); + rebuildObjectService(); + + blockCheck.run(); + verify(notificationService, times(1)).publishUserNotification(any()); + + vehicle1 = vehicle1.withCurrentPosition(null); + vehicle3 = vehicle3.withCurrentPosition(point.getReference()); + vehicle2 = vehicle2.withCurrentPosition(point.getReference()); + rebuildObjectService(); + + blockCheck.run(); + verify(notificationService, times(2)).publishUserNotification(any()); + } + +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/extensions/watchdog/StrandedVehicleCheckTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/extensions/watchdog/StrandedVehicleCheckTest.java new file mode 100644 index 0000000..23c4bbd --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/extensions/watchdog/StrandedVehicleCheckTest.java @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.watchdog; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.services.NotificationService; +import org.opentcs.data.model.Vehicle; +import org.opentcs.kernel.extensions.watchdog.StrandedVehicles.VehicleSnapshot; + +/** + * Tests for {@link StrandedVehicleCheck}. + */ +public class StrandedVehicleCheckTest { + + private StrandedVehicleCheck strandedVehicleCheck; + private StrandedVehicles stranded; + private NotificationService notificationService; + private WatchdogConfiguration watchdogConfiguration; + private ScheduledExecutorService executorService; + private Vehicle vehicle1; + private VehicleSnapshot vehicleSnapshot; + + @BeforeEach + void setup() { + notificationService = mock(); + watchdogConfiguration = mock(); + executorService = mock(); + stranded = mock(); + + vehicle1 = new Vehicle("vehicle 1") + .withState(Vehicle.State.IDLE); + vehicleSnapshot = new VehicleSnapshot(vehicle1); + + when(watchdogConfiguration.strandedVehicleCheckInterval()).thenReturn(1000); + when(watchdogConfiguration.strandedVehicleDurationThreshold()).thenReturn(300000); + + strandedVehicleCheck = new StrandedVehicleCheck( + executorService, + notificationService, + watchdogConfiguration, + stranded + ); + } + + @Test + void strandedVehicleShouldSendNotification() { + when(stranded.newlyStrandedVehicles()) + .thenReturn(Set.of(vehicleSnapshot)); + + strandedVehicleCheck.run(); + + verify(notificationService).publishUserNotification(any()); + } + + @Test + void strandedVehicleShouldNotSendNotification() { + when(stranded.newlyStrandedVehicles()) + .thenReturn(Set.of()); + + strandedVehicleCheck.run(); + + verify(notificationService, never()).publishUserNotification(any()); + } + + @Test + void shouldUpdateNotification() { + when(stranded.newlyStrandedVehicles()) + .thenReturn(Set.of(vehicleSnapshot)); + + strandedVehicleCheck.run(); + + verify(notificationService, times(1)).publishUserNotification(any()); + + when(stranded.newlyStrandedVehicles()) + .thenReturn(Set.of()); + + when(stranded.noLongerStrandedVehicles()) + .thenReturn(Set.of(vehicleSnapshot)); + + strandedVehicleCheck.run(); + + verify(notificationService, times(2)).publishUserNotification(any()); + } + +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/extensions/watchdog/StrandedVehiclesTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/extensions/watchdog/StrandedVehiclesTest.java new file mode 100644 index 0000000..e76b33e --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/extensions/watchdog/StrandedVehiclesTest.java @@ -0,0 +1,223 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.extensions.watchdog; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.kernel.extensions.watchdog.StrandedVehicles.VehicleSnapshot; + +/** + * Tests for {@link StrandedVehicles}. + */ +public class StrandedVehiclesTest { + + private StrandedVehicles stranded; + private TCSObjectService objectService; + private Vehicle vehicle; + private Point parkingPoint; + private Point noParkingPoint; + private TransportOrder transportOrder; + private TimeProvider timeProvider; + + @BeforeEach + public void setUp() { + objectService = mock(); + timeProvider = mock(); + + vehicle = new Vehicle("vehicle"); + + transportOrder = new TransportOrder("TransportOrder", mock()); + + parkingPoint = new Point("point").withType(Point.Type.PARK_POSITION); + noParkingPoint = new Point("point 2").withType(Point.Type.HALT_POSITION); + + when(objectService.fetchObject(Point.class, parkingPoint.getReference())) + .thenReturn(parkingPoint); + when(objectService.fetchObject(Point.class, noParkingPoint.getReference())) + .thenReturn(noParkingPoint); + when(timeProvider.getCurrentTime()).thenReturn(0L); + + stranded = new StrandedVehicles(objectService, timeProvider); + } + + @Test + void considerVehicleAtNoParkingPositionAsNewlyStranded() { + vehicle = vehicle + .withState(Vehicle.State.IDLE) + .withCurrentPosition(noParkingPoint.getReference()) + .withTransportOrder(transportOrder.getReference()); + when(objectService.fetchObjects(Vehicle.class)).thenReturn(Set.of(vehicle)); + stranded.initialize(); + + long strandedDurationThreshold = 300000; + long firstInvocationTime = 10000; + long secondInvocationTime = firstInvocationTime + strandedDurationThreshold + 1000; + + // After the first invocation (when not exceeding the stranded duration threshold), the vehicle + // should not be considered stranded. + stranded.identifyStrandedVehicles(firstInvocationTime, strandedDurationThreshold); + Set result = stranded.newlyStrandedVehicles(); + assertTrue(result.isEmpty()); + + // After the second invocation (when exceeding the stranded duration threshold), the vehicle + // should be considered stranded. + stranded.identifyStrandedVehicles(secondInvocationTime, strandedDurationThreshold); + result = stranded.newlyStrandedVehicles(); + assertThat(result, hasSize(1)); + assertThat(result.iterator().next().isStranded(), is(true)); + assertThat(result.iterator().next().getVehicle(), is(equalTo(vehicle))); + + stranded.terminate(); + } + + @Test + void newlyStrandedVehicleShouldNotContainAlreadyStrandedVehicle() { + vehicle = vehicle + .withState(Vehicle.State.IDLE) + .withCurrentPosition(noParkingPoint.getReference()) + .withTransportOrder(transportOrder.getReference()); + when(objectService.fetchObjects(Vehicle.class)).thenReturn(Set.of(vehicle)); + stranded.initialize(); + + long strandedDurationThreshold = 300000; + long firstInvocationTime = strandedDurationThreshold + 1000; + long secondInvocationTime = firstInvocationTime + strandedDurationThreshold + 1000; + + // After the first invocation (when exceeding the stranded duration threshold), the vehicle + // should be considered stranded. + stranded.identifyStrandedVehicles(firstInvocationTime, strandedDurationThreshold); + Set result = stranded.newlyStrandedVehicles(); + assertThat(result, hasSize(1)); + assertThat(result.iterator().next().isStranded(), is(true)); + assertThat(result.iterator().next().getVehicle(), is(equalTo(vehicle))); + + // After the second invocation (when exceeding the stranded duration threshold), the already + // stranded vehicle should not be considered as newly stranded. + stranded.identifyStrandedVehicles(secondInvocationTime, strandedDurationThreshold); + result = stranded.newlyStrandedVehicles(); + assertTrue(result.isEmpty()); + + stranded.terminate(); + } + + @Test + void considerVehicleWithChangedStateAsNoLongerStranded() { + vehicle = vehicle + .withState(Vehicle.State.IDLE) + .withCurrentPosition(noParkingPoint.getReference()) + .withTransportOrder(transportOrder.getReference()); + when(objectService.fetchObjects(Vehicle.class)).thenReturn(Set.of(vehicle)); + stranded.initialize(); + + long strandedDurationThreshold = 300000; + long firstInvocationTime = strandedDurationThreshold + 1000; + long secondInvocationTime = firstInvocationTime + 1000; + + // After the first invocation (when exceeding the stranded duration threshold), the vehicle + // should be considered stranded. + stranded.identifyStrandedVehicles(firstInvocationTime, strandedDurationThreshold); + Set result = stranded.newlyStrandedVehicles(); + assertThat(result, hasSize(1)); + assertThat(result.iterator().next().isStranded(), is(true)); + + vehicle = vehicle.withState(Vehicle.State.EXECUTING); + when(objectService.fetchObjects(Vehicle.class)).thenReturn(Set.of(vehicle)); + + // After the second invocation (when the vehicle is no longer in a "stranded" state), the + // vehicle should be considered no longer stranded. + stranded.identifyStrandedVehicles(secondInvocationTime, strandedDurationThreshold); + result = stranded.newlyStrandedVehicles(); + assertThat(result, is(empty())); + result = stranded.noLongerStrandedVehicles(); + assertThat(result, hasSize(1)); + assertThat(result.iterator().next().isStranded(), is(false)); + + stranded.terminate(); + } + + @Test + void considerVehicleWithChangedPositionAsNoLongerStranded() { + vehicle = vehicle + .withState(Vehicle.State.IDLE) + .withCurrentPosition(noParkingPoint.getReference()) + .withTransportOrder(transportOrder.getReference()); + when(objectService.fetchObjects(Vehicle.class)).thenReturn(Set.of(vehicle)); + stranded.initialize(); + + long strandedDurationThreshold = 300000; + long firstInvocationTime = strandedDurationThreshold + 1000; + long secondInvocationTime = firstInvocationTime + 1000; + + // After the first invocation (when exceeding the stranded duration threshold), the vehicle + // should be considered stranded. + stranded.identifyStrandedVehicles(firstInvocationTime, strandedDurationThreshold); + Set result = stranded.newlyStrandedVehicles(); + assertThat(result, hasSize(1)); + assertThat(result.iterator().next().isStranded(), is(true)); + + vehicle = vehicle.withCurrentPosition(parkingPoint.getReference()); + when(objectService.fetchObjects(Vehicle.class)).thenReturn(Set.of(vehicle)); + + // After the second invocation (when the vehicle is no longer in a "stranded" state), the + // vehicle should be considered no longer stranded. + stranded.identifyStrandedVehicles(secondInvocationTime, strandedDurationThreshold); + result = stranded.newlyStrandedVehicles(); + assertThat(result, is(empty())); + result = stranded.noLongerStrandedVehicles(); + assertThat(result, hasSize(1)); + assertThat(result.iterator().next().isStranded(), is(false)); + + stranded.terminate(); + } + + @Test + void considerVehicleWithChangedTransportOrderAsNoLongerStranded() { + vehicle = vehicle + .withState(Vehicle.State.IDLE) + .withCurrentPosition(noParkingPoint.getReference()) + .withTransportOrder(transportOrder.getReference()); + when(objectService.fetchObjects(Vehicle.class)).thenReturn(Set.of(vehicle)); + stranded.initialize(); + + long strandedDurationThreshold = 300000; + long firstInvocationTime = strandedDurationThreshold + 1000; + long secondInvocationTime = firstInvocationTime + 1000; + + // After the first invocation (when exceeding the stranded duration threshold), the vehicle + // should be considered stranded. + stranded.identifyStrandedVehicles(firstInvocationTime, strandedDurationThreshold); + Set result = stranded.newlyStrandedVehicles(); + assertThat(result, hasSize(1)); + assertThat(result.iterator().next().isStranded(), is(true)); + + vehicle = vehicle + .withTransportOrder( + new TransportOrder("TransportOrder2", mock()).getReference() + ); + when(objectService.fetchObjects(Vehicle.class)).thenReturn(Set.of(vehicle)); + // After the second invocation (when the vehicle is no longer in a "stranded" state), the + // vehicle should be considered no longer stranded. + stranded.identifyStrandedVehicles(secondInvocationTime, strandedDurationThreshold); + result = stranded.newlyStrandedVehicles(); + assertThat(result, is(empty())); + result = stranded.noLongerStrandedVehicles(); + assertThat(result, hasSize(1)); + assertThat(result.iterator().next().isStranded(), is(false)); + + stranded.terminate(); + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/peripherals/PeripheralAttachmentManagerTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/peripherals/PeripheralAttachmentManagerTest.java new file mode 100644 index 0000000..ee73b1b --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/peripherals/PeripheralAttachmentManagerTest.java @@ -0,0 +1,249 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.peripherals; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.common.peripherals.NullPeripheralCommAdapterDescription; +import org.opentcs.components.kernel.services.InternalPeripheralService; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.drivers.peripherals.PeripheralAdapterCommand; +import org.opentcs.drivers.peripherals.PeripheralCommAdapter; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterFactory; +import org.opentcs.drivers.peripherals.PeripheralJobCallback; +import org.opentcs.drivers.peripherals.PeripheralProcessModel; +import org.opentcs.drivers.peripherals.management.PeripheralAttachmentInformation; +import org.opentcs.kernel.KernelApplicationConfiguration; +import org.opentcs.util.ExplainedBoolean; +import org.opentcs.util.event.EventHandler; + +/** + * Tests for the {@link PeripheralAttachmentManager}. + */ +class PeripheralAttachmentManagerTest { + + private static final String LOCATION_NAME = "Location-01"; + + private final PeripheralAttachmentManager attachmentManager; + private final InternalPeripheralService peripheralService; + private final PeripheralCommAdapterRegistry commAdapterRegistry; + private final PeripheralCommAdapterFactory commAdapterFactory; + private final PeripheralEntryPool peripheralEntryPool; + + private final Location location; + + PeripheralAttachmentManagerTest() { + peripheralService = mock(InternalPeripheralService.class); + commAdapterRegistry = mock(PeripheralCommAdapterRegistry.class); + commAdapterFactory = mock(PeripheralCommAdapterFactory.class); + peripheralEntryPool = new PeripheralEntryPool(peripheralService, commAdapterRegistry); + attachmentManager = spy( + new PeripheralAttachmentManager( + peripheralService, + mock(LocalPeripheralControllerPool.class), + commAdapterRegistry, + peripheralEntryPool, + mock(EventHandler.class), + mock(KernelApplicationConfiguration.class) + ) + ); + + location = createLocation(LOCATION_NAME); + } + + @BeforeEach + void setUp() { + Set locations = new HashSet<>(); + locations.add(location); + when(peripheralService.fetchObjects(Location.class)).thenReturn(locations); + when(peripheralService.fetchObject(any(), eq(location.getReference()))).thenReturn(location); + } + + @Test + void shouldHaveInitializedPeripheralEntryPool() { + attachmentManager.initialize(); + + assertThat(peripheralEntryPool.getEntries().size(), is(1)); + assertThat( + peripheralEntryPool.getEntryFor(location.getReference()).getCommAdapter(), + is(instanceOf(NullPeripheralCommAdapter.class)) + ); + } + + @Test + void shouldAttachAdapterToLocation() { + PeripheralCommAdapter commAdapter = new SimpleCommAdapter(location); + PeripheralCommAdapterDescription description = new SimpleCommAdapterDescription(); + when(commAdapterRegistry.findFactoryFor(description)).thenReturn(commAdapterFactory); + when(commAdapterFactory.getAdapterFor(location)).thenReturn(commAdapter); + when(commAdapterFactory.getDescription()).thenReturn(description); + + attachmentManager.initialize(); + attachmentManager.attachAdapterToLocation(location.getReference(), description); + + assertNotNull(peripheralEntryPool.getEntryFor(location.getReference())); + assertThat( + peripheralEntryPool.getEntryFor(location.getReference()).getCommAdapter(), + is(commAdapter) + ); + assertThat( + peripheralEntryPool.getEntryFor(location.getReference()).getCommAdapterFactory(), + is(commAdapterFactory) + ); + } + + @Test + void shouldAutoAttachAdapterToLocation() { + PeripheralCommAdapterFactory factory = new SimpleCommAdapterFactory(); + when(commAdapterRegistry.findFactoriesFor(location)).thenReturn(Arrays.asList(factory)); + + attachmentManager.initialize(); + + assertNotNull(peripheralEntryPool.getEntryFor(location.getReference())); + assertThat( + peripheralEntryPool.getEntryFor(location.getReference()).getCommAdapter(), + is(instanceOf(SimpleCommAdapter.class)) + ); + assertThat( + peripheralEntryPool.getEntryFor(location.getReference()).getCommAdapterFactory(), + is(factory) + ); + } + + @Test + void shouldGetAttachmentInformation() { + attachmentManager.initialize(); + PeripheralAttachmentInformation result + = attachmentManager.getAttachmentInformation(location.getReference()); + + assertThat(result.getLocationReference(), is(location.getReference())); + assertThat( + result.getAttachedCommAdapter(), + is(instanceOf(NullPeripheralCommAdapterDescription.class)) + ); + } + + private Location createLocation(String locationName) { + LocationType locationType = new LocationType("LocationType-01"); + return new Location(locationName, locationType.getReference()); + } + + private class SimpleCommAdapter + implements + PeripheralCommAdapter { + + private final PeripheralProcessModel processModel; + + SimpleCommAdapter(Location location) { + this.processModel = new PeripheralProcessModel(location.getReference()); + } + + @Override + public void initialize() { + } + + @Override + public boolean isInitialized() { + return true; + } + + @Override + public void terminate() { + } + + @Override + public void enable() { + } + + @Override + public void disable() { + } + + @Override + public boolean isEnabled() { + return false; + } + + @Override + public PeripheralProcessModel getProcessModel() { + return processModel; + } + + @Override + public ExplainedBoolean canProcess(PeripheralJob job) { + return new ExplainedBoolean(true, ""); + } + + @Override + public void process(PeripheralJob job, PeripheralJobCallback callback) { + } + + @Override + public void abortJob() { + } + + @Override + public void execute(PeripheralAdapterCommand command) { + } + } + + private class SimpleCommAdapterFactory + implements + PeripheralCommAdapterFactory { + + @Override + public void initialize() { + } + + @Override + public boolean isInitialized() { + return true; + } + + @Override + public void terminate() { + } + + @Override + public PeripheralCommAdapterDescription getDescription() { + return new SimpleCommAdapterDescription(); + } + + @Override + public boolean providesAdapterFor(Location location) { + return true; + } + + @Override + public PeripheralCommAdapter getAdapterFor(Location location) { + return new SimpleCommAdapter(location); + } + + } + + private class SimpleCommAdapterDescription + extends + PeripheralCommAdapterDescription { + + @Override + public String getDescription() { + return getClass().getName(); + } + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/persistence/XMLFileModelPersisterTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/persistence/XMLFileModelPersisterTest.java new file mode 100644 index 0000000..d474298 --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/persistence/XMLFileModelPersisterTest.java @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.persistence; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +import java.io.File; +import java.io.IOException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mockito; +import org.opentcs.TestEnvironment; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.access.to.model.PointCreationTO; +import org.opentcs.access.to.model.VehicleCreationTO; +import org.opentcs.util.persistence.ModelParser; + +/** + * Tests the {@link XMLFileModelPersister}. + */ +public class XMLFileModelPersisterTest { + + /** + * The name of the test model. + */ + private static final String MODEL_NAME = "Testmodel"; + + /** + * Reads and writes the model. + */ + private ModelParser modelParser; + + /** + * The persister instance for testing. + */ + private XMLFileModelPersister persister; + + /** + * Captures the first argument when the method writeModelMethod of the modelParser is called. + */ + @Captor + private ArgumentCaptor modelCaptor; + /** + * Captures the second argument when the method writeModelMethod of the modelParser is called. + */ + + @Captor + private ArgumentCaptor fileCaptor; + + @BeforeEach + void setUp() + throws IOException { + modelParser = mock(ModelParser.class); + persister = new XMLFileModelPersister( + TestEnvironment.getKernelHomeDirectory(), + modelParser + ); + modelCaptor = ArgumentCaptor.forClass(PlantModelCreationTO.class); + fileCaptor = ArgumentCaptor.forClass(File.class); + } + + @Test + void createXmlFileInGivenDirectory() + throws IOException { + persister.saveModel(createTestModel(MODEL_NAME)); + + Mockito.verify(modelParser).writeModel(modelCaptor.capture(), fileCaptor.capture()); + + assertEquals( + TestEnvironment.getKernelHomeDirectory(), + fileCaptor.getValue().getParentFile().getParentFile() + ); + assertEquals(".xml", getFileExtension(fileCaptor.getValue())); + } + + private PlantModelCreationTO createTestModel(String name) { + return new PlantModelCreationTO(name) + .withPoint(new PointCreationTO("testPointName")) + .withVehicle(new VehicleCreationTO("testVehicleName")); + } + + private String getFileExtension(File file) { + return file.getName().substring(file.getName().lastIndexOf(".")); + } + +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/CommandProcessingTrackerTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/CommandProcessingTrackerTest.java new file mode 100644 index 0000000..36e929b --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/CommandProcessingTrackerTest.java @@ -0,0 +1,534 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.drivers.vehicle.MovementCommand; + +/** + * Tests for {@link CommandProcessingTracker}. + */ +class CommandProcessingTrackerTest { + + private Point pointA; + private Point pointB; + private Point pointC; + private Point pointC2; + private Point pointD; + private Path pathAB; + private Path pathBC; + private Path pathCD; + private Path pathC2D; + + private CommandProcessingTracker commandProcessingTracker; + + @BeforeEach + void setUp() { + pointA = new Point("A"); + pointB = new Point("B"); + pointC = new Point("C"); + pointC2 = new Point("C2"); + pointD = new Point("D"); + pathAB = new Path("A --- B", pointA.getReference(), pointB.getReference()); + pathBC = new Path("B --- C", pointB.getReference(), pointC.getReference()); + pathBC = new Path("B --- C2", pointB.getReference(), pointC2.getReference()); + pathCD = new Path("C --- D", pointC.getReference(), pointD.getReference()); + pathC2D = new Path("C2 --- D", pointC2.getReference(), pointD.getReference()); + + commandProcessingTracker = new CommandProcessingTracker(); + } + + @Test + void initiallyEmpty() { + assertThat(commandProcessingTracker.getClaimedResources()).isEmpty(); + assertThat(commandProcessingTracker.getAllocatedResources()).isEmpty(); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()).isEmpty(); + assertThat(commandProcessingTracker.getAllocationPendingResources()).isEmpty(); + } + + @Test + void regularProcessingOfDriveOrder() { + List movementCommands = createMovementCommandList( + List.of( + new Route.Step(pathAB, pointA, pointB, Vehicle.Orientation.FORWARD, 0), + new Route.Step(pathBC, pointB, pointC, Vehicle.Orientation.FORWARD, 1), + new Route.Step(pathCD, pointC, pointD, Vehicle.Orientation.FORWARD, 2) + ) + ); + + // Initially, a vehicle reports the position it's located at and corresponding resources are + // allocated + commandProcessingTracker.allocationReset(Set.of(pointA)); + assertThat(commandProcessingTracker.hasCommandsToBeSent()).isFalse(); + assertThat(commandProcessingTracker.getClaimedResources()).isEmpty(); + assertThat(commandProcessingTracker.getAllocatedResources()).containsExactly(Set.of(pointA)); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()).isEmpty(); + assertThat(commandProcessingTracker.getAllocationPendingCommand()).isEmpty(); + assertThat(commandProcessingTracker.getSendingPendingCommand()).isEmpty(); + assertThat(commandProcessingTracker.getSentCommands()).isEmpty(); + assertThat(commandProcessingTracker.getLastCommandExecuted()).isEmpty(); + assertThat(commandProcessingTracker.getNextAllocationCommand()).isEmpty(); + assertThat(commandProcessingTracker.isWaitingForAllocation()).isFalse(); + + // Then, a transport order / drive order is assigned to the vehicle and the resources for the + // current drive order and its movement commands are claimed + commandProcessingTracker.driveOrderUpdated(movementCommands); + assertThat(commandProcessingTracker.hasCommandsToBeSent()).isTrue(); + assertThat(commandProcessingTracker.getClaimedResources()).containsExactly( + Set.of(pathAB, pointB), + Set.of(pathBC, pointC), + Set.of(pathCD, pointD) + ); + assertThat(commandProcessingTracker.getAllocatedResources()).containsExactly(Set.of(pointA)); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()).isEmpty(); + assertThat(commandProcessingTracker.getAllocationPendingCommand()).isEmpty(); + + // Then, allocation for the first set of resources is requested + commandProcessingTracker.allocationRequested(Set.of(pathAB, pointB)); + assertThat(commandProcessingTracker.getClaimedResources()).containsExactly( + Set.of(pathAB, pointB), + Set.of(pathBC, pointC), + Set.of(pathCD, pointD) + ); + assertThat(commandProcessingTracker.getAllocatedResources()).containsExactly(Set.of(pointA)); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()).isEmpty(); + assertThat(commandProcessingTracker.getAllocationPendingCommand()) + .contains(movementCommands.get(0)); + assertThat(commandProcessingTracker.isWaitingForAllocation()).isTrue(); + + // Then, allocation for the requested resources is confirmed + commandProcessingTracker.allocationConfirmed(Set.of(pathAB, pointB)); + assertThat(commandProcessingTracker.getClaimedResources()).containsExactly( + Set.of(pathBC, pointC), + Set.of(pathCD, pointD) + ); + assertThat(commandProcessingTracker.getAllocatedResources()).containsExactly( + Set.of(pointA), + Set.of(pathAB, pointB) + ); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()) + .containsExactly(Set.of(pathAB, pointB)); + assertThat(commandProcessingTracker.getAllocationPendingCommand()).isEmpty(); + assertThat(commandProcessingTracker.getSendingPendingCommand()) + .contains(movementCommands.get(0)); + assertThat(commandProcessingTracker.getSentCommands()).isEmpty(); + assertThat(commandProcessingTracker.isWaitingForAllocation()).isFalse(); + + // Then, the movement command for which resources have been allocated is sent to the vehicle + commandProcessingTracker.commandSent(movementCommands.get(0)); + assertThat(commandProcessingTracker.hasCommandsToBeSent()).isTrue(); + assertThat(commandProcessingTracker.getClaimedResources()).containsExactly( + Set.of(pathBC, pointC), + Set.of(pathCD, pointD) + ); + assertThat(commandProcessingTracker.getAllocatedResources()).containsExactly( + Set.of(pointA), + Set.of(pathAB, pointB) + ); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()) + .containsExactly(Set.of(pathAB, pointB)); + assertThat(commandProcessingTracker.getSendingPendingCommand()).isEmpty(); + assertThat(commandProcessingTracker.getSentCommands()).containsExactly(movementCommands.get(0)); + assertThat(commandProcessingTracker.getLastCommandExecuted()).isEmpty(); + + // Then, the movement command is reported as executed and resources for that command are still + // allocated but no longer ahead of the vehicle and split into the command's path, and its + // point & location + commandProcessingTracker.commandExecuted(movementCommands.get(0)); + assertThat(commandProcessingTracker.getClaimedResources()).containsExactly( + Set.of(pathBC, pointC), + Set.of(pathCD, pointD) + ); + assertThat(commandProcessingTracker.getAllocatedResources()).containsExactly( + Set.of(pointA), + Set.of(pathAB), + Set.of(pointB) + ); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()).isEmpty(); + assertThat(commandProcessingTracker.getSentCommands()).isEmpty(); + assertThat(commandProcessingTracker.getLastCommandExecuted()).contains(movementCommands.get(0)); + + // Then, allocation for the resources that are no longer needed is released + commandProcessingTracker.allocationReleased(Set.of(pointA)); + assertThat(commandProcessingTracker.getClaimedResources()).containsExactly( + Set.of(pathBC, pointC), + Set.of(pathCD, pointD) + ); + assertThat(commandProcessingTracker.getAllocatedResources()).containsExactly( + Set.of(pathAB), + Set.of(pointB) + ); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()).isEmpty(); + + // Then, the next movement command is processed + commandProcessingTracker.allocationRequested(Set.of(pathBC, pointC)); + commandProcessingTracker.allocationConfirmed(Set.of(pathBC, pointC)); + commandProcessingTracker.commandSent(movementCommands.get(1)); + commandProcessingTracker.commandExecuted(movementCommands.get(1)); + commandProcessingTracker.allocationReleased(Set.of(pathAB)); + commandProcessingTracker.allocationReleased(Set.of(pointB)); + assertThat(commandProcessingTracker.hasCommandsToBeSent()).isTrue(); + assertThat(commandProcessingTracker.getClaimedResources()) + .containsExactly(Set.of(pathCD, pointD)); + assertThat(commandProcessingTracker.getAllocatedResources()) + .containsExactly(Set.of(pathBC), Set.of(pointC)); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()).isEmpty(); + assertThat(commandProcessingTracker.getLastCommandExecuted()).contains(movementCommands.get(1)); + + // Then, the next (and last) movement command is processed + commandProcessingTracker.allocationRequested(Set.of(pathCD, pointD)); + commandProcessingTracker.allocationConfirmed(Set.of(pathCD, pointD)); + commandProcessingTracker.commandSent(movementCommands.get(2)); + commandProcessingTracker.commandExecuted(movementCommands.get(2)); + commandProcessingTracker.allocationReleased(Set.of(pathBC)); + commandProcessingTracker.allocationReleased(Set.of(pointC)); + assertThat(commandProcessingTracker.hasCommandsToBeSent()).isFalse(); + assertThat(commandProcessingTracker.getClaimedResources()).isEmpty(); + assertThat(commandProcessingTracker.getAllocatedResources()) + .containsExactly(Set.of(pathCD), Set.of(pointD)); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()).isEmpty(); + assertThat(commandProcessingTracker.getLastCommandExecuted()).contains(movementCommands.get(2)); + + // At this point, the drive order is considered finished + assertThat(commandProcessingTracker.isDriveOrderFinished()).isTrue(); + } + + @Test + void processingOfDriveOrderWithNewRoute() { + List movementCommands = createMovementCommandList( + List.of( + new Route.Step(pathAB, pointA, pointB, Vehicle.Orientation.FORWARD, 0), + new Route.Step(pathBC, pointB, pointC, Vehicle.Orientation.FORWARD, 1), + new Route.Step(pathCD, pointC, pointD, Vehicle.Orientation.FORWARD, 2) + ) + ); + + // Regular processing of the first movement command up to the point where the command is sent + commandProcessingTracker.allocationReset(Set.of(pointA)); + commandProcessingTracker.driveOrderUpdated(movementCommands); + commandProcessingTracker.allocationRequested(Set.of(pathAB, pointB)); + commandProcessingTracker.allocationConfirmed(Set.of(pathAB, pointB)); + commandProcessingTracker.commandSent(movementCommands.get(0)); + assertThat(commandProcessingTracker.hasCommandsToBeSent()).isTrue(); + assertThat(commandProcessingTracker.getClaimedResources()).containsExactly( + Set.of(pathBC, pointC), + Set.of(pathCD, pointD) + ); + assertThat(commandProcessingTracker.getAllocatedResources()).containsExactly( + Set.of(pointA), + Set.of(pathAB, pointB) + ); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()) + .containsExactly(Set.of(pathAB, pointB)); + assertThat(commandProcessingTracker.getAllocationPendingCommand()).isEmpty(); + assertThat(commandProcessingTracker.getSentCommands()).containsExactly(movementCommands.get(0)); + assertThat(commandProcessingTracker.getNextAllocationCommand()) + .contains(movementCommands.get(1)); + + // Then, a drive order update (with a new route) is received + movementCommands = createMovementCommandList( + List.of( + new Route.Step(pathAB, pointA, pointB, Vehicle.Orientation.FORWARD, 0), + new Route.Step(pathBC, pointB, pointC2, Vehicle.Orientation.FORWARD, 1), + new Route.Step(pathC2D, pointC2, pointD, Vehicle.Orientation.FORWARD, 2) + ) + ); + commandProcessingTracker.driveOrderUpdated(movementCommands); + assertThat(commandProcessingTracker.getClaimedResources()).containsExactly( + Set.of(pathBC, pointC2), + Set.of(pathC2D, pointD) + ); + assertThat(commandProcessingTracker.getAllocatedResources()).containsExactly( + Set.of(pointA), + Set.of(pathAB, pointB) + ); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()) + .containsExactly(Set.of(pathAB, pointB)); + assertThat(commandProcessingTracker.getAllocationPendingCommand()).isEmpty(); + assertThat(commandProcessingTracker.getSentCommands()).containsExactly(movementCommands.get(0)); + assertThat(commandProcessingTracker.getNextAllocationCommand()) + .contains(movementCommands.get(1)); + + // Then, the first movement command is reported as executed and allocation of corresponding + // resources is released + commandProcessingTracker.commandExecuted(movementCommands.get(0)); + commandProcessingTracker.allocationReleased(Set.of(pointA)); + assertThat(commandProcessingTracker.getAllocatedResources()) + .containsExactly(Set.of(pathAB), Set.of(pointB)); + assertThat(commandProcessingTracker.getLastCommandExecuted()).contains(movementCommands.get(0)); + + // Then, the next movement command is processed (the one for the new route) + commandProcessingTracker.allocationRequested(Set.of(pathBC, pointC2)); + commandProcessingTracker.allocationConfirmed(Set.of(pathBC, pointC2)); + commandProcessingTracker.commandSent(movementCommands.get(1)); + commandProcessingTracker.commandExecuted(movementCommands.get(1)); + commandProcessingTracker.allocationReleased(Set.of(pathAB)); + commandProcessingTracker.allocationReleased(Set.of(pointB)); + assertThat(commandProcessingTracker.hasCommandsToBeSent()).isTrue(); + assertThat(commandProcessingTracker.getClaimedResources()) + .containsExactly(Set.of(pathC2D, pointD)); + assertThat(commandProcessingTracker.getAllocatedResources()) + .containsExactly(Set.of(pathBC), Set.of(pointC2)); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()).isEmpty(); + assertThat(commandProcessingTracker.getLastCommandExecuted()).contains(movementCommands.get(1)); + + // Then, the next (and last) movement command is processed (the one for the new route) + commandProcessingTracker.allocationRequested(Set.of(pathC2D, pointD)); + commandProcessingTracker.allocationConfirmed(Set.of(pathC2D, pointD)); + commandProcessingTracker.commandSent(movementCommands.get(2)); + commandProcessingTracker.commandExecuted(movementCommands.get(2)); + commandProcessingTracker.allocationReleased(Set.of(pathBC)); + commandProcessingTracker.allocationReleased(Set.of(pointC2)); + assertThat(commandProcessingTracker.hasCommandsToBeSent()).isFalse(); + assertThat(commandProcessingTracker.getClaimedResources()).isEmpty(); + assertThat(commandProcessingTracker.getAllocatedResources()) + .containsExactly(Set.of(pathC2D), Set.of(pointD)); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()).isEmpty(); + assertThat(commandProcessingTracker.getLastCommandExecuted()).contains(movementCommands.get(2)); + + // At this point, the drive order is considered finished + assertThat(commandProcessingTracker.isDriveOrderFinished()).isTrue(); + } + + @Test + void processingOfRegularDriveOrderAbortion() { + List movementCommands = createMovementCommandList( + List.of( + new Route.Step(pathAB, pointA, pointB, Vehicle.Orientation.FORWARD, 0), + new Route.Step(pathBC, pointB, pointC, Vehicle.Orientation.FORWARD, 1), + new Route.Step(pathCD, pointC, pointD, Vehicle.Orientation.FORWARD, 2) + ) + ); + + // Regular processing of the first movement command up to the point where the allocation is + // confirmed + commandProcessingTracker.allocationReset(Set.of(pointA)); + commandProcessingTracker.driveOrderUpdated(movementCommands); + commandProcessingTracker.allocationRequested(Set.of(pathAB, pointB)); + commandProcessingTracker.allocationConfirmed(Set.of(pathAB, pointB)); + assertThat(commandProcessingTracker.hasCommandsToBeSent()).isTrue(); + assertThat(commandProcessingTracker.getClaimedResources()).containsExactly( + Set.of(pathBC, pointC), + Set.of(pathCD, pointD) + ); + assertThat(commandProcessingTracker.getAllocatedResources()).containsExactly( + Set.of(pointA), + Set.of(pathAB, pointB) + ); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()) + .containsExactly(Set.of(pathAB, pointB)); + + // Then, the drive order is aborted regularly + commandProcessingTracker.driveOrderAborted(false); + assertThat(commandProcessingTracker.hasCommandsToBeSent()).isTrue(); + assertThat(commandProcessingTracker.getClaimedResources()).isEmpty(); + assertThat(commandProcessingTracker.getAllocatedResources()).containsExactly( + Set.of(pointA), + Set.of(pathAB, pointB) + ); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()) + .containsExactly(Set.of(pathAB, pointB)); + assertThat(commandProcessingTracker.getSendingPendingCommand()) + .contains(movementCommands.get(0)); + + // Then, the movement command for which resources have already been allocated (and which is + // pending to be sent) is sent and reported as executed + commandProcessingTracker.commandSent(movementCommands.get(0)); + assertThat(commandProcessingTracker.hasCommandsToBeSent()).isFalse(); + commandProcessingTracker.commandExecuted(movementCommands.get(0)); + commandProcessingTracker.allocationReleased(Set.of(pointA)); + + // At this point, the drive order is considered finished and further processing should result + // in an exception + assertThat(commandProcessingTracker.isDriveOrderFinished()).isTrue(); + assertThatThrownBy(() -> commandProcessingTracker.allocationRequested(Set.of(pathBC, pointC))) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void processingOfImmediateDriveOrderAbortion() { + List movementCommands = createMovementCommandList( + List.of( + new Route.Step(pathAB, pointA, pointB, Vehicle.Orientation.FORWARD, 0), + new Route.Step(pathBC, pointB, pointC, Vehicle.Orientation.FORWARD, 1), + new Route.Step(pathCD, pointC, pointD, Vehicle.Orientation.FORWARD, 2) + ) + ); + + // Regular processing of the first movement command up to the point where the allocation is + // confirmed + commandProcessingTracker.allocationReset(Set.of(pointA)); + commandProcessingTracker.driveOrderUpdated(movementCommands); + commandProcessingTracker.allocationRequested(Set.of(pathAB, pointB)); + commandProcessingTracker.allocationConfirmed(Set.of(pathAB, pointB)); + assertThat(commandProcessingTracker.hasCommandsToBeSent()).isTrue(); + assertThat(commandProcessingTracker.getClaimedResources()).containsExactly( + Set.of(pathBC, pointC), + Set.of(pathCD, pointD) + ); + assertThat(commandProcessingTracker.getAllocatedResources()).containsExactly( + Set.of(pointA), + Set.of(pathAB, pointB) + ); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()) + .containsExactly(Set.of(pathAB, pointB)); + + // Then, the drive order is aborted immediately + commandProcessingTracker.driveOrderAborted(true); + assertThat(commandProcessingTracker.hasCommandsToBeSent()).isFalse(); + assertThat(commandProcessingTracker.getClaimedResources()).isEmpty(); + assertThat(commandProcessingTracker.getAllocatedResources()).containsExactly(Set.of(pointA)); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()).isEmpty(); + + // At this point, the drive order is considered finished and further processing should result + // in an exception + assertThat(commandProcessingTracker.isDriveOrderFinished()).isTrue(); + assertThatThrownBy(() -> commandProcessingTracker.commandSent(movementCommands.get(0))) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void processingOfDriveOrderWithRevokedAllocation() { + List movementCommands = createMovementCommandList( + List.of( + new Route.Step(pathAB, pointA, pointB, Vehicle.Orientation.FORWARD, 0), + new Route.Step(pathBC, pointB, pointC, Vehicle.Orientation.FORWARD, 1), + new Route.Step(pathCD, pointC, pointD, Vehicle.Orientation.FORWARD, 2) + ) + ); + + // Regular processing of the first movement command up to the point where the allocation is + // confirmed + commandProcessingTracker.allocationReset(Set.of(pointA)); + commandProcessingTracker.driveOrderUpdated(movementCommands); + commandProcessingTracker.allocationRequested(Set.of(pathAB, pointB)); + commandProcessingTracker.allocationConfirmed(Set.of(pathAB, pointB)); + assertThat(commandProcessingTracker.getClaimedResources()).containsExactly( + Set.of(pathBC, pointC), + Set.of(pathCD, pointD) + ); + assertThat(commandProcessingTracker.getAllocatedResources()).containsExactly( + Set.of(pointA), + Set.of(pathAB, pointB) + ); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()) + .containsExactly(Set.of(pathAB, pointB)); + assertThat(commandProcessingTracker.getSendingPendingCommand()) + .contains(movementCommands.get(0)); + + // Then, allocation for the resources that have been allocated last is revoked + commandProcessingTracker.allocationRevoked(Set.of(pathAB, pointB)); + assertThat(commandProcessingTracker.getClaimedResources()).containsExactly( + Set.of(pathBC, pointC), + Set.of(pathCD, pointD) + ); + assertThat(commandProcessingTracker.getAllocatedResources()).containsExactly(Set.of(pointA)); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()).isEmpty(); + assertThat(commandProcessingTracker.getSendingPendingCommand()).isEmpty(); + + // At this point, the drive order is still considered being processed but further processing + // should result in an exception + assertThat(commandProcessingTracker.isDriveOrderFinished()).isFalse(); + assertThatThrownBy(() -> commandProcessingTracker.commandSent(movementCommands.get(0))) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void processingOfDriveOrderWhereSendingIsAborted() { + List movementCommands = createMovementCommandList( + List.of( + new Route.Step(pathAB, pointA, pointB, Vehicle.Orientation.FORWARD, 0), + new Route.Step(pathBC, pointB, pointC, Vehicle.Orientation.FORWARD, 1), + new Route.Step(pathCD, pointC, pointD, Vehicle.Orientation.FORWARD, 2) + ) + ); + + // Regular processing of the first movement command up to the point where the allocation is + // confirmed + commandProcessingTracker.allocationReset(Set.of(pointA)); + commandProcessingTracker.driveOrderUpdated(movementCommands); + commandProcessingTracker.allocationRequested(Set.of(pathAB, pointB)); + commandProcessingTracker.allocationConfirmed(Set.of(pathAB, pointB)); + assertThat(commandProcessingTracker.getClaimedResources()).containsExactly( + Set.of(pathBC, pointC), + Set.of(pathCD, pointD) + ); + assertThat(commandProcessingTracker.getAllocatedResources()).containsExactly( + Set.of(pointA), + Set.of(pathAB, pointB) + ); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()) + .containsExactly(Set.of(pathAB, pointB)); + assertThat(commandProcessingTracker.getSendingPendingCommand()) + .contains(movementCommands.get(0)); + + // Then, sending of the first movement command is aborted, but allocations don't change + commandProcessingTracker.commandSendingStopped(movementCommands.get(0)); + assertThat(commandProcessingTracker.getClaimedResources()).containsExactly( + Set.of(pathBC, pointC), + Set.of(pathCD, pointD) + ); + assertThat(commandProcessingTracker.getAllocatedResources()).containsExactly( + Set.of(pointA), + Set.of(pathAB, pointB) + ); + assertThat(commandProcessingTracker.getAllocatedResourcesAhead()) + .containsExactly(Set.of(pathAB, pointB)); + assertThat(commandProcessingTracker.getSendingPendingCommand()).isEmpty(); + + // At this point, the drive order is still considered being processed but further processing + // should result in an exception + assertThat(commandProcessingTracker.isDriveOrderFinished()).isFalse(); + assertThatThrownBy(() -> commandProcessingTracker.commandSent(movementCommands.get(0))) + .isInstanceOf(IllegalArgumentException.class); + } + + private List createMovementCommandList(List steps) { + Point finalDestinationPoint = steps.getLast().getDestinationPoint(); + DriveOrder driveOrder = new DriveOrder( + new DriveOrder.Destination(finalDestinationPoint.getReference()) + ).withRoute(new Route(steps, steps.size())); + TransportOrder transportOrder = new TransportOrder( + String.format( + "%s-to-%s", + steps.getFirst().getSourcePoint().getName(), + steps.getLast().getDestinationPoint().getName() + ), + List.of(driveOrder) + ).withProcessingVehicle(new Vehicle("vehicle").getReference()); + + List movementCommands = new ArrayList<>(); + for (Route.Step step : steps) { + movementCommands.add( + new MovementCommand( + transportOrder, + driveOrder, + step, + MovementCommand.MOVE_OPERATION, + null, + true, + null, + finalDestinationPoint, + MovementCommand.MOVE_OPERATION, + Map.of() + ) + ); + } + + return movementCommands; + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/DefaultVehicleControllerTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/DefaultVehicleControllerTest.java new file mode 100644 index 0000000..ffc47b3 --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/DefaultVehicleControllerTest.java @@ -0,0 +1,306 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.opentcs.DataObjectFactory; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.components.kernel.services.InternalTransportOrderService; +import org.opentcs.components.kernel.services.InternalVehicleService; +import org.opentcs.components.kernel.services.NotificationService; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Pose; +import org.opentcs.data.model.Triple; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.drivers.vehicle.IncomingPoseTransformer; +import org.opentcs.drivers.vehicle.LoadHandlingDevice; +import org.opentcs.drivers.vehicle.MovementCommand; +import org.opentcs.drivers.vehicle.MovementCommandTransformer; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; +import org.opentcs.drivers.vehicle.VehicleCommAdapterEvent; +import org.opentcs.drivers.vehicle.VehicleDataTransformerFactory; +import org.opentcs.drivers.vehicle.VehicleProcessModel; +import org.opentcs.drivers.vehicle.management.VehicleProcessModelTO; +import org.opentcs.kernel.KernelApplicationConfiguration; +import org.opentcs.kernel.vehicles.transformers.VehicleDataTransformerRegistry; +import org.opentcs.strategies.basic.scheduling.DummyScheduler; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.SimpleEventBus; + +/** + * Test cases for StandardVehicleController. + */ +class DefaultVehicleControllerTest { + + private static final String RECHARGE_OP = "recharge"; + /** + * Creates model objects for us. + */ + private final DataObjectFactory dataObjectFactory = new DataObjectFactory(); + /** + * The kernel application's event bus. + */ + private final EventBus eventBus = new SimpleEventBus(); + /** + * A vehicle. + */ + private Vehicle vehicle; + /** + * A vehicle model. + */ + private VehicleProcessModel vehicleModel; + /** + * A serializable representation of the vehicle model. + */ + private VehicleProcessModelTO vehicleModelTO; + /** + * A (mocked) communication adapter. + */ + private VehicleCommAdapter commAdapter; + /** + * The (mocked) vehicle service. + */ + private InternalVehicleService vehicleService; + /** + * A dummy scheduler. + */ + private Scheduler scheduler; + /** + * A (mocked) components factory. + */ + private VehicleControllerComponentsFactory componentsFactory; + /** + * A (mocked) vehicle data transformer registry. + */ + private VehicleDataTransformerRegistry dataTransformerRegistry; + /** + * A (mocked) vehicle data transformer factory. + */ + private VehicleDataTransformerFactory dataTransformerFactory; + /** + * A (mocked) incoming pose transformer. + */ + private IncomingPoseTransformer poseTransformer; + /** + * A (mocked) movement command transformer. + */ + private MovementCommandTransformer movementCommandTransformer; + /** + * A (mocked) peripheral interactor. + */ + private PeripheralInteractor peripheralInteractor; + /** + * The instance we're testing. + */ + private DefaultVehicleController stdVehicleController; + + @BeforeEach + void setUp() { + vehicle = dataObjectFactory + .createVehicle() + .withProperties(Map.of("tcs:vehicleDataTransformer", "dummyFactory")); + vehicleModel = new VehicleProcessModel(vehicle); + vehicleModelTO = new VehicleProcessModelTO(); + commAdapter = mock(VehicleCommAdapter.class); + vehicleService = mock(InternalVehicleService.class); + componentsFactory = mock(VehicleControllerComponentsFactory.class); + peripheralInteractor = mock(PeripheralInteractor.class); + dataTransformerFactory = mock(VehicleDataTransformerFactory.class); + poseTransformer = mock(IncomingPoseTransformer.class); + movementCommandTransformer = mock(MovementCommandTransformer.class); + dataTransformerRegistry = new VehicleDataTransformerRegistry(Set.of(dataTransformerFactory)); + + doReturn("dummyFactory").when(dataTransformerFactory).getName(); + doReturn(poseTransformer).when(dataTransformerFactory).createIncomingPoseTransformer(vehicle); + doReturn(movementCommandTransformer) + .when(dataTransformerFactory).createMovementCommandTransformer(vehicle); + doReturn(true).when(dataTransformerFactory).providesTransformersFor(vehicle); + when(poseTransformer.apply(any(Pose.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + when(movementCommandTransformer.apply(any(MovementCommand.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + doReturn(RECHARGE_OP).when(commAdapter).getRechargeOperation(); + doReturn(vehicleModel).when(commAdapter).getProcessModel(); + doReturn(vehicleModelTO).when(commAdapter).createTransferableProcessModel(); + + doReturn(vehicle).when(vehicleService).fetchObject(Vehicle.class, vehicle.getReference()); + doReturn(vehicle).when(vehicleService).fetchObject(Vehicle.class, vehicle.getName()); + + doReturn(peripheralInteractor).when(componentsFactory) + .createPeripheralInteractor(vehicle.getReference()); + + scheduler = spy(new DummyScheduler()); + scheduler.initialize(); + stdVehicleController = new DefaultVehicleController( + vehicle, + commAdapter, + vehicleService, + mock(InternalTransportOrderService.class), + mock(NotificationService.class), + mock(DispatcherService.class), + scheduler, + eventBus, + componentsFactory, + mock(MovementCommandMapper.class), + mock(KernelApplicationConfiguration.class), + new CommandProcessingTracker(), + dataTransformerRegistry + ); + stdVehicleController.initialize(); + } + + @AfterEach + void tearDown() { + stdVehicleController.terminate(); + scheduler.terminate(); + } + + // Test cases for implementation of interface VehicleManager start here. + @Test + void shouldForwardPositionChangeToKernel() { + Point point = dataObjectFactory.createPoint(); + doReturn(point).when(vehicleService).fetchObject(Point.class, point.getName()); + + vehicleModel.setPosition(point.getName()); + + verify(vehicleService).updateVehiclePosition( + vehicle.getReference(), + point.getReference() + ); + } + + @Test + void shouldForwardPoseChangeToKernel() { + Pose newPose = new Pose(new Triple(211, 391, 0), 7.5); + vehicleModel.setPose(newPose); + + verify(vehicleService).updateVehiclePose( + vehicle.getReference(), + newPose + ); + } + + @Test + void shouldTransformPoseWhenUsingDifferentCoordinateSystems() { + // The initial call to the transformer should have already been made during initialization. + verify(poseTransformer, times(1)).apply(any(Pose.class)); + + vehicleModel.setPose(new Pose(new Triple(211, 391, 0), Double.NaN)); + verify(poseTransformer, times(2)).apply(any(Pose.class)); + + vehicleModel.setPose(new Pose(null, 33.0)); + verify(poseTransformer, times(3)).apply(any(Pose.class)); + } + + @Test + void shouldForwardEnergyLevelChangeToKernel() { + int newLevel = 80; + vehicleModel.setEnergyLevel(newLevel); + verify(vehicleService).updateVehicleEnergyLevel( + vehicle.getReference(), + newLevel + ); + } + + @Test + void shouldForwardLoadHandlingDevicesChangeToKernel() { + List devices + = List.of(new LoadHandlingDevice("MyLoadHandlingDevice", true)); + vehicleModel.setLoadHandlingDevices(devices); + + verify(vehicleService).updateVehicleLoadHandlingDevices( + vehicle.getReference(), + devices + ); + } + + @Test + void shouldForwardVehicleStateChangeToKernel() { + vehicleModel.setState(Vehicle.State.EXECUTING); + + verify(vehicleService).updateVehicleState( + vehicle.getReference(), + Vehicle.State.EXECUTING + ); + } + + @Test + void shouldForwardEventToBus() { + final String adapterName = "myAdapter"; + final String eventString = "myString"; + final List eventsReceived = new ArrayList<>(); + + eventBus.subscribe(event -> { + if (event instanceof VehicleCommAdapterEvent) { + eventsReceived.add((VehicleCommAdapterEvent) event); + } + }); + + vehicleModel.publishEvent(new VehicleCommAdapterEvent(adapterName, eventString)); + + assertEquals(1, eventsReceived.size()); + VehicleCommAdapterEvent event = eventsReceived.get(0); + assertEquals(eventString, event.getAppendix()); + } + + // Test cases for implementation of interface VehicleController start here. + @Test + void shouldHaveIdempotentEnabledState() { + stdVehicleController.initialize(); + assertTrue(stdVehicleController.isInitialized()); + stdVehicleController.initialize(); + assertTrue(stdVehicleController.isInitialized()); + stdVehicleController.terminate(); + assertFalse(stdVehicleController.isInitialized()); + stdVehicleController.terminate(); + assertFalse(stdVehicleController.isInitialized()); + } + + @Test + void shouldSetClaimOnNewTransportOrder() { + Location location = dataObjectFactory.createLocation(); + + Point dstPoint = dataObjectFactory.createPoint(); + Path stepPath = dataObjectFactory.createPath(dstPoint.getReference()); + List steps = List.of( + new Route.Step(stepPath, null, dstPoint, Vehicle.Orientation.FORWARD, 0) + ); + + DriveOrder driveOrder = new DriveOrder(new DriveOrder.Destination(location.getReference())) + .withRoute(new Route(steps, 1)); + + TransportOrder transportOrder + = new TransportOrder("some-transport-order", List.of(driveOrder)) + .withCurrentDriveOrderIndex(0); + + stdVehicleController.setTransportOrder(transportOrder); + + verify(scheduler).claim(eq(stdVehicleController), Mockito.any()); + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/MovementCommandMapperTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/MovementCommandMapperTest.java new file mode 100644 index 0000000..a74478a --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/MovementCommandMapperTest.java @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.theInstance; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.drivers.vehicle.MovementCommand; + +/** + * Tests for {@link MovementCommandMapper}. + */ +class MovementCommandMapperTest { + + private MovementCommandMapper mapper; + private TCSObjectService objectService; + + @BeforeEach + void setUp() { + objectService = mock(TCSObjectService.class); + mapper = new MovementCommandMapper(objectService); + + } + + @Test + void mapDriveOrderToMovementCommands() { + Point pointA = new Point("point-a"); + Point pointB = new Point("point-b"); + Point pointC = new Point("point-c"); + Path pathAB = new Path("path-ab", pointA.getReference(), pointB.getReference()); + Path pathBC = new Path("path-bc", pointB.getReference(), pointC.getReference()); + LocationType locationType = new LocationType("location-type"); + Location destinationLocation = new Location("location", locationType.getReference()); + when(objectService.fetchObject(eq(Location.class), eq("location"))) + .thenReturn(destinationLocation); + + Route.Step stepAB = new Route.Step(pathAB, pointA, pointB, Vehicle.Orientation.FORWARD, 0); + Route.Step stepBC = new Route.Step(pathBC, pointB, pointC, Vehicle.Orientation.FORWARD, 1); + Route route = new Route(List.of(stepAB, stepBC), 1L); + DriveOrder driveOrder + = new DriveOrder( + new DriveOrder.Destination(destinationLocation.getReference()) + .withOperation("operation") + .withProperties(Map.of("key1", "value1")) + ).withRoute(route); + + TransportOrder transportOrder + = new TransportOrder("some-order", List.of(driveOrder)) + .withProperties(Map.of("key2", "value2", "key3", "value3")); + + List result = mapper.toMovementCommands(driveOrder, transportOrder); + + assertThat(result, hasSize(2)); + + assertThat(result.get(0).getTransportOrder(), is(theInstance(transportOrder))); + assertThat(result.get(0).getDriveOrder(), is(theInstance(driveOrder))); + assertThat(result.get(0).getStep(), is(equalTo(stepAB))); + assertThat(result.get(0).getOperation(), is(MovementCommand.NO_OPERATION)); + assertThat(result.get(0).getOpLocation(), is(nullValue())); + assertThat(result.get(0).isFinalMovement(), is(false)); + assertThat(result.get(0).getFinalDestination(), is(pointC)); + assertThat(result.get(0).getFinalDestinationLocation(), is(destinationLocation)); + assertThat(result.get(0).getFinalOperation(), is("operation")); + assertThat(result.get(0).getProperties(), is(aMapWithSize(3))); + assertThat(result.get(0).getProperties().keySet(), containsInAnyOrder("key1", "key2", "key3")); + + assertThat(result.get(1).getTransportOrder(), is(theInstance(transportOrder))); + assertThat(result.get(1).getDriveOrder(), is(theInstance(driveOrder))); + assertThat(result.get(1).getStep(), is(equalTo(stepBC))); + assertThat(result.get(1).getOperation(), is("operation")); + assertThat(result.get(1).getOpLocation(), is(destinationLocation)); + assertThat(result.get(1).isFinalMovement(), is(true)); + assertThat(result.get(1).getFinalDestination(), is(pointC)); + assertThat(result.get(1).getFinalDestinationLocation(), is(destinationLocation)); + assertThat(result.get(1).getFinalOperation(), is("operation")); + assertThat(result.get(1).getProperties().keySet(), containsInAnyOrder("key1", "key2", "key3")); + assertThat(result.get(1).getProperties(), is(aMapWithSize(3))); + assertThat(result.get(1).getProperties().keySet(), containsInAnyOrder("key1", "key2", "key3")); + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/MovementComparisonsTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/MovementComparisonsTest.java new file mode 100644 index 0000000..0575ece --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/MovementComparisonsTest.java @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.data.order.Route.Step; + +/** + * Tests for {@link MovementComparisons}. + */ +class MovementComparisonsTest { + + @Test + void considerIdenticalStepsEqual() { + List stepsA = List.of( + createStep("A", "B", Vehicle.Orientation.FORWARD, 0, true, null), + createStep("B", "C", Vehicle.Orientation.FORWARD, 1, true, null) + ); + List stepsB = new ArrayList<>(stepsA); + + assertTrue(MovementComparisons.equalsInMovement(stepsA, stepsB)); + } + + @Test + void considerStepsWithDifferentReroutingTypeEqual() { + List stepsA = List.of( + createStep("A", "B", Vehicle.Orientation.FORWARD, 0, true, null), + createStep("B", "C", Vehicle.Orientation.FORWARD, 1, true, null) + ); + List stepsB = List.of( + createStep("A", "B", Vehicle.Orientation.FORWARD, 0, true, null), + createStep("B", "C", Vehicle.Orientation.FORWARD, 1, true, ReroutingType.REGULAR) + ); + + assertTrue(MovementComparisons.equalsInMovement(stepsA, stepsB)); + } + + @Test + void considerStepsWithDifferentExecutionAllowedEqual() { + List stepsA = List.of( + createStep("A", "B", Vehicle.Orientation.FORWARD, 0, true, null), + createStep("B", "C", Vehicle.Orientation.FORWARD, 1, true, null) + ); + List stepsB = List.of( + createStep("A", "B", Vehicle.Orientation.FORWARD, 0, true, null), + createStep("B", "C", Vehicle.Orientation.FORWARD, 1, false, null) + ); + + assertTrue(MovementComparisons.equalsInMovement(stepsA, stepsB)); + } + + @Test + void considerDivergingStepsNotEqual() { + List stepsA = List.of( + createStep("A", "B", Vehicle.Orientation.FORWARD, 0, true, null), + createStep("B", "C", Vehicle.Orientation.FORWARD, 1, true, null), + createStep("C", "D", Vehicle.Orientation.FORWARD, 2, true, null), + createStep("D", "E", Vehicle.Orientation.FORWARD, 3, true, null) + ); + List stepsB = List.of( + createStep("A", "B", Vehicle.Orientation.FORWARD, 0, true, null), + createStep("B", "C", Vehicle.Orientation.FORWARD, 1, true, null), + createStep("C", "Y", Vehicle.Orientation.FORWARD, 2, true, null), + createStep("Y", "Z", Vehicle.Orientation.FORWARD, 3, true, null) + ); + + assertFalse(MovementComparisons.equalsInMovement(stepsA, stepsB)); + } + + @Test + void considerStepsWithDifferentPointsNotEqual() { + List stepsA = List.of( + createStep("A", "B", Vehicle.Orientation.FORWARD, 0, true, null), + createStep("B", "C", Vehicle.Orientation.FORWARD, 1, true, null) + ); + List stepsB = List.of( + createStep("X", "Y", Vehicle.Orientation.FORWARD, 0, true, null), + createStep("Y", "Z", Vehicle.Orientation.FORWARD, 1, true, null) + ); + + assertFalse(MovementComparisons.equalsInMovement(stepsA, stepsB)); + } + + @Test + void considerStepsWithDifferentOrientationAngleNotEqual() { + List stepsA = List.of( + createStep("A", "B", Vehicle.Orientation.FORWARD, 0, true, null), + createStep("B", "C", Vehicle.Orientation.FORWARD, 1, true, null) + ); + List stepsB = List.of( + createStep("A", "B", Vehicle.Orientation.BACKWARD, 0, true, null), + createStep("B", "C", Vehicle.Orientation.BACKWARD, 1, true, null) + ); + + assertFalse(MovementComparisons.equalsInMovement(stepsA, stepsB)); + } + + @Test + void considerStepsWithDifferentRouteIndicesNotEqual() { + List stepsA = List.of( + createStep("A", "B", Vehicle.Orientation.FORWARD, 0, true, null), + createStep("B", "C", Vehicle.Orientation.FORWARD, 1, true, null) + ); + List stepsB = List.of( + createStep("A", "B", Vehicle.Orientation.FORWARD, 5, true, null), + createStep("B", "C", Vehicle.Orientation.FORWARD, 6, true, null) + ); + + assertFalse(MovementComparisons.equalsInMovement(stepsA, stepsB)); + } + + private Step createStep( + @Nonnull + String srcPointName, + @Nonnull + String destPointName, + @Nonnull + Vehicle.Orientation orientation, + int routeIndex, + boolean executionAllowed, + @Nullable + ReroutingType reroutingType + ) { + requireNonNull(srcPointName, "srcPointName"); + requireNonNull(destPointName, "destPointName"); + requireNonNull(orientation, "orientation"); + + Point srcPoint = new Point(srcPointName); + Point destPoint = new Point(destPointName); + Path path = new Path( + srcPointName + "-" + destPointName, + srcPoint.getReference(), + destPoint.getReference() + ); + + return new Step( + path, + srcPoint, + destPoint, + orientation, + routeIndex, + executionAllowed, + reroutingType + ); + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/PeripheralInteractionTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/PeripheralInteractionTest.java new file mode 100644 index 0000000..bac641c --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/PeripheralInteractionTest.java @@ -0,0 +1,206 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.services.PeripheralJobService; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.data.peripherals.PeripheralOperation; +import org.opentcs.data.peripherals.PeripheralOperation.ExecutionTrigger; +import org.opentcs.drivers.vehicle.MovementCommand; + +/** + * Defines test cases for {@link PeripheralInteraction}. + */ +class PeripheralInteractionTest { + + /** + * The vehicle instance to use for the tests. + */ + private static final Vehicle VEHICLE = new Vehicle("Vehicle-01"); + /** + * The transport order to use for the tests. + */ + private static final TransportOrder ORDER = new TransportOrder("TransportOrder-01", List.of()); + /** + * The reservation token to use for the tests. + */ + private static final String RESERVATION_TOKEN = "SomeToken"; + /** + * A (mocked) peripheral job service. + */ + private PeripheralJobService peripheralJobService; + /** + * A (mocked) callback to be executed when interactions succeeded. + */ + private Runnable succeededCallback; + /** + * A (mocked) callback to be executed when interactions failed. + */ + private Runnable failedCallback; + /** + * The instance to test. + */ + private PeripheralInteraction peripheralInteraction; + + PeripheralInteractionTest() { + } + + @BeforeEach + void setUp() { + peripheralJobService = mock(PeripheralJobService.class); + succeededCallback = mock(Runnable.class); + failedCallback = mock(Runnable.class); + } + + @Test + void shouldNotWaitForOperationCompletion() { + PeripheralOperation operation = new PeripheralOperation( + createLocation().getReference(), + RESERVATION_TOKEN, + ExecutionTrigger.AFTER_ALLOCATION, + false + ); + + peripheralInteraction = new PeripheralInteraction( + VEHICLE.getReference(), + ORDER.getReference(), + createDummyMovementCommand(), + Arrays.asList(operation), + peripheralJobService, + RESERVATION_TOKEN + ); + + verify(succeededCallback, times(0)).run(); + assertThat(peripheralInteraction.hasState(PeripheralInteraction.State.PRISTINE), is(true)); + + peripheralInteraction.start(succeededCallback, failedCallback); + verify(peripheralJobService, times(1)).createPeripheralJob(any()); + verify(succeededCallback, times(1)).run(); + assertThat(peripheralInteraction.isFinished(), is(true)); + + verify(succeededCallback, times(1)).run(); + verify(failedCallback, times(0)).run(); + } + + @Test + void shouldWaitForOperationCompletion() { + PeripheralOperation operation = new PeripheralOperation( + createLocation().getReference(), + RESERVATION_TOKEN, + ExecutionTrigger.AFTER_ALLOCATION, + true + ); + + PeripheralJob peripheralJob = new PeripheralJob("SomeJob", RESERVATION_TOKEN, operation); + when(peripheralJobService.createPeripheralJob(any())).thenReturn(peripheralJob); + + peripheralInteraction = new PeripheralInteraction( + VEHICLE.getReference(), + ORDER.getReference(), + createDummyMovementCommand(), + Arrays.asList(operation), + peripheralJobService, + RESERVATION_TOKEN + ); + + verify(succeededCallback, times(0)).run(); + assertThat(peripheralInteraction.hasState(PeripheralInteraction.State.PRISTINE), is(true)); + + peripheralInteraction.start(succeededCallback, failedCallback); + verify(peripheralJobService, times(1)).createPeripheralJob(any()); + verify(succeededCallback, times(0)).run(); + assertThat(peripheralInteraction.isFinished(), is(false)); + + peripheralInteraction.onPeripheralJobFinished(peripheralJob); + verify(succeededCallback, times(1)).run(); + assertThat(peripheralInteraction.isFinished(), is(true)); + + verify(failedCallback, times(0)).run(); + } + + @Test + void shouldCallbackOnFailedRequiredInteraction() { + PeripheralOperation operation = new PeripheralOperation( + createLocation().getReference(), + RESERVATION_TOKEN, + ExecutionTrigger.AFTER_ALLOCATION, + true + ); + + PeripheralJob peripheralJob = new PeripheralJob("SomeJob", RESERVATION_TOKEN, operation); + when(peripheralJobService.createPeripheralJob(any())).thenReturn(peripheralJob); + + peripheralInteraction = new PeripheralInteraction( + VEHICLE.getReference(), + ORDER.getReference(), + createDummyMovementCommand(), + Arrays.asList(operation), + peripheralJobService, + RESERVATION_TOKEN + ); + + verify(succeededCallback, times(0)).run(); + assertThat(peripheralInteraction.hasState(PeripheralInteraction.State.PRISTINE), is(true)); + + peripheralInteraction.start(succeededCallback, failedCallback); + verify(peripheralJobService, times(1)).createPeripheralJob(any()); + verify(failedCallback, times(0)).run(); + assertThat(peripheralInteraction.isFailed(), is(false)); + + peripheralInteraction.onPeripheralJobFailed(peripheralJob); + verify(failedCallback, times(1)).run(); + assertThat(peripheralInteraction.isFailed(), is(true)); + + verify(succeededCallback, times(0)).run(); + } + + private Location createLocation() { + LocationType locationType = new LocationType("LocationType-01"); + return new Location("Location-01", locationType.getReference()); + } + + private MovementCommand createDummyMovementCommand() { + Point srcPoint = new Point("Point-01"); + Point destPoint = new Point("Point-02"); + Path path = new Path( + "Point-01 --- Point-02", + srcPoint.getReference(), + destPoint.getReference() + ); + Route.Step step = new Route.Step(path, srcPoint, destPoint, Vehicle.Orientation.FORWARD, 0); + + return new MovementCommand( + new TransportOrder("dummy-transport-order", List.of()), + new DriveOrder(new DriveOrder.Destination(destPoint.getReference())), + step, + MovementCommand.MOVE_OPERATION, + null, + true, + null, + destPoint, + MovementCommand.MOVE_OPERATION, + Map.of() + ); + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/ResourceMathTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/ResourceMathTest.java new file mode 100644 index 0000000..7fb9161 --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/ResourceMathTest.java @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResource; + +/** + * Tests for {@link ResourceMath}. + */ +class ResourceMathTest { + + private Point pointA; + private Point pointB; + private Point pointC; + private Point pointD; + + private Path pathAB; + private Path pathBC; + private Path pathCD; + + private List>> allResources; + + @BeforeEach + void setUp() { + pointA = new Point("A"); + pointB = new Point("B"); + pointC = new Point("C"); + pointD = new Point("D"); + + pathAB = new Path("AB", pointA.getReference(), pointB.getReference()).withLength(3000); + pathBC = new Path("BC", pointB.getReference(), pointC.getReference()).withLength(2000); + pathCD = new Path("CD", pointC.getReference(), pointD.getReference()).withLength(1000); + + allResources = List.of(Set.of(pathAB, pointB), Set.of(pathBC, pointC), Set.of(pathCD, pointD)); + } + + @Test + void handleVehicleCoveringLastResourceSet() { + assertThat(ResourceMath.freeableResourceSetCount(allResources, 500), is(2)); + } + + @Test + void handleVehicleCoveringLastResourceSetExactly() { + assertThat(ResourceMath.freeableResourceSetCount(allResources, 1000), is(2)); + } + + @Test + void handleVehicleCoveringLastTwoResourceSets() { + assertThat(ResourceMath.freeableResourceSetCount(allResources, 1001), is(1)); + } + + @Test + void handleVehicleCoveringLastThreeResourceSets() { + assertThat(ResourceMath.freeableResourceSetCount(allResources, 3001), is(0)); + } + + @Test + void handleVehicleCoveringMoreResourceSetsThanGiven() { + assertThat(ResourceMath.freeableResourceSetCount(allResources, Integer.MAX_VALUE), is(0)); + } + +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/SplitResourcesTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/SplitResourcesTest.java new file mode 100644 index 0000000..830b666 --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/SplitResourcesTest.java @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResource; + +/** + * Tests for {@link SplitResources}. + */ +class SplitResourcesTest { + + private Point pointA; + private Point pointB; + private Point pointC; + private Point pointD; + + private Path pathAB; + private Path pathBC; + private Path pathCD; + + private List>> allResources; + + @BeforeEach + void setUp() { + pointA = new Point("A"); + pointB = new Point("B"); + pointC = new Point("C"); + pointD = new Point("D"); + + pathAB = new Path("AB", pointA.getReference(), pointB.getReference()); + pathBC = new Path("BC", pointB.getReference(), pointC.getReference()); + pathCD = new Path("CD", pointC.getReference(), pointD.getReference()); + + allResources = List.of(Set.of(pathAB, pointB), Set.of(pathBC, pointC), Set.of(pathCD, pointD)); + } + + @Test + void handleEmptyInput() { + SplitResources result = SplitResources.from(List.of(), Set.of()); + + assertThat(result, is(notNullValue())); + assertThat(result.getResourcesPassed(), is(empty())); + assertThat(result.getResourcesAhead(), is(empty())); + } + + @Test + void treatAllResourcesAsPassedForNonexistentDelimiter() { + SplitResources result = SplitResources.from(allResources, Set.of()); + + assertThat(result, is(notNullValue())); + assertThat(result.getResourcesPassed(), is(equalTo(allResources))); + assertThat(result.getResourcesAhead(), is(empty())); + } + + @Test + void treatResourcesAsPassedForCompleteDelimiterSet() { + SplitResources result = SplitResources.from(allResources, Set.of(pathBC, pointC)); + + assertThat(result, is(notNullValue())); + assertThat( + result.getResourcesPassed(), + contains(Set.of(pathAB, pointB), Set.of(pathBC, pointC)) + ); + assertThat(result.getResourcesAhead(), contains(Set.of(pathCD, pointD))); + } + + @Test + void treatResourcesAsPassedForPartialDelimiterSet() { + SplitResources result = SplitResources.from(allResources, Set.of(pointC)); + + assertThat(result, is(notNullValue())); + assertThat( + result.getResourcesPassed(), + contains(Set.of(pathAB, pointB), Set.of(pathBC, pointC)) + ); + assertThat(result.getResourcesAhead(), contains(Set.of(pathCD, pointD))); + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/StandardVehicleManagerPoolTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/StandardVehicleManagerPoolTest.java new file mode 100644 index 0000000..c314b80 --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/StandardVehicleManagerPoolTest.java @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.services.InternalVehicleService; +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; + +/** + * Tests for the {@link StandardVehicleManagerPoolTest}. + */ +class StandardVehicleManagerPoolTest { + + /** + * A name for a vehicle. + */ + private static final String A_VEHICLE_NAME = "MyVehicle"; + /** + * An name for a vehicle that does not exist. + */ + private static final String UNKNOWN_VEHICLE_NAME = "SomeUnknownVehicle"; + /** + * The (mocked) vehicle service. + */ + private InternalVehicleService vehicleService; + /** + * A (mocked) communication adapter. + */ + private VehicleCommAdapter commAdapter; + /** + * The VehicleManagerpool we're testing. + */ + private LocalVehicleControllerPool vehManagerPool; + + @BeforeEach + void setUp() { + vehicleService = mock(InternalVehicleService.class); + commAdapter = mock(VehicleCommAdapter.class); + vehManagerPool = new DefaultVehicleControllerPool( + vehicleService, + new MockedVehicleManagerFactory() + ); + } + + @Test + void testThrowsNPEIfVehicleNameIsNull() { + assertThrows( + NullPointerException.class, + () -> vehManagerPool.attachVehicleController(null, commAdapter) + ); + } + + @Test + void testThrowsNPEIfCommAdapterIsNull() { + assertThrows( + NullPointerException.class, + () -> vehManagerPool.attachVehicleController(A_VEHICLE_NAME, null) + ); + } + + @Test + void testThrowsExceptionForUnknownVehicleName() { + assertThrows( + IllegalArgumentException.class, + () -> vehManagerPool.attachVehicleController(UNKNOWN_VEHICLE_NAME, commAdapter) + ); + } + + @Test + void testThrowsNPEIfDetachingNullVehicleName() { + assertThrows( + NullPointerException.class, + () -> vehManagerPool.detachVehicleController(null) + ); + } + + /** + * A factory delivering vehicle manager mocks. + */ + private static class MockedVehicleManagerFactory + implements + VehicleControllerFactory { + + @Override + public DefaultVehicleController createVehicleController( + Vehicle vehicle, + VehicleCommAdapter commAdapter + ) { + return mock(DefaultVehicleController.class); + } + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemIncomingPoseTransformerTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemIncomingPoseTransformerTest.java new file mode 100644 index 0000000..7686c73 --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemIncomingPoseTransformerTest.java @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles.transformers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.opentcs.data.model.Pose; +import org.opentcs.data.model.Triple; + +/** + * Tests for {@link CoordinateSystemIncomingPoseTransformer}. + */ +public class CoordinateSystemIncomingPoseTransformerTest { + + @Test + public void applyTransformation() { + CoordinateSystemIncomingPoseTransformer transformer + = new CoordinateSystemIncomingPoseTransformer( + new CoordinateSystemTransformation(10, 20, 30, 40) + ); + + assertThat( + transformer.apply(new Pose(new Triple(0, 0, 0), 40.0)), + is(equalTo(new Pose(new Triple(-10, -20, -30), 0.0))) + ); + } + + @ParameterizedTest + @CsvSource({"380.0,-20.0", "-430.0,70.0"}) + void limitTransformedOrientationAngle( + double offsetOrientation, + double expectedTransformedOrientation + ) { + CoordinateSystemTransformation transformation = new CoordinateSystemTransformation( + 0, + 0, + 0, + offsetOrientation + ); + CoordinateSystemIncomingPoseTransformer transformer + = new CoordinateSystemIncomingPoseTransformer(transformation); + + assertThat( + transformer.apply(new Pose(new Triple(0, 0, 0), 0.0)), + is(equalTo(new Pose(new Triple(0, 0, 0), expectedTransformedOrientation))) + ); + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemMovementCommandTransformerTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemMovementCommandTransformerTest.java new file mode 100644 index 0000000..d49754c --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemMovementCommandTransformerTest.java @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles.transformers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Pose; +import org.opentcs.data.model.Triple; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.DriveOrder.Destination; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.drivers.vehicle.MovementCommand; + +/** + * Tests for {@link CoordinateSystemMovementCommandTransformer}. + */ +public class CoordinateSystemMovementCommandTransformerTest { + + private MovementCommand command; + + @BeforeEach + public void setUp() { + Point pointDestDriveOrder = new Point("P1"); + Point pointDestStep = new Point("P2") + .withPose(new Pose(new Triple(20, 20, 20), -45)); + Point finalDest = new Point("P3") + .withPose(new Pose(new Triple(30, 30, 30), 45)); + Location locationDest = new Location("L1", new LocationType("LT1").getReference()) + .withPosition(new Triple(35, 35, 35)); + command = new MovementCommand( + new TransportOrder("T1", List.of()), + new DriveOrder(new Destination(pointDestDriveOrder.getReference())), + new Route.Step( + null, + null, + pointDestStep, + Vehicle.Orientation.FORWARD, + 1, true, + null + ), + MovementCommand.MOVE_OPERATION, + null, + false, + locationDest, + finalDest, + MovementCommand.PARK_OPERATION, + Map.of() + ); + } + + @Test + void applyTransformation() { + CoordinateSystemTransformation transformation = new CoordinateSystemTransformation( + 10, + 20, + 30, + 40 + ); + CoordinateSystemMovementCommandTransformer transformer + = new CoordinateSystemMovementCommandTransformer(transformation); + + MovementCommand transformedCommand = transformer.apply(command); + + assertThat( + transformedCommand.getStep().getDestinationPoint().getPose(), + is(equalTo(new Pose(new Triple(30, 40, 50), -5.0))) + ); + assertThat( + transformedCommand.getFinalDestination().getPose(), + is(equalTo(new Pose(new Triple(40, 50, 60), 85.0))) + ); + assertThat( + transformedCommand.getFinalDestinationLocation().getPosition(), + is(equalTo(new Triple(45, 55, 65))) + ); + } + + @ParameterizedTest + @CsvSource({"420.0,15.0,105.0", "-470.0,-155,-65.0"}) + void limitTransformedOrientationAngle( + double offsetOrientation, + double expectedDestinationPointOrientation, + double expectedFinalDestinationOrientation + ) { + CoordinateSystemTransformation transformation = new CoordinateSystemTransformation( + 0, + 0, + 0, + offsetOrientation + ); + CoordinateSystemMovementCommandTransformer transformer + = new CoordinateSystemMovementCommandTransformer(transformation); + + MovementCommand transformedCommand = transformer.apply(command); + + assertThat( + transformedCommand.getStep().getDestinationPoint().getPose(), + is(equalTo(new Pose(new Triple(20, 20, 20), expectedDestinationPointOrientation))) + ); + assertThat( + transformedCommand.getFinalDestination().getPose(), + is(equalTo(new Pose(new Triple(30, 30, 30), expectedFinalDestinationOrientation))) + ); + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemTransformationTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemTransformationTest.java new file mode 100644 index 0000000..5c5a139 --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/transformers/CoordinateSystemTransformationTest.java @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles.transformers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.opentcs.data.model.Vehicle; + +/** + * Tests for {@link CoordinateSystemTransformation}. + */ +class CoordinateSystemTransformationTest { + + @Test + public void parseValidPropertyValues() { + Vehicle vehicle = new Vehicle("v1") + .withProperties( + Map.of( + "tcs:offsetTransformer.x", "10", + "tcs:offsetTransformer.y", "20", + "tcs:offsetTransformer.z", "30", + "tcs:offsetTransformer.orientation", "40" + ) + ); + Optional transformation + = CoordinateSystemTransformation.fromVehicle(vehicle); + + assertTrue(transformation.isPresent()); + assertThat(transformation.get().getOffsetX(), equalTo(10)); + assertThat(transformation.get().getOffsetY(), equalTo(20)); + assertThat(transformation.get().getOffsetZ(), equalTo(30)); + assertThat(transformation.get().getOffsetOrientation(), equalTo(40.0)); + } + + @Test + public void ignoreMissingPropertyValues() { + Vehicle vehicle = new Vehicle("v1").withProperties(Map.of()); + Optional transformation + = CoordinateSystemTransformation.fromVehicle(vehicle); + + assertTrue(transformation.isPresent()); + assertThat(transformation.get().getOffsetX(), equalTo(0)); + assertThat(transformation.get().getOffsetY(), equalTo(0)); + assertThat(transformation.get().getOffsetZ(), equalTo(0)); + assertThat(transformation.get().getOffsetOrientation(), equalTo(0.0)); + } + + @ParameterizedTest + @ValueSource( + strings = { + "tcs:offsetTransformer.x", + "tcs:offsetTransformer.y", + "tcs:offsetTransformer.z", + "tcs:offsetTransformer.orientation", + } + ) + void returnEmptyOptionalOnInvalidPropertyValue(String propertyKey) { + Vehicle vehicle = new Vehicle("v1") + .withProperties(Map.of(propertyKey, "none")); + Optional transformation + = CoordinateSystemTransformation.fromVehicle(vehicle); + + assertFalse(transformation.isPresent()); + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/transformers/VehicleDataTransformerRegistryTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/transformers/VehicleDataTransformerRegistryTest.java new file mode 100644 index 0000000..0f11c85 --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/vehicles/transformers/VehicleDataTransformerRegistryTest.java @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.vehicles.transformers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.VehicleDataTransformerFactory; + +/** + * Tests for {@link VehicleDataTransformerRegistry}. + */ +public class VehicleDataTransformerRegistryTest { + + private VehicleDataTransformerRegistry vehicleDataTransformerRegistry; + private VehicleDataTransformerFactory dummyFactory; + private Vehicle vehicle; + + @BeforeEach + void setUp() { + vehicle = new Vehicle("v1") + .withProperties( + Map.of( + "tcs:vehicleDataTransformer", "dummyFactory" + ) + ); + dummyFactory = mock(VehicleDataTransformerFactory.class); + vehicleDataTransformerRegistry = new VehicleDataTransformerRegistry( + Set.of(new DefaultVehicleDataTransformerFactory(), dummyFactory) + ); + when(dummyFactory.getName()).thenReturn("dummyFactory"); + } + + @Test + void fallBackToDefaultFactoryForInvalidFactoryName() { + vehicle = vehicle.withProperties( + Map.of( + "tcs:vehicleDataTransformer", "newFactory" + ) + ); + + assertThat( + vehicleDataTransformerRegistry.findFactoryFor(vehicle), + is(instanceOf(DefaultVehicleDataTransformerFactory.class)) + ); + } + + @Test + void fallBackToDefaultFactoryForMissingProperties() { + vehicle = vehicle.withProperties(Map.of()); + assertThat( + vehicleDataTransformerRegistry.findFactoryFor(vehicle), + is(instanceOf(DefaultVehicleDataTransformerFactory.class)) + ); + } + + @Test + void provideAcceptableFactoryForVehicle() { + when(dummyFactory.providesTransformersFor(vehicle)).thenReturn(true); + assertThat(vehicleDataTransformerRegistry.findFactoryFor(vehicle), is(dummyFactory)); + } + + @Test + void fallBackToDefaultFactoryForUnacceptableSelectedFactory() { + when(dummyFactory.providesTransformersFor(vehicle)).thenReturn(false); + assertThat(vehicleDataTransformerRegistry.findFactoryFor(vehicle).getClass(), + is(DefaultVehicleDataTransformerFactory.class)); + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/DefaultOrderSequenceCleanupApprovalTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/DefaultOrderSequenceCleanupApprovalTest.java new file mode 100644 index 0000000..5c5240a --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/DefaultOrderSequenceCleanupApprovalTest.java @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import java.time.Instant; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; + +/** + * Test for {@link DefaultOrderSequenceCleanupApproval}. + */ +class DefaultOrderSequenceCleanupApprovalTest { + + private TransportOrderPoolManager orderPoolManager; + private TCSObjectRepository objectRepo; + private DefaultTransportOrderCleanupApproval defaultTransportOrderCleanupApproval; + private DefaultOrderSequenceCleanupApproval approval; + + @BeforeEach + void setUp() { + orderPoolManager = mock(); + objectRepo = mock(); + defaultTransportOrderCleanupApproval = mock(); + given(orderPoolManager.getObjectRepo()).willReturn(objectRepo); + + approval = new DefaultOrderSequenceCleanupApproval( + orderPoolManager, + defaultTransportOrderCleanupApproval + ); + } + + @Test + void approveOrderSequence() { + OrderSequence sequence = createOrderSequence() + .withFinished(true); + + assertTrue(approval.test(sequence)); + } + + @Test + void disapproveOrderSequenceNotFinished() { + OrderSequence sequence = createOrderSequence() + .withFinished(false); + + assertFalse(approval.test(sequence)); + } + + @Test + void disapproveOrderSequenceWithRelatedTransportOrderCreatedAfterCreationTimeThreshold() { + TransportOrder order = createTransportOrder() + .withState(TransportOrder.State.FINISHED) + .withCreationTime(Instant.parse("2024-01-01T15:00:00.00Z")); + OrderSequence sequence = createOrderSequence() + .withOrder(order.getReference()) + .withFinished(true); + given(objectRepo.getObject(TransportOrder.class, order.getReference())).willReturn(order); + + assertFalse(approval.test(sequence)); + } + + @Test + void disapproveOrderSequenceWithUnapprovedTransportOrder() { + TransportOrder order = createTransportOrder() + .withState(TransportOrder.State.FINISHED) + .withCreationTime(Instant.parse("2024-01-01T09:00:00.00Z")); + OrderSequence sequence = createOrderSequence() + .withOrder(order.getReference()) + .withFinished(true); + given(objectRepo.getObjects(eq(TransportOrder.class), any())).willReturn(Set.of(order)); + given(defaultTransportOrderCleanupApproval.test(order)).willReturn(false); + + assertFalse(approval.test(sequence)); + } + + private OrderSequence createOrderSequence() { + return new OrderSequence("some-sequence"); + } + + private TransportOrder createTransportOrder() { + return new TransportOrder("some-order", List.of()); + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/DefaultPeripheralJobCleanupApprovalTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/DefaultPeripheralJobCleanupApprovalTest.java new file mode 100644 index 0000000..45432bb --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/DefaultPeripheralJobCleanupApprovalTest.java @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import java.time.Instant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.data.peripherals.PeripheralOperation; + +/** + * Test for {@link DefaultPeripheralJobCleanupApproval}. + */ +class DefaultPeripheralJobCleanupApprovalTest { + + private CreationTimeThreshold creationTimeThreshold; + private DefaultPeripheralJobCleanupApproval approval; + + @BeforeEach + void setUp() { + creationTimeThreshold = mock(); + given(creationTimeThreshold.getCurrentThreshold()) + .willReturn(Instant.parse("2024-01-01T12:00:00.00Z")); + + approval = new DefaultPeripheralJobCleanupApproval(creationTimeThreshold); + } + + @ParameterizedTest + @EnumSource( + value = PeripheralJob.State.class, + names = {"FINISHED", "FAILED"} + ) + void approvePeripheralJob(PeripheralJob.State state) { + PeripheralJob job = createPeripheralJob() + .withState(state) + .withCreationTime(Instant.parse("2024-01-01T09:00:00.00Z")); + + assertTrue(approval.test(job)); + } + + @ParameterizedTest + @EnumSource( + value = PeripheralJob.State.class, + mode = EnumSource.Mode.EXCLUDE, + names = {"FINISHED", "FAILED"} + ) + void disapprovePeripheralJobNotInFinalState(PeripheralJob.State state) { + PeripheralJob job = createPeripheralJob() + .withState(state) + .withCreationTime(Instant.parse("2024-01-01T09:00:00.00Z")); + + assertFalse(approval.test(job)); + } + + @Test + void disapprovePeripheralJobCreatedAfterCurrentThreshold() { + PeripheralJob job = createPeripheralJob() + .withState(PeripheralJob.State.FINISHED) + .withCreationTime(Instant.parse("2024-01-01T15:00:00.00Z")); + + assertFalse(approval.test(job)); + } + + private PeripheralJob createPeripheralJob() { + LocationType locationType = new LocationType("some-location-type"); + Location location = new Location("some-location", locationType.getReference()); + PeripheralOperation peripheralOperation + = new PeripheralOperation( + location.getReference(), + "some-operation", + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + true + ); + return new PeripheralJob("some-job-name", "some-token", peripheralOperation); + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/DefaultTransportOrderCleanupApprovalTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/DefaultTransportOrderCleanupApprovalTest.java new file mode 100644 index 0000000..d677384 --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/DefaultTransportOrderCleanupApprovalTest.java @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import java.time.Instant; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.data.peripherals.PeripheralOperation; + +/** + * Test for {@link DefaultTransportOrderCleanupApproval}. + */ +class DefaultTransportOrderCleanupApprovalTest { + + private PeripheralJobPoolManager peripheralJobPoolManager; + private TCSObjectRepository objectRepo; + private DefaultPeripheralJobCleanupApproval defaultPeripheralJobCleanupApproval; + private CreationTimeThreshold creationTimeThreshold; + private DefaultTransportOrderCleanupApproval approval; + + @BeforeEach + void setUp() { + peripheralJobPoolManager = mock(); + objectRepo = mock(); + defaultPeripheralJobCleanupApproval = mock(); + creationTimeThreshold = mock(); + given(peripheralJobPoolManager.getObjectRepo()).willReturn(objectRepo); + given(creationTimeThreshold.getCurrentThreshold()) + .willReturn(Instant.parse("2024-01-01T12:00:00.00Z")); + + approval = new DefaultTransportOrderCleanupApproval( + peripheralJobPoolManager, + defaultPeripheralJobCleanupApproval, + creationTimeThreshold + ); + } + + @ParameterizedTest + @EnumSource( + value = TransportOrder.State.class, + names = {"FINISHED", "FAILED", "UNROUTABLE"} + ) + void approveTransportOrder(TransportOrder.State state) { + TransportOrder order = createTransportOrder() + .withState(state) + .withCreationTime(Instant.parse("2024-01-01T09:00:00.00Z")); + + assertTrue(approval.test(order)); + } + + @ParameterizedTest + @EnumSource( + value = TransportOrder.State.class, + mode = EnumSource.Mode.EXCLUDE, + names = {"FINISHED", "FAILED", "UNROUTABLE"} + ) + void disapproveTransportOrderInNonFinalState(TransportOrder.State state) { + TransportOrder order = createTransportOrder() + .withState(state) + .withCreationTime(Instant.parse("2024-01-01T09:00:00.00Z")); + + assertFalse(approval.test(order)); + } + + @Test + void disapproveTransportOrderRelatedToJobWithNonFinalState() { + TransportOrder order = createTransportOrder() + .withState(TransportOrder.State.FINISHED) + .withCreationTime(Instant.parse("2024-01-01T09:00:00.00Z")); + PeripheralJob job = createPeripheralJob() + .withState(PeripheralJob.State.BEING_PROCESSED) + .withRelatedTransportOrder(order.getReference()); + given(objectRepo.getObjects(eq(PeripheralJob.class), any())).willReturn(Set.of(job)); + + assertFalse(approval.test(order)); + } + + @Test + void disapproveTransportOrderRelatedToUnapprovedJob() { + TransportOrder order = createTransportOrder() + .withState(TransportOrder.State.FINISHED) + .withCreationTime(Instant.parse("2024-01-01T09:00:00.00Z")); + PeripheralJob job = createPeripheralJob() + .withState(PeripheralJob.State.FAILED) + .withRelatedTransportOrder(order.getReference()); + given(objectRepo.getObjects(eq(PeripheralJob.class), any())).willReturn(Set.of(job)); + given(defaultPeripheralJobCleanupApproval.test(job)).willReturn(false); + + assertFalse(approval.test(order)); + } + + @Test + void disapproveTransportOrderCreatedAfterCreationTime() { + TransportOrder order = createTransportOrder() + .withState(TransportOrder.State.FINISHED) + .withCreationTime(Instant.parse("2024-01-01T15:00:00.00Z")); + + assertFalse(approval.test(order)); + } + + private TransportOrder createTransportOrder() { + return new TransportOrder("some-order", List.of()); + } + + private PeripheralJob createPeripheralJob() { + LocationType locationType = new LocationType("some-location-type"); + Location location = new Location("some-location", locationType.getReference()); + PeripheralOperation peripheralOperation + = new PeripheralOperation( + location.getReference(), + "some-operation", + PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION, + true + ); + return new PeripheralJob("some-job-name", "some-token", peripheralOperation); + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/NotificationBufferTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/NotificationBufferTest.java new file mode 100644 index 0000000..8263888 --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/NotificationBufferTest.java @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.notification.UserNotification; +import org.opentcs.util.event.SimpleEventBus; + +/** + * Tests for {@link NotificationBuffer}. + */ +class NotificationBufferTest { + + private NotificationBuffer notificationBuffer; + + private UserNotification notification1; + private UserNotification notification2; + private UserNotification notification3; + private UserNotification notification4; + + @BeforeEach + void setUp() { + notificationBuffer = new NotificationBuffer(new SimpleEventBus()); + + notification1 = new UserNotification("notification-1", UserNotification.Level.NOTEWORTHY); + notification2 = new UserNotification("notification-2", UserNotification.Level.NOTEWORTHY); + notification3 = new UserNotification("notification-3", UserNotification.Level.NOTEWORTHY); + notification4 = new UserNotification("notification-4", UserNotification.Level.NOTEWORTHY); + } + + @Test + void keepAllNotificationsBeforeOverflow() { + notificationBuffer.setCapacity(3); + + notificationBuffer.addNotification(notification1); + notificationBuffer.addNotification(notification2); + notificationBuffer.addNotification(notification3); + + assertThat( + notificationBuffer.getNotifications(null), + contains(notification1, notification2, notification3) + ); + } + + @Test + void keepYoungestNotificationsAfterOverflow() { + notificationBuffer.setCapacity(3); + + notificationBuffer.addNotification(notification1); + notificationBuffer.addNotification(notification2); + notificationBuffer.addNotification(notification3); + notificationBuffer.addNotification(notification4); + + assertThat( + notificationBuffer.getNotifications(null), + contains(notification2, notification3, notification4) + ); + } + + @Test + void removeExtraNotificationsOnCapacityReduction() { + notificationBuffer.setCapacity(4); + + notificationBuffer.addNotification(notification1); + notificationBuffer.addNotification(notification2); + notificationBuffer.addNotification(notification3); + notificationBuffer.addNotification(notification4); + + assertThat(notificationBuffer.getNotifications(null), hasSize(4)); + assertThat( + notificationBuffer.getNotifications(null), + contains(notification1, notification2, notification3, notification4) + ); + + notificationBuffer.setCapacity(2); + + assertThat(notificationBuffer.getNotifications(null), hasSize(2)); + assertThat(notificationBuffer.getNotifications(null), contains(notification3, notification4)); + } + + @Test + void removeAllNotificationsOnClear() { + notificationBuffer.setCapacity(3); + notificationBuffer.addNotification(notification1); + notificationBuffer.addNotification(notification2); + + notificationBuffer.clear(); + + assertThat(notificationBuffer.getNotifications(null), is(empty())); + } + + @Test + void returnNotificationsMatchingFilter() { + notificationBuffer.setCapacity(3); + + notificationBuffer.addNotification(notification1); + notificationBuffer.addNotification(notification2); + notificationBuffer.addNotification(notification3); + notificationBuffer.addNotification(notification4); + + assertThat( + notificationBuffer.getNotifications( + notification -> notification.getText().equals("notification-2") + ), + contains(notification2) + ); + } + +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/PeripheralJobPoolManagerTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/PeripheralJobPoolManagerTest.java new file mode 100644 index 0000000..c32c4fe --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/PeripheralJobPoolManagerTest.java @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.access.to.model.LocationCreationTO; +import org.opentcs.access.to.model.LocationTypeCreationTO; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.access.to.peripherals.PeripheralJobCreationTO; +import org.opentcs.access.to.peripherals.PeripheralOperationCreationTO; +import org.opentcs.data.model.Triple; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.data.peripherals.PeripheralOperation; +import org.opentcs.util.event.SimpleEventBus; + +/** + * Unit tests for {@link PeripheralJobPoolManager}. + */ +class PeripheralJobPoolManagerTest { + + /** + * The object repository. + */ + private TCSObjectRepository objectRepo; + /** + * Manages plant model data. + */ + private PlantModelManager plantModelManager; + /** + * The job pool manager to be tested here. + */ + private PeripheralJobPoolManager jobPoolManager; + + @BeforeEach + void setUp() { + objectRepo = new TCSObjectRepository(); + plantModelManager = new PlantModelManager(objectRepo, new SimpleEventBus()); + jobPoolManager = new PeripheralJobPoolManager( + objectRepo, + new SimpleEventBus(), + new PrefixedUlidObjectNameProvider() + ); + + // Set up a minimal plant model. + plantModelManager.createPlantModelObjects( + new PlantModelCreationTO("some-plant-model") + .withLocationType(new LocationTypeCreationTO("some-location-type")) + .withLocation( + new LocationCreationTO( + "some-location", + "some-location-type", + new Triple(1, 2, 3) + ) + ) + ); + } + + @Test + void storeCreatedObjectsInRepo() { + jobPoolManager.createPeripheralJob( + new PeripheralJobCreationTO( + "some-job", + "some-token", + new PeripheralOperationCreationTO("some-operation", "some-location") + ) + ); + + assertThat(objectRepo.getObjects(PeripheralJob.class), hasSize(1)); + assertThat(objectRepo.getObject(PeripheralJob.class, "some-job"), is(notNullValue())); + } + + @Test + void removeAllCreatedObjectsOnClear() { + jobPoolManager.createPeripheralJob( + new PeripheralJobCreationTO( + "some-job", + "some-token", + new PeripheralOperationCreationTO("some-operation", "some-location") + ) + ); + + jobPoolManager.clear(); + + assertThat(objectRepo.getObjects(PeripheralJob.class), is(empty())); + } + + @Test + public void doNotCreateJobWithCompletionRequiredAndExecutionTriggerImmediate() { + assertThrows( + IllegalArgumentException.class, () -> { + jobPoolManager.createPeripheralJob( + new PeripheralJobCreationTO( + "some-job", + "some-token", + new PeripheralOperationCreationTO("some-operation", "some-location") + .withExecutionTrigger(PeripheralOperation.ExecutionTrigger.IMMEDIATE) + .withCompletionRequired(true) + ) + ); + } + ); + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/PlantModelManagerTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/PlantModelManagerTest.java new file mode 100644 index 0000000..987cf7c --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/PlantModelManagerTest.java @@ -0,0 +1,193 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.access.to.model.BlockCreationTO; +import org.opentcs.access.to.model.LocationCreationTO; +import org.opentcs.access.to.model.LocationTypeCreationTO; +import org.opentcs.access.to.model.PathCreationTO; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.access.to.model.PointCreationTO; +import org.opentcs.access.to.model.VehicleCreationTO; +import org.opentcs.access.to.model.VisualLayoutCreationTO; +import org.opentcs.access.to.peripherals.PeripheralOperationCreationTO; +import org.opentcs.data.model.Block; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Triple; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.model.visualization.VisualLayout; +import org.opentcs.util.event.SimpleEventBus; + +/** + * Unit tests for {@link PlantModelManager}. + */ +class PlantModelManagerTest { + + private TCSObjectRepository objectRepo; + private PlantModelManager plantModelManager; + private PlantModelCreationTO plantModelCreationTo; + + @BeforeEach + void setUp() { + objectRepo = new TCSObjectRepository(); + plantModelManager = new PlantModelManager(objectRepo, new SimpleEventBus()); + plantModelCreationTo = new PlantModelCreationTO("some-plant-model") + .withPoint(new PointCreationTO("point1")) + .withPoint(new PointCreationTO("point2")) + .withPath( + new PathCreationTO("some-path", "point1", "point2") + .withPeripheralOperations( + List.of( + new PeripheralOperationCreationTO("some-op", "some-location") + ) + ) + ) + .withLocationType( + new LocationTypeCreationTO("some-location-type") + .withAllowedOperations(List.of("some-op")) + ) + .withLocation( + new LocationCreationTO( + "some-location", + "some-location-type", + new Triple(1, 2, 3) + ) + .withLink("point1", Set.of("some-op")) + ) + .withBlock(new BlockCreationTO("some-block")) + .withVehicle(new VehicleCreationTO("some-vehicle")) + .withVisualLayout(new VisualLayoutCreationTO("some-visual-layout")) + .withProperty("some-prop-key", "some-prop-value"); + } + + @Test + void importPlantModel() { + plantModelManager.createPlantModelObjects(plantModelCreationTo); + + assertThat(plantModelManager.getName(), is(equalTo("some-plant-model"))); + assertThat(plantModelManager.getProperties(), hasEntry("some-prop-key", "some-prop-value")); + assertThat(objectRepo.getObjects(Point.class), hasSize(2)); + assertThat(objectRepo.getObject(Point.class, "point1"), is(notNullValue())); + assertThat(objectRepo.getObject(Point.class, "point2"), is(notNullValue())); + assertThat(objectRepo.getObjects(Path.class), hasSize(1)); + assertThat(objectRepo.getObject(Path.class, "some-path"), is(notNullValue())); + assertThat(objectRepo.getObjects(LocationType.class), hasSize(1)); + assertThat(objectRepo.getObject(LocationType.class, "some-location-type"), is(notNullValue())); + assertThat(objectRepo.getObjects(Location.class), hasSize(1)); + assertThat(objectRepo.getObject(Location.class, "some-location"), is(notNullValue())); + assertThat(objectRepo.getObjects(Block.class), hasSize(1)); + assertThat(objectRepo.getObject(Block.class, "some-block"), is(notNullValue())); + assertThat(objectRepo.getObjects(Vehicle.class), hasSize(1)); + assertThat(objectRepo.getObject(Vehicle.class, "some-vehicle"), is(notNullValue())); + assertThat(objectRepo.getObjects(VisualLayout.class), hasSize(1)); + assertThat(objectRepo.getObject(VisualLayout.class, "some-visual-layout"), is(notNullValue())); + } + + @Test + void exportPlantModel() { + plantModelManager.createPlantModelObjects(plantModelCreationTo); + + PlantModelCreationTO exportedModel = plantModelManager.createPlantModelCreationTO(); + + assertThat(exportedModel.getName(), is(equalTo("some-plant-model"))); + assertThat(exportedModel.getPoints(), hasSize(2)); + assertThat(exportedModel.getPaths(), hasSize(1)); + assertThat(exportedModel.getLocationTypes(), hasSize(1)); + assertThat(exportedModel.getLocations(), hasSize(1)); + assertThat(exportedModel.getBlocks(), hasSize(1)); + assertThat(exportedModel.getVehicles(), hasSize(1)); + } + + @Test + void clearPlantModel() { + plantModelManager.createPlantModelObjects(plantModelCreationTo); + + plantModelManager.clear(); + + assertThat(objectRepo.getObjects(Point.class), is(empty())); + assertThat(objectRepo.getObjects(Path.class), is(empty())); + assertThat(objectRepo.getObjects(LocationType.class), is(empty())); + assertThat(objectRepo.getObjects(Location.class), is(empty())); + assertThat(objectRepo.getObjects(Block.class), is(empty())); + assertThat(objectRepo.getObjects(Vehicle.class), is(empty())); + assertThat(objectRepo.getObjects(VisualLayout.class), is(empty())); + } + + @Test + void expandResources() { + plantModelManager.createPlantModelObjects( + new PlantModelCreationTO("some-plant-model") + .withPoint(new PointCreationTO("point-in-block-1")) + .withPoint(new PointCreationTO("point-in-block-2")) + .withPoint(new PointCreationTO("point-outside-of-block")) + .withPath(new PathCreationTO("path-in-block", "point-in-block-1", "point-in-block-2")) + .withLocationType(new LocationTypeCreationTO("some-location-type")) + .withLocation( + new LocationCreationTO( + "location-in-block", + "some-location-type", + new Triple(1, 2, 3) + ) + ) + .withBlock( + new BlockCreationTO("some-block") + .withMemberNames( + Set.of( + "point-in-block-1", + "point-in-block-2", + "path-in-block", + "location-in-block" + ) + ) + ) + ); + + Path pathInBlock = objectRepo.getObject(Path.class, "path-in-block"); + Point pointInBlock = objectRepo.getObject(Point.class, "point-in-block-1"); + Point pointOutsideOfBlock = objectRepo.getObject(Point.class, "point-outside-of-block"); + + assertThat( + "Single element in block should result in all elements of block.", + plantModelManager.expandResources(Set.of(pathInBlock.getReference())), + hasSize(4) + ); + assertThat( + "Multiple elements in block should result in all elements of block.", + plantModelManager.expandResources( + Set.of( + pathInBlock.getReference(), pointInBlock.getReference() + ) + ), + hasSize(4) + ); + assertThat( + "Single element not in block should result in this single element.", + plantModelManager.expandResources(Set.of(pointOutsideOfBlock.getReference())), + hasSize(1) + ); + assertThat( + "Elements inside and outside of block should result in block + outside elements.", + plantModelManager.expandResources( + Set.of( + pointOutsideOfBlock.getReference(), pointInBlock.getReference() + ) + ), + hasSize(5) + ); + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/PrefixedUlidObjectNameProviderTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/PrefixedUlidObjectNameProviderTest.java new file mode 100644 index 0000000..962e09b --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/PrefixedUlidObjectNameProviderTest.java @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.startsWith; + +import com.google.common.collect.Ordering; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.access.to.CreationTO; + +/** + * Tests for a {@link PrefixedUlidObjectNameProvider}. + */ +class PrefixedUlidObjectNameProviderTest { + + private PrefixedUlidObjectNameProvider nameProvider; + + @BeforeEach + void setUp() { + this.nameProvider = new PrefixedUlidObjectNameProvider(); + } + + @Test + void shouldUsePrefixFromCreationTO() { + assertThat(nameProvider.apply(new CreationTO("SomeName-")), startsWith("SomeName-")); + } + + @Test + void shouldAppendSuffix() { + assertThat(nameProvider.apply(new CreationTO("")), is(not(""))); + } + + @Test + void shouldProvideNamesInChronologicalOrder() { + final int count = 100000; + final CreationTO to = new CreationTO("SomeName-"); + + List namesInOrderOfCreation = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + namesInOrderOfCreation.add(nameProvider.apply(to)); + } + + List namesLexicographic = Ordering.natural().sortedCopy(namesInOrderOfCreation); + + assertThat(namesInOrderOfCreation, is(equalTo(namesLexicographic))); + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/TCSObjectManagerTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/TCSObjectManagerTest.java new file mode 100644 index 0000000..9ca5b8a --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/TCSObjectManagerTest.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.model.Point; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.SimpleEventBus; + +/** + * Unit tests for {@link TCSObjectManager}. + */ +class TCSObjectManagerTest { + + private TCSObjectRepository objectRepo; + private EventBus eventBus; + private TCSObjectManager objectManager; + + @BeforeEach + void setUp() { + objectRepo = new TCSObjectRepository(); + eventBus = new SimpleEventBus(); + objectManager = new TCSObjectManager(objectRepo, eventBus); + } + + @Test + void emitEvent() { + List receivedEvents = new ArrayList<>(); + eventBus.subscribe(event -> receivedEvents.add(event)); + Point someObject = new Point("Point-00001").withType(Point.Type.HALT_POSITION); + + assertThat(receivedEvents, hasSize(0)); + + objectManager.emitObjectEvent( + someObject.withType(Point.Type.PARK_POSITION), + someObject, + TCSObjectEvent.Type.OBJECT_MODIFIED + ); + + assertThat(receivedEvents, hasSize(1)); + } + +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/TCSObjectRepositoryTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/TCSObjectRepositoryTest.java new file mode 100644 index 0000000..d4db23d --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/TCSObjectRepositoryTest.java @@ -0,0 +1,231 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.ObjectExistsException; +import org.opentcs.data.ObjectUnknownException; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; + +/** + * Unit tests for {@link TCSObjectRepository}. + */ +class TCSObjectRepositoryTest { + + private TCSObjectRepository pool; + + @BeforeEach + void setUp() { + pool = new TCSObjectRepository(); + } + + @Test + void returnObjectByClassAndName() { + Point point1 = new Point("Point-00001"); + Point point2 = new Point("Point-00002"); + + pool.addObject(point1); + pool.addObject(point2); + + assertThat(pool.getObjectOrNull(Point.class, "Point-00001"), is(point1)); + assertThat(pool.getObjectOrNull(Point.class, "Point-00002"), is(point2)); + assertThat(pool.getObject(Point.class, "Point-00001"), is(point1)); + assertThat(pool.getObject(Point.class, "Point-00002"), is(point2)); + } + + @Test + void returnNullForNonexistentObjectByClassAndName() { + assertThat(pool.getObjectOrNull(Point.class, "some-name"), is(nullValue())); + } + + @Test + void throwOnGetNonexistentObjectByClassAndName() { + assertThrows(ObjectUnknownException.class, () -> pool.getObject(Point.class, "some-name")); + } + + @Test + void returnObjectByName() { + Point point1 = new Point("Point-00001"); + Point point2 = new Point("Point-00002"); + + pool.addObject(point1); + pool.addObject(point2); + + assertThat(pool.getObjectOrNull("Point-00001"), is(point1)); + assertThat(pool.getObjectOrNull("Point-00002"), is(point2)); + assertThat(pool.getObject("Point-00001"), is(point1)); + assertThat(pool.getObject("Point-00002"), is(point2)); + } + + @Test + void returnNullForNonexistentObjectByName() { + assertThat(pool.getObjectOrNull("some-name"), is(nullValue())); + } + + @Test + void throwOnGetNonexistentObjectByName() { + assertThrows(ObjectUnknownException.class, () -> pool.getObject("some-name")); + } + + @Test + void returnObjectByClassAndRef() { + Point point1 = new Point("Point-00001"); + Point point2 = new Point("Point-00002"); + + pool.addObject(point1); + pool.addObject(point2); + + assertThat(pool.getObjectOrNull(Point.class, point1.getReference()), is(point1)); + assertThat(pool.getObjectOrNull(Point.class, point2.getReference()), is(point2)); + assertThat(pool.getObject(Point.class, point1.getReference()), is(point1)); + assertThat(pool.getObject(Point.class, point2.getReference()), is(point2)); + } + + @Test + void returnNullForNonexistentObjectByClassAndRef() { + assertThat( + pool.getObjectOrNull(Point.class, new Point("some-point").getReference()), + is(nullValue()) + ); + } + + @Test + void throwOnGetNonexistentObjectByClassAndRef() { + assertThrows( + ObjectUnknownException.class, + () -> pool.getObject(Point.class, new Point("some-point").getReference()) + ); + } + + @Test + void returnObjectByRef() { + Point point1 = new Point("Point-00001"); + Point point2 = new Point("Point-00002"); + + pool.addObject(point1); + pool.addObject(point2); + + assertThat(pool.getObjectOrNull(point1.getReference()), is(point1)); + assertThat(pool.getObjectOrNull(point2.getReference()), is(point2)); + assertThat(pool.getObject(point1.getReference()), is(point1)); + assertThat(pool.getObject(point2.getReference()), is(point2)); + } + + @Test + void returnNullForNonexistentObjectByRef() { + assertThat(pool.getObjectOrNull(new Point("some-point").getReference()), is(nullValue())); + } + + @Test + void throwOnGetNonexistentObjectByRef() { + Point point = new Point("some-point"); + + assertThrows(ObjectUnknownException.class, () -> pool.getObject(point.getReference())); + } + + @Test + void returnObjectsByClass() { + Point point1 = new Point("Point-00001"); + Point point2 = new Point("Point-00002"); + Path path1 = new Path("Path-00001", point1.getReference(), point2.getReference()); + + pool.addObject(point1); + pool.addObject(point2); + pool.addObject(path1); + + Set points = pool.getObjects(Point.class); + Set paths = pool.getObjects(Path.class); + + assertThat(points.size(), is(2)); + assertThat(points, containsInAnyOrder(point1, point2)); + + assertThat(paths.size(), is(1)); + assertThat(paths, contains(path1)); + } + + @Test + void returnObjectsByClassAndPredicate() { + Point point1 = new Point("Point-00001"); + Point point2 = new Point("Point-00002"); + Path path1 = new Path("Path-00001", point1.getReference(), point2.getReference()); + + pool.addObject(point1); + pool.addObject(point2); + pool.addObject(path1); + + Set points = pool.getObjects(Point.class, point -> true); + Set paths = pool.getObjects(Path.class, path -> false); + + assertThat(points.size(), is(2)); + assertThat(points, containsInAnyOrder(point1, point2)); + + assertThat(paths, is(empty())); + } + + @Test + void replaceObjectWithSameName() { + Point pointV1 = new Point("some-point").withType(Point.Type.HALT_POSITION); + Point pointV2 = pointV1.withType(Point.Type.PARK_POSITION); + + pool.addObject(pointV1); + pool.replaceObject(pointV2); + + assertThat(pool.getObjects(Point.class).size(), is(1)); + assertThat(pool.getObjects(Point.class), contains(pointV2)); + } + + @Test + void throwOnReplaceObjectWithNonexistentName() { + Point point1 = new Point("some-point").withType(Point.Type.HALT_POSITION); + Point point2 = new Point("some-other-point").withType(Point.Type.PARK_POSITION); + + pool.addObject(point1); + assertThrows(IllegalArgumentException.class, () -> pool.replaceObject(point2)); + } + + @Test + void throwOnReplaceObjectWithDifferentType() { + Point point = new Point("my-object"); + LocationType locationType = new LocationType("my-object"); + + pool.addObject(point); + assertThrows(IllegalArgumentException.class, () -> pool.replaceObject(locationType)); + } + + @Test + void removeObjectByRef() { + Point point1 = new Point("Point-00001"); + + pool.addObject(point1); + pool.removeObject(point1.getReference()); + + assertThat(pool.getObjectOrNull(point1.getReference()), is(nullValue())); + } + + @Test + void throwOnRemoveNonexistentObjectByRef() { + assertThrows( + ObjectUnknownException.class, + () -> pool.removeObject(new Point("some-point").getReference()) + ); + } + + @Test + void throwOnAddObjectWithExistingName() { + pool.addObject(new Point("some-point")); + // Another object with the same name. + assertThrows(ObjectExistsException.class, () -> pool.addObject(new Point("some-point"))); + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/TransportOrderPoolManagerTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/TransportOrderPoolManagerTest.java new file mode 100644 index 0000000..6b7546a --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/TransportOrderPoolManagerTest.java @@ -0,0 +1,217 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.opentcs.access.to.model.LocationCreationTO; +import org.opentcs.access.to.model.LocationTypeCreationTO; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.access.to.model.PointCreationTO; +import org.opentcs.access.to.model.VehicleCreationTO; +import org.opentcs.access.to.order.DestinationCreationTO; +import org.opentcs.access.to.order.OrderSequenceCreationTO; +import org.opentcs.access.to.order.TransportOrderCreationTO; +import org.opentcs.data.model.Triple; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.util.event.SimpleEventBus; + +/** + * Unit tests for {@link TransportOrderPoolManager}. + */ +class TransportOrderPoolManagerTest { + + /** + * The object repository. + */ + private TCSObjectRepository objectRepo; + /** + * Manages plant model data. + */ + private PlantModelManager plantModelManager; + /** + * The order pool manager to be tested here. + */ + private TransportOrderPoolManager orderPoolManager; + + @BeforeEach + void setUp() { + objectRepo = new TCSObjectRepository(); + plantModelManager = new PlantModelManager(objectRepo, new SimpleEventBus()); + orderPoolManager = new TransportOrderPoolManager( + objectRepo, + new SimpleEventBus(), + new PrefixedUlidObjectNameProvider() + ); + + // Set up a minimal plant model. + plantModelManager.createPlantModelObjects( + new PlantModelCreationTO("some-plant-model") + .withLocationType(new LocationTypeCreationTO("some-location-type")) + .withLocation( + new LocationCreationTO( + "some-location", + "some-location-type", + new Triple(1, 2, 3) + ) + .withLink("some-point", new HashSet<>()) + ) + .withPoint(new PointCreationTO("some-point")) + ); + } + + @Test + void storeCreatedObjectsInRepo() { + orderPoolManager.createTransportOrder( + new TransportOrderCreationTO( + "some-order", + List.of(new DestinationCreationTO("some-location", "NOP")) + ) + .withIncompleteName(false) + ); + orderPoolManager.createOrderSequence(new OrderSequenceCreationTO("some-sequence")); + + assertThat(objectRepo.getObjects(TransportOrder.class), hasSize(1)); + assertThat(objectRepo.getObject(TransportOrder.class, "some-order"), is(notNullValue())); + assertThat(objectRepo.getObjects(OrderSequence.class), hasSize(1)); + assertThat(objectRepo.getObject(OrderSequence.class, "some-sequence"), is(notNullValue())); + } + + @Test + void removeAllCreatedObjectsOnClear() { + orderPoolManager.createTransportOrder( + new TransportOrderCreationTO( + "some-order", + List.of(new DestinationCreationTO("some-location", "NOP")) + ) + ); + orderPoolManager.createOrderSequence(new OrderSequenceCreationTO("some-sequence")); + + orderPoolManager.clear(); + + assertThat(objectRepo.getObjects(TransportOrder.class), is(empty())); + assertThat(objectRepo.getObjects(OrderSequence.class), is(empty())); + } + + @ParameterizedTest + @EnumSource( + value = TransportOrder.State.class, + names = {"RAW", "ACTIVE", "DISPATCHABLE"} + ) + void allowSettingIntendedVehicleOnUnassignedTransportOrder(TransportOrder.State state) { + plantModelManager.createPlantModelObjects( + new PlantModelCreationTO("some-model") + .withPoint(new PointCreationTO("some-point")) + .withLocationType( + new LocationTypeCreationTO("some-location-type") + .withAllowedOperations(List.of("NOP")) + ) + .withLocation( + new LocationCreationTO("some-location", "some-location-type", new Triple(1, 2, 3)) + .withLink("some-point", Set.of("NOP")) + ) + .withVehicle(new VehicleCreationTO("some-vehicle")) + ); + Vehicle vehicle = objectRepo.getObject(Vehicle.class, "some-vehicle"); + + TransportOrder order = orderPoolManager.createTransportOrder( + new TransportOrderCreationTO( + "some-order", + List.of(new DestinationCreationTO("some-location", "NOP")) + ) + ); + orderPoolManager.setTransportOrderState(order.getReference(), state); + + TransportOrder result + = orderPoolManager.setTransportOrderIntendedVehicle( + order.getReference(), + vehicle.getReference() + ); + + assertThat(result.getIntendedVehicle(), is(equalTo(vehicle.getReference()))); + } + + @ParameterizedTest + @EnumSource( + value = TransportOrder.State.class, + names = {"BEING_PROCESSED", "WITHDRAWN", "FINISHED", "FAILED", "UNROUTABLE"} + ) + void disallowSettingIntendedVehicleOnAssignedTransportOrder(TransportOrder.State state) { + plantModelManager.createPlantModelObjects( + new PlantModelCreationTO("some-model") + .withPoint(new PointCreationTO("some-point")) + .withLocationType( + new LocationTypeCreationTO("some-location-type") + .withAllowedOperations(List.of("NOP")) + ) + .withLocation( + new LocationCreationTO("some-location", "some-location-type", new Triple(1, 2, 3)) + .withLink("some-point", Set.of("NOP")) + ) + .withVehicle(new VehicleCreationTO("some-vehicle")) + ); + Vehicle vehicle = objectRepo.getObject(Vehicle.class, "some-vehicle"); + + TransportOrder order = orderPoolManager.createTransportOrder( + new TransportOrderCreationTO( + "some-order", + List.of(new DestinationCreationTO("some-location", "NOP")) + ) + ); + orderPoolManager.setTransportOrderState(order.getReference(), state); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> { + orderPoolManager.setTransportOrderIntendedVehicle( + order.getReference(), + vehicle.getReference() + ); + } + ); + } + + @ParameterizedTest + @EnumSource( + value = TransportOrder.State.class, + names = {"FINISHED", "FAILED", "UNROUTABLE"} + ) + void removeSingleTransportOrderIfFinished(TransportOrder.State state) { + TransportOrder order = orderPoolManager.createTransportOrder( + new TransportOrderCreationTO( + "some-order", + List.of(new DestinationCreationTO("some-location", "NOP")) + ) + ); + orderPoolManager.setTransportOrderState(order.getReference(), state); + orderPoolManager.removeTransportOrder(order.getReference()); + + assertThat(objectRepo.getObjects(TransportOrder.class), is(empty())); + } + + @Test + void removeSingleOrderSequence() { + OrderSequence sequence = orderPoolManager.createOrderSequence( + new OrderSequenceCreationTO("some-sequence") + ); + + orderPoolManager.removeOrderSequence(sequence.getReference()); + + assertThat(objectRepo.getObjects(OrderSequence.class), is(empty())); + } +} diff --git a/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/WorkingSetCleanupTaskTest.java b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/WorkingSetCleanupTaskTest.java new file mode 100644 index 0000000..3774777 --- /dev/null +++ b/opentcs-kernel/src/test/java/org/opentcs/kernel/workingset/WorkingSetCleanupTaskTest.java @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernel.workingset; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.kernel.OrderPoolConfiguration; + +class WorkingSetCleanupTaskTest { + + private WorkingSetCleanupTask cleanupTask; + private TCSObjectRepository objectRepository; + + private OrderPoolConfiguration configuration; + + @BeforeEach + void setup() { + configuration = mock(); + objectRepository = new TCSObjectRepository(); + CreationTimeThreshold creationTimeThreshold = new CreationTimeThreshold(); + + PeripheralJobPoolManager peripheralJobPoolManager + = new PeripheralJobPoolManager( + objectRepository, + mock(), + new PrefixedUlidObjectNameProvider() + ); + TransportOrderPoolManager orderPoolManager + = new TransportOrderPoolManager( + objectRepository, + mock(), + new PrefixedUlidObjectNameProvider() + ); + DefaultPeripheralJobCleanupApproval peripheralJobCleanupApproval + = new DefaultPeripheralJobCleanupApproval(creationTimeThreshold); + DefaultTransportOrderCleanupApproval orderCleanupApproval + = new DefaultTransportOrderCleanupApproval( + peripheralJobPoolManager, + peripheralJobCleanupApproval, + creationTimeThreshold + ); + DefaultOrderSequenceCleanupApproval orderSequenceCleanupApproval + = new DefaultOrderSequenceCleanupApproval(orderPoolManager, orderCleanupApproval); + cleanupTask = new WorkingSetCleanupTask( + new Object(), + orderPoolManager, + peripheralJobPoolManager, + configuration, + new CompositeOrderSequenceCleanupApproval( + Set.of(), + orderSequenceCleanupApproval + ), + new CompositeTransportOrderCleanupApproval( + Set.of(), + orderCleanupApproval + ), + new CompositePeripheralJobCleanupApproval( + Set.of(), + peripheralJobCleanupApproval + ), + creationTimeThreshold + ); + } + + @Test + void cleanExpiredTransportOrders() { + when(configuration.sweepAge()).thenReturn(60000); + + objectRepository.addObject( + new TransportOrder("Order-1", List.of()) + .withCreationTime(Instant.now().minusMillis(70000)) + .withState(TransportOrder.State.FINISHED) + ); + + objectRepository.addObject( + new TransportOrder("Order-2", List.of()) + .withCreationTime(Instant.now().minusMillis(50000)) + .withState(TransportOrder.State.FINISHED) + ); + assertEquals(2, objectRepository.getObjects(TransportOrder.class).size()); + cleanupTask.run(); + assertEquals(1, objectRepository.getObjects(TransportOrder.class).size()); + } + + @Test + void cleanExpiredOrderSequences() { + when(configuration.sweepAge()).thenReturn(60000); + + OrderSequence orderSequence = new OrderSequence("seq-1"); + objectRepository.addObject(orderSequence); + + TransportOrder order = new TransportOrder("Order-1", List.of()) + .withCreationTime(Instant.now().minusMillis(70000)) + .withState(TransportOrder.State.FINISHED); + objectRepository.addObject(order); + + objectRepository.replaceObject(order.withWrappingSequence(orderSequence.getReference())); + objectRepository.replaceObject( + orderSequence.withOrder(order.getReference()) + .withComplete(true) + .withFinished(true) + ); + + assertEquals(1, objectRepository.getObjects(TransportOrder.class).size()); + assertEquals(1, objectRepository.getObjects(OrderSequence.class).size()); + cleanupTask.run(); + assertEquals(0, objectRepository.getObjects(TransportOrder.class).size()); + assertEquals(0, objectRepository.getObjects(OrderSequence.class).size()); + } + + @Test + void cleanRelatedPeripheralJob() { + when(configuration.sweepAge()).thenReturn(60000); + + TransportOrder order = new TransportOrder("Order-1", List.of()) + .withCreationTime(Instant.now().minusMillis(70000)) + .withState(TransportOrder.State.FINISHED); + objectRepository.addObject(order); + + objectRepository.addObject( + new PeripheralJob("Job-1", "Vehicle-1", mock()) + .withCreationTime(Instant.now().minusMillis(65000)) + .withState(PeripheralJob.State.FINISHED) + .withRelatedTransportOrder(order.getReference()) + ); + + assertEquals(1, objectRepository.getObjects(TransportOrder.class).size()); + assertEquals(1, objectRepository.getObjects(PeripheralJob.class).size()); + cleanupTask.run(); + assertEquals(0, objectRepository.getObjects(PeripheralJob.class).size()); + assertEquals(0, objectRepository.getObjects(TransportOrder.class).size()); + } + +} diff --git a/opentcs-kernelcontrolcenter/build.gradle b/opentcs-kernelcontrolcenter/build.gradle new file mode 100644 index 0000000..7a0e1b2 --- /dev/null +++ b/opentcs-kernelcontrolcenter/build.gradle @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply from: "${rootDir}/gradle/java-project.gradle" +apply from: "${rootDir}/gradle/java-codequality.gradle" +apply from: "${rootDir}/gradle/guice-application.gradle" +apply from: "${rootDir}/gradle/publishing-java.gradle" + +if (!hasProperty('mainClass')) { + ext.mainClass = 'org.opentcs.kernelcontrolcenter.RunKernelControlCenter' +} +application.mainClass = ext.mainClass + +ext.collectableDistDir = new File(buildDir, 'install') + +dependencies { + api project(':opentcs-api-injection') + api project(':opentcs-common') + api project(':opentcs-commadapter-loopback') + api project(':opentcs-peripheralcommadapter-loopback') + api project(':opentcs-impl-configuration-gestalt') + + runtimeOnly group: 'org.slf4j', name: 'slf4j-jdk14', version: '2.0.16' +} + +compileJava { + options.compilerArgs << "-Xlint:-rawtypes" +} + +distributions { + main { + contents { + from "${sourceSets.main.resources.srcDirs[0]}/org/opentcs/kernelcontrolcenter/distribution" + } + } +} + +// For now, we're using hand-crafted start scripts, so disable the application plugin's start +// script generation. +startScripts.enabled = false + +distTar.enabled = false + +task release { + dependsOn build + dependsOn installDist +} + +run { + systemProperties(['java.util.logging.config.file':'./config/logging.config',\ + 'sun.java2d.d3d':'false',\ + 'opentcs.base':'.',\ + 'opentcs.home':'.',\ + 'opentcs.configuration.reload.interval':'10000',\ + 'opentcs.configuration.provider':'gestalt']) + jvmArgs('-XX:-OmitStackTraceInFastThrow',\ + '-splash:bin/splash-image.gif') +} diff --git a/opentcs-kernelcontrolcenter/gradle.properties b/opentcs-kernelcontrolcenter/gradle.properties new file mode 100644 index 0000000..14431ac --- /dev/null +++ b/opentcs-kernelcontrolcenter/gradle.properties @@ -0,0 +1,40 @@ +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineMethodParams=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAfterDotInChainedMethodCalls=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineDisjunctiveCatchTypes=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineFor=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineImplements=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.sortMembersByVisibility=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.visibilityOrder=PUBLIC;PROTECTED;DEFAULT;PRIVATE +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.enable-indent=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineArrayInit=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineCallArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.keepGettersAndSettersTogether=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classMembersOrder=STATIC FIELD;FIELD;STATIC_INIT;CONSTRUCTOR;INSTANCE_INIT;STATIC METHOD;METHOD;STATIC CLASS;CLASS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapCommentText=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.importGroupsOrder=* +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize=4 +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineAnnotationArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineTryResources=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.preserveNewLinesInComments=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineParenthesized=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineThrows=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=100 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project diff --git a/opentcs-kernelcontrolcenter/src/dist/bin/splash-image.gif b/opentcs-kernelcontrolcenter/src/dist/bin/splash-image.gif new file mode 100644 index 0000000..9e6f131 Binary files /dev/null and b/opentcs-kernelcontrolcenter/src/dist/bin/splash-image.gif differ diff --git a/opentcs-kernelcontrolcenter/src/dist/bin/splash-image.gif.license b/opentcs-kernelcontrolcenter/src/dist/bin/splash-image.gif.license new file mode 100644 index 0000000..777faa6 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/dist/bin/splash-image.gif.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 diff --git a/opentcs-kernelcontrolcenter/src/dist/config/logging.config b/opentcs-kernelcontrolcenter/src/dist/config/logging.config new file mode 100644 index 0000000..46ee265 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/dist/config/logging.config @@ -0,0 +1,83 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +############################################################ +# Default Logging Configuration File +# +# You can use a different file by specifying a filename +# with the java.util.logging.config.file system property. +# For example java -Djava.util.logging.config.file=myfile +############################################################ + +############################################################ +# Global properties +############################################################ + +# "handlers" specifies a comma separated list of log Handler +# classes. These handlers will be installed during VM startup. +# Note that these classes must be on the system classpath. +# By default we only configure a ConsoleHandler, which will only +# show messages at the INFO and above levels. +#handlers= java.util.logging.ConsoleHandler + +# To also add the FileHandler, use the following line instead. +handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler + +# Default global logging level. +# This specifies which kinds of events are logged across +# all loggers. For any given facility this global level +# can be overriden by a facility specific level +# Note that the ConsoleHandler also has a separate level +# setting to limit messages printed to the console. +.level= INFO + +############################################################ +# Handler specific properties. +# Describes specific configuration info for Handlers. +############################################################ + +# default file output is in user's home directory. +java.util.logging.FileHandler.pattern = ./log/opentcs-kernelcontrolcenter.%g.log +java.util.logging.FileHandler.limit = 500000 +java.util.logging.FileHandler.count = 10 +#java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.FileHandler.formatter = org.opentcs.util.logging.SingleLineFormatter +java.util.logging.FileHandler.append = true +java.util.logging.FileHandler.level = FINE + +# Limit the message that are printed on the console to INFO and above. +java.util.logging.ConsoleHandler.level = FINE +#java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.ConsoleHandler.formatter = org.opentcs.util.logging.SingleLineFormatter + +# Our own handler for the GUI: +#org.opentcs.kernel.controlcenter.ControlCenterInfoHandler.level = WARNING + + +############################################################ +# Facility specific properties. +# Provides extra control for each logger. +############################################################ + +# For example, set the com.xyz.foo logger to only log SEVERE +# messages: +#com.xyz.foo.level = SEVERE + +# Logging configuration for single classes. Remember that you might also have to +# adjust handler levels! + +#org.opentcs.kernel.vehicles.StandardVehicleController.level = FINE +#org.opentcs.strategies.basic.dispatching.DefaultDispatcher.level = FINE +org.opentcs.kernel.util.RegistryProvider.level = FINE +org.opentcs.kernel.services.StandardRemoteKernelClientPortal.level = FINE +org.opentcs.kernel.services.StandardRemotePlantModelService.level = FINE +org.opentcs.kernel.services.StandardRemoteTransportOrderService.level = FINE +org.opentcs.kernel.services.StandardRemoteVehicleService.level = FINE +org.opentcs.kernel.services.StandardRemoteNotificationService.level = FINE +org.opentcs.kernel.services.StandardRemoteDispatcherService.level = FINE +org.opentcs.kernel.services.StandardRemoteRouterService.level = FINE +org.opentcs.kernel.services.StandardRemoteSchedulerService.level = FINE +org.opentcs.kernel.StandardRemoteKernel.level = FINE +org.opentcs.kernel.UserManager.level = FINE +org.opentcs.kernelcontrolcenter.KernelControlCenter.level = FINE +#org.opentcs.access.services.DefaultNotificationService.level = FINE \ No newline at end of file diff --git a/opentcs-kernelcontrolcenter/src/dist/config/opentcs-kernelcontrolcenter.properties b/opentcs-kernelcontrolcenter/src/dist/config/opentcs-kernelcontrolcenter.properties new file mode 100644 index 0000000..777faa6 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/dist/config/opentcs-kernelcontrolcenter.properties @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 diff --git a/opentcs-kernelcontrolcenter/src/dist/lib/openTCS-extensions/.keepme b/opentcs-kernelcontrolcenter/src/dist/lib/openTCS-extensions/.keepme new file mode 100644 index 0000000..e69de29 diff --git a/opentcs-kernelcontrolcenter/src/dist/log/statistics/.keepme b/opentcs-kernelcontrolcenter/src/dist/log/statistics/.keepme new file mode 100644 index 0000000..e69de29 diff --git a/opentcs-kernelcontrolcenter/src/dist/startKernelControlCenter.bat b/opentcs-kernelcontrolcenter/src/dist/startKernelControlCenter.bat new file mode 100644 index 0000000..58a4592 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/dist/startKernelControlCenter.bat @@ -0,0 +1,37 @@ +@echo off +rem SPDX-FileCopyrightText: The openTCS Authors +rem SPDX-License-Identifier: MIT +rem +rem Start the openTCS kernel control center. +rem + +rem Set window title +title KernelControlCenter (openTCS) + +rem Don't export variables to the parent shell +setlocal + +rem Set base directory names. +set OPENTCS_BASE=. +set OPENTCS_HOME=. +set OPENTCS_CONFIGDIR=%OPENTCS_HOME%\config +set OPENTCS_LIBDIR=%OPENTCS_BASE%\lib + +rem Set the class path +set OPENTCS_CP=%OPENTCS_LIBDIR%\*; +set OPENTCS_CP=%OPENTCS_CP%;%OPENTCS_LIBDIR%\openTCS-extensions\*; + +rem XXX Be a bit more clever to find out the name of the JVM runtime. +set JAVA=javaw + +rem Start kernel control center +start /b %JAVA% -enableassertions ^ + -Dopentcs.base="%OPENTCS_BASE%" ^ + -Dopentcs.home="%OPENTCS_HOME%" ^ + -Dopentcs.configuration.provider=gestalt ^ + -Dopentcs.configuration.reload.interval=10000 ^ + -Djava.util.logging.config.file="%OPENTCS_CONFIGDIR%\logging.config" ^ + -XX:-OmitStackTraceInFastThrow ^ + -classpath "%OPENTCS_CP%" ^ + -splash:bin/splash-image.gif ^ + org.opentcs.kernelcontrolcenter.RunKernelControlCenter diff --git a/opentcs-kernelcontrolcenter/src/dist/startKernelControlCenter.sh b/opentcs-kernelcontrolcenter/src/dist/startKernelControlCenter.sh new file mode 100644 index 0000000..a407f59 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/dist/startKernelControlCenter.sh @@ -0,0 +1,35 @@ +#!/bin/sh +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT +# +# Start the openTCS kernel control center. +# + +# Set base directory names. +export OPENTCS_BASE=. +export OPENTCS_HOME=. +export OPENTCS_CONFIGDIR="${OPENTCS_HOME}/config" +export OPENTCS_LIBDIR="${OPENTCS_BASE}/lib" + +# Set the class path +export OPENTCS_CP="${OPENTCS_LIBDIR}/*" +export OPENTCS_CP="${OPENTCS_CP}:${OPENTCS_LIBDIR}/openTCS-extensions/*" + +if [ -n "${OPENTCS_JAVAVM}" ]; then + export JAVA="${OPENTCS_JAVAVM}" +else + # XXX Be a bit more clever to find out the name of the JVM runtime. + export JAVA="java" +fi + +# Start kernel control center +${JAVA} -enableassertions \ + -Dopentcs.base="${OPENTCS_BASE}" \ + -Dopentcs.home="${OPENTCS_HOME}" \ + -Dopentcs.configuration.provider=gestalt \ + -Dopentcs.configuration.reload.interval=10000 \ + -Djava.util.logging.config.file=${OPENTCS_CONFIGDIR}/logging.config \ + -XX:-OmitStackTraceInFastThrow \ + -classpath "${OPENTCS_CP}" \ + -splash:bin/splash-image.gif \ + org.opentcs.kernelcontrolcenter.RunKernelControlCenter diff --git a/opentcs-kernelcontrolcenter/src/guiceConfig/java/org/opentcs/kernelcontrolcenter/DefaultKernelControlCenterExtensionsModule.java b/opentcs-kernelcontrolcenter/src/guiceConfig/java/org/opentcs/kernelcontrolcenter/DefaultKernelControlCenterExtensionsModule.java new file mode 100644 index 0000000..ae64432 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/guiceConfig/java/org/opentcs/kernelcontrolcenter/DefaultKernelControlCenterExtensionsModule.java @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter; + +import com.google.inject.assistedinject.FactoryModuleBuilder; +import com.google.inject.multibindings.Multibinder; +import jakarta.inject.Singleton; +import org.opentcs.components.kernelcontrolcenter.ControlCenterPanel; +import org.opentcs.customizations.controlcenter.ControlCenterInjectionModule; +import org.opentcs.kernelcontrolcenter.peripherals.PeripheralsPanel; +import org.opentcs.kernelcontrolcenter.util.KernelControlCenterConfiguration; +import org.opentcs.kernelcontrolcenter.vehicles.DriverGUI; + +/** + * Configures the default extensions of the openTCS kernel control center application. + */ +public class DefaultKernelControlCenterExtensionsModule + extends + ControlCenterInjectionModule { + + /** + * Creates a new instance. + */ + public DefaultKernelControlCenterExtensionsModule() { + } + + @Override + protected void configure() { + configureControlCenterDependencies(); + } + + private void configureControlCenterDependencies() { + KernelControlCenterConfiguration configuration + = getConfigBindingProvider().get( + KernelControlCenterConfiguration.PREFIX, + KernelControlCenterConfiguration.class + ); + bind(KernelControlCenterConfiguration.class).toInstance(configuration); + + // Ensure these binders are initialized. + commAdapterPanelFactoryBinder(); + peripheralCommAdapterPanelFactoryBinder(); + + Multibinder modellingBinder = controlCenterPanelBinderModelling(); + // No extensions for modelling mode, yet. + + Multibinder operatingBinder = controlCenterPanelBinderOperating(); + operatingBinder.addBinding().to(DriverGUI.class); + if (configuration.enablePeripheralsPanel()) { + operatingBinder.addBinding().to(PeripheralsPanel.class); + } + + install(new FactoryModuleBuilder().build(ControlCenterInfoHandlerFactory.class)); + + bind(KernelControlCenter.class).in(Singleton.class); + } +} diff --git a/opentcs-kernelcontrolcenter/src/guiceConfig/java/org/opentcs/kernelcontrolcenter/DefaultKernelControlCenterInjectionModule.java b/opentcs-kernelcontrolcenter/src/guiceConfig/java/org/opentcs/kernelcontrolcenter/DefaultKernelControlCenterInjectionModule.java new file mode 100644 index 0000000..be8cd3a --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/guiceConfig/java/org/opentcs/kernelcontrolcenter/DefaultKernelControlCenterInjectionModule.java @@ -0,0 +1,172 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter; + +import com.google.inject.TypeLiteral; +import com.google.inject.assistedinject.FactoryModuleBuilder; +import jakarta.inject.Singleton; +import java.io.File; +import java.util.List; +import java.util.Locale; +import javax.swing.ToolTipManager; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.SslParameterSet; +import org.opentcs.access.rmi.KernelServicePortalBuilder; +import org.opentcs.access.rmi.factories.NullSocketFactoryProvider; +import org.opentcs.access.rmi.factories.SecureSocketFactoryProvider; +import org.opentcs.access.rmi.factories.SocketFactoryProvider; +import org.opentcs.common.DefaultPortalManager; +import org.opentcs.common.GuestUserCredentials; +import org.opentcs.common.KernelClientApplication; +import org.opentcs.common.PortalManager; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.customizations.ApplicationHome; +import org.opentcs.customizations.ConfigurableInjectionModule; +import org.opentcs.customizations.ServiceCallWrapper; +import org.opentcs.kernelcontrolcenter.exchange.DefaultServiceCallWrapper; +import org.opentcs.kernelcontrolcenter.exchange.SslConfiguration; +import org.opentcs.kernelcontrolcenter.util.KernelControlCenterConfiguration; +import org.opentcs.kernelcontrolcenter.vehicles.LocalVehicleEntryPool; +import org.opentcs.util.CallWrapper; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.event.EventSource; +import org.opentcs.util.event.SimpleEventBus; +import org.opentcs.util.gui.dialog.ConnectionParamSet; +import org.opentcs.virtualvehicle.AdapterPanelComponentsFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A Guice module for the openTCS kernel control center application. + */ +public class DefaultKernelControlCenterInjectionModule + extends + ConfigurableInjectionModule { + + /** + * This class' logger. + */ + private static final Logger LOG + = LoggerFactory.getLogger(DefaultKernelControlCenterInjectionModule.class); + + /** + * Creates a new instance. + */ + public DefaultKernelControlCenterInjectionModule() { + } + + @Override + protected void configure() { + File applicationHome = new File(System.getProperty("opentcs.home", ".")); + bind(File.class) + .annotatedWith(ApplicationHome.class) + .toInstance(applicationHome); + + bind(LocalVehicleEntryPool.class) + .in(Singleton.class); + + bind(KernelClientApplication.class) + .to(KernelControlCenterApplication.class) + .in(Singleton.class); + + install(new FactoryModuleBuilder().build(AdapterPanelComponentsFactory.class)); + + configureEventBus(); + configureKernelControlCenterDependencies(); + configureExchangeInjectionModules(); + } + + private void configureEventBus() { + EventBus newEventBus = new SimpleEventBus(); + bind(EventHandler.class) + .annotatedWith(ApplicationEventBus.class) + .toInstance(newEventBus); + bind(EventSource.class) + .annotatedWith(ApplicationEventBus.class) + .toInstance(newEventBus); + bind(EventBus.class) + .annotatedWith(ApplicationEventBus.class) + .toInstance(newEventBus); + } + + private void configureExchangeInjectionModules() { + bind(PortalManager.class) + .to(DefaultPortalManager.class) + .in(Singleton.class); + } + + private void configureKernelControlCenterDependencies() { + KernelControlCenterConfiguration configuration + = getConfigBindingProvider().get( + KernelControlCenterConfiguration.PREFIX, + KernelControlCenterConfiguration.class + ); + bind(KernelControlCenterConfiguration.class) + .toInstance(configuration); + configureKernelControlCenter(configuration); + configureSocketConnections(); + + bind(CallWrapper.class) + .annotatedWith(ServiceCallWrapper.class) + .to(DefaultServiceCallWrapper.class) + .in(Singleton.class); + + bind(new TypeLiteral>() { + }) + .toInstance(configuration.connectionBookmarks()); + } + + private void configureSocketConnections() { + SslConfiguration sslConfiguration = getConfigBindingProvider().get( + SslConfiguration.PREFIX, + SslConfiguration.class + ); + + //Create the data object for the ssl configuration + SslParameterSet sslParamSet = new SslParameterSet( + SslParameterSet.DEFAULT_KEYSTORE_TYPE, + null, + null, + new File(sslConfiguration.truststoreFile()), + sslConfiguration.truststorePassword() + ); + bind(SslParameterSet.class).toInstance(sslParamSet); + + SocketFactoryProvider socketFactoryProvider; + if (sslConfiguration.enable()) { + socketFactoryProvider = new SecureSocketFactoryProvider(sslParamSet); + } + else { + LOG.warn("SSL encryption disabled, connections will not be secured!"); + socketFactoryProvider = new NullSocketFactoryProvider(); + } + + //Bind socket provider to the kernel portal + bind(KernelServicePortal.class) + .toInstance( + new KernelServicePortalBuilder( + GuestUserCredentials.USER, + GuestUserCredentials.PASSWORD + ) + .setSocketFactoryProvider(socketFactoryProvider) + .build() + ); + } + + private void configureKernelControlCenter(KernelControlCenterConfiguration configuration) { + Locale.setDefault(Locale.forLanguageTag(configuration.locale())); + + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } + catch (ClassNotFoundException | InstantiationException | IllegalAccessException + | UnsupportedLookAndFeelException ex) { + LOG.warn("Could not set look-and-feel", ex); + } + // Show tooltips for 30 seconds (Default: 4 sec) + ToolTipManager.sharedInstance().setDismissDelay(30 * 1000); + } +} diff --git a/opentcs-kernelcontrolcenter/src/guiceConfig/java/org/opentcs/kernelcontrolcenter/LoopbackCommAdapterPanelsModule.java b/opentcs-kernelcontrolcenter/src/guiceConfig/java/org/opentcs/kernelcontrolcenter/LoopbackCommAdapterPanelsModule.java new file mode 100644 index 0000000..7467c7f --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/guiceConfig/java/org/opentcs/kernelcontrolcenter/LoopbackCommAdapterPanelsModule.java @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter; + +import org.opentcs.customizations.controlcenter.ControlCenterInjectionModule; +import org.opentcs.virtualvehicle.LoopbackCommAdapterPanelFactory; + +/** + * Registers the loopback adapter's panels. + */ +public class LoopbackCommAdapterPanelsModule + extends + ControlCenterInjectionModule { + + /** + * Creates a new instance. + */ + public LoopbackCommAdapterPanelsModule() { + } + + // tag::documentation_createCommAdapterPanelsModule[] + @Override + protected void configure() { + commAdapterPanelFactoryBinder().addBinding().to(LoopbackCommAdapterPanelFactory.class); + } + // end::documentation_createCommAdapterPanelsModule[] +} diff --git a/opentcs-kernelcontrolcenter/src/guiceConfig/java/org/opentcs/kernelcontrolcenter/RunKernelControlCenter.java b/opentcs-kernelcontrolcenter/src/guiceConfig/java/org/opentcs/kernelcontrolcenter/RunKernelControlCenter.java new file mode 100644 index 0000000..69c1830 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/guiceConfig/java/org/opentcs/kernelcontrolcenter/RunKernelControlCenter.java @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.util.Modules; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; +import org.opentcs.configuration.ConfigurationBindingProvider; +import org.opentcs.configuration.gestalt.GestaltConfigurationBindingProvider; +import org.opentcs.customizations.ConfigurableInjectionModule; +import org.opentcs.customizations.controlcenter.ControlCenterInjectionModule; +import org.opentcs.util.Environment; +import org.opentcs.util.logging.UncaughtExceptionLogger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The kernel control center process's default entry point. + */ +public class RunKernelControlCenter { + + /** + * This class' logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(RunKernelControlCenter.class); + + /** + * Prevents external instantiation. + */ + private RunKernelControlCenter() { + } + + /** + * The kernel control center client's main entry point. + * + * @param args the command line arguments + */ + public static void main(final String[] args) { + Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionLogger(false)); + + Environment.logSystemInfo(); + + Injector injector = Guice.createInjector(customConfigurationModule()); + injector.getInstance(KernelControlCenterApplication.class).initialize(); + } + + /** + * Builds and returns a Guice module containing the custom configuration for the kernel control + * center application, including additions and overrides by the user. + * + * @return The custom configuration module. + */ + private static Module customConfigurationModule() { + ConfigurationBindingProvider bindingProvider = configurationBindingProvider(); + ConfigurableInjectionModule kernelControlCenterInjectionModule + = new DefaultKernelControlCenterInjectionModule(); + kernelControlCenterInjectionModule.setConfigBindingProvider(bindingProvider); + return Modules.override(kernelControlCenterInjectionModule) + .with(findRegisteredModules(bindingProvider)); + } + + /** + * Finds and returns all Guice modules registered via ServiceLoader. + * + * @return The registered/found modules. + */ + private static List findRegisteredModules( + ConfigurationBindingProvider bindingProvider + ) { + List registeredModules = new ArrayList<>(); + for (ControlCenterInjectionModule module : ServiceLoader.load( + ControlCenterInjectionModule.class + )) { + LOG.info( + "Integrating injection module {} (source: {})", + module.getClass().getName(), + module.getClass().getProtectionDomain().getCodeSource() + ); + module.setConfigBindingProvider(bindingProvider); + registeredModules.add(module); + } + return registeredModules; + } + + private static ConfigurationBindingProvider configurationBindingProvider() { + String chosenProvider = System.getProperty("opentcs.configuration.provider", "gestalt"); + switch (chosenProvider) { + case "gestalt": + default: + LOG.info("Using gestalt as the configuration provider."); + return gestaltConfigurationBindingProvider(); + } + } + + private static ConfigurationBindingProvider gestaltConfigurationBindingProvider() { + return new GestaltConfigurationBindingProvider( + Paths.get( + System.getProperty("opentcs.base", "."), + "config", + "opentcs-kernelcontrolcenter-defaults-baseline.properties" + ) + .toAbsolutePath(), + Paths.get( + System.getProperty("opentcs.base", "."), + "config", + "opentcs-kernelcontrolcenter-defaults-custom.properties" + ) + .toAbsolutePath(), + Paths.get( + System.getProperty("opentcs.home", "."), + "config", + "opentcs-kernelcontrolcenter.properties" + ) + .toAbsolutePath() + ); + } + +} diff --git a/opentcs-kernelcontrolcenter/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.controlcenter.ControlCenterInjectionModule b/opentcs-kernelcontrolcenter/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.controlcenter.ControlCenterInjectionModule new file mode 100644 index 0000000..e4e8652 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.controlcenter.ControlCenterInjectionModule @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT + +org.opentcs.kernelcontrolcenter.DefaultKernelControlCenterExtensionsModule +org.opentcs.kernelcontrolcenter.LoopbackCommAdapterPanelsModule diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/AboutDialog.form b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/AboutDialog.form new file mode 100644 index 0000000..efd9b34 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/AboutDialog.form @@ -0,0 +1,266 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/AboutDialog.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/AboutDialog.java new file mode 100644 index 0000000..00196e6 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/AboutDialog.java @@ -0,0 +1,253 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter; + +import java.awt.Frame; +import javax.swing.JDialog; +import org.opentcs.util.Environment; + +/** + * An about dialog. + */ +public class AboutDialog + extends + JDialog { + + /** + * Creates new AboutDialog. + * + * @param parent The parent frame. + * @param modal Whether the dialog blocks user input to other top-level windows when shown. + */ + @SuppressWarnings("this-escape") + public AboutDialog(Frame parent, boolean modal) { + super(parent, modal); + initComponents(); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + logoPanel = new javax.swing.JPanel(); + opentcsLogoLbl = new javax.swing.JLabel(); + contactPanel = new javax.swing.JPanel(); + opentcsContactPanel = new javax.swing.JPanel(); + opentcsLbl = new javax.swing.JLabel(); + versionLbl = new javax.swing.JLabel(); + versionTxtLbl = new javax.swing.JLabel(); + customVersionLbl = new javax.swing.JLabel(); + customVersionTxtLbl = new javax.swing.JLabel(); + homepageLbl = new javax.swing.JLabel(); + homepageTxtLbl = new javax.swing.JLabel(); + emailLbl = new javax.swing.JLabel(); + emailTxtLbl = new javax.swing.JLabel(); + imlPanel = new javax.swing.JPanel(); + fraunhoferImlLbl = new javax.swing.JLabel(); + homepageImlLbl = new javax.swing.JLabel(); + homepageImlTxtLbl = new javax.swing.JLabel(); + fillingLbl = new javax.swing.JLabel(); + closeButton = new javax.swing.JButton(); + fillingLbl2 = new javax.swing.JLabel(); + + setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/kernelcontrolcenter/Bundle"); // NOI18N + setTitle(bundle.getString("aboutDialog.title")); // NOI18N + setResizable(false); + getContentPane().setLayout(new java.awt.GridBagLayout()); + + logoPanel.setBackground(new java.awt.Color(255, 255, 255)); + logoPanel.setLayout(new java.awt.BorderLayout()); + + opentcsLogoLbl.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + opentcsLogoLbl.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/opentcs/kernelcontrolcenter/res/logos/opentcs.gif"))); // NOI18N + logoPanel.add(opentcsLogoLbl, java.awt.BorderLayout.CENTER); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.ipadx = 6; + gridBagConstraints.ipady = 6; + getContentPane().add(logoPanel, gridBagConstraints); + + contactPanel.setLayout(new javax.swing.BoxLayout(contactPanel, javax.swing.BoxLayout.Y_AXIS)); + + opentcsContactPanel.setLayout(new java.awt.GridBagLayout()); + + opentcsLbl.setFont(opentcsLbl.getFont().deriveFont(opentcsLbl.getFont().getStyle() | java.awt.Font.BOLD)); + opentcsLbl.setText("open Transportation Control System"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(6, 0, 0, 0); + opentcsContactPanel.add(opentcsLbl, gridBagConstraints); + + versionLbl.setText(bundle.getString("aboutDialog.label_baselineVersion.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + opentcsContactPanel.add(versionLbl, gridBagConstraints); + + versionTxtLbl.setText(Environment.getBaselineVersion()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 6, 0, 0); + opentcsContactPanel.add(versionTxtLbl, gridBagConstraints); + + customVersionLbl.setText(bundle.getString("aboutDialog.label_customVersion.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + opentcsContactPanel.add(customVersionLbl, gridBagConstraints); + + customVersionTxtLbl.setText(Environment.getCustomizationName() + " " + Environment.getCustomizationVersion()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 6, 0, 0); + opentcsContactPanel.add(customVersionTxtLbl, gridBagConstraints); + + homepageLbl.setText(bundle.getString("aboutDialog.label_homepage.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + opentcsContactPanel.add(homepageLbl, gridBagConstraints); + + homepageTxtLbl.setText("https://www.opentcs.org/"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 6, 0, 0); + opentcsContactPanel.add(homepageTxtLbl, gridBagConstraints); + + emailLbl.setText(bundle.getString("aboutDialog.label_email.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + opentcsContactPanel.add(emailLbl, gridBagConstraints); + + emailTxtLbl.setText("business-info@opentcs.org"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 6, 0, 0); + opentcsContactPanel.add(emailTxtLbl, gridBagConstraints); + + contactPanel.add(opentcsContactPanel); + + imlPanel.setLayout(new java.awt.GridBagLayout()); + + fraunhoferImlLbl.setFont(fraunhoferImlLbl.getFont().deriveFont(fraunhoferImlLbl.getFont().getStyle() | java.awt.Font.BOLD)); + fraunhoferImlLbl.setText(bundle.getString("aboutDialog.label_fraunhoferIml.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(6, 0, 0, 0); + imlPanel.add(fraunhoferImlLbl, gridBagConstraints); + + homepageImlLbl.setText(bundle.getString("aboutDialog.label_homepage.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + imlPanel.add(homepageImlLbl, gridBagConstraints); + + homepageImlTxtLbl.setText("http://www.iml.fraunhofer.de/"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 6, 0, 0); + imlPanel.add(homepageImlTxtLbl, gridBagConstraints); + + contactPanel.add(imlPanel); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + getContentPane().add(contactPanel, gridBagConstraints); + + fillingLbl.setText(" "); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weighty = 1.0; + getContentPane().add(fillingLbl, gridBagConstraints); + + closeButton.setText(bundle.getString("aboutDialog.button_close.text")); // NOI18N + closeButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + closeButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + getContentPane().add(closeButton, gridBagConstraints); + + fillingLbl2.setText(" "); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weighty = 1.0; + getContentPane().add(fillingLbl2, gridBagConstraints); + + pack(); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void closeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_closeButtonActionPerformed + this.setVisible(false); + }//GEN-LAST:event_closeButtonActionPerformed + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton closeButton; + private javax.swing.JPanel contactPanel; + private javax.swing.JLabel customVersionLbl; + private javax.swing.JLabel customVersionTxtLbl; + private javax.swing.JLabel emailLbl; + private javax.swing.JLabel emailTxtLbl; + private javax.swing.JLabel fillingLbl; + private javax.swing.JLabel fillingLbl2; + private javax.swing.JLabel fraunhoferImlLbl; + private javax.swing.JLabel homepageImlLbl; + private javax.swing.JLabel homepageImlTxtLbl; + private javax.swing.JLabel homepageLbl; + private javax.swing.JLabel homepageTxtLbl; + private javax.swing.JPanel imlPanel; + private javax.swing.JPanel logoPanel; + private javax.swing.JPanel opentcsContactPanel; + private javax.swing.JLabel opentcsLbl; + private javax.swing.JLabel opentcsLogoLbl; + private javax.swing.JLabel versionLbl; + private javax.swing.JLabel versionTxtLbl; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/ControlCenterInfoHandler.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/ControlCenterInfoHandler.java new file mode 100644 index 0000000..f4d6466 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/ControlCenterInfoHandler.java @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.Locale; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; +import javax.swing.text.BadLocationException; +import javax.swing.text.DefaultCaret; +import org.opentcs.access.NotificationPublicationEvent; +import org.opentcs.common.ClientConnectionMode; +import org.opentcs.common.PortalManager; +import org.opentcs.data.notification.UserNotification; +import org.opentcs.kernelcontrolcenter.util.KernelControlCenterConfiguration; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A logging handler that writes all INFO-logs to KernelControlCenter's logging text area. + */ +public class ControlCenterInfoHandler + implements + EventHandler { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ControlCenterInfoHandler.class); + /** + * Formats time stamps. + */ + private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter + .ofLocalizedDateTime(FormatStyle.SHORT) + .withLocale(Locale.getDefault()) + .withZone(ZoneId.systemDefault()); + /** + * This class's configuration. + */ + private final KernelControlCenterConfiguration configuration; + /** + * The text area we're writing in. + */ + private final JTextArea textArea; + /** + * A flag whether the text area scrolls. + */ + private boolean autoScroll; + + /** + * Creates a new ControlCenterInfoHandler. + * + * @param textArea The textArea we are writing to. + * @param configuration This class' configuration. + */ + @Inject + public ControlCenterInfoHandler( + @Assisted + JTextArea textArea, + KernelControlCenterConfiguration configuration + ) { + this.textArea = requireNonNull(textArea, "textArea"); + this.configuration = requireNonNull(configuration, "configuration"); + + autoScroll = true; + } + + @Override + public void onEvent(Object event) { + if (event instanceof PortalManager.ConnectionState) { + PortalManager.ConnectionState connectionState = (PortalManager.ConnectionState) event; + SwingUtilities.invokeLater(() -> { + publish( + new UserNotification( + "Kernel connection state: " + connectionState.name(), + UserNotification.Level.INFORMATIONAL + ) + ); + }); + } + else if (event instanceof ClientConnectionMode) { + ClientConnectionMode applicationState = (ClientConnectionMode) event; + SwingUtilities.invokeLater(() -> { + publish( + new UserNotification( + "Application state: " + applicationState.name(), + UserNotification.Level.INFORMATIONAL + ) + ); + }); + } + else if (event instanceof NotificationPublicationEvent) { + SwingUtilities.invokeLater( + () -> publish(((NotificationPublicationEvent) event).getNotification()) + ); + } + } + + /** + * Defines if the textArea autoscrolls. + * + * @param autoScroll true if it should, false otherwise + */ + public void setAutoScroll(boolean autoScroll) { + this.autoScroll = autoScroll; + } + + /** + * Displays the notification. + * + * @param notification The notification + */ + private void publish(UserNotification notification) { + DefaultCaret caret = (DefaultCaret) textArea.getCaret(); + if (autoScroll) { + caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE); + textArea.setCaretPosition(textArea.getDocument().getLength()); + } + else { + caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE); + } + + textArea.append(format(notification)); + textArea.append("\n"); + checkLength(); + } + + private String format(UserNotification notification) { + return DATE_FORMAT.format(notification.getTimestamp()) + + " " + notification.getLevel() + + ": [" + notification.getSource() + "] " + + notification.getText(); + } + + /** + * Checks if the length of the document in our textArea is greater than our {@code maxDocLength} + * and cuts it if neccessary. + */ + private synchronized void checkLength() { + SwingUtilities.invokeLater(() -> { + int docLength = textArea.getDocument().getLength(); + + if (docLength > configuration.loggingAreaCapacity()) { + try { + textArea.getDocument().remove(0, docLength - configuration.loggingAreaCapacity()); + } + catch (BadLocationException e) { + LOG.warn("Caught exception", e); + } + } + }); + } +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/ControlCenterInfoHandlerFactory.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/ControlCenterInfoHandlerFactory.java new file mode 100644 index 0000000..7d33439 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/ControlCenterInfoHandlerFactory.java @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter; + +import javax.swing.JTextArea; + +/** + * A factory providing {@link ControlCenterInfoHandler} instances. + */ +public interface ControlCenterInfoHandlerFactory { + + /** + * Creates a new ControlCenterInfoHandler. + * + * @param textArea The text area. + * @return A new ControlCenterInfoHandler. + */ + ControlCenterInfoHandler createHandler(JTextArea textArea); +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/I18nKernelControlCenter.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/I18nKernelControlCenter.java new file mode 100644 index 0000000..a635340 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/I18nKernelControlCenter.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter; + +/** + * Defines constants regarding internationalization. + */ +public interface I18nKernelControlCenter { + + /** + * The path to the project's resource bundle. + */ + String BUNDLE_PATH = "i18n/org/opentcs/kernelcontrolcenter/Bundle"; +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/KernelControlCenter.form b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/KernelControlCenter.form new file mode 100644 index 0000000..128c722 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/KernelControlCenter.form @@ -0,0 +1,174 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/KernelControlCenter.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/KernelControlCenter.java new file mode 100644 index 0000000..dbc6151 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/KernelControlCenter.java @@ -0,0 +1,531 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.common.PortalManager.ConnectionState.CONNECTED; +import static org.opentcs.common.PortalManager.ConnectionState.DISCONNECTED; +import static org.opentcs.kernelcontrolcenter.I18nKernelControlCenter.BUNDLE_PATH; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import java.awt.EventQueue; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.ResourceBundle; +import java.util.Set; +import javax.swing.JFrame; +import javax.swing.SwingUtilities; +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.KernelStateTransitionEvent; +import org.opentcs.access.ModelTransitionEvent; +import org.opentcs.common.ClientConnectionMode; +import org.opentcs.common.KernelClientApplication; +import org.opentcs.common.PortalManager; +import org.opentcs.components.Lifecycle; +import org.opentcs.components.kernelcontrolcenter.ControlCenterPanel; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.customizations.ServiceCallWrapper; +import org.opentcs.customizations.controlcenter.ActiveInModellingMode; +import org.opentcs.customizations.controlcenter.ActiveInOperatingMode; +import org.opentcs.util.CallWrapper; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.event.EventSource; +import org.opentcs.util.gui.Icons; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A GUI frontend for basic control over the kernel. + */ +public class KernelControlCenter + extends + JFrame + implements + Lifecycle, + EventHandler { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(KernelControlCenter.class); + /** + * This class's resource bundle. + */ + private static final ResourceBundle BUNDLE = ResourceBundle.getBundle(BUNDLE_PATH); + /** + * The factory providing a ControlCenterInfoHandler. + */ + private final ControlCenterInfoHandlerFactory controlCenterInfoHandlerFactoy; + /** + * Providers for panels shown in modelling mode. + */ + private final Collection> panelProvidersModelling; + /** + * Providers for panels shown in operating mode. + */ + private final Collection> panelProvidersOperating; + /** + * An about dialog. + */ + private final AboutDialog aboutDialog; + /** + * Panels currently active/shown. + */ + private final Set activePanels = Collections.synchronizedSet(new HashSet<>()); + /** + * The application running this panel. + */ + private final KernelClientApplication application; + /** + * Where this instance registers for application events. + */ + private final EventSource eventSource; + /** + * The service portal to use for kernel interaction. + */ + private final KernelServicePortal servicePortal; + /** + * The portal manager. + */ + private final PortalManager portalManager; + /** + * The call wrapper to use for service calls. + */ + private final CallWrapper callWrapper; + /** + * Indicates whether this component is initialized. + */ + private boolean initialized; + /** + * The ControlCenterInfoHandler. + */ + private ControlCenterInfoHandler infoHandler; + /** + * The current Model Name. + */ + private String currentModel = ""; + + /** + * Creates new form KernelControlCenter. + * + * @param application The application running this panel. + * @param servicePortal The service portal to use for kernel interaction. + * @param callWrapper The call wrapper to use for service calls. + * @param portalManager The portal manager. + * @param eventSource Where this instance registers for application events. + * @param controlCenterInfoHandlerFactory The factory providing a ControlCenterInfoHandler. + * @param panelProvidersModelling Providers for panels in modelling mode. + * @param panelProvidersOperating Providers for panels in operating mode. + */ + @Inject + @SuppressWarnings("this-escape") + public KernelControlCenter( + @Nonnull + KernelClientApplication application, + @Nonnull + KernelServicePortal servicePortal, + @Nonnull + @ServiceCallWrapper + CallWrapper callWrapper, + @Nonnull + PortalManager portalManager, + @Nonnull + @ApplicationEventBus + EventSource eventSource, + @Nonnull + ControlCenterInfoHandlerFactory controlCenterInfoHandlerFactory, + @Nonnull + @ActiveInModellingMode + Collection> panelProvidersModelling, + @Nonnull + @ActiveInOperatingMode + Collection> panelProvidersOperating + ) { + this.application = requireNonNull(application, "application"); + this.servicePortal = requireNonNull(servicePortal, "servicePortal"); + this.callWrapper = requireNonNull(callWrapper, "callWrapper"); + this.portalManager = requireNonNull(portalManager, "portalManager"); + this.eventSource = requireNonNull(eventSource, "eventSource"); + this.controlCenterInfoHandlerFactoy = requireNonNull( + controlCenterInfoHandlerFactory, + "controlCenterInfoHandlerFactory" + ); + this.panelProvidersModelling = requireNonNull( + panelProvidersModelling, + "panelProvidersModelling" + ); + this.panelProvidersOperating = requireNonNull( + panelProvidersOperating, + "panelProvidersOperating" + ); + + initComponents(); + setIconImages(Icons.getOpenTCSIcons()); + aboutDialog = new AboutDialog(this, false); + aboutDialog.setAlwaysOnTop(true); + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void initialize() { + if (initialized) { + LOG.debug("Already initialized."); + return; + } + + registerControlCenterInfoHandler(); + eventSource.subscribe(this); + + enteringKernelState(Kernel.State.MODELLING); + + try { + EventQueue.invokeAndWait(() -> setVisible(true)); + } + catch (InterruptedException | InvocationTargetException exc) { + throw new IllegalStateException("Unexpected exception initializing", exc); + } + + initialized = true; + } + + @Override + public void terminate() { + if (!initialized) { + LOG.debug("Not initialized"); + return; + } + + removePanels(activePanels); + + eventSource.unsubscribe(this); + eventSource.unsubscribe(infoHandler); + + // Hide the window. + setVisible(false); + dispose(); + + initialized = false; + } + + private void onKernelConnect() { + try { + Kernel.State kernelState = callWrapper.call(() -> servicePortal.getState()); + enteringKernelState(kernelState); + } + catch (Exception ex) { + LOG.warn("Error getting the kernel state", ex); + } + } + + private void onKernelDisconnect() { + leavingKernelState(Kernel.State.OPERATING); + enteringKernelState(Kernel.State.MODELLING); + } + + @Override + public void onEvent(Object event) { + if (event instanceof ClientConnectionMode) { + ClientConnectionMode applicationState = (ClientConnectionMode) event; + switch (applicationState) { + case ONLINE: + onKernelConnect(); + break; + case OFFLINE: + onKernelDisconnect(); + break; + default: + LOG.debug("Unhandled connection state: {}", applicationState.name()); + } + } + else if (event instanceof PortalManager.ConnectionState) { + PortalManager.ConnectionState connectionState = (PortalManager.ConnectionState) event; + switch (connectionState) { + case CONNECTED: + updateWindowTitle(); + break; + case DISCONNECTED: + updateWindowTitle(); + break; + default: + } + + menuButtonConnect.setEnabled(!portalManager.isConnected()); + menuButtonDisconnect.setEnabled(portalManager.isConnected()); + } + else if (event instanceof KernelStateTransitionEvent) { + KernelStateTransitionEvent stateEvent = (KernelStateTransitionEvent) event; + if (!stateEvent.isTransitionFinished()) { + leavingKernelState(stateEvent.getLeftState()); + } + else { + enteringKernelState(stateEvent.getEnteredState()); + } + } + else if (event instanceof ModelTransitionEvent) { + ModelTransitionEvent modelEvent = (ModelTransitionEvent) event; + updateModelName(modelEvent.getNewModelName()); + } + } + + /** + * Perfoms some tasks when a state is being leaved. + * + * @param oldState The state we're leaving + */ + private void leavingKernelState(Kernel.State oldState) { + requireNonNull(oldState, "oldState"); + + removePanels(activePanels); + activePanels.clear(); + } + + /** + * Notifies this control center that the kernel has entered a different state. + * + * @param newState + */ + private void enteringKernelState(Kernel.State newState) { + requireNonNull(newState, "newState"); + + switch (newState) { + case OPERATING: + addPanels(panelProvidersOperating); + break; + case MODELLING: + addPanels(panelProvidersModelling); + break; + default: + // Do nada. + } + // Updating the window title + updateWindowTitle(); + } + + private void addPanels(Collection> providers) { + for (Provider provider : providers) { + SwingUtilities.invokeLater(() -> addPanel(provider.get())); + } + } + + private void addPanel(ControlCenterPanel panel) { + panel.initialize(); + activePanels.add(panel); + tabbedPaneMain.add(panel.getTitle(), panel); + } + + private void removePanels(Collection panels) { + List panelsCopy = new ArrayList<>(panels); + SwingUtilities.invokeLater(() -> { + for (ControlCenterPanel panel : panelsCopy) { + tabbedPaneMain.remove(panel); + panel.terminate(); + } + }); + } + + /** + * Updates the model name to the current one. + * + * @param newModelName The new/updated model name. + */ + private void updateModelName(String newModelName) { + this.currentModel = newModelName; + updateWindowTitle(); + } + + /** + * Adds the ControlCenterInfoHandler to the root logger. + */ + private void registerControlCenterInfoHandler() { + infoHandler = controlCenterInfoHandlerFactoy.createHandler(loggingTextArea); + eventSource.subscribe(infoHandler); + } + + private void updateWindowTitle() { + String titleBase = BUNDLE.getString("kernelControlCenter.title"); + String loadedModel = currentModel.equals("") ? "" : " - " + "\"" + currentModel + "\""; + String connectedTo = " - " + BUNDLE.getString("kernelControlCenter.title.connectedTo") + + portalManager.getDescription() + + " (" + portalManager.getHost() + + ":" + + portalManager.getPort() + ")"; + + setTitle(titleBase + loadedModel + connectedTo); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Generated code starts here. + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + tabbedPaneMain = new javax.swing.JTabbedPane(); + loggingPanel = new javax.swing.JPanel(); + loggingScrollPane = new javax.swing.JScrollPane(); + loggingTextArea = new javax.swing.JTextArea(); + loggingPropertyPanel = new javax.swing.JPanel(); + autoScrollCheckBox = new javax.swing.JCheckBox(); + menuBarMain = new javax.swing.JMenuBar(); + menuKernel = new javax.swing.JMenu(); + menuButtonConnect = new javax.swing.JMenuItem(); + menuButtonDisconnect = new javax.swing.JMenuItem(); + jSeparator1 = new javax.swing.JPopupMenu.Separator(); + menuButtonExit = new javax.swing.JMenuItem(); + menuHelp = new javax.swing.JMenu(); + menuAbout = new javax.swing.JMenuItem(); + + setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/kernelcontrolcenter/Bundle"); // NOI18N + setTitle(bundle.getString("kernelControlCenter.title")); // NOI18N + setMinimumSize(new java.awt.Dimension(1200, 750)); + addWindowListener(new java.awt.event.WindowAdapter() { + public void windowClosing(java.awt.event.WindowEvent evt) { + formWindowClosing(evt); + } + }); + + loggingPanel.setLayout(new java.awt.BorderLayout()); + + loggingTextArea.setEditable(false); + loggingScrollPane.setViewportView(loggingTextArea); + + loggingPanel.add(loggingScrollPane, java.awt.BorderLayout.CENTER); + + loggingPropertyPanel.setLayout(new java.awt.GridBagLayout()); + + autoScrollCheckBox.setSelected(true); + autoScrollCheckBox.setText(bundle.getString("kernelControlCenter.checkBox_autoScroll.text")); // NOI18N + autoScrollCheckBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + autoScrollCheckBoxActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 1.0; + loggingPropertyPanel.add(autoScrollCheckBox, gridBagConstraints); + + loggingPanel.add(loggingPropertyPanel, java.awt.BorderLayout.PAGE_START); + + tabbedPaneMain.addTab(bundle.getString("kernelControlCenter.tab_logging.title"), loggingPanel); // NOI18N + + getContentPane().add(tabbedPaneMain, java.awt.BorderLayout.CENTER); + + menuKernel.setText("KernelControlCenter"); + + menuButtonConnect.setText(bundle.getString("kernelControlCenter.menu_kernel.menuItem_connect.text")); // NOI18N + menuButtonConnect.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + menuButtonConnectActionPerformed(evt); + } + }); + menuKernel.add(menuButtonConnect); + + menuButtonDisconnect.setText(bundle.getString("kernelControlCenter.menu_kernel.menuItem_disconnect.text")); // NOI18N + menuButtonDisconnect.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + menuButtonDisconnectActionPerformed(evt); + } + }); + menuKernel.add(menuButtonDisconnect); + menuKernel.add(jSeparator1); + + menuButtonExit.setText(bundle.getString("kernelControlCenter.menu_kernel.menuItem_exit.text")); // NOI18N + menuButtonExit.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + menuButtonExitActionPerformed(evt); + } + }); + menuKernel.add(menuButtonExit); + + menuBarMain.add(menuKernel); + + menuHelp.setText(bundle.getString("kernelControlCenter.menu_help.text")); // NOI18N + + menuAbout.setText(bundle.getString("kernelControlCenter.menu_about.text")); // NOI18N + menuAbout.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + menuAboutActionPerformed(evt); + } + }); + menuHelp.add(menuAbout); + + menuBarMain.add(menuHelp); + + setJMenuBar(menuBarMain); + + setSize(new java.awt.Dimension(1208, 782)); + setLocationRelativeTo(null); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void menuButtonExitActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_menuButtonExitActionPerformed + application.terminate(); + }//GEN-LAST:event_menuButtonExitActionPerformed + + private void autoScrollCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_autoScrollCheckBoxActionPerformed + if (autoScrollCheckBox.isSelected()) { + infoHandler.setAutoScroll(true); + } + else { + infoHandler.setAutoScroll(false); + } + }//GEN-LAST:event_autoScrollCheckBoxActionPerformed + + private void menuAboutActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_menuAboutActionPerformed + aboutDialog.setLocationRelativeTo(null); + aboutDialog.setVisible(true); + }//GEN-LAST:event_menuAboutActionPerformed + + private void formWindowClosing(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_formWindowClosing + application.terminate(); + }//GEN-LAST:event_formWindowClosing + + private void menuButtonConnectActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_menuButtonConnectActionPerformed + application.online(false); + }//GEN-LAST:event_menuButtonConnectActionPerformed + + private void menuButtonDisconnectActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_menuButtonDisconnectActionPerformed + application.offline(); + }//GEN-LAST:event_menuButtonDisconnectActionPerformed + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JCheckBox autoScrollCheckBox; + private javax.swing.JPopupMenu.Separator jSeparator1; + private javax.swing.JPanel loggingPanel; + private javax.swing.JPanel loggingPropertyPanel; + private javax.swing.JScrollPane loggingScrollPane; + private javax.swing.JTextArea loggingTextArea; + private javax.swing.JMenuItem menuAbout; + private javax.swing.JMenuBar menuBarMain; + private javax.swing.JMenuItem menuButtonConnect; + private javax.swing.JMenuItem menuButtonDisconnect; + private javax.swing.JMenuItem menuButtonExit; + private javax.swing.JMenu menuHelp; + private javax.swing.JMenu menuKernel; + private javax.swing.JTabbedPane tabbedPaneMain; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/KernelControlCenterApplication.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/KernelControlCenterApplication.java new file mode 100644 index 0000000..195cd0c --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/KernelControlCenterApplication.java @@ -0,0 +1,186 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.common.ClientConnectionMode; +import org.opentcs.common.KernelClientApplication; +import org.opentcs.common.PortalManager; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.kernelcontrolcenter.exchange.KernelEventFetcher; +import org.opentcs.kernelcontrolcenter.util.KernelControlCenterConfiguration; +import org.opentcs.util.event.EventBus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The kernel control center application's entry point. + */ +public class KernelControlCenterApplication + implements + KernelClientApplication { + + /** + * This class' logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(KernelControlCenterApplication.class); + /** + * The instance fetching for kernel events. + */ + private final KernelEventFetcher eventFetcher; + /** + * The actual kernel control center. + */ + private final KernelControlCenter kernelControlCenter; + /** + * The service portal manager. + */ + private final PortalManager portalManager; + /** + * The application's event bus. + */ + private final EventBus eventBus; + /** + * The application's configuration. + */ + private final KernelControlCenterConfiguration configuration; + /** + * Whether this application is online or not. + */ + private ConnectionState connectionState = ConnectionState.OFFLINE; + /** + * Whether this application is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param eventFetcher The instance fetching for kernel events. + * @param kernelControlCenter The actual kernel control center. + * @param portalManager The service portal manager. + * @param eventBus The application's event bus. + * @param configuration The application's configuration. + */ + @Inject + public KernelControlCenterApplication( + KernelEventFetcher eventFetcher, + KernelControlCenter kernelControlCenter, + PortalManager portalManager, + @ApplicationEventBus + EventBus eventBus, + KernelControlCenterConfiguration configuration + ) { + this.eventFetcher = requireNonNull(eventFetcher, "eventHub"); + this.kernelControlCenter = requireNonNull(kernelControlCenter, "kernelControlCenter"); + this.portalManager = requireNonNull(portalManager, "portalManager"); + this.eventBus = requireNonNull(eventBus, "eventBus"); + this.configuration = requireNonNull(configuration, "configuration"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + kernelControlCenter.initialize(); + eventFetcher.initialize(); + + // Trigger initial connect + online(configuration.connectAutomaticallyOnStartup()); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + // If we want to terminate but are still online, go offline first + offline(); + + eventFetcher.terminate(); + kernelControlCenter.terminate(); + + initialized = false; + } + + @Override + public void online(boolean autoConnect) { + if (isOnline() || isConnecting()) { + return; + } + + connectionState = ConnectionState.CONNECTING; + if (portalManager.connect(toConnectionMode(autoConnect))) { + LOG.info("Switching application state to online..."); + connectionState = ConnectionState.ONLINE; + eventBus.onEvent(ClientConnectionMode.ONLINE); + } + else { + connectionState = ConnectionState.OFFLINE; + } + } + + @Override + public void offline() { + if (!isOnline() && !isConnecting()) { + return; + } + + portalManager.disconnect(); + + LOG.info("Switching application state to offline..."); + connectionState = ConnectionState.OFFLINE; + eventBus.onEvent(ClientConnectionMode.OFFLINE); + } + + @Override + public boolean isOnline() { + return connectionState == ConnectionState.ONLINE; + } + + /** + * Returns true if, and only if the control center is trying to establish a + * connection to the kernel. + * + * @return true if, and only if the control center is trying to establish a + * connection to the kernel + */ + public boolean isConnecting() { + return connectionState == ConnectionState.CONNECTING; + } + + private PortalManager.ConnectionMode toConnectionMode(boolean autoConnect) { + return autoConnect ? PortalManager.ConnectionMode.AUTO : PortalManager.ConnectionMode.MANUAL; + } + + /** + * An enum to display the different states of the control center application connection to the + * kernel. + */ + private enum ConnectionState { + /** + * The control center is not connected to the kernel and is not trying to. + */ + OFFLINE, + /** + * The control center is currently trying to connect to the kernel. + */ + CONNECTING, + /** + * The control center is connected to the kernel. + */ + ONLINE + } +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/exchange/DefaultServiceCallWrapper.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/exchange/DefaultServiceCallWrapper.java new file mode 100644 index 0000000..de8d517 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/exchange/DefaultServiceCallWrapper.java @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.exchange; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.kernelcontrolcenter.I18nKernelControlCenter.BUNDLE_PATH; + +import jakarta.inject.Inject; +import java.util.ResourceBundle; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import javax.swing.JOptionPane; +import org.opentcs.common.KernelClientApplication; +import org.opentcs.common.PortalManager; +import org.opentcs.components.kernel.services.ServiceUnavailableException; +import org.opentcs.util.CallWrapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The default {@link CallWrapper} implementation used for calling kernel service methods. + *

+ * If a service method is called using this implementation and the corresponding service is no + * longer available, this implementation will ask to retry the service method call: + *

+ *
    + *
  • + * If 'Retry - Yes' is selected, this implementation will handle reestablishment of the kernel + * connection and (upon successful reestablishment) try to call the service method again. + *
  • + *
  • + * If 'Retry - No' is selected, this implementation will throw the exception thrown by the service + * mehtod call itself. + *
  • + *
  • + * If 'Cancel' is selected, this implementation will throw the exception thrown by the service + * mehtod call itself and additionally will notify the application it's no longer online, i.e. + * connected to the kernel. + *
  • + *
+ */ +public class DefaultServiceCallWrapper + implements + CallWrapper { + + /** + * This class' logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(DefaultServiceCallWrapper.class); + /** + * This class' resource bundle. + */ + private static final ResourceBundle BUNDLE = ResourceBundle.getBundle(BUNDLE_PATH); + /** + * The application using this utility. + */ + private final KernelClientApplication application; + /** + * The portal manager taking care of the portal connection. + */ + private final PortalManager portalManager; + + /** + * Creates a new instance. + * + * @param application The application. + * @param portalManager The portal manager. + */ + @Inject + public DefaultServiceCallWrapper( + KernelClientApplication application, + PortalManager portalManager + ) { + this.application = requireNonNull(application, "application"); + this.portalManager = requireNonNull(portalManager, "portalManager"); + } + + @Override + public R call(Callable callable) + throws Exception { + boolean retry = true; + Exception failureReason = null; + + while (retry) { + try { + return callable.call(); + } + catch (Exception ex) { + LOG.warn("Failed to call remote service method: {}", callable, ex); + failureReason = ex; + + if (ex instanceof ServiceUnavailableException) { + portalManager.disconnect(); + retry = showRetryDialog(); + } + else { + retry = false; + } + } + } + + // At this point the method call failed and we don't want to try it again anymore. + // Therefore throw the exception we caught last. + throw failureReason; + } + + @Override + public void call(Runnable runnable) + throws Exception { + Callable callable = Executors.callable(runnable); + call(callable); + } + + private boolean showRetryDialog() { + int dialogSelection + = JOptionPane.showConfirmDialog( + null, + BUNDLE.getString("defaultServiceCallWrapper.optionPane_retryConfirmation.message"), + BUNDLE.getString("defaultServiceCallWrapper.optionPane_retryConfirmation.title"), + JOptionPane.YES_NO_CANCEL_OPTION + ); + + switch (dialogSelection) { + case JOptionPane.YES_OPTION: + // Only retry if we connected successfully + return portalManager.connect(PortalManager.ConnectionMode.RECONNECT); + case JOptionPane.NO_OPTION: + return false; + case JOptionPane.CANCEL_OPTION: + application.offline(); + return false; + case JOptionPane.CLOSED_OPTION: + return false; + default: + return false; + } + } +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/exchange/KernelEventFetcher.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/exchange/KernelEventFetcher.java new file mode 100644 index 0000000..b811064 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/exchange/KernelEventFetcher.java @@ -0,0 +1,214 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.exchange; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkInRange; + +import jakarta.inject.Inject; +import java.util.List; +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.KernelStateTransitionEvent; +import org.opentcs.common.ClientConnectionMode; +import org.opentcs.common.KernelClientApplication; +import org.opentcs.common.PortalManager; +import org.opentcs.components.Lifecycle; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.customizations.ServiceCallWrapper; +import org.opentcs.util.CallWrapper; +import org.opentcs.util.CyclicTask; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Controls a task that periodically fetches for kernel events. + */ +public class KernelEventFetcher + implements + EventHandler, + Lifecycle { + + /** + * This class' logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(KernelEventFetcher.class); + /** + * The time to wait between event fetches with the service portal (in ms). + */ + private final long eventFetchInterval = 100; + /** + * The time to wait for events to arrive when fetching (in ms). + */ + private final long eventFetchTimeout = 1000; + /** + * The application using this event hub. + */ + private final KernelClientApplication application; + /** + * The service portal to fetch events from. + */ + private final KernelServicePortal servicePortal; + /** + * The call wrapper to use. + */ + private final CallWrapper callWrapper; + /** + * The application's event bus. + */ + private final EventBus eventBus; + /** + * The task fetching the service portal for new events. + */ + private EventFetcherTask eventFetcherTask; + /** + * Whether this event hub is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param application The application using this event hub. + * @param servicePortal The service portal to fetch events from. + * @param callWrapper The call wrapper to use for fetching events. + * @param eventBus The application's event bus. + */ + @Inject + public KernelEventFetcher( + KernelClientApplication application, + KernelServicePortal servicePortal, + @ServiceCallWrapper + CallWrapper callWrapper, + @ApplicationEventBus + EventBus eventBus + ) { + this.application = requireNonNull(application, "application"); + this.servicePortal = requireNonNull(servicePortal, "servicePortal"); + this.callWrapper = requireNonNull(callWrapper, "callWrapper"); + this.eventBus = requireNonNull(eventBus, "eventBus"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + eventBus.subscribe(this); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + eventBus.unsubscribe(this); + + initialized = false; + } + + @Override + public void onEvent(Object event) { + if (event == PortalManager.ConnectionState.DISCONNECTING) { + onKernelDisconnect(); + } + else if (event instanceof ClientConnectionMode) { + ClientConnectionMode applicationState = (ClientConnectionMode) event; + switch (applicationState) { + case ONLINE: + onKernelConnect(); + break; + case OFFLINE: + onKernelDisconnect(); + break; + default: + LOG.debug("Unhandled portal connection state: {}", applicationState.name()); + } + } + } + + private void onKernelConnect() { + if (eventFetcherTask != null) { + return; + } + + eventFetcherTask = new EventFetcherTask(eventFetchInterval, eventFetchTimeout); + Thread eventFetcherThread = new Thread(eventFetcherTask, getClass().getName() + "-fetcherTask"); + eventFetcherThread.start(); + } + + private void onKernelDisconnect() { + if (eventFetcherTask == null) { + return; + } + + // Stop polling for events. + eventFetcherTask.terminateAndWait(); + eventFetcherTask = null; + } + + /** + * A task fetching the service portal for events in regular intervals. + */ + private class EventFetcherTask + extends + CyclicTask { + + /** + * The poll timeout. + */ + private final long timeout; + + /** + * Creates a new instance. + * + * @param interval The time to wait between polls in ms. + * @param timeout The timeout in ms for which to wait for events to arrive with each polling + * call. + */ + private EventFetcherTask(long interval, long timeout) { + super(interval); + this.timeout = checkInRange(timeout, 1, Long.MAX_VALUE, "timeout"); + } + + @Override + protected void runActualTask() { + boolean shutDown = false; + try { + LOG.debug("Fetching remote kernel for events"); + List events = callWrapper.call(() -> servicePortal.fetchEvents(timeout)); + + for (Object event : events) { + LOG.debug("Processing fetched event: {}", event); + // Forward received events to all registered listeners. + eventBus.onEvent(event); + + // Check if the kernel notifies us about a state change. + if (event instanceof KernelStateTransitionEvent) { + KernelStateTransitionEvent stateEvent = (KernelStateTransitionEvent) event; + // If the kernel switches to SHUTDOWN, remember to shut down. + shutDown = stateEvent.getEnteredState() == Kernel.State.SHUTDOWN; + } + } + } + catch (Exception exc) { + LOG.error("Exception fetching events", exc); + } + + if (shutDown) { + application.offline(); + } + } + } +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/exchange/SslConfiguration.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/exchange/SslConfiguration.java new file mode 100644 index 0000000..25fed47 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/exchange/SslConfiguration.java @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.exchange; + +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; + +/** + * Provides methods to configure the ssl connection. + */ +@ConfigurationPrefix(SslConfiguration.PREFIX) +public interface SslConfiguration { + + /** + * This configuration's prefix. + */ + String PREFIX = "ssl"; + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to use SSL to encrypt RMI connections to the kernel.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_connection_0" + ) + boolean enable(); + + @ConfigurationEntry( + type = "String", + description = "The path to the SSL truststore.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_connection_1" + ) + String truststoreFile(); + + @ConfigurationEntry( + type = "String", + description = "The password for the SSL truststore.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_connection_2" + ) + String truststorePassword(); +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/AdapterFactoryCellRenderer.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/AdapterFactoryCellRenderer.java new file mode 100644 index 0000000..8266dbd --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/AdapterFactoryCellRenderer.java @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.peripherals; + +import java.awt.Component; +import javax.swing.DefaultListCellRenderer; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.ListCellRenderer; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; + +/** + * ListCellRenderer for the adapter combo box. + */ +final class AdapterFactoryCellRenderer + implements + ListCellRenderer { + + /** + * A default renderer for creating the label. + */ + private final DefaultListCellRenderer defaultRenderer = new DefaultListCellRenderer(); + + /** + * Creates a new instance. + */ + AdapterFactoryCellRenderer() { + } + + @Override + public Component getListCellRendererComponent( + JList list, + PeripheralCommAdapterDescription value, + int index, + boolean isSelected, + boolean cellHasFocus + ) { + JLabel label = (JLabel) defaultRenderer.getListCellRendererComponent( + list, + value, + index, + isSelected, + cellHasFocus + ); + if (value != null) { + label.setText(value.getDescription()); + } + else { + label.setText(" "); + } + return label; + } +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/CommAdapterFactoryTableCellRenderer.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/CommAdapterFactoryTableCellRenderer.java new file mode 100644 index 0000000..ddc2787 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/CommAdapterFactoryTableCellRenderer.java @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.peripherals; + +import java.awt.Component; +import javax.swing.JTable; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableCellRenderer; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; + +/** + * A {@link TableCellRenderer} for {@link PeripheralCommAdapterDescription} instances. + * This class provides a representation of any PeripheralCommAdapterDescription instance by writing + * its actual description on a JLabel. + */ +class CommAdapterFactoryTableCellRenderer + extends + DefaultTableCellRenderer { + + CommAdapterFactoryTableCellRenderer() { + } + + @Override + public Component getTableCellRendererComponent( + JTable table, + Object value, + boolean isSelected, + boolean hasFocus, + int row, + int column + ) + throws IllegalArgumentException { + + super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + + if (value == null) { + setText(""); + } + else if (value instanceof PeripheralCommAdapterDescription) { + setText(((PeripheralCommAdapterDescription) value).getDescription()); + } + else { + throw new IllegalArgumentException("value"); + } + return this; + } + +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/LocalPeripheralEntry.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/LocalPeripheralEntry.java new file mode 100644 index 0000000..4a4cc54 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/LocalPeripheralEntry.java @@ -0,0 +1,137 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.peripherals; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; +import org.opentcs.drivers.peripherals.PeripheralProcessModel; + +/** + * An entry for a peripheral device registered with the kernel. + */ +public class LocalPeripheralEntry { + + /** + * Used for implementing property change events. + */ + @SuppressWarnings("this-escape") + private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); + /** + * The location representing the peripheral device. + */ + private final TCSResourceReference location; + /** + * The description of the attached peripheral comm adapter. + */ + private PeripheralCommAdapterDescription attachedCommAdapter; + /** + * The process model that describes the peripheral device's state. + */ + private PeripheralProcessModel processModel; + + /** + * Creates a new instance. + * + * @param location The location representing the peripheral device. + * @param attachedCommAdapter The description of the attached peripheral comm adapter. + * @param processModel The process model that describes the peripheral device's state. + */ + public LocalPeripheralEntry( + @Nonnull + TCSResourceReference location, + @Nonnull + PeripheralCommAdapterDescription attachedCommAdapter, + @Nonnull + PeripheralProcessModel processModel + ) { + this.location = requireNonNull(location, "location"); + this.attachedCommAdapter = requireNonNull(attachedCommAdapter, "attachedCommAdapter"); + this.processModel = requireNonNull(processModel, "processModel"); + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + pcs.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + pcs.removePropertyChangeListener(listener); + } + + /** + * Returns the location representing the peripheral device. + * + * @return The location representing the peripheral device. + */ + @Nonnull + public TCSResourceReference getLocation() { + return location; + } + + /** + * Returns the description of the attached peripheral comm adapter. + * + * @return The description of the attached peripheral comm adapter. + */ + @Nonnull + public PeripheralCommAdapterDescription getAttachedCommAdapter() { + return attachedCommAdapter; + } + + public void setAttachedCommAdapter( + @Nonnull + PeripheralCommAdapterDescription attachedCommAdapter + ) { + PeripheralCommAdapterDescription oldAttachedCommAdapter = this.attachedCommAdapter; + this.attachedCommAdapter = requireNonNull(attachedCommAdapter, "attachedCommAdapter"); + + pcs.firePropertyChange( + Attribute.ATTACHED_COMM_ADAPTER.name(), + oldAttachedCommAdapter, + attachedCommAdapter + ); + } + + /** + * Returns the process model that describes the peripheral device's state. + * + * @return The process model that describes the peripheral device's state. + */ + @Nonnull + public PeripheralProcessModel getProcessModel() { + return processModel; + } + + public void setProcessModel( + @Nonnull + PeripheralProcessModel processModel + ) { + PeripheralProcessModel oldProcessModel = this.processModel; + this.processModel = requireNonNull(processModel, "processModel"); + + pcs.firePropertyChange( + Attribute.PROCESS_MODEL.name(), + oldProcessModel, + processModel + ); + } + + /** + * Enum elements used as notification arguments to specify which argument changed. + */ + public enum Attribute { + /** + * Indicates a change of the process model. + */ + PROCESS_MODEL, + /** + * Indicates a change of the attached comm adapter. + */ + ATTACHED_COMM_ADAPTER + } +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/LocalPeripheralEntryPool.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/LocalPeripheralEntryPool.java new file mode 100644 index 0000000..57c5a1d --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/LocalPeripheralEntryPool.java @@ -0,0 +1,173 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.peripherals; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.components.Lifecycle; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.customizations.ServiceCallWrapper; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.drivers.peripherals.PeripheralProcessModel; +import org.opentcs.drivers.peripherals.management.PeripheralAttachmentEvent; +import org.opentcs.drivers.peripherals.management.PeripheralAttachmentInformation; +import org.opentcs.drivers.peripherals.management.PeripheralProcessModelEvent; +import org.opentcs.util.CallWrapper; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.event.EventSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A pool of {@link LocalPeripheralEntry}'s for every location in the kernel that represents a + * peripheral device. + */ +public class LocalPeripheralEntryPool + implements + EventHandler, + Lifecycle { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(LocalPeripheralEntryPool.class); + /** + * The service portal to use for kernel interactions. + */ + private final KernelServicePortal servicePortal; + /** + * The call wrapper to use for service calls. + */ + private final CallWrapper callWrapper; + /** + * Where this instance registers for application events. + */ + private final EventSource eventSource; + /** + * The entries of this pool. + */ + private final Map, LocalPeripheralEntry> entries = new HashMap<>(); + /** + * Whether the pool is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param servicePortal The service portal to use for kernel interactions. + * @param callWrapper The call wrapper to use for service calls. + * @param eventSource Where this instance registers for application events. + */ + @Inject + public LocalPeripheralEntryPool( + KernelServicePortal servicePortal, + @ServiceCallWrapper + CallWrapper callWrapper, + @ApplicationEventBus + EventSource eventSource + ) { + this.servicePortal = requireNonNull(servicePortal, "servicePortal"); + this.callWrapper = requireNonNull(callWrapper, "callWrapper"); + this.eventSource = requireNonNull(eventSource, "eventSource"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + try { + initializeEntryMap(); + LOG.debug("Initialized peripheral entry pool: {}", entries); + } + catch (Exception e) { + LOG.warn("Error initializing peripheral entry pool.", e); + entries.clear(); + return; + } + + eventSource.subscribe(this); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + eventSource.unsubscribe(this); + entries.clear(); + + initialized = false; + } + + @Override + public void onEvent(Object event) { + if (event instanceof PeripheralProcessModelEvent) { + onPeripheralProcessModelEvent((PeripheralProcessModelEvent) event); + } + else if (event instanceof PeripheralAttachmentEvent) { + onPeripheralAttachmentEvent((PeripheralAttachmentEvent) event); + } + } + + public Map, LocalPeripheralEntry> getEntries() { + return entries; + } + + private void initializeEntryMap() + throws Exception { + Set locations + = callWrapper.call(() -> servicePortal.getPlantModelService().fetchObjects(Location.class)); + for (Location location : locations) { + PeripheralAttachmentInformation ai = callWrapper.call(() -> { + return servicePortal.getPeripheralService() + .fetchAttachmentInformation(location.getReference()); + }); + PeripheralProcessModel processModel = callWrapper.call( + () -> servicePortal.getPeripheralService().fetchProcessModel(location.getReference()) + ); + entries.put( + location.getReference(), new LocalPeripheralEntry( + location.getReference(), + ai.getAttachedCommAdapter(), + processModel + ) + ); + } + } + + private void onPeripheralProcessModelEvent(PeripheralProcessModelEvent event) { + if (!entries.containsKey(event.getLocation())) { + LOG.warn("Received an event for an unknown location: {}", event.getLocation().getName()); + return; + } + + entries.get(event.getLocation()).setProcessModel(event.getProcessModel()); + } + + private void onPeripheralAttachmentEvent(PeripheralAttachmentEvent event) { + if (!entries.containsKey(event.getLocation())) { + LOG.warn("Received an event for an unknown location: {}", event.getLocation().getName()); + return; + } + + entries.get(event.getLocation()) + .setAttachedCommAdapter(event.getAttachmentInformation().getAttachedCommAdapter()); + } +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/PeripheralAdapterComboBox.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/PeripheralAdapterComboBox.java new file mode 100644 index 0000000..6424985 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/PeripheralAdapterComboBox.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.peripherals; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Objects; +import javax.swing.JComboBox; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; + +/** + * A wide combobox which sets the selected item when receiving an update event from a + * {@link LocalPeripheralEntry}. + */ +public class PeripheralAdapterComboBox + extends + JComboBox + implements + PropertyChangeListener { + + /** + * Creates a new instance. + */ + public PeripheralAdapterComboBox() { + } + + @Override + public PeripheralCommAdapterDescription getSelectedItem() { + return (PeripheralCommAdapterDescription) super.getSelectedItem(); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (!(evt.getSource() instanceof LocalPeripheralEntry)) { + return; + } + + LocalPeripheralEntry entry = (LocalPeripheralEntry) evt.getSource(); + if (Objects.equals(entry.getAttachedCommAdapter(), getModel().getSelectedItem())) { + return; + } + + super.setSelectedItem(entry.getAttachedCommAdapter()); + } + +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/PeripheralDetailPanel.form b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/PeripheralDetailPanel.form new file mode 100644 index 0000000..b20f1df --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/PeripheralDetailPanel.form @@ -0,0 +1,67 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/PeripheralDetailPanel.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/PeripheralDetailPanel.java new file mode 100644 index 0000000..4608f1e --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/PeripheralDetailPanel.java @@ -0,0 +1,265 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.peripherals; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import javax.swing.JPanel; +import javax.swing.border.TitledBorder; +import org.opentcs.components.Lifecycle; +import org.opentcs.drivers.peripherals.management.PeripheralCommAdapterPanel; +import org.opentcs.drivers.peripherals.management.PeripheralCommAdapterPanelFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Displays the comm adapter panels for a given peripheral device. + */ +public class PeripheralDetailPanel + extends + JPanel + implements + PropertyChangeListener, + Lifecycle { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PeripheralDetailPanel.class); + /** + * A panel's default border title. + */ + private static final String DEFAULT_BORDER_TITLE = ""; + /** + * The adapter specific list of panels. + */ + private final List customPanelList = new ArrayList<>(); + /** + * The set of factories to create adapter specific panels with. + */ + private final Set panelFactories; + /** + * The peripheral device that is currently associated with/being displayed in this panel. + */ + private LocalPeripheralEntry peripheralEntry; + /** + * Whether this panel is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param panelFactories The factories to create adapter specific panels with. + */ + @Inject + @SuppressWarnings("this-escape") + public PeripheralDetailPanel(Set panelFactories) { + this.panelFactories = requireNonNull(panelFactories, "panelFactories"); + + initComponents(); + + // Make sure we start with an empty panel. + detachFromEntry(); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + for (PeripheralCommAdapterPanelFactory panelFactory : panelFactories) { + panelFactory.initialize(); + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + detachFromEntry(); + + for (PeripheralCommAdapterPanelFactory panelFactory : panelFactories) { + panelFactory.terminate(); + } + + initialized = false; + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (!(evt.getSource() instanceof LocalPeripheralEntry)) { + return; + } + + LocalPeripheralEntry entry = (LocalPeripheralEntry) evt.getSource(); + if (!Objects.equals(entry, peripheralEntry)) { + // Since we're registered only to the currently selected/attached entry, this should never + // happen. + return; + } + + if (Objects.equals( + evt.getPropertyName(), + LocalPeripheralEntry.Attribute.PROCESS_MODEL.name() + )) { + for (PeripheralCommAdapterPanel panel : customPanelList) { + panel.processModelChanged(entry.getProcessModel()); + } + } + } + + /** + * Attaches this panel to a peripheral device. + * + * @param newPeripheralEntry The peripheral entry to attach to. + */ + public void attachToEntry(LocalPeripheralEntry newPeripheralEntry) { + requireNonNull(newPeripheralEntry, "newPeripheralEntry"); + + // Clean up first - but only if we're not reattaching to the same entry. + if (peripheralEntry != newPeripheralEntry) { + detachFromEntry(); + } + peripheralEntry = newPeripheralEntry; + + setBorderTitle(peripheralEntry.getProcessModel().getLocation().getName()); + + // Ensure the tabbed pane containing peripheral information is shown. + removeAll(); + add(tabbedPane); + + updateCustomPanels(); + + // Update panel contents. + validate(); + if (!customPanelList.isEmpty()) { + tabbedPane.setSelectedIndex(0); + } + + peripheralEntry.addPropertyChangeListener(this); + } + + /** + * Detaches this panel from a peripheral device (if it is currently attached to any). + */ + private void detachFromEntry() { + if (peripheralEntry != null) { + peripheralEntry.removePropertyChangeListener(this); + removeAndClearCustomPanels(); + peripheralEntry = null; + } + setBorderTitle(DEFAULT_BORDER_TITLE); + // Remove the contents of this panel. + removeAll(); + add(noPeripheralDevicePanel); + // Update panel contents. + validate(); + } + + /** + * Removes the custom panels from this panel's tabbed pane. + */ + private void removeAndClearCustomPanels() { + for (PeripheralCommAdapterPanel panel : customPanelList) { + LOG.debug("Removing {} from tabbedPane.", panel); + tabbedPane.remove(panel); + } + + customPanelList.clear(); + } + + /** + * Update the list of custom panels in the tabbed pane. + */ + private void updateCustomPanels() { + removeAndClearCustomPanels(); + + if (peripheralEntry == null) { + return; + } + + for (PeripheralCommAdapterPanelFactory panelFactory : panelFactories) { + customPanelList.addAll( + panelFactory.getPanelsFor( + peripheralEntry.getAttachedCommAdapter(), + peripheralEntry.getLocation(), + peripheralEntry.getProcessModel() + ) + ); + } + + for (PeripheralCommAdapterPanel curPanel : customPanelList) { + LOG.debug("Adding {} with title {} to tabbedPane.", curPanel, curPanel.getTitle()); + tabbedPane.addTab(curPanel.getTitle(), curPanel); + } + } + + /** + * Sets this panel's border title. + * + * @param title This panel's new border title. + */ + private void setBorderTitle(String title) { + requireNonNull(title, "title"); + ((TitledBorder) getBorder()).setTitle(title); + // Trigger a repaint - the title sometimes looks strange otherwise. + repaint(); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + + noPeripheralDevicePanel = new javax.swing.JPanel(); + noPeripheralDeviceLabel = new javax.swing.JLabel(); + tabbedPane = new javax.swing.JTabbedPane(); + + noPeripheralDevicePanel.setLayout(new java.awt.BorderLayout()); + + noPeripheralDeviceLabel.setFont(noPeripheralDeviceLabel.getFont().deriveFont(noPeripheralDeviceLabel.getFont().getSize()+3f)); + noPeripheralDeviceLabel.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/kernelcontrolcenter/Bundle"); // NOI18N + noPeripheralDeviceLabel.setText(bundle.getString("peripheralDetailPanel.label_noPeripheralDeviceAttached.text")); // NOI18N + noPeripheralDevicePanel.add(noPeripheralDeviceLabel, java.awt.BorderLayout.CENTER); + + setBorder(javax.swing.BorderFactory.createTitledBorder(DEFAULT_BORDER_TITLE)); + setLayout(new java.awt.BorderLayout()); + + tabbedPane.setTabLayoutPolicy(javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT); + add(tabbedPane, java.awt.BorderLayout.CENTER); + }// //GEN-END:initComponents + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel noPeripheralDeviceLabel; + private javax.swing.JPanel noPeripheralDevicePanel; + private javax.swing.JTabbedPane tabbedPane; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON + +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/PeripheralTableModel.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/PeripheralTableModel.java new file mode 100644 index 0000000..43801d6 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/PeripheralTableModel.java @@ -0,0 +1,244 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.peripherals; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.kernelcontrolcenter.I18nKernelControlCenter.BUNDLE_PATH; +import static org.opentcs.util.Assertions.checkArgument; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.ResourceBundle; +import javax.swing.SwingUtilities; +import javax.swing.table.AbstractTableModel; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; +import org.opentcs.drivers.peripherals.PeripheralProcessModel; +import org.opentcs.util.CallWrapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A model for displaying a table of peripherals. + */ +public class PeripheralTableModel + extends + AbstractTableModel + implements + PropertyChangeListener { + + /** + * The index of the column showing the peripheral device's name. + */ + public static final int COLUMN_LOCATION = 0; + /** + * The index of the column showing the associated adapter. + */ + public static final int COLUMN_ADAPTER = 1; + /** + * The index of the column showing the adapter's enabled state. + */ + public static final int COLUMN_ENABLED = 2; + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PeripheralTableModel.class); + /** + * This class's resource bundle. + */ + private static final ResourceBundle BUNDLE = ResourceBundle.getBundle(BUNDLE_PATH); + /** + * The collumn names for the table header. + */ + private static final String[] COLUMN_NAMES + = new String[]{ + BUNDLE.getString("peripheralTableModel.column_location.headerText"), + BUNDLE.getString("peripheralTableModel.column_adapter.headerText"), + BUNDLE.getString("peripheralTableModel.column_enabled.headerText") + }; + /** + * The column classes. + */ + private static final Class[] COLUMN_CLASSES + = new Class[]{ + String.class, + PeripheralCommAdapterDescription.class, + Boolean.class + }; + /** + * The service portal to use for kernel interaction. + */ + private final KernelServicePortal servicePortal; + /** + * The call wrapper to use. + */ + private final CallWrapper callWrapper; + /** + * The entries in the table. + */ + private final List entries = new ArrayList<>(); + + public PeripheralTableModel( + KernelServicePortal servicePortal, + CallWrapper callWrapper + ) { + this.servicePortal = requireNonNull(servicePortal, "servicePortal"); + this.callWrapper = requireNonNull(callWrapper, "callWrapper"); + } + + /** + * Returns the identifier for the adapter column. + * + * @return the idenfitier for the adapter column. + */ + public static String adapterColumnIdentifier() { + return COLUMN_NAMES[COLUMN_ADAPTER]; + } + + public void addData(LocalPeripheralEntry entry) { + entries.add(entry); + fireTableRowsInserted(entries.size() - 1, entries.size() - 1); + } + + public LocalPeripheralEntry getDataAt(int index) { + checkArgument( + index >= 0 && index < entries.size(), + "index must be in 0..%d: %d", + entries.size(), + index + ); + return entries.get(index); + } + + @Override + public int getRowCount() { + return entries.size(); + } + + @Override + public int getColumnCount() { + return COLUMN_NAMES.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (rowIndex >= entries.size()) { + return null; + } + + LocalPeripheralEntry entry = entries.get(rowIndex); + + switch (columnIndex) { + case COLUMN_LOCATION: + return entry.getProcessModel().getLocation().getName(); + case COLUMN_ADAPTER: + return entry.getAttachedCommAdapter(); + case COLUMN_ENABLED: + return entry.getProcessModel().isCommAdapterEnabled(); + default: + throw new IllegalArgumentException("Invalid column index: " + columnIndex); + } + } + + @Override + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + requireNonNull(aValue, "aValue"); + + LocalPeripheralEntry entry = entries.get(rowIndex); + switch (columnIndex) { + case COLUMN_LOCATION: + break; + case COLUMN_ADAPTER: + break; + case COLUMN_ENABLED: + try { + if ((boolean) aValue) { + callWrapper.call( + () -> servicePortal.getPeripheralService().enableCommAdapter(entry.getLocation()) + ); + } + else { + callWrapper.call( + () -> servicePortal.getPeripheralService().disableCommAdapter(entry.getLocation()) + ); + } + } + catch (Exception ex) { + LOG.warn( + "Error enabling/disabling peripheral comm adapter for {}", + entry.getLocation().getName(), + ex + ); + } + break; + default: + throw new IllegalArgumentException("Invalid column index: " + columnIndex); + } + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + switch (columnIndex) { + case COLUMN_LOCATION: + return false; + case COLUMN_ADAPTER: + return true; + case COLUMN_ENABLED: + return true; + default: + return false; + } + } + + @Override + public String getColumnName(int columnIndex) { + return COLUMN_NAMES[columnIndex]; + } + + @Override + public Class getColumnClass(int columnIndex) { + return COLUMN_CLASSES[columnIndex]; + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (!isRelevantUpdate(evt)) { + return; + } + + LocalPeripheralEntry entry = (LocalPeripheralEntry) evt.getSource(); + int index = entries.indexOf(entry); + SwingUtilities.invokeLater(() -> fireTableRowsUpdated(index, index)); + + } + + private boolean isRelevantUpdate(PropertyChangeEvent evt) { + if (!(evt.getSource() instanceof LocalPeripheralEntry)) { + return false; + } + + if (Objects.equals( + evt.getPropertyName(), + LocalPeripheralEntry.Attribute.ATTACHED_COMM_ADAPTER.name() + )) { + PeripheralCommAdapterDescription oldInfo + = (PeripheralCommAdapterDescription) evt.getOldValue(); + PeripheralCommAdapterDescription newInfo + = (PeripheralCommAdapterDescription) evt.getNewValue(); + return !oldInfo.equals(newInfo); + } + if (Objects.equals( + evt.getPropertyName(), + LocalPeripheralEntry.Attribute.PROCESS_MODEL.name() + )) { + PeripheralProcessModel oldTo = (PeripheralProcessModel) evt.getOldValue(); + PeripheralProcessModel newTo = (PeripheralProcessModel) evt.getNewValue(); + return oldTo.isCommAdapterEnabled() != newTo.isCommAdapterEnabled(); + } + + return false; + } +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/PeripheralsPanel.form b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/PeripheralsPanel.form new file mode 100644 index 0000000..af8006e --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/PeripheralsPanel.form @@ -0,0 +1,102 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/PeripheralsPanel.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/PeripheralsPanel.java new file mode 100644 index 0000000..faa63bc --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/peripherals/PeripheralsPanel.java @@ -0,0 +1,389 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.peripherals; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.kernelcontrolcenter.I18nKernelControlCenter.BUNDLE_PATH; +import static org.opentcs.util.Assertions.checkState; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.awt.EventQueue; +import java.awt.event.ItemEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.ResourceBundle; +import javax.swing.DefaultCellEditor; +import javax.swing.JOptionPane; +import javax.swing.RowFilter; +import javax.swing.table.TableRowSorter; +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.common.peripherals.NullPeripheralCommAdapterDescription; +import org.opentcs.components.kernelcontrolcenter.ControlCenterPanel; +import org.opentcs.customizations.ServiceCallWrapper; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; +import org.opentcs.drivers.peripherals.management.PeripheralAttachmentInformation; +import org.opentcs.kernelcontrolcenter.util.SingleCellEditor; +import org.opentcs.util.CallWrapper; +import org.opentcs.util.gui.BoundsPopupMenuListener; +import org.opentcs.util.gui.StringTableCellRenderer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A panel containing all locations representing peripheral devices. + */ +public class PeripheralsPanel + extends + ControlCenterPanel { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PeripheralsPanel.class); + /** + * The service portal to use for kernel interaction. + */ + private final KernelServicePortal servicePortal; + /** + * The call wrapper to use. + */ + private final CallWrapper callWrapper; + /** + * The pool of peripheral devices. + */ + private final LocalPeripheralEntryPool peripheralEntryPool; + /** + * The details panel. + */ + private final PeripheralDetailPanel detailPanel; + /** + * The table row sorter to use. + */ + private TableRowSorter sorter; + /** + * This instance's resource bundle. + */ + private final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_PATH); + /** + * Whether this component is initialized. + */ + private boolean initialized; + + /** + * Creates new instance. + * + * @param servicePortal The service portal to use for kernel interaction. + * @param callWrapper The call wrapper to use for publishing events to the kernel. + * @param peripheralEntryPool The pool of peripheral devices. + * @param detailPanel The details panel. + */ + @Inject + @SuppressWarnings("this-escape") + public PeripheralsPanel( + @Nonnull + KernelServicePortal servicePortal, + @Nonnull + @ServiceCallWrapper + CallWrapper callWrapper, + @Nonnull + LocalPeripheralEntryPool peripheralEntryPool, + @Nonnull + PeripheralDetailPanel detailPanel + ) { + this.servicePortal = requireNonNull(servicePortal, "servicePortal"); + this.callWrapper = requireNonNull(callWrapper, "callWrapper"); + this.peripheralEntryPool = requireNonNull(peripheralEntryPool, "peripheralEntryPool"); + this.detailPanel = requireNonNull(detailPanel, "detailPanel"); + + initComponents(); + + peripheralTable.setDefaultRenderer( + PeripheralCommAdapterDescription.class, + new CommAdapterFactoryTableCellRenderer() + ); + + peripheralDetailsPanel.add(detailPanel); + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + // Verify that the kernel is in a state in which controlling peripherals is possible. + Kernel.State kernelState; + try { + kernelState = callWrapper.call(() -> servicePortal.getState()); + } + catch (Exception ex) { + LOG.warn("Error getting the kernel state", ex); + return; + } + checkState( + Kernel.State.OPERATING.equals(kernelState), + "Cannot work in kernel state %s", + kernelState + ); + + peripheralEntryPool.initialize(); + detailPanel.initialize(); + + EventQueue.invokeLater(() -> { + initPeripheralTable(); + }); + + initialized = true; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + detailPanel.terminate(); + peripheralEntryPool.terminate(); + + initialized = false; + } + + private void initPeripheralTable() { + PeripheralTableModel model = new PeripheralTableModel(servicePortal, callWrapper); + peripheralTable.setModel(model); + peripheralTable.getColumnModel().getColumn(PeripheralTableModel.COLUMN_ADAPTER).setCellRenderer( + new StringTableCellRenderer( + description -> description.getDescription() + ) + ); + sorter = new TableRowSorter<>(model); + peripheralTable.setRowSorter(sorter); + + peripheralEntryPool.getEntries().forEach((location, entry) -> { + model.addData(entry); + entry.addPropertyChangeListener(model); + }); + + initComboBoxes(); + updateRowFilter(); + } + + private void initComboBoxes() { + SingleCellEditor adpaterCellEditor = new SingleCellEditor(peripheralTable); + + int index = 0; + for (LocalPeripheralEntry entry : peripheralEntryPool.getEntries().values()) { + initCommAdaptersComboBox(entry, index, adpaterCellEditor); + index++; + } + + peripheralTable.getColumn(PeripheralTableModel.adapterColumnIdentifier()) + .setCellEditor(adpaterCellEditor); + } + + private void initCommAdaptersComboBox( + LocalPeripheralEntry peripheralEntry, + int rowIndex, + SingleCellEditor adapterCellEditor + ) { + final PeripheralAdapterComboBox comboBox = new PeripheralAdapterComboBox(); + PeripheralAttachmentInformation ai; + try { + ai = callWrapper.call( + () -> servicePortal.getPeripheralService().fetchAttachmentInformation( + peripheralEntry.getLocation() + ) + ); + } + catch (Exception ex) { + LOG.warn( + "Error fetching attachment information for {}", + peripheralEntry.getLocation().getName(), + ex + ); + return; + } + + for (PeripheralCommAdapterDescription factory : ai.getAvailableCommAdapters()) { + comboBox.addItem(factory); + } + if (ai.getAvailableCommAdapters().isEmpty()) { + comboBox.addItem(new NullPeripheralCommAdapterDescription()); + } + + // Set the selection to the attached comm adapter. (The peripheral is already attached to a comm + // adapter due to auto attachment on startup.) + comboBox.setSelectedItem(ai.getAttachedCommAdapter()); + + comboBox.setRenderer(new AdapterFactoryCellRenderer()); + comboBox.addPopupMenuListener(new BoundsPopupMenuListener()); + comboBox.addItemListener((ItemEvent evt) -> { + if (evt.getStateChange() == ItemEvent.DESELECTED) { + return; + } + + // If we selected a comm adapter that's already attached, do nothing. + if (Objects.equals(evt.getItem(), peripheralEntry.getAttachedCommAdapter())) { + LOG.debug( + "{} is already attached to: {}", + peripheralEntry.getLocation().getName(), + evt.getItem() + ); + return; + } + + int reply = JOptionPane.showConfirmDialog( + null, + bundle.getString("peripheralsPanel.optionPane_driverChangeConfirmation.message"), + bundle.getString("peripheralsPanel.optionPane_driverChangeConfirmation.title"), + JOptionPane.YES_NO_OPTION + ); + if (reply == JOptionPane.NO_OPTION) { + return; + } + + PeripheralCommAdapterDescription factory = comboBox.getSelectedItem(); + try { + callWrapper.call( + () -> servicePortal.getPeripheralService().attachCommAdapter( + peripheralEntry.getLocation(), + factory + ) + ); + } + catch (Exception ex) { + LOG.warn( + "Error attaching adapter {} to vehicle {}", + factory, + peripheralEntry.getLocation().getName(), + ex + ); + return; + } + LOG.info("Attaching comm adapter {} to {}", factory, peripheralEntry.getLocation().getName()); + }); + adapterCellEditor.setEditorAt(rowIndex, new DefaultCellEditor(comboBox)); + + peripheralEntry.addPropertyChangeListener(comboBox); + } + + private void updateRowFilter() { + sorter.setRowFilter(RowFilter.andFilter(toRegexFilters())); + } + + private List> toRegexFilters() { + List> result = new ArrayList<>(); + result.add(RowFilter.regexFilter(".*", PeripheralTableModel.COLUMN_ADAPTER)); + if (hideDetachedLocationsCheckBox.isSelected()) { + result.add( + RowFilter.notFilter( + RowFilter.regexFilter( + NullPeripheralCommAdapterDescription.class.getSimpleName(), + PeripheralTableModel.COLUMN_ADAPTER + ) + ) + ); + } + + return result; + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + listPanel = new javax.swing.JPanel(); + filterPanel = new javax.swing.JPanel(); + hideDetachedLocationsCheckBox = new javax.swing.JCheckBox(); + jScrollPane1 = new javax.swing.JScrollPane(); + peripheralTable = new javax.swing.JTable(); + peripheralDetailsPanel = new javax.swing.JPanel(); + + setLayout(new javax.swing.BoxLayout(this, javax.swing.BoxLayout.LINE_AXIS)); + + listPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Peripheral devices in model")); + listPanel.setMaximumSize(new java.awt.Dimension(464, 2147483647)); + listPanel.setMinimumSize(new java.awt.Dimension(464, 425)); + listPanel.setPreferredSize(new java.awt.Dimension(464, 424)); + listPanel.setLayout(new java.awt.BorderLayout()); + + filterPanel.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.LEFT)); + + hideDetachedLocationsCheckBox.setSelected(true); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/kernelcontrolcenter/Bundle"); // NOI18N + hideDetachedLocationsCheckBox.setText(bundle.getString("peripheralsPanel.checkBox_hideDetachedLocations.text")); // NOI18N + hideDetachedLocationsCheckBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + hideDetachedLocationsCheckBoxActionPerformed(evt); + } + }); + filterPanel.add(hideDetachedLocationsCheckBox); + + listPanel.add(filterPanel, java.awt.BorderLayout.NORTH); + + peripheralTable.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + peripheralTableMouseClicked(evt); + } + }); + jScrollPane1.setViewportView(peripheralTable); + + listPanel.add(jScrollPane1, java.awt.BorderLayout.CENTER); + + add(listPanel); + + peripheralDetailsPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Peripheral details")); + peripheralDetailsPanel.setPreferredSize(new java.awt.Dimension(800, 23)); + peripheralDetailsPanel.setLayout(new java.awt.BorderLayout()); + add(peripheralDetailsPanel); + + getAccessibleContext().setAccessibleName(bundle.getString("peripheralsPanel.accessibleName")); // NOI18N + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void peripheralTableMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_peripheralTableMouseClicked + if (evt.getClickCount() == 2) { + int index = peripheralTable.getSelectedRow(); + if (index >= 0) { + PeripheralTableModel model = (PeripheralTableModel) peripheralTable.getModel(); + LocalPeripheralEntry selectedEntry + = model.getDataAt(peripheralTable.convertRowIndexToModel(index)); + detailPanel.attachToEntry(selectedEntry); + } + } + }//GEN-LAST:event_peripheralTableMouseClicked + + private void hideDetachedLocationsCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_hideDetachedLocationsCheckBoxActionPerformed + updateRowFilter(); + }//GEN-LAST:event_hideDetachedLocationsCheckBoxActionPerformed + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel filterPanel; + private javax.swing.JCheckBox hideDetachedLocationsCheckBox; + private javax.swing.JScrollPane jScrollPane1; + private javax.swing.JPanel listPanel; + private javax.swing.JPanel peripheralDetailsPanel; + private javax.swing.JTable peripheralTable; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/util/KernelControlCenterConfiguration.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/util/KernelControlCenterConfiguration.java new file mode 100644 index 0000000..9c13db2 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/util/KernelControlCenterConfiguration.java @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.util; + +import java.util.List; +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; +import org.opentcs.util.gui.dialog.ConnectionParamSet; + +/** + * Provides methods to configure the KernelControlCenter application. + */ +@ConfigurationPrefix(KernelControlCenterConfiguration.PREFIX) +public interface KernelControlCenterConfiguration { + + /** + * This configuration's prefix. + */ + String PREFIX = "kernelcontrolcenter"; + + @ConfigurationEntry( + type = "String", + description = {"The kernel control center application's locale, as a BCP 47 language tag.", + "Examples: 'en', 'de', 'zh'"}, + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_init_0" + ) + String locale(); + + @ConfigurationEntry( + type = "Comma-separated list of \\|\\|", + description = "Kernel connection bookmarks to be used.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "1_connection_0" + ) + List connectionBookmarks(); + + @ConfigurationEntry( + type = "Boolean", + description = {"Whether to automatically connect to the kernel on startup.", + "If 'true', the first connection bookmark will be used for the initial " + + "connection attempt.", + "If 'false', a dialog will be shown to enter connection parameters."}, + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "1_connection_1" + ) + boolean connectAutomaticallyOnStartup(); + + @ConfigurationEntry( + type = "Integer", + description = "The maximum number of characters in the logging text area.", + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "9_misc_0" + ) + int loggingAreaCapacity(); + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to enable and show the panel for peripheral drivers.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "9_misc_1" + ) + boolean enablePeripheralsPanel(); +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/util/SingleCellEditor.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/util/SingleCellEditor.java new file mode 100644 index 0000000..af24f85 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/util/SingleCellEditor.java @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.util; + +import java.awt.Component; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.util.EventObject; +import java.util.HashMap; +import java.util.Map; +import javax.swing.DefaultCellEditor; +import javax.swing.JTable; +import javax.swing.JTextField; +import javax.swing.event.CellEditorListener; +import javax.swing.table.TableCellEditor; + +/** + * A cell editor for maintaining different editors in one column. + */ +public final class SingleCellEditor + implements + TableCellEditor { + + /** + * The TableCellEditors for every cell. + */ + private final Map editors; + /** + * The current editor. + */ + private TableCellEditor editor; + /** + * The default editor. + */ + private final TableCellEditor defaultEditor; + /** + * The table associated with the editors. + */ + private final JTable table; + + /** + * Constructs a SingleCellEditor. + * + * @param table The JTable associated + */ + public SingleCellEditor(JTable table) { + this.table = table; + editors = new HashMap<>(); + defaultEditor = new DefaultCellEditor(new JTextField()); + } + + /** + * Assigns an editor to a row. + * + * @param row table row + * @param rowEditor table cell editor + */ + public void setEditorAt(int row, TableCellEditor rowEditor) { + editors.put(row, rowEditor); + } + + @Override + public Component getTableCellEditorComponent( + JTable whichTable, + Object value, + boolean isSelected, + int row, + int column + ) { + return editor.getTableCellEditorComponent( + whichTable, + value, + isSelected, + row, + column + ); + } + + @Override + public Object getCellEditorValue() { + return editor.getCellEditorValue(); + } + + @Override + public boolean stopCellEditing() { + return editor.stopCellEditing(); + } + + @Override + public void cancelCellEditing() { + editor.cancelCellEditing(); + } + + @Override + public boolean isCellEditable(EventObject anEvent) { + if (anEvent instanceof KeyEvent) { + return false; + } + selectEditor((MouseEvent) anEvent); + return editor.isCellEditable(anEvent); + } + + @Override + public void addCellEditorListener(CellEditorListener l) { + editor.addCellEditorListener(l); + } + + @Override + public void removeCellEditorListener(CellEditorListener l) { + editor.removeCellEditorListener(l); + } + + @Override + public boolean shouldSelectCell(EventObject anEvent) { + selectEditor((MouseEvent) anEvent); + return editor.shouldSelectCell(anEvent); + } + + /** + * Sets the current editor. + * + * @param e A MouseEvent + */ + public void selectEditor(MouseEvent e) { + int row; + if (e == null) { + row = table.getSelectionModel().getAnchorSelectionIndex(); + } + else { + row = table.convertRowIndexToModel(table.rowAtPoint(e.getPoint())); + } + editor = editors.get(row); + if (editor == null) { + editor = defaultEditor; + } + table.changeSelection(row, table.getColumn("Adapter").getModelIndex(), false, false); + } +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/AdapterFactoryCellRenderer.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/AdapterFactoryCellRenderer.java new file mode 100644 index 0000000..1ab5c4e --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/AdapterFactoryCellRenderer.java @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.vehicles; + +import java.awt.Component; +import javax.swing.DefaultListCellRenderer; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.ListCellRenderer; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; + +/** + * ListCellRenderer for the adapter combo box. + */ +final class AdapterFactoryCellRenderer + implements + ListCellRenderer { + + /** + * A default renderer for creating the label. + */ + private final DefaultListCellRenderer defaultRenderer = new DefaultListCellRenderer(); + + /** + * Creates a new instance. + */ + AdapterFactoryCellRenderer() { + } + + @Override + public Component getListCellRendererComponent( + JList list, + VehicleCommAdapterDescription value, + int index, + boolean isSelected, + boolean cellHasFocus + ) { + JLabel label = (JLabel) defaultRenderer.getListCellRendererComponent( + list, + value, + index, + isSelected, + cellHasFocus + ); + if (value != null) { + label.setText(value.getDescription()); + } + else { + label.setText(" "); + } + return label; + } +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/CommAdapterComboBox.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/CommAdapterComboBox.java new file mode 100644 index 0000000..13b29bd --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/CommAdapterComboBox.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.vehicles; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Objects; +import javax.swing.JComboBox; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; + +/** + * A wide combobox which sets the selected item when receiving an update event from a + * {@link LocalVehicleEntry}. + */ +public class CommAdapterComboBox + extends + JComboBox + implements + PropertyChangeListener { + + /** + * Creates a new instance. + */ + public CommAdapterComboBox() { + } + + @Override + public VehicleCommAdapterDescription getSelectedItem() { + return (VehicleCommAdapterDescription) super.getSelectedItem(); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (!(evt.getSource() instanceof LocalVehicleEntry)) { + return; + } + + LocalVehicleEntry entry = (LocalVehicleEntry) evt.getSource(); + if (Objects.equals(entry.getAttachedCommAdapterDescription(), getModel().getSelectedItem())) { + return; + } + + super.setSelectedItem(entry.getAttachedCommAdapterDescription()); + } + +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/DetailPanel.form b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/DetailPanel.form new file mode 100644 index 0000000..20b8f96 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/DetailPanel.form @@ -0,0 +1,392 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+ diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/DetailPanel.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/DetailPanel.java new file mode 100644 index 0000000..7dab7fa --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/DetailPanel.java @@ -0,0 +1,736 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.vehicles; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Queue; +import java.util.Set; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.border.TitledBorder; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.components.Lifecycle; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.customizations.ServiceCallWrapper; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.notification.UserNotification; +import org.opentcs.drivers.vehicle.VehicleProcessModel; +import org.opentcs.drivers.vehicle.management.ProcessModelEvent; +import org.opentcs.drivers.vehicle.management.VehicleAttachmentEvent; +import org.opentcs.drivers.vehicle.management.VehicleAttachmentInformation; +import org.opentcs.drivers.vehicle.management.VehicleCommAdapterPanel; +import org.opentcs.drivers.vehicle.management.VehicleCommAdapterPanelFactory; +import org.opentcs.util.CallWrapper; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.event.EventSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Displays information about a vehicle (VehicleModel) graphically. + */ +public class DetailPanel + extends + JPanel + implements + EventHandler, + Lifecycle { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(DetailPanel.class); + /** + * A panel's default border title. + */ + private static final String DEFAULT_BORDER_TITLE = ""; + /** + * A DateFormat instance for formatting message's time stamps. + */ + private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter + .ofLocalizedDateTime(FormatStyle.SHORT) + .withLocale(Locale.getDefault()) + .withZone(ZoneId.systemDefault()); + /** + * The adapter specific list of JPanels. + */ + private final List customPanelList = new ArrayList<>(); + /** + * The set of factories to create adapter specific panels. + */ + private final Set panelFactories; + /** + * The logging table model to use. + */ + private final LogTableModel loggingTableModel = new LogTableModel(); + /** + * Where this instance registers for application events. + */ + private final EventSource eventSource; + /** + * The service portal to use for kernel interaction. + */ + private final KernelServicePortal servicePortal; + /** + * The call wrapper to use for service calls. + */ + private final CallWrapper callWrapper; + /** + * The vehicle model of the vehicle current associated with this window. + */ + private LocalVehicleEntry vehicleEntry; + /** + * The comm adapter currently attached to the vehicle (model). + */ + private VehicleAttachmentInformation attachmentInfo; + /** + * Whether this panel is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param servicePortal The service portal to use for kernel interaction. + * @param callWrapper The call wrapper to use for service calls. + * @param eventSource Where this instance registers for application events. + * @param panelFactories The factories to create adapter specific panels. + */ + @Inject + @SuppressWarnings("this-escape") + public DetailPanel( + KernelServicePortal servicePortal, + @ServiceCallWrapper + CallWrapper callWrapper, + @ApplicationEventBus + EventSource eventSource, + Set panelFactories + ) { + this.servicePortal = requireNonNull(servicePortal, "servicePortal"); + this.callWrapper = requireNonNull(callWrapper, "callWrapper"); + this.eventSource = requireNonNull(eventSource, "eventSource"); + this.panelFactories = requireNonNull(panelFactories, "panelFactories"); + + initComponents(); + + loggingTable.setModel(loggingTableModel); + loggingTable.getColumnModel().getColumn(0).setPreferredWidth(40); + loggingTable.getColumnModel().getColumn(1).setPreferredWidth(110); + loggingTable.getSelectionModel().addListSelectionListener(new RowListener()); + // Make sure we start with an empty panel. + detachFromVehicle(); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + for (VehicleCommAdapterPanelFactory fatory : panelFactories) { + fatory.initialize(); + } + + eventSource.subscribe(this); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + detachFromVehicle(); + + eventSource.unsubscribe(this); + + for (VehicleCommAdapterPanelFactory fatory : panelFactories) { + fatory.terminate(); + } + + initialized = false; + } + + @Override + public void onEvent(Object e) { + // Ignore events if no vehicle entry is associated with this panel. + if (vehicleEntry == null) { + return; + } + + if (e instanceof VehicleAttachmentEvent) { + VehicleAttachmentEvent event = (VehicleAttachmentEvent) e; + if (Objects.equals( + vehicleEntry.getVehicleName(), + event.getVehicleName() + )) { + updateFromVehicleEntry(event); + } + } + if (e instanceof ProcessModelEvent) { + ProcessModelEvent event = (ProcessModelEvent) e; + if (Objects.equals( + vehicleEntry.getVehicleName(), + event.getUpdatedProcessModel().getName() + )) { + updateFromVehicleProcessModel(event); + + // Forward event to the comm adapter panels + customPanelList.forEach( + panel -> panel.processModelChange( + event.getAttributeChanged(), + event.getUpdatedProcessModel() + ) + ); + } + } + } + + /** + * Attaches this panel to a vehicle. + * + * @param newVehicleEntry The vehicle entry to attach to. + */ + void attachToVehicle(LocalVehicleEntry newVehicleEntry) { + requireNonNull(newVehicleEntry, "newVehicleEntry"); + + // Clean up first - but only if we're not reattaching the vehicle model which is already + // attached to this panel. + if (vehicleEntry != newVehicleEntry) { + detachFromVehicle(); + } + vehicleEntry = newVehicleEntry; + + setBorderTitle(vehicleEntry.getVehicleName()); + + // Ensure the tabbed pane containing vehicle information is shown. + removeAll(); + add(tabbedPane); + + // Init vehicle status. + loggingTableModel.clear(); + for (UserNotification notification : vehicleEntry.getProcessModel().getNotifications()) { + loggingTableModel.addRow(notification); + } + initVehicleEntryAttributes(); + + // Update panel contents. + validate(); + tabbedPane.setSelectedIndex(0); + } + + /** + * Detaches this panel from a vehicle (if it is currently attached to any). + */ + private void detachFromVehicle() { + if (vehicleEntry != null) { + // Remove all custom panels and let the comm adapter know we don't need them any more. + removeCustomPanels(); + if (attachmentInfo != null) { + attachmentInfo = null; + } + customPanelList.clear(); + vehicleEntry = null; + } + // Clear the log message table. + loggingTableModel.clear(); + setBorderTitle(DEFAULT_BORDER_TITLE); + // Remove the contents of this panel. + removeAll(); + add(noVehiclePanel); + // Update panel contents. + validate(); + } + + private void initVehicleEntryAttributes() { + updateCommAdapter(vehicleEntry.getAttachmentInformation()); + updateCommAdapterEnabled(vehicleEntry.getProcessModel().isCommAdapterEnabled()); + updateVehiclePosition(vehicleEntry.getProcessModel().getPosition()); + updateVehicleState(vehicleEntry.getProcessModel().getState()); + } + + private void updateFromVehicleEntry(VehicleAttachmentEvent evt) { + updateCommAdapter(evt.getAttachmentInformation()); + } + + private void updateFromVehicleProcessModel(ProcessModelEvent evt) { + if (Objects.equals( + evt.getAttributeChanged(), + VehicleProcessModel.Attribute.COMM_ADAPTER_ENABLED.name() + )) { + updateCommAdapterEnabled(evt.getUpdatedProcessModel().isCommAdapterEnabled()); + } + else if (Objects.equals( + evt.getAttributeChanged(), + VehicleProcessModel.Attribute.POSITION.name() + )) { + updateVehiclePosition(evt.getUpdatedProcessModel().getPosition()); + } + else if (Objects.equals( + evt.getAttributeChanged(), + VehicleProcessModel.Attribute.STATE.name() + )) { + updateVehicleState(evt.getUpdatedProcessModel().getState()); + } + else if (Objects.equals( + evt.getAttributeChanged(), + VehicleProcessModel.Attribute.USER_NOTIFICATION.name() + )) { + updateUserNotification(evt.getUpdatedProcessModel().getNotifications()); + } + } + + private void updateCommAdapter(VehicleAttachmentInformation newAttachmentInfo) { + SwingUtilities.invokeLater(() -> { + // If there was a comm adapter and it changed, we need to clean up a few things first. + if (attachmentInfo != null) { + // Detach all custom panels of the old comm adapter. + removeCustomPanels(); + customPanelList.clear(); + } + // Update the comm adapter reference. + attachmentInfo = newAttachmentInfo; + // If we have a new comm adapter, set up a few things. + if (attachmentInfo != null) { + // Update the custom panels displayed. + updateCustomPanels(); + } + chkBoxEnable.setEnabled(attachmentInfo != null); + chkBoxEnable.setSelected( + attachmentInfo != null + && vehicleEntry.getProcessModel().isCommAdapterEnabled() + ); + }); + } + + private void updateCommAdapterEnabled(boolean enabled) { + SwingUtilities.invokeLater(() -> chkBoxEnable.setSelected(enabled)); + } + + private void updateVehiclePosition(String position) { + SwingUtilities.invokeLater(() -> curPosTxt.setText(position)); + } + + private void updateVehicleState(Vehicle.State state) { + SwingUtilities.invokeLater(() -> curStateTxt.setText(state.toString())); + } + + private void updateUserNotification(Queue notifications) { + SwingUtilities.invokeLater(() -> { + loggingTableModel.clear(); + notifications.forEach(notification -> loggingTableModel.addRow(notification)); + }); + } + + /** + * Update the list of custom panels in the tabbed pane. + */ + private void updateCustomPanels() { + for (VehicleCommAdapterPanel curPanel : customPanelList) { + LOG.debug("Removing {} from tabbedPane.", curPanel); + tabbedPane.remove(curPanel); + } + customPanelList.clear(); + if (attachmentInfo != null) { + for (VehicleCommAdapterPanelFactory panelFactory : panelFactories) { + customPanelList.addAll( + panelFactory.getPanelsFor( + vehicleEntry.getAttachedCommAdapterDescription(), + vehicleEntry.getAttachmentInformation().getVehicleReference(), + vehicleEntry.getProcessModel() + ) + ); + for (VehicleCommAdapterPanel curPanel : customPanelList) { + LOG.debug("Adding {} with title {} to tabbedPane.", curPanel, curPanel.getTitle()); + tabbedPane.addTab(curPanel.getTitle(), curPanel); + } + } + } + } + + /** + * Removes the custom panels from this panel's tabbed pane. + */ + private void removeCustomPanels() { + LOG.debug("Setting selected component of tabbedPane to overviewTabPanel."); + tabbedPane.setSelectedComponent(overviewTabPanel); + for (VehicleCommAdapterPanel panel : customPanelList) { + LOG.debug("Removing {} from tabbedPane.", panel); + tabbedPane.remove(panel); + } + } + + /** + * Sets this panel's border title. + * + * @param newTitle This panel's new border title. + */ + private void setBorderTitle(String newTitle) { + requireNonNull(newTitle, "newTitle"); + ((TitledBorder) getBorder()).setTitle(newTitle); + // Trigger a repaint - the title sometimes looks strange otherwise. + repaint(); + } + + /** + * This method appends the selected notification to the text area in the log tab. + * + * @param row The selected row in the table. + */ + private void outputLogNotification(int row) { + UserNotification message = loggingTableModel.getRow(row); + String timestamp = DATE_FORMAT.format(message.getTimestamp()); + String output = timestamp + " (" + message.getLevel() + "):\n" + message.getText(); + loggingTextArea.setText(output); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + logPopupMenu = new javax.swing.JPopupMenu(); + clearMenuItem = new javax.swing.JMenuItem(); + loggingTablePopupMenu = new javax.swing.JPopupMenu(); + filterMenu = new javax.swing.JMenu(); + everythingCheckBoxMenuItem = new javax.swing.JCheckBoxMenuItem(); + warningsCheckBoxMenuItem = new javax.swing.JCheckBoxMenuItem(); + errorsCheckBoxMenuItem = new javax.swing.JCheckBoxMenuItem(); + noVehiclePanel = new javax.swing.JPanel(); + noVehicleLabel = new javax.swing.JLabel(); + tabbedPane = new javax.swing.JTabbedPane(); + overviewTabPanel = new javax.swing.JPanel(); + headPanel = new javax.swing.JPanel(); + statusPanel = new javax.swing.JPanel(); + adapterStatusPanel = new javax.swing.JPanel(); + chkBoxEnable = new javax.swing.JCheckBox(); + statusFiguresPanel = new javax.swing.JPanel(); + curPosLbl = new javax.swing.JLabel(); + curPosTxt = new javax.swing.JTextField(); + curStateLbl = new javax.swing.JLabel(); + curStateTxt = new javax.swing.JTextField(); + fillingLbl = new javax.swing.JLabel(); + logoPanel = new javax.swing.JPanel(); + logoLbl = new javax.swing.JLabel(); + logPanel = new javax.swing.JPanel(); + logTableScrollPane = new javax.swing.JScrollPane(); + loggingTable = new javax.swing.JTable(); + logTextScrollPane = new javax.swing.JScrollPane(); + loggingTextArea = new javax.swing.JTextArea(); + + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/kernelcontrolcenter/Bundle"); // NOI18N + clearMenuItem.setText(bundle.getString("detailPanel.popupMenu_messageDetails.menuItem_clear.text")); // NOI18N + clearMenuItem.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + clearMenuItemActionPerformed(evt); + } + }); + logPopupMenu.add(clearMenuItem); + + filterMenu.setText(bundle.getString("detailPanel.popupMenu_messagesTable.subMenu_filter.text")); // NOI18N + filterMenu.setActionCommand(" message filtering"); + + everythingCheckBoxMenuItem.setText(bundle.getString("detailPanel.popupMenu_messagesTable.subMenu_filter.menuItem_showAll.text")); // NOI18N + everythingCheckBoxMenuItem.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + everythingCheckBoxMenuItemActionPerformed(evt); + } + }); + filterMenu.add(everythingCheckBoxMenuItem); + + warningsCheckBoxMenuItem.setText(bundle.getString("detailPanel.popupMenu_messagesTable.subMenu_filter.menuItem_showErrorsAndWarnings.text")); // NOI18N + warningsCheckBoxMenuItem.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + warningsCheckBoxMenuItemActionPerformed(evt); + } + }); + filterMenu.add(warningsCheckBoxMenuItem); + + errorsCheckBoxMenuItem.setText(bundle.getString("detailPanel.popupMenu_messagesTable.subMenu_filter.menuItem_showErrors.text")); // NOI18N + errorsCheckBoxMenuItem.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + errorsCheckBoxMenuItemActionPerformed(evt); + } + }); + filterMenu.add(errorsCheckBoxMenuItem); + + loggingTablePopupMenu.add(filterMenu); + + noVehiclePanel.setLayout(new java.awt.BorderLayout()); + + noVehicleLabel.setFont(noVehicleLabel.getFont().deriveFont(noVehicleLabel.getFont().getSize()+3f)); + noVehicleLabel.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + noVehicleLabel.setText(bundle.getString("detailPanel.label_noVehicleAttached.text")); // NOI18N + noVehiclePanel.add(noVehicleLabel, java.awt.BorderLayout.CENTER); + + setBorder(javax.swing.BorderFactory.createTitledBorder(DEFAULT_BORDER_TITLE)); + setLayout(new java.awt.BorderLayout()); + + tabbedPane.setTabLayoutPolicy(javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT); + + overviewTabPanel.setLayout(new java.awt.GridBagLayout()); + + headPanel.setLayout(new java.awt.BorderLayout()); + + statusPanel.setLayout(new javax.swing.BoxLayout(statusPanel, javax.swing.BoxLayout.LINE_AXIS)); + + adapterStatusPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("detailPanel.panel_adapterStatus.border.title"))); // NOI18N + adapterStatusPanel.setLayout(new java.awt.BorderLayout()); + + chkBoxEnable.setText(bundle.getString("detailPanel.checkBox_enableAdapter.text")); // NOI18N + chkBoxEnable.setEnabled(false); + chkBoxEnable.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkBoxEnableActionPerformed(evt); + } + }); + adapterStatusPanel.add(chkBoxEnable, java.awt.BorderLayout.CENTER); + + statusPanel.add(adapterStatusPanel); + + statusFiguresPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("detailPanel.panel_vehicleStatus.border.title"))); // NOI18N + statusFiguresPanel.setLayout(new java.awt.GridBagLayout()); + + curPosLbl.setText(bundle.getString("detailPanel.label_currentPosition.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + statusFiguresPanel.add(curPosLbl, gridBagConstraints); + + curPosTxt.setEditable(false); + curPosTxt.setColumns(9); + curPosTxt.setHorizontalAlignment(javax.swing.JTextField.RIGHT); + curPosTxt.setText("Point-0001"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + statusFiguresPanel.add(curPosTxt, gridBagConstraints); + + curStateLbl.setText(bundle.getString("detailPanel.label_currentState.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 0); + statusFiguresPanel.add(curStateLbl, gridBagConstraints); + + curStateTxt.setEditable(false); + curStateTxt.setColumns(9); + curStateTxt.setHorizontalAlignment(javax.swing.JTextField.RIGHT); + curStateTxt.setText(Vehicle.State.UNKNOWN.name()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 0); + statusFiguresPanel.add(curStateTxt, gridBagConstraints); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.weightx = 1.0; + statusFiguresPanel.add(fillingLbl, gridBagConstraints); + + statusPanel.add(statusFiguresPanel); + + headPanel.add(statusPanel, java.awt.BorderLayout.WEST); + + logoPanel.setBackground(new java.awt.Color(255, 255, 255)); + logoPanel.setLayout(new java.awt.BorderLayout()); + + logoLbl.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + logoLbl.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/opentcs/kernelcontrolcenter/res/logos/opentcs_logo.gif"))); // NOI18N + logoPanel.add(logoLbl, java.awt.BorderLayout.CENTER); + + headPanel.add(logoPanel, java.awt.BorderLayout.CENTER); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + overviewTabPanel.add(headPanel, gridBagConstraints); + + logPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("detailPanel.panel_messages.border.title"))); // NOI18N + logPanel.setPreferredSize(new java.awt.Dimension(468, 200)); + logPanel.setLayout(new java.awt.BorderLayout()); + + logTableScrollPane.setComponentPopupMenu(loggingTablePopupMenu); + + loggingTable.setModel(new javax.swing.table.DefaultTableModel( + new Object [][] { + + }, + new String [] { + "Time stamp", "Message" + } + ) { + boolean[] canEdit = new boolean [] { + false, false + }; + + public boolean isCellEditable(int rowIndex, int columnIndex) { + return canEdit [columnIndex]; + } + }); + loggingTable.setComponentPopupMenu(loggingTablePopupMenu); + loggingTable.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + logTableScrollPane.setViewportView(loggingTable); + + logPanel.add(logTableScrollPane, java.awt.BorderLayout.CENTER); + + loggingTextArea.setEditable(false); + loggingTextArea.setColumns(20); + loggingTextArea.setFont(new java.awt.Font("Courier New", 0, 12)); // NOI18N + loggingTextArea.setLineWrap(true); + loggingTextArea.setRows(3); + loggingTextArea.setComponentPopupMenu(logPopupMenu); + logTextScrollPane.setViewportView(loggingTextArea); + + logPanel.add(logTextScrollPane, java.awt.BorderLayout.SOUTH); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + overviewTabPanel.add(logPanel, gridBagConstraints); + + tabbedPane.addTab(bundle.getString("detailPanel.tab_generalStatus.text"), overviewTabPanel); // NOI18N + + add(tabbedPane, java.awt.BorderLayout.CENTER); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void warningsCheckBoxMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_warningsCheckBoxMenuItemActionPerformed + loggingTableModel.filterMessages( + (notification) -> notification.getLevel().equals(UserNotification.Level.IMPORTANT) + || notification.getLevel().equals(UserNotification.Level.NOTEWORTHY) + ); + warningsCheckBoxMenuItem.setSelected(true); + errorsCheckBoxMenuItem.setSelected(false); + everythingCheckBoxMenuItem.setSelected(false); + }//GEN-LAST:event_warningsCheckBoxMenuItemActionPerformed + + private void everythingCheckBoxMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_everythingCheckBoxMenuItemActionPerformed + loggingTableModel.filterMessages((notification) -> true); + everythingCheckBoxMenuItem.setSelected(true); + errorsCheckBoxMenuItem.setSelected(false); + warningsCheckBoxMenuItem.setSelected(false); + }//GEN-LAST:event_everythingCheckBoxMenuItemActionPerformed + + private void errorsCheckBoxMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_errorsCheckBoxMenuItemActionPerformed + loggingTableModel.filterMessages( + (notification) -> notification.getLevel().equals(UserNotification.Level.IMPORTANT) + ); + errorsCheckBoxMenuItem.setSelected(true); + everythingCheckBoxMenuItem.setSelected(false); + warningsCheckBoxMenuItem.setSelected(false); + }//GEN-LAST:event_errorsCheckBoxMenuItemActionPerformed + + private void clearMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_clearMenuItemActionPerformed + loggingTextArea.setText(""); + }//GEN-LAST:event_clearMenuItemActionPerformed + + private void chkBoxEnableActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chkBoxEnableActionPerformed + try { + if (chkBoxEnable.isSelected()) { + callWrapper.call( + () -> servicePortal.getVehicleService() + .enableCommAdapter(vehicleEntry.getAttachmentInformation().getVehicleReference()) + ); + } + else { + callWrapper.call( + () -> servicePortal.getVehicleService() + .disableCommAdapter(vehicleEntry.getAttachmentInformation().getVehicleReference()) + ); + } + } + catch (Exception ex) { + LOG.warn("Error enabling/disabling comm adapter for {}", vehicleEntry.getVehicleName(), ex); + } + }//GEN-LAST:event_chkBoxEnableActionPerformed + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel adapterStatusPanel; + private javax.swing.JCheckBox chkBoxEnable; + private javax.swing.JMenuItem clearMenuItem; + private javax.swing.JLabel curPosLbl; + private javax.swing.JTextField curPosTxt; + private javax.swing.JLabel curStateLbl; + private javax.swing.JTextField curStateTxt; + private javax.swing.JCheckBoxMenuItem errorsCheckBoxMenuItem; + private javax.swing.JCheckBoxMenuItem everythingCheckBoxMenuItem; + private javax.swing.JLabel fillingLbl; + private javax.swing.JMenu filterMenu; + private javax.swing.JPanel headPanel; + private javax.swing.JPanel logPanel; + private javax.swing.JPopupMenu logPopupMenu; + private javax.swing.JScrollPane logTableScrollPane; + private javax.swing.JScrollPane logTextScrollPane; + private javax.swing.JTable loggingTable; + private javax.swing.JPopupMenu loggingTablePopupMenu; + private javax.swing.JTextArea loggingTextArea; + private javax.swing.JLabel logoLbl; + private javax.swing.JPanel logoPanel; + private javax.swing.JLabel noVehicleLabel; + private javax.swing.JPanel noVehiclePanel; + private javax.swing.JPanel overviewTabPanel; + private javax.swing.JPanel statusFiguresPanel; + private javax.swing.JPanel statusPanel; + private javax.swing.JTabbedPane tabbedPane; + private javax.swing.JCheckBoxMenuItem warningsCheckBoxMenuItem; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON + + /** + * A ListSelectionListener for handling the logging table selection events. + */ + private final class RowListener + implements + ListSelectionListener { + + /** + * Creates a new instance. + */ + private RowListener() { + } + + @Override + public void valueChanged(ListSelectionEvent event) { + if (event.getValueIsAdjusting()) { + return; + } + if (loggingTable.getSelectedRow() >= 0) { + outputLogNotification(loggingTable.getSelectedRow()); + } + else { + loggingTextArea.setText(""); + } + } + } + +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/DriverGUI.form b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/DriverGUI.form new file mode 100644 index 0000000..442522b --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/DriverGUI.form @@ -0,0 +1,174 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/DriverGUI.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/DriverGUI.java new file mode 100644 index 0000000..0643329 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/DriverGUI.java @@ -0,0 +1,682 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.vehicles; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.kernelcontrolcenter.I18nKernelControlCenter.BUNDLE_PATH; +import static org.opentcs.util.Assertions.checkState; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.awt.EventQueue; +import java.awt.event.ActionEvent; +import java.awt.event.ItemEvent; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.stream.Collectors; +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.DefaultCellEditor; +import javax.swing.JComboBox; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.components.kernelcontrolcenter.ControlCenterPanel; +import org.opentcs.customizations.ServiceCallWrapper; +import org.opentcs.data.model.Point; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; +import org.opentcs.drivers.vehicle.commands.InitPositionCommand; +import org.opentcs.drivers.vehicle.management.VehicleAttachmentInformation; +import org.opentcs.drivers.vehicle.management.VehicleProcessModelTO; +import org.opentcs.kernelcontrolcenter.util.SingleCellEditor; +import org.opentcs.util.CallWrapper; +import org.opentcs.util.Comparators; +import org.opentcs.util.gui.BoundsPopupMenuListener; +import org.opentcs.util.gui.StringListCellRenderer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A panel containing all vehicles and detailed information. + */ +public class DriverGUI + extends + ControlCenterPanel { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(DriverGUI.class); + /** + * This instance's resource bundle. + */ + private final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_PATH); + /** + * The service portal to use for kernel interaction. + */ + private final KernelServicePortal servicePortal; + /** + * The call wrapper to use for service calls. + */ + private final CallWrapper callWrapper; + /** + * The pool of vehicle entries. + */ + private final LocalVehicleEntryPool vehicleEntryPool; + /** + * The detail panel to dispay when selecting a vehicle. + */ + private final DetailPanel detailPanel; + /** + * Whether this panel is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param servicePortal The service portal to use for kernel interaction. + * @param callWrapper The call wrapper to use for service calls. + * @param vehicleEntryPool The pool of vehicle entries. + * @param detailPanel The detail panel to display. + */ + @Inject + @SuppressWarnings("this-escape") + public DriverGUI( + @Nonnull + KernelServicePortal servicePortal, + @Nonnull + @ServiceCallWrapper + CallWrapper callWrapper, + @Nonnull + LocalVehicleEntryPool vehicleEntryPool, + @Nonnull + DetailPanel detailPanel + ) { + this.servicePortal = requireNonNull(servicePortal, "servicePortal"); + this.callWrapper = requireNonNull(callWrapper, "callWrapper"); + this.vehicleEntryPool = requireNonNull(vehicleEntryPool, "vehicleEntryPool"); + this.detailPanel = requireNonNull(detailPanel, "detailPanel"); + + initComponents(); + + vehicleTable.setDefaultRenderer( + VehicleCommAdapterDescription.class, + new VehicleCommAdapterFactoryTableCellRenderer() + ); + vehicleDetailPanel.add(detailPanel); + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void initialize() { + if (initialized) { + LOG.debug("Already initialized."); + return; + } + + // Verify that the kernel is in a state in which controlling vehicles is possible. + Kernel.State kernelState; + try { + kernelState = callWrapper.call(() -> servicePortal.getState()); + } + catch (Exception ex) { + LOG.warn("Error getting the kernel state", ex); + return; + } + checkState( + Kernel.State.OPERATING.equals(kernelState), + "Cannot work in kernel state %s", + kernelState + ); + + vehicleEntryPool.initialize(); + detailPanel.initialize(); + + EventQueue.invokeLater(() -> { + initVehicleList(); + }); + + initialized = true; + } + + @Override + public void terminate() { + if (!initialized) { + LOG.debug("Not initialized."); + return; + } + + detailPanel.terminate(); + vehicleEntryPool.terminate(); + initialized = false; + } + + private void initVehicleList() { + vehicleTable.setModel(new VehicleTableModel(servicePortal.getVehicleService(), callWrapper)); + VehicleTableModel model = (VehicleTableModel) vehicleTable.getModel(); + + vehicleEntryPool.getEntries().forEach((vehicleName, entry) -> { + model.addData(entry); + entry.addPropertyChangeListener(model); + }); + + vehicleTable.getComponentPopupMenu().setEnabled(!model.getVehicleEntries().isEmpty()); + + initAdapterComboBoxes(); + } + + /** + * Initializes the combo boxes with available adapters for every vehicle. + */ + private void initAdapterComboBoxes() { + SingleCellEditor adapterCellEditor = new SingleCellEditor(vehicleTable); + SingleCellEditor pointsCellEditor = new SingleCellEditor(vehicleTable); + + int index = 0; + for (LocalVehicleEntry entry : vehicleEntryPool.getEntries().values()) { + initCommAdaptersComboBox(entry, index, adapterCellEditor); + initPointsComboBox(index, pointsCellEditor); + index++; + } + + vehicleTable.getColumn(VehicleTableModel.adapterColumnIdentifier()) + .setCellEditor(adapterCellEditor); + vehicleTable.getColumn(VehicleTableModel.positionColumnIdentifier()) + .setCellEditor(pointsCellEditor); + } + + private void initCommAdaptersComboBox( + LocalVehicleEntry vehicleEntry, + int rowIndex, + SingleCellEditor adapterCellEditor + ) { + final CommAdapterComboBox comboBox = new CommAdapterComboBox(); + VehicleAttachmentInformation ai; + try { + ai = callWrapper.call( + () -> servicePortal.getVehicleService().fetchAttachmentInformation( + vehicleEntry.getAttachmentInformation().getVehicleReference() + ) + ); + } + catch (Exception ex) { + LOG.warn("Error fetching attachment information for {}", vehicleEntry.getVehicleName(), ex); + return; + } + ai.getAvailableCommAdapters().forEach(factory -> comboBox.addItem(factory)); + + // Set the selection to the attached comm adapter, (The vehicle is already attached to a comm + // adapter due to auto attachment on startup.) + comboBox.setSelectedItem(vehicleEntry.getAttachmentInformation().getAttachedCommAdapter()); + + comboBox.setRenderer(new AdapterFactoryCellRenderer()); + comboBox.addPopupMenuListener(new BoundsPopupMenuListener()); + comboBox.addItemListener((ItemEvent evt) -> { + if (evt.getStateChange() == ItemEvent.DESELECTED) { + return; + } + + // If we selected a comm adapter that's already attached, do nothing. + if (Objects.equals(evt.getItem(), vehicleEntry.getAttachedCommAdapterDescription())) { + LOG.debug("{} is already attached to: {}", vehicleEntry.getVehicleName(), evt.getItem()); + return; + } + + int reply = JOptionPane.showConfirmDialog( + null, + bundle.getString("driverGui.optionPane_driverChangeConfirmation.message"), + bundle.getString("driverGui.optionPane_driverChangeConfirmation.title"), + JOptionPane.YES_NO_OPTION + ); + if (reply == JOptionPane.NO_OPTION) { + return; + } + + VehicleCommAdapterDescription factory = comboBox.getSelectedItem(); + try { + callWrapper.call( + () -> servicePortal.getVehicleService().attachCommAdapter( + vehicleEntry.getAttachmentInformation().getVehicleReference(), factory + ) + ); + } + catch (Exception ex) { + LOG.warn( + "Error attaching adapter {} to vehicle {}", + factory, + vehicleEntry.getVehicleName(), + ex + ); + return; + } + LOG.info("Attaching comm adapter {} to {}", factory, vehicleEntry.getVehicleName()); + }); + adapterCellEditor.setEditorAt(rowIndex, new DefaultCellEditor(comboBox)); + + vehicleEntry.addPropertyChangeListener(comboBox); + } + + /** + * If a loopback adapter was chosen, this method initializes the combo boxes with positions the + * user can set the vehicle to. + * + * @param rowIndex An index indicating which row this combo box belongs to + * @param pointsCellEditor The SingleCellEditor containing the combo boxes. + */ + private void initPointsComboBox(int rowIndex, SingleCellEditor pointsCellEditor) { + final JComboBox pointComboBox = new JComboBox<>(); + + Set points; + try { + points = callWrapper.call(() -> servicePortal.getVehicleService().fetchObjects(Point.class)); + } + catch (Exception ex) { + LOG.warn("Error fetching points", ex); + return; + } + + points.stream().sorted(Comparators.objectsByName()) + .forEach(point -> pointComboBox.addItem(point)); + pointComboBox.setSelectedIndex(-1); + pointComboBox.setRenderer(new StringListCellRenderer<>(x -> x == null ? "" : x.getName())); + + pointComboBox.addItemListener((ItemEvent e) -> { + try { + if (e.getStateChange() == ItemEvent.SELECTED) { + Point newPoint = (Point) e.getItem(); + LocalVehicleEntry vehicleEntry = vehicleEntryPool.getEntryFor(getSelectedVehicleName()); + if (vehicleEntry.getAttachedCommAdapterDescription().isSimVehicleCommAdapter()) { + callWrapper.call( + () -> servicePortal.getVehicleService().sendCommAdapterCommand( + vehicleEntry.getAttachmentInformation().getVehicleReference(), + new InitPositionCommand(newPoint.getName()) + ) + ); + } + else { + LOG.debug( + "Vehicle {}: Not a simulation adapter -> not setting initial position.", + vehicleEntry.getVehicleName() + ); + } + } + } + catch (Exception ex) { + LOG.warn("Error sending init position command", ex); + } + }); + pointsCellEditor.setEditorAt(rowIndex, new DefaultCellEditor(pointComboBox)); + } + + private void enableAllCommAdapters() { + enableCommAdapters(vehicleEntryPool.getEntries().values()); + } + + private void enableSelectedCommAdapters() { + enableCommAdapters(getSelectedVehicleEntries()); + } + + private void enableCommAdapters(Collection selectedEntries) { + Collection entries = selectedEntries.stream() + .filter(entry -> !entry.getProcessModel().isCommAdapterEnabled()) + .collect(Collectors.toList()); + + try { + for (LocalVehicleEntry entry : entries) { + callWrapper.call( + () -> servicePortal.getVehicleService().enableCommAdapter( + entry.getAttachmentInformation().getVehicleReference() + ) + ); + } + } + catch (Exception ex) { + LOG.warn("Error enabling comm adapter, canceling", ex); + } + } + + private void disableAllCommAdapters() { + disableCommAdapters(vehicleEntryPool.getEntries().values()); + } + + private void disableSelectedCommAdapters() { + disableCommAdapters(getSelectedVehicleEntries()); + } + + private void disableCommAdapters(Collection selectedEntries) { + Collection entries = selectedEntries.stream() + .filter(entry -> entry.getProcessModel().isCommAdapterEnabled()) + .collect(Collectors.toList()); + + try { + for (LocalVehicleEntry entry : entries) { + callWrapper.call( + () -> servicePortal.getVehicleService().disableCommAdapter( + entry.getAttachmentInformation().getVehicleReference() + ) + ); + } + } + catch (Exception ex) { + LOG.warn("Error disabling comm adapter, canceling", ex); + } + } + + private String getSelectedVehicleName() { + VehicleTableModel model = (VehicleTableModel) vehicleTable.getModel(); + return model.getDataAt(vehicleTable.getSelectedRow()).getVehicleName(); + } + + private List getSelectedVehicleNames() { + List selectedVehicleNames = new ArrayList<>(); + VehicleTableModel model = (VehicleTableModel) vehicleTable.getModel(); + for (int selectedRow : vehicleTable.getSelectedRows()) { + String selectedVehicleName = model.getDataAt(selectedRow).getVehicleName(); + selectedVehicleNames.add(selectedVehicleName); + } + return selectedVehicleNames; + } + + private List getSelectedVehicleEntries() { + List selectedEntries = new ArrayList<>(); + for (String selectedVehicleName : getSelectedVehicleNames()) { + selectedEntries.add(vehicleEntryPool.getEntryFor(selectedVehicleName)); + } + return selectedEntries; + } + + private void createDriverMenu() { + driverMenu.removeAll(); + + // Collect all available comm adapters/factories + Set availableDescriptions = new HashSet<>(); + vehicleEntryPool.getEntries().forEach((vehicleName, entry) -> { + availableDescriptions.addAll(entry.getAttachmentInformation().getAvailableCommAdapters()); + }); + + for (VehicleCommAdapterDescription description : availableDescriptions) { + // If there's one vehicle not supported by this factory the selection can't be attached to it + boolean factorySupportsSelectedVehicles = getSelectedVehicleEntries().stream() + .map(entry -> entry.getAttachmentInformation().getAvailableCommAdapters()) + .allMatch(descriptions -> !Collections.disjoint(descriptions, availableDescriptions)); + + List vehiclesToAttach = new ArrayList<>(); + if (factorySupportsSelectedVehicles) { + vehiclesToAttach = getSelectedVehicleNames(); + } + + Action action = new AttachCommAdapterAction(vehiclesToAttach, description); + JMenuItem menuItem = driverMenu.add(action); + menuItem.setEnabled(!vehiclesToAttach.isEmpty()); + } + } + + private void createPopupMenu() { + // Find out how many vehicles (don't) have a driver attached. + StatesCounts stateCounts = getCommAdapterStateCountsFor(vehicleEntryPool.getEntries().values()); + enableAllMenuItem.setEnabled(stateCounts.disabledCount > 0); + disableAllMenuItem.setEnabled(stateCounts.enabledCount > 0); + + // Now do the same for those that are selected. + stateCounts = getCommAdapterStateCountsFor(getSelectedVehicleEntries()); + enableAllSelectedMenuItem.setEnabled(stateCounts.disabledCount > 0); + disableAllSelectedMenuItem.setEnabled(stateCounts.enabledCount > 0); + } + + private StatesCounts getCommAdapterStateCountsFor(Collection entries) { + StatesCounts stateCounts = new StatesCounts(); + for (LocalVehicleEntry entry : entries) { + VehicleProcessModelTO processModel = entry.getProcessModel(); + if (processModel.isCommAdapterEnabled()) { + stateCounts.enabledCount++; + } + else { + stateCounts.disabledCount++; + } + } + return stateCounts; + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + + vehicleListPopupMenu = new javax.swing.JPopupMenu(); + driverMenu = new javax.swing.JMenu(); + noDriversMenuItem = new javax.swing.JMenuItem(); + jSeparator1 = new javax.swing.JSeparator(); + enableAllMenuItem = new javax.swing.JMenuItem(); + enableAllSelectedMenuItem = new javax.swing.JMenuItem(); + jSeparator4 = new javax.swing.JSeparator(); + disableAllMenuItem = new javax.swing.JMenuItem(); + disableAllSelectedMenuItem = new javax.swing.JMenuItem(); + listDisplayPanel = new javax.swing.JPanel(); + jScrollPane1 = new javax.swing.JScrollPane(); + vehicleTable = new javax.swing.JTable(); + vehicleDetailPanel = new javax.swing.JPanel(); + + vehicleListPopupMenu.addPopupMenuListener(new javax.swing.event.PopupMenuListener() { + public void popupMenuCanceled(javax.swing.event.PopupMenuEvent evt) { + } + public void popupMenuWillBecomeInvisible(javax.swing.event.PopupMenuEvent evt) { + } + public void popupMenuWillBecomeVisible(javax.swing.event.PopupMenuEvent evt) { + vehicleListPopupMenuPopupMenuWillBecomeVisible(evt); + } + }); + + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/kernelcontrolcenter/Bundle"); // NOI18N + driverMenu.setText(bundle.getString("driverGui.popupMenu_vehicles.subMenu_driver.text")); // NOI18N + driverMenu.addMenuListener(new javax.swing.event.MenuListener() { + public void menuCanceled(javax.swing.event.MenuEvent evt) { + } + public void menuDeselected(javax.swing.event.MenuEvent evt) { + } + public void menuSelected(javax.swing.event.MenuEvent evt) { + driverMenuMenuSelected(evt); + } + }); + + noDriversMenuItem.setText("No drivers available."); + noDriversMenuItem.setEnabled(false); + driverMenu.add(noDriversMenuItem); + + vehicleListPopupMenu.add(driverMenu); + vehicleListPopupMenu.add(jSeparator1); + + enableAllMenuItem.setText(bundle.getString("driverGui.popupMenu_vehicles.menuItem_enableAll.text")); // NOI18N + enableAllMenuItem.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + enableAllMenuItemActionPerformed(evt); + } + }); + vehicleListPopupMenu.add(enableAllMenuItem); + + enableAllSelectedMenuItem.setText(bundle.getString("driverGui.popupMenu_vehicles.menuItem_enableSelected.text")); // NOI18N + enableAllSelectedMenuItem.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + enableAllSelectedMenuItemActionPerformed(evt); + } + }); + vehicleListPopupMenu.add(enableAllSelectedMenuItem); + vehicleListPopupMenu.add(jSeparator4); + + disableAllMenuItem.setText(bundle.getString("driverGui.popupMenu_vehicles.menuItem_disableAll.text")); // NOI18N + disableAllMenuItem.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + disableAllMenuItemActionPerformed(evt); + } + }); + vehicleListPopupMenu.add(disableAllMenuItem); + + disableAllSelectedMenuItem.setText(bundle.getString("driverGui.popupMenu_vehicles.menuItem_disableSelected.text")); // NOI18N + disableAllSelectedMenuItem.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + disableAllSelectedMenuItemActionPerformed(evt); + } + }); + vehicleListPopupMenu.add(disableAllSelectedMenuItem); + + setLayout(new javax.swing.BoxLayout(this, javax.swing.BoxLayout.X_AXIS)); + + listDisplayPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("driverGui.panel_vehicles.border.title"))); // NOI18N + listDisplayPanel.setMaximumSize(new java.awt.Dimension(464, 2147483647)); + listDisplayPanel.setMinimumSize(new java.awt.Dimension(464, 425)); + listDisplayPanel.setLayout(new java.awt.BorderLayout()); + + vehicleTable.setComponentPopupMenu(vehicleListPopupMenu); + vehicleTable.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + vehicleTableMouseClicked(evt); + } + }); + jScrollPane1.setViewportView(vehicleTable); + + listDisplayPanel.add(jScrollPane1, java.awt.BorderLayout.CENTER); + + add(listDisplayPanel); + + vehicleDetailPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("driverGui.panel_vehicleDetails.border.title"))); // NOI18N + vehicleDetailPanel.setPreferredSize(new java.awt.Dimension(800, 23)); + vehicleDetailPanel.setLayout(new java.awt.BorderLayout()); + add(vehicleDetailPanel); + vehicleDetailPanel.getAccessibleContext().setAccessibleName(bundle.getString("driverGui.panel_vehicleDetails.accessibleName")); // NOI18N + + getAccessibleContext().setAccessibleName(bundle.getString("driverGui.accessibleName")); // NOI18N + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void driverMenuMenuSelected(javax.swing.event.MenuEvent evt) {//GEN-FIRST:event_driverMenuMenuSelected + createDriverMenu(); + }//GEN-LAST:event_driverMenuMenuSelected + + private void enableAllMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_enableAllMenuItemActionPerformed + enableAllCommAdapters(); + }//GEN-LAST:event_enableAllMenuItemActionPerformed + + private void enableAllSelectedMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_enableAllSelectedMenuItemActionPerformed + enableSelectedCommAdapters(); + }//GEN-LAST:event_enableAllSelectedMenuItemActionPerformed + + private void disableAllMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_disableAllMenuItemActionPerformed + disableAllCommAdapters(); + }//GEN-LAST:event_disableAllMenuItemActionPerformed + + private void disableAllSelectedMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_disableAllSelectedMenuItemActionPerformed + disableSelectedCommAdapters(); + }//GEN-LAST:event_disableAllSelectedMenuItemActionPerformed + + private void vehicleListPopupMenuPopupMenuWillBecomeVisible( + javax.swing.event.PopupMenuEvent evt + ) {//GEN-FIRST:event_vehicleListPopupMenuPopupMenuWillBecomeVisible + createPopupMenu(); + }//GEN-LAST:event_vehicleListPopupMenuPopupMenuWillBecomeVisible + + private void vehicleTableMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_vehicleTableMouseClicked + if (evt.getClickCount() == 2) { + int index = vehicleTable.getSelectedRow(); + if (index >= 0) { + VehicleTableModel model = (VehicleTableModel) vehicleTable.getModel(); + LocalVehicleEntry clickedEntry = model.getDataAt(index); + DetailPanel detailPanel = (DetailPanel) vehicleDetailPanel.getComponent(0); + detailPanel.attachToVehicle(clickedEntry); + } + } + }//GEN-LAST:event_vehicleTableMouseClicked + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JMenuItem disableAllMenuItem; + private javax.swing.JMenuItem disableAllSelectedMenuItem; + private javax.swing.JMenu driverMenu; + private javax.swing.JMenuItem enableAllMenuItem; + private javax.swing.JMenuItem enableAllSelectedMenuItem; + private javax.swing.JScrollPane jScrollPane1; + private javax.swing.JSeparator jSeparator1; + private javax.swing.JSeparator jSeparator4; + private javax.swing.JPanel listDisplayPanel; + private javax.swing.JMenuItem noDriversMenuItem; + private javax.swing.JPanel vehicleDetailPanel; + private javax.swing.JPopupMenu vehicleListPopupMenu; + private javax.swing.JTable vehicleTable; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON + + /** + * Attaches adapters produced by a given factory to a set of vehicles when performed. + */ + private class AttachCommAdapterAction + extends + AbstractAction { + + /** + * The affected vehicles' entries. + */ + private final List vehicleNames; + /** + * The factory providing the communication adapter. + */ + private final VehicleCommAdapterDescription commAdapterDescription; + + /** + * Creates a new AttachCommAdapterAction. + * + * @param vehicleNames The affected vehicles' entries. + * @param commAdapterDescription The factory providing the communication adapter. + */ + private AttachCommAdapterAction( + List vehicleNames, + VehicleCommAdapterDescription commAdapterDescription + ) { + super(commAdapterDescription.getDescription()); + this.vehicleNames = requireNonNull(vehicleNames, "vehicleNames"); + this.commAdapterDescription = requireNonNull(commAdapterDescription, "factory"); + } + + @Override + public void actionPerformed(ActionEvent evt) { + for (String vehicleName : vehicleNames) { + servicePortal.getVehicleService().attachCommAdapter( + vehicleEntryPool.getEntryFor(vehicleName) + .getAttachmentInformation() + .getVehicleReference(), + commAdapterDescription + ); + } + } + } + + private class StatesCounts { + + private int enabledCount; + private int disabledCount; + + /** + * Creates a new instance. + */ + StatesCounts() { + } + } +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/LocalVehicleEntry.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/LocalVehicleEntry.java new file mode 100644 index 0000000..9b0d200 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/LocalVehicleEntry.java @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.vehicles; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; +import org.opentcs.drivers.vehicle.management.VehicleAttachmentInformation; +import org.opentcs.drivers.vehicle.management.VehicleProcessModelTO; + +/** + * An entry for a vehicle present in kernel with detailed information about its attachment state + * and latest process model. + */ +public class LocalVehicleEntry { + + /** + * Used for implementing property change events. + */ + @SuppressWarnings("this-escape") + private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); + /** + * Detailed information about the attachment state. + */ + private VehicleAttachmentInformation attachmentInformation; + /** + * The current process model to this entry. + */ + private VehicleProcessModelTO processModel; + + /** + * Creates a new instance. + * + * @param attachmentInformation Detailed information about the attachment state. + * @param processModel The current process model to this entry. + */ + public LocalVehicleEntry( + VehicleAttachmentInformation attachmentInformation, + VehicleProcessModelTO processModel + ) { + this.attachmentInformation = requireNonNull(attachmentInformation, "attachmentInformation"); + this.processModel = requireNonNull(processModel, "processModel"); + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + pcs.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + pcs.removePropertyChangeListener(listener); + } + + @Nonnull + public VehicleAttachmentInformation getAttachmentInformation() { + return attachmentInformation; + } + + @Nonnull + public VehicleProcessModelTO getProcessModel() { + return processModel; + } + + @Nonnull + public String getVehicleName() { + return attachmentInformation.getVehicleReference().getName(); + } + + @Nonnull + public VehicleCommAdapterDescription getAttachedCommAdapterDescription() { + return attachmentInformation.getAttachedCommAdapter(); + } + + public void setAttachmentInformation( + @Nonnull + VehicleAttachmentInformation attachmentInformation + ) { + VehicleAttachmentInformation oldAttachmentInformation = this.attachmentInformation; + this.attachmentInformation = requireNonNull(attachmentInformation, "attachmentInformation"); + + pcs.firePropertyChange( + Attribute.ATTACHMENT_INFORMATION.name(), + oldAttachmentInformation, + attachmentInformation + ); + } + + public void setProcessModel( + @Nonnull + VehicleProcessModelTO processModel + ) { + VehicleProcessModelTO oldProcessModel = this.processModel; + this.processModel = requireNonNull(processModel, "processModel"); + + pcs.firePropertyChange( + Attribute.PROCESS_MODEL.name(), + oldProcessModel, + processModel + ); + } + + /** + * Enum elements used as notification arguments to specify which argument changed. + */ + public enum Attribute { + /** + * Indicates a change of the process model reference. + */ + PROCESS_MODEL, + /** + * Indicates a change of the attachment information reference. + */ + ATTACHMENT_INFORMATION + } +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/LocalVehicleEntryPool.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/LocalVehicleEntryPool.java new file mode 100644 index 0000000..3803763 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/LocalVehicleEntryPool.java @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.vehicles; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.components.Lifecycle; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.customizations.ServiceCallWrapper; +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.management.ProcessModelEvent; +import org.opentcs.drivers.vehicle.management.VehicleAttachmentEvent; +import org.opentcs.drivers.vehicle.management.VehicleAttachmentInformation; +import org.opentcs.drivers.vehicle.management.VehicleProcessModelTO; +import org.opentcs.util.CallWrapper; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.event.EventSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides a pool of {@link LocalVehicleEntry}s with an entry for every {@link Vehicle} object in + * the kernel. + */ +public class LocalVehicleEntryPool + implements + EventHandler, + Lifecycle { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(LocalVehicleEntryPool.class); + /** + * The service portal to use for kernel interactions. + */ + private final KernelServicePortal servicePortal; + /** + * The call wrapper to use for service calls. + */ + private final CallWrapper callWrapper; + /** + * Where this instance registers for application events. + */ + private final EventSource eventSource; + /** + * The entries of this pool. + */ + private final Map entries = new TreeMap<>(); + /** + * Whether the pool is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param servicePortal The service portal to use for kernel interactions. + * @param callWrapper The call wrapper to use for service calls. + * @param eventSource Where this instance registers for application events. + */ + @Inject + public LocalVehicleEntryPool( + KernelServicePortal servicePortal, + @ServiceCallWrapper + CallWrapper callWrapper, + @ApplicationEventBus + EventSource eventSource + ) { + this.servicePortal = requireNonNull(servicePortal, "servicePortal"); + this.callWrapper = requireNonNull(callWrapper, "callWrapper"); + this.eventSource = requireNonNull(eventSource, "eventSource"); + } + + @Override + public void initialize() { + if (isInitialized()) { + LOG.debug("Already initialized."); + return; + } + + eventSource.subscribe(this); + + try { + + Set vehicles + = callWrapper.call(() -> servicePortal.getVehicleService().fetchObjects(Vehicle.class)); + for (Vehicle vehicle : vehicles) { + VehicleAttachmentInformation ai = callWrapper.call(() -> { + return servicePortal.getVehicleService() + .fetchAttachmentInformation(vehicle.getReference()); + }); + VehicleProcessModelTO processModel = callWrapper.call(() -> { + return servicePortal.getVehicleService().fetchProcessModel(vehicle.getReference()); + }); + LocalVehicleEntry entry = new LocalVehicleEntry(ai, processModel); + entries.put(vehicle.getName(), entry); + } + } + catch (Exception ex) { + LOG.warn("Error initializing local vehicle entry pool", ex); + entries.clear(); + return; + } + + LOG.debug("Initialized vehicle entry pool: {}", entries); + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + LOG.debug("Not initialized."); + return; + } + + eventSource.unsubscribe(this); + + entries.clear(); + initialized = false; + } + + @Override + public void onEvent(Object event) { + if (event instanceof ProcessModelEvent) { + ProcessModelEvent e = (ProcessModelEvent) event; + LocalVehicleEntry entry = getEntryFor(e.getUpdatedProcessModel().getName()); + if (entry == null) { + return; + } + entry.setProcessModel(e.getUpdatedProcessModel()); + } + else if (event instanceof VehicleAttachmentEvent) { + VehicleAttachmentEvent e = (VehicleAttachmentEvent) event; + LocalVehicleEntry entry = getEntryFor(e.getVehicleName()); + if (entry == null) { + return; + } + entry.setAttachmentInformation(e.getAttachmentInformation()); + } + } + + @Nonnull + public Map getEntries() { + return entries; + } + + @Nullable + public LocalVehicleEntry getEntryFor(String vehicleName) { + return vehicleName == null ? null : entries.get(vehicleName); + } +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/LogTableModel.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/LogTableModel.java new file mode 100644 index 0000000..0044d64 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/LogTableModel.java @@ -0,0 +1,177 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.vehicles; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.kernelcontrolcenter.I18nKernelControlCenter.BUNDLE_PATH; + +import java.text.DateFormat; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import javax.swing.table.AbstractTableModel; +import org.opentcs.data.notification.UserNotification; + +/** + * A table model for holding {@link UserNotification} instances in rows. + */ +final class LogTableModel + extends + AbstractTableModel { + + /** + * The column names. + */ + private static final String[] COLUMN_NAMES + = new String[]{ + ResourceBundle.getBundle(BUNDLE_PATH) + .getString("logTableModel.column_timeStamp.headerText"), + ResourceBundle.getBundle(BUNDLE_PATH) + .getString("logTableModel.column_message.headerText") + }; + /** + * The column classes. + */ + private static final Class[] COLUMN_CLASSES + = new Class[]{ + String.class, + String.class + }; + /** + * A {@link DateFormat} instance for formatting notifications' time stamps. + */ + private final DateTimeFormatter dateFormat = DateTimeFormatter + .ofLocalizedDateTime(FormatStyle.SHORT) + .withLocale(Locale.getDefault()) + .withZone(ZoneId.systemDefault()); + /** + * The buffer for holding the model data. + */ + private final Set values + = new TreeSet<>((n1, n2) -> n1.getTimestamp().compareTo(n2.getTimestamp())); + /** + * The actual data displayed in the table. + */ + private List filteredValues = new ArrayList<>(); + /** + * The predicate used for filtering table rows. + */ + private Predicate filterPredicate = (notification) -> true; + + /** + * Creates a new instance. + */ + LogTableModel() { + } + + @Override + public Object getValueAt(int row, int column) { + if (row < 0 || row >= filteredValues.size()) { + return null; + } + switch (column) { + case 0: + return dateFormat.format(filteredValues.get(row).getTimestamp()); + case 1: + return filteredValues.get(row).getText(); + default: + return new IllegalArgumentException("Column out of bounds."); + } + } + + /** + * Adds a row to the model, containing the given notification. + * + * @param notification The notification to be added in a new row. + */ + public void addRow(UserNotification notification) { + requireNonNull(notification, "notification"); + + values.add(notification); + updateFilteredValues(); + fireTableDataChanged(); + } + + /** + * Removes the given notification from the internal data set and from the current data model. + * + * @param notification The notification to be removed. + */ + public void removeRow(UserNotification notification) { + if (values.contains(notification)) { + values.remove(notification); + updateFilteredValues(); + fireTableDataChanged(); + } + } + + /** + * Removes all notifications from the model. + */ + public void clear() { + if (!values.isEmpty()) { + values.clear(); + updateFilteredValues(); + fireTableDataChanged(); + } + } + + @Override + public Class getColumnClass(int columnIndex) { + return COLUMN_CLASSES[columnIndex]; + } + + /** + * Returns the notification object representing the indexed row. + * + * @param row The row for which to fetch the notification object. + * @return The notification object representing the indexed row. + */ + public UserNotification getRow(int row) { + return filteredValues.get(row); + } + + /** + * Filters the notifications and shows only errors and warnings. + * + * @param predicate The predicate used for filtering table rows. + */ + public void filterMessages(Predicate predicate) { + this.filterPredicate = requireNonNull(predicate, "predicate"); + + updateFilteredValues(); + fireTableDataChanged(); + } + + @Override + public int getRowCount() { + return filteredValues.size(); + } + + @Override + public int getColumnCount() { + return COLUMN_NAMES.length; + } + + @Override + public String getColumnName(int columnIndex) { + try { + return COLUMN_NAMES[columnIndex]; + } + catch (ArrayIndexOutOfBoundsException exc) { + return "ERROR"; + } + } + + private void updateFilteredValues() { + filteredValues = values.stream().filter(filterPredicate).collect(Collectors.toList()); + } +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/VehicleCommAdapterFactoryTableCellRenderer.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/VehicleCommAdapterFactoryTableCellRenderer.java new file mode 100644 index 0000000..da48fb6 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/VehicleCommAdapterFactoryTableCellRenderer.java @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.vehicles; + +import java.awt.Component; +import javax.swing.JTable; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableCellRenderer; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; + +/** + * A {@link TableCellRenderer} for {@link VehicleCommAdapterDescription} instances. + * This class provides a representation of any VehicleCommAdapterDescription instance by writing + * its actual description on a JLabel. + */ +class VehicleCommAdapterFactoryTableCellRenderer + extends + DefaultTableCellRenderer { + + VehicleCommAdapterFactoryTableCellRenderer() { + } + + @Override + public Component getTableCellRendererComponent( + JTable table, + Object value, + boolean isSelected, + boolean hasFocus, + int row, + int column + ) + throws IllegalArgumentException { + + super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + + if (value == null) { + setText(""); + } + else if (value instanceof VehicleCommAdapterDescription) { + setText(((VehicleCommAdapterDescription) value).getDescription()); + } + else { + throw new IllegalArgumentException("value"); + } + return this; + } + +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/VehicleTableModel.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/VehicleTableModel.java new file mode 100644 index 0000000..5d31334 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/VehicleTableModel.java @@ -0,0 +1,319 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter.vehicles; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.kernelcontrolcenter.I18nKernelControlCenter.BUNDLE_PATH; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.ResourceBundle; +import javax.swing.SwingUtilities; +import javax.swing.table.AbstractTableModel; +import org.opentcs.components.kernel.services.VehicleService; +import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription; +import org.opentcs.drivers.vehicle.management.VehicleAttachmentInformation; +import org.opentcs.drivers.vehicle.management.VehicleProcessModelTO; +import org.opentcs.util.CallWrapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A model for displaying a list/table of vehicles in the kernel GUI. + */ +public class VehicleTableModel + extends + AbstractTableModel + implements + PropertyChangeListener { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(VehicleTableModel.class); + /** + * This class's resource bundle. + */ + private static final ResourceBundle BUNDLE = ResourceBundle.getBundle(BUNDLE_PATH); + /** + * The column names. + */ + private static final String[] COLUMN_NAMES + = new String[]{ + BUNDLE.getString("vehicleTableModel.column_vehicle.headerText"), + BUNDLE.getString("vehicleTableModel.column_state.headerText"), + BUNDLE.getString("vehicleTableModel.column_adapter.headerText"), + BUNDLE.getString("vehicleTableModel.column_enabled.headerText"), + BUNDLE.getString("vehicleTableModel.column_position.headerText") + }; + /** + * The column classes. + */ + private static final Class[] COLUMN_CLASSES + = new Class[]{ + String.class, + String.class, + VehicleCommAdapterDescription.class, + Boolean.class, + String.class + }; + /** + * The index of the column showing the vehicle name. + */ + private static final int VEHICLE_COLUMN = 0; + /** + * The index of the column showing the vehicle state. + */ + private static final int STATE_COLUMNN = 1; + /** + * The index of the column showing the associated adapter. + */ + private static final int ADAPTER_COLUMN = 2; + /** + * The index of the column showing the adapter's enabled state. + */ + private static final int ENABLED_COLUMN = 3; + /** + * The index of the column showing the vehicle's current position. + */ + private static final int POSITION_COLUMN = 4; + /** + * The identifier for the adapter column. + */ + private static final String ADAPTER_COLUMN_IDENTIFIER = COLUMN_NAMES[ADAPTER_COLUMN]; + /** + * The identifier for the position column. + */ + private static final String POSITION_COLUMN_IDENTIFIER = COLUMN_NAMES[POSITION_COLUMN]; + /** + * The vehicles we're controlling. + */ + private final List entries = new ArrayList<>(); + /** + * The vehicle service used for interactions. + */ + private final VehicleService vehicleService; + /** + * The call wrapper to use for service calls. + */ + private final CallWrapper callWrapper; + + /** + * Creates a new instance. + * + * @param vehicleService The vehicle service used for interactions. + * @param callWrapper The call wrapper to use for service calls. + */ + public VehicleTableModel( + VehicleService vehicleService, + CallWrapper callWrapper + ) { + this.vehicleService = requireNonNull(vehicleService, "vehicleService"); + this.callWrapper = requireNonNull(callWrapper, "callWrapper"); + } + + /** + * Returns the identifier for the adapter column. + * + * @return The identifier for the adapter column. + */ + public static String adapterColumnIdentifier() { + return ADAPTER_COLUMN_IDENTIFIER; + } + + /** + * Returns the identifier for the position column. + * + * @return The identifier for the position column. + */ + public static String positionColumnIdentifier() { + return POSITION_COLUMN_IDENTIFIER; + } + + /** + * Adds a new entry to this model. + * + * @param newEntry The new entry. + */ + public void addData(LocalVehicleEntry newEntry) { + entries.add(newEntry); + fireTableRowsInserted(entries.size(), entries.size()); + } + + /** + * Returns the vehicle entry at the given row. + * + * @param row The row. + * @return The entry at the given row. + */ + public LocalVehicleEntry getDataAt(int row) { + if (row >= 0) { + return entries.get(row); + } + else { + return null; + } + } + + @Override + public int getRowCount() { + return entries.size(); + } + + @Override + public int getColumnCount() { + return COLUMN_NAMES.length; + } + + @Override + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + LocalVehicleEntry entry = entries.get(rowIndex); + switch (columnIndex) { + case ADAPTER_COLUMN: + break; + case ENABLED_COLUMN: + setEnabledState((boolean) aValue, entry); + break; + case POSITION_COLUMN: + break; + default: + LOG.warn("Unhandled column index: {}", columnIndex); + } + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (rowIndex >= entries.size()) { + return null; + } + + LocalVehicleEntry entry = entries.get(rowIndex); + + switch (columnIndex) { + case VEHICLE_COLUMN: + return entry.getVehicleName(); + case STATE_COLUMNN: + return getVehicleState(entry); + case ADAPTER_COLUMN: + return entry.getAttachmentInformation().getAttachedCommAdapter(); + case ENABLED_COLUMN: + return entry.getProcessModel().isCommAdapterEnabled(); + case POSITION_COLUMN: + return entry.getProcessModel().getPosition(); + default: + LOG.warn("Unhandled column index: {}", columnIndex); + return "Invalid column index " + columnIndex; + } + } + + @Override + public String getColumnName(int columnIndex) { + try { + return COLUMN_NAMES[columnIndex]; + } + catch (ArrayIndexOutOfBoundsException exc) { + LOG.warn("Invalid columnIndex", exc); + return "Invalid column index " + columnIndex; + } + } + + @Override + public Class getColumnClass(int columnIndex) { + return COLUMN_CLASSES[columnIndex]; + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + switch (columnIndex) { + case ADAPTER_COLUMN: + return true; + case ENABLED_COLUMN: + return true; + case POSITION_COLUMN: + LocalVehicleEntry entry = entries.get(rowIndex); + return entry.getAttachedCommAdapterDescription().isSimVehicleCommAdapter() + && entry.getProcessModel().isCommAdapterEnabled(); + default: + return false; + } + } + + /** + * Returns a list containing the vehicle models associated with this model. + * + * @return A list containing the vehicle models associated with this model. + */ + public List getVehicleEntries() { + return entries; + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (!(evt.getSource() instanceof LocalVehicleEntry)) { + return; + } + + if (!isRelevantUpdate(evt)) { + return; + } + + LocalVehicleEntry entry = (LocalVehicleEntry) evt.getSource(); + for (int index = 0; index < entries.size(); index++) { + if (entry == entries.get(index)) { + int myIndex = index; + SwingUtilities.invokeLater(() -> fireTableRowsUpdated(myIndex, myIndex)); + } + } + } + + private void setEnabledState(boolean enabled, LocalVehicleEntry entry) { + try { + if (enabled) { + callWrapper.call( + () -> vehicleService.enableCommAdapter( + entry.getAttachmentInformation().getVehicleReference() + ) + ); + } + else { + callWrapper.call( + () -> vehicleService.disableCommAdapter( + entry.getAttachmentInformation().getVehicleReference() + ) + ); + } + } + catch (Exception ex) { + LOG.warn("Error enabling/disabling comm adapter for {}", entry.getVehicleName(), ex); + } + } + + private String getVehicleState(LocalVehicleEntry entry) { + return entry.getProcessModel().getState().name(); + } + + private boolean isRelevantUpdate(PropertyChangeEvent evt) { + if (Objects.equals( + evt.getPropertyName(), + LocalVehicleEntry.Attribute.ATTACHMENT_INFORMATION.name() + )) { + VehicleAttachmentInformation oldInfo = (VehicleAttachmentInformation) evt.getOldValue(); + VehicleAttachmentInformation newInfo = (VehicleAttachmentInformation) evt.getNewValue(); + return !oldInfo.getAttachedCommAdapter().equals(newInfo.getAttachedCommAdapter()); + } + if (Objects.equals( + evt.getPropertyName(), + LocalVehicleEntry.Attribute.PROCESS_MODEL.name() + )) { + VehicleProcessModelTO oldTo = (VehicleProcessModelTO) evt.getOldValue(); + VehicleProcessModelTO newTo = (VehicleProcessModelTO) evt.getNewValue(); + return oldTo.isCommAdapterEnabled() != newTo.isCommAdapterEnabled() + || oldTo.getState() != newTo.getState() + || !Objects.equals(oldTo.getPosition(), newTo.getPosition()); + } + return false; + } +} diff --git a/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/package-info.java b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/package-info.java new file mode 100644 index 0000000..e119be0 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/java/org/opentcs/kernelcontrolcenter/vehicles/package-info.java @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * Classes of the kernel control center concerning vehicles/vehicle drivers. + * The GUI parts of the driver framework. + */ +package org.opentcs.kernelcontrolcenter.vehicles; diff --git a/opentcs-kernelcontrolcenter/src/main/resources/REUSE.toml b/opentcs-kernelcontrolcenter/src/main/resources/REUSE.toml new file mode 100644 index 0000000..42aab58 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/resources/REUSE.toml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC0-1.0 + +version = 1 + +[[annotations]] +path = ["**/*.gif", "**/*.jpg", "**/*.png", "**/*.svg"] +precedence = "closest" +SPDX-FileCopyrightText = "The openTCS Authors" +SPDX-License-Identifier = "CC-BY-4.0" diff --git a/opentcs-kernelcontrolcenter/src/main/resources/i18n/org/opentcs/kernelcontrolcenter/Bundle.properties b/opentcs-kernelcontrolcenter/src/main/resources/i18n/org/opentcs/kernelcontrolcenter/Bundle.properties new file mode 100644 index 0000000..96faa72 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/resources/i18n/org/opentcs/kernelcontrolcenter/Bundle.properties @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +aboutDialog.button_close.text=Close +aboutDialog.label_baselineVersion.text=Baseline version: +aboutDialog.label_customVersion.text=Custom version: +aboutDialog.label_email.text=E-mail: +aboutDialog.label_fraunhoferIml.text=Maintainer:
Fraunhofer Institute for
Material Flow and Logistics (IML)
+aboutDialog.label_homepage.text=Home page: +aboutDialog.title=About openTCS +defaultServiceCallWrapper.optionPane_retryConfirmation.message=Do you want to retry the last service call? +defaultServiceCallWrapper.optionPane_retryConfirmation.title=Retry service call? +detailPanel.checkBox_enableAdapter.text=Enable adapter +detailPanel.label_currentPosition.text=Current position: +detailPanel.label_currentState.text=Current state: +detailPanel.label_noVehicleAttached.text=No vehicle attached to this panel.
Doubleclick on a vehicle to display it here. +detailPanel.panel_adapterStatus.border.title=Adapter status +detailPanel.panel_messages.border.title=Messages +detailPanel.panel_vehicleStatus.border.title=Vehicle status +detailPanel.popupMenu_messageDetails.menuItem_clear.text=Clear log message text +detailPanel.popupMenu_messagesTable.subMenu_filter.menuItem_showAll.text=Show all messages +detailPanel.popupMenu_messagesTable.subMenu_filter.menuItem_showErrors.text=Show errors only +detailPanel.popupMenu_messagesTable.subMenu_filter.menuItem_showErrorsAndWarnings.text=Show errors and warnings only +detailPanel.popupMenu_messagesTable.subMenu_filter.text=Filter messages... +detailPanel.tab_generalStatus.text=General status +driverGui.accessibleName=Vehicle driver +driverGui.optionPane_driverChangeConfirmation.message=Do you really want to change the associated driver?
(This will reset the vehicle's resource allocations.) +driverGui.optionPane_driverChangeConfirmation.title=Really change the driver? +driverGui.panel_vehicleDetails.accessibleName=Vehicle details +driverGui.panel_vehicleDetails.border.title=Vehicle details +driverGui.panel_vehicles.border.title=Vehicles in model +driverGui.popupMenu_vehicles.menuItem_disableAll.text=Disable all +driverGui.popupMenu_vehicles.menuItem_disableSelected.text=Disable selected +driverGui.popupMenu_vehicles.menuItem_enableAll.text=Enable all +driverGui.popupMenu_vehicles.menuItem_enableSelected.text=Enable selected +driverGui.popupMenu_vehicles.subMenu_driver.text=Driver +kernelControlCenter.checkBox_autoScroll.text=Enable automatic scrolling (i.e. always show youngest messages) +kernelControlCenter.menu_about.text=About openTCS +kernelControlCenter.menu_help.text=Help +kernelControlCenter.menu_kernel.menuItem_connect.text=Connect to kernel +kernelControlCenter.menu_kernel.menuItem_disconnect.text=Disconnect from kernel +kernelControlCenter.menu_kernel.menuItem_exit.text=Exit +kernelControlCenter.tab_logging.title=Logging +kernelControlCenter.title.connectedTo=Connected to: +kernelControlCenter.title=Kernel Control Center +logTableModel.column_message.headerText=Message +logTableModel.column_timeStamp.headerText=Time stamp +peripheralDetailPanel.label_noPeripheralDeviceAttached.text=No peripheral device attached to this panel.
Doubleclick on a peripheral device to display it here. +peripheralTableModel.column_adapter.headerText=Adapter +peripheralTableModel.column_enabled.headerText=Enabled? +peripheralTableModel.column_location.headerText=Location +peripheralsPanel.accessibleName=Peripheral driver +peripheralsPanel.checkBox_hideDetachedLocations.text=Hide detached locations +peripheralsPanel.optionPane_driverChangeConfirmation.message=Do you really want to change the associated driver? +peripheralsPanel.optionPane_driverChangeConfirmation.title=Really change the driver? +vehicleTableModel.column_adapter.headerText=Adapter +vehicleTableModel.column_enabled.headerText=Enabled? +vehicleTableModel.column_position.headerText=Position +vehicleTableModel.column_state.headerText=State +vehicleTableModel.column_vehicle.headerText=Vehicle diff --git a/opentcs-kernelcontrolcenter/src/main/resources/i18n/org/opentcs/kernelcontrolcenter/Bundle_de.properties b/opentcs-kernelcontrolcenter/src/main/resources/i18n/org/opentcs/kernelcontrolcenter/Bundle_de.properties new file mode 100644 index 0000000..9608503 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/resources/i18n/org/opentcs/kernelcontrolcenter/Bundle_de.properties @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +aboutDialog.button_close.text=Schlie\u00dfen +aboutDialog.label_baselineVersion.text=Baseline-Version: +aboutDialog.label_customVersion.text=Custom-Version: +aboutDialog.label_email.text=E-Mail: +aboutDialog.label_fraunhoferIml.text=Maintainer:
Fraunhofer-Institut f\u00fcr
Materialfluss und Logistik (IML)
+aboutDialog.label_homepage.text=Homepage: +aboutDialog.title=\u00dcber openTCS +defaultServiceCallWrapper.optionPane_retryConfirmation.message=Wollen Sie den letzten Serviceaufruf erneut versuchen? +defaultServiceCallWrapper.optionPane_retryConfirmation.title=Erneuter Serviceaufruf? +detailPanel.checkBox_enableAdapter.text=Adapter einschalten +detailPanel.label_currentPosition.text=Aktuelle Position: +detailPanel.label_currentState.text=Aktueller Zustand: +detailPanel.label_noVehicleAttached.text=Kein Fahrzeug in diesem Fenster.
Klicken Sie ein Fahrzeug doppelt an, um es hier anzuzeigen. +detailPanel.panel_adapterStatus.border.title=Adapterzustand +detailPanel.panel_messages.border.title=Nachrichten +detailPanel.panel_vehicleStatus.border.title=Fahrzeugzustand +detailPanel.popupMenu_messageDetails.menuItem_clear.text=Nachrichtentext l\u00f6schen +detailPanel.popupMenu_messagesTable.subMenu_filter.menuItem_showAll.text=Alle Nachrichten anzeigen +detailPanel.popupMenu_messagesTable.subMenu_filter.menuItem_showErrors.text=Nur Fehler anzeigen +detailPanel.popupMenu_messagesTable.subMenu_filter.menuItem_showErrorsAndWarnings.text=Nur Fehler und Warnungen anzeigen +detailPanel.popupMenu_messagesTable.subMenu_filter.text=Nachrichten filtern... +detailPanel.tab_generalStatus.text=Zustand allgemein +driverGui.accessibleName=Fahrzeugtreiber +driverGui.optionPane_driverChangeConfirmation.message=Wollen Sie wirklich den zugewiesenen Treiber \u00e4ndern?
(Dies wird die Ressourcenzuweisungen des Fahrzeugs zur\u00fccksetzen.) +driverGui.optionPane_driverChangeConfirmation.title=Wirklich den Treiber \u00e4ndern? +driverGui.panel_vehicleDetails.border.title=Detailansicht +driverGui.panel_vehicles.border.title=Modellansicht +driverGui.popupMenu_vehicles.menuItem_disableAll.text=Alle Treiber ausschalten +driverGui.popupMenu_vehicles.menuItem_disableSelected.text=Ausgew\u00e4hlte Treiber ausschalten +driverGui.popupMenu_vehicles.menuItem_enableAll.text=Alle Treiber einschalten +driverGui.popupMenu_vehicles.menuItem_enableSelected.text=Ausgew\u00e4hlte Treiber einschalten +driverGui.popupMenu_vehicles.subMenu_driver.text=Treiber +kernelControlCenter.checkBox_autoScroll.text=Automatischen Bildlauf aktivieren (d.h. immer die j\u00fcngste Nachricht anzeigen) +kernelControlCenter.menu_about.text=\u00dcber openTCS +kernelControlCenter.menu_help.text=Hilfe +kernelControlCenter.menu_kernel.menuItem_connect.text=Mit Kernel verbinden +kernelControlCenter.menu_kernel.menuItem_disconnect.text=Von Kernel trennen +kernelControlCenter.menu_kernel.menuItem_exit.text=Beenden +kernelControlCenter.tab_logging.title=Protokollierung +kernelControlCenter.title.connectedTo=Verbunden mit: +kernelControlCenter.title=Kernel-Kontrollzentrum +logTableModel.column_message.headerText=Nachricht +logTableModel.column_timeStamp.headerText=Zeitstempel +peripheralDetailPanel.label_noPeripheralDeviceAttached.text=Kein Peripherieger\u00e4t in diesem Fenster.
Klicken Sie eine Station doppelt an, um sie hier anzuzeigen. +peripheralTableModel.column_adapter.headerText=Adapter +peripheralTableModel.column_enabled.headerText=Aktiviert? +peripheralTableModel.column_location.headerText=Station +peripheralsPanel.accessibleName=Peripherietreiber +peripheralsPanel.checkBox_hideDetachedLocations.text=Nicht zugewiesene Stationen ausblenden +peripheralsPanel.optionPane_driverChangeConfirmation.message=Wollen Sie wirklich den zugewiesenen Treiber \u00e4ndern? +peripheralsPanel.optionPane_driverChangeConfirmation.title=Wirklich den Treiber \u00e4ndern? +vehicleTableModel.column_adapter.headerText=Adapter +vehicleTableModel.column_enabled.headerText=Aktiviert? +vehicleTableModel.column_position.headerText=Position +vehicleTableModel.column_state.headerText=Zustand +vehicleTableModel.column_vehicle.headerText=Fahrzeug diff --git a/opentcs-kernelcontrolcenter/src/main/resources/org/opentcs/kernelcontrolcenter/distribution/config/opentcs-kernelcontrolcenter-defaults-baseline.properties b/opentcs-kernelcontrolcenter/src/main/resources/org/opentcs/kernelcontrolcenter/distribution/config/opentcs-kernelcontrolcenter-defaults-baseline.properties new file mode 100644 index 0000000..bcc23eb --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/main/resources/org/opentcs/kernelcontrolcenter/distribution/config/opentcs-kernelcontrolcenter-defaults-baseline.properties @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +# This file contains default configuration values and should not be modified. +# To adjust the application configuration, override values in a separate file. + +kernelcontrolcenter.locale = en +kernelcontrolcenter.connectionBookmarks = Localhost|localhost|1099 +kernelcontrolcenter.connectAutomaticallyOnStartup = true +kernelcontrolcenter.loggingAreaCapacity = 3000 +kernelcontrolcenter.enablePeripheralsPanel = true + +ssl.enable = false +ssl.truststoreFile = ./config/truststore.p12 +ssl.truststorePassword = password diff --git a/opentcs-kernelcontrolcenter/src/main/resources/org/opentcs/kernelcontrolcenter/res/logos/opentcs.gif b/opentcs-kernelcontrolcenter/src/main/resources/org/opentcs/kernelcontrolcenter/res/logos/opentcs.gif new file mode 100644 index 0000000..17a9130 Binary files /dev/null and b/opentcs-kernelcontrolcenter/src/main/resources/org/opentcs/kernelcontrolcenter/res/logos/opentcs.gif differ diff --git a/opentcs-kernelcontrolcenter/src/main/resources/org/opentcs/kernelcontrolcenter/res/logos/opentcs_logo.gif b/opentcs-kernelcontrolcenter/src/main/resources/org/opentcs/kernelcontrolcenter/res/logos/opentcs_logo.gif new file mode 100644 index 0000000..bc38a31 Binary files /dev/null and b/opentcs-kernelcontrolcenter/src/main/resources/org/opentcs/kernelcontrolcenter/res/logos/opentcs_logo.gif differ diff --git a/opentcs-kernelcontrolcenter/src/test/java/org/opentcs/kernelcontrolcenter/KernelControlCenterApplicationTest.java b/opentcs-kernelcontrolcenter/src/test/java/org/opentcs/kernelcontrolcenter/KernelControlCenterApplicationTest.java new file mode 100644 index 0000000..7e06ff1 --- /dev/null +++ b/opentcs-kernelcontrolcenter/src/test/java/org/opentcs/kernelcontrolcenter/KernelControlCenterApplicationTest.java @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.kernelcontrolcenter; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.common.PortalManager; +import org.opentcs.kernelcontrolcenter.exchange.KernelEventFetcher; +import org.opentcs.kernelcontrolcenter.util.KernelControlCenterConfiguration; +import org.opentcs.util.event.EventBus; + +/** + * Test cases for the {@link KernelControlCenterApplication}. + */ +class KernelControlCenterApplicationTest { + + /** + * The class to test. + */ + private KernelControlCenterApplication application; + + /** + * The event bus for online events. + */ + private EventBus eventBus; + + /** + * The portal manager to establish kernel connections. + */ + private PortalManager portalManager; + + /** + * The control center gui. + */ + private KernelControlCenter controlCenter; + + /** + * The configuration of the control center. + */ + private KernelControlCenterConfiguration configuration; + + @BeforeEach + void setUp() { + eventBus = mock(EventBus.class); + portalManager = mock(PortalManager.class); + configuration = mock(KernelControlCenterConfiguration.class); + controlCenter = mock(KernelControlCenter.class); + application = spy( + new KernelControlCenterApplication( + mock(KernelEventFetcher.class), + controlCenter, + portalManager, + eventBus, + configuration + ) + ); + } + + @Test + void onlyInitializeOnce() { + when(configuration.connectAutomaticallyOnStartup()).thenReturn(true); + application.initialize(); + application.initialize(); + + assertTrue(application.isInitialized()); + verify(controlCenter, times(1)).initialize(); + verify(portalManager, times(1)).connect(any()); + verify(application, times(1)).online(anyBoolean()); + } + + @Test + void onlyTerminateOnce() { + when(configuration.connectAutomaticallyOnStartup()).thenReturn(false); + application.initialize(); + application.terminate(); + application.terminate(); + + assertFalse(application.isInitialized()); + verify(controlCenter, times(1)).terminate(); + verify(application, times(1)).offline(); + } + + @Test + void shouldOnlyConnectOnce() { + //When trying to connect, return the value that indicates a successful connection + when(portalManager.connect(any())).thenReturn(true); + application.initialize(); + application.online(true); + application.online(true); + + assertTrue(application.isInitialized()); + assertTrue(application.isOnline()); + verify(controlCenter, times(1)).initialize(); + verify(portalManager, times(1)).connect(any()); + } +} diff --git a/opentcs-modeleditor/build.gradle b/opentcs-modeleditor/build.gradle new file mode 100644 index 0000000..36453e7 --- /dev/null +++ b/opentcs-modeleditor/build.gradle @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply from: "${rootDir}/gradle/java-project.gradle" +apply from: "${rootDir}/gradle/java-codequality.gradle" +apply from: "${rootDir}/gradle/guice-application.gradle" +apply from: "${rootDir}/gradle/publishing-java.gradle" + +if (!hasProperty('mainClass')) { + ext.mainClass = 'org.opentcs.modeleditor.RunModelEditor' +} +application.mainClass = ext.mainClass + +ext.collectableDistDir = new File(buildDir, 'install') + +dependencies { + api project(':opentcs-common') + api project(':opentcs-impl-configuration-gestalt') + api project(':opentcs-plantoverview-common') + api project(':opentcs-plantoverview-themes-default') + + runtimeOnly group: 'org.slf4j', name: 'slf4j-jdk14', version: '2.0.16' +} + +compileJava { + options.compilerArgs << "-Xlint:-rawtypes" +} + +distributions { + main { + contents { + from "${sourceSets.main.resources.srcDirs[0]}/org/opentcs/modeleditor/distribution" + } + } +} + +// For now, we're using hand-crafted start scripts, so disable the application +// plugin's start script generation. +startScripts.enabled = false + +distTar.enabled = false + +task release { + dependsOn build + dependsOn installDist +} + +run { + systemProperties(['java.util.logging.config.file':'./config/logging.config',\ + 'sun.java2d.d3d':'false',\ + 'opentcs.base':'.',\ + 'opentcs.home':'.',\ + 'opentcs.configuration.reload.interval':'10000',\ + 'opentcs.configuration.provider':'gestalt']) + jvmArgs('-XX:-OmitStackTraceInFastThrow',\ + '-splash:bin/splash-image.gif') +} diff --git a/opentcs-modeleditor/gradle.properties b/opentcs-modeleditor/gradle.properties new file mode 100644 index 0000000..14431ac --- /dev/null +++ b/opentcs-modeleditor/gradle.properties @@ -0,0 +1,40 @@ +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineMethodParams=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAfterDotInChainedMethodCalls=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineDisjunctiveCatchTypes=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineFor=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineImplements=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.sortMembersByVisibility=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.visibilityOrder=PUBLIC;PROTECTED;DEFAULT;PRIVATE +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.enable-indent=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineArrayInit=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineCallArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.keepGettersAndSettersTogether=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classMembersOrder=STATIC FIELD;FIELD;STATIC_INIT;CONSTRUCTOR;INSTANCE_INIT;STATIC METHOD;METHOD;STATIC CLASS;CLASS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapCommentText=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.importGroupsOrder=* +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize=4 +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineAnnotationArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineTryResources=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.preserveNewLinesInComments=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineParenthesized=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineThrows=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=100 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project diff --git a/opentcs-modeleditor/src/dist/bin/splash-image.gif b/opentcs-modeleditor/src/dist/bin/splash-image.gif new file mode 100644 index 0000000..9e6f131 Binary files /dev/null and b/opentcs-modeleditor/src/dist/bin/splash-image.gif differ diff --git a/opentcs-modeleditor/src/dist/bin/splash-image.gif.license b/opentcs-modeleditor/src/dist/bin/splash-image.gif.license new file mode 100644 index 0000000..777faa6 --- /dev/null +++ b/opentcs-modeleditor/src/dist/bin/splash-image.gif.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 diff --git a/opentcs-modeleditor/src/dist/config/logging.config b/opentcs-modeleditor/src/dist/config/logging.config new file mode 100644 index 0000000..8630ef1 --- /dev/null +++ b/opentcs-modeleditor/src/dist/config/logging.config @@ -0,0 +1,66 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +############################################################ +# Default Logging Configuration File +# +# You can use a different file by specifying a filename +# with the java.util.logging.config.file system property. +# For example java -Djava.util.logging.config.file=myfile +############################################################ + +############################################################ +# Global properties +############################################################ + +# "handlers" specifies a comma separated list of log Handler +# classes. These handlers will be installed during VM startup. +# Note that these classes must be on the system classpath. +# By default we only configure a ConsoleHandler, which will only +# show messages at the INFO and above levels. +#handlers= java.util.logging.ConsoleHandler + +# To also add the FileHandler, use the following line instead. +handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler + +# Default global logging level. +# This specifies which kinds of events are logged across +# all loggers. For any given facility this global level +# can be overriden by a facility specific level +# Note that the ConsoleHandler also has a separate level +# setting to limit messages printed to the console. +.level= INFO + +############################################################ +# Handler specific properties. +# Describes specific configuration info for Handlers. +############################################################ + +# default file output is in user's home directory. +java.util.logging.FileHandler.pattern = ./log/opentcs-modeleditor.%g.log +java.util.logging.FileHandler.limit = 500000 +java.util.logging.FileHandler.count = 10 +#java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.FileHandler.formatter = org.opentcs.util.logging.SingleLineFormatter +java.util.logging.FileHandler.append = true +java.util.logging.FileHandler.level = FINE + +# Limit the message that are printed on the console to INFO and above. +java.util.logging.ConsoleHandler.level = INFO +#java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.ConsoleHandler.formatter = org.opentcs.util.logging.SingleLineFormatter + +############################################################ +# Facility specific properties. +# Provides extra control for each logger. +############################################################ + +# For example, set the com.xyz.foo logger to only log SEVERE +# messages: +#com.xyz.foo.level = SEVERE + +# Logging configuration for single classes. Remember that you might also have to +# adjust handler levels! + +#org.opentcs.guing.*.level = FINE +#org.opentcs.access.rmi.ProxyInvocationHandler.level = FINE diff --git a/opentcs-modeleditor/src/dist/config/opentcs-modeleditor.properties b/opentcs-modeleditor/src/dist/config/opentcs-modeleditor.properties new file mode 100644 index 0000000..777faa6 --- /dev/null +++ b/opentcs-modeleditor/src/dist/config/opentcs-modeleditor.properties @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 diff --git a/opentcs-modeleditor/src/dist/data/Demo-01.xml b/opentcs-modeleditor/src/dist/data/Demo-01.xml new file mode 100644 index 0000000..691fe4a --- /dev/null +++ b/opentcs-modeleditor/src/dist/data/Demo-01.xml @@ -0,0 +1,810 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-modeleditor/src/dist/data/Demo-01.xml.license b/opentcs-modeleditor/src/dist/data/Demo-01.xml.license new file mode 100644 index 0000000..777faa6 --- /dev/null +++ b/opentcs-modeleditor/src/dist/data/Demo-01.xml.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 diff --git a/opentcs-modeleditor/src/dist/lib/openTCS-extensions/.keepme b/opentcs-modeleditor/src/dist/lib/openTCS-extensions/.keepme new file mode 100644 index 0000000..e69de29 diff --git a/opentcs-modeleditor/src/dist/log/.keepme b/opentcs-modeleditor/src/dist/log/.keepme new file mode 100644 index 0000000..e69de29 diff --git a/opentcs-modeleditor/src/dist/startModelEditor.bat b/opentcs-modeleditor/src/dist/startModelEditor.bat new file mode 100644 index 0000000..124e8fa --- /dev/null +++ b/opentcs-modeleditor/src/dist/startModelEditor.bat @@ -0,0 +1,38 @@ +@echo off +rem SPDX-FileCopyrightText: The openTCS Authors +rem SPDX-License-Identifier: MIT +rem +rem Start the openTCS Model Editor application. +rem + +rem Set window title +title Model Editor (openTCS) + +rem Don't export variables to the parent shell +setlocal + +rem Set base directory names. +set OPENTCS_BASE=. +set OPENTCS_HOME=. +set OPENTCS_CONFIGDIR=%OPENTCS_HOME%\config +set OPENTCS_LIBDIR=%OPENTCS_BASE%\lib + +rem Set the class path +set OPENTCS_CP=%OPENTCS_LIBDIR%\*; +set OPENTCS_CP=%OPENTCS_CP%;%OPENTCS_LIBDIR%\openTCS-extensions\*; + +rem XXX Be a bit more clever to find out the name of the JVM runtime. +set JAVA=javaw + +rem Start plant overview +start /b %JAVA% -enableassertions ^ + -Dopentcs.base="%OPENTCS_BASE%" ^ + -Dopentcs.home="%OPENTCS_HOME%" ^ + -Dopentcs.configuration.provider=gestalt ^ + -Dopentcs.configuration.reload.interval=10000 ^ + -Djava.util.logging.config.file="%OPENTCS_CONFIGDIR%\logging.config" ^ + -Dsun.java2d.d3d=false ^ + -XX:-OmitStackTraceInFastThrow ^ + -classpath "%OPENTCS_CP%" ^ + -splash:bin/splash-image.gif ^ + org.opentcs.modeleditor.RunModelEditor diff --git a/opentcs-modeleditor/src/dist/startModelEditor.sh b/opentcs-modeleditor/src/dist/startModelEditor.sh new file mode 100644 index 0000000..81c93f5 --- /dev/null +++ b/opentcs-modeleditor/src/dist/startModelEditor.sh @@ -0,0 +1,35 @@ +#!/bin/sh +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT +# +# Start the openTCS Model Editor application. +# + +# Set base directory names. +export OPENTCS_BASE=. +export OPENTCS_HOME=. +export OPENTCS_CONFIGDIR="${OPENTCS_HOME}/config" +export OPENTCS_LIBDIR="${OPENTCS_BASE}/lib" + +# Set the class path +export OPENTCS_CP="${OPENTCS_LIBDIR}/*" +export OPENTCS_CP="${OPENTCS_CP}:${OPENTCS_LIBDIR}/openTCS-extensions/*" + +if [ -n "${OPENTCS_JAVAVM}" ]; then + export JAVA="${OPENTCS_JAVAVM}" +else + # XXX Be a bit more clever to find out the name of the JVM runtime. + export JAVA="java" +fi + +# Start plant overview +${JAVA} -enableassertions \ + -Dopentcs.base="${OPENTCS_BASE}" \ + -Dopentcs.home="${OPENTCS_HOME}" \ + -Dopentcs.configuration.provider=gestalt \ + -Dopentcs.configuration.reload.interval=10000 \ + -Djava.util.logging.config.file=${OPENTCS_CONFIGDIR}/logging.config \ + -XX:-OmitStackTraceInFastThrow \ + -classpath "${OPENTCS_CP}" \ + -splash:bin/splash-image.gif \ + org.opentcs.modeleditor.RunModelEditor diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/DefaultImportersExportersModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/DefaultImportersExportersModule.java new file mode 100644 index 0000000..6440626 --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/DefaultImportersExportersModule.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor; + +import org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule; + +/** + * Configures/binds the default importers and exporters of the openTCS plant overview. + */ +public class DefaultImportersExportersModule + extends + PlantOverviewInjectionModule { + + /** + * Creates a new instance. + */ + public DefaultImportersExportersModule() { + } + + @Override + protected void configure() { + plantModelImporterBinder(); + plantModelExporterBinder(); + } +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/DefaultPlantOverviewInjectionModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/DefaultPlantOverviewInjectionModule.java new file mode 100644 index 0000000..886c561 --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/DefaultPlantOverviewInjectionModule.java @@ -0,0 +1,170 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor; + +import com.google.inject.TypeLiteral; +import jakarta.inject.Singleton; +import java.io.File; +import java.util.List; +import java.util.Locale; +import javax.swing.ToolTipManager; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.SslParameterSet; +import org.opentcs.access.rmi.KernelServicePortalBuilder; +import org.opentcs.access.rmi.factories.NullSocketFactoryProvider; +import org.opentcs.access.rmi.factories.SecureSocketFactoryProvider; +import org.opentcs.access.rmi.factories.SocketFactoryProvider; +import org.opentcs.common.GuestUserCredentials; +import org.opentcs.components.plantoverview.LocationTheme; +import org.opentcs.customizations.ApplicationHome; +import org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule; +import org.opentcs.drivers.LowLevelCommunicationEvent; +import org.opentcs.guing.common.exchange.ApplicationPortalProviderConfiguration; +import org.opentcs.guing.common.exchange.SslConfiguration; +import org.opentcs.modeleditor.application.ApplicationInjectionModule; +import org.opentcs.modeleditor.components.ComponentsInjectionModule; +import org.opentcs.modeleditor.exchange.ExchangeInjectionModule; +import org.opentcs.modeleditor.math.path.PathLengthFunctionInjectionModule; +import org.opentcs.modeleditor.model.ModelInjectionModule; +import org.opentcs.modeleditor.persistence.DefaultPersistenceInjectionModule; +import org.opentcs.modeleditor.transport.TransportInjectionModule; +import org.opentcs.modeleditor.util.ElementNamingSchemeConfiguration; +import org.opentcs.modeleditor.util.ModelEditorConfiguration; +import org.opentcs.modeleditor.util.UtilInjectionModule; +import org.opentcs.util.ClassMatcher; +import org.opentcs.util.gui.dialog.ConnectionParamSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A Guice module for the openTCS plant overview application. + */ +public class DefaultPlantOverviewInjectionModule + extends + PlantOverviewInjectionModule { + + /** + * This class's logger. + */ + private static final Logger LOG + = LoggerFactory.getLogger(DefaultPlantOverviewInjectionModule.class); + + /** + * Creates a new instance. + */ + public DefaultPlantOverviewInjectionModule() { + } + + @Override + protected void configure() { + File applicationHome = new File(System.getProperty("opentcs.home", ".")); + bind(File.class) + .annotatedWith(ApplicationHome.class) + .toInstance(applicationHome); + + configurePlantOverviewDependencies(); + install(new ApplicationInjectionModule()); + install(new ComponentsInjectionModule()); + install(new ExchangeInjectionModule()); + install(new PathLengthFunctionInjectionModule()); + install(new ModelInjectionModule()); + install(new DefaultPersistenceInjectionModule()); + install(new TransportInjectionModule()); + install(new UtilInjectionModule()); + + // Ensure there is at least an empty binder for pluggable panels. + pluggablePanelFactoryBinder(); + // Ensure there is at least an empty binder for history entry formatters. + objectHistoryEntryFormatterBinder(); + } + + private void configurePlantOverviewDependencies() { + ModelEditorConfiguration configuration + = getConfigBindingProvider().get( + ModelEditorConfiguration.PREFIX, + ModelEditorConfiguration.class + ); + bind(ApplicationPortalProviderConfiguration.class) + .toInstance(configuration); + bind(ModelEditorConfiguration.class) + .toInstance(configuration); + configurePlantOverview(configuration); + configureThemes(configuration); + configureSocketConnections(); + configureNamingConfiguration(); + + bind(new TypeLiteral>() { + }) + .toInstance(configuration.connectionBookmarks()); + } + + private void configureNamingConfiguration() { + ElementNamingSchemeConfiguration configuration + = getConfigBindingProvider().get( + ElementNamingSchemeConfiguration.PREFIX, + ElementNamingSchemeConfiguration.class + ); + bind(ElementNamingSchemeConfiguration.class) + .toInstance(configuration); + } + + private void configureSocketConnections() { + SslConfiguration sslConfiguration = getConfigBindingProvider().get( + SslConfiguration.PREFIX, + SslConfiguration.class + ); + + //Create the data object for the ssl configuration + SslParameterSet sslParamSet = new SslParameterSet( + SslParameterSet.DEFAULT_KEYSTORE_TYPE, + null, + null, + new File(sslConfiguration.truststoreFile()), + sslConfiguration.truststorePassword() + ); + bind(SslParameterSet.class).toInstance(sslParamSet); + + SocketFactoryProvider socketFactoryProvider; + if (sslConfiguration.enable()) { + socketFactoryProvider = new SecureSocketFactoryProvider(sslParamSet); + } + else { + LOG.warn("SSL encryption disabled, connections will not be secured!"); + socketFactoryProvider = new NullSocketFactoryProvider(); + } + + //Bind socket provider to the kernel portal + bind(KernelServicePortal.class) + .toInstance( + new KernelServicePortalBuilder( + GuestUserCredentials.USER, + GuestUserCredentials.PASSWORD + ) + .setSocketFactoryProvider(socketFactoryProvider) + .setEventFilter(new ClassMatcher(LowLevelCommunicationEvent.class).negate()) + .build() + ); + } + + private void configureThemes(ModelEditorConfiguration configuration) { + bind(LocationTheme.class) + .to(configuration.locationThemeClass()) + .in(Singleton.class); + } + + private void configurePlantOverview(ModelEditorConfiguration configuration) { + Locale.setDefault(Locale.forLanguageTag(configuration.locale())); + + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } + catch (ClassNotFoundException | InstantiationException | IllegalAccessException + | UnsupportedLookAndFeelException ex) { + LOG.warn("Could not set look-and-feel", ex); + } + // Show tooltips for 30 seconds (Default: 4 sec) + ToolTipManager.sharedInstance().setDismissDelay(30 * 1000); + } +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/DefaultPropertySuggestions.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/DefaultPropertySuggestions.java new file mode 100644 index 0000000..345989d --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/DefaultPropertySuggestions.java @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor; + +import java.util.HashSet; +import java.util.Set; +import org.opentcs.common.LoopbackAdapterConstants; +import org.opentcs.components.kernel.Dispatcher; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.components.plantoverview.PropertySuggestions; +import org.opentcs.data.ObjectPropConstants; + +/** + * The default property suggestions of the baseline plant overview. + */ +public class DefaultPropertySuggestions + implements + PropertySuggestions { + + private final Set keySuggestions = new HashSet<>(); + private final Set valueSuggestions = new HashSet<>(); + + /** + * Creates a new instance. + */ + public DefaultPropertySuggestions() { + keySuggestions.add(Scheduler.PROPKEY_BLOCK_ENTRY_DIRECTION); + keySuggestions.add(Router.PROPKEY_ROUTING_GROUP); + keySuggestions.add(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY); + keySuggestions.add(Dispatcher.PROPKEY_ASSIGNED_PARKING_POSITION); + keySuggestions.add(Dispatcher.PROPKEY_PREFERRED_PARKING_POSITION); + keySuggestions.add(Dispatcher.PROPKEY_ASSIGNED_RECHARGE_LOCATION); + keySuggestions.add(Dispatcher.PROPKEY_PREFERRED_RECHARGE_LOCATION); + keySuggestions.add(LoopbackAdapterConstants.PROPKEY_INITIAL_POSITION); + keySuggestions.add(LoopbackAdapterConstants.PROPKEY_OPERATING_TIME); + keySuggestions.add(LoopbackAdapterConstants.PROPKEY_LOAD_OPERATION); + keySuggestions.add(LoopbackAdapterConstants.PROPKEY_UNLOAD_OPERATION); + keySuggestions.add(LoopbackAdapterConstants.PROPKEY_ACCELERATION); + keySuggestions.add(LoopbackAdapterConstants.PROPKEY_DECELERATION); + keySuggestions.add(ObjectPropConstants.VEHICLE_DATA_TRANSFORMER); + } + + @Override + public Set getKeySuggestions() { + return keySuggestions; + } + + @Override + public Set getValueSuggestions() { + return valueSuggestions; + } + +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/PropertySuggestionsModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/PropertySuggestionsModule.java new file mode 100644 index 0000000..b92051f --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/PropertySuggestionsModule.java @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor; + +import jakarta.inject.Singleton; +import org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule; + +/** + * This module configures the multibinder used to suggest key value properties in the editor. + */ +public class PropertySuggestionsModule + extends + PlantOverviewInjectionModule { + + /** + * Creates a new instance. + */ + public PropertySuggestionsModule() { + } + + @Override + protected void configure() { + propertySuggestionsBinder().addBinding() + .to(DefaultPropertySuggestions.class) + .in(Singleton.class); + } +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/RunModelEditor.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/RunModelEditor.java new file mode 100644 index 0000000..d0e35c1 --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/RunModelEditor.java @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.util.Modules; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; +import org.opentcs.configuration.ConfigurationBindingProvider; +import org.opentcs.configuration.gestalt.GestaltConfigurationBindingProvider; +import org.opentcs.customizations.ConfigurableInjectionModule; +import org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule; +import org.opentcs.guing.common.util.CompatibilityChecker; +import org.opentcs.modeleditor.application.PlantOverviewStarter; +import org.opentcs.util.Environment; +import org.opentcs.util.logging.UncaughtExceptionLogger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The plant overview process's default entry point. + */ +public class RunModelEditor { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(RunModelEditor.class); + + /** + * Prevents external instantiation. + */ + private RunModelEditor() { + } + + /** + * The plant overview client's main entry point. + * + * @param args the command line arguments + */ + public static void main(final String[] args) { + Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionLogger(false)); + + Environment.logSystemInfo(); + ensureVersionCompatibility(); + + Injector injector = Guice.createInjector(customConfigurationModule()); + injector.getInstance(PlantOverviewStarter.class).startPlantOverview(); + } + + private static void ensureVersionCompatibility() { + String version = System.getProperty("java.version"); + if (!CompatibilityChecker.versionCompatibleWithDockingFrames(version)) { + LOG.error("Version incompatible with Docking Frames: '{}'", version); + CompatibilityChecker.showVersionIncompatibleWithDockingFramesMessage(); + System.exit(1); + } + } + + /** + * Builds and returns a Guice module containing the custom configuration for the plant overview + * application, including additions and overrides by the user. + * + * @return The custom configuration module. + */ + private static Module customConfigurationModule() { + ConfigurationBindingProvider bindingProvider = configurationBindingProvider(); + ConfigurableInjectionModule plantOverviewInjectionModule + = new DefaultPlantOverviewInjectionModule(); + plantOverviewInjectionModule.setConfigBindingProvider(bindingProvider); + return Modules.override(plantOverviewInjectionModule) + .with(findRegisteredModules(bindingProvider)); + } + + /** + * Finds and returns all Guice modules registered via ServiceLoader. + * + * @return The registered/found modules. + */ + private static List findRegisteredModules( + ConfigurationBindingProvider bindingProvider + ) { + List registeredModules = new ArrayList<>(); + for (PlantOverviewInjectionModule module : ServiceLoader.load( + PlantOverviewInjectionModule.class + )) { + LOG.info( + "Integrating injection module {} (source: {})", + module.getClass().getName(), + module.getClass().getProtectionDomain().getCodeSource() + ); + module.setConfigBindingProvider(bindingProvider); + registeredModules.add(module); + } + return registeredModules; + } + + private static ConfigurationBindingProvider configurationBindingProvider() { + String chosenProvider = System.getProperty("opentcs.configuration.provider", "gestalt"); + switch (chosenProvider) { + case "gestalt": + default: + LOG.info("Using gestalt as the configuration provider."); + return gestaltConfigurationBindingProvider(); + } + } + + private static ConfigurationBindingProvider gestaltConfigurationBindingProvider() { + return new GestaltConfigurationBindingProvider( + Paths.get( + System.getProperty("opentcs.base", "."), + "config", + "opentcs-modeleditor-defaults-baseline.properties" + ) + .toAbsolutePath(), + Paths.get( + System.getProperty("opentcs.base", "."), + "config", + "opentcs-modeleditor-defaults-custom.properties" + ) + .toAbsolutePath(), + Paths.get( + System.getProperty("opentcs.home", "."), + "config", + "opentcs-modeleditor.properties" + ) + .toAbsolutePath() + ); + } + +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/application/ApplicationInjectionModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/application/ApplicationInjectionModule.java new file mode 100644 index 0000000..e8652f0 --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/application/ApplicationInjectionModule.java @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application; + +import com.google.inject.AbstractModule; +import jakarta.inject.Singleton; +import java.awt.Component; +import javax.swing.JFrame; +import org.jhotdraw.app.Application; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.guing.common.application.ApplicationState; +import org.opentcs.guing.common.application.ComponentsManager; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.application.PluginPanelManager; +import org.opentcs.guing.common.application.ProgressIndicator; +import org.opentcs.guing.common.application.SplashFrame; +import org.opentcs.guing.common.application.StatusPanel; +import org.opentcs.guing.common.application.ViewManager; +import org.opentcs.modeleditor.application.action.ActionInjectionModule; +import org.opentcs.modeleditor.application.menus.MenusInjectionModule; +import org.opentcs.modeleditor.application.toolbar.ToolBarInjectionModule; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.UndoRedoManager; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.OpenTCSSDIApplication; + +/** + * An injection module for this package. + */ +public class ApplicationInjectionModule + extends + AbstractModule { + + /** + * The application's main frame. + */ + private final JFrame applicationFrame = new JFrame(); + + /** + * Creates a new instance. + */ + public ApplicationInjectionModule() { + } + + @Override + protected void configure() { + install(new ActionInjectionModule()); + install(new MenusInjectionModule()); + install(new ToolBarInjectionModule()); + + bind(ApplicationState.class).in(Singleton.class); + + bind(UndoRedoManager.class).in(Singleton.class); + + bind(ProgressIndicator.class) + .to(SplashFrame.class) + .in(Singleton.class); + bind(StatusPanel.class).in(Singleton.class); + + bind(JFrame.class) + .annotatedWith(ApplicationFrame.class) + .toInstance(applicationFrame); + bind(Component.class) + .annotatedWith(ApplicationFrame.class) + .toInstance(applicationFrame); + + bind(ViewManagerModeling.class) + .in(Singleton.class); + bind(ViewManager.class).to(ViewManagerModeling.class); + + bind(Application.class) + .to(OpenTCSSDIApplication.class) + .in(Singleton.class); + + bind(OpenTCSView.class).in(Singleton.class); + bind(GuiManager.class).to(OpenTCSView.class); + bind(ComponentsManager.class).to(OpenTCSView.class); + bind(PluginPanelManager.class).to(OpenTCSView.class); + } + +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/application/action/ActionInjectionModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/application/action/ActionInjectionModule.java new file mode 100644 index 0000000..94e5572 --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/application/action/ActionInjectionModule.java @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.action; + +import com.google.inject.AbstractModule; +import jakarta.inject.Singleton; + +/** + * An injection module for this package. + */ +public class ActionInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public ActionInjectionModule() { + } + + @Override + protected void configure() { + bind(ViewActionMap.class).in(Singleton.class); + } + +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/application/menus/MenusInjectionModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/application/menus/MenusInjectionModule.java new file mode 100644 index 0000000..b5c003b --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/application/menus/MenusInjectionModule.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.menus; + +import com.google.inject.AbstractModule; +import com.google.inject.assistedinject.FactoryModuleBuilder; + +/** + */ +public class MenusInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public MenusInjectionModule() { + } + + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(MenuFactory.class)); + } + +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/application/toolbar/ToolBarInjectionModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/application/toolbar/ToolBarInjectionModule.java new file mode 100644 index 0000000..4545780 --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/application/toolbar/ToolBarInjectionModule.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.toolbar; + +import com.google.inject.AbstractModule; +import com.google.inject.assistedinject.FactoryModuleBuilder; +import jakarta.inject.Singleton; +import org.jhotdraw.draw.tool.DragTracker; +import org.jhotdraw.draw.tool.SelectAreaTracker; +import org.opentcs.modeleditor.application.action.ToolBarManager; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.toolbar.OpenTCSDragTracker; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.toolbar.OpenTCSSelectAreaTracker; + +/** + */ +public class ToolBarInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public ToolBarInjectionModule() { + } + + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(SelectionToolFactory.class)); + install(new FactoryModuleBuilder().build(CreationToolFactory.class)); + + bind(ToolBarManager.class).in(Singleton.class); + + bind(SelectAreaTracker.class).to(OpenTCSSelectAreaTracker.class); + bind(DragTracker.class).to(OpenTCSDragTracker.class); + } + +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/ComponentsInjectionModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/ComponentsInjectionModule.java new file mode 100644 index 0000000..5d2ed4d --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/ComponentsInjectionModule.java @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components; + +import com.google.inject.AbstractModule; +import com.google.inject.PrivateModule; +import jakarta.inject.Singleton; +import java.awt.event.MouseListener; +import org.opentcs.guing.common.components.tree.AbstractTreeViewPanel; +import org.opentcs.guing.common.components.tree.BlockMouseListener; +import org.opentcs.guing.common.components.tree.BlocksTreeViewManager; +import org.opentcs.guing.common.components.tree.BlocksTreeViewPanel; +import org.opentcs.guing.common.components.tree.ComponentsTreeViewManager; +import org.opentcs.guing.common.components.tree.ComponentsTreeViewPanel; +import org.opentcs.guing.common.components.tree.TreeMouseAdapter; +import org.opentcs.guing.common.components.tree.TreeView; +import org.opentcs.modeleditor.components.dialogs.DialogsInjectionModule; +import org.opentcs.modeleditor.components.dockable.DockableInjectionModule; +import org.opentcs.modeleditor.components.drawing.DrawingInjectionModule; +import org.opentcs.modeleditor.components.layer.LayersInjectionModule; +import org.opentcs.modeleditor.components.properties.PropertiesInjectionModule; +import org.opentcs.modeleditor.components.tree.elements.TreeElementsInjectionModule; + +/** + * A Guice module for this package. + */ +public class ComponentsInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public ComponentsInjectionModule() { + } + + @Override + protected void configure() { + install(new DialogsInjectionModule()); + install(new DockableInjectionModule()); + install(new DrawingInjectionModule()); + install(new PropertiesInjectionModule()); + install(new TreeElementsInjectionModule()); + + install(new ComponentsTreeViewModule()); + install(new BlocksTreeViewModule()); + + install(new LayersInjectionModule()); + } + + private static class ComponentsTreeViewModule + extends + PrivateModule { + + ComponentsTreeViewModule() { + } + + @Override + protected void configure() { + // Within this (private) module, there should only be a single tree panel. + bind(ComponentsTreeViewPanel.class) + .in(Singleton.class); + + // Bind the tree panel annotated with the given annotation to our single + // instance and expose only this annotated version. + bind(AbstractTreeViewPanel.class) + .to(ComponentsTreeViewPanel.class); + expose(ComponentsTreeViewPanel.class); + + // Bind TreeView to the single tree panel, too. + bind(TreeView.class) + .to(ComponentsTreeViewPanel.class); + + // Bind and expose a single manager for the single tree view/panel. + bind(ComponentsTreeViewManager.class) + .in(Singleton.class); + expose(ComponentsTreeViewManager.class); + + bind(MouseListener.class) + .to(TreeMouseAdapter.class); + } + } + + private static class BlocksTreeViewModule + extends + PrivateModule { + + BlocksTreeViewModule() { + } + + @Override + protected void configure() { + // Within this (private) module, there should only be a single tree panel. + bind(BlocksTreeViewPanel.class) + .in(Singleton.class); + + // Bind the tree panel annotated with the given annotation to our single + // instance and expose only this annotated version. + bind(AbstractTreeViewPanel.class) + .to(BlocksTreeViewPanel.class); + expose(BlocksTreeViewPanel.class); + + // Bind TreeView to the single tree panel, too. + bind(TreeView.class) + .to(BlocksTreeViewPanel.class); + + // Bind and expose a single manager for the single tree view/panel. + bind(BlocksTreeViewManager.class) + .in(Singleton.class); + expose(BlocksTreeViewManager.class); + + bind(MouseListener.class) + .to(BlockMouseListener.class); + } + } +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/dialogs/DialogsInjectionModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/dialogs/DialogsInjectionModule.java new file mode 100644 index 0000000..5a56b54 --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/dialogs/DialogsInjectionModule.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.dialogs; + +import com.google.inject.AbstractModule; + +/** + * A Guice module for this package. + */ +public class DialogsInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public DialogsInjectionModule() { + } + + @Override + protected void configure() { + } +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/dockable/DockableInjectionModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/dockable/DockableInjectionModule.java new file mode 100644 index 0000000..d201a90 --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/dockable/DockableInjectionModule.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.dockable; + +import com.google.inject.AbstractModule; +import com.google.inject.assistedinject.FactoryModuleBuilder; +import jakarta.inject.Singleton; +import org.opentcs.guing.common.components.dockable.DockableHandlerFactory; +import org.opentcs.guing.common.components.dockable.DockingManager; + +/** + * A Guice module for this package. + */ +public class DockableInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public DockableInjectionModule() { + } + + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(DockableHandlerFactory.class)); + + bind(DockingManagerModeling.class).in(Singleton.class); + bind(DockingManager.class).to(DockingManagerModeling.class); + } +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/drawing/DrawingInjectionModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/drawing/DrawingInjectionModule.java new file mode 100644 index 0000000..501d182 --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/drawing/DrawingInjectionModule.java @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.drawing; + +import com.google.inject.AbstractModule; +import com.google.inject.assistedinject.FactoryModuleBuilder; +import jakarta.inject.Singleton; +import org.jhotdraw.draw.DrawingEditor; +import org.opentcs.guing.common.components.drawing.DrawingOptions; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingView; +import org.opentcs.guing.common.components.drawing.figures.FigureFactory; +import org.opentcs.thirdparty.modeleditor.jhotdraw.components.drawing.OpenTCSDrawingViewModeling; + +/** + * A Guice module for this package. + */ +public class DrawingInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public DrawingInjectionModule() { + } + + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(FigureFactory.class)); + + bind(OpenTCSDrawingEditor.class).in(Singleton.class); + bind(DrawingEditor.class).to(OpenTCSDrawingEditor.class); + + bind(OpenTCSDrawingView.class).to(OpenTCSDrawingViewModeling.class); + + bind(DrawingOptions.class).in(Singleton.class); + } +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/layer/LayersInjectionModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/layer/LayersInjectionModule.java new file mode 100644 index 0000000..4032793 --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/layer/LayersInjectionModule.java @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.layer; + +import jakarta.inject.Singleton; +import org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule; +import org.opentcs.guing.common.components.layer.LayerGroupManager; +import org.opentcs.guing.common.components.layer.LayerManager; + +/** + * A Guice module for this package. + */ +public class LayersInjectionModule + extends + PlantOverviewInjectionModule { + + /** + * Creates a new instance. + */ + public LayersInjectionModule() { + } + + @Override + protected void configure() { + bind(LayerManagerModeling.class).in(Singleton.class); + bind(LayerManager.class).to(LayerManagerModeling.class); + bind(LayerEditorModeling.class).to(LayerManagerModeling.class); + bind(ActiveLayerProvider.class).to(LayerManagerModeling.class); + bind(LayersPanel.class).in(Singleton.class); + + bind(LayerGroupManager.class).to(LayerManagerModeling.class); + bind(LayerGroupEditorModeling.class).to(LayerManagerModeling.class); + bind(LayerGroupsPanel.class).in(Singleton.class); + } +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/properties/PropertiesInjectionModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/properties/PropertiesInjectionModule.java new file mode 100644 index 0000000..a16610d --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/properties/PropertiesInjectionModule.java @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.properties; + +import com.google.inject.TypeLiteral; +import com.google.inject.assistedinject.FactoryModuleBuilder; +import com.google.inject.multibindings.MapBinder; +import jakarta.inject.Singleton; +import org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule; +import org.opentcs.guing.base.components.properties.type.AbstractComplexProperty; +import org.opentcs.guing.base.components.properties.type.BoundingBoxProperty; +import org.opentcs.guing.base.components.properties.type.EnergyLevelThresholdSetProperty; +import org.opentcs.guing.base.components.properties.type.EnvelopesProperty; +import org.opentcs.guing.base.components.properties.type.KeyValueProperty; +import org.opentcs.guing.base.components.properties.type.KeyValueSetProperty; +import org.opentcs.guing.base.components.properties.type.LinkActionsProperty; +import org.opentcs.guing.base.components.properties.type.LocationTypeActionsProperty; +import org.opentcs.guing.base.components.properties.type.OrderTypesProperty; +import org.opentcs.guing.base.components.properties.type.PeripheralOperationsProperty; +import org.opentcs.guing.base.components.properties.type.SymbolProperty; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; +import org.opentcs.guing.common.components.properties.PropertiesComponentsFactory; +import org.opentcs.guing.common.components.properties.SelectionPropertiesComponent; +import org.opentcs.guing.common.components.properties.panel.BoundingBoxPropertyEditorPanel; +import org.opentcs.guing.common.components.properties.panel.EnergyLevelThresholdSetPropertyEditorPanel; +import org.opentcs.guing.common.components.properties.panel.EnvelopesPropertyEditorPanel; +import org.opentcs.guing.common.components.properties.panel.KeyValuePropertyEditorPanel; +import org.opentcs.guing.common.components.properties.panel.KeyValueSetPropertyEditorPanel; +import org.opentcs.guing.common.components.properties.panel.LinkActionsEditorPanel; +import org.opentcs.guing.common.components.properties.panel.LocationTypeActionsEditorPanel; +import org.opentcs.guing.common.components.properties.panel.OrderTypesPropertyEditorPanel; +import org.opentcs.guing.common.components.properties.panel.PeripheralOperationsPropertyEditorPanel; +import org.opentcs.guing.common.components.properties.panel.PropertiesPanelFactory; +import org.opentcs.guing.common.components.properties.panel.SymbolPropertyEditorPanel; +import org.opentcs.guing.common.components.properties.table.CellEditorFactory; +import org.opentcs.modeleditor.application.menus.MenuItemComponentsFactory; + +/** + * A Guice module for this package. + */ +public class PropertiesInjectionModule + extends + PlantOverviewInjectionModule { + + /** + * Creates a new instance. + */ + public PropertiesInjectionModule() { + } + + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(PropertiesPanelFactory.class)); + install(new FactoryModuleBuilder().build(CellEditorFactory.class)); + install(new FactoryModuleBuilder().build(PropertiesComponentsFactory.class)); + install(new FactoryModuleBuilder().build(MenuItemComponentsFactory.class)); + + MapBinder, DetailsDialogContent> dialogContentMapBinder + = MapBinder.newMapBinder( + binder(), + new TypeLiteral>() { + }, + new TypeLiteral() { + } + ); + dialogContentMapBinder + .addBinding(KeyValueProperty.class) + .to(KeyValuePropertyEditorPanel.class); + dialogContentMapBinder + .addBinding(KeyValueSetProperty.class) + .to(KeyValueSetPropertyEditorPanel.class); + dialogContentMapBinder + .addBinding(LocationTypeActionsProperty.class) + .to(LocationTypeActionsEditorPanel.class); + dialogContentMapBinder + .addBinding(LinkActionsProperty.class) + .to(LinkActionsEditorPanel.class); + dialogContentMapBinder + .addBinding(SymbolProperty.class) + .to(SymbolPropertyEditorPanel.class); + dialogContentMapBinder + .addBinding(OrderTypesProperty.class) + .to(OrderTypesPropertyEditorPanel.class); + dialogContentMapBinder + .addBinding(PeripheralOperationsProperty.class) + .to(PeripheralOperationsPropertyEditorPanel.class); + dialogContentMapBinder + .addBinding(EnvelopesProperty.class) + .to(EnvelopesPropertyEditorPanel.class); + dialogContentMapBinder + .addBinding(BoundingBoxProperty.class) + .to(BoundingBoxPropertyEditorPanel.class); + dialogContentMapBinder + .addBinding(EnergyLevelThresholdSetProperty.class) + .to(EnergyLevelThresholdSetPropertyEditorPanel.class); + + bind(SelectionPropertiesComponent.class) + .in(Singleton.class); + + } + +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/tree/elements/TreeElementsInjectionModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/tree/elements/TreeElementsInjectionModule.java new file mode 100644 index 0000000..67d2a8a --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/components/tree/elements/TreeElementsInjectionModule.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.tree.elements; + +import com.google.inject.AbstractModule; +import com.google.inject.assistedinject.FactoryModuleBuilder; +import org.opentcs.guing.common.components.tree.elements.UserObjectFactory; +import org.opentcs.guing.common.components.tree.elements.VehicleUserObject; + +/** + * A Guice module for this package. + */ +public class TreeElementsInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public TreeElementsInjectionModule() { + } + + @Override + protected void configure() { + install( + new FactoryModuleBuilder() + .implement(VehicleUserObject.class, VehicleUserObjectModeling.class) + .build(UserObjectFactory.class) + ); + } +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/exchange/ExchangeInjectionModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/exchange/ExchangeInjectionModule.java new file mode 100644 index 0000000..d54e5d0 --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/exchange/ExchangeInjectionModule.java @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.exchange; + +import com.google.inject.AbstractModule; +import jakarta.inject.Singleton; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.common.DefaultPortalManager; +import org.opentcs.common.PortalManager; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.guing.common.exchange.ApplicationPortalProvider; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.event.EventSource; +import org.opentcs.util.event.SimpleEventBus; + +/** + * A Guice configuration module for this package. + */ +public class ExchangeInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public ExchangeInjectionModule() { + } + + @Override + protected void configure() { + bind(PortalManager.class) + .to(DefaultPortalManager.class) + .in(Singleton.class); + + EventBus eventBus = new SimpleEventBus(); + bind(EventSource.class) + .annotatedWith(ApplicationEventBus.class) + .toInstance(eventBus); + bind(EventHandler.class) + .annotatedWith(ApplicationEventBus.class) + .toInstance(eventBus); + bind(EventBus.class) + .annotatedWith(ApplicationEventBus.class) + .toInstance(eventBus); + + bind(SharedKernelServicePortalProvider.class) + .to(ApplicationPortalProvider.class) + .in(Singleton.class); + } +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/math/path/PathLengthFunctionInjectionModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/math/path/PathLengthFunctionInjectionModule.java new file mode 100644 index 0000000..4290411 --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/math/path/PathLengthFunctionInjectionModule.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.math.path; + +import com.google.inject.AbstractModule; +import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.MapBinder; +import org.opentcs.guing.base.model.elements.PathModel; + +/** + * A Guice configuration module for this package. + */ +public class PathLengthFunctionInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public PathLengthFunctionInjectionModule() { + } + + @Override + protected void configure() { + MapBinder pathLengthFunctionBinder + = MapBinder.newMapBinder( + binder(), + new TypeLiteral() { + }, + new TypeLiteral() { + } + ); + pathLengthFunctionBinder + .addBinding(PathModel.Type.DIRECT) + .to(EuclideanDistance.class); + pathLengthFunctionBinder + .addBinding(PathModel.Type.ELBOW) + .to(EuclideanDistance.class); + pathLengthFunctionBinder + .addBinding(PathModel.Type.SLANTED) + .to(EuclideanDistance.class); + pathLengthFunctionBinder + .addBinding(PathModel.Type.POLYPATH) + .to(PolyPathLength.class); + pathLengthFunctionBinder + .addBinding(PathModel.Type.BEZIER) + .to(BezierLength.class); + pathLengthFunctionBinder + .addBinding(PathModel.Type.BEZIER_3) + .to(BezierThreeLength.class); + + bind(PathLengthFunction.class) + .to(CompositePathLengthFunction.class); + } +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/model/ModelInjectionModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/model/ModelInjectionModule.java new file mode 100644 index 0000000..43a75ad --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/model/ModelInjectionModule.java @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.model; + +import com.google.inject.AbstractModule; +import org.opentcs.guing.common.model.StandardSystemModel; +import org.opentcs.guing.common.model.SystemModel; + +/** + * A Guice module for the model package. + */ +public class ModelInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public ModelInjectionModule() { + } + + @Override + protected void configure() { + bind(SystemModel.class).to(StandardSystemModel.class); + } + +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/persistence/DefaultPersistenceInjectionModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/persistence/DefaultPersistenceInjectionModule.java new file mode 100644 index 0000000..6fbae40 --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/persistence/DefaultPersistenceInjectionModule.java @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.persistence; + +import com.google.inject.AbstractModule; +import com.google.inject.Singleton; +import org.opentcs.guing.common.persistence.ModelFilePersistor; +import org.opentcs.guing.common.persistence.ModelFileReader; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.persistence.unified.UnifiedModelPersistor; +import org.opentcs.modeleditor.persistence.unified.UnifiedModelReader; + +/** + * Default bindings for model readers and persistors. + */ +public class DefaultPersistenceInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public DefaultPersistenceInjectionModule() { + } + + @Override + protected void configure() { + bind(OpenTCSModelManagerModeling.class).in(Singleton.class); + bind(ModelManager.class).to(OpenTCSModelManagerModeling.class); + bind(ModelManagerModeling.class).to(OpenTCSModelManagerModeling.class); + + bind(ModelFileReader.class).to(UnifiedModelReader.class); + bind(ModelFilePersistor.class).to(UnifiedModelPersistor.class); + } + +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/transport/DefaultOrderTypeSuggestions.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/transport/DefaultOrderTypeSuggestions.java new file mode 100644 index 0000000..9b636e8 --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/transport/DefaultOrderTypeSuggestions.java @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.transport; + +import java.util.HashSet; +import java.util.Set; +import org.opentcs.components.plantoverview.OrderTypeSuggestions; +import org.opentcs.data.order.OrderConstants; + +/** + * The default suggestions for transport order types. + */ +public class DefaultOrderTypeSuggestions + implements + OrderTypeSuggestions { + + /** + * The transport order type suggestions. + */ + private final Set typeSuggestions = new HashSet<>(); + + /** + * Creates a new instance. + */ + public DefaultOrderTypeSuggestions() { + typeSuggestions.add(OrderConstants.TYPE_NONE); + typeSuggestions.add(OrderConstants.TYPE_CHARGE); + typeSuggestions.add(OrderConstants.TYPE_PARK); + typeSuggestions.add(OrderConstants.TYPE_TRANSPORT); + } + + @Override + public Set getTypeSuggestions() { + return typeSuggestions; + } +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/transport/OrderTypeSuggestionsModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/transport/OrderTypeSuggestionsModule.java new file mode 100644 index 0000000..1ed460e --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/transport/OrderTypeSuggestionsModule.java @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.transport; + +import jakarta.inject.Singleton; +import org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule; +import org.opentcs.guing.common.transport.OrderTypeSuggestionsPool; + +/** + * A Guice module for the transport order type suggestions. + */ +public class OrderTypeSuggestionsModule + extends + PlantOverviewInjectionModule { + + /** + * Creates a new instance. + */ + public OrderTypeSuggestionsModule() { + } + + @Override + protected void configure() { + orderTypeSuggestionsBinder().addBinding() + .to(DefaultOrderTypeSuggestions.class) + .in(Singleton.class); + + bind(OrderTypeSuggestionsPool.class).in(Singleton.class); + } +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/transport/TransportInjectionModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/transport/TransportInjectionModule.java new file mode 100644 index 0000000..0dfd07d --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/transport/TransportInjectionModule.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.transport; + +import com.google.inject.AbstractModule; + +/** + * A Guice module for this package. + */ +public class TransportInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public TransportInjectionModule() { + } + + @Override + protected void configure() { + } +} diff --git a/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/util/UtilInjectionModule.java b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/util/UtilInjectionModule.java new file mode 100644 index 0000000..abcc080 --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/java/org/opentcs/modeleditor/util/UtilInjectionModule.java @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.util; + +import com.google.inject.AbstractModule; +import jakarta.inject.Singleton; +import org.opentcs.guing.common.util.PanelRegistry; + +/** + * A default Guice module for this package. + */ +public class UtilInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public UtilInjectionModule() { + } + + @Override + protected void configure() { + bind(PanelRegistry.class).in(Singleton.class); + } +} diff --git a/opentcs-modeleditor/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule b/opentcs-modeleditor/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule new file mode 100644 index 0000000..258d579 --- /dev/null +++ b/opentcs-modeleditor/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT + +org.opentcs.modeleditor.DefaultImportersExportersModule +org.opentcs.modeleditor.PropertySuggestionsModule +org.opentcs.modeleditor.transport.OrderTypeSuggestionsModule diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/OpenTCSView.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/OpenTCSView.java new file mode 100644 index 0000000..5f9d54a --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/OpenTCSView.java @@ -0,0 +1,1828 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application; + +import static java.util.Objects.requireNonNull; + +import bibliothek.gui.dock.common.DefaultSingleCDockable; +import bibliothek.gui.dock.common.SingleCDockable; +import bibliothek.gui.dock.common.event.CVetoClosingEvent; +import bibliothek.gui.dock.common.event.CVetoClosingListener; +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.FocusEvent; +import java.awt.geom.AffineTransform; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JToggleButton; +import javax.swing.JToolBar; +import javax.swing.SwingUtilities; +import javax.swing.border.EtchedBorder; +import org.jhotdraw.app.AbstractView; +import org.jhotdraw.draw.Drawing; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.gui.URIChooser; +import org.jhotdraw.util.ReversedList; +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.components.kernel.services.ServiceUnavailableException; +import org.opentcs.components.plantoverview.PlantModelExporter; +import org.opentcs.components.plantoverview.PlantModelImporter; +import org.opentcs.components.plantoverview.PluggablePanel; +import org.opentcs.components.plantoverview.PluggablePanelFactory; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.event.AttributesChangeListener; +import org.opentcs.guing.base.components.properties.type.AbstractProperty; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.event.BlockChangeEvent; +import org.opentcs.guing.base.event.BlockChangeListener; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.PropertiesCollection; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.application.ApplicationState; +import org.opentcs.guing.common.application.ComponentsManager; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.application.GuiManagerModeling; +import org.opentcs.guing.common.application.ModelRestorationProgressStatus; +import org.opentcs.guing.common.application.OperationMode; +import org.opentcs.guing.common.application.PluginPanelManager; +import org.opentcs.guing.common.application.ProgressIndicator; +import org.opentcs.guing.common.application.StartupProgressStatus; +import org.opentcs.guing.common.application.StatusPanel; +import org.opentcs.guing.common.components.dockable.DrawingViewFocusHandler; +import org.opentcs.guing.common.components.drawing.DrawingViewScrollPane; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingView; +import org.opentcs.guing.common.components.drawing.course.Origin; +import org.opentcs.guing.common.components.drawing.course.OriginChangeListener; +import org.opentcs.guing.common.components.drawing.figures.FigureConstants; +import org.opentcs.guing.common.components.drawing.figures.LabeledFigure; +import org.opentcs.guing.common.components.drawing.figures.SimpleLineConnection; +import org.opentcs.guing.common.components.drawing.figures.TCSFigure; +import org.opentcs.guing.common.components.layer.LayerManager; +import org.opentcs.guing.common.components.properties.SelectionPropertiesComponent; +import org.opentcs.guing.common.components.properties.panel.PropertiesPanelFactory; +import org.opentcs.guing.common.components.tree.BlocksTreeViewManager; +import org.opentcs.guing.common.components.tree.ComponentsTreeViewManager; +import org.opentcs.guing.common.components.tree.TreeViewManager; +import org.opentcs.guing.common.components.tree.elements.ContextObject; +import org.opentcs.guing.common.components.tree.elements.UserObject; +import org.opentcs.guing.common.components.tree.elements.UserObjectContext; +import org.opentcs.guing.common.components.tree.elements.UserObjectContext.ContextType; +import org.opentcs.guing.common.components.tree.elements.UserObjectUtil; +import org.opentcs.guing.common.event.DrawingEditorEvent; +import org.opentcs.guing.common.event.DrawingEditorListener; +import org.opentcs.guing.common.event.ModelNameChangeEvent; +import org.opentcs.guing.common.event.OperationModeChangeEvent; +import org.opentcs.guing.common.event.ResetInteractionToolCommand; +import org.opentcs.guing.common.event.SystemModelTransitionEvent; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.ModelComponentFactory; +import org.opentcs.guing.common.util.PanelRegistry; +import org.opentcs.guing.common.util.UserMessageHelper; +import org.opentcs.modeleditor.application.action.ToolBarManager; +import org.opentcs.modeleditor.application.action.ViewActionMap; +import org.opentcs.modeleditor.components.dockable.DockingManagerModeling; +import org.opentcs.modeleditor.components.drawing.DrawingViewFactory; +import org.opentcs.modeleditor.components.layer.LayerEditorEventHandler; +import org.opentcs.modeleditor.persistence.ModelManagerModeling; +import org.opentcs.modeleditor.util.Colors; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.modeleditor.util.UniqueNameGenerator; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.UndoRedoManager; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.toolbar.PaletteToolBarBorder; +import org.opentcs.thirdparty.guing.common.jhotdraw.components.drawing.AbstractOpenTCSDrawingView; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.file.CloseFileAction; +import org.opentcs.thirdparty.modeleditor.jhotdraw.components.drawing.OpenTCSDrawingViewModeling; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Visualizes the driving course and other kernel objects as well as messages + * received by the kernel. + * (Contains everything underneath the tool bars.) + */ +public class OpenTCSView + extends + AbstractView + implements + GuiManager, + GuiManagerModeling, + ComponentsManager, + PluginPanelManager, + EventHandler { + + /** + * The name/title of this application. + */ + public static final String NAME + = ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MISC_PATH) + .getString("openTcsView.applicationName.text"); + /** + * Property key for the currently loaded driving course model. + * The corresponding value contains a "*" if the model has been modified. + */ + public static final String MODELNAME_PROPERTY = "modelName"; + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(OpenTCSView.class); + /** + * This instance's resource bundle. + */ + private final ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MISC_PATH); + /** + * Provides/manages the application's current state. + */ + private final ApplicationState appState; + /** + * Allows for undoing and redoing actions. + */ + private final UndoRedoManager fUndoRedoManager; + /** + * The drawing editor. + */ + private final OpenTCSDrawingEditor fDrawingEditor; + /** + * The JFrame. + */ + private final JFrame fFrame; + /** + * Utility to manage the views. + */ + private final ViewManagerModeling viewManager; + /** + * A manager for the components tree view. + */ + private final TreeViewManager fComponentsTreeManager; + /** + * A manager for the blocks tree view. + */ + private final TreeViewManager fBlocksTreeManager; + /** + * Displays properties of the currently selected model component(s). + */ + private final SelectionPropertiesComponent fPropertiesComponent; + /** + * Manages driving course models. + */ + private final ModelManagerModeling fModelManager; + /** + * Indicates the progress for lengthy operations. + */ + private final ProgressIndicator progressIndicator; + /** + * Manages docking frames. + */ + private final DockingManagerModeling dockingManager; + /** + * Registry for plugin panels. + */ + private final PanelRegistry panelRegistry; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * A panel for mouse position/status. + */ + private final StatusPanel statusPanel; + /** + * The model component factory to be used. + */ + private final ModelComponentFactory modelComponentFactory; + /** + * Shows messages to the user. + */ + private final UserMessageHelper userMessageHelper; + /** + * A factory for drawing views. + */ + private final DrawingViewFactory drawingViewFactory; + /** + * Handles block events. + */ + private final BlockChangeListener blockEventHandler = new BlockEventHandler(); + /** + * Handles events for changes of properties. + */ + private final AttributesChangeListener attributesEventHandler = new AttributesEventHandler(); + /** + * Generates names for model objects. + */ + private final UniqueNameGenerator modelCompNameGen; + /** + * A factory for UserObject instances. + */ + private final UserObjectUtil userObjectUtil; + /** + * A provider for ActionMaps. + */ + private final Provider actionMapProvider; + /** + * A provider for the tool bar manager. + */ + private final Provider toolBarManagerProvider; + /** + * A factory for properties-related panels. + */ + private final PropertiesPanelFactory propertiesPanelFactory; + /** + * The application's event bus. + */ + private final EventBus eventBus; + /** + * Handles focussing of dockables. + */ + private final DrawingViewFocusHandler drawingViewFocusHandler; + /** + * The layer manager. + */ + private final LayerManager layerManager; + /** + * Handles drawing editor events that the layer editor needs to know about. + */ + private final LayerEditorEventHandler layerEditorEventHandler; + /** + * Provides the application's tool bars. + */ + private ToolBarManager toolBarManager; + + /** + * Creates a new instance. + * + * @param appState Provides/manages the application's current state. + * @param appFrame The JFrame this view is wrapped in. + * @param progressIndicator The progress indicator to be used. + * @param portalProvider Provides a access to a portal. + * @param viewManager The view manager to be used. + * @param tcsDrawingEditor The drawing editor to be used. + * @param modelManager The model manager to be used. + * @param statusPanel The status panel to be used. + * @param panelRegistry The plugin panel registry to be used. + * @param modelComponentFactory The model component factory to be used. + * @param userMessageHelper An UserMessageHelper + * @param drawingViewFactory A factory for drawing views. + * @param modelCompNameGen A generator for model components' names. + * @param undoRedoManager Allows for undoing and redoing actions. + * @param componentsTreeManager Manages the components tree view. + * @param blocksTreeManager Manages the blocks tree view. + * @param propertiesComponent Displays properties of the currently selected model component(s). + * @param userObjectUtil A factory for UserObject instances. + * @param actionMapProvider A provider for ActionMaps. + * @param toolBarManagerProvider A provider for the tool bar manager. + * @param propertiesPanelFactory A factory for properties-related panels. + * @param eventBus The application's event bus. + * @param dockingManager Manages docking frames. + * @param drawingViewFocusHandler Handles focussing of dockables. + * @param layerManager The layer manager. + * @param layerEditorEventHandler Handles drawing editor events that the layer editor needs to + * know about. + */ + @Inject + public OpenTCSView( + ApplicationState appState, + @ApplicationFrame + JFrame appFrame, + ProgressIndicator progressIndicator, + SharedKernelServicePortalProvider portalProvider, + ViewManagerModeling viewManager, + OpenTCSDrawingEditor tcsDrawingEditor, + ModelManagerModeling modelManager, + StatusPanel statusPanel, + PanelRegistry panelRegistry, + ModelComponentFactory modelComponentFactory, + UserMessageHelper userMessageHelper, + DrawingViewFactory drawingViewFactory, + UniqueNameGenerator modelCompNameGen, + UndoRedoManager undoRedoManager, + ComponentsTreeViewManager componentsTreeManager, + BlocksTreeViewManager blocksTreeManager, + SelectionPropertiesComponent propertiesComponent, + UserObjectUtil userObjectUtil, + Provider actionMapProvider, + Provider toolBarManagerProvider, + PropertiesPanelFactory propertiesPanelFactory, + @ApplicationEventBus + EventBus eventBus, + DockingManagerModeling dockingManager, + DrawingViewFocusHandler drawingViewFocusHandler, + LayerManager layerManager, + LayerEditorEventHandler layerEditorEventHandler + ) { + this.appState = requireNonNull(appState, "appState"); + this.fFrame = requireNonNull(appFrame, "appFrame"); + this.progressIndicator = requireNonNull(progressIndicator, "progressIndicator"); + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.viewManager = requireNonNull(viewManager, "viewManager"); + this.fDrawingEditor = requireNonNull(tcsDrawingEditor, "tcsDrawingEditor"); + this.fModelManager = requireNonNull(modelManager, "modelManager"); + this.statusPanel = requireNonNull(statusPanel, "statusPanel"); + this.panelRegistry = requireNonNull(panelRegistry, "panelRegistry"); + this.modelComponentFactory = requireNonNull(modelComponentFactory, "modelComponentFactory"); + this.userMessageHelper = requireNonNull(userMessageHelper, "userMessageHelper"); + this.drawingViewFactory = requireNonNull(drawingViewFactory, "drawingViewFactory"); + this.modelCompNameGen = requireNonNull(modelCompNameGen, "modelCompNameGen"); + this.fUndoRedoManager = requireNonNull(undoRedoManager, "undoRedoManager"); + this.fComponentsTreeManager = requireNonNull(componentsTreeManager, "componentsTreeManager"); + this.fBlocksTreeManager = requireNonNull(blocksTreeManager, "blocksTreeManager"); + this.fPropertiesComponent = requireNonNull(propertiesComponent, "propertiesComponent"); + this.userObjectUtil = requireNonNull(userObjectUtil, "userObjectUtil"); + this.actionMapProvider = requireNonNull(actionMapProvider, "actionMapProvider"); + this.toolBarManagerProvider = requireNonNull(toolBarManagerProvider, "toolBarManagerProvider"); + this.propertiesPanelFactory = requireNonNull(propertiesPanelFactory, "propertiesPanelFactory"); + this.eventBus = requireNonNull(eventBus, "eventBus"); + this.dockingManager = requireNonNull(dockingManager, "dockingManager"); + this.drawingViewFocusHandler = requireNonNull( + drawingViewFocusHandler, + "drawingViewFocusHandler" + ); + this.layerManager = requireNonNull(layerManager, "layerManager"); + this.layerEditorEventHandler = requireNonNull( + layerEditorEventHandler, + "layerEditorEventHandler" + ); + } + + @Override // AbstractView + public void init() { + eventBus.subscribe(this); + + progressIndicator.setProgress(StartupProgressStatus.INITIALIZED); + + fDrawingEditor.addDrawingEditorListener(layerEditorEventHandler); + fDrawingEditor.addDrawingEditorListener(new DrawingEditorEventHandler(fModelManager)); + + eventBus.subscribe(fComponentsTreeManager); + + progressIndicator.setProgress(StartupProgressStatus.INITIALIZE_MODEL); + setSystemModel(fModelManager.getModel()); + + // Properties view (lower left corner) + fPropertiesComponent.setPropertiesContent( + propertiesPanelFactory.createPropertiesTableContent(this) + ); + + setActionMap(actionMapProvider.get()); + this.toolBarManager = toolBarManagerProvider.get(); + eventBus.subscribe(toolBarManager); + + eventBus.subscribe(fPropertiesComponent); + eventBus.subscribe(fUndoRedoManager); + eventBus.subscribe(fDrawingEditor); + + layerManager.initialize(); + + initializeFrame(); + viewManager.init(); + createEmptyModel(); + + setModelingState(); + } + + @Override // AbstractView + public void stop() { + LOG.info("GUI terminating..."); + eventBus.unsubscribe(this); + System.exit(0); + } + + @Override // AbstractView + public void clear() { + } + + @Override + public void onEvent(Object event) { + if (event instanceof SystemModelTransitionEvent) { + handleSystemModelTransition((SystemModelTransitionEvent) event); + } + } + + private void handleSystemModelTransition(SystemModelTransitionEvent evt) { + switch (evt.getStage()) { + case LOADED: + setHasUnsavedChanges(false); + // Update model name in title + setModelNameProperty(fModelManager.getModel().getName()); + break; + default: + // Do nada. + } + } + + @Override + public void showPluginPanel(PluggablePanelFactory factory, boolean visible) { + String id = factory.getClass().getName(); + SingleCDockable dockable = dockingManager.getCControl().getSingleDockable(id); + if (dockable != null) { + // dockable is not null at this point when the user hides the plugin + // panel by clicking on its menu entry + PluggablePanel panel = (PluggablePanel) dockable.getFocusComponent(); + panel.terminate(); + if (!dockingManager.getCControl().removeDockable(dockable)) { + LOG.warn("Couldn't remove dockable for plugin panel factory '{}'", factory); + return; + } + } + if (!visible) { + return; + } + if (factory.providesPanel(Kernel.State.MODELLING)) { + PluggablePanel panel = factory.createPanel(Kernel.State.MODELLING); + DefaultSingleCDockable factoryDockable = dockingManager.createFloatingDockable( + factory.getClass().getName(), + factory.getPanelDescription(), + panel + ); + factoryDockable.addVetoClosingListener(new CVetoClosingListener() { + + @Override + public void closing(CVetoClosingEvent event) { + } + + @Override + public void closed(CVetoClosingEvent event) { + panel.terminate(); + dockingManager.getCControl().removeDockable(factoryDockable); + } + }); + panel.initialize(); + } + } + + /** + * Adds a new drawing view to the tabbed wrappingPanel. + * + * @return The newly created Dockable. + */ + public DefaultSingleCDockable addDrawingView() { + DrawingViewScrollPane newScrollPane + = drawingViewFactory.createDrawingView( + fModelManager.getModel(), + toolBarManager.getSelectionToolButton(), + toolBarManager.getDragToolButton(), + toolBarManager.getButtonCreateLink(), + toolBarManager.getButtonCreatePath() + ); + + int drawingViewIndex = viewManager.getNextDrawingViewIndex(); + + String title + = bundle.getString("openTcsView.panel_operatingDrawingView.title") + " " + drawingViewIndex; + DefaultSingleCDockable newDockable + = dockingManager.createDockable( + "drivingCourse" + drawingViewIndex, + title, + newScrollPane, + true + ); + viewManager.addDrawingView(newDockable, newScrollPane); + + int lastIndex = Math.max(0, drawingViewIndex - 1); + dockingManager.addTabTo(newDockable, DockingManagerModeling.COURSE_TAB_PANE_ID, lastIndex); + + newDockable.addVetoClosingListener(new DrawingViewClosingListener(newDockable)); + newDockable.addFocusListener(drawingViewFocusHandler); + + newScrollPane.getDrawingView().getComponent().dispatchEvent( + new FocusEvent(this, FocusEvent.FOCUS_GAINED) + ); + firePropertyChange(AbstractOpenTCSDrawingView.FOCUS_GAINED, null, newDockable); + + return newDockable; + } + + /** + * Restores the layout to default. + */ + public void resetWindowArrangement() { + for (DefaultSingleCDockable dock : new ArrayList<>(viewManager.getDrawingViewMap().keySet())) { + removeDrawingView(dock); + } + + dockingManager.reset(); + closeOpenedPluginPanels(); + viewManager.reset(); + + initializeFrame(); + viewManager.init(); + + // Depending on the current kernel state there may exist panels, now, that shouldn't be visible. + new Thread(() -> setModelingState()).start(); + } + + @Override // GuiManager + public void createEmptyModel() { + CloseFileAction action = (CloseFileAction) getActionMap().get(CloseFileAction.ID); + if (action != null) { + action.actionPerformed( + new ActionEvent( + this, + ActionEvent.ACTION_PERFORMED, + CloseFileAction.ID_MODEL_CLOSING + ) + ); + if (action.getFileSavedStatus() == JOptionPane.CANCEL_OPTION) { + return; + } + } + + // Clean up first... + eventBus.onEvent( + new SystemModelTransitionEvent( + this, + SystemModelTransitionEvent.Stage.UNLOADING, + fModelManager.getModel() + ) + ); + + eventBus.onEvent( + new SystemModelTransitionEvent( + this, + SystemModelTransitionEvent.Stage.UNLOADED, + fModelManager.getModel() + ) + ); + + // Create the new, empty model. + LOG.debug("Creating new driving course model..."); + fModelManager.createEmptyModel(); + + eventBus.onEvent( + new SystemModelTransitionEvent( + this, + SystemModelTransitionEvent.Stage.LOADING, + fModelManager.getModel() + ) + ); + + // Now let components set themselves up for the new model. + setSystemModel(fModelManager.getModel()); + + eventBus.onEvent( + new SystemModelTransitionEvent( + this, + SystemModelTransitionEvent.Stage.LOADED, + fModelManager.getModel() + ) + ); + + // makes sure the origin is on the lower left side and the ruler + // are correctly drawn + fDrawingEditor.initializeViewport(); + } + + public void downloadModelFromKernel() { + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + downloadModelFromKernel(sharedPortal.getPortal()); + } + catch (ServiceUnavailableException exc) { + LOG.info("Kernel unavailable, aborting.", exc); + } + } + + /** + * Loads the current kernel model. + */ + private void downloadModelFromKernel(KernelServicePortal portal) { + if (hasUnsavedChanges()) { + if (!showUnsavedChangesDialog()) { + return; + } + } + + restoreModel(portal); + } + + /** + * Initializes the model stored in the kernel or in the model manager. + * + * @param portal If not null, the model from the given kernel will be loaded, else the model from + * the model manager + */ + private void restoreModel( + @Nullable + KernelServicePortal portal + ) { + progressIndicator.initialize(); + + progressIndicator.setProgress(ModelRestorationProgressStatus.CLEANUP); + eventBus.onEvent( + new SystemModelTransitionEvent( + this, + SystemModelTransitionEvent.Stage.UNLOADING, + fModelManager.getModel() + ) + ); + + progressIndicator.setProgress(ModelRestorationProgressStatus.START_LOADING_MODEL); + eventBus.onEvent( + new SystemModelTransitionEvent( + this, + SystemModelTransitionEvent.Stage.UNLOADED, + fModelManager.getModel() + ) + ); + + if (portal == null) { + fModelManager.restoreModel(); + } + else { + fModelManager.restoreModel(portal); + statusPanel.setLogMessage( + Level.INFO, + bundle.getFormatted( + "openTcsView.message_modelDownloaded.text", + fModelManager.getModel().getName() + ) + ); + } + + progressIndicator.setProgress(ModelRestorationProgressStatus.SET_UP_MODEL_VIEW); + eventBus.onEvent( + new SystemModelTransitionEvent( + this, + SystemModelTransitionEvent.Stage.LOADING, + fModelManager.getModel() + ) + ); + + setSystemModel(fModelManager.getModel()); + + progressIndicator.setProgress(ModelRestorationProgressStatus.SET_UP_DIRECTORY_TREE); + + progressIndicator.setProgress(ModelRestorationProgressStatus.SET_UP_WORKING_AREA); + + ModelComponent layoutComponent + = fModelManager.getModel().getMainFolder(SystemModel.FolderKey.LAYOUT); + layoutComponent.addAttributesChangeListener(attributesEventHandler); + + eventBus.onEvent( + new SystemModelTransitionEvent( + this, SystemModelTransitionEvent.Stage.LOADED, + fModelManager.getModel() + ) + ); + updateModelName(); + + progressIndicator.terminate(); + } + + private void setModelNameProperty(String modelName) { + fModelManager.getModel().setName(modelName); + eventBus.onEvent(new ModelNameChangeEvent(this, modelName)); + } + + public void updateModelName() { + String newName = fModelManager.getModel().getName(); + eventBus.onEvent(new ModelNameChangeEvent(this, newName)); + } + + /** + * Adds a background image to the currently active drawing view. + * + * @param file The file with the image. + */ + public void addBackgroundBitmap(File file) { + viewManager.setBitmapToModellingView(file); + } + + @Override + public List restoreModelComponents(List userObjects) { + List restoredUserObjects = new ArrayList<>(); + + for (UserObject userObject : userObjects) { + ModelComponent modelComponent = userObject.getModelComponent(); + ModelComponent folder = fModelManager.getModel().getFolder(modelComponent); + + if (folder == null) { + // Workaround: No folders should be selected in the tree! + return null; + } + + if (folder.contains(modelComponent)) { + try { + // Paste after Copy: Create clones of tree components (and figures) + Figure figure = fModelManager.getModel().getFigure(modelComponent); + if (figure != null) { + if (figure instanceof LabeledFigure) { + // Point, Location + // Create new Figure with a "cloned" model + final LabeledFigure clonedFigure = (LabeledFigure) figure.clone(); + // Place the figure relative to the position of the prototype + AffineTransform tx = new AffineTransform(); + // TODO: Make the duplicate's distance configurable. + // TODO: With multiple pastes, place the inserted figure relative + // to the predecessor, not the original. + tx.translate(50, 50); + clonedFigure.transform(tx); + getActiveDrawingView().getDrawing().add(clonedFigure); + // The new tree component will be created by "figureAdded()" + modelComponent = clonedFigure.get(FigureConstants.MODEL); + } + else if (figure instanceof TCSFigure) { + // Vehicle, ... + TCSFigure clonedFigure = (TCSFigure) figure.clone(); + modelComponent = clonedFigure.getModel(); + } + } + else { + // LocationType, Block, Group + modelComponent = modelComponent.clone(); + } + } + catch (CloneNotSupportedException ex) { + LOG.warn("clone() not supported for {}", modelComponent.getName()); + } + } + + Figure figure = fModelManager.getModel().getFigure(modelComponent); + if (figure != null + && !getActiveDrawingView().getDrawing().contains(figure)) { + getActiveDrawingView().getDrawing().add(figure); + } + + addModelComponent(folder, modelComponent); + ContextType type = null; + if (userObject instanceof ContextObject) { + ContextObject co = (ContextObject) userObject; + type = co.getContextType(); + } + UserObjectContext context = userObjectUtil.createContext(type); + restoredUserObjects.add(userObjectUtil.createUserObject(modelComponent, context)); + } + + return restoredUserObjects; + } + + @Override // View + public void write(URI f, URIChooser chooser) + throws IOException { + } + + @Override // View + public void read(URI f, URIChooser chooser) + throws IOException { + } + + @Override // AbstractView + public boolean canSaveTo(URI file) { + return new File(file).getName().endsWith(".xml"); + } + + @Override // AbstractView + public URI getURI() { + String modelName = fModelManager.getModel().getName(); + + try { + uri = new URI(modelName); + } + catch (URISyntaxException ex) { + LOG.warn("URISyntaxException in getURI({})", modelName, ex); + } + + return uri; + } + + /** + * Returns all drawing views (including the modelling view). + * + * @return List with all known OpenTCSDrawingViews. + */ + private List getDrawingViews() { + List views = new ArrayList<>(); + + for (DrawingViewScrollPane scrollPane : viewManager.getDrawingViewMap().values()) { + views.add(scrollPane.getDrawingView()); + } + + return views; + } + + @Override + public void selectModelComponent(ModelComponent modelComponent) { + fPropertiesComponent.setModel(modelComponent); + DrawingView drawingView = fDrawingEditor.getActiveView(); + drawingView.clearSelection(); + Figure figure = findFigure(modelComponent); + // LocationTypes don't have a figure. + if (figure != null) { + drawingView.toggleSelection(figure); + } + } + + @Override// GuiManager + public void addSelectedModelComponent(ModelComponent modelComponent) { + Set components = fComponentsTreeManager.getSelectedItems(); + + if (components.size() > 1) { + components.add(modelComponent); + + DrawingView drawingView = fDrawingEditor.getActiveView(); + drawingView.clearSelection(); + + Collection
figures = new ArrayList<>(components.size()); + for (ModelComponent comp : components) { + Figure figure = findFigure(comp); + + // At least LocationTypes do not have a Figure! + if (figure != null) { + figures.add(figure); + } + } + drawingView.addToSelection(figures); + + fPropertiesComponent.setModel(new PropertiesCollection(components)); + // Re-select all originally selected objects in the tree. + fComponentsTreeManager.selectItems(components); + } + else { + // In operating mode, only one component can be selected. + selectModelComponent(modelComponent); + } + } + + @Override// GuiManager + public boolean treeComponentRemoved(ModelComponent model) { + boolean componentRemoved = false; + boolean componentRemovedFromFolder = false; + // Point/Location: Remove corresponding Figure. + if (model instanceof PointModel || model instanceof LocationModel) { + LabeledFigure lf = (LabeledFigure) fModelManager.getModel().getFigure(model); + // The drawing will also remove any connected PathConnections or LinkConnections + // that belong to the figure. + fDrawingEditor.getActiveView().getDrawing().remove(lf); + componentRemoved = true; + } + // Link/Path: Remove corresponding Figure. + else if ((model instanceof LinkModel || model instanceof PathModel) + && !(model.getParent() instanceof BlockModel)) { + SimpleLineConnection figure + = (SimpleLineConnection) fModelManager.getModel().getFigure(model); + fDrawingEditor.getActiveView().getDrawing().remove(figure); + componentRemoved = true; + } + // Vehicle + else if (model instanceof VehicleModel) { + componentRemoved = true; + } + else if (model instanceof LocationTypeModel) { + // Search if any Locations of this type exist + for (LocationModel lm : fModelManager.getModel().getLocationModels()) { + if (lm.getLocationType() == model) { + JOptionPane.showMessageDialog( + this, + bundle.getString("openTcsView.optionPane_cannotDeleteLocationType.message"), + bundle.getString("openTcsView.optionPane_cannotDeleteLocationType.title"), + JOptionPane.ERROR_MESSAGE + ); + + return false; + } + } + + componentRemoved = true; + } + + ModelComponent folder = fModelManager.getModel().getFolder(model); + + if (folder != null) { + componentRemovedFromFolder = removeModelComponent(folder, model); + } + + return componentRemoved || componentRemovedFromFolder; + } + + @Override // GuiManager + public void figureSelected(ModelComponent modelComponent) { + modelComponent.addAttributesChangeListener(attributesEventHandler); + fPropertiesComponent.setModel(modelComponent); + + Figure figure = findFigure(modelComponent); + OpenTCSDrawingView drawingView = fDrawingEditor.getActiveView(); + + if (figure != null) { + drawingView.clearSelection(); + drawingView.addToSelection(figure); + // Scroll view to this figure. + drawingView.scrollTo(figure); + } + } + + @Override // GuiManager + public void loadModel() { + if (hasUnsavedChanges()) { + if (!showUnsavedChangesDialog()) { + return; + } + } + + if (!fModelManager.loadModel(null)) { + return; + } + restoreModel(null); + + setHasUnsavedChanges(false); + } + + @Override + public void importModel(PlantModelImporter importer) { + requireNonNull(importer, "importer"); + + if (hasUnsavedChanges()) { + if (!showUnsavedChangesDialog()) { + return; + } + } + + if (!fModelManager.importModel(importer)) { + return; + } + restoreModel(null); + + setHasUnsavedChanges(false); + } + + /** + * Shows a dialog to save unsaved changes. + * + * @return true if the user pressed yes or no, false + * if the user pressed cancel. + */ + private boolean showUnsavedChangesDialog() { + CloseFileAction action = (CloseFileAction) getActionMap().get(CloseFileAction.ID); + action.actionPerformed( + new ActionEvent( + this, + ActionEvent.ACTION_PERFORMED, + CloseFileAction.ID_MODEL_CLOSING + ) + ); + switch (action.getFileSavedStatus()) { + case JOptionPane.YES_OPTION: + super.setHasUnsavedChanges(false); + return true; + case JOptionPane.NO_OPTION: + return true; + case JOptionPane.CANCEL_OPTION: + return false; + default: + return false; + } + } + + /** + * Uploads the current (local) model to the kernel. + * + * @return Whether the model was actually uploaded. + */ + public boolean uploadModelToKernel() { + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + return uploadModelToKernel(sharedPortal.getPortal()); + } + catch (ServiceUnavailableException exc) { + LOG.warn("Exception uploading model", exc); + return false; + } + } + + private boolean uploadModelToKernel(KernelServicePortal portal) { + if (hasUnsavedChanges()) { + JOptionPane.showMessageDialog( + null, + bundle.getString("openTcsView.optionPane_saveModelBeforeUpload.message") + ); + if (fModelManager.saveModelToFile(true)) { + setHasUnsavedChanges(false); + String modelName = fModelManager.getModel().getName(); + setModelNameProperty(modelName); + return uploadModelToKernel(portal); + } + return false; + } + try { + if (portal.getState() != Kernel.State.OPERATING) { + if (userMessageHelper.showConfirmDialog( + bundle.getString("openTcsView.dialog_saveModelConfirmation.title"), + bundle.getString("openTcsView.dialog_saveModelConfirmation.message"), + UserMessageHelper.Type.QUESTION + ) != UserMessageHelper.ReturnType.OK) { + return false; + } + } + boolean didSave = fModelManager.uploadModel(portal); + if (didSave) { + String modelName = fModelManager.getModel().getName(); + setModelNameProperty(modelName); + setHasUnsavedChanges(false); + String persistMsg = bundle.getFormatted( + "openTcsView.message_modelUploaded.text", + modelName + ); + statusPanel.setLogMessage(Level.INFO, persistMsg); + } + return didSave; + } + catch (KernelRuntimeException e) { + LOG.warn("Exception uploading model {}", fModelManager.getModel().getName(), e); + statusPanel.setLogMessage(Level.WARNING, e.getMessage()); + return false; + } + } + + @Override + public boolean saveModel() { + boolean saved = fModelManager.saveModelToFile(false); + if (saved) { + String modelName = fModelManager.getModel().getName(); + setModelNameProperty(modelName); + setHasUnsavedChanges(false); + } + return saved; + } + + @Override // GuiManager + public boolean saveModelAs() { + boolean saved = fModelManager.saveModelToFile(true); + if (saved) { + String modelName = fModelManager.getModel().getName(); + setModelNameProperty(modelName); + setHasUnsavedChanges(false); + } + return saved; + } + + @Override + public void exportModel(PlantModelExporter exporter) { + fModelManager.exportModel(exporter); + } + + @Override + public VehicleModel createVehicleModel() { + VehicleModel vehicleModel = modelComponentFactory.createVehicleModel(); + vehicleModel.getPropertyRouteColor() + .setColor(Colors.unusedVehicleColor(fModelManager.getModel().getVehicleModels())); + addModelComponent(fModelManager.getModel().getFolder(vehicleModel), vehicleModel); + return vehicleModel; + } + + @Override + public LocationTypeModel createLocationTypeModel() { + LocationTypeModel locationTypeModel = modelComponentFactory.createLocationTypeModel(); + addModelComponent(fModelManager.getModel().getFolder(locationTypeModel), locationTypeModel); + return locationTypeModel; + } + + @Override + public BlockModel createBlockModel() { + BlockModel blockModel = modelComponentFactory.createBlockModel(); + blockModel.getPropertyColor() + .setColor(Colors.unusedBlockColor(fModelManager.getModel().getBlockModels())); + addModelComponent(fModelManager.getModel().getFolder(blockModel), blockModel); + return blockModel; + } + + @Override + public void removeBlockModel(BlockModel blockModel) { + requireNonNull(blockModel, "blockModel"); + + removeModelComponent(fModelManager.getModel().getFolder(blockModel), blockModel); + } + + private OpenTCSDrawingView getActiveDrawingView() { + return fDrawingEditor.getActiveView(); + } + + private void removeDrawingView(DefaultSingleCDockable dock) { + if (!viewManager.getDrawingViewMap().containsKey(dock)) { + return; + } + + fDrawingEditor.remove(viewManager.getDrawingViewMap().get(dock).getDrawingView()); + viewManager.removeDockable(dock); + dockingManager.removeDockable(dock); + } + + /** + * Combines the OpenTCSView panel and the panel for the tool bars to a new + * panel. + * + * @return The resulting panel. + */ + private JPanel wrapViewComponent() { + // Add a dummy toolbar for dragging. + // (Preview to see how the tool bar would look like after dragging?) + final JToolBar toolBar = new JToolBar(); + // A wholeComponentPanel for toolbars above the OpenTCSView wholeComponentPanel. + final JPanel toolBarPanel = new JPanel(); + toolBarPanel.setLayout(new BoxLayout(toolBarPanel, BoxLayout.LINE_AXIS)); + toolBar.setBorder(new PaletteToolBarBorder()); + + final List lToolBars = new ArrayList<>(); + + // The new wholeComponentPanel for the whole component. + JPanel wholeComponentPanel = new JPanel(new BorderLayout()); + wholeComponentPanel.add(toolBarPanel, BorderLayout.NORTH); + wholeComponentPanel.add(getComponent()); + lToolBars.add(toolBar); + + JPanel viewComponent = wholeComponentPanel; + + // XXX Why is this list iterated in *reverse* order? + for (JToolBar curToolBar : new ReversedList<>(toolBarManager.getToolBars())) { + // A panel that wraps the toolbar. + final JPanel curToolBarPanel = new JPanel(); + curToolBarPanel.setLayout(new BoxLayout(curToolBarPanel, BoxLayout.LINE_AXIS)); + // A panel that wraps the (wrapped) toolbar and the previous component + // (the whole view and the nested/wrapped toolbars). + JPanel wrappingPanel = new JPanel(new BorderLayout()); + curToolBar.setBorder(new PaletteToolBarBorder()); + + curToolBarPanel.add(curToolBar); + wrappingPanel.add(curToolBarPanel, BorderLayout.NORTH); + wrappingPanel.add(viewComponent); + + lToolBars.add(curToolBar); + viewComponent = wrappingPanel; + } + + for (JToolBar bar : lToolBars) { + configureToolBarButtons(bar); + } + + return viewComponent; + } + + private void configureToolBarButtons(JToolBar bar) { + final Dimension dimButton = new Dimension(32, 34); + for (Component comp : bar.getComponents()) { + if (comp instanceof JButton || comp instanceof JToggleButton) { + JComponent tbButton = (JComponent) comp; + tbButton.setMaximumSize(dimButton); + tbButton.setPreferredSize(dimButton); + tbButton.setBorder(new EtchedBorder()); + } + } + } + + private void closeOpenedPluginPanels() { + for (PluggablePanelFactory factory : panelRegistry.getFactories()) { + showPluginPanel(factory, false); + } + } + + /** + * Initializes modeling state. + */ + private void setModelingState() { + appState.setOperationMode(OperationMode.MODELLING); + Runnable run = new Runnable() { + + @Override + public void run() { + // XXX The event should probably be emitted in ApplicationState now. + eventBus.onEvent( + new OperationModeChangeEvent( + this, + OperationMode.UNDEFINED, + OperationMode.MODELLING + ) + ); + } + }; + + if (SwingUtilities.isEventDispatchThread()) { + // Called from File -> Mode + SwingUtilities.invokeLater(run); + } + else { + try { + // Called from Main.connectKernel() + SwingUtilities.invokeAndWait(run); + } + catch (InterruptedException | InvocationTargetException ex) { + LOG.error("Unexpected exception ", ex); + } + } + + // Switch to selection tool. + eventBus.onEvent(new ResetInteractionToolCommand(this)); + } + + /** + * Adds the given model component to the given folder. + * + * @param folder The folder. + * @param modelComponent The model component to be added. + */ + private void addModelComponent(ModelComponent folder, ModelComponent modelComponent) { + if (folder.contains(modelComponent)) { + return; + } + + // This method is being called by command objects that use undo/redo, so + // avoid calling commands via undo/redo here. + // Make sure the name of the modelComponent is unique + if (requiresName(modelComponent)) { + if (modelComponent.getName().isEmpty() + || modelCompNameGen.hasString(modelComponent.getName())) { + String name = modelCompNameGen.getUniqueString(modelComponent.getClass()); + modelComponent.setName(name); + modelCompNameGen.addString(name); + } + else { + modelCompNameGen.addString(modelComponent.getName()); + } + } + + if (modelComponent instanceof LocationModel) { + LocationModel location = (LocationModel) modelComponent; + // Add a default LocationType to new Locations. + if (location.getLocationType() == null) { + List types = fModelManager.getModel().getLocationTypeModels(); + LocationTypeModel type; + + if (types.isEmpty()) { + type = createLocationTypeModel(); + } + else { + type = types.get(0); + } + + location.setLocationType(type); + location.updateTypeProperty(fModelManager.getModel().getLocationTypeModels()); + } + } + + folder.add(modelComponent); + +// procAdapterUtil.registerProcessAdapter(modelComponent, +// fModelManager.getModel().getProcessAdapterPool()); + fComponentsTreeManager.addItem(folder, modelComponent); + modelComponent.addAttributesChangeListener(attributesEventHandler); + // Notify all locations of a new LocationType. + if (modelComponent instanceof LocationTypeModel) { + List types = fModelManager.getModel().getLocationTypeModels(); + + for (LocationModel location : fModelManager.getModel().getLocationModels()) { + location.updateTypeProperty(types); + } + } + + if (modelComponent instanceof BlockModel) { + BlockModel blockModel = (BlockModel) modelComponent; + fBlocksTreeManager.addItem(folder, modelComponent); + blockModel.addBlockChangeListener(blockEventHandler); + + for (DrawingView drawView : fDrawingEditor.getDrawingViews()) { + ((OpenTCSDrawingViewModeling) drawView).blockAdded(blockModel); + } + } + else if (modelComponent instanceof VehicleModel) { + } + + selectModelComponent(modelComponent); + + setHasUnsavedChanges(true); + } + + /** + * Checks whether the given model component should get a (generated) name if + * it doesn't have any, yet. + * + * @param model The model component to be checked. + * @return true if, and only if, a name should be generated for + * the given component. + */ + private boolean requiresName(ModelComponent model) { + if (model instanceof PointModel + || model instanceof PathModel + || model instanceof LocationTypeModel + || model instanceof LocationModel + || model instanceof BlockModel + || model instanceof LayoutModel + || model instanceof VehicleModel) { + return true; + } + return false; + } + + /** + * Removes the given model component from the given folder. + * + * @param folder The folder. + * @param model The component to be removed. + */ + private boolean removeModelComponent( + ModelComponent folder, + ModelComponent model + ) { + if (!folder.contains(model)) { + return false; + } + + // This method is being called by command objects that use undo/redo, so + // avoid calling commands via undo/redo here. + boolean componentRemoved = false; + + synchronized (model) { + if (!BlockModel.class.isInstance(folder)) { + // don't delete objects from a Blocks folder + synchronized (folder) { + folder.remove(model); + } + + model.removeAttributesChangeListener(attributesEventHandler); + componentRemoved = true; + } + + fPropertiesComponent.reset(); + + if (model instanceof BlockModel) { + BlockModel blockModel = (BlockModel) model; + // Remove Blocks from the Blocks tree + fBlocksTreeManager.removeItem(blockModel); + blockModel.blockRemoved(); + blockModel.removeBlockChangeListener(blockEventHandler); + } + else if (componentRemoved) { + fComponentsTreeManager.removeItem(model); + } + + if (model instanceof LocationTypeModel) { + for (LocationModel location : fModelManager.getModel().getLocationModels()) { + location.updateTypeProperty(fModelManager.getModel().getLocationTypeModels()); + } + } + + modelCompNameGen.removeString(model.getName()); + + setHasUnsavedChanges(true); + } + + return componentRemoved; + } + + /** + * Returns the figure that belongs to the given model component. + * + * @param model The model component. + * @return The figure that belongs to the given model component, or + * null, if there isn't any. + */ + private Figure findFigure(ModelComponent model) { + return fModelManager.getModel().getFigure(model); + } + + private void setSystemModel(SystemModel systemModel) { + requireNonNull(systemModel, "systemModel"); + + long timeBefore = System.currentTimeMillis(); + + // Notify the view's scroll panes about the new systemModel and therefore about the new/changed + // origin. This way they can handle changes made to the origin's scale. + for (DrawingViewScrollPane scrollPane : viewManager.getDrawingViewMap().values()) { + scrollPane.originChanged(systemModel.getDrawingMethod().getOrigin()); + } + + // Clear the name generator + modelCompNameGen.clear(); + fDrawingEditor.setSystemModel(systemModel); + + // --- Undo, Redo, Clipboard --- + Drawing drawing = fDrawingEditor.getDrawing(); + drawing.addUndoableEditListener(fUndoRedoManager); + + fComponentsTreeManager.restoreTreeView(systemModel); + fComponentsTreeManager.sortItems(); + fComponentsTreeManager.getTreeView().getTree().scrollRowToVisible(0); + fBlocksTreeManager.restoreTreeView(systemModel.getMainFolder(SystemModel.FolderKey.BLOCKS)); + fBlocksTreeManager.getTreeView().sortRoot(); + fBlocksTreeManager.getTreeView().getTree().scrollRowToVisible(0); + + // Add Attribute Change Listeners to all objects + for (VehicleModel vehicle : systemModel.getVehicleModels()) { + vehicle.addAttributesChangeListener(attributesEventHandler); + modelCompNameGen.addString(vehicle.getName()); + } + + LayoutModel layout = systemModel.getLayoutModel(); + layout.addAttributesChangeListener(attributesEventHandler); + modelCompNameGen.addString(layout.getName()); + + for (PointModel point : systemModel.getPointModels()) { + point.addAttributesChangeListener(attributesEventHandler); + modelCompNameGen.addString(point.getName()); + } + + for (PathModel path : systemModel.getPathModels()) { + path.addAttributesChangeListener(attributesEventHandler); + modelCompNameGen.addString(path.getName()); + } + + for (LocationTypeModel locationType : systemModel.getLocationTypeModels()) { + locationType.addAttributesChangeListener(attributesEventHandler); + modelCompNameGen.addString(locationType.getName()); + } + + for (LocationModel location : systemModel.getLocationModels()) { + location.addAttributesChangeListener(attributesEventHandler); + modelCompNameGen.addString(location.getName()); + } + + for (LinkModel link : systemModel.getLinkModels()) { + link.addAttributesChangeListener(attributesEventHandler); + modelCompNameGen.addString(link.getName()); + } + + for (BlockModel block : systemModel.getBlockModels()) { + block.addAttributesChangeListener(attributesEventHandler); + block.addBlockChangeListener(blockEventHandler); + modelCompNameGen.addString(block.getName()); + } + + LOG.debug("setSystemModel() took {} ms.", System.currentTimeMillis() - timeBefore); + } + + /** + * Initializes the frame with the toolbars and the dockable elements. + */ + private void initializeFrame() { + if (!SwingUtilities.isEventDispatchThread()) { + try { + SwingUtilities.invokeAndWait(() -> initializeFrame()); + } + catch (InterruptedException | InvocationTargetException e) { + LOG.warn("Exception initializing frame", e); + } + return; + } + + fFrame.getContentPane().removeAll(); + dockingManager.initializeDockables(); + // Frame + fFrame.setLayout(new BorderLayout()); + fFrame.add(wrapViewComponent(), BorderLayout.NORTH); + fFrame.add(dockingManager.getCControl().getContentArea()); + fFrame.add(statusPanel, BorderLayout.SOUTH); + restoreDockables(); + // Ensure that, after initialization, the selection tool is active. + // This needs to be done after the initial drawing views have been set + // up so they reflect the behaviour of the selected tool. + // XXX Maybe there is a better way to ensure this... + toolBarManager.getDragToolButton().doClick(); + toolBarManager.getSelectionToolButton().doClick(); + } + + private void restoreDockables() { + // --- DrawingView for modelling --- + DefaultSingleCDockable modellingDockable = addDrawingView(); + viewManager.initModellingDockable( + modellingDockable, + bundle.getString("openTcsView.panel_modellingDrawingView.title") + ); + + dockingManager.getTabPane(DockingManagerModeling.COURSE_TAB_PANE_ID) + .getStation() + .setFrontDockable(viewManager.evaluateFrontDockable()); + } + + private class AttributesEventHandler + implements + AttributesChangeListener { + + /** + * Creates a new instance. + */ + AttributesEventHandler() { + } + + @Override // AttributesChangeListener + public void propertiesChanged(AttributesChangeEvent event) { + if (event.getInitiator() == this) { + return; + } + + ModelComponent model = event.getModel(); + + // If a model component's name changed, update the blocks this component is a member of + if (model.getPropertyName() != null && model.getPropertyName().hasChanged()) { + fComponentsTreeManager.itemChanged(model); + + fModelManager.getModel().getBlockModels().stream() + .filter(block -> blockAffectedByNameChange(block, model)) + .forEach(block -> updateBlockMembers(block)); + } + + if (model instanceof LayoutModel) { + // Handle scale changes. + LengthProperty pScaleX = (LengthProperty) model.getProperty(LayoutModel.SCALE_X); + LengthProperty pScaleY = (LengthProperty) model.getProperty(LayoutModel.SCALE_Y); + + if (pScaleX.hasChanged() || pScaleY.hasChanged()) { + double scaleX = (double) pScaleX.getValue(); + double scaleY = (double) pScaleY.getValue(); + + if (scaleX != 0.0 && scaleY != 0.0) { + fModelManager.getModel().getDrawingMethod().getOrigin().setScale(scaleX, scaleY); + } + } + } + + if (model instanceof LocationModel) { + if (model.getProperty(LocationModel.TYPE).hasChanged()) { + AbstractProperty p = (AbstractProperty) model.getProperty(LocationModel.TYPE); + LocationTypeModel type + = fModelManager.getModel().getLocationTypeModel((String) p.getValue()); + ((LocationModel) model).setLocationType(type); + if (model != event.getInitiator()) { + model.propertiesChanged(this); + } + } + } + + if (model instanceof LocationTypeModel) { + for (LocationModel locModel : fModelManager.getModel().getLocationModels()) { + locModel.updateTypeProperty(fModelManager.getModel().getLocationTypeModels()); + } + } + } + + private boolean blockAffectedByNameChange(BlockModel block, ModelComponent model) { + return block.getChildComponents().stream().anyMatch(member -> member.equals(model)); + } + + private void updateBlockMembers(BlockModel block) { + List members = new ArrayList<>(); + for (ModelComponent component : block.getChildComponents()) { + members.add(component.getName()); + } + block.getPropertyElements().setItems(members); + } + } + + /** + * Handles events emitted for changes of blocks. + */ + private class BlockEventHandler + implements + BlockChangeListener { + + /** + * Creates a new instance. + */ + BlockEventHandler() { + } + + @Override // BlockChangeListener + public void courseElementsChanged(BlockChangeEvent event) { + BlockModel block = (BlockModel) event.getSource(); + // Remove all children from the block and re-add those that are still there. + fBlocksTreeManager.removeChildren(block); + for (ModelComponent component : block.getChildComponents()) { + fBlocksTreeManager.addItem(block, component); + } + + setHasUnsavedChanges(true); + } + + @Override + public void colorChanged(BlockChangeEvent event) { + } + + @Override // BlockChangeListener + public void blockRemoved(BlockChangeEvent event) { + } + } + + /** + * Handles events emitted by the drawing editor. + */ + private class DrawingEditorEventHandler + implements + DrawingEditorListener { + + /** + * Provides access to the current system model. + */ + private final ModelManager modelManager; + + /** + * Creates a new instance. + * + * @param modelManager Provides access to the current system model. + */ + DrawingEditorEventHandler(ModelManager modelManager) { + this.modelManager = requireNonNull(modelManager, "modelManager"); + } + + @Override // DrawingEditorListener + public void figureAdded(DrawingEditorEvent event) { + Figure figure = event.getFigure(); + ModelComponent model = figure.get(FigureConstants.MODEL); + + // Some figures do not have a model - OriginFigure, for instance. + // XXX Check if we can't unify all figures to have a model. + if (model == null) { + return; + } + + if (figure instanceof AttributesChangeListener) { + model.addAttributesChangeListener((AttributesChangeListener) figure); + } + + // The added figure shall react on changes of the layout's scale. + if (figure instanceof OriginChangeListener) { + Origin ref = modelManager.getModel().getDrawingMethod().getOrigin(); + + if (ref != null) { + ref.addListener((OriginChangeListener) figure); + figure.set(FigureConstants.ORIGIN, ref); + } + } + + fModelManager.getModel().registerFigure(model, figure); + + ModelComponent folder = modelManager.getModel().getFolder(model); + addModelComponent(folder, model); + + if (figure instanceof LabeledFigure) { + ((LabeledFigure) figure).updateModel(); + } + } + + @Override// DrawingEditorListener + public void figureRemoved(DrawingEditorEvent e) { + Figure figure = e.getFigure(); + + if (figure == null) { + return; + } + + ModelComponent model = figure.get(FigureConstants.MODEL); + + if (model == null) { + return; + } + + synchronized (model) { + // The removed figure shouldn't react on changes of the origin any more. + if (figure instanceof OriginChangeListener) { + Origin ref = figure.get(FigureConstants.ORIGIN); + + if (ref != null) { + ref.removeListener((OriginChangeListener) figure); + figure.set(FigureConstants.ORIGIN, null); + } + } + // Disassociate from blocks... + removeFromAllBlocks(model); + // ...and remove the object itself. + ModelComponent folder = modelManager.getModel().getFolder(model); + synchronized (folder) { + removeModelComponent(folder, model); + } + } + } + + @Override + public void figureSelected(DrawingEditorEvent event) { + if (event.getCount() == 0) { + fComponentsTreeManager.selectItems(null); + fBlocksTreeManager.selectItems(null); + } + else if (event.getCount() == 1) { + // Single figure selected. + Figure figure = event.getFigure(); + + if (figure != null) { + ModelComponent model = figure.get(FigureConstants.MODEL); + + if (model != null) { + model.addAttributesChangeListener(attributesEventHandler); + fPropertiesComponent.setModel(model); + fComponentsTreeManager.selectItem(model); + fBlocksTreeManager.selectItem(model); + } + } + } + else { + // Multiple figures selected. + List models = new ArrayList<>(); + Set components = new HashSet<>(); + + for (Figure figure : event.getFigures()) { + ModelComponent model = figure.get(FigureConstants.MODEL); + if (model != null) { + models.add(model); + components.add(model); + } + } + + // Display shared properties of the selected figures. + ModelComponent model = new PropertiesCollection(models); + fComponentsTreeManager.selectItems(components); + fBlocksTreeManager.selectItems(components); + fPropertiesComponent.setModel(model); + } + } + + /** + * Removes a component from all blocks in the model. + * + * @param model The component to be removed. + */ + private void removeFromAllBlocks(ModelComponent model) { + // The (invisible?) root folder of the "Blocks" tree... + ModelComponent mainFolder + = modelManager.getModel().getMainFolder(SystemModel.FolderKey.BLOCKS); + + synchronized (mainFolder) { + // ... contains one folder for each Block + + for (ModelComponent blockModelComp : mainFolder.getChildComponents()) { + BlockModel block = (BlockModel) blockModelComp; + + List elementsToRemove = new ArrayList<>(); + // All child components (Points, Paths) of one Block + for (ModelComponent blockChildComp : block.getChildComponents()) { + if (model == blockChildComp) { + elementsToRemove.add(blockChildComp); + } + } + + if (!elementsToRemove.isEmpty()) { + // At least one component found + for (ModelComponent mc : elementsToRemove) { + block.removeCourseElement(mc); + } + + block.courseElementsChanged(); + } + } + } + } + } + + private class DrawingViewClosingListener + implements + CVetoClosingListener { + + private final DefaultSingleCDockable newDockable; + + DrawingViewClosingListener(DefaultSingleCDockable newDockable) { + this.newDockable = newDockable; + } + + @Override + public void closing(CVetoClosingEvent event) { + } + + @Override + public void closed(CVetoClosingEvent event) { + // A dockable is closeable by default. It isn't closeable + // when switching kernel states and we want to hide additional views + if (newDockable.isCloseable()) { + removeDrawingView(newDockable); + } + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/PlantOverviewStarter.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/PlantOverviewStarter.java new file mode 100644 index 0000000..4b65412 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/PlantOverviewStarter.java @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.jhotdraw.app.Application; +import org.opentcs.guing.common.application.ProgressIndicator; +import org.opentcs.guing.common.application.StartupProgressStatus; +import org.opentcs.guing.common.event.EventLogger; + +/** + * The plant overview application's entry point. + */ +public class PlantOverviewStarter { + + /** + * Our startup progress indicator. + */ + private final ProgressIndicator progressIndicator; + /** + * The enclosing application. + */ + private final Application application; + /** + * The actual document view. + */ + private final OpenTCSView opentcsView; + /** + * Provides logging for events published on the application event bus. + */ + private final EventLogger eventLogger; + + /** + * Creates a new instance. + * + * @param progressIndicator The progress indicator to be used. + * @param application The application to be used. + * @param opentcsView The view to be used. + * @param eventLogger Provides logging for events published on the application event bus. + */ + @Inject + public PlantOverviewStarter( + ProgressIndicator progressIndicator, + Application application, + OpenTCSView opentcsView, + EventLogger eventLogger + ) { + this.progressIndicator = requireNonNull(progressIndicator, "progressIndicator"); + this.application = requireNonNull(application, "application"); + this.opentcsView = requireNonNull(opentcsView, "opentcsView"); + this.eventLogger = requireNonNull(eventLogger, "eventLogger"); + } + + public void startPlantOverview() { + eventLogger.initialize(); + + opentcsView.init(); + progressIndicator.initialize(); + progressIndicator.setProgress(StartupProgressStatus.START_PLANT_OVERVIEW); + progressIndicator.setProgress(StartupProgressStatus.SHOW_PLANT_OVERVIEW); + opentcsView.setApplication(application); + // Start the view. + application.show(opentcsView); + progressIndicator.terminate(); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/ViewManagerModeling.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/ViewManagerModeling.java new file mode 100644 index 0000000..19e6174 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/ViewManagerModeling.java @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application; + +import static java.util.Objects.requireNonNull; + +import bibliothek.gui.dock.common.DefaultSingleCDockable; +import jakarta.inject.Inject; +import java.awt.event.FocusEvent; +import java.io.File; +import java.util.ArrayList; +import org.jhotdraw.draw.DrawingView; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.guing.common.application.AbstractViewManager; +import org.opentcs.guing.common.components.dockable.CStackDockStation; +import org.opentcs.modeleditor.components.dockable.DockingManagerModeling; +import org.opentcs.util.event.EventSource; + +/** + * Manages the mapping of dockables to drawing views, transport order views and + * order sequence views. + */ +public class ViewManagerModeling + extends + AbstractViewManager { + + /** + * Manages the application's docking frames. + */ + private final DockingManagerModeling dockingManager; + /** + * The default modelling dockable. + */ + private DefaultSingleCDockable drawingViewModellingDockable; + + /** + * Creates a new instance. + * + * @param dockingManager Manages the application's docking frames. + * @param eventSource Where this instance registers event listeners. + */ + @Inject + public ViewManagerModeling( + DockingManagerModeling dockingManager, + @ApplicationEventBus + EventSource eventSource + ) { + super(eventSource); + this.dockingManager = requireNonNull(dockingManager, "dockingManager"); + } + + public void init() { + setPlantOverviewStateModelling(); + } + + /** + * Resets all components. + */ + @Override + public void reset() { + super.reset(); + drawingViewModellingDockable = null; + } + + /** + * Initializes the unique modelling dockable. + * + * @param dockable The dockable that will be the modelling dockable. + * @param title The title of this dockable. + */ + public void initModellingDockable(DefaultSingleCDockable dockable, String title) { + drawingViewModellingDockable = requireNonNull(dockable, "dockable"); + drawingViewModellingDockable.setTitleText(requireNonNull(title, "title")); + drawingViewModellingDockable.setCloseable(false); + } + + public void setBitmapToModellingView(File file) { + getDrawingViewMap().get(drawingViewModellingDockable) + .getDrawingView() + .addBackgroundBitmap(file); + } + + /** + * Sets visibility states of all dockables to modelling. + */ + private void setPlantOverviewStateModelling() { + CStackDockStation station + = dockingManager.getTabPane(DockingManagerModeling.COURSE_TAB_PANE_ID).getStation(); + + for (DefaultSingleCDockable dock : new ArrayList<>(getDrawingViewMap().keySet())) { + if (dock != drawingViewModellingDockable) { + // Setting it to closeable = false, so the ClosingListener + // doesn't remove the dockable when it's closed + dock.setCloseable(false); + dockingManager.setDockableVisibility(dock.getUniqueId(), false); + } + } + + dockingManager.showDockable(station, drawingViewModellingDockable, 0); + DrawingView view = getDrawingViewMap().get(drawingViewModellingDockable).getDrawingView(); + view.getComponent().dispatchEvent(new FocusEvent(view.getComponent(), FocusEvent.FOCUS_GAINED)); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/ToolBarManager.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/ToolBarManager.java new file mode 100644 index 0000000..6c7131b --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/ToolBarManager.java @@ -0,0 +1,471 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.action; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.awt.Rectangle; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.swing.Action; +import javax.swing.ButtonGroup; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JToggleButton; +import javax.swing.JToolBar; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.event.ToolAdapter; +import org.jhotdraw.draw.event.ToolEvent; +import org.jhotdraw.draw.event.ToolListener; +import org.jhotdraw.draw.tool.ConnectionTool; +import org.jhotdraw.draw.tool.CreationTool; +import org.jhotdraw.draw.tool.Tool; +import org.jhotdraw.gui.JPopupButton; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.common.application.action.ToolButtonListener; +import org.opentcs.guing.common.application.toolbar.DragTool; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; +import org.opentcs.guing.common.components.drawing.figures.LabeledLocationFigure; +import org.opentcs.guing.common.components.drawing.figures.LabeledPointFigure; +import org.opentcs.guing.common.components.drawing.figures.LinkConnection; +import org.opentcs.guing.common.components.drawing.figures.PathConnection; +import org.opentcs.guing.common.event.ResetInteractionToolCommand; +import org.opentcs.guing.common.util.CourseObjectFactory; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.modeleditor.application.action.actions.CreateBlockAction; +import org.opentcs.modeleditor.application.action.actions.CreateLocationTypeAction; +import org.opentcs.modeleditor.application.action.actions.CreateVehicleAction; +import org.opentcs.modeleditor.application.action.draw.DefaultPointSelectedAction; +import org.opentcs.modeleditor.application.toolbar.CreationToolFactory; +import org.opentcs.modeleditor.application.toolbar.MultipleSelectionTool; +import org.opentcs.modeleditor.application.toolbar.SelectionToolFactory; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.draw.SelectSameAction; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.ButtonFactory; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.draw.DefaultPathSelectedAction; +import org.opentcs.util.event.EventHandler; + +/** + * Sets up and manages a list of tool bars in the graphical user interface. + */ +public class ToolBarManager + implements + EventHandler { + + /** + * A factory for selectiont tools. + */ + private final SelectionToolFactory selectionToolFactory; + /** + * A list of all toolbars. + */ + private final List toolBarList = Collections.synchronizedList(new ArrayList<>()); + /** + * A tool bar for actions creating new items. + */ + private final JToolBar toolBarCreation = new JToolBar(); + /** + * A tool bar for actions regarding alignment. + */ + private final JToolBar toolBarAlignment = new JToolBar(); + /** + * A toggle button for the selection tool. + */ + private final JToggleButton selectionToolButton; + /** + * A toggle button for the drag tool. + */ + private final JToggleButton dragToolButton; + /** + * The actual drag tool. + */ + private DragTool dragTool; + /** + * A button for creating points. + */ + private final JPopupButton buttonCreatePoint; + /** + * A button for creating locations. + */ + private final JToggleButton buttonCreateLocation; + /** + * A button for creating paths. + */ + private final JPopupButton buttonCreatePath; + /** + * A button for creating location links. + */ + private final JToggleButton buttonCreateLink; + /** + * A button for creating location types. + */ + private final JButton buttonCreateLocationType; + /** + * A button for creating vehicles. + */ + private final JButton buttonCreateVehicle; + /** + * A button for creating blocks. + */ + private final JButton buttonCreateBlock; + + /** + * Creates a new instance. + * + * @param actionMap The action map to be used + * @param crsObjFactory A factory for course objects + * @param editor The drawing editor + * @param creationToolFactory The creation tool factory. + * @param selectionToolFactory The selection tool factory + */ + @Inject + public ToolBarManager( + ViewActionMap actionMap, + CourseObjectFactory crsObjFactory, + OpenTCSDrawingEditor editor, + CreationToolFactory creationToolFactory, + SelectionToolFactory selectionToolFactory + ) { + requireNonNull(actionMap, "actionMap"); + requireNonNull(crsObjFactory, "crsObjFactory"); + requireNonNull(editor, "editor"); + requireNonNull(creationToolFactory, "creationToolFactory"); + this.selectionToolFactory = requireNonNull(selectionToolFactory, "selectionToolFactory"); + + ResourceBundleUtil labels + = ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.TOOLBAR_PATH); + + // --- 1. ToolBar: Creation --- + // Selection, Drag | Create Point, Location, Path, Link | + // Create Location Type, Vehicle, Block, Static Route | + // Create Transport Order | Find, Show Vehicles + toolBarCreation.setActionMap(actionMap); + // --- Selection Tool --- + selectionToolButton = addSelectionToolButton(toolBarCreation, editor); + // --- Drag Tool --- + dragToolButton = addDragToolButton(toolBarCreation, editor); + + toolBarCreation.addSeparator(); + + // --- Create Point Figure --- + LabeledPointFigure lpf = crsObjFactory.createPointFigure(); + CreationTool creationTool = creationToolFactory.createCreationTool(lpf); + buttonCreatePoint = pointToolButton(toolBarCreation, editor, creationTool); + creationTool.setToolDoneAfterCreation(false); + + // --- Create Location Figure --- + LabeledLocationFigure llf = crsObjFactory.createLocationFigure(); + creationTool = creationToolFactory.createCreationTool(llf); + buttonCreateLocation + = addToolButton( + toolBarCreation, + editor, + creationTool, + labels.getString("toolBarManager.button_createLocation.tooltipText"), + ImageDirectory.getImageIcon("/toolbar/location.22.png") + ); + creationTool.setToolDoneAfterCreation(false); + + // --- Create Path Figure --- + PathConnection pc = crsObjFactory.createPathConnection(); + ConnectionTool connectionTool = creationToolFactory.createConnectionTool(pc); + buttonCreatePath = pathToolButton(toolBarCreation, editor, connectionTool); + connectionTool.setToolDoneAfterCreation(false); + + // --- Create Link --- + LinkConnection lc = crsObjFactory.createLinkConnection(); + connectionTool = creationToolFactory.createConnectionTool(lc); + buttonCreateLink + = addToolButton( + toolBarCreation, + editor, + connectionTool, + labels.getString("toolBarManager.button_createLink.tooltipText"), + ImageDirectory.getImageIcon("/toolbar/link.22.png") + ); + connectionTool.setToolDoneAfterCreation(false); + + toolBarCreation.addSeparator(); + + // --- Location Type: No Figure, just creates a tree entry --- + buttonCreateLocationType = new JButton(actionMap.get(CreateLocationTypeAction.ID)); + buttonCreateLocationType.setText(null); + toolBarCreation.add(buttonCreateLocationType); + + // --- Create Vehicle Figure --- + buttonCreateVehicle = new JButton(actionMap.get(CreateVehicleAction.ID)); + buttonCreateVehicle.setText(null); + toolBarCreation.add(buttonCreateVehicle); + + // --- Create Block --- + buttonCreateBlock = new JButton(actionMap.get(CreateBlockAction.ID)); + buttonCreateBlock.setText(null); + toolBarCreation.add(buttonCreateBlock); + + toolBarCreation.addSeparator(); + + toolBarCreation.setName(labels.getString("toolBarManager.toolbar_drawing.title")); + toolBarList.add(toolBarCreation); + + // --- 3. ToolBar: Alignment --- + // Align: West, East, Horizontal; North, South, Vertical + // Move: West, East, North, South + // Bring to front, Send to back + ButtonFactory.addAlignmentButtonsTo(toolBarAlignment, editor); + toolBarAlignment.setName(labels.getString("toolBarManager.toolbar_alignment.title")); + toolBarList.add(toolBarAlignment); + } + + public List getToolBars() { + return toolBarList; + } + + public JToolBar getToolBarCreation() { + return toolBarCreation; + } + + public JToggleButton getSelectionToolButton() { + return selectionToolButton; + } + + public JToggleButton getDragToolButton() { + return dragToolButton; + } + + public JPopupButton getButtonCreatePath() { + return buttonCreatePath; + } + + public JToggleButton getButtonCreateLink() { + return buttonCreateLink; + } + + @Override + public void onEvent(Object event) { + if (event instanceof ResetInteractionToolCommand) { + handleToolReset((ResetInteractionToolCommand) event); + } + } + + private void handleToolReset(ResetInteractionToolCommand evt) { + selectionToolButton.setSelected(true); + } + + /** + * Adds the selection tool to the given toolbar. + * + * @param toolBar The toolbar to add to. + * @param editor The DrawingEditor. + */ + private JToggleButton addSelectionToolButton( + JToolBar toolBar, + DrawingEditor editor + ) { + List drawingActions = new ArrayList<>(); + // Drawing Actions + drawingActions.add(new SelectSameAction(editor)); + + MultipleSelectionTool selectionTool + = selectionToolFactory.createMultipleSelectionTool(drawingActions, new ArrayList<>()); + + ButtonGroup buttonGroup; + + if (toolBar.getClientProperty("toolButtonGroup") instanceof ButtonGroup) { + buttonGroup = (ButtonGroup) toolBar.getClientProperty("toolButtonGroup"); + } + else { + buttonGroup = new ButtonGroup(); + toolBar.putClientProperty("toolButtonGroup", buttonGroup); + } + + // Selection tool + editor.setTool(selectionTool); + final JToggleButton toggleButton = new JToggleButton(); + + if (!(toolBar.getClientProperty("toolHandler") instanceof ToolListener)) { + ToolListener toolHandler = new ToolAdapter() { + @Override + public void toolDone(ToolEvent event) { + toggleButton.setSelected(true); + } + }; + + toolBar.putClientProperty("toolHandler", toolHandler); + } + + toggleButton.setIcon(ImageDirectory.getImageIcon("/toolbar/select-2.png")); + toggleButton.setText(null); + toggleButton.setToolTipText( + ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.TOOLBAR_PATH) + .getString("toolBarManager.button_selectionTool.tooltipText") + ); + + toggleButton.setSelected(true); + toggleButton.addItemListener(new ToolButtonListener(selectionTool, editor)); + buttonGroup.add(toggleButton); + toolBar.add(toggleButton); + + return toggleButton; + } + + /** + * + * @param toolBar + * @param editor + */ + private JToggleButton addDragToolButton(JToolBar toolBar, DrawingEditor editor) { + final JToggleButton button = new JToggleButton(); + dragTool = new DragTool(); + editor.setTool(dragTool); + + if (!(toolBar.getClientProperty("toolHandler") instanceof ToolListener)) { + ToolListener toolHandler = new ToolAdapter() { + @Override + public void toolDone(ToolEvent event) { + button.setSelected(true); + } + }; + toolBar.putClientProperty("toolHandler", toolHandler); + } + + URL url = getClass().getResource(ImageDirectory.DIR + "/toolbar/cursor-opened-hand.png"); + button.setIcon(new ImageIcon(url)); + button.setText(null); + button.setToolTipText( + ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.TOOLBAR_PATH) + .getString("toolBarManager.button_dragTool.tooltipText") + ); + + button.setSelected(false); + button.addItemListener(new ToolButtonListener(dragTool, editor)); + + ButtonGroup group = (ButtonGroup) toolBar.getClientProperty("toolButtonGroup"); + group.add(button); + toolBar.add(button); + return button; + } + + /** + * Configures a JPopupButton with all available Point types. + * + * @param toolBar + * @param editor OpenTCSDrawingEditor + * @param tool CreationTool + * @param labelKey + * @param labels + * @return + */ + private JPopupButton pointToolButton( + JToolBar toolBar, + DrawingEditor editor, + Tool tool + ) { + JPopupButton popupButton = new JPopupButton(); + ButtonGroup group = (ButtonGroup) toolBar.getClientProperty("toolButtonGroup"); + popupButton.setAction( + new DefaultPointSelectedAction(editor, tool, popupButton, group), + new Rectangle(0, 0, 16, 16) + ); + ToolListener toolHandler = (ToolListener) toolBar.getClientProperty("toolHandler"); + tool.addToolListener(toolHandler); + + for (PointModel.Type type : PointModel.Type.values()) { + DefaultPointSelectedAction action + = new DefaultPointSelectedAction(editor, tool, type, popupButton, group); + popupButton.add(action); + action.setEnabled(true); + } + + popupButton.setText(null); + popupButton.setToolTipText(PointModel.Type.values()[0].getHelptext()); + popupButton.setIcon(ImageDirectory.getImageIcon("/toolbar/point-halt-arrow.22.png")); + popupButton.setFocusable(true); + + group.add(popupButton); + toolBar.add(popupButton); + + return popupButton; + } + + /** + * Method addSelectionToolButton must have been invoked prior to this on the + * JToolBar. + * + * @param toolBar + * @param editor + * @param tool + * @param toolTipText + * @param labels + * @return + */ + private JToggleButton addToolButton( + JToolBar toolBar, + DrawingEditor editor, + Tool tool, + String toolTipText, + ImageIcon iconBase + ) { + JToggleButton toggleButton = new JToggleButton(); + + toggleButton.setIcon(iconBase); + toggleButton.setText(null); + toggleButton.setToolTipText(toolTipText); + toggleButton.addItemListener(new ToolButtonListener(tool, editor)); + + ToolListener toolHandler = (ToolListener) toolBar.getClientProperty("toolHandler"); + tool.addToolListener(toolHandler); + + ButtonGroup group = (ButtonGroup) toolBar.getClientProperty("toolButtonGroup"); + group.add(toggleButton); + toolBar.add(toggleButton); + + return toggleButton; + } + + /** + * Configures a JPopupButton with all available path types. + * + * @param toolBar + * @param editor + * @param tool + * @param labels + * @param types + * @return + */ + private JPopupButton pathToolButton( + JToolBar toolBar, + DrawingEditor editor, + Tool tool + ) { + JPopupButton popupButton = new JPopupButton(); + ButtonGroup group = (ButtonGroup) toolBar.getClientProperty("toolButtonGroup"); + popupButton.setAction( + new DefaultPathSelectedAction(editor, tool, popupButton, group), + new Rectangle(0, 0, 16, 16) + ); + ToolListener toolHandler = (ToolListener) toolBar.getClientProperty("toolHandler"); + tool.addToolListener(toolHandler); + + for (PathModel.Type type : PathModel.Type.values()) { + DefaultPathSelectedAction action + = new DefaultPathSelectedAction(editor, tool, type, popupButton, group); + popupButton.add(action); + action.setEnabled(true); + } + + popupButton.setText(null); + popupButton.setToolTipText(PathModel.Type.values()[0].getHelptext()); + popupButton.setIcon(ImageDirectory.getImageIcon("/toolbar/path-direct-arrow.22.png")); + popupButton.setFocusable(true); + + group.add(popupButton); + toolBar.add(popupButton); + + return popupButton; + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/ViewActionMap.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/ViewActionMap.java new file mode 100644 index 0000000..ff5a0de --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/ViewActionMap.java @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.action; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import javax.swing.ActionMap; +import org.opentcs.guing.common.application.action.file.ModelPropertiesAction; +import org.opentcs.guing.common.application.action.file.SaveModelAction; +import org.opentcs.guing.common.application.action.file.SaveModelAsAction; +import org.opentcs.modeleditor.application.OpenTCSView; +import org.opentcs.modeleditor.application.action.actions.CreateBlockAction; +import org.opentcs.modeleditor.application.action.actions.CreateLocationTypeAction; +import org.opentcs.modeleditor.application.action.actions.CreateVehicleAction; +import org.opentcs.modeleditor.application.action.app.AboutAction; +import org.opentcs.modeleditor.application.action.file.DownloadModelFromKernelAction; +import org.opentcs.modeleditor.application.action.file.LoadModelAction; +import org.opentcs.modeleditor.application.action.file.NewModelAction; +import org.opentcs.modeleditor.application.action.file.UploadModelToKernelAction; +import org.opentcs.modeleditor.application.action.view.AddBitmapAction; +import org.opentcs.modeleditor.application.action.view.RestoreDockingLayoutAction; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.DeleteAction; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.SelectAllAction; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.UndoRedoManager; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.edit.ClearSelectionAction; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.edit.CopyAction; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.edit.CutAction; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.edit.DuplicateAction; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.edit.PasteAction; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.file.CloseFileAction; + +/** + * A custom ActionMap for the plant overview application. + */ +public class ViewActionMap + extends + ActionMap { + + /** + * Creates a new instance. + * + * @param view The openTCS view + * @param undoRedoManager The undo redo manager + * @param aboutAction The action to show the about window + * @param modelPropertiesAction The action to show some model properties. + * @param loadModelFromKernelAction The action to load the current kernel model. + */ + @Inject + @SuppressWarnings("this-escape") + public ViewActionMap( + OpenTCSView view, + UndoRedoManager undoRedoManager, + AboutAction aboutAction, + ModelPropertiesAction modelPropertiesAction, + DownloadModelFromKernelAction loadModelFromKernelAction + ) { + requireNonNull(view, "view"); + requireNonNull(undoRedoManager, "undoRedoManager"); + requireNonNull(aboutAction, "aboutAction"); + + // --- Menu File --- + put(NewModelAction.ID, new NewModelAction(view)); + put(LoadModelAction.ID, new LoadModelAction(view)); + put(SaveModelAction.ID, new SaveModelAction(view)); + put(SaveModelAsAction.ID, new SaveModelAsAction(view)); + put(ModelPropertiesAction.ID, modelPropertiesAction); + put(CloseFileAction.ID, new CloseFileAction(view)); + + // --- Menu Synchronize --- + put(UploadModelToKernelAction.ID, new UploadModelToKernelAction(view)); + put(DownloadModelFromKernelAction.ID, loadModelFromKernelAction); + + // --- Menu Edit --- + // Undo, Redo + put(UndoRedoManager.UNDO_ACTION_ID, undoRedoManager.getUndoAction()); + put(UndoRedoManager.REDO_ACTION_ID, undoRedoManager.getRedoAction()); + // Cut, Copy, Paste, Duplicate, Delete + put(CutAction.ID, new CutAction()); + put(CopyAction.ID, new CopyAction()); + put(PasteAction.ID, new PasteAction()); + put(DuplicateAction.ID, new DuplicateAction()); + put(DeleteAction.ID, new DeleteAction()); + // Select all, Clear selection + put(SelectAllAction.ID, new SelectAllAction()); + put(ClearSelectionAction.ID, new ClearSelectionAction()); + + // --- Menu Actions --- + // Menu item Actions -> Create ... + put(CreateLocationTypeAction.ID, new CreateLocationTypeAction(view)); + put(CreateVehicleAction.ID, new CreateVehicleAction(view)); + put(CreateBlockAction.ID, new CreateBlockAction(view)); + + // --- Menu View --- + put(AddBitmapAction.ID, new AddBitmapAction(view)); + put(RestoreDockingLayoutAction.ID, new RestoreDockingLayoutAction(view)); + + // --- Menu Help --- + put(AboutAction.ID, aboutAction); + } + +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/actions/CreateBlockAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/actions/CreateBlockAction.java new file mode 100644 index 0000000..a2550f7 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/actions/CreateBlockAction.java @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.action.actions; + +import static javax.swing.Action.LARGE_ICON_KEY; +import static javax.swing.Action.SMALL_ICON; +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.TOOLBAR_PATH; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import javax.swing.ImageIcon; +import org.opentcs.guing.common.application.GuiManagerModeling; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * An action to trigger the creation of a block. + */ +public class CreateBlockAction + extends + AbstractAction { + + /** + * This action class's ID. + */ + public static final String ID = "openTCS.createBlock"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(TOOLBAR_PATH); + /** + * The GUI manager instance we're working with. + */ + private final GuiManagerModeling guiManager; + + /** + * Creates a new instance. + * + * @param guiManager The GUI manager instance we're working with. + */ + @SuppressWarnings("this-escape") + public CreateBlockAction(GuiManagerModeling guiManager) { + this.guiManager = guiManager; + + putValue(NAME, BUNDLE.getString("createBlockAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("createBlockAction.shortDescription")); + + ImageIcon iconSmall = ImageDirectory.getImageIcon("/toolbar/blockdevice-3.16.png"); + ImageIcon iconLarge = ImageDirectory.getImageIcon("/toolbar/blockdevice-3.22.png"); + putValue(SMALL_ICON, iconSmall); + putValue(LARGE_ICON_KEY, iconLarge); + } + + @Override + public void actionPerformed(ActionEvent evt) { + guiManager.createBlockModel(); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/actions/CreateLocationTypeAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/actions/CreateLocationTypeAction.java new file mode 100644 index 0000000..c29e773 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/actions/CreateLocationTypeAction.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.action.actions; + +import static javax.swing.Action.LARGE_ICON_KEY; +import static javax.swing.Action.SMALL_ICON; +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.TOOLBAR_PATH; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import javax.swing.ImageIcon; +import org.opentcs.guing.common.application.GuiManagerModeling; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * An action to trigger the creation of a location type. + */ +public class CreateLocationTypeAction + extends + AbstractAction { + + /** + * This action class's ID. + */ + public static final String ID = "openTCS.createLocationType"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(TOOLBAR_PATH); + /** + * The GUI manager instance we're working with. + */ + private final GuiManagerModeling guiManager; + + /** + * Creates a new instance. + * + * @param guiManager The GUI manager instance we're working with. + */ + @SuppressWarnings("this-escape") + public CreateLocationTypeAction(GuiManagerModeling guiManager) { + this.guiManager = guiManager; + + putValue(NAME, BUNDLE.getString("createLocationTypeAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("createLocationTypeAction.shortDescription")); + + ImageIcon icon = ImageDirectory.getImageIcon("/toolbar/locationType.22.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + } + + @Override + public void actionPerformed(ActionEvent evt) { + guiManager.createLocationTypeModel(); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/actions/CreateVehicleAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/actions/CreateVehicleAction.java new file mode 100644 index 0000000..832a3dc --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/actions/CreateVehicleAction.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.action.actions; + +import static javax.swing.Action.LARGE_ICON_KEY; +import static javax.swing.Action.SMALL_ICON; +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.TOOLBAR_PATH; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import javax.swing.ImageIcon; +import org.opentcs.guing.common.application.GuiManagerModeling; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * An action to trigger the creation of a vehicle. + */ +public class CreateVehicleAction + extends + AbstractAction { + + /** + * This action class's ID. + */ + public static final String ID = "openTCS.createVehicle"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(TOOLBAR_PATH); + /** + * The GUI manager instance we're working with. + */ + private final GuiManagerModeling guiManager; + + /** + * Creates a new instance. + * + * @param guiManager The GUI manager instance we're working with. + */ + @SuppressWarnings("this-escape") + public CreateVehicleAction(GuiManagerModeling guiManager) { + this.guiManager = guiManager; + + putValue(NAME, BUNDLE.getString("createVehicleAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("createVehicleAction.shortDescription")); + + ImageIcon icon = ImageDirectory.getImageIcon("/toolbar/car.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + } + + @Override + public void actionPerformed(ActionEvent evt) { + guiManager.createVehicleModel(); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/app/AboutAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/app/AboutAction.java new file mode 100644 index 0000000..19d93e3 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/app/AboutAction.java @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.action.app; + +import static java.util.Objects.requireNonNull; +import static javax.swing.Action.LARGE_ICON_KEY; +import static javax.swing.Action.MNEMONIC_KEY; +import static javax.swing.Action.SMALL_ICON; +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.MENU_PATH; + +import jakarta.inject.Inject; +import java.awt.Component; +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import javax.swing.ImageIcon; +import javax.swing.JOptionPane; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.guing.common.application.ApplicationState; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.modeleditor.application.OpenTCSView; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.util.Environment; + +/** + * Displays a dialog showing information about the application. + */ +public class AboutAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "application.about"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + /** + * Stores the application's current state. + */ + private final ApplicationState appState; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * The parent component for dialogs shown by this action. + */ + private final Component dialogParent; + + /** + * Creates a new instance. + * + * @param appState Stores the application's current state. + * @param portalProvider Provides access to a portal. + * @param dialogParent The parent component for dialogs shown by this action. + */ + @Inject + @SuppressWarnings("this-escape") + public AboutAction( + ApplicationState appState, + SharedKernelServicePortalProvider portalProvider, + @ApplicationFrame + Component dialogParent + ) { + this.appState = requireNonNull(appState, "appState"); + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.dialogParent = requireNonNull(dialogParent, "dialogParent"); + + putValue(NAME, BUNDLE.getString("aboutAction.name")); + putValue(MNEMONIC_KEY, Integer.valueOf('A')); + + ImageIcon icon = ImageDirectory.getImageIcon("/menu/help-contents.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + } + + @Override + public void actionPerformed(ActionEvent evt) { + JOptionPane.showMessageDialog( + dialogParent, + "

" + OpenTCSView.NAME + "
" + + BUNDLE.getFormatted( + "aboutAction.optionPane_applicationInformation.message.baselineVersion", + Environment.getBaselineVersion() + ) + + "
" + + BUNDLE.getFormatted( + "aboutAction.optionPane_applicationInformation.message.customization", + Environment.getCustomizationName(), + Environment.getCustomizationVersion() + ) + + "
" + + BUNDLE.getString( + "aboutAction.optionPane_applicationInformation.message.copyright" + ) + + "
" + + BUNDLE.getString( + "aboutAction.optionPane_applicationInformation.message.runningOn" + ) + + "
" + + "Java: " + + System.getProperty("java.version") + + ", " + + System.getProperty("java.vendor") + + "
" + + "JVM: " + + System.getProperty("java.vm.version") + + ", " + + System.getProperty("java.vm.vendor") + + "
" + + "OS: " + + System.getProperty("os.name") + + " " + + System.getProperty("os.version") + + ", " + + System.getProperty("os.arch") + + "
" + + "Kernel
" + + portalProvider.getPortalDescription() + + "
" + + BUNDLE.getFormatted( + "aboutAction.optionPane_applicationInformation.message.mode", + appState.getOperationMode() + ) + + "

", + BUNDLE.getString( + "aboutAction.optionPane_applicationInformation.title" + ), + JOptionPane.PLAIN_MESSAGE, + new ImageIcon( + getClass().getResource("/org/opentcs/guing/res/symbols/openTCS/openTCS.300x132.gif") + ) + ); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/draw/DefaultPointSelectedAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/draw/DefaultPointSelectedAction.java new file mode 100644 index 0000000..441ecd7 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/draw/DefaultPointSelectedAction.java @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.action.draw; + +import static java.util.Objects.requireNonNull; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import javax.swing.ButtonGroup; +import javax.swing.ImageIcon; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.action.AbstractSelectedAction; +import org.jhotdraw.draw.tool.CreationTool; +import org.jhotdraw.draw.tool.Tool; +import org.jhotdraw.gui.JPopupButton; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.common.components.drawing.figures.LabeledPointFigure; +import org.opentcs.guing.common.components.drawing.figures.PointFigure; +import org.opentcs.guing.common.util.ImageDirectory; + +/** + * This action manages the behaviour when the user selects the point button. + */ +public class DefaultPointSelectedAction + extends + AbstractSelectedAction { + + /** + * The SelectionProperty contains all point types in the model. + */ + private final PointModel.Type pointType; + + private final Tool tool; + /** + * The button this action belongs to. + */ + private final JPopupButton popupButton; + /** + * The Icon the popup button uses when this action is selected. + */ + private final ImageIcon largeIcon; + /** + * The ButtonGroup the popupButton belongs to. It is necessary to know it, + * because + * DrawingEditor.setTool() doesn't select or deselect the + * popupButton, so we have to do it manually. + */ + private final ButtonGroup group; + + /** + * Constructor for an action of a button in the toolbar. + * + * @param editor The drawing editor + * @param tool The tool + * @param popupButton The popup button + * @param group The button group + */ + public DefaultPointSelectedAction( + DrawingEditor editor, + Tool tool, + JPopupButton popupButton, + ButtonGroup group + ) { + super(editor); + this.tool = requireNonNull(tool); + this.popupButton = requireNonNull(popupButton); + this.group = requireNonNull(group); + + this.pointType = null; + this.largeIcon = null; + } + + /** + * Constructor for a button inside a drop down menu of another button. + * + * @param editor The drawing editor + * @param tool The tool + * @param pointType The point type + * @param popupButton The popup button + * @param group The button group + */ + @SuppressWarnings("this-escape") + public DefaultPointSelectedAction( + DrawingEditor editor, + Tool tool, + PointModel.Type pointType, + JPopupButton popupButton, + ButtonGroup group + ) { + super(editor); + this.pointType = requireNonNull(pointType); + this.tool = requireNonNull(tool); + this.popupButton = requireNonNull(popupButton); + this.group = requireNonNull(group); + + this.largeIcon = getLargeImageIconByType(pointType); + + putValue(AbstractAction.NAME, pointType.getDescription()); + putValue(AbstractAction.SHORT_DESCRIPTION, pointType.getHelptext()); + putValue(AbstractAction.SMALL_ICON, getImageIconByType(pointType)); + } + + @Override + public void actionPerformed(ActionEvent e) { + if (pointType != null) { + CreationTool creationTool = (CreationTool) tool; + LabeledPointFigure lpf = (LabeledPointFigure) creationTool.getPrototype(); + PointFigure pointFigure = lpf.getPresentationFigure(); + pointFigure.getModel().getPropertyType().setValue(pointType); + + popupButton.setText(null); + popupButton.setToolTipText(pointType.getHelptext()); + popupButton.setIcon(largeIcon); + } + + getEditor().setTool(tool); + group.setSelected(popupButton.getModel(), true); + } + + @Override + protected void updateEnabledState() { + setEnabled(getView() != null && getView().isEnabled()); + } + + private ImageIcon getImageIconByType(PointModel.Type pointType) { + switch (pointType) { + case HALT: + return ImageDirectory.getImageIcon("/toolbar/point-halt.22.png"); + case PARK: + return ImageDirectory.getImageIcon("/toolbar/point-park.22.png"); + default: + return null; + } + } + + private ImageIcon getLargeImageIconByType(PointModel.Type pointType) { + switch (pointType) { + case HALT: + return ImageDirectory.getImageIcon("/toolbar/point-halt-arrow.22.png"); + case PARK: + return ImageDirectory.getImageIcon("/toolbar/point-park-arrow.22.png"); + default: + return null; + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/file/DownloadModelFromKernelAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/file/DownloadModelFromKernelAction.java new file mode 100644 index 0000000..0f123e6 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/file/DownloadModelFromKernelAction.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.action.file; + +import static java.util.Objects.requireNonNull; +import static javax.swing.Action.ACCELERATOR_KEY; +import static javax.swing.Action.MNEMONIC_KEY; +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.MENU_PATH; + +import jakarta.inject.Inject; +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import javax.swing.KeyStroke; +import org.opentcs.modeleditor.application.OpenTCSView; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * An action to load the current kernel model in the plant overview. + */ +public class DownloadModelFromKernelAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "file.downloadModelFromKernel"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + /** + * The OpenTCS view. + */ + private final OpenTCSView openTCSView; + + /** + * Creates a new instance. + * + * @param openTCSView The openTCS view. + */ + @Inject + @SuppressWarnings("this-escape") + public DownloadModelFromKernelAction(OpenTCSView openTCSView) { + this.openTCSView = requireNonNull(openTCSView); + + putValue(NAME, BUNDLE.getString("downloadModelFromKernelAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("downloadModelFromKernelAction.shortDescription")); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke("alt K")); + putValue(MNEMONIC_KEY, Integer.valueOf('K')); + } + + @Override + public void actionPerformed(ActionEvent e) { + openTCSView.downloadModelFromKernel(); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/file/ExportPlantModelAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/file/ExportPlantModelAction.java new file mode 100644 index 0000000..3d1bc38 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/file/ExportPlantModelAction.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.action.file; + +import static java.util.Objects.requireNonNull; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import org.opentcs.components.plantoverview.PlantModelExporter; +import org.opentcs.guing.common.application.GuiManager; + +/** + */ +public class ExportPlantModelAction + extends + AbstractAction { + + private final PlantModelExporter exporter; + private final GuiManager guiManager; + + /** + * Creates a new instance. + * + * @param exporter The importer. + * @param guiManager The gui manager + */ + @SuppressWarnings("this-escape") + public ExportPlantModelAction(PlantModelExporter exporter, GuiManager guiManager) { + this.exporter = requireNonNull(exporter, "exporter"); + this.guiManager = requireNonNull(guiManager, "guiManager"); + this.putValue(NAME, exporter.getDescription()); + } + + @Override + public void actionPerformed(ActionEvent evt) { + guiManager.exportModel(exporter); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/file/ImportPlantModelAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/file/ImportPlantModelAction.java new file mode 100644 index 0000000..7feeb76 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/file/ImportPlantModelAction.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.action.file; + +import static java.util.Objects.requireNonNull; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import org.opentcs.components.plantoverview.PlantModelImporter; +import org.opentcs.guing.common.application.GuiManager; + +/** + */ +public class ImportPlantModelAction + extends + AbstractAction { + + private final PlantModelImporter importer; + private final GuiManager guiManager; + + /** + * Creates a new instance. + * + * @param importer The importer. + * @param guiManager The gui manager + */ + @SuppressWarnings("this-escape") + public ImportPlantModelAction(PlantModelImporter importer, GuiManager guiManager) { + this.importer = requireNonNull(importer, "importer"); + this.guiManager = requireNonNull(guiManager, "guiManager"); + this.putValue(NAME, importer.getDescription()); + } + + @Override + public void actionPerformed(ActionEvent evt) { + guiManager.importModel(importer); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/file/LoadModelAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/file/LoadModelAction.java new file mode 100644 index 0000000..211dc19 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/file/LoadModelAction.java @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.action.file; + +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.MENU_PATH; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import javax.swing.ImageIcon; +import javax.swing.KeyStroke; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + */ +public class LoadModelAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "file.loadModel"; + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + private final GuiManager view; + + /** + * Creates a new instance. + * + * @param view The gui manager + */ + @SuppressWarnings("this-escape") + public LoadModelAction(GuiManager view) { + this.view = view; + + putValue(NAME, BUNDLE.getString("loadModelAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("loadModelAction.shortDescription")); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke("ctrl L")); + putValue(MNEMONIC_KEY, Integer.valueOf('L')); + + ImageIcon icon = ImageDirectory.getImageIcon("/menu/document-import-2.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + } + + @Override + public void actionPerformed(ActionEvent evt) { + view.loadModel(); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/file/NewModelAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/file/NewModelAction.java new file mode 100644 index 0000000..2933bc0 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/file/NewModelAction.java @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.action.file; + +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.MENU_PATH; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import javax.swing.ImageIcon; +import javax.swing.KeyStroke; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + */ +public class NewModelAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "file.newModel"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + + private final GuiManager view; + + /** + * Creates a new instance. + * + * @param view The gui manager + */ + @SuppressWarnings("this-escape") + public NewModelAction(GuiManager view) { + this.view = view; + + putValue(NAME, BUNDLE.getString("newModelAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("newModelAction.shortDescription")); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke("ctrl N")); + putValue(MNEMONIC_KEY, Integer.valueOf('N')); + + ImageIcon icon = ImageDirectory.getImageIcon("/menu/document-new.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + } + + @Override + public void actionPerformed(ActionEvent evt) { + view.createEmptyModel(); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/file/UploadModelToKernelAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/file/UploadModelToKernelAction.java new file mode 100644 index 0000000..6488337 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/file/UploadModelToKernelAction.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.action.file; + +import static javax.swing.Action.ACCELERATOR_KEY; +import static javax.swing.Action.MNEMONIC_KEY; +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.MENU_PATH; + +import java.awt.event.ActionEvent; +import java.util.Objects; +import javax.swing.AbstractAction; +import javax.swing.KeyStroke; +import org.opentcs.modeleditor.application.OpenTCSView; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * An action to upload the (local) model to the kernel. + */ +public class UploadModelToKernelAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "file.uploadModelToKernel"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + + private final OpenTCSView openTCSView; + + /** + * Creates a new instance. + * + * @param openTCSView The openTCS view + */ + @SuppressWarnings("this-escape") + public UploadModelToKernelAction(OpenTCSView openTCSView) { + this.openTCSView = Objects.requireNonNull(openTCSView); + + putValue(NAME, BUNDLE.getString("uploadModelToKernelAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("uploadModelToKernelAction.shortDescription")); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke("alt P")); + putValue(MNEMONIC_KEY, Integer.valueOf('P')); + + } + + @Override + public void actionPerformed(ActionEvent e) { + openTCSView.uploadModelToKernel(); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/view/AddBitmapAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/view/AddBitmapAction.java new file mode 100644 index 0000000..376beb5 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/view/AddBitmapAction.java @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.action.view; + +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.MENU_PATH; + +import java.awt.event.ActionEvent; +import java.io.File; +import java.util.Objects; +import javax.swing.AbstractAction; +import javax.swing.JFileChooser; +import javax.swing.filechooser.FileNameExtensionFilter; +import org.opentcs.modeleditor.application.OpenTCSView; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Actions for adding background bitmaps to the drawing view. + */ +public class AddBitmapAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "view.addBitmap"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + + private final JFileChooser fc; + + private final OpenTCSView view; + + /** + * Creates a new instance. + * + * @param view The openTCS view + */ + @SuppressWarnings("this-escape") + public AddBitmapAction(OpenTCSView view) { + this.view = Objects.requireNonNull(view, "view"); + + this.fc = new JFileChooser(System.getProperty("opentcs.home")); + this.fc.setFileFilter( + new FileNameExtensionFilter( + "Bitmaps (PNG, JPG, BMP, GIF)", + "png", + "jpg", + "bmp", + "gif" + ) + ); + this.fc.setFileSelectionMode(JFileChooser.FILES_ONLY); + + putValue(NAME, BUNDLE.getString("addBitmapAction.name")); + } + + @Override + public void actionPerformed(ActionEvent e) { + int returnVal = fc.showOpenDialog(null); + + if (returnVal == JFileChooser.APPROVE_OPTION) { + File file = fc.getSelectedFile(); + view.addBackgroundBitmap(file); + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/view/RestoreDockingLayoutAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/view/RestoreDockingLayoutAction.java new file mode 100644 index 0000000..196f292 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/action/view/RestoreDockingLayoutAction.java @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.action.view; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import org.opentcs.modeleditor.application.OpenTCSView; + +/** + * Action for resetting the docking layout. + */ +public class RestoreDockingLayoutAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "openTCS.restoreDockingLayout"; + private final OpenTCSView view; + + /** + * Creates a new instance. + * + * @param view The openTCS view + */ + public RestoreDockingLayoutAction(OpenTCSView view) { + this.view = view; + } + + @Override + public void actionPerformed(ActionEvent e) { + view.resetWindowArrangement(); + } + +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/CalculatePathLengthMenuItem.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/CalculatePathLengthMenuItem.java new file mode 100644 index 0000000..8214333 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/CalculatePathLengthMenuItem.java @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.menus; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import java.awt.event.ActionEvent; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import org.jhotdraw.draw.DrawingEditor; +import org.opentcs.guing.base.components.properties.event.NullAttributesChangeListener; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.common.components.dialogs.StandardContentDialog; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; +import org.opentcs.guing.common.components.drawing.figures.FigureConstants; +import org.opentcs.guing.common.components.properties.PropertyUndoActivity; +import org.opentcs.modeleditor.components.dialog.PathTypeSelectionPanel; +import org.opentcs.modeleditor.math.path.PathLengthFunction; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.UndoRedoManager; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.util.gui.Icons; + +/** + * A menu item to calculate and update the lengths of paths. + */ +public class CalculatePathLengthMenuItem + extends + JMenuItem { + + /** + * The DrawingEditor instance. + */ + private final DrawingEditor drawingEditor; + /** + * The UndoRedoManager instance to be used. + */ + private final UndoRedoManager undoRedoManager; + /** + * The calculator to use to calculate the path length. + */ + private final Provider pathLengthFunctionProvider; + + @Inject + @SuppressWarnings("this-escape") + public CalculatePathLengthMenuItem( + OpenTCSDrawingEditor drawingEditor, + UndoRedoManager undoRedoManager, + Provider pathLengthFunctionProvider + ) { + super( + ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MENU_PATH) + .getString("calculatePathLengthMenuItem.text") + ); + this.drawingEditor = requireNonNull(drawingEditor, "drawingEditor"); + this.undoRedoManager = requireNonNull(undoRedoManager, "undoRedoManager"); + this.pathLengthFunctionProvider + = requireNonNull(pathLengthFunctionProvider, "pathLengthFunctionProvider"); + + addActionListener(this::calculatePathLength); + } + + private void calculatePathLength(ActionEvent e) { + PathTypeSelectionPanel content = new PathTypeSelectionPanel(); + StandardContentDialog dialog + = new StandardContentDialog(JOptionPane.getFrameForComponent(this), content); + dialog.setIconImages(Icons.getOpenTCSIcons()); + dialog.setVisible(true); + + if (dialog.getReturnStatus() != StandardContentDialog.RET_OK) { + return; + } + + PathLengthFunction pathLengthFunction = pathLengthFunctionProvider.get(); + + drawingEditor.getActiveView().getDrawing().getFiguresFrontToBack().stream() + .map(figure -> figure.get(FigureConstants.MODEL)) + .filter(model -> model instanceof PathModel) + .map(model -> (PathModel) model) + .filter(path -> content.isPathTypeSelected(connectionType(path))) + .forEach(path -> updatePath(path, pathLengthFunction)); + } + + private PathModel.Type connectionType(PathModel path) { + return (PathModel.Type) path.getPropertyPathConnType().getValue(); + } + + private void updatePath(PathModel path, PathLengthFunction pathLengthFunction) { + updatePathLength(path, Math.round(pathLengthFunction.applyAsDouble(path))); + path.propertiesChanged(new NullAttributesChangeListener()); + } + + private void updatePathLength(PathModel path, double length) { + LengthProperty pathLengthProperty = path.getPropertyLength(); + + PropertyUndoActivity pua = new PropertyUndoActivity(pathLengthProperty); + pua.snapShotBeforeModification(); + + pathLengthProperty.setValueAndUnit(length, LengthProperty.Unit.MM); + pathLengthProperty.markChanged(); + + pua.snapShotAfterModification(); + undoRedoManager.addEdit(pua); + } + +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/LayoutToModelCoordinateUndoActivity.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/LayoutToModelCoordinateUndoActivity.java new file mode 100644 index 0000000..02cc726 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/LayoutToModelCoordinateUndoActivity.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.menus; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import org.opentcs.guing.base.components.properties.type.CoordinateProperty; +import org.opentcs.guing.common.components.properties.CoordinateUndoActivity; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + */ +public class LayoutToModelCoordinateUndoActivity + extends + CoordinateUndoActivity { + + @Inject + public LayoutToModelCoordinateUndoActivity( + @Assisted + CoordinateProperty property, + ModelManager modelManager + ) { + super(property, modelManager); + } + + @Override + public String getPresentationName() { + return ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MISC_PATH) + .getString("layoutToModelCoordinateUndoActivity.presentationName"); + } + + @Override + protected void saveTransformBeforeModification() { + } + + @Override + protected void saveTransformForUndo() { + } + + @Override + protected void saveTransformForRedo() { + } + +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/LayoutToModelMenuItem.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/LayoutToModelMenuItem.java new file mode 100644 index 0000000..3bb94f0 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/LayoutToModelMenuItem.java @@ -0,0 +1,185 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.menus; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import javax.swing.ImageIcon; +import javax.swing.JMenuItem; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.Figure; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.model.visualization.ElementPropKeys; +import org.opentcs.guing.base.components.properties.event.NullAttributesChangeListener; +import org.opentcs.guing.base.components.properties.type.CoordinateProperty; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; +import org.opentcs.guing.common.components.drawing.figures.FigureConstants; +import org.opentcs.guing.common.components.properties.CoordinateUndoActivity; +import org.opentcs.guing.common.event.ResetInteractionToolCommand; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.UndoRedoManager; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.util.event.EventHandler; + +/** + * A menu item for copying the value of the layout properties of selected points + * or locations to the corresponding model properties. + */ +public class LayoutToModelMenuItem + extends + JMenuItem { + + /** + * The DrawingEditor instance. + */ + private final DrawingEditor drawingEditor; + /** + * The UndoRedoManager instance to be used. + */ + private final UndoRedoManager undoRedoManager; + /** + * Where we send events. + */ + private final EventHandler eventHandler; + /** + * The components factory. + */ + private final MenuItemComponentsFactory componentsFactory; + /** + * A flag if the values of ALL points and location shall be copied when + * the menu item is clicked. If false only the selected figures will be + * considered. + */ + private final boolean copyAll; + + /** + * Creates a new instance. + * + * @param drawingEditor A DrawingEditor instance. + * @param undoRedoManager The application's undo/redo manager. + * @param eventHandler Where this instance sends events. + * @param componentsFactory The components factory. + * @param copyAll Indicates whether the values of ALL points and locations + * shall be copied when the menu item is clicked. If false only the selected + * figures will be considered. + */ + @Inject + @SuppressWarnings("this-escape") + public LayoutToModelMenuItem( + OpenTCSDrawingEditor drawingEditor, + UndoRedoManager undoRedoManager, + @ApplicationEventBus + EventHandler eventHandler, + MenuItemComponentsFactory componentsFactory, + @Assisted + boolean copyAll + ) { + super( + ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MENU_PATH) + .getString("layoutToModelMenuItem.text") + ); + this.drawingEditor = requireNonNull(drawingEditor, "drawingEditor"); + this.undoRedoManager = requireNonNull(undoRedoManager, "undoRedoManager"); + this.eventHandler = requireNonNull(eventHandler, "eventHandler"); + this.componentsFactory = requireNonNull(componentsFactory, "componentsFactory"); + this.copyAll = copyAll; + + setIcon( + new ImageIcon( + getClass().getClassLoader() + .getResource("org/opentcs/guing/res/symbols/menu/arrow-up-3.png") + ) + ); + setMargin(new Insets(0, 2, 0, 2)); + addActionListener(); + } + + private void addActionListener() { + addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + for (Figure figure : copyAll + ? getView().getDrawing().getFiguresFrontToBack() + : getView().getSelectedFigures()) { + ModelComponent model = figure.get(FigureConstants.MODEL); + if (model instanceof PointModel || model instanceof LocationModel) { + updateModelX(model); + updateModelY(model); + model.propertiesChanged(new NullAttributesChangeListener()); + eventHandler.onEvent(new ResetInteractionToolCommand(this)); + } + } + } + }); + } + + private DrawingView getView() { + return drawingEditor.getActiveView(); + } + + private void updateModelY(ModelComponent model) + throws IllegalArgumentException { + CoordinateProperty modelProperty; + if (model instanceof PointModel) { + modelProperty = (CoordinateProperty) model.getProperty(PointModel.MODEL_Y_POSITION); + } + else { + modelProperty = (CoordinateProperty) model.getProperty(LocationModel.MODEL_Y_POSITION); + } + CoordinateUndoActivity cua + = componentsFactory.createLayoutToModelCoordinateUndoActivity(modelProperty); + cua.snapShotBeforeModification(); + StringProperty spy; + if (model instanceof PointModel) { + spy = (StringProperty) model.getProperty(ElementPropKeys.POINT_POS_Y); + } + else { + spy = (StringProperty) model.getProperty(ElementPropKeys.LOC_POS_Y); + } + + if (!spy.getText().isEmpty()) { + modelProperty.setValueAndUnit(Double.parseDouble(spy.getText()), modelProperty.getUnit()); + modelProperty.markChanged(); + } + cua.snapShotAfterModification(); + undoRedoManager.addEdit(cua); + } + + private void updateModelX(ModelComponent model) + throws IllegalArgumentException { + CoordinateProperty modelProperty; + if (model instanceof PointModel) { + modelProperty = (CoordinateProperty) model.getProperty(PointModel.MODEL_X_POSITION); + } + else { + modelProperty = (CoordinateProperty) model.getProperty(LocationModel.MODEL_X_POSITION); + } + CoordinateUndoActivity cua + = componentsFactory.createLayoutToModelCoordinateUndoActivity(modelProperty); + cua.snapShotBeforeModification(); + StringProperty spx; + if (model instanceof PointModel) { + spx = (StringProperty) model.getProperty(ElementPropKeys.POINT_POS_X); + } + else { + spx = (StringProperty) model.getProperty(ElementPropKeys.LOC_POS_X); + } + if (!spx.getText().isEmpty()) { + modelProperty.setValueAndUnit(Double.parseDouble(spx.getText()), modelProperty.getUnit()); + modelProperty.markChanged(); + } + cua.snapShotAfterModification(); + undoRedoManager.addEdit(cua); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/MenuFactory.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/MenuFactory.java new file mode 100644 index 0000000..c108803 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/MenuFactory.java @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.menus; + +/** + * A factory for various menus and menu items. + */ +public interface MenuFactory { + + /** + * Creates a menu item for copying the value of the layout properties of + * selected elements to the corresponding model properties. + * + * @param copyAll Indicates whether the values of ALL points and locations + * shall be copied when the menu item is clicked. If false only the selected + * figures will be considered. + * @return The created menu item. + */ + LayoutToModelMenuItem createLayoutToModelMenuItem(boolean copyAll); + + /** + * + * @param copyAll Indicates whether the values of ALL points and locations + * shall be copied when the menu item is clicked. If false only the selected + * figures will be considered. + * @return The created menu item. + */ + ModelToLayoutMenuItem createModelToLayoutMenuItem(boolean copyAll); + + /** + * Creates a menu item for calculating the length of paths. + * + * @return The created menu item. + */ + CalculatePathLengthMenuItem createCalculatePathLengthMenuItem(); +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/MenuItemComponentsFactory.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/MenuItemComponentsFactory.java new file mode 100644 index 0000000..0cb646e --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/MenuItemComponentsFactory.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.menus; + +import org.opentcs.guing.base.components.properties.type.CoordinateProperty; + +/** + * A factory for creating instances in relation to menu items. + */ +public interface MenuItemComponentsFactory { + + /** + * Creates a {@link LayoutToModelCoordinateUndoActivity} for the given coordinate property. + * + * @param property The property. + * @return The {@link LayoutToModelCoordinateUndoActivity}. + */ + LayoutToModelCoordinateUndoActivity createLayoutToModelCoordinateUndoActivity( + CoordinateProperty property + ); + + /** + * Creates a {@link ModelToLayoutCoordinateUndoActivity} for the given coordinate property. + * + * @param property The property. + * @return The {@link ModelToLayoutCoordinateUndoActivity}. + */ + ModelToLayoutCoordinateUndoActivity createModelToLayoutCoordinateUndoActivity( + CoordinateProperty property + ); +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/ModelToLayoutCoordinateUndoActivity.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/ModelToLayoutCoordinateUndoActivity.java new file mode 100644 index 0000000..9a86423 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/ModelToLayoutCoordinateUndoActivity.java @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.menus; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.geom.AffineTransform; +import java.awt.geom.NoninvertibleTransformException; +import org.opentcs.data.model.visualization.ElementPropKeys; +import org.opentcs.guing.base.components.properties.type.CoordinateProperty; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.base.model.AbstractConnectableModelComponent; +import org.opentcs.guing.common.components.drawing.course.Origin; +import org.opentcs.guing.common.components.drawing.figures.FigureConstants; +import org.opentcs.guing.common.components.drawing.figures.TCSFigure; +import org.opentcs.guing.common.components.properties.CoordinateUndoActivity; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + */ +public class ModelToLayoutCoordinateUndoActivity + extends + CoordinateUndoActivity { + + /** + * This class's logger. + */ + private static final Logger LOG + = LoggerFactory.getLogger(ModelToLayoutCoordinateUndoActivity.class); + + @Inject + public ModelToLayoutCoordinateUndoActivity( + @Assisted + CoordinateProperty property, + ModelManager modelManager + ) { + super(property, modelManager); + } + + @Override + public String getPresentationName() { + return ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MISC_PATH) + .getString("modelToLayoutCoordinateUndoActivity.presentationName"); + } + + @Override + protected void saveTransformBeforeModification() { + AbstractConnectableModelComponent model + = (AbstractConnectableModelComponent) property.getModel(); + StringProperty pxLayout = (StringProperty) model.getProperty(ElementPropKeys.POINT_POS_X); + StringProperty pyLayout = (StringProperty) model.getProperty(ElementPropKeys.POINT_POS_Y); + + Origin origin = bufferedFigure.get(FigureConstants.ORIGIN); + TCSFigure pf = bufferedFigure.getPresentationFigure(); + double zoomScale = pf.getZoomPoint().scale(); + double xModel + = pxModel.getValueByUnit(CoordinateProperty.Unit.MM) / (zoomScale * origin.getScaleX()); + double yModel + = pyModel.getValueByUnit(CoordinateProperty.Unit.MM) / (-zoomScale * origin.getScaleY()); + String sx = (String) pxLayout.getComparableValue(); + double xLayout = Double.parseDouble(sx) / (zoomScale * origin.getScaleX()); + String sy = (String) pyLayout.getComparableValue(); + double yLayout = Double.parseDouble(sy) / (-zoomScale * origin.getScaleY()); + + bufferedTransform.translate(xModel - xLayout, yModel - yLayout); + } + + @Override + protected void saveTransformForUndo() { + try { + AffineTransform inverse = bufferedTransform.createInverse(); + bufferedFigure.willChange(); + bufferedFigure.transform(inverse); + bufferedFigure.changed(); + } + catch (NoninvertibleTransformException e) { + LOG.warn("Exception inverting transform.", e); + } + } + + @Override + protected void saveTransformForRedo() { + bufferedFigure.willChange(); + bufferedFigure.transform(bufferedTransform); + bufferedFigure.changed(); + } + +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/ModelToLayoutMenuItem.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/ModelToLayoutMenuItem.java new file mode 100644 index 0000000..094707f --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/ModelToLayoutMenuItem.java @@ -0,0 +1,192 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.menus; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import javax.swing.ImageIcon; +import javax.swing.JMenuItem; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.Figure; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.model.visualization.ElementPropKeys; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.event.NullAttributesChangeListener; +import org.opentcs.guing.base.components.properties.type.CoordinateProperty; +import org.opentcs.guing.base.components.properties.type.ModelAttribute; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; +import org.opentcs.guing.common.components.drawing.figures.FigureConstants; +import org.opentcs.guing.common.components.drawing.figures.LabeledFigure; +import org.opentcs.guing.common.components.properties.CoordinateUndoActivity; +import org.opentcs.guing.common.event.ResetInteractionToolCommand; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.UndoRedoManager; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.util.event.EventHandler; + +/** + * A menu item for copying the value of the model properties of selected points + * or locations to the corresponding layout properties. + */ +public class ModelToLayoutMenuItem + extends + JMenuItem { + + /** + * The DrawingEditor instance. + */ + private final DrawingEditor drawingEditor; + /** + * The UndoRedoManager instance to be used. + */ + private final UndoRedoManager undoRedoManager; + /** + * Where we send events. + */ + private final EventHandler eventBus; + /** + * The components factory. + */ + private final MenuItemComponentsFactory componentsFactory; + /** + * A flag if the values of ALL points and location shall be copied when + * the menu item is clicked. If false only the selected figures will be + * considered. + */ + private final boolean copyAll; + + /** + * Creates a new instance. + * + * @param drawingEditor A DrawingEditor instance. + * @param undoRedoManager The application's undo/redo manager. + * @param eventHandler Where this instance sends events. + * @param componentsFactory The components factory. + * @param copyAll Indicates whether the values of ALL points and locations + * shall be copied when the menu item is clicked. If false only the selected + * figures will be considered. + */ + @Inject + @SuppressWarnings("this-escape") + public ModelToLayoutMenuItem( + OpenTCSDrawingEditor drawingEditor, + UndoRedoManager undoRedoManager, + @ApplicationEventBus + EventHandler eventHandler, + MenuItemComponentsFactory componentsFactory, + @Assisted + boolean copyAll + ) { + super( + ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MENU_PATH) + .getString("modelToLayoutMenuItem.text") + ); + this.drawingEditor = requireNonNull(drawingEditor, "drawingEditor"); + this.undoRedoManager = requireNonNull(undoRedoManager, "undoRedoManager"); + this.eventBus = requireNonNull(eventHandler, "eventHandler"); + this.componentsFactory = requireNonNull(componentsFactory, "componentsFactory"); + this.copyAll = copyAll; + + setIcon( + new ImageIcon( + getClass().getClassLoader() + .getResource("org/opentcs/guing/res/symbols/menu/arrow-down-3.png") + ) + ); + setMargin(new Insets(0, 2, 0, 2)); + addActionListener(); + } + + private void addActionListener() { + addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + for (Figure figure : copyAll + ? getView().getDrawing().getFiguresFrontToBack() + : getView().getSelectedFigures()) { + ModelComponent model = figure.get(FigureConstants.MODEL); + if (model instanceof PointModel || model instanceof LocationModel) { + updateLayoutX(model); + updateLayoutY(model); + // ... and move the figure + final LabeledFigure labeledFigure = (LabeledFigure) figure; + labeledFigure.propertiesChanged( + new AttributesChangeEvent( + new NullAttributesChangeListener(), model + ) + ); + + model.propertiesChanged(new NullAttributesChangeListener()); + eventBus.onEvent(new ResetInteractionToolCommand(this)); + } + } + } + }); + } + + private DrawingView getView() { + return drawingEditor.getActiveView(); + } + + private void updateLayoutY(ModelComponent model) { + CoordinateProperty modelProperty; + if (model instanceof PointModel) { + modelProperty = (CoordinateProperty) model.getProperty(PointModel.MODEL_Y_POSITION); + } + else { + modelProperty = (CoordinateProperty) model.getProperty(LocationModel.MODEL_Y_POSITION); + } + CoordinateUndoActivity cua + = componentsFactory.createModelToLayoutCoordinateUndoActivity(modelProperty); + cua.snapShotBeforeModification(); + modelProperty.setChangeState(ModelAttribute.ChangeState.DETAIL_CHANGED); + StringProperty spy; + if (model instanceof PointModel) { + spy = (StringProperty) model.getProperty(ElementPropKeys.POINT_POS_Y); + } + else { + spy = (StringProperty) model.getProperty(ElementPropKeys.LOC_POS_Y); + } + spy.setText(String.valueOf(((Number) modelProperty.getValue()).intValue())); + spy.markChanged(); + cua.snapShotAfterModification(); + undoRedoManager.addEdit(cua); + } + + private void updateLayoutX(ModelComponent model) { + CoordinateProperty modelProperty; + if (model instanceof PointModel) { + modelProperty = (CoordinateProperty) model.getProperty(PointModel.MODEL_X_POSITION); + } + else { + modelProperty = (CoordinateProperty) model.getProperty(LocationModel.MODEL_X_POSITION); + } + CoordinateUndoActivity cua + = componentsFactory.createModelToLayoutCoordinateUndoActivity(modelProperty); + cua.snapShotBeforeModification(); + modelProperty.setChangeState(ModelAttribute.ChangeState.DETAIL_CHANGED); + // Copy the model coordinates to the layout coordinates... + StringProperty spx; + if (model instanceof PointModel) { + spx = (StringProperty) model.getProperty(ElementPropKeys.POINT_POS_X); + } + else { + spx = (StringProperty) model.getProperty(ElementPropKeys.LOC_POS_X); + } + spx.setText(String.valueOf(((Number) modelProperty.getValue()).intValue())); + spx.markChanged(); + cua.snapShotAfterModification(); + undoRedoManager.addEdit(cua); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/ActionsMenu.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/ActionsMenu.java new file mode 100644 index 0000000..adeb54f --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/ActionsMenu.java @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.menus.menubar; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; +import org.opentcs.modeleditor.application.action.ViewActionMap; +import org.opentcs.modeleditor.application.menus.MenuFactory; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * The application's menu for run-time actions. + */ +public class ActionsMenu + extends + JMenu { + + /** + * A menu item for assuming the model coordinates from the layout coordinates. + */ + private final JMenuItem cbiAlignLayoutWithModel; + /** + * A menu item for assuming the layout coordinates from the model coordinates. + */ + private final JMenuItem cbiAlignModelWithLayout; + /** + * A menu item for calculating the euclidean distance for paths. + */ + private final JMenuItem calculatePathLength; + + /** + * Creates a new instance. + * + * @param actionMap The application's action map. + * @param drawingEditor The application's drawing editor. + * @param menuFactory A factory for menu items. + */ + @Inject + @SuppressWarnings("this-escape") + public ActionsMenu( + ViewActionMap actionMap, + OpenTCSDrawingEditor drawingEditor, + MenuFactory menuFactory + ) { + requireNonNull(actionMap, "actionMap"); + requireNonNull(drawingEditor, "drawingEditor"); + requireNonNull(menuFactory, "menuFactory"); + + final ResourceBundleUtil labels + = ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MENU_PATH); + + this.setText(labels.getString("actionsMenu.text")); + this.setToolTipText(labels.getString("actionsMenu.tooltipText")); + this.setMnemonic('A'); + + // Menu item Actions -> Copy model to layout + cbiAlignModelWithLayout = menuFactory.createModelToLayoutMenuItem(true); + add(cbiAlignModelWithLayout); + + // Menu item Actions -> Copy layout to model + cbiAlignLayoutWithModel = menuFactory.createLayoutToModelMenuItem(true); + add(cbiAlignLayoutWithModel); + + calculatePathLength = menuFactory.createCalculatePathLengthMenuItem(); + add(calculatePathLength); + } + +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/ApplicationMenuBar.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/ApplicationMenuBar.java new file mode 100644 index 0000000..a9a1ec4 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/ApplicationMenuBar.java @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.menus.menubar; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import javax.swing.JMenuBar; + +/** + * The plant overview's main menu bar. + */ +public class ApplicationMenuBar + extends + JMenuBar { + + private final FileMenu menuFile; + private final EditMenu menuEdit; + private final ActionsMenu menuActions; + private final ViewMenu menuView; + private final HelpMenu menuHelp; + + /** + * Creates a new instance. + * + * @param menuFile The "File" menu. + * @param menuEdit The "Edit" menu. + * @param menuActions The "Actions" menu. + * @param menuView The "View" menu. + * @param menuHelp The "Help menu. + */ + @Inject + @SuppressWarnings("this-escape") + public ApplicationMenuBar( + FileMenu menuFile, + EditMenu menuEdit, + ActionsMenu menuActions, + ViewMenu menuView, + HelpMenu menuHelp + ) { + requireNonNull(menuFile, "menuFile"); + requireNonNull(menuEdit, "menuEdit"); + requireNonNull(menuActions, "menuActions"); + requireNonNull(menuView, "menuView"); + requireNonNull(menuHelp, "menuHelp"); + + this.menuFile = menuFile; + add(menuFile); + + this.menuEdit = menuEdit; + add(menuEdit); + + this.menuActions = menuActions; + add(menuActions); + + this.menuView = menuView; + add(menuView); + + this.menuHelp = menuHelp; + add(menuHelp); + } + +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/EditMenu.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/EditMenu.java new file mode 100644 index 0000000..9bf18e5 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/EditMenu.java @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.menus.menubar; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import javax.swing.JMenu; +import org.opentcs.modeleditor.application.action.ViewActionMap; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.DeleteAction; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.SelectAllAction; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.UndoRedoManager; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.edit.ClearSelectionAction; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.edit.CopyAction; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.edit.CutAction; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.edit.DuplicateAction; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.edit.PasteAction; + +/** + * The application's "Edit" menu. + */ +public class EditMenu + extends + JMenu { + +// private final JMenuItem menuItemCopy; +// private final JMenuItem menuItemCut; +// private final JMenuItem menuItemDuplicate; +// private final JMenuItem menuItemPaste; + /** + * Creates a new instance. + * + * @param actionMap The application's action map. + */ + @Inject + @SuppressWarnings("this-escape") + public EditMenu(ViewActionMap actionMap) { + requireNonNull(actionMap, "actionMap"); + + final ResourceBundleUtil labels + = ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MENU_PATH); + + this.setText(labels.getString("editMenu.text")); + this.setToolTipText(labels.getString("editMenu.tooltipText")); + this.setMnemonic('E'); + + // Undo, Redo + add(actionMap.get(UndoRedoManager.UNDO_ACTION_ID)); + add(actionMap.get(UndoRedoManager.REDO_ACTION_ID)); + addSeparator(); + // Cut, Copy, Paste, Duplicate +// menuItemCut = menuEdit.add(actionMap.get(CutAction.ID)); +// menuItemCopy = menuEdit.add(actionMap.get(CopyAction.ID)); +// menuItemPaste = menuEdit.add(actionMap.get(PasteAction.ID)); +// menuItemDuplicate = menuEdit.add(actionMap.get(DuplicateAction.ID)); + // Delete + add(actionMap.get(DeleteAction.ID)); + add(actionMap.get(CopyAction.ID)); + add(actionMap.get(PasteAction.ID)); + add(actionMap.get(DuplicateAction.ID)); + add(actionMap.get(CutAction.ID)); + addSeparator(); + // Select all, Clear selection + add(actionMap.get(SelectAllAction.ID)); + add(actionMap.get(ClearSelectionAction.ID)); + } + +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/FileExportMenu.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/FileExportMenu.java new file mode 100644 index 0000000..6c9b8f0 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/FileExportMenu.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.menus.menubar; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Set; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import org.opentcs.components.plantoverview.PlantModelExporter; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.modeleditor.application.action.file.ExportPlantModelAction; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + */ +public class FileExportMenu + extends + JMenu { + + private static final ResourceBundleUtil LABELS + = ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MENU_PATH); + + @Inject + @SuppressWarnings("this-escape") + public FileExportMenu( + Set exporters, + GuiManager guiManager + ) { + super(LABELS.getString("fileExportMenu.text")); + requireNonNull(exporters, "exporters"); + requireNonNull(guiManager, "guiManager"); + + for (PlantModelExporter exporter : exporters) { + add(new JMenuItem(new ExportPlantModelAction(exporter, guiManager))); + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/FileImportMenu.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/FileImportMenu.java new file mode 100644 index 0000000..2c3579e --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/FileImportMenu.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.menus.menubar; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Set; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import org.opentcs.components.plantoverview.PlantModelImporter; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.modeleditor.application.action.file.ImportPlantModelAction; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + */ +public class FileImportMenu + extends + JMenu { + + private static final ResourceBundleUtil LABELS + = ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MENU_PATH); + + @Inject + @SuppressWarnings("this-escape") + public FileImportMenu( + Set importers, + GuiManager guiManager + ) { + super(LABELS.getString("fileImportMenu.text")); + requireNonNull(importers, "importers"); + requireNonNull(guiManager, "guiManager"); + + for (PlantModelImporter importer : importers) { + add(new JMenuItem(new ImportPlantModelAction(importer, guiManager))); + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/FileMenu.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/FileMenu.java new file mode 100644 index 0000000..26d4e6a --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/FileMenu.java @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.menus.menubar; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import org.opentcs.guing.common.application.action.file.ModelPropertiesAction; +import org.opentcs.guing.common.application.action.file.SaveModelAction; +import org.opentcs.guing.common.application.action.file.SaveModelAsAction; +import org.opentcs.modeleditor.application.action.ViewActionMap; +import org.opentcs.modeleditor.application.action.file.DownloadModelFromKernelAction; +import org.opentcs.modeleditor.application.action.file.LoadModelAction; +import org.opentcs.modeleditor.application.action.file.NewModelAction; +import org.opentcs.modeleditor.application.action.file.UploadModelToKernelAction; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.file.CloseFileAction; + +/** + * The application's "File" menu. + */ +public class FileMenu + extends + JMenu { + + /** + * A menu item for creating a new, empty system model. + */ + private final JMenuItem menuItemNewModel; + /** + * A menu item for loading a mode into the kernel. + */ + private final JMenuItem menuItemLoadModel; + /** + * A menu item for saving the kernel's current model. + */ + private final JMenuItem menuItemSaveModel; + /** + * A menu item for saving the kernel's current model with a new name. + */ + private final JMenuItem menuItemSaveModelAs; + /** + * A menu item for retrieving the system model data from the kernel. + */ + private final JMenuItem menuItemDownloadModelFromKernel; + /** + * A menu item for transferring the system model data to the kernel. + */ + private final JMenuItem menuItemUploadModelToKernel; + /** + * A menu item for showing the current model's properties. + */ + private final JMenuItem menuItemModelProperties; + /** + * A menu item for closing the application. + */ + private final JMenuItem menuItemClose; + + /** + * Creates a new instance. + * + * @param actionMap The application's action map. + * @param menuImport The sub-menu for the selectable plant model importers. + * @param menuExport The sub-menu for the selectable plant model exporters. + */ + @Inject + @SuppressWarnings("this-escape") + public FileMenu( + ViewActionMap actionMap, + FileImportMenu menuImport, + FileExportMenu menuExport + ) { + requireNonNull(actionMap, "actionMap"); + requireNonNull(menuImport, "menuImport"); + requireNonNull(menuExport, "menuExport"); + + final ResourceBundleUtil labels + = ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MENU_PATH); + + this.setText(labels.getString("fileMenu.text")); + this.setToolTipText(labels.getString("fileMenu.tooltipText")); + this.setMnemonic('F'); + + // Menu item File -> New Model + menuItemNewModel = new JMenuItem(actionMap.get(NewModelAction.ID)); + add(menuItemNewModel); + + // Menu item File -> Load Model + menuItemLoadModel = new JMenuItem(actionMap.get(LoadModelAction.ID)); + add(menuItemLoadModel); + + // Menu item File -> Save Model + menuItemSaveModel = new JMenuItem(actionMap.get(SaveModelAction.ID)); + add(menuItemSaveModel); + + // Menu item File -> Save Model As + menuItemSaveModelAs = new JMenuItem(actionMap.get(SaveModelAsAction.ID)); + add(menuItemSaveModelAs); + + addSeparator(); + + add(menuImport); + add(menuExport); + + addSeparator(); + + // Load model from kernel + menuItemDownloadModelFromKernel + = new JMenuItem(actionMap.get(DownloadModelFromKernelAction.ID)); + add(menuItemDownloadModelFromKernel); + + // Persist model in kernel + menuItemUploadModelToKernel = new JMenuItem(actionMap.get(UploadModelToKernelAction.ID)); + add(menuItemUploadModelToKernel); + + addSeparator(); + + menuItemModelProperties = new JMenuItem(actionMap.get(ModelPropertiesAction.ID)); + add(menuItemModelProperties); + + addSeparator(); + + // Menu item File -> Close + menuItemClose = new JMenuItem(actionMap.get(CloseFileAction.ID)); + add(menuItemClose); + } + +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/HelpMenu.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/HelpMenu.java new file mode 100644 index 0000000..b566d21 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/HelpMenu.java @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.menus.menubar; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import org.opentcs.modeleditor.application.action.ViewActionMap; +import org.opentcs.modeleditor.application.action.app.AboutAction; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * The application's "Help" menu. + */ +public class HelpMenu + extends + JMenu { + + /** + * A menu item for showing the application's "about" panel. + */ + private final JMenuItem menuItemAbout; + + /** + * Creates a new instance. + * + * @param actionMap The application's action map. + */ + @Inject + @SuppressWarnings("this-escape") + public HelpMenu(ViewActionMap actionMap) { + requireNonNull(actionMap, "actionMap"); + + final ResourceBundleUtil labels + = ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MENU_PATH); + + this.setText(labels.getString("helpMenu.text")); + this.setToolTipText(labels.getString("helpMenu.tooltipText")); + this.setMnemonic('?'); + + menuItemAbout = add(actionMap.get(AboutAction.ID)); + } + +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/ViewMenu.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/ViewMenu.java new file mode 100644 index 0000000..09177ea --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/ViewMenu.java @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.menus.menubar; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.List; +import javax.swing.Action; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JToolBar; +import org.jhotdraw.app.action.window.ToggleVisibleAction; +import org.opentcs.guing.common.application.OperationMode; +import org.opentcs.guing.common.application.menus.menubar.ViewPluginPanelsMenu; +import org.opentcs.modeleditor.application.action.ToolBarManager; +import org.opentcs.modeleditor.application.action.ViewActionMap; +import org.opentcs.modeleditor.application.action.view.AddBitmapAction; +import org.opentcs.modeleditor.application.action.view.RestoreDockingLayoutAction; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * The application's menu for view-related operations. + */ +public class ViewMenu + extends + JMenu { + + /** + * The toolbar manager. + */ + private final ToolBarManager toolBarManager; + /** + * A menu item for setting a bitmap for the current drawing view. + */ + private final JMenuItem menuAddBitmap; + /** + * A menu for manipulating the application's tool bars. + */ + private final ViewToolBarsMenu menuViewToolBars; + /** + * A menu for showing/hiding plugin panels. + */ + private final ViewPluginPanelsMenu menuPluginPanels; + /** + * A menu item for restoring the default GUI layout. + */ + private final JMenuItem menuItemRestoreDockingLayout; + + /** + * Creates a new instance. + * + * @param actionMap The application's action map. + * @param toolBarManager The toolbar manager. + * @param menuPluginPanels A menu for showing/hiding plugin panels. + */ + @Inject + @SuppressWarnings("this-escape") + public ViewMenu( + ViewActionMap actionMap, + ToolBarManager toolBarManager, + ViewPluginPanelsMenu menuPluginPanels + ) { + requireNonNull(actionMap, "actionMap"); + this.toolBarManager = requireNonNull(toolBarManager, "toolBarManager"); + requireNonNull(menuPluginPanels, "menuPluginPanels"); + + final ResourceBundleUtil labels + = ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MENU_PATH); + + this.setText(labels.getString("viewMenu.text")); + this.setToolTipText(labels.getString("viewMenu.tooltipText")); + this.setMnemonic('V'); + + // Menu item View -> Add Background Image + menuAddBitmap = new JMenuItem(actionMap.get(AddBitmapAction.ID)); + add(menuAddBitmap); + + addSeparator(); + + List viewActions = createToolBarActions(); + if (!viewActions.isEmpty()) { + menuViewToolBars = new ViewToolBarsMenu(viewActions); + add(menuViewToolBars); + } + else { + menuViewToolBars = null; + } + + // Menu item View -> Plugins + this.menuPluginPanels = menuPluginPanels; + menuPluginPanels.setOperationMode(OperationMode.MODELLING); + add(menuPluginPanels); + + // Menu item View -> Restore docking layout + menuItemRestoreDockingLayout = new JMenuItem(actionMap.get(RestoreDockingLayoutAction.ID)); + menuItemRestoreDockingLayout.setText( + labels.getString("viewMenu.menuItem_restoreWindowArrangement.text") + ); + add(menuItemRestoreDockingLayout); + } + + private List createToolBarActions() { + List toolBarActions = new ArrayList<>(); + for (JToolBar curToolBar : toolBarManager.getToolBars()) { + toolBarActions.add(new ToggleVisibleAction(curToolBar, curToolBar.getName())); + } + return toolBarActions; + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/ViewToolBarsMenu.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/ViewToolBarsMenu.java new file mode 100644 index 0000000..b25beae --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/menus/menubar/ViewToolBarsMenu.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.menus.menubar; + +import static java.util.Objects.requireNonNull; + +import java.util.Collection; +import javax.swing.Action; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JMenu; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + */ +public class ViewToolBarsMenu + extends + JMenu { + + private static final ResourceBundleUtil LABELS_MENU + = ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MENU_PATH); + private static final ResourceBundleUtil LABELS_TOOLBAR + = ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.TOOLBAR_PATH); + + @SuppressWarnings("this-escape") + public ViewToolBarsMenu(Collection viewActions) { + super(LABELS_MENU.getString("viewToolBarsMenu.text")); + requireNonNull(viewActions, "viewActions"); + + JCheckBoxMenuItem checkBoxMenuItem; + for (Action a : viewActions) { + checkBoxMenuItem = new JCheckBoxMenuItem(a); + add(checkBoxMenuItem); + + if (checkBoxMenuItem.getText().equals( + LABELS_TOOLBAR.getString("toolBarManager.toolbar_drawing.title") + )) { + checkBoxMenuItem.setEnabled(false); // "Draw"-Toolbar musn't be disabled. + } + } + + } + +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/toolbar/CreationToolFactory.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/toolbar/CreationToolFactory.java new file mode 100644 index 0000000..3726134 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/toolbar/CreationToolFactory.java @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.toolbar; + +import org.jhotdraw.draw.ConnectionFigure; +import org.jhotdraw.draw.Figure; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.toolbar.OpenTCSConnectionTool; + +/** + * A factory for tools concerned with the creation of figures/model elements. + */ +public interface CreationToolFactory { + + OpenTCSCreationTool createCreationTool(Figure prototype); + + OpenTCSConnectionTool createConnectionTool(ConnectionFigure prototype); +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/toolbar/MultipleSelectionTool.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/toolbar/MultipleSelectionTool.java new file mode 100644 index 0000000..64b1473 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/toolbar/MultipleSelectionTool.java @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.toolbar; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javax.swing.Action; +import javax.swing.JMenuItem; +import org.jhotdraw.draw.tool.DragTracker; +import org.jhotdraw.draw.tool.SelectAreaTracker; +import org.opentcs.guing.common.application.ApplicationState; +import org.opentcs.modeleditor.application.menus.MenuFactory; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.toolbar.AbstractMultipleSelectionTool; + +/** + * The default selection tool. + */ +public class MultipleSelectionTool + extends + AbstractMultipleSelectionTool { + + /** + * A factory for menu items. + */ + private final MenuFactory menuFactory; + + /** + * Creates a new instance. + * + * @param appState Stores the application's current state. + * @param menuFactory A factory for menu items in popup menus created by this tool. + * @param selectAreaTracker The tracker to be used for area selections in the drawing. + * @param dragTracker The tracker to be used for dragging figures. + * @param drawingActions Drawing-related actions for the popup menus created by this tool. + * @param selectionActions Selection-related actions for the popup menus created by this tool. + */ + @Inject + public MultipleSelectionTool( + ApplicationState appState, + MenuFactory menuFactory, + SelectAreaTracker selectAreaTracker, + DragTracker dragTracker, + @Assisted("drawingActions") + Collection drawingActions, + @Assisted("selectionActions") + Collection selectionActions + ) { + super(appState, selectAreaTracker, dragTracker, drawingActions, selectionActions); + this.menuFactory = requireNonNull(menuFactory, "menuFactory"); + } + + @Override + public List customPopupMenuItems() { + // Points and Locations get two additional entries + List actions = new ArrayList<>(); + actions.add(menuFactory.createModelToLayoutMenuItem(false)); + actions.add(menuFactory.createLayoutToModelMenuItem(false)); + return actions; + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/toolbar/OpenTCSCreationTool.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/toolbar/OpenTCSCreationTool.java new file mode 100644 index 0000000..d807a60 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/toolbar/OpenTCSCreationTool.java @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.toolbar; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.TOOLBAR_PATH; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.event.MouseEvent; +import javax.swing.JOptionPane; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.tool.CreationTool; +import org.opentcs.guing.common.components.drawing.figures.LabeledFigure; +import org.opentcs.guing.common.components.drawing.figures.ModelBasedFigure; +import org.opentcs.modeleditor.components.layer.ActiveLayerProvider; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * A custom tool used to create {@code PointFigure}s and {@code LocationFigure}s. + */ +public class OpenTCSCreationTool + extends + CreationTool { + + /** + * The resource bundle to use. + */ + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(TOOLBAR_PATH); + /** + * Provides the currently active layer. + */ + private final ActiveLayerProvider activeLayerProvider; + + /** + * Creates a new instance. + * + * @param activeLayerProvider Provides the currently active layer. + * @param prototype The figure to be used as a prototype. + */ + @Inject + public OpenTCSCreationTool( + ActiveLayerProvider activeLayerProvider, + @Assisted + Figure prototype + ) { + super(prototype); + this.activeLayerProvider = requireNonNull(activeLayerProvider, "activeLayerProvider"); + } + + @Override + public void mousePressed(MouseEvent evt) { + if (!activeLayerProvider.getActiveLayer().getLayer().isVisible() + || !activeLayerProvider.getActiveLayer().getLayerGroup().isVisible()) { + JOptionPane.showMessageDialog( + evt.getComponent(), + BUNDLE.getString("openTcsCreationTool.optionPane_activeLayerNotVisible.message"), + BUNDLE.getString("openTcsCreationTool.optionPane_activeLayerNotVisible.title"), + JOptionPane.INFORMATION_MESSAGE + ); + return; + } + + super.mousePressed(evt); + } + + @Override + protected Figure createFigure() { + Figure figure = super.createFigure(); + + if (figure instanceof ModelBasedFigure) { + ((ModelBasedFigure) figure).getModel() + .getPropertyLayerWrapper().setValue(activeLayerProvider.getActiveLayer()); + } + else if (figure instanceof LabeledFigure) { + ((LabeledFigure) figure).getPresentationFigure().getModel() + .getPropertyLayerWrapper().setValue(activeLayerProvider.getActiveLayer()); + } + + return figure; + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/toolbar/SelectionToolFactory.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/toolbar/SelectionToolFactory.java new file mode 100644 index 0000000..1e06067 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/application/toolbar/SelectionToolFactory.java @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.application.toolbar; + +import com.google.inject.assistedinject.Assisted; +import java.util.Collection; +import javax.swing.Action; + +/** + */ +public interface SelectionToolFactory { + + MultipleSelectionTool createMultipleSelectionTool( + @Assisted("drawingActions") + Collection drawingActions, + @Assisted("selectionActions") + Collection selectionActions + ); +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/dialog/PathTypeSelectionPanel.form b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/dialog/PathTypeSelectionPanel.form new file mode 100644 index 0000000..e5a1bca --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/dialog/PathTypeSelectionPanel.form @@ -0,0 +1,127 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/dialog/PathTypeSelectionPanel.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/dialog/PathTypeSelectionPanel.java new file mode 100644 index 0000000..dc967eb --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/dialog/PathTypeSelectionPanel.java @@ -0,0 +1,264 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.dialog; + +import static org.opentcs.guing.base.model.elements.PathModel.Type.BEZIER; +import static org.opentcs.guing.base.model.elements.PathModel.Type.BEZIER_3; +import static org.opentcs.guing.base.model.elements.PathModel.Type.DIRECT; +import static org.opentcs.guing.base.model.elements.PathModel.Type.ELBOW; +import static org.opentcs.guing.base.model.elements.PathModel.Type.POLYPATH; +import static org.opentcs.guing.base.model.elements.PathModel.Type.SLANTED; + +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.common.components.dialogs.DialogContent; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * A dialog content to select a set of path types. + */ +public class PathTypeSelectionPanel + extends + DialogContent { + + /** + * Creates new form PathTypeSelection. + */ + @SuppressWarnings("this-escape") + public PathTypeSelectionPanel() { + setDialogTitle( + ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MISC_PATH) + .getString("pathTypeSelection.title") + ); + initComponents(); + } + + @Override + public void initFields() { + } + + @Override + public void update() { + } + + /** + * Test if the given path type is selected. + * + * @param type The path type to test. + * @return True if the path type is selected. + */ + public boolean isPathTypeSelected(PathModel.Type type) { + switch (type) { + case DIRECT: + return directCheckBox.isSelected(); + case ELBOW: + return elbowCheckBox.isSelected(); + case SLANTED: + return slantedCheckBox.isSelected(); + case POLYPATH: + return polypathCheckBox.isSelected(); + case BEZIER: + return bezierCheckBox.isSelected(); + case BEZIER_3: + return bezier3CheckBox.isSelected(); + default: + return false; + } + } + + private void updateAllTypesCheckBox() { + allTypesCheckBox.setSelected( + directCheckBox.isSelected() + && elbowCheckBox.isSelected() + && slantedCheckBox.isSelected() + && polypathCheckBox.isSelected() + && bezierCheckBox.isSelected() + && bezier3CheckBox.isSelected() + ); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + allTypesCheckBox = new javax.swing.JCheckBox(); + directCheckBox = new javax.swing.JCheckBox(); + elbowCheckBox = new javax.swing.JCheckBox(); + slantedCheckBox = new javax.swing.JCheckBox(); + polypathCheckBox = new javax.swing.JCheckBox(); + bezierCheckBox = new javax.swing.JCheckBox(); + bezier3CheckBox = new javax.swing.JCheckBox(); + + setLayout(new java.awt.GridBagLayout()); + + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/modeling/miscellaneous"); // NOI18N + allTypesCheckBox.setText(bundle.getString("pathTypeSelection.allTypes_label.text")); // NOI18N + allTypesCheckBox.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + allTypesCheckBox.setHorizontalTextPosition(javax.swing.SwingConstants.LEADING); + allTypesCheckBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + allTypesCheckBoxActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + add(allTypesCheckBox, gridBagConstraints); + + directCheckBox.setText("Direct:"); + directCheckBox.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + directCheckBox.setHorizontalTextPosition(javax.swing.SwingConstants.LEADING); + directCheckBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + directCheckBoxActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(3, 0, 0, 0); + add(directCheckBox, gridBagConstraints); + + elbowCheckBox.setText("Elbow:"); + elbowCheckBox.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + elbowCheckBox.setHorizontalTextPosition(javax.swing.SwingConstants.LEADING); + elbowCheckBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + elbowCheckBoxActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(3, 0, 0, 0); + add(elbowCheckBox, gridBagConstraints); + + slantedCheckBox.setText("Slanted:"); + slantedCheckBox.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + slantedCheckBox.setHorizontalTextPosition(javax.swing.SwingConstants.LEADING); + slantedCheckBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + slantedCheckBoxActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(3, 0, 0, 0); + add(slantedCheckBox, gridBagConstraints); + + polypathCheckBox.setText("Polypath:"); + polypathCheckBox.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + polypathCheckBox.setHorizontalTextPosition(javax.swing.SwingConstants.LEADING); + polypathCheckBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + polypathCheckBoxActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(3, 0, 0, 0); + add(polypathCheckBox, gridBagConstraints); + + bezierCheckBox.setText("Bezier:"); + bezierCheckBox.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + bezierCheckBox.setHorizontalTextPosition(javax.swing.SwingConstants.LEADING); + bezierCheckBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bezierCheckBoxActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 5; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(3, 0, 0, 0); + add(bezierCheckBox, gridBagConstraints); + + bezier3CheckBox.setText("Bezier 3:"); + bezier3CheckBox.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + bezier3CheckBox.setHorizontalTextPosition(javax.swing.SwingConstants.LEADING); + bezier3CheckBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bezier3CheckBoxActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 6; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(3, 0, 0, 0); + add(bezier3CheckBox, gridBagConstraints); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void allTypesCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_allTypesCheckBoxActionPerformed + directCheckBox.setSelected(allTypesCheckBox.isSelected()); + elbowCheckBox.setSelected(allTypesCheckBox.isSelected()); + slantedCheckBox.setSelected(allTypesCheckBox.isSelected()); + polypathCheckBox.setSelected(allTypesCheckBox.isSelected()); + bezierCheckBox.setSelected(allTypesCheckBox.isSelected()); + bezier3CheckBox.setSelected(allTypesCheckBox.isSelected()); + }//GEN-LAST:event_allTypesCheckBoxActionPerformed + + private void directCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_directCheckBoxActionPerformed + updateAllTypesCheckBox(); + }//GEN-LAST:event_directCheckBoxActionPerformed + + private void elbowCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_elbowCheckBoxActionPerformed + updateAllTypesCheckBox(); + }//GEN-LAST:event_elbowCheckBoxActionPerformed + + private void slantedCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_slantedCheckBoxActionPerformed + updateAllTypesCheckBox(); + }//GEN-LAST:event_slantedCheckBoxActionPerformed + + private void polypathCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_polypathCheckBoxActionPerformed + updateAllTypesCheckBox(); + }//GEN-LAST:event_polypathCheckBoxActionPerformed + + private void bezierCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bezierCheckBoxActionPerformed + updateAllTypesCheckBox(); + }//GEN-LAST:event_bezierCheckBoxActionPerformed + + private void bezier3CheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bezier3CheckBoxActionPerformed + updateAllTypesCheckBox(); + }//GEN-LAST:event_bezier3CheckBoxActionPerformed + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JCheckBox allTypesCheckBox; + private javax.swing.JCheckBox bezier3CheckBox; + private javax.swing.JCheckBox bezierCheckBox; + private javax.swing.JCheckBox directCheckBox; + private javax.swing.JCheckBox elbowCheckBox; + private javax.swing.JCheckBox polypathCheckBox; + private javax.swing.JCheckBox slantedCheckBox; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/dockable/DockingManagerModeling.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/dockable/DockingManagerModeling.java new file mode 100644 index 0000000..647e063 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/dockable/DockingManagerModeling.java @@ -0,0 +1,196 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.dockable; + +import static java.util.Objects.requireNonNull; + +import bibliothek.gui.dock.common.CControl; +import bibliothek.gui.dock.common.CGrid; +import bibliothek.gui.dock.common.DefaultSingleCDockable; +import bibliothek.gui.dock.common.group.CGroupBehavior; +import jakarta.inject.Inject; +import javax.swing.JComponent; +import javax.swing.JFrame; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.guing.common.components.dockable.AbstractDockingManager; +import org.opentcs.guing.common.components.dockable.CStack; +import org.opentcs.guing.common.components.properties.SelectionPropertiesComponent; +import org.opentcs.guing.common.components.tree.BlocksTreeViewManager; +import org.opentcs.guing.common.components.tree.ComponentsTreeViewManager; +import org.opentcs.modeleditor.components.layer.LayerGroupsPanel; +import org.opentcs.modeleditor.components.layer.LayersPanel; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Utility class for working with dockables. + */ +public class DockingManagerModeling + extends + AbstractDockingManager { + + /** + * ID of the tab pane that contains the course, transport orders and order sequences. + */ + public static final String COURSE_TAB_PANE_ID = "course_tab_pane"; + /** + * ID of the tab pane that contains the components, blocks and groups. + */ + private static final String TREE_TAB_PANE_ID = "tree_tab_pane"; + private static final String LAYER_TAB_PANE_ID = "layer_tab_pane"; + private static final String COMPONENTS_ID = "comp_dock"; + private static final String BLOCKS_ID = "block_dock"; + private static final String PROPERTIES_ID = "properties_id"; + private static final String LAYERS_ID = "layers_id"; + private static final String LAYER_GROUPS_ID = "layer_groups_id"; + /** + * The tree view manager for components. + */ + private final ComponentsTreeViewManager componentsTreeViewManager; + /** + * The tree view manager for blocks. + */ + private final BlocksTreeViewManager blocksTreeViewManager; + /** + * The panel displaying the properties of the currently selected driving course components. + */ + private final SelectionPropertiesComponent selectionPropertiesComponent; + /** + * The panel displaying the layers in the plant model. + */ + private final LayersPanel layersPanel; + /** + * The panel displaying the layer groups in the plant model. + */ + private final LayerGroupsPanel layerGroupsPanel; + /** + * Tab pane that contains the components, blocks and groups. + */ + private CStack treeTabPane; + /** + * Tab pane that contains the course, transport orders and order sequences. + */ + private CStack courseTabPane; + /** + * Tab pane that contains layers and layer groups. + */ + private CStack layerTabPane; + + /** + * Creates a new instance. + * + * @param applicationFrame The application's main frame. + * @param componentsTreeViewManager The tree view manager for components. + * @param blocksTreeViewManager The tree view manager for blocks. + * @param selectionPropertiesComponent The panel displaying the properties of the currently + * selected driving course components. + * @param layersPanel The panel displaying the layers in the plant model. + * @param layerGroupsPanel The panel displaying the layer groups in the plant model. + */ + @Inject + public DockingManagerModeling(@ApplicationFrame + JFrame applicationFrame, + ComponentsTreeViewManager componentsTreeViewManager, + BlocksTreeViewManager blocksTreeViewManager, + SelectionPropertiesComponent selectionPropertiesComponent, + LayersPanel layersPanel, + LayerGroupsPanel layerGroupsPanel + ) { + super(new CControl(applicationFrame)); + this.componentsTreeViewManager = requireNonNull( + componentsTreeViewManager, + "componentsTreeViewManager" + ); + this.blocksTreeViewManager = requireNonNull(blocksTreeViewManager, "blocksTreeViewManager"); + this.selectionPropertiesComponent = requireNonNull( + selectionPropertiesComponent, + "selectionPropertiesComponent" + ); + this.layersPanel = requireNonNull(layersPanel, "layersPanel"); + this.layerGroupsPanel = requireNonNull(layerGroupsPanel, "layerGroupsPanel"); + } + + @Override + public void reset() { + removeDockable(BLOCKS_ID); + removeDockable(COMPONENTS_ID); + removeDockable(PROPERTIES_ID); + removeDockable(LAYERS_ID); + removeDockable(LAYER_GROUPS_ID); + getCControl().removeStation(getTabPane(COURSE_TAB_PANE_ID)); + getCControl().removeStation(getTabPane(TREE_TAB_PANE_ID)); + getCControl().removeStation(getTabPane(LAYER_TAB_PANE_ID)); + } + + @Override + public void initializeDockables() { + getCControl().setGroupBehavior(CGroupBehavior.TOPMOST); + + // Disable keyboard shortcuts to avoid collisions. + getCControl().putProperty(CControl.KEY_GOTO_NORMALIZED, null); + getCControl().putProperty(CControl.KEY_GOTO_EXTERNALIZED, null); + getCControl().putProperty(CControl.KEY_GOTO_MAXIMIZED, null); + getCControl().putProperty(CControl.KEY_MAXIMIZE_CHANGE, null); + + ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.DOCKABLE_PATH); + CGrid grid = new CGrid(getCControl()); + courseTabPane = new CStack(COURSE_TAB_PANE_ID); + addTabPane(COURSE_TAB_PANE_ID, courseTabPane); + treeTabPane = new CStack(TREE_TAB_PANE_ID); + addTabPane(TREE_TAB_PANE_ID, treeTabPane); + layerTabPane = new CStack(LAYER_TAB_PANE_ID); + addTabPane(LAYER_TAB_PANE_ID, layerTabPane); + DefaultSingleCDockable treeViewDock + = createDockable( + COMPONENTS_ID, + bundle.getString("dockingManagerModeling.panel_components.title"), + (JComponent) componentsTreeViewManager.getTreeView(), + false + ); + DefaultSingleCDockable treeBlocks + = createDockable( + BLOCKS_ID, + bundle.getString("dockingManagerModeling.panel_blocks.title"), + (JComponent) blocksTreeViewManager.getTreeView(), + false + ); + + grid.add(0, 0, 250, 400, treeTabPane); + grid.add( + 0, 400, 250, 300, + createDockable( + PROPERTIES_ID, + bundle.getString("dockingManagerModeling.panel_properties.title"), + selectionPropertiesComponent, + false + ) + ); + DefaultSingleCDockable layersDock + = createDockable( + LAYERS_ID, + bundle.getString("dockingManagerModeling.panel_layers.title"), + layersPanel, + false + ); + DefaultSingleCDockable layerGroupsDock + = createDockable( + LAYER_GROUPS_ID, + bundle.getString("dockingManagerModeling.panel_layerGroups.title"), + layerGroupsPanel, + false + ); + grid.add(0, 700, 250, 300, layerTabPane); + grid.add(400, 0, 1000, 500, courseTabPane); + + getCControl().getContentArea().deploy(grid); + + // init tab panes + addTabTo(treeViewDock, TREE_TAB_PANE_ID, 0); + addTabTo(treeBlocks, TREE_TAB_PANE_ID, 1); + addTabTo(layersDock, LAYER_TAB_PANE_ID, 0); + addTabTo(layerGroupsDock, LAYER_TAB_PANE_ID, 1); + treeTabPane.getStation().setFrontDockable(treeViewDock.intern()); + layerTabPane.getStation().setFrontDockable(layersDock.intern()); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/drawing/BlockChangeHandler.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/drawing/BlockChangeHandler.java new file mode 100644 index 0000000..3f66768 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/drawing/BlockChangeHandler.java @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.drawing; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.jhotdraw.draw.AbstractFigure; +import org.jhotdraw.draw.Figure; +import org.opentcs.guing.base.event.BlockChangeEvent; +import org.opentcs.guing.base.event.BlockChangeListener; +import org.opentcs.guing.base.model.FigureDecorationDetails; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.ModelComponentUtil; + +/** + */ +public class BlockChangeHandler + implements + BlockChangeListener { + + /** + * The members/elements of a block mapped to the block model. + */ + private final Map> blockElementsHistory + = new HashMap<>(); + + /** + * The manager keeping/providing the currently loaded model. + */ + private final ModelManager modelManager; + + /** + * Creates a new instance. + * + * @param modelManager The model manager. + */ + @Inject + public BlockChangeHandler(ModelManager modelManager) { + this.modelManager = requireNonNull(modelManager, "modelManager"); + } + + @Override // BlockChangeListener + public void courseElementsChanged(BlockChangeEvent e) { + BlockModel block = (BlockModel) e.getSource(); + + // Let the block's elements know the block they are now part of. + Set blockElements = block.getPropertyElements().getItems().stream() + .map(elementName -> modelManager.getModel().getModelComponent(elementName)) + .filter(modelComponent -> modelComponent instanceof FigureDecorationDetails) + .map(modelComponent -> (FigureDecorationDetails) modelComponent) + .collect(Collectors.toSet()); + for (FigureDecorationDetails component : blockElements) { + component.addBlockModel(block); + } + + // The elements that are no longer part of the block should also know this. + Set removedBlockElements = updateBlockElementHistory( + block, + blockElements + ); + for (FigureDecorationDetails component : removedBlockElements) { + component.removeBlockModel(block); + // Update the figure so that it no longer appears as being part of the block. + Figure figure = modelManager.getModel().getFigure(((ModelComponent) component)); + ((AbstractFigure) figure).fireFigureChanged(); + } + + updateBlock(block); + } + + @Override // BlockChangeListener + public void colorChanged(BlockChangeEvent e) { + updateBlock((BlockModel) e.getSource()); + } + + @Override // BlockChangeListener + public void blockRemoved(BlockChangeEvent e) { + BlockModel block = (BlockModel) e.getSource(); + + // Let the block's elements know they are no longer part of the block. + Set removedBlockElements + = updateBlockElementHistory(block, new HashSet<>()); + for (FigureDecorationDetails component : removedBlockElements) { + component.removeBlockModel(block); + } + + block.removeBlockChangeListener(this); + updateBlock(block); + } + + /** + * Remembers the given set of components as the new block elements for the given + * block model and returns the set difference of the old and the new block elements + * (e.g. the elements that are no longer part of the block). + * + * @param block The block model. + * @param newBlockElements The new block elements. + * @return The set difference of the old and the new block elements. + */ + private Set updateBlockElementHistory( + BlockModel block, + Set newBlockElements + ) { + Set oldBlockElements = getBlockElements(block); + Set removedBlockElements = new HashSet<>(oldBlockElements); + + removedBlockElements.removeAll(newBlockElements); + + oldBlockElements.clear(); + oldBlockElements.addAll(newBlockElements); + + return removedBlockElements; + } + + private Set getBlockElements(BlockModel block) { + return blockElementsHistory.computeIfAbsent(block, b -> new HashSet<>()); + } + + private void updateBlock(BlockModel block) { + for (Figure figure : ModelComponentUtil.getChildFigures(block, modelManager.getModel())) { + ((AbstractFigure) figure).fireFigureChanged(); + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/drawing/DeleteEdit.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/drawing/DeleteEdit.java new file mode 100644 index 0000000..7d9e4e4 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/drawing/DeleteEdit.java @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.drawing; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; +import javax.swing.undo.AbstractUndoableEdit; +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.Figure; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Undoes or redoes the "delete" action. + */ +public class DeleteEdit + extends + AbstractUndoableEdit { + + /** + * The drawing view we're working with. + */ + private final DrawingView drawingView; + /** + * The deleted figures. + */ + private final ArrayList
figures = new ArrayList<>(); + + /** + * Creates a new instance. + * + * @param drawingView The drawing view the delete happened in. + * @param figures The deleted figures. + */ + public DeleteEdit(DrawingView drawingView, List
figures) { + this.drawingView = requireNonNull(drawingView, "drawingView"); + this.figures.addAll(figures); + } + + @Override + public String getPresentationName() { + return ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MENU_PATH) + .getString("deleteEdit.presentationName"); + } + + @Override + public void undo() + throws CannotUndoException { + super.undo(); + drawingView.clearSelection(); + for (Figure figure : figures) { + drawingView.getDrawing().add(figure); + } + } + + @Override + public void redo() + throws CannotRedoException { + super.redo(); + for (Figure figure : figures) { + drawingView.getDrawing().remove(figure); + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/drawing/DrawingViewFactory.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/drawing/DrawingViewFactory.java new file mode 100644 index 0000000..686e5b1 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/drawing/DrawingViewFactory.java @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.drawing; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import javax.swing.JToggleButton; +import org.jhotdraw.gui.JPopupButton; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.application.StatusPanel; +import org.opentcs.guing.common.components.drawing.DrawingOptions; +import org.opentcs.guing.common.components.drawing.DrawingViewPlacardPanel; +import org.opentcs.guing.common.components.drawing.DrawingViewScrollPane; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingView; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * A factory for drawing views. + */ +public class DrawingViewFactory { + + /** + * A provider for drawing views. + */ + private final Provider drawingViewProvider; + /** + * The drawing editor. + */ + private final OpenTCSDrawingEditor drawingEditor; + /** + * The status panel to display the current mouse position in. + */ + private final StatusPanel statusPanel; + /** + * The manager keeping/providing the currently loaded model. + */ + private final ModelManager modelManager; + /** + * The drawing options. + */ + private final DrawingOptions drawingOptions; + + @Inject + public DrawingViewFactory( + Provider drawingViewProvider, + OpenTCSDrawingEditor drawingEditor, + StatusPanel statusPanel, + ModelManager modelManager, + DrawingOptions drawingOptions + ) { + this.drawingViewProvider = requireNonNull(drawingViewProvider, "drawingViewProvider"); + this.drawingEditor = requireNonNull(drawingEditor, "drawingEditor"); + this.statusPanel = requireNonNull(statusPanel, "statusPanel"); + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.drawingOptions = requireNonNull(drawingOptions, "drawingOptions"); + } + + /** + * Creates and returns a new drawing view along with its placard panel, both + * wrapped in a scroll pane. + * + * @param systemModel The system model. + * @param selectionToolButton The selection tool button in the tool bar. + * @param dragToolButton The drag tool button in the tool bar. + * @param linkCreationToolButton The link creation tool button in the tool bar. + * @param pathCreationToolButton The path creation tool button in the tool bar. + * @return A new drawing view, wrapped in a scroll pane. + */ + public DrawingViewScrollPane createDrawingView( + SystemModel systemModel, + JToggleButton selectionToolButton, + JToggleButton dragToolButton, + JToggleButton linkCreationToolButton, + JPopupButton pathCreationToolButton + ) { + requireNonNull(systemModel, "systemModel"); + requireNonNull(selectionToolButton, "selectionToolButton"); + requireNonNull(dragToolButton, "dragToolButton"); + + OpenTCSDrawingView drawingView = drawingViewProvider.get(); + drawingEditor.add(drawingView); + drawingEditor.setActiveView(drawingView); + for (VehicleModel vehicle : systemModel.getVehicleModels()) { + drawingView.displayDriveOrders(vehicle, vehicle.getDisplayDriveOrders()); + } + drawingView.setBlocks(systemModel.getMainFolder(SystemModel.FolderKey.BLOCKS)); + + DrawingViewPlacardPanel placardPanel = new DrawingViewPlacardPanel(drawingView, drawingOptions); + + DrawingViewScrollPane scrollPane = new DrawingViewScrollPane(drawingView, placardPanel); + scrollPane.originChanged(systemModel.getDrawingMethod().getOrigin()); + + // --- Listens to draggings in the drawing --- + ViewDragScrollListener dragScrollListener + = new ViewDragScrollListener( + scrollPane, + placardPanel.getZoomComboBox(), + selectionToolButton, + dragToolButton, + linkCreationToolButton, + pathCreationToolButton, + statusPanel, + modelManager + ); + drawingView.addMouseListener(dragScrollListener); + drawingView.addMouseMotionListener(dragScrollListener); + drawingView.getComponent().addMouseWheelListener(dragScrollListener); + + return scrollPane; + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/drawing/PasteEdit.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/drawing/PasteEdit.java new file mode 100644 index 0000000..587bcbd --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/drawing/PasteEdit.java @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.drawing; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; +import javax.swing.undo.AbstractUndoableEdit; +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.Figure; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Undoes or redoes a "paste" action. + */ +public class PasteEdit + extends + AbstractUndoableEdit { + + /** + * The drawing view we're working with. + */ + private final DrawingView drawingView; + /** + * The pasted figures. + */ + private final ArrayList
figures = new ArrayList<>(); + + /** + * Creates a new instance. + * + * @param drawingView The drawing view the paste happened in. + * @param figures The pasted figures. + */ + public PasteEdit(DrawingView drawingView, List
figures) { + this.drawingView = requireNonNull(drawingView, "drawingView"); + this.figures.addAll(figures); + } + + @Override + public String getPresentationName() { + return ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MENU_PATH) + .getString("pasteEdit.presentationName"); + } + + @Override + public void undo() + throws CannotUndoException { + super.undo(); + drawingView.getDrawing().removeAll(figures); + } + + @Override + public void redo() + throws CannotRedoException { + super.redo(); + drawingView.getDrawing().addAll(figures); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/drawing/ViewDragScrollListener.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/drawing/ViewDragScrollListener.java new file mode 100644 index 0000000..18786ce --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/drawing/ViewDragScrollListener.java @@ -0,0 +1,346 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.drawing; + +import static java.util.Objects.requireNonNull; + +import java.awt.Container; +import java.awt.Cursor; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseWheelEvent; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import javax.swing.JComboBox; +import javax.swing.JToggleButton; +import javax.swing.JViewport; +import javax.swing.SwingUtilities; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.gui.JPopupButton; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.common.application.StatusPanel; +import org.opentcs.guing.common.components.drawing.DrawingViewScrollPane; +import org.opentcs.guing.common.components.drawing.ZoomItem; +import org.opentcs.guing.common.components.drawing.figures.LabeledLocationFigure; +import org.opentcs.guing.common.components.drawing.figures.LabeledPointFigure; +import org.opentcs.guing.common.components.drawing.figures.PathConnection; +import org.opentcs.guing.common.components.drawing.figures.liner.TripleBezierLiner; +import org.opentcs.guing.common.components.drawing.figures.liner.TupelBezierLiner; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * A listener for dragging of the drawing view and single objects inside the + * view. + */ +public class ViewDragScrollListener + extends + MouseAdapter { + + /** + * The scroll pane enclosing the drawing view. + */ + private final DrawingViewScrollPane scrollPane; + /** + * The combo box for selecting the zoom level. + */ + private final JComboBox zoomComboBox; + /** + * The button for enabling object selection. + */ + private final JToggleButton selectionTool; + /** + * The button for enabling dragging. + */ + private final JToggleButton dragTool; + /** + * The button for creating a link. + */ + private final JToggleButton linkCreationTool; + /** + * The button for creating a path. + */ + private final JPopupButton pathCreationTool; + /** + * The status panel to display the current mouse position in. + */ + private final StatusPanel statusPanel; + /** + * The manager keeping/providing the currently loaded model. + */ + private final ModelManager modelManager; + /** + * A default cursor for the drawing view. + */ + private final Cursor defaultCursor; + /** + * The start position of drag movements. + */ + private final Point startPoint = new Point(); + /** + * Start coordinate for measuring. + * XXX Is this redundant, or is it used for something different than startPoint? + */ + private final Point2D.Double fMouseStartPoint = new Point2D.Double(); + /** + * Current coordinate for measuring. + */ + private final Point2D.Double fMouseCurrentPoint = new Point2D.Double(); + /** + * End coordinate for measuring. + */ + private final Point2D.Double fMouseEndPoint = new Point2D.Double(); + /** + * The figure a user may have pressed on / want to drag. + */ + private Figure pressedFigure; + + /** + * Creates a new instance. + * + * @param scrollPane The scroll pane enclosing the drawing view. + * @param zoomComboBox The combo box for selecting the zoom level. + * @param selectionTool The button for enabling object selection. + * @param dragTool The button for enabling dragging. + * @param linkCreationTool The button for creating a link. + * @param pathCreationTool The button for creating a path. + * @param statusPanel The status panel to display the current mouse position in. + * @param modelManager The manager keeping/providing the currently loaded model. + */ + public ViewDragScrollListener( + DrawingViewScrollPane scrollPane, + JComboBox zoomComboBox, + JToggleButton selectionTool, + JToggleButton dragTool, + JToggleButton linkCreationTool, + JPopupButton pathCreationTool, + StatusPanel statusPanel, + ModelManager modelManager + ) { + this.scrollPane = requireNonNull(scrollPane, "scrollPane"); + this.zoomComboBox = requireNonNull(zoomComboBox, "zoomComboBox"); + this.selectionTool = requireNonNull(selectionTool, "selectionTool"); + this.dragTool = requireNonNull(dragTool, "dragTool"); + this.linkCreationTool = requireNonNull(linkCreationTool, "linkCreationTool"); + this.pathCreationTool = requireNonNull(pathCreationTool, "pathCreationTool"); + this.statusPanel = requireNonNull(statusPanel, "statusPanel"); + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.defaultCursor = scrollPane.getDrawingView().getComponent().getCursor(); + } + + @Override + public void mouseDragged(final MouseEvent evt) { + final DrawingView drawingView = scrollPane.getDrawingView(); + if (!(drawingView.getComponent().getParent() instanceof JViewport)) { + return; + } + + final JViewport viewport = (JViewport) drawingView.getComponent().getParent(); + Point cp = SwingUtilities.convertPoint(drawingView.getComponent(), evt.getPoint(), viewport); + + if (dragTool.isSelected()) { + int dx = startPoint.x - cp.x; + int dy = startPoint.y - cp.y; + Point vp = viewport.getViewPosition(); + vp.translate(dx, dy); + drawingView.getComponent().scrollRectToVisible(new Rectangle(vp, viewport.getSize())); + } + else if (linkCreationTool.isSelected() || pathCreationTool.isSelected()) { + viewport.revalidate(); + // Start scrolling as soon as the mouse is hitting the view bounds. + drawingView.getComponent().scrollRectToVisible(new Rectangle(evt.getX(), evt.getY(), 1, 1)); + } + else { // The selection tool is selected + viewport.revalidate(); + + if (isMovableFigure(pressedFigure)) { + if (!isFigureCompletelyInView(pressedFigure, viewport, drawingView)) { + // If the figure exceeds the current view, start scrolling as soon as the mouse is + // hitting the view bounds. + drawingView.getComponent().scrollRectToVisible( + new Rectangle(evt.getX(), evt.getY(), 1, 1) + ); + } + + fMouseCurrentPoint.setLocation(drawingView.viewToDrawing(evt.getPoint())); + showPositionStatus(false); + startPoint.setLocation(cp); + } + } + SwingUtilities.invokeLater(() -> { + Rectangle2D.Double drawingArea = drawingView.getDrawing().getDrawingArea(); + scrollPane.getHorizontalRuler().setPreferredWidth((int) drawingArea.width); + scrollPane.getVerticalRuler().setPreferredHeight((int) drawingArea.height); + }); + } + + private boolean isMovableFigure(Figure figure) { + return (figure instanceof LabeledPointFigure) + || (figure instanceof LabeledLocationFigure) + || ((figure instanceof PathConnection) + && (((PathConnection) figure).getLiner() instanceof TupelBezierLiner)) + || ((figure instanceof PathConnection) + && (((PathConnection) figure).getLiner() instanceof TripleBezierLiner)); + } + + private boolean isFigureCompletelyInView( + Figure figure, + JViewport viewport, + DrawingView drawingView + ) { + Rectangle viewPortBounds = viewport.getViewRect(); + Rectangle figureBounds = drawingView.drawingToView(figure.getDrawingArea()); + + return (figureBounds.getMinX() > viewPortBounds.getMinX()) + && (figureBounds.getMinY() > viewPortBounds.getMinY()) + && (figureBounds.getMaxX() < viewPortBounds.getMaxX()) + && (figureBounds.getMaxY() < viewPortBounds.getMaxY()); + } + + @Override + public void mousePressed(MouseEvent evt) { + final DrawingView drawingView = scrollPane.getDrawingView(); + if (dragIsSelected()) { + drawingView.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); + } + Container c = drawingView.getComponent().getParent(); + if (c instanceof JViewport) { + JViewport viewPort = (JViewport) c; + Point cp = SwingUtilities.convertPoint(drawingView.getComponent(), evt.getPoint(), viewPort); + startPoint.setLocation(cp); + } + pressedFigure = drawingView.findFigure(evt.getPoint()); + fMouseCurrentPoint.setLocation(drawingView.viewToDrawing(evt.getPoint())); + fMouseStartPoint.setLocation(drawingView.viewToDrawing(evt.getPoint())); + showPositionStatus(false); + } + + @Override + public void mouseReleased(MouseEvent evt) { + if (dragIsSelected()) { + return; + } + + final DrawingView drawingView = scrollPane.getDrawingView(); + pressedFigure = null; + fMouseEndPoint.setLocation(drawingView.viewToDrawing(evt.getPoint())); + if (evt.getButton() != 2) { + showPositionStatus(true); + } + else { + showPositionStatus(false); + } + } + + @Override + public void mouseExited(MouseEvent evt) { + dragIsSelected(); + clearPositionStatus(); + } + + @Override + public void mouseEntered(MouseEvent evt) { + dragIsSelected(); + } + + @Override + public void mouseMoved(MouseEvent evt) { + final DrawingView drawingView = scrollPane.getDrawingView(); + fMouseCurrentPoint.setLocation(drawingView.viewToDrawing(evt.getPoint())); + showPositionStatus(false); + } + + @Override + public void mouseClicked(MouseEvent evt) { + if (evt.getButton() == 2) { + if (dragTool.isSelected()) { + selectionTool.setSelected(true); + } + else if (selectionTool.isSelected()) { + dragTool.setSelected(true); + } + // Sets the correct cursor + dragIsSelected(); + } + } + + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + if (e.isControlDown()) { + int zoomLevel = zoomComboBox.getSelectedIndex(); + int notches = e.getWheelRotation(); + if (zoomLevel != -1) { + if (notches < 0) { + if (zoomLevel > 0) { + zoomLevel--; + zoomComboBox.setSelectedIndex(zoomLevel); + } + } + else { + if (zoomLevel < zoomComboBox.getItemCount() - 1) { + zoomLevel++; + zoomComboBox.setSelectedIndex(zoomLevel); + } + } + } + } + } + + /** + * Checks whether the drag tool is selected. + * + * @return true if the drag tool is selected, false otherwise. + */ + private boolean dragIsSelected() { + final DrawingView drawingView = scrollPane.getDrawingView(); + if (!selectionTool.isSelected() && dragTool.isSelected()) { + drawingView.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + return true; + } + else if (selectionTool.isSelected() && !dragTool.isSelected()) { + drawingView.setCursor(defaultCursor); + return false; + } + else { + return false; + } + } + + /** + * Clears the mouse position information in the status panel. + */ + private void clearPositionStatus() { + statusPanel.setPositionText(""); + } + + /** + * Displays the current mouse position or covered area in the status panel. + * + * @param showCoveredArea Whether to display the dimensions of the covered + * area instead of the current mouse coordinates. + */ + private void showPositionStatus(boolean showCoveredArea) { + double x = fMouseCurrentPoint.x; + double y = -fMouseCurrentPoint.y; + + if (showCoveredArea) { + double w = Math.abs(fMouseEndPoint.x - fMouseStartPoint.x); + double h = Math.abs(fMouseEndPoint.y - fMouseStartPoint.y); + statusPanel.setPositionText( + String.format("X %.0f Y %.0f W %.0f H %.0f", x, y, w, h) + ); + } + else { + LayoutModel layout = modelManager.getModel().getLayoutModel(); + double scaleX = (double) layout.getPropertyScaleX().getValue(); + double scaleY = (double) layout.getPropertyScaleY().getValue(); + double xmm = x * scaleX; + double ymm = y * scaleY; + statusPanel.setPositionText( + String.format("X %.0f (%.0fmm) Y %.0f (%.0fmm)", x, xmm, y, ymm) + ); + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/ActiveLayerProvider.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/ActiveLayerProvider.java new file mode 100644 index 0000000..e43fb14 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/ActiveLayerProvider.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.layer; + +import org.opentcs.guing.base.components.layer.LayerWrapper; + +/** + * Provides a method to get the currently active layer. + */ +public interface ActiveLayerProvider { + + /** + * Returns the {@link LayerWrapper} instance that holds the currently active layer. + * + * @return The currently active layer. + */ + LayerWrapper getActiveLayer(); +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayerEditorEventHandler.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayerEditorEventHandler.java new file mode 100644 index 0000000..2e448b0 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayerEditorEventHandler.java @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.layer; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.jhotdraw.draw.Figure; +import org.opentcs.guing.base.model.DrawnModelComponent; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.common.components.drawing.figures.FigureConstants; +import org.opentcs.guing.common.event.DrawingEditorEvent; +import org.opentcs.guing.common.event.DrawingEditorListener; + +/** + * Handles drawing editor events that the layer editor needs to know about. + */ +public class LayerEditorEventHandler + implements + DrawingEditorListener { + + /** + * The layer editor. + */ + private final LayerEditorModeling layerEditor; + + @Inject + public LayerEditorEventHandler(LayerEditorModeling layerEditor) { + this.layerEditor = requireNonNull(layerEditor, "layerEditor"); + } + + @Override + public void figureAdded(DrawingEditorEvent e) { + Figure figure = e.getFigure(); + ModelComponent model = figure.get(FigureConstants.MODEL); + if (model instanceof DrawnModelComponent) { + layerEditor.add((DrawnModelComponent) model); + } + } + + @Override + public void figureRemoved(DrawingEditorEvent e) { + Figure figure = e.getFigure(); + ModelComponent model = figure.get(FigureConstants.MODEL); + if (model instanceof DrawnModelComponent) { + layerEditor.remove((DrawnModelComponent) model); + } + } + + @Override + public void figureSelected(DrawingEditorEvent e) { + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayerEditorModeling.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayerEditorModeling.java new file mode 100644 index 0000000..7d2872e --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayerEditorModeling.java @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.layer; + +import org.opentcs.guing.base.model.DrawnModelComponent; +import org.opentcs.guing.common.components.layer.LayerEditor; + +/** + * Provides methods to edit layers. + */ +public interface LayerEditorModeling + extends + LayerEditor { + + /** + * Creates a new layer. + */ + void createLayer(); + + /** + * Deletes the layer with the given layer ID. + * + * @param layerId The ID of the layer to delete. + * @throws IllegalArgumentException If a layer with the given layer ID doesn't exist. + */ + void deleteLayer(int layerId) + throws IllegalArgumentException; + + /** + * Adds the given model component to the layer that is set in the component's layer wrapper + * property. + * + * @param modelComponent The model component to add. + */ + void add(DrawnModelComponent modelComponent); + + /** + * Removes the given model component from its layer. + * + * @param modelComponent The model component to remove. + */ + void remove(DrawnModelComponent modelComponent); + + /** + * Moves the layer with the given ID one level down. + * + * @param layerId The ID of the layer. + */ + void moveLayerDown(int layerId); + + /** + * Moves the layer with the given ID one level up. + * + * @param layerId The ID of the layer. + */ + void moveLayerUp(int layerId); + + /** + * Sets the layer with the given ID as the active layer. + * + * @param layerId The ID of the layer. + */ + void setLayerActive(int layerId); + + /** + * Sets the group ID for the layer with the given ID. + * + * @param layerId The ID of the layer. + * @param groupId The ID of the layer group. + */ + void setLayerGroupId(int layerId, int groupId); +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayerGroupEditorModeling.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayerGroupEditorModeling.java new file mode 100644 index 0000000..22a9fad --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayerGroupEditorModeling.java @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.layer; + +import org.opentcs.guing.common.components.layer.LayerGroupEditor; + +/** + * Provides methods to edit layer groups. + */ +public interface LayerGroupEditorModeling + extends + LayerGroupEditor { + + /** + * Creates a new layer group. + */ + void createLayerGroup(); + + /** + * Deletes the layer group with the given layer group ID. + * + * @param groupId The ID of the layer group to delete. + * @throws IllegalArgumentException If a layer group with the given layer group ID doesn't exist. + */ + void deleteLayerGroup(int groupId) + throws IllegalArgumentException; +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayerGroupsPanel.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayerGroupsPanel.java new file mode 100644 index 0000000..527cfad --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayerGroupsPanel.java @@ -0,0 +1,218 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.layer; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.stream.Collectors; +import javax.swing.JButton; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.JToolBar; +import javax.swing.ListSelectionModel; +import javax.swing.RowSorter; +import javax.swing.SortOrder; +import javax.swing.table.TableRowSorter; +import org.opentcs.data.model.visualization.Layer; +import org.opentcs.guing.common.components.layer.LayerGroupManager; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.IconToolkit; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; + +/** + * A panel to display and edit layer groups. + */ +public class LayerGroupsPanel + extends + JPanel { + + /** + * The path containing the icons. + */ + private static final String ICON_PATH = "/org/opentcs/guing/res/symbols/layer/"; + /** + * The resource bundle to use. + */ + private static final ResourceBundle BUNDLE + = ResourceBundle.getBundle(I18nPlantOverviewModeling.LAYERS_PATH); + /** + * The model manager. + */ + private final ModelManager modelManager; + /** + * The layer manager. + */ + private final LayerGroupManager layerGroupManager; + /** + * The layer editor. + */ + private final LayerGroupEditorModeling layerGroupEditor; + /** + * The table to display available layers. + */ + private JTable table; + /** + * The table model. + */ + private LayerGroupsTableModel tableModel; + + @Inject + @SuppressWarnings("this-escape") + public LayerGroupsPanel( + ModelManager modelManager, + LayerGroupManager layerGroupManager, + LayerGroupEditorModeling layerGroupEditor + ) { + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.layerGroupManager = requireNonNull(layerGroupManager, "layerGroupManager"); + this.layerGroupEditor = requireNonNull(layerGroupEditor, "layerGroupEditor"); + + initComponents(); + } + + private void initComponents() { + setLayout(new BorderLayout()); + + tableModel = new LayerGroupsTableModel(modelManager, layerGroupEditor); + layerGroupManager.addLayerGroupChangeListener(tableModel); + table = new JTable(tableModel); + initTable(); + + add(createToolBar(), BorderLayout.NORTH); + add(new JScrollPane(table), BorderLayout.CENTER); + } + + private void initTable() { + table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + TableRowSorter sorter = new TableRowSorter<>(tableModel); + // Sort the table by the layer ordinals... + sorter.setSortKeys( + Arrays.asList( + new RowSorter.SortKey(LayerGroupsTableModel.COLUMN_ID, SortOrder.DESCENDING) + ) + ); + // ...but prevent manual sorting. + for (int i = 0; i < table.getColumnCount(); i++) { + sorter.setSortable(i, false); + } + sorter.setSortsOnUpdates(true); + table.setRowSorter(sorter); + + // Hide the column that shows the layer group IDs. + table.removeColumn( + table.getColumnModel() + .getColumn(table.convertColumnIndexToView(LayerGroupsTableModel.COLUMN_ID)) + ); + } + + private JToolBar createToolBar() { + JToolBar toolBar = new JToolBar(); + toolBar.setFloatable(false); + + toolBar.add(createAddGroupButton()); + toolBar.add(createRemoveGroupButton()); + + return toolBar; + } + + private JButton createAddGroupButton() { + IconToolkit iconkit = IconToolkit.instance(); + JButton button + = new JButton(iconkit.getImageIconByFullPath(ICON_PATH + "create-layer-group.16.png")); + button.addActionListener(actionEvent -> { + layerGroupEditor.createLayerGroup(); + table.getSelectionModel().setSelectionInterval(0, 0); + }); + button.setToolTipText(BUNDLE.getString("layerGroupsPanel.button_addGroup.tooltipText")); + + return button; + } + + private JButton createRemoveGroupButton() { + IconToolkit iconkit = IconToolkit.instance(); + JButton button + = new JButton(iconkit.getImageIconByFullPath(ICON_PATH + "delete-layer-group.16.png")); + button.addActionListener(new RemoveGroupListener()); + button.setToolTipText(BUNDLE.getString("layerGroupsPanel.button_removeGroup.tooltipText")); + + // Allow the remove group button to be pressed only if there's a group selected and if there's + // more than one group in the model. + table.getSelectionModel().addListSelectionListener(listSelectionEvent -> { + button.setEnabled(table.getSelectedRow() != -1 && tableModel.getRowCount() > 1); + }); + tableModel.addTableModelListener(tableModelEvent -> { + button.setEnabled(table.getSelectedRow() != -1 && tableModel.getRowCount() > 1); + }); + + return button; + } + + private class RemoveGroupListener + implements + ActionListener { + + /** + * Creates a new instance. + */ + RemoveGroupListener() { + } + + @Override + public void actionPerformed(ActionEvent e) { + int selectedRow = table.getSelectedRow(); + int selectedGroupId = tableModel.getDataAt(table.convertRowIndexToModel(selectedRow)).getId(); + + Map> layersByGroupAssignment = modelManager.getModel() + .getLayoutModel().getPropertyLayerWrappers().getValue().values().stream() + .map(wrapper -> wrapper.getLayer()) + .collect(Collectors.partitioningBy(layer -> layer.getGroupId() == selectedGroupId)); + List layersAssignedToGroupToDelete = layersByGroupAssignment.get(Boolean.TRUE); + List layersAssignedToOtherGroups = layersByGroupAssignment.get(Boolean.FALSE); + + if (layersAssignedToOtherGroups.isEmpty()) { + // All layers in the model are assigned to the group the user wants to remove. + // In this case, removing the group is not allowed as that would mean that all layers would + // be removed as well and there wouldn't be any layers left. + JOptionPane.showMessageDialog( + LayerGroupsPanel.this, + BUNDLE.getString("layerGroupsPanel.optionPane_groupRemovalNotPossible.message"), + BUNDLE.getString("layerGroupsPanel.optionPane_groupRemovalNotPossible.title"), + JOptionPane.INFORMATION_MESSAGE + ); + return; + } + + if (!layersAssignedToGroupToDelete.isEmpty()) { + // The user is about remove a group with layers assigned to it. Removing the group results + // in the assigned layers and the model components they contain to be removed as well. + int selectedOption = JOptionPane.showConfirmDialog( + LayerGroupsPanel.this, + BUNDLE.getString( + "layerGroupsPanel.optionPane_confirmGroupAndAssignedLayersRemoval.message" + ), + BUNDLE.getString( + "layerGroupsPanel.optionPane_confirmGroupAndAssignedLayersRemoval.title" + ), + JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE + ); + if (selectedOption == JOptionPane.NO_OPTION) { + return; + } + } + + layerGroupEditor.deleteLayerGroup(selectedGroupId); + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayerGroupsTableModel.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayerGroupsTableModel.java new file mode 100644 index 0000000..0a97a4c --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayerGroupsTableModel.java @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.layer; + +import javax.swing.SwingUtilities; +import org.opentcs.guing.common.components.layer.AbstractLayerGroupsTableModel; +import org.opentcs.guing.common.components.layer.LayerGroupEditor; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * The table model for layer groups for the Model Editor application. + */ +class LayerGroupsTableModel + extends + AbstractLayerGroupsTableModel { + + /** + * Creates a new instance. + * + * @param modelManager The model manager. + * @param layerGroupEditor The layer group editor. + */ + LayerGroupsTableModel(ModelManager modelManager, LayerGroupEditor layerGroupEditor) { + super(modelManager, layerGroupEditor); + } + + @Override + protected boolean isNameColumnEditable() { + return true; + } + + @Override + protected boolean isVisibleColumnEditable() { + return true; + } + + @Override + public void groupsInitialized() { + // Once the layers are initialized we want to redraw the entire table to avoid any + // display errors. + executeOnEventDispatcherThread(() -> fireTableDataChanged()); + } + + @Override + public void groupsChanged() { + // Update the entire table but don't use fireTableDataChanged() to preserve the current + // selection. + executeOnEventDispatcherThread(() -> fireTableRowsUpdated(0, getRowCount() - 1)); + } + + @Override + public void groupAdded() { + // Groups are always added to the top (with regard to sorting). + executeOnEventDispatcherThread(() -> fireTableRowsInserted(0, 0)); + } + + @Override + public void groupRemoved() { + // At this point, there's no way for us to determine the row the removed layer was in. The + // entry has already been remove from this table model's data source which is provided by + // layersByOrdinal(). + // Workaround: Since the table now contains one entry less, pretend that the last entry was + // deleted. + executeOnEventDispatcherThread(() -> fireTableRowsDeleted(getRowCount(), getRowCount())); + } + + /** + * Ensures the given runnable is executed on the EDT. + * If the runnable is already being called on the EDT, the runnable is executed immediately. + * Otherwise it is scheduled for execution on the EDT. + *

+ * Note: Deferring a runnable by scheduling it for execution on the EDT even though it would + * have already been executed on the EDT may lead to exceptions due to data inconsistency. + *

+ * + * @param runnable The runnable. + */ + private void executeOnEventDispatcherThread(Runnable runnable) { + if (SwingUtilities.isEventDispatchThread()) { + runnable.run(); + } + else { + SwingUtilities.invokeLater(runnable); + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayerManagerModeling.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayerManagerModeling.java new file mode 100644 index 0000000..f32be35 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayerManagerModeling.java @@ -0,0 +1,421 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.layer; + +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.inject.Inject; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import org.jhotdraw.draw.AbstractFigure; +import org.jhotdraw.draw.Drawing; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.model.visualization.Layer; +import org.opentcs.data.model.visualization.LayerGroup; +import org.opentcs.guing.base.components.layer.LayerWrapper; +import org.opentcs.guing.base.model.DrawnModelComponent; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.common.application.ViewManager; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingView; +import org.opentcs.guing.common.components.layer.DefaultLayerManager; +import org.opentcs.guing.common.components.layer.LayerManager; +import org.opentcs.util.event.EventBus; + +/** + * The {@link LayerManager} implementation for the model editor application. + */ +public class LayerManagerModeling + extends + DefaultLayerManager + implements + LayerEditorModeling, + LayerGroupEditorModeling, + ActiveLayerProvider { + + /** + * The currently active layer. + */ + private LayerWrapper activeLayerWrapper; + + @Inject + public LayerManagerModeling( + ViewManager viewManager, + @ApplicationEventBus + EventBus eventBus + ) { + super(viewManager, eventBus); + } + + @Override + public void createLayer() { + Layer layer = createLayerWrapper().getLayer(); + getLayerChangeListener().layerAdded(); + + setLayerActive(layer.getId()); + } + + @Override + public void deleteLayer(int layerId) + throws IllegalArgumentException { + checkArgument( + getLayerWrapper(layerId) != null, + "A layer with layer ID '%d' doesn't exist.", + layerId + ); + + deleteLayerWrapper(layerId); + getLayerChangeListener().layerRemoved(); + + if (layerId == activeLayerWrapper.getLayer().getId()) { + handleActiveLayerRemoved(); + } + } + + @Override + public void add(DrawnModelComponent modelComponent) { + int layerId = modelComponent.getPropertyLayerWrapper().getValue().getLayer().getId(); + checkArgument( + getLayerWrapper(layerId) != null, + "A layer with layer ID '%d' doesn't exist.", + layerId + ); + + addComponent(modelComponent, layerId); + } + + @Override + public void remove(DrawnModelComponent modelComponent) { + removeComponent(modelComponent); + } + + @Override + public void moveLayerDown(int layerId) { + checkArgument( + getLayerWrapper(layerId) != null, + "A layer with layer ID '%d' doesn't exist.", + layerId + ); + + List layerWrappersByOrdinal = getLayerWrappers().values().stream() + .sorted(Comparator.comparing(wrapper -> wrapper.getLayer().getOrdinal())) + .collect(Collectors.toList()); + + if (layerId == layerWrappersByOrdinal.get(0).getLayer().getId()) { + // The layer with the given layer ID is already the lowest layer. + return; + } + + shiftLayerByOne(layerId, layerWrappersByOrdinal); + + getLayerChangeListener().layersChanged(); + } + + @Override + public void moveLayerUp(int layerId) { + checkArgument( + getLayerWrapper(layerId) != null, + "A layer with layer ID '%d' doesn't exist.", + layerId + ); + + List layerWrappersByReverseOrdinal = getLayerWrappers().values().stream() + .sorted( + Comparator.comparing( + wrapper -> wrapper.getLayer().getOrdinal(), + Comparator.reverseOrder() + ) + ) + .collect(Collectors.toList()); + + if (layerId == layerWrappersByReverseOrdinal.get(0).getLayer().getId()) { + // The layer with the given layer ID is already the heighest layer. + return; + } + + shiftLayerByOne(layerId, layerWrappersByReverseOrdinal); + + getLayerChangeListener().layersChanged(); + } + + @Override + public void setLayerActive(int layerId) { + checkArgument( + getLayerWrapper(layerId) != null, + "A layer with layer ID '%d' doesn't exist.", + layerId + ); + + activeLayerWrapper = getLayerWrapper(layerId); + + getLayerChangeListener().layersChanged(); + } + + @Override + public void createLayerGroup() { + int groupId = getNextAvailableLayerGroupId(); + + // Add the created layer group to the system model's layout. + addLayerGroup(new LayerGroup(groupId, "Group " + groupId, true)); + + notifyGroupAdded(); + } + + @Override + public void deleteLayerGroup(int groupId) + throws IllegalArgumentException { + checkArgument( + getLayerGroup(groupId) != null, + "A layer group with layer group ID '%d' doesn't exist.", + groupId + ); + + // Delete the layers that are assigned to the group. + Set layerAssignedToGroup = getLayerWrappers().values().stream() + .map(wrapper -> wrapper.getLayer()) + .filter(layer -> layer.getGroupId() == groupId) + .map(layer -> layer.getId()) + .collect(Collectors.toSet()); + layerAssignedToGroup.forEach(layerId -> deleteLayer(layerId)); + + removeLayerGroup(groupId); + notifyGroupRemoved(); + } + + @Override + public void setLayerGroupId(int layerId, int groupId) { + checkArgument( + getLayerWrapper(layerId) != null, + "A layer with layer ID '%d' doesn't exist.", + layerId + ); + checkArgument( + getLayerGroup(groupId) != null, + "A layer group with layer group ID '%d' doesn't exist.", + groupId + ); + + LayerWrapper wrapper = getLayerWrapper(layerId); + + boolean visibleBefore = wrapper.getLayer().isVisible() && wrapper.getLayerGroup().isVisible(); + wrapper.setLayer(wrapper.getLayer().withGroupId(groupId)); + wrapper.setLayerGroup(getLayerGroup(groupId)); + boolean visibleAfter = wrapper.getLayer().isVisible() && wrapper.getLayerGroup().isVisible(); + + if (visibleBefore != visibleAfter) { + layerVisibilityChanged(wrapper.getLayer(), visibleAfter); + } + + getLayerChangeListener().layersChanged(); + } + + @Override + public LayerWrapper getActiveLayer() { + return activeLayerWrapper; + } + + @Override + protected void reset() { + super.reset(); + } + + @Override + protected void restoreLayers() { + // Make sure there will be one layer set as the active layer. Do it for the highest layer. + activeLayerWrapper = getLayerWrappers().values().stream() + .sorted( + Comparator.comparing( + wrapper -> wrapper.getLayer().getOrdinal(), + Comparator.reverseOrder() + ) + ) + .findFirst() + .get(); + + super.restoreLayers(); + } + + private LayerWrapper createLayerWrapper() { + int layerId = getNextAvailableLayerId(); + int layerOrdinal = getNextAvailableLayerOrdinal(); + LayerGroup group = getLayerGroups().values().iterator().next(); + Layer layer = new Layer(layerId, layerOrdinal, true, "Layer " + layerId, group.getId()); + LayerWrapper wrapper = new LayerWrapper(layer, group); + getComponents().put(layerId, new HashSet<>()); + + // Add the created layer wrapper to the system model's layout. + addLayerWrapper(wrapper); + + return wrapper; + } + + private int getNextAvailableLayerId() { + return getLayerWrappers().values().stream() + .mapToInt(wrapper -> wrapper.getLayer().getId()) + .max() + .getAsInt() + 1; + } + + private int getNextAvailableLayerOrdinal() { + return getLayerWrappers().values().stream() + .mapToInt(wrapper -> wrapper.getLayer().getOrdinal()) + .max() + .getAsInt() + 1; + } + + private int getNextAvailableLayerGroupId() { + return getLayerGroups().values().stream() + .mapToInt(group -> group.getId()) + .max() + .getAsInt() + 1; + } + + private void deleteLayerWrapper(int layerId) { + Set drawingViews = getDrawingViews(); + drawingViews.forEach(drawingView -> drawingView.getDrawing().willChange()); + + Set componentsToDelete = getComponents().get(layerId).stream() + .map(component -> (ModelComponent) component) + .collect(Collectors.toSet()); + drawingViews.forEach(drawingView -> drawingView.delete(componentsToDelete)); + + drawingViews.forEach(drawingView -> drawingView.getDrawing().changed()); + + getComponents().remove(layerId); + + // Remove the deleted layer wrapper from the system model's layout. + removeLayerWrapper(layerId); + } + + /** + * Returns the set of drawing views the layer manager is working with. + * + * @return The set of drawing views the layer manager is working with. + */ + private Set getDrawingViews() { + return getViewManager().getDrawingViewMap().values().stream() + .map(scrollPane -> scrollPane.getDrawingView()) + .collect(Collectors.toSet()); + } + + private void handleActiveLayerRemoved() { + List layersByOrdinal = getLayerWrappers().values().stream() + .map(wrapper -> wrapper.getLayer()) + .sorted(Comparator.comparing(layer -> layer.getOrdinal())) + .collect(Collectors.toList()); + + Optional layerUnderRemovedActiveLayer = layersByOrdinal.stream() + .takeWhile(layer -> layer.getOrdinal() < activeLayerWrapper.getLayer().getOrdinal()) + .reduce((layer1, layer2) -> layer2); + + // If there's a layer right under the active layer that just has been removed, select that layer + // as the new active layer. Otherwise, just select the lowest layer. + if (layerUnderRemovedActiveLayer.isPresent()) { + setLayerActive(layerUnderRemovedActiveLayer.get().getId()); + } + else { + setLayerActive(layersByOrdinal.get(0).getId()); + } + } + + private void shiftLayerByOne(int layerId, List layers) { + LayerWrapper layerWrapper = getLayerWrapper(layerId); + int layerWrapperBeforeIndex = layers.indexOf(layerWrapper) - 1; + LayerWrapper layerWrapperBefore = layers.get(layerWrapperBeforeIndex); + swapLayerOrdinals(layerWrapper, layerWrapperBefore); + updateLayerComponentsInDrawing(layerWrapper.getLayer(), layerWrapperBefore.getLayer()); + } + + private void swapLayerOrdinals(LayerWrapper layerWrapper1, LayerWrapper layerWrapper2) { + int ordinal1 = layerWrapper1.getLayer().getOrdinal(); + int ordinal2 = layerWrapper2.getLayer().getOrdinal(); + + Layer oldLayer = layerWrapper1.getLayer(); + Layer newLayer = oldLayer.withOrdinal(ordinal2); + layerWrapper1.setLayer(newLayer); + + oldLayer = layerWrapper2.getLayer(); + newLayer = oldLayer.withOrdinal(ordinal1); + layerWrapper2.setLayer(newLayer); + } + + private void updateLayerComponentsInDrawing(Layer... layers) { + Set componentsToUpdate = new HashSet<>(); + for (Layer layer : layers) { + if (layer.isVisible()) { + componentsToUpdate.addAll(getComponents().get(layer.getId())); + } + } + + Set drawings = getDrawings(); + + drawings.forEach(drawing -> drawing.willChange()); + for (DrawnModelComponent modelComponent : componentsToUpdate) { + AbstractFigure figure = (AbstractFigure) getSystemModel().getFigure(modelComponent); + drawings.forEach(drawing -> { + drawing.basicRemove(figure); + drawing.basicAdd(figure); + }); + } + drawings.forEach(drawing -> drawing.changed()); + } + + /** + * Adds the given layer group to the system model's layout. + * + * @param layerWrapper The layer group to add. + * @throws IllegalArgumentException If a layer group with the same layer group ID already exists + * in the system model's layout. + */ + private void addLayerGroup(LayerGroup layerGroup) + throws IllegalArgumentException { + int groupId = layerGroup.getId(); + checkArgument( + getLayerGroup(groupId) == null, + "A layer group for group ID '%d' already exists in the model.", + groupId + ); + + getLayerGroups().put(groupId, layerGroup); + } + + /** + * Removes the layer group with the given group ID from the system model's layout. + * + * @param layerId The layer group ID. + */ + private void removeLayerGroup(int groupId) { + getLayerGroups().remove(groupId); + } + + /** + * Adds the given layer wrapper to the system model's layout. + * + * @param layerWrapper The layer wrapper to add. + * @throws IllegalArgumentException If a layer wrapper with the same layer ID already exists + * in the system model's layout. + */ + private void addLayerWrapper(LayerWrapper layerWrapper) + throws IllegalArgumentException { + int layerId = layerWrapper.getLayer().getId(); + checkArgument( + getLayerWrapper(layerId) == null, + "A layer wrapper for layer ID '%d' already exists in the model.", + layerId + ); + + getLayerWrappers().put(layerId, layerWrapper); + } + + /** + * Removes the layer wrapper with the given layer ID from the system model's layout. + * + * @param layerId The layer ID. + */ + private void removeLayerWrapper(int layerId) { + getLayerWrappers().remove(layerId); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayersPanel.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayersPanel.java new file mode 100644 index 0000000..ff0d561 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayersPanel.java @@ -0,0 +1,380 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.layer; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Arrays; +import java.util.List; +import java.util.ResourceBundle; +import java.util.stream.Collectors; +import javax.swing.AbstractCellEditor; +import javax.swing.BorderFactory; +import javax.swing.DefaultCellEditor; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.JToolBar; +import javax.swing.ListSelectionModel; +import javax.swing.RowSorter; +import javax.swing.SortOrder; +import javax.swing.UIManager; +import javax.swing.border.Border; +import javax.swing.table.TableCellEditor; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import javax.swing.table.TableRowSorter; +import org.opentcs.data.model.visualization.LayerGroup; +import org.opentcs.guing.common.components.layer.DisabledCheckBoxCellRenderer; +import org.opentcs.guing.common.components.layer.LayerGroupCellRenderer; +import org.opentcs.guing.common.components.layer.LayerGroupManager; +import org.opentcs.guing.common.components.layer.LayerManager; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.IconToolkit; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.util.gui.StringListCellRenderer; + +/** + * A panel to display and edit layers. + */ +public class LayersPanel + extends + JPanel { + + /** + * The path containing the icons. + */ + private static final String ICON_PATH = "/org/opentcs/guing/res/symbols/layer/"; + /** + * The resource bundle to use. + */ + private static final ResourceBundle BUNDLE + = ResourceBundle.getBundle(I18nPlantOverviewModeling.LAYERS_PATH); + /** + * The model manager. + */ + private final ModelManager modelManager; + /** + * The layer manager. + */ + private final LayerManager layerManager; + /** + * The layer group manager. + */ + private final LayerGroupManager layerGroupManager; + /** + * The layer editor. + */ + private final LayerEditorModeling layerEditor; + /** + * Provides the currently active layer. + */ + private final ActiveLayerProvider activeLayerProvider; + /** + * The table to display available layers. + */ + private JTable table; + /** + * The table model. + */ + private LayersTableModel tableModel; + + @Inject + @SuppressWarnings("this-escape") + public LayersPanel( + ModelManager modelManager, + LayerManager layerManager, + LayerGroupManager layerGroupManager, + LayerEditorModeling layerEditor, + ActiveLayerProvider activeLayerProvider + ) { + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.layerManager = requireNonNull(layerManager, "layerManager"); + this.layerGroupManager = requireNonNull(layerGroupManager, "layerGroupManager"); + this.layerEditor = requireNonNull(layerEditor, "layerEditor"); + this.activeLayerProvider = requireNonNull(activeLayerProvider, "activeLayerProvider"); + + initComponents(); + } + + private void initComponents() { + setLayout(new BorderLayout()); + + tableModel = new LayersTableModel(modelManager, activeLayerProvider, layerEditor); + layerManager.setLayerChangeListener(tableModel); + layerGroupManager.addLayerGroupChangeListener(tableModel); + table = new JTable(tableModel); + initTable(); + + add(createToolBar(), BorderLayout.NORTH); + add(new JScrollPane(table), BorderLayout.CENTER); + } + + private void initTable() { + table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + TableRowSorter sorter = new TableRowSorter<>(tableModel); + // Sort the table by the layer ordinals... + sorter.setSortKeys( + Arrays.asList( + new RowSorter.SortKey(LayersTableModel.COLUMN_ORDINAL, SortOrder.DESCENDING) + ) + ); + // ...but prevent manual sorting. + for (int i = 0; i < table.getColumnCount(); i++) { + sorter.setSortable(i, false); + } + sorter.setSortsOnUpdates(true); + table.setRowSorter(sorter); + + // Hide the column that shows the layer ordinals. + table.removeColumn( + table.getColumnModel() + .getColumn(table.convertColumnIndexToView(LayersTableModel.COLUMN_ORDINAL)) + ); + + table.getColumnModel() + .getColumn(table.convertColumnIndexToView(LayersTableModel.COLUMN_GROUP_VISIBLE)) + .setCellRenderer(new DisabledCheckBoxCellRenderer()); + + initActiveColumn(); + initGroupColumn(); + } + + private JToolBar createToolBar() { + JToolBar toolBar = new JToolBar(); + toolBar.setFloatable(false); + + toolBar.add(createAddLayerButton()); + toolBar.add(createRemoveLayerButton()); + toolBar.add(createMoveLayerUpButton()); + toolBar.add(createMoveLayerDownButton()); + + return toolBar; + } + + private void initActiveColumn() { + // Since our concept of layers allows only one active layer at a time, a representation of that + // state as radio buttons seems to have the potential to be more intuitive for users. + // Radio buttons usually come with/in a button group which ensures that only one of the radio + // buttons in that group can be selected at a time. In a table, though, such a button group + // doesn't seem to work very well if it's used to group radio buttons across multiple rows. + // For that reason the behavior of radio buttons in a button group is "emulated" in/trough + // the LayerManager implementation. + TableColumn columnActive = table.getColumnModel() + .getColumn(table.convertColumnIndexToView(LayersTableModel.COLUMN_ACTIVE)); + columnActive.setCellEditor(new RadioButtonCellEditor()); + columnActive.setCellRenderer(new RadioButtonCellRenderer()); + } + + private void initGroupColumn() { + TableColumn columnGroup = table.getColumnModel() + .getColumn(table.convertColumnIndexToView(LayersTableModel.COLUMN_GROUP)); + columnGroup.setCellRenderer(new LayerGroupCellRenderer()); + columnGroup.setCellEditor(new GroupCellEditor()); + } + + private JButton createAddLayerButton() { + IconToolkit iconkit = IconToolkit.instance(); + JButton button = new JButton(iconkit.getImageIconByFullPath(ICON_PATH + "create-layer.16.png")); + button.addActionListener(actionEvent -> { + layerEditor.createLayer(); + table.getSelectionModel().setSelectionInterval(0, 0); + }); + button.setToolTipText(BUNDLE.getString("layersPanel.button_addLayer.tooltipText")); + + return button; + } + + private JButton createRemoveLayerButton() { + IconToolkit iconkit = IconToolkit.instance(); + JButton button = new JButton(iconkit.getImageIconByFullPath(ICON_PATH + "delete-layer.16.png")); + button.addActionListener(new RemoveLayerListener()); + button.setToolTipText(BUNDLE.getString("layersPanel.button_removeLayer.tooltipText")); + + // Allow the remove layer button to be pressed only if there's a layer selected and if there's + // more than one layer in the model. + table.getSelectionModel().addListSelectionListener(listSelectionEvent -> { + button.setEnabled(table.getSelectedRow() != -1 && tableModel.getRowCount() > 1); + }); + tableModel.addTableModelListener(tableModelEvent -> { + button.setEnabled(table.getSelectedRow() != -1 && tableModel.getRowCount() > 1); + }); + + return button; + } + + private JButton createMoveLayerUpButton() { + IconToolkit iconkit = IconToolkit.instance(); + JButton button + = new JButton(iconkit.getImageIconByFullPath(ICON_PATH + "move-layer-up.16.png")); + button.setEnabled(false); + button.addActionListener(actionEvent -> { + int selectedRow = table.getSelectedRow(); + int selectedLayerId = tableModel.getDataAt(table.convertRowIndexToModel(selectedRow)).getId(); + layerEditor.moveLayerUp(selectedLayerId); + }); + button.setToolTipText(BUNDLE.getString("layersPanel.button_moveLayerUp.tooltipText")); + + // Allow the button to be pressed only if there's a layer selected. + table.getSelectionModel().addListSelectionListener(listSelectionEvent -> { + button.setEnabled(table.getSelectedRow() != -1); + }); + + return button; + } + + private JButton createMoveLayerDownButton() { + IconToolkit iconkit = IconToolkit.instance(); + JButton button + = new JButton(iconkit.getImageIconByFullPath(ICON_PATH + "move-layer-down.16.png")); + button.setEnabled(false); + button.addActionListener(actionEvent -> { + int selectedRow = table.getSelectedRow(); + int selectedLayerId = tableModel.getDataAt(table.convertRowIndexToModel(selectedRow)).getId(); + layerEditor.moveLayerDown(selectedLayerId); + }); + button.setToolTipText(BUNDLE.getString("layersPanel.button_moveLayerDown.tooltipText")); + + // Allow the button to be pressed only if there's a layer selected. + table.getSelectionModel().addListSelectionListener(listSelectionEvent -> { + button.setEnabled(table.getSelectedRow() != -1); + }); + + return button; + } + + private class RemoveLayerListener + implements + ActionListener { + + /** + * Creates a new instance. + */ + RemoveLayerListener() { + } + + @Override + public void actionPerformed(ActionEvent e) { + int selectedRow = table.getSelectedRow(); + int selectedLayerId = tableModel.getDataAt(table.convertRowIndexToModel(selectedRow)).getId(); + + if (layerManager.containsComponents(selectedLayerId)) { + int selectedOption = JOptionPane.showConfirmDialog( + LayersPanel.this, + BUNDLE.getString("layersPanel.optionPane_confirmLayerWithComponentsRemoval.message"), + BUNDLE.getString("layersPanel.optionPane_confirmLayerWithComponentsRemoval.title"), + JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE + ); + if (selectedOption == JOptionPane.NO_OPTION) { + return; + } + } + + layerEditor.deleteLayer(selectedLayerId); + } + } + + private class RadioButtonCellRenderer + implements + TableCellRenderer { + + private final Border unfocusedCellBorder = BorderFactory.createEmptyBorder(); + private final Border focusedCellBorder = UIManager.getBorder("Table.focusCellHighlightBorder"); + private final JRadioButton radioButton; + + RadioButtonCellRenderer() { + radioButton = new JRadioButton(); + radioButton.setHorizontalAlignment(JRadioButton.CENTER); + radioButton.setBorderPainted(true); + } + + @Override + public Component getTableCellRendererComponent( + JTable table, Object value, boolean isSelected, + boolean hasFocus, int row, int col + ) { + radioButton.setBackground( + isSelected ? table.getSelectionBackground() : table.getBackground() + ); + radioButton.setForeground( + isSelected ? table.getSelectionForeground() : table.getForeground() + ); + radioButton.setBorder(hasFocus ? focusedCellBorder : unfocusedCellBorder); + radioButton.setSelected(Boolean.TRUE.equals(value)); + + return radioButton; + } + } + + private class RadioButtonCellEditor + extends + AbstractCellEditor + implements + TableCellEditor { + + private final JRadioButton radioButton; + + RadioButtonCellEditor() { + radioButton = new JRadioButton(); + radioButton.setHorizontalAlignment(JRadioButton.CENTER); + radioButton.addActionListener(actionEvent -> stopCellEditing()); + } + + @Override + public Component getTableCellEditorComponent( + JTable table, Object value, + boolean isSelected, int row, int col + ) { + radioButton.setSelected(Boolean.TRUE.equals(value)); + return radioButton; + } + + @Override + public Object getCellEditorValue() { + return radioButton.isSelected(); + } + } + + private class GroupCellEditor + extends + DefaultCellEditor { + + private final DefaultComboBoxModel model; + + GroupCellEditor() { + super(new JComboBox()); + @SuppressWarnings("unchecked") + JComboBox combobox = (JComboBox) getComponent(); + combobox.setRenderer(new StringListCellRenderer<>(group -> group.getName())); + this.model = (DefaultComboBoxModel) combobox.getModel(); + } + + @Override + public Component getTableCellEditorComponent( + JTable table, Object value, + boolean isSelected, int row, int column + ) { + model.removeAllElements(); + List groups = modelManager.getModel().getLayoutModel().getPropertyLayerGroups() + .getValue().values().stream() + .sorted((o1, o2) -> Integer.compare(o1.getId(), o2.getId())) + .collect(Collectors.toList()); + model.addAll(groups); + model.setSelectedItem(table.getModel().getValueAt(row, column)); + + return super.getTableCellEditorComponent(table, value, isSelected, row, column); + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayersTableModel.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayersTableModel.java new file mode 100644 index 0000000..838c19f --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/layer/LayersTableModel.java @@ -0,0 +1,311 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.layer; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.stream.Collectors; +import javax.swing.SwingUtilities; +import javax.swing.table.AbstractTableModel; +import org.opentcs.data.model.visualization.Layer; +import org.opentcs.data.model.visualization.LayerGroup; +import org.opentcs.guing.base.components.layer.LayerWrapper; +import org.opentcs.guing.common.components.layer.LayerChangeListener; +import org.opentcs.guing.common.components.layer.LayerGroupChangeListener; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; + +/** + * A table model for layers. + */ +class LayersTableModel + extends + AbstractTableModel + implements + LayerChangeListener, + LayerGroupChangeListener { + + /** + * The number of the "Active" column. + */ + public static final int COLUMN_ACTIVE = 0; + /** + * The number of the "Ordinal" column. + */ + public static final int COLUMN_ORDINAL = 1; + /** + * The number of the "Visible" column. + */ + public static final int COLUMN_VISIBLE = 2; + /** + * The number of the "Name" column. + */ + public static final int COLUMN_NAME = 3; + /** + * The number of the "Group" column. + */ + public static final int COLUMN_GROUP = 4; + /** + * The number of the "Group visible" column. + */ + public static final int COLUMN_GROUP_VISIBLE = 5; + /** + * The resource bundle to use. + */ + private static final ResourceBundle BUNDLE + = ResourceBundle.getBundle(I18nPlantOverviewModeling.LAYERS_PATH); + /** + * The column names. + */ + private static final String[] COLUMN_NAMES + = new String[]{ + BUNDLE.getString( + "layersTableModel.column_active.headerText" + ), + BUNDLE.getString( + "layersTableModel.column_ordinal.headerText" + ), + BUNDLE.getString( + "layersTableModel.column_visible.headerText" + ), + BUNDLE.getString( + "layersTableModel.column_name.headerText" + ), + BUNDLE.getString( + "layersTableModel.column_group.headerText" + ), + BUNDLE.getString( + "layersTableModel.column_groupVisible.headerText" + ) + }; + /** + * The column classes. + */ + private static final Class[] COLUMN_CLASSES + = new Class[]{ + Boolean.class, + Integer.class, + Boolean.class, + String.class, + LayerGroup.class, + Boolean.class + }; + /** + * The model manager. + */ + private final ModelManager modelManager; + /** + * Provides the currently active layer. + */ + private final ActiveLayerProvider activeLayerProvider; + /** + * The layer editor. + */ + private final LayerEditorModeling layerEditor; + + /** + * Creates a new instance. + * + * @param modelManager The model manager. + * @param activeLayerProvider Provides the currently active layer. + * @param layerEditor The layer editor. + */ + LayersTableModel( + ModelManager modelManager, + ActiveLayerProvider activeLayerProvider, + LayerEditorModeling layerEditor + ) { + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.activeLayerProvider = requireNonNull(activeLayerProvider, "activeLayerProvider"); + this.layerEditor = requireNonNull(layerEditor, "layerEditor"); + } + + @Override + public int getRowCount() { + return getLayers().size(); + } + + @Override + public int getColumnCount() { + return COLUMN_NAMES.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= getRowCount()) { + return null; + } + + Layer entry = getLayers().get(rowIndex); + switch (columnIndex) { + case COLUMN_ACTIVE: + return entry.getId() == activeLayerProvider.getActiveLayer().getLayer().getId(); + case COLUMN_ORDINAL: + return entry.getOrdinal(); + case COLUMN_VISIBLE: + return entry.isVisible(); + case COLUMN_NAME: + return entry.getName(); + case COLUMN_GROUP: + return getLayerGroups().get(entry.getGroupId()); + case COLUMN_GROUP_VISIBLE: + return getLayerGroups().get(entry.getGroupId()).isVisible(); + default: + throw new IllegalArgumentException("Invalid column index: " + columnIndex); + } + } + + @Override + public String getColumnName(int columnIndex) { + return COLUMN_NAMES[columnIndex]; + } + + @Override + public Class getColumnClass(int columnIndex) { + return COLUMN_CLASSES[columnIndex]; + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + switch (columnIndex) { + case COLUMN_ACTIVE: + return true; + case COLUMN_ORDINAL: + return false; + case COLUMN_VISIBLE: + return true; + case COLUMN_NAME: + return true; + case COLUMN_GROUP: + return true; + case COLUMN_GROUP_VISIBLE: + return false; + default: + throw new IllegalArgumentException("Invalid column index: " + columnIndex); + } + } + + @Override + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= getRowCount()) { + return; + } + + if (aValue == null) { + return; + } + + Layer entry = getLayers().get(rowIndex); + switch (columnIndex) { + case COLUMN_ACTIVE: + layerEditor.setLayerActive(entry.getId()); + break; + case COLUMN_ORDINAL: + // Do nothing. + break; + case COLUMN_VISIBLE: + layerEditor.setLayerVisible(entry.getId(), (boolean) aValue); + break; + case COLUMN_NAME: + layerEditor.setLayerName(entry.getId(), aValue.toString()); + break; + case COLUMN_GROUP: + layerEditor.setLayerGroupId(entry.getId(), ((LayerGroup) aValue).getId()); + break; + case COLUMN_GROUP_VISIBLE: + // Do nothing. + break; + default: + throw new IllegalArgumentException("Invalid column index: " + columnIndex); + } + } + + @Override + public void layersInitialized() { + // Once the layers are initialized we want to redraw the entire table to avoid any + // display errors. + executeOnEventDispatcherThread(() -> fireTableDataChanged()); + } + + @Override + public void layersChanged() { + // Update the entire table but don't use fireTableDataChanged() to preserve the current + // selection. + executeOnEventDispatcherThread(() -> fireTableRowsUpdated(0, getRowCount() - 1)); + } + + @Override + public void layerAdded() { + // Layers are always added to the top (with regard to sorting). + executeOnEventDispatcherThread(() -> fireTableRowsInserted(0, 0)); + } + + @Override + public void layerRemoved() { + // At this point, there's no way for us to determine the row the removed layer was in. The + // entry has already been remove from this table model's data source which is provided by + // layersByOrdinal(). + // Workaround: Since the table now contains one entry less, pretend that the last entry was + // deleted. + executeOnEventDispatcherThread(() -> fireTableRowsDeleted(getRowCount(), getRowCount())); + } + + @Override + public void groupsInitialized() { + } + + @Override + public void groupsChanged() { + // The visibility of a group, which we display as well, may have changed. Update the table. + executeOnEventDispatcherThread(() -> fireTableRowsUpdated(0, getRowCount() - 1)); + } + + @Override + public void groupAdded() { + } + + @Override + public void groupRemoved() { + } + + public Layer getDataAt(int index) { + return getLayers().get(index); + } + + private List getLayers() { + return getLayerWrappers().values().stream() + .map(wrapper -> wrapper.getLayer()) + .collect(Collectors.toList()); + } + + private Map getLayerWrappers() { + return modelManager.getModel().getLayoutModel().getPropertyLayerWrappers().getValue(); + } + + private Map getLayerGroups() { + return modelManager.getModel().getLayoutModel().getPropertyLayerGroups().getValue(); + } + + /** + * Ensures the given runnable is executed on the EDT. + * If the runnable is already being called on the EDT, the runnable is executed immediately. + * Otherwise it is scheduled for execution on the EDT. + *

+ * Note: Deferring a runnable by scheduling it for execution on the EDT even though it would + * have already been executed on the EDT may lead to exceptions due to data inconsistency. + *

+ * + * @param runnable The runnable. + */ + private void executeOnEventDispatcherThread(Runnable runnable) { + if (SwingUtilities.isEventDispatchThread()) { + runnable.run(); + } + else { + SwingUtilities.invokeLater(runnable); + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/tree/elements/VehicleUserObjectModeling.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/tree/elements/VehicleUserObjectModeling.java new file mode 100644 index 0000000..9ebd619 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/components/tree/elements/VehicleUserObjectModeling.java @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.components.tree.elements; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import javax.swing.JPopupMenu; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.components.tree.elements.VehicleUserObject; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * A Vehicle object in the tree view. + */ +public class VehicleUserObjectModeling + extends + VehicleUserObject { + + /** + * Creates a new instance. + * + * @param model The corresponding vehicle object. + * @param guiManager The gui manager. + * @param modelManager Provides the current system model. + */ + @Inject + public VehicleUserObjectModeling( + @Assisted + VehicleModel model, + GuiManager guiManager, + ModelManager modelManager + ) { + super(model, guiManager, modelManager); + } + + @Override // AbstractUserObject + public JPopupMenu getPopupMenu() { + return null; + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/BezierLength.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/BezierLength.java new file mode 100644 index 0000000..eb3e35a --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/BezierLength.java @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.math.path; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * Calculates the length of {@link PathModel.Type#BEZIER} paths. + */ +public class BezierLength + implements + PathLengthFunction { + + private final double scaleX; + private final double scaleY; + private final PathLengthMath pathLengthMath; + + /** + * Creates a new instance. + * + * @param manager Provides access to the current system model. + * @param pathLengthMath Provides methods to evaluate the position of a point of a Bezier curve. + */ + @Inject + public BezierLength( + @Nonnull + ModelManager manager, + @Nonnull + PathLengthMath pathLengthMath + ) { + requireNonNull(manager, "manager"); + this.pathLengthMath = requireNonNull(pathLengthMath, "pathLengthMath"); + scaleX = manager.getModel().getLayoutModel().getPropertyScaleX() + .getValueByUnit(LengthProperty.Unit.MM); + scaleY = manager.getModel().getLayoutModel().getPropertyScaleY() + .getValueByUnit(LengthProperty.Unit.MM); + } + + @Override + public double applyAsDouble( + @Nonnull + PathModel path + ) { + requireNonNull(path, "path"); + + String[] cps = path.getPropertyPathControlPoints().getText().split(";"); + checkArgument( + cps.length == 2, + String.format("Path '%s' does not have exactly two control points.", path.getName()) + ); + + PointModel start = (PointModel) path.getStartComponent(); + PointModel end = (PointModel) path.getEndComponent(); + + return pathLengthMath.approximateCubicBezierCurveLength( + new Coordinate( + start.getPropertyModelPositionX().getValueByUnit(LengthProperty.Unit.MM), + start.getPropertyModelPositionY().getValueByUnit(LengthProperty.Unit.MM) + ), + new Coordinate( + Double.parseDouble(cps[0].split(",")[0]) * scaleX, + Double.parseDouble(cps[0].split(",")[1]) * scaleY * (-1) + ), + new Coordinate( + Double.parseDouble(cps[1].split(",")[0]) * scaleX, + Double.parseDouble(cps[1].split(",")[1]) * scaleY * (-1) + ), + new Coordinate( + end.getPropertyModelPositionX().getValueByUnit(LengthProperty.Unit.MM), + end.getPropertyModelPositionY().getValueByUnit(LengthProperty.Unit.MM) + ), + 1000 + ); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/BezierThreeLength.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/BezierThreeLength.java new file mode 100644 index 0000000..226691a --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/BezierThreeLength.java @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.math.path; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * Calculates the length of {@link PathModel.Type#BEZIER_3} paths. + */ +public class BezierThreeLength + implements + PathLengthFunction { + + private final double scaleX; + private final double scaleY; + private final PathLengthMath pathLengthMath; + + /** + * Creates a new instance. + * + * @param manager Provides access to the current system model. + * @param pathLengthMath Provides methods to evaluate the position of a point of a Bezier curve. + */ + @Inject + public BezierThreeLength( + @Nonnull + ModelManager manager, + @Nonnull + PathLengthMath pathLengthMath + ) { + requireNonNull(manager, "manager"); + this.pathLengthMath = requireNonNull(pathLengthMath, "pathLengthMath"); + scaleX = manager.getModel().getLayoutModel().getPropertyScaleX() + .getValueByUnit(LengthProperty.Unit.MM); + scaleY = manager.getModel().getLayoutModel().getPropertyScaleY() + .getValueByUnit(LengthProperty.Unit.MM); + } + + @Override + public double applyAsDouble( + @Nonnull + PathModel path + ) { + requireNonNull(path, "path"); + + String[] cps = path.getPropertyPathControlPoints().getText().split(";"); + checkArgument( + cps.length == 5, + String.format("Path '%s' does not have exactly five control points.", path.getName()) + ); + + PointModel start = (PointModel) path.getStartComponent(); + PointModel end = (PointModel) path.getEndComponent(); + + return calculateBezierCurveLength( + new Coordinate( + start.getPropertyModelPositionX().getValueByUnit(LengthProperty.Unit.MM), + start.getPropertyModelPositionY().getValueByUnit(LengthProperty.Unit.MM) + ), + new Coordinate( + Double.parseDouble(cps[0].split(",")[0]) * scaleX, + Double.parseDouble(cps[0].split(",")[1]) * scaleY * (-1) + ), + new Coordinate( + Double.parseDouble(cps[1].split(",")[0]) * scaleX, + Double.parseDouble(cps[1].split(",")[1]) * scaleY * (-1) + ), + new Coordinate( + Double.parseDouble(cps[2].split(",")[0]) * scaleX, + Double.parseDouble(cps[2].split(",")[1]) * scaleY * (-1) + ), + new Coordinate( + Double.parseDouble(cps[3].split(",")[0]) * scaleX, + Double.parseDouble(cps[3].split(",")[1]) * scaleY * (-1) + ), + new Coordinate( + Double.parseDouble(cps[4].split(",")[0]) * scaleX, + Double.parseDouble(cps[4].split(",")[1]) * scaleY * (-1) + ), + new Coordinate( + end.getPropertyModelPositionX().getValueByUnit(LengthProperty.Unit.MM), + end.getPropertyModelPositionY().getValueByUnit(LengthProperty.Unit.MM) + ) + ); + } + + private double calculateBezierCurveLength( + Coordinate start, Coordinate cp0, Coordinate cp1, + Coordinate cp2, Coordinate cp3, Coordinate cp4, + Coordinate end + ) { + return pathLengthMath.approximateCubicBezierCurveLength(start, cp0, cp1, cp2, 1000) + + pathLengthMath.approximateCubicBezierCurveLength(cp2, cp3, cp4, end, 1000); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/CompositePathLengthFunction.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/CompositePathLengthFunction.java new file mode 100644 index 0000000..544a071 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/CompositePathLengthFunction.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.math.path; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.Map; +import org.opentcs.guing.base.model.elements.PathModel; + +/** + * A composite of various {@link PathLengthFunction}s for different {@link PathModel.Type}s which + * falls back to {@link EuclideanDistance}. + */ +public class CompositePathLengthFunction + implements + PathLengthFunction { + + private final Map pathLengthFunctions; + private final EuclideanDistance euclideanDistance; + + @Inject + public CompositePathLengthFunction( + @Nonnull + Map pathLengthFunctions, + @Nonnull + EuclideanDistance euclideanDistance + ) { + this.pathLengthFunctions = requireNonNull(pathLengthFunctions, "pathLengthFunctions"); + this.euclideanDistance = requireNonNull(euclideanDistance, "euclideanDistance"); + } + + @Override + public double applyAsDouble(PathModel path) { + return pathLengthFunctions + .getOrDefault((PathModel.Type) path.getPropertyPathConnType().getValue(), euclideanDistance) + .applyAsDouble(path); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/Coordinate.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/Coordinate.java new file mode 100644 index 0000000..0331de3 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/Coordinate.java @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.math.path; + +/** + * A generic 2-tuple of double values, usable for 2D coordinates and vectors, for instance. + */ +public class Coordinate { + + /** + * The X coordinate. + */ + private final double x; + /** + * The Y coordinate. + */ + private final double y; + + /** + * Creates a new instance. + * + * @param x The X coordinate. + * @param y The Y coordinate. + */ + public Coordinate(double x, double y) { + this.x = x; + this.y = y; + } + + /** + * Returns the x coordinate. + * + * @return x + */ + public double getX() { + return x; + } + + /** + * Returns the y coordinate. + * + * @return y + */ + public double getY() { + return y; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Coordinate)) { + return false; + } + Coordinate other = (Coordinate) obj; + if (this.x != other.x) { + return false; + } + if (this.y != other.y) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return (int) (x * y); + } + + @Override + public String toString() { + return "Coordinate{" + "x=" + x + ", y=" + y + '}'; + } + +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/EuclideanDistance.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/EuclideanDistance.java new file mode 100644 index 0000000..478e76e --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/EuclideanDistance.java @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.math.path; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; + +/** + * Calculates the length of a path as the Euclidean distance between the start and end point. + */ +public class EuclideanDistance + implements + PathLengthFunction { + + private final PathLengthMath pathLengthMath; + + /** + * Creates a new instance. + * + * @param pathLengthMath Provides a method for the euclidean distance. + */ + @Inject + public EuclideanDistance( + @Nonnull + PathLengthMath pathLengthMath + ) { + this.pathLengthMath = requireNonNull(pathLengthMath, "pathLengthMath"); + } + + @Override + public double applyAsDouble( + @Nonnull + PathModel path + ) { + requireNonNull(path, "path"); + + PointModel start = (PointModel) path.getStartComponent(); + PointModel end = (PointModel) path.getEndComponent(); + + Coordinate x = new Coordinate( + start.getPropertyModelPositionX().getValueByUnit(LengthProperty.Unit.MM), + start.getPropertyModelPositionY().getValueByUnit(LengthProperty.Unit.MM) + ); + Coordinate y = new Coordinate( + end.getPropertyModelPositionX().getValueByUnit(LengthProperty.Unit.MM), + end.getPropertyModelPositionY().getValueByUnit(LengthProperty.Unit.MM) + ); + + return pathLengthMath.euclideanDistance(x, y); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/PathLengthFunction.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/PathLengthFunction.java new file mode 100644 index 0000000..1954982 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/PathLengthFunction.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.math.path; + +import java.util.function.ToDoubleFunction; +import org.opentcs.guing.base.model.elements.PathModel; + +/** + * A function that computes the length of a path. + */ +public interface PathLengthFunction + extends + ToDoubleFunction { +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/PathLengthMath.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/PathLengthMath.java new file mode 100644 index 0000000..f8b1c25 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/PathLengthMath.java @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.math.path; + +/** + * Provides the euclidean distance and methods to evaluate the position of a point of a Bezier curve + * with n control points at a given parameter value. + */ +public class PathLengthMath { + + public PathLengthMath() { + } + + /** + * Approximates the length of a cubic Bezier curve (described via its control points) by + * discretization of the curve. + * + * @param cp0 The control point with index 0 (i.e. the start point of the Bezier curve). + * @param cp1 The control point with index 1. + * @param cp2 The control point with index 2. + * @param cp3 The control point with index 3 (i.e. the end point of the Bezier curve). + * @param granularity The granularity of the discretized curve. A higher granularity results in a + * more precise approximation. + * + * @return The approximated length of the Bezier curve. + */ + public double approximateCubicBezierCurveLength( + Coordinate cp0, + Coordinate cp1, + Coordinate cp2, + Coordinate cp3, + double granularity + ) { + double length = 0.0; + + for (int i = 0; i < granularity; i++) { + length += euclideanDistance( + evaluatePointOnCubicBezierCurve((double) i / granularity, cp0, cp1, cp2, cp3), + evaluatePointOnCubicBezierCurve((double) (i + 1) / granularity, cp0, cp1, cp2, cp3) + ); + } + + return length; + } + + /** + * Calculates a point's position on a cubic Bezier curve (described via its control points) using + * Bernstein basis polynomials. + * + * @param t The parameter (with a value between 0 and 1) that defines the position of a point + * along the Bezier curve. A value of 0 corresponds to the position of the first control point and + * a value of 1 to the position of the last control point. + * @param cp0 The control point with index 0 (i.e. the start point of the Bezier curve). + * @param cp1 The control point with index 1. + * @param cp2 The control point with index 2. + * @param cp3 The control point with index 3 (i.e. the end point of the Bezier curve). + * @return A point's position on a cubic Bezier curve. + */ + public Coordinate evaluatePointOnCubicBezierCurve( + double t, + Coordinate cp0, + Coordinate cp1, + Coordinate cp2, + Coordinate cp3 + ) { + return new Coordinate( + bernsteinPolynomialOfDegree3(t, cp0.getX(), cp1.getX(), cp2.getX(), cp3.getX()), + bernsteinPolynomialOfDegree3(t, cp0.getY(), cp1.getY(), cp2.getY(), cp3.getY()) + ); + } + + /** + * Calculates the distance between two given points using the euclidean algorithm. + * + * @param p1 The first point. + * @param p2 The second point. + * @return The distance between these two points. + */ + public double euclideanDistance(Coordinate p1, Coordinate p2) { + return Math.sqrt(Math.pow(p1.getX() - p2.getX(), 2) + Math.pow(p1.getY() - p2.getY(), 2)); + } + + private double bernsteinPolynomialOfDegree3( + double t, + double cp0, + double cp1, + double cp2, + double cp3 + ) { + double u = 1.0 - t; + return Math.pow(u, 3) * cp0 + + 3 * Math.pow(u, 2) * t * cp1 + + 3 * u * Math.pow(t, 2) * cp2 + + Math.pow(t, 3) * cp3; + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/PolyPathLength.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/PolyPathLength.java new file mode 100644 index 0000000..dfaec77 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/math/path/PolyPathLength.java @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.math.path; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * Calculates the length of {@link PathModel.Type#POLYPATH} paths. + */ +public class PolyPathLength + implements + PathLengthFunction { + + private final double scaleX; + private final double scaleY; + private final PathLengthMath pathLengthMath; + + /** + * Creates a new instance. + * + * @param manager Provides access to the current system model. + * @param pathLengthMath Provides euclidean distance method. + */ + @Inject + public PolyPathLength( + @Nonnull + ModelManager manager, + @Nonnull + PathLengthMath pathLengthMath + ) { + requireNonNull(manager, "manager"); + this.pathLengthMath = requireNonNull(pathLengthMath, "pathLengthMath"); + scaleX = manager.getModel().getLayoutModel().getPropertyScaleX() + .getValueByUnit(LengthProperty.Unit.MM); + scaleY = manager.getModel().getLayoutModel().getPropertyScaleY() + .getValueByUnit(LengthProperty.Unit.MM); + } + + @Override + public double applyAsDouble(PathModel path) { + requireNonNull(path, "path"); + + String[] cps = path.getPropertyPathControlPoints().getText().split(";"); + checkArgument( + cps.length >= 1, + String.format("Path '%s' does not have at least one control point.", path.getName()) + ); + + Coordinate[] controlCoordinates = new Coordinate[cps.length + 2]; + controlCoordinates[0] = new Coordinate( + ((PointModel) path.getStartComponent()).getPropertyModelPositionX() + .getValueByUnit(LengthProperty.Unit.MM), + ((PointModel) path.getStartComponent()).getPropertyModelPositionY() + .getValueByUnit(LengthProperty.Unit.MM) + ); + + for (int i = 0; i < cps.length; i++) { + String couple = cps[i]; + double x = Double.parseDouble(couple.split(",")[0]) * scaleX; + double y = Double.parseDouble(couple.split(",")[1]) * scaleY * (-1); + controlCoordinates[i + 1] = new Coordinate(x, y); + } + + controlCoordinates[cps.length + 1] = new Coordinate( + ((PointModel) path.getEndComponent()).getPropertyModelPositionX() + .getValueByUnit(LengthProperty.Unit.MM), + ((PointModel) path.getEndComponent()).getPropertyModelPositionY() + .getValueByUnit(LengthProperty.Unit.MM) + ); + + return calculatePolyPathDistance(controlCoordinates); + } + + private double calculatePolyPathDistance(Coordinate[] controlCoordinates) { + double length = 0; + + for (int i = 0; i < controlCoordinates.length - 1; i++) { + length += pathLengthMath.euclideanDistance(controlCoordinates[i], controlCoordinates[i + 1]); + } + + return length; + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/ModelImportAdapter.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/ModelImportAdapter.java new file mode 100644 index 0000000..ba5cb26 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/ModelImportAdapter.java @@ -0,0 +1,285 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.persistence; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.opentcs.access.to.model.BlockCreationTO; +import org.opentcs.access.to.model.LocationCreationTO; +import org.opentcs.access.to.model.LocationTypeCreationTO; +import org.opentcs.access.to.model.PathCreationTO; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.access.to.model.PointCreationTO; +import org.opentcs.access.to.model.VehicleCreationTO; +import org.opentcs.access.to.model.VisualLayoutCreationTO; +import org.opentcs.guing.base.components.properties.type.KeyValueProperty; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.application.StatusPanel; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.modeleditor.persistence.unified.PlantModelElementConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Converts plant model data to {@link SystemModel} instances. + */ +public class ModelImportAdapter { + + /** + * This class' logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ModelImportAdapter.class); + + private final Provider systemModelProvider; + /** + * Converts single elements of a plant model. + */ + private final PlantModelElementConverter elementConverter; + /** + * Validates model components and the system model. + */ + private final ModelValidator validator; + /** + * The status panel of the plant overview. + */ + private final StatusPanel statusPanel; + + @Inject + public ModelImportAdapter( + Provider systemModelProvider, + PlantModelElementConverter elementConverter, + ModelValidator validator, + StatusPanel statusPanel + ) { + this.systemModelProvider = requireNonNull(systemModelProvider, "systemModelProvider"); + this.elementConverter = requireNonNull(elementConverter, "elementConverter"); + this.validator = requireNonNull(validator, "validator"); + this.statusPanel = requireNonNull(statusPanel, "statusPanel"); + } + + /** + * Converts the given plant model data to a {@link SystemModel} instance. + * + * @param model The plant model data to be converted. + * @return The converted model. + * @throws IllegalArgumentException If the given plant model data was inconsistent in some way. + */ + @Nonnull + public SystemModel convert(PlantModelCreationTO model) + throws IllegalArgumentException { + requireNonNull(model, "model"); + + SystemModel systemModel = systemModelProvider.get(); + systemModel.setName(model.getName()); + + VisualLayoutCreationTO layoutTO = model.getVisualLayout(); + + Set collectedErrorMessages = new HashSet<>(); + + importVisualLayout(layoutTO, systemModel, collectedErrorMessages); + importPoints(model, systemModel, collectedErrorMessages); + importPaths(model, systemModel, collectedErrorMessages); + importVehicles(model, systemModel, collectedErrorMessages); + importLocationTypes(model, systemModel, collectedErrorMessages); + importLocations(model, systemModel, collectedErrorMessages); + importBlocks(model, systemModel, collectedErrorMessages); + + importProperties(model, systemModel); + + // If any errors occurred, show the dialog with all errors listed + if (!collectedErrorMessages.isEmpty()) { + validator.showLoadingValidationWarning(statusPanel, collectedErrorMessages); + } + + return systemModel; + } + + private void importProperties(PlantModelCreationTO model, SystemModel systemModel) { + for (Map.Entry property : model.getProperties().entrySet()) { + systemModel.getPropertyMiscellaneous().addItem( + new KeyValueProperty( + systemModel, + property.getKey(), + property.getValue() + ) + ); + } + } + + private void importVisualLayout( + VisualLayoutCreationTO layoutTO, SystemModel systemModel, + Set collectedErrorMessages + ) { + LayoutModel layoutModel = elementConverter.importLayout(layoutTO); + if (validModelComponent(layoutModel, systemModel, collectedErrorMessages)) { + updateLayoutInModel(layoutModel, systemModel); + } + } + + private void importBlocks( + PlantModelCreationTO model, + SystemModel systemModel, + Set collectedErrorMessages + ) { + for (BlockCreationTO blockTO : model.getBlocks()) { + BlockModel blockModel = elementConverter.importBlock(blockTO); + if (validModelComponent(blockModel, systemModel, collectedErrorMessages)) { + addBlockToModel(blockModel, systemModel); + } + } + } + + private void importLocations( + PlantModelCreationTO model, + SystemModel systemModel, + Set collectedErrorMessages + ) { + for (LocationCreationTO locationTO : model.getLocations()) { + LocationModel locationModel = elementConverter.importLocation( + locationTO, + model.getLocationTypes(), + systemModel + ); + if (validModelComponent(locationModel, systemModel, collectedErrorMessages)) { + addLocationToModel(locationModel, systemModel); + + for (Map.Entry> entry : locationTO.getLinks().entrySet()) { + LinkModel linkModel = elementConverter.importLocationLink( + locationTO, + entry.getKey(), + entry.getValue(), + systemModel + ); + if (validModelComponent(linkModel, systemModel, collectedErrorMessages)) { + addLinkToModel(linkModel, systemModel); + } + } + } + } + } + + private void importLocationTypes( + PlantModelCreationTO model, + SystemModel systemModel, + Set collectedErrorMessages + ) { + for (LocationTypeCreationTO locTypeTO : model.getLocationTypes()) { + LocationTypeModel locTypeModel = elementConverter.importLocationType(locTypeTO); + if (validModelComponent(locTypeModel, systemModel, collectedErrorMessages)) { + addLocationTypeToModel(locTypeModel, systemModel); + } + } + } + + private void importVehicles( + PlantModelCreationTO model, + SystemModel systemModel, + Set collectedErrorMessages + ) { + for (VehicleCreationTO vehicleTO : model.getVehicles()) { + VehicleModel vehicleModel = elementConverter.importVehicle(vehicleTO); + if (validModelComponent(vehicleModel, systemModel, collectedErrorMessages)) { + addVehicleToModel(vehicleModel, systemModel); + } + } + } + + private void importPaths( + PlantModelCreationTO model, + SystemModel systemModel, + Set collectedErrorMessages + ) { + for (PathCreationTO pathTO : model.getPaths()) { + PathModel pathModel = elementConverter.importPath(pathTO, systemModel); + if (validModelComponent(pathModel, systemModel, collectedErrorMessages)) { + addPathToModel(pathModel, systemModel); + } + } + } + + private void importPoints( + PlantModelCreationTO model, + SystemModel systemModel, + Set collectedErrorMessages + ) { + for (PointCreationTO pointTO : model.getPoints()) { + PointModel pointModel = elementConverter.importPoint(pointTO, systemModel); + if (validModelComponent(pointModel, systemModel, collectedErrorMessages)) { + addPointToModel(pointModel, systemModel); + } + } + } + + private boolean validModelComponent( + ModelComponent modelComponent, + SystemModel systemModel, + Set collectedErrorMessages + ) { + if (validator.isValidWith(systemModel, modelComponent)) { + return true; + } + else { + String deserializationError = validator.formatDeserializationErrors( + modelComponent, + validator.getErrors() + ); + validator.formatDeserializationErrors(modelComponent, validator.getErrors()); + validator.resetErrors(); + LOG.warn("Deserialization error: {}", deserializationError); + collectedErrorMessages.add(deserializationError); + return false; + } + } + + private void addPointToModel(PointModel point, SystemModel systemModel) { + systemModel.getMainFolder(SystemModel.FolderKey.POINTS).add(point); + } + + private void addPathToModel(PathModel path, SystemModel systemModel) { + systemModel.getMainFolder(SystemModel.FolderKey.PATHS).add(path); + } + + private void addVehicleToModel(VehicleModel vehicle, SystemModel systemModel) { + systemModel.getMainFolder(SystemModel.FolderKey.VEHICLES).add(vehicle); + } + + private void addLocationTypeToModel(LocationTypeModel locType, SystemModel systemModel) { + systemModel.getMainFolder(SystemModel.FolderKey.LOCATION_TYPES).add(locType); + } + + private void addLocationToModel(LocationModel location, SystemModel systemModel) { + systemModel.getMainFolder(SystemModel.FolderKey.LOCATIONS).add(location); + } + + private void addLinkToModel(LinkModel link, SystemModel systemModel) { + systemModel.getMainFolder(SystemModel.FolderKey.LINKS).add(link); + } + + private void addBlockToModel(BlockModel block, SystemModel systemModel) { + systemModel.getMainFolder(SystemModel.FolderKey.BLOCKS).add(block); + } + + private void updateLayoutInModel(LayoutModel layout, SystemModel systemModel) { + // SystemModel already contains a LayoutModel, just copy the properties + ModelComponent layoutComponent = systemModel.getMainFolder(SystemModel.FolderKey.LAYOUT); + for (Map.Entry property : layout.getProperties().entrySet()) { + layoutComponent.setProperty(property.getKey(), property.getValue()); + } + } + +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/ModelKernelPersistor.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/ModelKernelPersistor.java new file mode 100644 index 0000000..def0d81 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/ModelKernelPersistor.java @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.persistence; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import java.util.HashSet; +import java.util.Set; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.common.application.StatusPanel; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.guing.common.persistence.ModelExportAdapter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Persists data kept in {@link SystemModel}s to the kernel. + */ +public class ModelKernelPersistor { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ModelKernelPersistor.class); + /** + * The status panel for logging error messages. + */ + private final StatusPanel statusPanel; + /** + * Provides new instances to validate a system model. + */ + private final Provider validatorProvider; + + private final ModelExportAdapter modelExportAdapter; + + /** + * Creates a new instance. + * + * @param statusPanel A status panel for logging error messages. + * @param validatorProvider Provides validators for system models. + * @param modelExportAdapter Converts model data on export. + */ + @Inject + public ModelKernelPersistor( + @Nonnull + StatusPanel statusPanel, + @Nonnull + Provider validatorProvider, + ModelExportAdapter modelExportAdapter + ) { + this.statusPanel = requireNonNull(statusPanel, "statusPanel"); + this.validatorProvider = requireNonNull(validatorProvider, "validatorProvider"); + this.modelExportAdapter = requireNonNull(modelExportAdapter, "modelExportAdapter"); + } + + /** + * Persists the given model to the given kernel. + * + * @param systemModel The model to be persisted. + * @param portal The plant model service used to persist to the kernel. + * @param ignoreValidationErrors Whether to ignore any validation errors. + * @return {@code true} if, and only if, the model was valid or validation errors were to be + * ignored. + * @throws IllegalStateException If there was a problem persisting the model on the kernel side. + */ + public boolean persist( + SystemModel systemModel, + KernelServicePortal portal, + boolean ignoreValidationErrors + ) + throws IllegalStateException { + requireNonNull(systemModel, "systemModel"); + requireNonNull(portal, "plantModelService"); + + LOG.debug("Validating model..."); + long timeBefore = System.currentTimeMillis(); + if (!valid(systemModel) && !ignoreValidationErrors) { + return false; + } + LOG.debug("Validating took {} milliseconds.", System.currentTimeMillis() - timeBefore); + + LOG.debug("Persisting model..."); + timeBefore = System.currentTimeMillis(); + portal.getPlantModelService().createPlantModel(modelExportAdapter.convert(systemModel)); + LOG.debug( + "Persisting to kernel took {} milliseconds.", + System.currentTimeMillis() - timeBefore + ); + + return true; + } + + private boolean valid(SystemModel systemModel) { + ModelValidator validator = validatorProvider.get(); + boolean valid = true; + for (ModelComponent component : systemModel.getAll()) { + valid &= validator.isValidWith(systemModel, component); + } + //Report possible duplicates if we persist to the kernel + if (!valid) { + //Use a hash set to avoid duplicate errors + Set errors = new HashSet<>(validator.getErrors()); + validator.showSavingValidationWarning(statusPanel, errors); + } + return valid; + } + +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/ModelManagerModeling.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/ModelManagerModeling.java new file mode 100644 index 0000000..a244654 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/ModelManagerModeling.java @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.persistence; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.io.File; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.components.plantoverview.PlantModelExporter; +import org.opentcs.components.plantoverview.PlantModelImporter; +import org.opentcs.guing.common.persistence.ModelFileReader; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * Manages (loads, saves and keeps) the driving course model. + */ +public interface ModelManagerModeling + extends + ModelManager { + + /** + * Shows a dialog to select a model and loads it. + * + * @param modelFile The nullable model file to be loaded. If it + * is not present a dialog to select a file will be shown. + * @return true if, and only if, a model was successfully + * loaded. + */ + boolean loadModel( + @Nullable + File modelFile + ); + + /** + * Shows a dialog to select a model and loads it. + * + * @param modelFile The nullable model file to be loaded. If it + * is not present a dialog to select a file will be shown. + * @param reader The reader which reads and parses the file. + * @return true if, and only if, a model was successfully + * loaded. + */ + boolean loadModel( + @Nullable + File modelFile, ModelFileReader reader + ); + + /** + * Imports a model using the given importer. + * + * @param importer The importer to be used. + * @return true if, and only if, a model was successfully imported. + */ + boolean importModel( + @Nonnull + PlantModelImporter importer + ); + + /** + * Uploads the given system model to the kernel. + * + * @param portal The kernel client portal to upload the model to. + * @return Whether the model was actually uploaded. + */ + boolean uploadModel(KernelServicePortal portal); + + /** + * Exports a model using the given exporter. + * + * @param exporter The exporter to be used. + * @return true if, and only if, the model was successfully exported. + */ + boolean exportModel( + @Nonnull + PlantModelExporter exporter + ); +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/ModelValidator.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/ModelValidator.java new file mode 100644 index 0000000..67497d4 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/ModelValidator.java @@ -0,0 +1,653 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.persistence; + +import static com.google.common.base.Strings.isNullOrEmpty; + +import com.google.common.base.Strings; +import jakarta.inject.Inject; +import java.awt.Component; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.opentcs.data.model.Couple; +import org.opentcs.guing.base.components.properties.type.AngleProperty; +import org.opentcs.guing.base.components.properties.type.BoundingBoxProperty; +import org.opentcs.guing.base.components.properties.type.EnergyLevelThresholdSetModel; +import org.opentcs.guing.base.components.properties.type.EnergyLevelThresholdSetProperty; +import org.opentcs.guing.base.components.properties.type.KeyValueSetProperty; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.components.properties.type.LocationTypeProperty; +import org.opentcs.guing.base.components.properties.type.PercentProperty; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.base.components.properties.type.SpeedProperty; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.base.components.properties.type.StringSetProperty; +import org.opentcs.guing.base.model.BoundingBoxModel; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.modeleditor.util.TextAreaDialog; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Validator for a {@link SystemModel} and its {@link ModelComponent}s. + * Validates if the model component can safely be added to a system model. + */ +public class ModelValidator { + + /** + * This class' logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ModelValidator.class); + /** + * This class' resource bundle. + */ + private final ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.VALIDATOR_PATH); + /** + * The collection of errors which happened after the last reset. + */ + private final List errors = new ArrayList<>(); + + /** + * Creates a new instance. + */ + @Inject + public ModelValidator() { + } + + /** + * Returns all errors which happened after the last reset. + * + * @return the collection of errors as string + */ + public final List getErrors() { + return new ArrayList<>(errors); + } + + /** + * Clears all error messages. + */ + public void resetErrors() { + errors.clear(); + } + + /** + * Checks whether the given model will be valid if the component would be added to it. + * + * @param model the system model + * @param component the model component + * @return true if the model will be valid after adding the component, false otherwise + */ + public boolean isValidWith(SystemModel model, ModelComponent component) { + if (model == null) { + errorOccurred(model, "modelValidator.error_modelNull.text"); + return false; + } + if (component == null) { + errorOccurred(component, "modelValidator.error_componentNull.text"); + return false; + } + //Validate the name of the component + if (Strings.isNullOrEmpty(component.getName())) { + errorOccurred( + component, + "modelValidator.error_componentNameInvalid.text", + component.getName() + ); + return false; + } + if (nameExists(model, component)) { + errorOccurred( + component, + "modelValidator.error_componentNameExists.text", + component.getName() + ); + return false; + } + //Validate the miscellaneous property of the component + //TODO: Seems to be optional in some models?! + KeyValueSetProperty miscellaneous + = (KeyValueSetProperty) component.getProperty(ModelComponent.MISCELLANEOUS); + /* + * if (miscellaneous == null) { + * errorOccurred(component, "Miscellaneous key-value-set does not exist."); + * return false; + * } + */ + boolean valid = true; + if (component instanceof LayoutModel) { + } + else if (component instanceof PointModel) { + valid = validatePoint(model, (PointModel) component); + } + else if (component instanceof PathModel) { + valid = validatePath(model, (PathModel) component); + } + else if (component instanceof LocationTypeModel) { + } + else if (component instanceof LocationModel) { + valid = validateLocation(model, (LocationModel) component); + } + else if (component instanceof LinkModel) { + valid = validateLink(model, (LinkModel) component); + } + else if (component instanceof BlockModel) { + valid = validateBlock(model, (BlockModel) component); + } + else if (component instanceof VehicleModel) { + valid = validateVehicle(model, (VehicleModel) component); + } + else { + LOG.warn("Unknown model component {} - skipping validation.", component.getClass()); + } + return valid; + } + + public void showLoadingValidationWarning(Component parent, Collection content) { + TextAreaDialog panel + = new TextAreaDialog( + parent, + true, + bundle.getString("modelValidator.dialog_validationWarning.message.loadingError") + ); + panel.setContent(content); + panel.setTitle(bundle.getString("modelValidator.dialog_validationWarning.title")); + panel.setLocationRelativeTo(null); + panel.setVisible(true); + } + + public void showSavingValidationWarning(Component parent, Collection content) { + TextAreaDialog panel + = new TextAreaDialog( + parent, + true, + bundle.getString("modelValidator.dialog_validationWarning.message.savingError") + ); + panel.setContent(content); + panel.setTitle(bundle.getString("modelValidator.dialog_validationWarning.title")); + panel.setLocationRelativeTo(null); + panel.setVisible(true); + } + + public String formatDeserializationErrors(ModelComponent component, Collection errors) { + return ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.VALIDATOR_PATH) + .getFormatted( + "modelValidator.error_deserialization.text", + component.getName(), + errors + ); + } + + /** + * Handles all occurred errors while validating the system model. + * + * @param component the component where the error occurred + * @param bundleKey the bundle key with the error description + * @param args a list of arguments for the error description + */ + private void errorOccurred(ModelComponent component, String bundleKey, Object... args) { + String componentName = component == null ? "null" : component.getName(); + String message = componentName + ": " + bundle.getFormatted(bundleKey, args); + LOG.info(message); + errors.add(message); + } + + /** + * Validates the properties of a point model. + * + * @param model the system model to validate against + * @param point the point model to validate + * @return true if the point model is valid, false otherwise + */ + private boolean validatePoint(SystemModel model, PointModel point) { + boolean valid = true; + + //Validate the vehicle orientation angle + AngleProperty orientationProperty + = (AngleProperty) point.getProperty(PointModel.VEHICLE_ORIENTATION_ANGLE); + if (!(orientationProperty.getValue() instanceof Double)) { + LOG.warn( + "{}: Orientation angle property ('{}') is not a number. Setting it to 0.", + point.getName(), + orientationProperty.getValue() + ); + orientationProperty.setValueAndUnit(0, AngleProperty.Unit.DEG); + } + else { + Double angle = (Double) orientationProperty.getValue(); + if (angle < 0) { + LOG.warn( + "{}: Orientation angle property is {} but has to be > 0." + + " Transforming to positive angle.", + point.getName(), + orientationProperty.getValue() + ); + orientationProperty.setValueAndUnit(360 + angle % 360, AngleProperty.Unit.DEG); + } + } + point.setProperty(PointModel.VEHICLE_ORIENTATION_ANGLE, orientationProperty); + + // Validate the maximum vehicle bounding box + BoundingBoxProperty boundingBoxProperty + = (BoundingBoxProperty) point.getProperty(PointModel.MAX_VEHICLE_BOUNDING_BOX); + if (boundingBoxProperty.getValue().getLength() < 1 + || boundingBoxProperty.getValue().getWidth() < 1 + || boundingBoxProperty.getValue().getHeight() < 1) { + LOG.warn( + "{}: Some bounding box property dimensions are smaller than 1 but have to be > 0. " + + "Setting them to 1.", + point.getName() + ); + boundingBoxProperty.setValue( + new BoundingBoxModel( + Math.max(boundingBoxProperty.getValue().getLength(), 1), + Math.max(boundingBoxProperty.getValue().getWidth(), 1), + Math.max(boundingBoxProperty.getValue().getHeight(), 1), + new Couple( + boundingBoxProperty.getValue().getReferenceOffset().getX(), + boundingBoxProperty.getValue().getReferenceOffset().getY() + ) + ) + ); + point.setProperty(PointModel.MAX_VEHICLE_BOUNDING_BOX, boundingBoxProperty); + } + + return valid; + } + + /** + * Validates the properties of a path model. + * + * @param model the system model to validate against + * @param path the path model to validate + * @return true if the path model is valid, false otherwise + */ + private boolean validatePath(SystemModel model, PathModel path) { + boolean valid = true; + + //Validate the start component of this path + StringProperty startProperty = (StringProperty) path.getProperty(PathModel.START_COMPONENT); + if (!nameExists(model, startProperty.getText())) { + errorOccurred( + model, + "modelValidator.error_pathStartComponentNotExisting.text", + startProperty.getText() + ); + valid = false; + } + + //Validate the end component of this path + StringProperty endProperty = (StringProperty) path.getProperty(PathModel.END_COMPONENT); + if (!nameExists(model, endProperty.getText())) { + errorOccurred( + model, + "modelValidator.error_pathEndComponentNotExisting.text", + endProperty.getText() + ); + valid = false; + } + + //Validate the length of the path + LengthProperty lengthProperty = (LengthProperty) path.getProperty(PathModel.LENGTH); + if (((Double) lengthProperty.getValue()) < 1) { + LOG.warn( + "{}: Length property is {} but has to be > 0. Setting it to 1.", + path.getName(), + lengthProperty.getValue() + ); + lengthProperty.setValueAndUnit(1, LengthProperty.Unit.MM); + path.setProperty(PathModel.LENGTH, lengthProperty); + } + + //Validate the max velocity + SpeedProperty maxVelocityProperty = (SpeedProperty) path.getProperty(PathModel.MAX_VELOCITY); + if (((Double) maxVelocityProperty.getValue()) < 0) { + LOG.warn( + "{}: Max. velocity property is {} but has to be >= 0. Setting it to 0.", + path.getName(), + maxVelocityProperty.getValue() + ); + maxVelocityProperty.setValueAndUnit(0, SpeedProperty.Unit.MM_S); + path.setProperty(PathModel.MAX_VELOCITY, maxVelocityProperty); + } + + //Validate the maximum reverse velocity + SpeedProperty maxRevVelocityProperty + = (SpeedProperty) path.getProperty(PathModel.MAX_REVERSE_VELOCITY); + if (((Double) maxRevVelocityProperty.getValue()) < 0) { + LOG.warn( + "{}: Max. reverse velocity property is {} but has to be >= 0. Setting it to 0.", + path.getName(), + maxRevVelocityProperty.getValue() + ); + maxRevVelocityProperty.setValueAndUnit(0, SpeedProperty.Unit.MM_S); + path.setProperty(PathModel.MAX_VELOCITY, maxRevVelocityProperty); + } + + return valid; + } + + /** + * Validates the properties of a location model + * + * @param model the system model to validate against + * @param location the location model to validate + * @return true if the location model is valid, false otherwise + */ + private boolean validateLocation(SystemModel model, LocationModel location) { + boolean valid = true; + + //Validate the location type + LocationTypeProperty locTypeProperty + = (LocationTypeProperty) location.getProperty(LocationModel.TYPE); + boolean locTypeExists = model.getLocationTypeModels().stream() + .map(type -> type.getName()) + .anyMatch(typeName -> typeName.equals(locTypeProperty.getValue())); + if (!locTypeExists) { + errorOccurred( + location, "modelValidator.error_locationTypeInvalid.text", + locTypeProperty.getValue() + ); + valid = false; + } + + return valid; + } + + /** + * Validates the properties of a link model. + * + * @param model the system model to validate against + * @param link the link model to validate + * @return true if the link model is valid, false otherwise + */ + private boolean validateLink(SystemModel model, LinkModel link) { + boolean valid = true; + + //Validate whether the start component exists + StringProperty startProperty = (StringProperty) link.getProperty(LinkModel.START_COMPONENT); + if (!nameExists(model, startProperty.getText())) { + errorOccurred( + link, "modelValidator.error_linkStartComponentNotExisting.text", + startProperty.getText() + ); + valid = false; + } + //Validate whether the point exists + StringProperty endProperty = (StringProperty) link.getProperty(LinkModel.END_COMPONENT); + if (!nameExists(model, endProperty.getText())) { + errorOccurred( + link, "modelValidator.error_linkEndComponentNotExisting.text", + endProperty.getText() + ); + valid = false; + } + return valid; + } + + /** + * Validates the properties of a block model. + * + * @param model the system model to validate against + * @param block the block model to validate + * @return true if the block model is valid, false otherwise + */ + private boolean validateBlock(SystemModel model, BlockModel block) { + boolean valid = true; + + //Validate that all members of the block exists + StringSetProperty elementsProperty = (StringSetProperty) block.getProperty(BlockModel.ELEMENTS); + Set elements = new HashSet<>(); + for (String element : elementsProperty.getItems()) { + if (elements.contains(element)) { + errorOccurred(block, "modelValidator.error_blockElementsDuplicate.text", element); + valid = false; + } + elements.add(element); + if (!nameExists(model, element)) { + errorOccurred(block, "modelValidator.error_blockElementsBotExisting.text", element); + valid = false; + } + } + + return valid; + } + + /** + * Validates the properties of a vehicle model. + * + * @param model the system model to validate against + * @param vehicle the vehicle model to validate + * @return true if the vehicle model is valid, false otherwise + */ + private boolean validateVehicle(SystemModel model, VehicleModel vehicle) { + boolean valid = true; + + //Validate that all properties needed exist + BoundingBoxProperty boundingBoxProperty + = (BoundingBoxProperty) vehicle.getProperty(VehicleModel.BOUNDING_BOX); + if (boundingBoxProperty.getValue().getLength() < 1 + || boundingBoxProperty.getValue().getWidth() < 1 + || boundingBoxProperty.getValue().getHeight() < 1) { + LOG.warn( + "{}: Some bounding box property dimensions are smaller than 1 but have to be > 0. " + + "Setting them to 1.", + vehicle.getName() + ); + boundingBoxProperty.setValue( + new BoundingBoxModel( + Math.max(boundingBoxProperty.getValue().getLength(), 1), + Math.max(boundingBoxProperty.getValue().getWidth(), 1), + Math.max(boundingBoxProperty.getValue().getHeight(), 1), + new Couple( + boundingBoxProperty.getValue().getReferenceOffset().getX(), + boundingBoxProperty.getValue().getReferenceOffset().getY() + ) + ) + ); + vehicle.setProperty(VehicleModel.BOUNDING_BOX, boundingBoxProperty); + } + + EnergyLevelThresholdSetProperty energyLevelThresholdSetProperty + = (EnergyLevelThresholdSetProperty) vehicle.getProperty( + VehicleModel.ENERGY_LEVEL_THRESHOLD_SET + ); + // Validate the critical energy level + if (energyLevelThresholdSetProperty.getValue().getEnergyLevelCritical() < 0 + || energyLevelThresholdSetProperty.getValue().getEnergyLevelCritical() > 100) { + LOG.warn( + "{}: Energy level critical is {}, should be in [0..100]. Setting to 0.", + vehicle.getName(), + energyLevelThresholdSetProperty.getValue().getEnergyLevelCritical() + ); + energyLevelThresholdSetProperty.setValue( + new EnergyLevelThresholdSetModel( + 0, + energyLevelThresholdSetProperty.getValue().getEnergyLevelGood(), + energyLevelThresholdSetProperty.getValue().getEnergyLevelSufficientlyRecharged(), + energyLevelThresholdSetProperty.getValue().getEnergyLevelFullyRecharged() + ) + ); + vehicle.setProperty(VehicleModel.ENERGY_LEVEL_THRESHOLD_SET, energyLevelThresholdSetProperty); + } + + // Validate the good energy level + if (energyLevelThresholdSetProperty.getValue().getEnergyLevelGood() < 0 + || energyLevelThresholdSetProperty.getValue().getEnergyLevelGood() > 100) { + LOG.warn( + "{}: Energy level good is {}, should be in [0..100]. Setting to 100.", + vehicle.getName(), + energyLevelThresholdSetProperty.getValue().getEnergyLevelGood() + ); + energyLevelThresholdSetProperty.setValue( + new EnergyLevelThresholdSetModel( + energyLevelThresholdSetProperty.getValue().getEnergyLevelCritical(), + 100, + energyLevelThresholdSetProperty.getValue().getEnergyLevelSufficientlyRecharged(), + energyLevelThresholdSetProperty.getValue().getEnergyLevelFullyRecharged() + ) + ); + vehicle.setProperty(VehicleModel.ENERGY_LEVEL_THRESHOLD_SET, energyLevelThresholdSetProperty); + } + + // Validate that the good energy level is greater than or equals the critical energy level + if (energyLevelThresholdSetProperty.getValue().getEnergyLevelGood() + < energyLevelThresholdSetProperty.getValue().getEnergyLevelCritical()) { + LOG.warn( + "{}: Energy level good ({}) not >= energy level critical ({}). Setting to {}.", + vehicle.getName(), + energyLevelThresholdSetProperty.getValue().getEnergyLevelGood(), + energyLevelThresholdSetProperty.getValue().getEnergyLevelCritical(), + energyLevelThresholdSetProperty.getValue().getEnergyLevelCritical() + ); + energyLevelThresholdSetProperty.setValue( + new EnergyLevelThresholdSetModel( + energyLevelThresholdSetProperty.getValue().getEnergyLevelCritical(), + energyLevelThresholdSetProperty.getValue().getEnergyLevelCritical(), + energyLevelThresholdSetProperty.getValue().getEnergyLevelSufficientlyRecharged(), + energyLevelThresholdSetProperty.getValue().getEnergyLevelFullyRecharged() + ) + ); + vehicle.setProperty(VehicleModel.ENERGY_LEVEL_THRESHOLD_SET, energyLevelThresholdSetProperty); + } + + // Validate that the fully recharged energy level is greater than or equals the sufficiently + // recharged energy level + if (energyLevelThresholdSetProperty.getValue().getEnergyLevelFullyRecharged() + < energyLevelThresholdSetProperty.getValue().getEnergyLevelSufficientlyRecharged()) { + LOG.warn( + "{}: Energy level fully recharged ({}) not >= energy level sufficiently recharged ({})." + + " Setting to {}.", + vehicle.getName(), + energyLevelThresholdSetProperty.getValue().getEnergyLevelFullyRecharged(), + energyLevelThresholdSetProperty.getValue().getEnergyLevelSufficientlyRecharged(), + energyLevelThresholdSetProperty.getValue().getEnergyLevelSufficientlyRecharged() + ); + energyLevelThresholdSetProperty.setValue( + new EnergyLevelThresholdSetModel( + energyLevelThresholdSetProperty.getValue().getEnergyLevelCritical(), + energyLevelThresholdSetProperty.getValue().getEnergyLevelGood(), + energyLevelThresholdSetProperty.getValue().getEnergyLevelSufficientlyRecharged(), + energyLevelThresholdSetProperty.getValue().getEnergyLevelSufficientlyRecharged() + ) + ); + vehicle.setProperty(VehicleModel.ENERGY_LEVEL_THRESHOLD_SET, energyLevelThresholdSetProperty); + } + + //Validate the current energy level + PercentProperty energyLevelProperty + = (PercentProperty) vehicle.getProperty(VehicleModel.ENERGY_LEVEL); + if (((int) energyLevelProperty.getValue()) < 0 + || ((int) energyLevelProperty.getValue()) > 100) { + LOG.warn("{}: Energy level is {} but has to be in range of [0..100]. Setting it to 50."); + energyLevelProperty.setValueAndUnit(50, PercentProperty.Unit.PERCENT); + vehicle.setProperty(VehicleModel.ENERGY_LEVEL, energyLevelProperty); + } + + //Validate the precise position happens in the property converter + AngleProperty orientationProperty + = (AngleProperty) vehicle.getProperty(VehicleModel.ORIENTATION_ANGLE); + if (((Double) orientationProperty.getValue()) < 0) { + LOG.warn("{}: Orientation angle is {} but has to be >= 0. Setting it to 0."); + orientationProperty.setValueAndUnit(0, AngleProperty.Unit.DEG); + vehicle.setProperty(VehicleModel.ORIENTATION_ANGLE, orientationProperty); + } + + //Validate whether the current point exists + StringProperty currentPointProperty = (StringProperty) vehicle.getProperty(VehicleModel.POINT); + String currentPoint = currentPointProperty.getText(); + if (!isNullOrEmptyPoint(currentPoint) && !nameExists(model, currentPoint)) { + errorOccurred( + vehicle, "modelValidator.error_vehicleCurrentPointNotExisting.text", + currentPointProperty.getText() + ); + valid = false; + } + //Validate whether the next point exists + StringProperty nextPointProperty + = (StringProperty) vehicle.getProperty(VehicleModel.NEXT_POINT); + String nextPoint = nextPointProperty.getText(); + if (!isNullOrEmptyPoint(nextPoint) && !nameExists(model, nextPoint)) { + errorOccurred( + vehicle, "modelValidator.error_vehicleNextPointNotExisting.text", + nextPointProperty.getText() + ); + valid = false; + } + return valid; + } + + /** + * Checks whether the given property exists in the model component. + * //TODO: Use this method to validate all required properties + * + * @param propertyName the name of the property + * @param component the model component + * @return true if the property exists, false otherwise + */ + private boolean checkPropertyExists(String propertyName, ModelComponent component) { + Property property = component.getProperty(propertyName); + if (property == null) { + errorOccurred(component, "{} property does not exist.", propertyName); + return false; + } + else { + return true; + } + } + + /** + * Checks whether the name of the component is already present in the system model and the object + * is not equals to the component to check. + * + * @param model the system model + * @param component the component + * @return true if the name is present, false otherwise + */ + private boolean nameExists(SystemModel model, ModelComponent component) { + if (Strings.isNullOrEmpty(component.getName())) { + return false; + } + ModelComponent foundComponent = model.getModelComponent(component.getName()); + return foundComponent != null && foundComponent != component; + } + + /** + * Checks whether the name of the component is already present in the system model. + * + * @param model the system model + * @param name the component name + * @return true if the name is present, false otherwise + */ + private boolean nameExists(SystemModel model, String name) { + if (Strings.isNullOrEmpty(name)) { + return false; + } + return model.getModelComponent(name) != null; + } + + /** + * Returns true if the given name is null or empty or equals the string "null", because that's + * a valid content in a model file for no point. + * + * @param name The point name + * @return True if the given name is null or empty or equals the string "null" + */ + private boolean isNullOrEmptyPoint(String name) { + return isNullOrEmpty(name) || name.equals("null"); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/OpenTCSModelManagerModeling.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/OpenTCSModelManagerModeling.java new file mode 100644 index 0000000..a997163 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/OpenTCSModelManagerModeling.java @@ -0,0 +1,299 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.persistence; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import java.io.File; +import java.io.IOException; +import java.util.Optional; +import java.util.logging.Level; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import org.opentcs.access.CredentialsException; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.components.plantoverview.PlantModelExporter; +import org.opentcs.components.plantoverview.PlantModelImporter; +import org.opentcs.customizations.ApplicationHome; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.guing.common.application.ProgressIndicator; +import org.opentcs.guing.common.application.StatusPanel; +import org.opentcs.guing.common.exchange.adapter.ProcessAdapterUtil; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.guing.common.persistence.ModelExportAdapter; +import org.opentcs.guing.common.persistence.ModelFilePersistor; +import org.opentcs.guing.common.persistence.ModelFileReader; +import org.opentcs.guing.common.persistence.OpenTCSModelManager; +import org.opentcs.guing.common.util.CourseObjectFactory; +import org.opentcs.guing.common.util.ModelComponentFactory; +import org.opentcs.guing.common.util.SynchronizedFileChooser; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages (loads, persists and keeps) the driving course model. + */ +public class OpenTCSModelManagerModeling + extends + OpenTCSModelManager + implements + ModelManagerModeling { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(OpenTCSModelManagerModeling.class); + /** + * A file chooser for selecting model files to be opened. + */ + private final JFileChooser modelReaderFileChooser; + /** + * A file chooser for selecting model files to be saved. + */ + private final JFileChooser modelPersistorFileChooser; + /** + * Persists a model to a kernel. + */ + private final ModelKernelPersistor kernelPersistor; + /** + * The file filters for different model readers that can be used to load models from a file. + */ + private final ModelFileReader modelReader; + /** + * Converts model data on import. + */ + private final ModelImportAdapter modelImportAdapter; + + /** + * Creates a new instance. + * + * @param applicationFrame The application's main frame. + * @param crsObjFactory A course object factory to be used. + * @param modelComponentFactory The model component factory to be used. + * @param procAdapterUtil A utility class for process adapters. + * @param systemModelProvider Provides instances of SystemModel. + * @param statusPanel StatusPanel to log messages. + * @param homeDir The application's home directory. + * @param kernelPersistor Persists a model to a kernel. + * @param modelReader The model reader. + * @param modelPersistor The model persistor. + * @param modelImportAdapter Converts model data on import. + * @param modelExportAdapter Converts model data on export. + * @param progressIndicator The progress indicator to be used. + */ + @Inject + public OpenTCSModelManagerModeling( + @ApplicationFrame + JFrame applicationFrame, + CourseObjectFactory crsObjFactory, + ModelComponentFactory modelComponentFactory, + ProcessAdapterUtil procAdapterUtil, + Provider systemModelProvider, + StatusPanel statusPanel, + @ApplicationHome + File homeDir, + ModelKernelPersistor kernelPersistor, + ModelFileReader modelReader, + ModelFilePersistor modelPersistor, + ModelImportAdapter modelImportAdapter, + ModelExportAdapter modelExportAdapter, + ProgressIndicator progressIndicator + ) { + super( + applicationFrame, + crsObjFactory, + modelComponentFactory, + procAdapterUtil, + systemModelProvider, + statusPanel, + homeDir, + modelPersistor, + modelExportAdapter, + progressIndicator + ); + this.kernelPersistor = requireNonNull(kernelPersistor, "kernelPersistor"); + + this.modelReader = requireNonNull(modelReader, "modelReader"); + this.modelReaderFileChooser = new SynchronizedFileChooser(new File(homeDir, "data")); + this.modelReaderFileChooser.setAcceptAllFileFilterUsed(false); + this.modelReaderFileChooser.setFileFilter(modelReader.getDialogFileFilter()); + + this.modelPersistorFileChooser = new SynchronizedFileChooser(new File(homeDir, "data")); + this.modelPersistorFileChooser.setAcceptAllFileFilterUsed(false); + this.modelPersistorFileChooser.setFileFilter(modelPersistor.getDialogFileFilter()); + + this.modelImportAdapter = requireNonNull(modelImportAdapter, "modelImportAdapter"); + } + + @Override + public boolean loadModel( + @Nullable + File modelFile + ) { + File file = modelFile != null ? modelFile : showOpenDialog(); + if (file == null) { + return false; + } + + return loadModel(file, modelReader); + } + + @Override + public boolean loadModel( + @Nullable + File modelFile, ModelFileReader reader + ) { + requireNonNull(reader, "reader"); + File file = modelFile != null ? modelFile : showOpenDialog(); + if (file == null) { + return false; + } + + try { + Optional opt = reader.deserialize(file); + if (!opt.isPresent()) { + LOG.debug("Loading model canceled."); + return false; + } + setModel(modelImportAdapter.convert(opt.get())); + setCurrentModelFile(file); + initializeSystemModel(getModel()); + return true; + } + catch (IOException | IllegalArgumentException ex) { + getStatusPanel().setLogMessage( + Level.SEVERE, + ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MISC_PATH) + .getFormatted( + "openTcsModelManagerModeling.message_notLoaded.text", + file.getName() + ) + ); + LOG.info("Error reading file", ex); + } + return false; + } + + @Override + public boolean importModel(PlantModelImporter importer) { + requireNonNull(importer, "importer"); + + try { + Optional opt = importer.importPlantModel(); + if (!opt.isPresent()) { + LOG.debug("Model import cancelled."); + return false; + } + SystemModel newSystemModel = modelImportAdapter.convert(opt.get()); + setModel(newSystemModel); + setCurrentModelFile(null); + initializeSystemModel(getModel()); + return true; + } + catch (IOException | IllegalArgumentException ex) { + getStatusPanel().setLogMessage( + Level.SEVERE, + ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MISC_PATH) + .getFormatted("openTcsModelManagerModeling.message_notImported.text") + ); + LOG.warn("Exception importing model", ex); + return false; + } + } + + @Override + public boolean uploadModel(KernelServicePortal portal) { + try { + setModelName(getModel().getName()); + getStatusPanel().clear(); + return persistModel(getModel(), portal, kernelPersistor, false); + } + catch (IllegalStateException | CredentialsException e) { + getStatusPanel().setLogMessage( + Level.SEVERE, + ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MISC_PATH) + .getString("openTcsModelManagerModeling.message_notSaved.text") + ); + LOG.warn("Exception persisting model", e); + return false; + } + catch (IllegalArgumentException e) { + getStatusPanel().setLogMessage( + Level.SEVERE, + e.getMessage() + ); + LOG.warn("Exception persisting model", e); + return false; + } + } + + @Override + public boolean exportModel(PlantModelExporter exporter) { + requireNonNull(exporter, "exporter"); + + try { + exporter.exportPlantModel(getModelExportAdapter().convert(getModel())); + return true; + } + catch (IOException | IllegalArgumentException ex) { + getStatusPanel().setLogMessage( + Level.SEVERE, + ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.MISC_PATH) + .getString("openTcsModelManagerModeling.message_notExported.text") + ); + LOG.warn("Exception exporting model", ex); + return false; + } + } + + /** + * Persist model with the persistor. + * + * @param systemModel The system model to be persisted. + * @param persistor The persistor to be used. + * @param ignoreError whether the model should be persisted when duplicates exist + * @return Whether the model was actually saved. + * @throws IllegalStateException If there was a problem persisting the model + */ + private boolean persistModel( + SystemModel systemModel, + KernelServicePortal portal, + ModelKernelPersistor persistor, + boolean ignoreError + ) + throws IllegalStateException, + KernelRuntimeException { + requireNonNull(systemModel, "systemModel"); + requireNonNull(persistor, "persistor"); + + if (!persistor.persist(systemModel, portal, ignoreError)) { + return false; + } + + systemModel.setName(getModelName()); + return true; + } + + /** + * Shows a dialog to select a model to load. + * + * @return The selected file or null, if nothing was selected. + */ + private File showOpenDialog() { + if (!modelReaderFileChooser.getCurrentDirectory().isDirectory()) { + modelReaderFileChooser.getCurrentDirectory().mkdir(); + } + if (modelReaderFileChooser.showOpenDialog(getApplicationFrame()) + != JFileChooser.APPROVE_OPTION) { + return null; + } + return modelReaderFileChooser.getSelectedFile(); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/unified/PlantModelElementConverter.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/unified/PlantModelElementConverter.java new file mode 100644 index 0000000..7a6fafc --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/unified/PlantModelElementConverter.java @@ -0,0 +1,435 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.persistence.unified; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.access.to.model.BlockCreationTO; +import org.opentcs.access.to.model.LocationCreationTO; +import org.opentcs.access.to.model.LocationTypeCreationTO; +import org.opentcs.access.to.model.PathCreationTO; +import org.opentcs.access.to.model.PointCreationTO; +import org.opentcs.access.to.model.VehicleCreationTO; +import org.opentcs.access.to.model.VisualLayoutCreationTO; +import org.opentcs.access.to.peripherals.PeripheralOperationCreationTO; +import org.opentcs.data.model.Block; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Envelope; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.visualization.Layer; +import org.opentcs.data.model.visualization.LayerGroup; +import org.opentcs.data.model.visualization.LocationRepresentation; +import org.opentcs.guing.base.components.layer.LayerWrapper; +import org.opentcs.guing.base.components.properties.type.AngleProperty; +import org.opentcs.guing.base.components.properties.type.EnergyLevelThresholdSetModel; +import org.opentcs.guing.base.components.properties.type.KeyValueProperty; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.components.properties.type.SpeedProperty; +import org.opentcs.guing.base.model.BoundingBoxModel; +import org.opentcs.guing.base.model.EnvelopeModel; +import org.opentcs.guing.base.model.PeripheralOperationModel; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.model.SystemModel; + +/** + */ +public class PlantModelElementConverter { + + public PlantModelElementConverter() { + } + + public PointModel importPoint(PointCreationTO pointTO, SystemModel systemModel) { + requireNonNull(pointTO, "pointTO"); + requireNonNull(systemModel, "systemModel"); + + PointModel model = new PointModel(); + + model.setName(pointTO.getName()); + + model.getPropertyModelPositionX().setValueAndUnit( + pointTO.getPose().getPosition().getX(), + LengthProperty.Unit.MM + ); + model.getPropertyModelPositionY().setValueAndUnit( + pointTO.getPose().getPosition().getY(), + LengthProperty.Unit.MM + ); + model.getPropertyVehicleOrientationAngle().setValueAndUnit( + pointTO.getPose().getOrientationAngle(), + AngleProperty.Unit.DEG + ); + model.getPropertyType().setValue(mapPointType(pointTO.getType())); + + for (Map.Entry entry : pointTO.getVehicleEnvelopes().entrySet()) { + model.getPropertyVehicleEnvelopes().getValue().add( + new EnvelopeModel(entry.getKey(), entry.getValue().getVertices()) + ); + } + + model.getPropertyMaxVehicleBoundingBox().setValue( + new BoundingBoxModel( + pointTO.getMaxVehicleBoundingBox().getLength(), + pointTO.getMaxVehicleBoundingBox().getWidth(), + pointTO.getMaxVehicleBoundingBox().getHeight(), + new Couple( + pointTO.getMaxVehicleBoundingBox().getReferenceOffset().getX(), + pointTO.getMaxVehicleBoundingBox().getReferenceOffset().getY() + ) + ) + ); + + for (Map.Entry property : pointTO.getProperties().entrySet()) { + model.getPropertyMiscellaneous().addItem( + new KeyValueProperty( + model, + property.getKey(), + property.getValue() + ) + ); + } + + // Gather information contained in the layout + model.getPropertyLayoutPosX().setText(String.valueOf(pointTO.getLayout().getPosition().getX())); + model.getPropertyLayoutPosY().setText(String.valueOf(pointTO.getLayout().getPosition().getY())); + model.getPropertyPointLabelOffsetX().setText( + String.valueOf(pointTO.getLayout().getLabelOffset().getX()) + ); + model.getPropertyPointLabelOffsetY().setText( + String.valueOf(pointTO.getLayout().getLabelOffset().getY()) + ); + model.getPropertyPointLabelOrientationAngle().setText(""); + LayerWrapper layerWrapper = systemModel.getLayoutModel().getPropertyLayerWrappers() + .getValue().get(pointTO.getLayout().getLayerId()); + model.getPropertyLayerWrapper().setValue(layerWrapper); + + return model; + } + + public PathModel importPath(PathCreationTO pathTO, SystemModel systemModel) { + PathModel model = new PathModel(); + + model.setName(pathTO.getName()); + model.getPropertyLength().setValueAndUnit(pathTO.getLength(), LengthProperty.Unit.MM); + model.getPropertyMaxVelocity().setValueAndUnit( + pathTO.getMaxVelocity(), + SpeedProperty.Unit.MM_S + ); + model.getPropertyMaxReverseVelocity().setValueAndUnit( + pathTO.getMaxReverseVelocity(), + SpeedProperty.Unit.MM_S + ); + model.getPropertyStartComponent().setText(pathTO.getSrcPointName()); + model.getPropertyEndComponent().setText(pathTO.getDestPointName()); + model.getPropertyLocked().setValue(pathTO.isLocked()); + + for (Map.Entry entry : pathTO.getVehicleEnvelopes().entrySet()) { + model.getPropertyVehicleEnvelopes().getValue().add( + new EnvelopeModel(entry.getKey(), entry.getValue().getVertices()) + ); + } + + for (Map.Entry property : pathTO.getProperties().entrySet()) { + model.getPropertyMiscellaneous().addItem( + new KeyValueProperty( + model, + property.getKey(), + property.getValue() + ) + ); + } + + for (PeripheralOperationCreationTO operationTO : pathTO.getPeripheralOperations()) { + model.getPropertyPeripheralOperations().getValue().add( + new PeripheralOperationModel( + operationTO.getLocationName(), + operationTO.getOperation(), + operationTO.getExecutionTrigger(), + operationTO.isCompletionRequired() + ) + ); + } + + // Gather information contained in the layout + model.getPropertyPathConnType() + .setValue(PathModel.Type.valueOf(pathTO.getLayout().getConnectionType().name())); + model.getPropertyPathControlPoints().setText( + pathTO.getLayout().getControlPoints().stream() + .map(controlPoint -> String.format("%d,%d", controlPoint.getX(), controlPoint.getY())) + .collect(Collectors.joining(";")) + ); + LayerWrapper layerWrapper = systemModel.getLayoutModel().getPropertyLayerWrappers() + .getValue().get(pathTO.getLayout().getLayerId()); + model.getPropertyLayerWrapper().setValue(layerWrapper); + + return model; + } + + public VehicleModel importVehicle(VehicleCreationTO vehicleTO) { + VehicleModel model = new VehicleModel(); + + model.setName(vehicleTO.getName()); + model.getPropertyBoundingBox().setValue( + new BoundingBoxModel( + vehicleTO.getBoundingBox().getLength(), + vehicleTO.getBoundingBox().getWidth(), + vehicleTO.getBoundingBox().getHeight(), + new Couple( + vehicleTO.getBoundingBox().getReferenceOffset().getX(), + vehicleTO.getBoundingBox().getReferenceOffset().getY() + ) + ) + ); + model.getPropertyMaxVelocity().setValueAndUnit( + ((double) vehicleTO.getMaxVelocity()), + SpeedProperty.Unit.MM_S + ); + model.getPropertyMaxReverseVelocity().setValueAndUnit( + ((double) vehicleTO.getMaxReverseVelocity()), SpeedProperty.Unit.MM_S + ); + + model.getPropertyEnergyLevelThresholdSet().setValue( + new EnergyLevelThresholdSetModel( + vehicleTO.getEnergyLevelThresholdSet().getEnergyLevelCritical(), + vehicleTO.getEnergyLevelThresholdSet().getEnergyLevelGood(), + vehicleTO.getEnergyLevelThresholdSet().getEnergyLevelSufficientlyRecharged(), + vehicleTO.getEnergyLevelThresholdSet().getEnergyLevelFullyRecharged() + ) + ); + + model.getPropertyEnvelopeKey().setText(vehicleTO.getEnvelopeKey()); + + for (Map.Entry property : vehicleTO.getProperties().entrySet()) { + model.getPropertyMiscellaneous().addItem( + new KeyValueProperty( + model, + property.getKey(), + property.getValue() + ) + ); + } + + // Gather information contained in the layout + model.getPropertyRouteColor().setColor(vehicleTO.getLayout().getRouteColor()); + + return model; + } + + public LocationTypeModel importLocationType(LocationTypeCreationTO locTypeTO) { + LocationTypeModel model = new LocationTypeModel(); + + model.setName(locTypeTO.getName()); + for (String allowedOperation : locTypeTO.getAllowedOperations()) { + model.getPropertyAllowedOperations().addItem(allowedOperation); + } + + for (String allowedPeripheralOperations : locTypeTO.getAllowedPeripheralOperations()) { + model.getPropertyAllowedPeripheralOperations().addItem(allowedPeripheralOperations); + } + + for (Map.Entry property : locTypeTO.getProperties().entrySet()) { + model.getPropertyMiscellaneous().addItem( + new KeyValueProperty( + model, + property.getKey(), + property.getValue() + ) + ); + } + + // Gather information contained in the layout + model.getPropertyDefaultRepresentation().setLocationRepresentation( + LocationRepresentation.valueOf(locTypeTO.getLayout().getLocationRepresentation().name()) + ); + + return model; + } + + public LocationModel importLocation( + LocationCreationTO locationTO, + Collection locTypes, + SystemModel systemModel + ) { + LocationModel model = new LocationModel(); + + model.setName(locationTO.getName()); + model.getPropertyModelPositionX().setValueAndUnit( + locationTO.getPosition().getX(), + LengthProperty.Unit.MM + ); + model.getPropertyModelPositionY().setValueAndUnit( + locationTO.getPosition().getY(), + LengthProperty.Unit.MM + ); + + List possibleLocationTypes = new ArrayList<>(); + for (LocationTypeCreationTO locType : locTypes) { + if (!possibleLocationTypes.contains(locType.getName())) { + possibleLocationTypes.add(locType.getName()); + } + } + model.getPropertyType().setPossibleValues(possibleLocationTypes); + model.getPropertyType().setValue(locationTO.getTypeName()); + model.getPropertyLocked().setValue(locationTO.isLocked()); + + for (Map.Entry property : locationTO.getProperties().entrySet()) { + model.getPropertyMiscellaneous().addItem( + new KeyValueProperty( + model, + property.getKey(), + property.getValue() + ) + ); + } + + // Gather information contained in the layout + model.getPropertyLayoutPositionX().setText( + String.valueOf(locationTO.getLayout().getPosition().getX()) + ); + model.getPropertyLayoutPositionY().setText( + String.valueOf(locationTO.getLayout().getPosition().getY()) + ); + model.getPropertyLabelOffsetX().setText( + String.valueOf(locationTO.getLayout().getLabelOffset().getX()) + ); + model.getPropertyLabelOffsetY().setText( + String.valueOf(locationTO.getLayout().getLabelOffset().getY()) + ); + model.getPropertyDefaultRepresentation().setLocationRepresentation( + LocationRepresentation.valueOf(locationTO.getLayout().getLocationRepresentation().name()) + ); + model.getPropertyLabelOrientationAngle().setText(""); + LayerWrapper layerWrapper = systemModel.getLayoutModel().getPropertyLayerWrappers() + .getValue().get(locationTO.getLayout().getLayerId()); + model.getPropertyLayerWrapper().setValue(layerWrapper); + + return model; + } + + public LinkModel importLocationLink( + LocationCreationTO locationTO, + String pointName, + Set operations, + SystemModel systemModel + ) { + LinkModel model = new LinkModel(); + + model.setName(String.format("%s --- %s", pointName, locationTO.getName())); + + for (String operation : operations) { + model.getPropertyAllowedOperations().addItem(operation); + } + + model.getPropertyStartComponent().setText(pointName); + model.getPropertyEndComponent().setText(locationTO.getName()); + LayerWrapper layerWrapper = systemModel.getLayoutModel().getPropertyLayerWrappers() + .getValue().get(locationTO.getLayout().getLayerId()); + model.getPropertyLayerWrapper().setValue(layerWrapper); + + return model; + } + + public BlockModel importBlock(BlockCreationTO blockTO) { + BlockModel model = new BlockModel(); + + model.setName(blockTO.getName()); + + model.getPropertyType().setValue(mapBlockType(blockTO.getType())); + + for (String member : blockTO.getMemberNames()) { + model.getPropertyElements().addItem(member); + } + + for (Map.Entry property : blockTO.getProperties().entrySet()) { + model.getPropertyMiscellaneous().addItem( + new KeyValueProperty( + model, + property.getKey(), + property.getValue() + ) + ); + } + + // Gather information contained in the layout + model.getPropertyColor().setColor(blockTO.getLayout().getColor()); + + return model; + } + + public LayoutModel importLayout(VisualLayoutCreationTO layoutTO) { + LayoutModel model = new LayoutModel(); + + model.setName(layoutTO.getName()); + model.getPropertyScaleX().setValueAndUnit(layoutTO.getScaleX(), LengthProperty.Unit.MM); + model.getPropertyScaleY().setValueAndUnit(layoutTO.getScaleY(), LengthProperty.Unit.MM); + initLayerGroups(model, layoutTO.getLayerGroups()); + initLayers(model, layoutTO.getLayers()); + + for (Map.Entry property : layoutTO.getProperties().entrySet()) { + model.getPropertyMiscellaneous().addItem( + new KeyValueProperty( + model, + property.getKey(), + property.getValue() + ) + ); + } + + return model; + } + + private PointModel.Type mapPointType(Point.Type type) { + switch (type) { + case HALT_POSITION: + return PointModel.Type.HALT; + case PARK_POSITION: + return PointModel.Type.PARK; + default: + throw new IllegalArgumentException("Unhandled point type: " + type); + } + } + + private BlockModel.Type mapBlockType(Block.Type type) { + switch (type) { + case SINGLE_VEHICLE_ONLY: + return BlockModel.Type.SINGLE_VEHICLE_ONLY; + case SAME_DIRECTION_ONLY: + return BlockModel.Type.SAME_DIRECTION_ONLY; + default: + throw new IllegalArgumentException("Unhandled block type: " + type); + } + } + + private void initLayerGroups(LayoutModel model, Collection groups) { + Map layerGroups = model.getPropertyLayerGroups().getValue(); + layerGroups.clear(); + for (LayerGroup group : groups) { + layerGroups.put(group.getId(), group); + } + } + + private void initLayers(LayoutModel model, Collection layers) { + Map layerWrappers = model.getPropertyLayerWrappers().getValue(); + layerWrappers.clear(); + + Map layerGroups = model.getPropertyLayerGroups().getValue(); + for (Layer layer : layers) { + layerWrappers.put( + layer.getId(), + new LayerWrapper(layer, layerGroups.get(layer.getGroupId())) + ); + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/unified/UnifiedModelReader.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/unified/UnifiedModelReader.java new file mode 100644 index 0000000..37d985e --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/persistence/unified/UnifiedModelReader.java @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.persistence.unified; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.util.Optional; +import javax.swing.filechooser.FileFilter; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.guing.common.persistence.ModelFileReader; +import org.opentcs.guing.common.persistence.unified.UnifiedModelConstants; +import org.opentcs.util.persistence.ModelParser; + +/** + * Implementation of [@link ModelFileReader} to deserialize a {@link PlantModelCreationTO} from a + * xml file. + */ +public class UnifiedModelReader + implements + ModelFileReader { + + /** + * The model parser. + */ + private final ModelParser modelParser; + + @Inject + public UnifiedModelReader(ModelParser modelParser) { + this.modelParser = requireNonNull(modelParser, "modelParser"); + } + + @Override + public Optional deserialize(File file) + throws IOException { + requireNonNull(file, "file"); + + PlantModelCreationTO plantModel = modelParser.readModel(file); + return Optional.of(plantModel); + } + + @Override + public FileFilter getDialogFileFilter() { + return UnifiedModelConstants.DIALOG_FILE_FILTER; + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/Colors.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/Colors.java new file mode 100644 index 0000000..6c3a6f4 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/Colors.java @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.util; + +import static java.util.Objects.requireNonNull; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.VehicleModel; + +/** + * Utility methods concerning colors/colored elements. + */ +public class Colors { + + /** + * Prevents undesired instantiation. + */ + private Colors() { + // Do nada. + } + + public static final List defaultColors() { + List colors = new ArrayList<>(); + colors.add(new Color(255, 0, 0)); // Rot + colors.add(new Color(0, 0, 255)); // Blau + colors.add(new Color(0, 255, 255)); // Cyan + colors.add(new Color(255, 255, 0)); // Gelb + colors.add(new Color(255, 0, 255)); // Magenta + colors.add(new Color(153, 0, 204)); // Lila + colors.add(new Color(255, 102, 0)); // Orange + colors.add(new Color(204, 204, 255)); // Taubenblau + colors.add(new Color(153, 153, 255)); // Pastelbalu + colors.add(new Color(0, 51, 153)); // Marineblau + colors.add(new Color(51, 204, 102)); // Hellgrün + colors.add(new Color(0, 102, 51)); // Waldgrün + colors.add(new Color(102, 255, 204)); // Türkis + colors.add(new Color(255, 204, 0)); // Dunkelgelb + colors.add(new Color(255, 153, 255)); // Hellviolett + colors.add(new Color(255, 102, 102)); // Tropischrosa + + return colors; + } + + /** + * Returns a (preferredly unused) color for a new block. + * + * @param blocks The existing blocks. + * @return The color to be used. + */ + public static Color unusedBlockColor(List blocks) { + requireNonNull(blocks, "blocks"); + + List colors = defaultColors(); + + List usedColors = new ArrayList<>(); + for (BlockModel block : blocks) { + usedColors.add(block.getPropertyColor().getColor()); + } + for (Color color : colors) { + if (!usedColors.contains(color)) { + return color; + } + } + + return colors.get(0); + } + + /** + * Returns a (preferredly unused) color for a new vehicle. + * + * @param vehicles The existing vehicles. + * @return The color to be used. + */ + public static Color unusedVehicleColor(List vehicles) { + requireNonNull(vehicles, "vehicles"); + + List colors = defaultColors(); + + List usedColors = new ArrayList<>(); + for (VehicleModel vehicle : vehicles) { + usedColors.add(vehicle.getPropertyRouteColor().getColor()); + } + for (Color color : colors) { + if (!usedColors.contains(color)) { + return color; + } + } + + return colors.get(0); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/ElementNamingSchemeConfiguration.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/ElementNamingSchemeConfiguration.java new file mode 100644 index 0000000..5ccd2b0 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/ElementNamingSchemeConfiguration.java @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.util; + +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; + +/** + * Provides methods to configure the naming convention for model elements. + */ +@ConfigurationPrefix(ElementNamingSchemeConfiguration.PREFIX) +public interface ElementNamingSchemeConfiguration { + + /** + * This configuration's prefix. + */ + String PREFIX = "elementnamingscheme"; + + @ConfigurationEntry( + type = "String", + description = "The default prefix for a new point element.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_point_0" + ) + String pointPrefix(); + + @ConfigurationEntry( + type = "String", + description = "The numbering pattern for a new point element.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_point_1" + ) + String pointNumberPattern(); + + @ConfigurationEntry( + type = "String", + description = "The default prefix for a new path element.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "1_path_0" + ) + String pathPrefix(); + + @ConfigurationEntry( + type = "String", + description = "The numbering pattern for a new path element.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "1_path_1" + ) + String pathNumberPattern(); + + @ConfigurationEntry( + type = "String", + description = "The default prefix for a new location type element.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "2_loctype_0" + ) + String locationTypePrefix(); + + @ConfigurationEntry( + type = "String", + description = "The numbering pattern for a new location type element.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "2_loctype_1" + ) + String locationTypeNumberPattern(); + + @ConfigurationEntry( + type = "String", + description = "The default prefix for a new location element.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "3_loc_0" + ) + String locationPrefix(); + + @ConfigurationEntry( + type = "String", + description = "The numbering pattern for a new location element.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "3_loc_1" + ) + String locationNumberPattern(); + + @ConfigurationEntry( + type = "String", + description = "The default prefix for a new link element.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "4_link_0" + ) + String linkPrefix(); + + @ConfigurationEntry( + type = "String", + description = "The numbering pattern for a new link element.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "4_link_1" + ) + String linkNumberPattern(); + + @ConfigurationEntry( + type = "String", + description = "The default prefix for a new block.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "5_block_0" + ) + String blockPrefix(); + + @ConfigurationEntry( + type = "String", + description = "The numbering pattern for a new block.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "5_block_1" + ) + String blockNumberPattern(); + + @ConfigurationEntry( + type = "String", + description = "The default prefix for a new layout element.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "7_layout_0" + ) + String layoutPrefix(); + + @ConfigurationEntry( + type = "String", + description = "The numbering pattern for a new layout element.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "7_layout_1" + ) + String layoutNumberPattern(); + + @ConfigurationEntry( + type = "String", + description = "The default prefix for a new vehicle.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "8_vehicle_0" + ) + String vehiclePrefix(); + + @ConfigurationEntry( + type = "String", + description = "The numbering pattern for a new vehicle.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "8_vehicle_1" + ) + String vehicleNumberPattern(); + +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/FigureCloner.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/FigureCloner.java new file mode 100644 index 0000000..174ddfc --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/FigureCloner.java @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.util; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.awt.geom.AffineTransform; +import java.io.File; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.connector.Connector; +import org.opentcs.guing.base.model.ConnectableModelComponent; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.AbstractConnection; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingView; +import org.opentcs.guing.common.components.drawing.figures.BitmapFigure; +import org.opentcs.guing.common.components.drawing.figures.FigureConstants; +import org.opentcs.guing.common.components.drawing.figures.LabeledFigure; +import org.opentcs.guing.common.components.drawing.figures.SimpleLineConnection; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * A helper class for cloning figures. + */ +public class FigureCloner { + + /** + * The application's model manager. + */ + private final ModelManager fModelManager; + /** + * The application's drawing editor. + */ + private final OpenTCSDrawingEditor fDrawingEditor; + + /** + * Creates a new instance. + * + * @param modelManager The application's model manager. + * @param drawingEditor The application's drawing editor. + */ + @Inject + public FigureCloner(ModelManager modelManager, OpenTCSDrawingEditor drawingEditor) { + this.fModelManager = requireNonNull(modelManager, "modelManager"); + this.fDrawingEditor = requireNonNull(drawingEditor, "drawingEditor"); + } + + public List
cloneFigures(List
figuresToClone) { + requireNonNull(figuresToClone, "figuresToClone"); + + // Buffer for Links and Paths associated with the cloned Points and Locations + TreeSet bufferedConnections + = new TreeSet<>(Comparator.comparing(AbstractConnection::getName)); + // References the prototype Points and Locations to their clones + Map mClones = new HashMap<>(); + List
clonedFigures = new ArrayList<>(); + + for (Figure figure : figuresToClone) { + if (figure instanceof LabeledFigure) { + // Location or Point + ConnectableModelComponent model + = (ConnectableModelComponent) figure.get(FigureConstants.MODEL); + + if (model != null) { + bufferedConnections.addAll(model.getConnections()); + } + + LabeledFigure clonedFigure = (LabeledFigure) figure.clone(); + ModelComponent clonedModel = clonedFigure.get(FigureConstants.MODEL); + + if (model != null) { + mClones.put(model, clonedModel); + } + // Paste cloned figure to the drawing + AffineTransform tx = new AffineTransform(); + // TODO: Make the duplicate's distance configurable. + // TODO: With multiple pastes, place the inserted figure relative to + // the predecessor, not the original. + tx.translate(50, 50); + clonedFigure.transform(tx); + getActiveDrawingView().getDrawing().add(clonedFigure); + // The new tree component will be created by "figureAdded()" + clonedFigures.add(clonedFigure); + } + else if (figure instanceof BitmapFigure) { + BitmapFigure clonedFigure + = new BitmapFigure(new File(((BitmapFigure) figure).getImagePath())); + AffineTransform tx = new AffineTransform(); + // TODO: Make the duplicate's distance configurable. + // TODO: With multiple pastes, place the inserted figure relative to + // the predecessor, not the original. + tx.translate(50, 50); + clonedFigure.transform(tx); + getActiveDrawingView().addBackgroundBitmap(clonedFigure); + } + } + + for (Figure figure : figuresToClone) { + if (figure instanceof SimpleLineConnection) { + // Link or Path + SimpleLineConnection clonedFigure = (SimpleLineConnection) figure.clone(); + AbstractConnection model = (AbstractConnection) figure.get(FigureConstants.MODEL); + AbstractConnection clonedModel + = (AbstractConnection) clonedFigure.get(FigureConstants.MODEL); + + if (bufferedConnections.contains(model)) { + if (model != null) { + ModelComponent sourcePoint = model.getStartComponent(); + ModelComponent clonedSource = mClones.get(sourcePoint); + Iterator iConnectors + = fModelManager.getModel().getFigure(clonedSource).getConnectors(null).iterator(); + clonedFigure.setStartConnector(iConnectors.next()); + + ModelComponent destinationPoint = model.getEndComponent(); + ModelComponent clonedDestination = mClones.get(destinationPoint); + iConnectors = fModelManager.getModel().getFigure(clonedDestination) + .getConnectors(null).iterator(); + clonedFigure.setEndConnector(iConnectors.next()); + + clonedModel.setConnectedComponents(clonedSource, clonedDestination); + clonedModel.updateName(); + } + } + + getActiveDrawingView().getDrawing().add(clonedFigure); + // The new tree component will be created by "figureAdded()" + clonedFigures.add(clonedFigure); + } + } + + return clonedFigures; + } + + private OpenTCSDrawingView getActiveDrawingView() { + return fDrawingEditor.getActiveView(); + } + +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/I18nPlantOverviewModeling.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/I18nPlantOverviewModeling.java new file mode 100644 index 0000000..be98332 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/I18nPlantOverviewModeling.java @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.util; + +/** + * Defines application-specific constants regarding internationalization. + */ +public interface I18nPlantOverviewModeling { + + /** + * Path to the resources related to the create group dialog. + */ + String CREATEGROUP_PATH = "i18n.org.opentcs.plantoverview.modeling.dialogs.createGroup"; + /** + * Path to the resources related to dockables. + */ + String DOCKABLE_PATH = "i18n.org.opentcs.plantoverview.modeling.panels.dockable"; + /** + * Path to the resources related to layers. + */ + String LAYERS_PATH = "i18n.org.opentcs.plantoverview.modeling.panels.layers"; + /** + * Path to the resources related the menu bar. + */ + String MENU_PATH = "i18n.org.opentcs.plantoverview.modeling.mainMenu"; + /** + * Path to miscellaneous resources + */ + String MISC_PATH = "i18n.org.opentcs.plantoverview.modeling.miscellaneous"; + /** + * Path to the resources related to the system. + */ + String SYSTEM_PATH = "i18n.org.opentcs.plantoverview.modeling.system"; + /** + * Path to the resources related to toolbars. + */ + String TOOLBAR_PATH = "i18n.org.opentcs.plantoverview.modeling.toolbar"; + /** + * Path to the resources related to model validator. + */ + String VALIDATOR_PATH = "i18n.org.opentcs.plantoverview.modeling.modelValidation"; +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/ModelEditorConfiguration.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/ModelEditorConfiguration.java new file mode 100644 index 0000000..082ae37 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/ModelEditorConfiguration.java @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.util; + +import org.opentcs.components.plantoverview.LocationTheme; +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; +import org.opentcs.guing.common.exchange.ApplicationPortalProviderConfiguration; + +/** + * Provides methods to configure the Model Editor application. + */ +@ConfigurationPrefix(ModelEditorConfiguration.PREFIX) +public interface ModelEditorConfiguration + extends + ApplicationPortalProviderConfiguration { + + /** + * This configuration's prefix. + */ + String PREFIX = "modeleditor"; + + @ConfigurationEntry( + type = "String", + description = {"The plant overview application's locale, as a BCP 47 language tag.", + "Examples: 'en', 'de', 'zh'"}, + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_init_0" + ) + String locale(); + + @ConfigurationEntry( + type = "Class name", + description = { + "The name of the class to be used for the location theme.", + "Must be a class extending org.opentcs.components.plantoverview.LocationTheme" + }, + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "3_themes_0" + ) + Class locationThemeClass(); +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/TextAreaDialog.form b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/TextAreaDialog.form new file mode 100644 index 0000000..eab8559 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/TextAreaDialog.form @@ -0,0 +1,131 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/TextAreaDialog.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/TextAreaDialog.java new file mode 100644 index 0000000..05db728 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/TextAreaDialog.java @@ -0,0 +1,145 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.util; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Collection; +import javax.swing.JDialog; +import javax.swing.JOptionPane; +import javax.swing.UIManager; + +/** + * Shows a dialog with a text area and a label to describe the content of it. + */ +public class TextAreaDialog + extends + JDialog { + + private Collection contents; + + /** + * Creates new form TextAreaDialog. + * + * @param parent the parent of this dialog + * @param modal specifies whether dialog blocks user input to other top-level windows when shown + * @param description the description of the text area content + */ + @SuppressWarnings("this-escape") + public TextAreaDialog(java.awt.Component parent, boolean modal, String description) { + super(JOptionPane.getFrameForComponent(parent), modal); + initComponents(); + textAreaLabel.setText(description); + } + + /** + * Returns the content of the text area. + * + * @return the content of the text area + */ + public Collection getContent() { + return new ArrayList<>(contents); + } + + /** + * Sets the content of the text area. + * + * @param contents the content of the text area + */ + public void setContent(Collection contents) { + this.contents = requireNonNull(contents, "contents"); + + contentTextArea.setText(""); + contents.stream().map(o -> o + "\n").forEach(contentTextArea::append); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + contentPane = new javax.swing.JPanel(); + contentScrollPane = new javax.swing.JScrollPane(); + contentTextArea = new javax.swing.JTextArea(); + textAreaLabel = new javax.swing.JLabel(); + buttonPane = new javax.swing.JPanel(); + okButton = new javax.swing.JButton(); + symbolPane = new javax.swing.JPanel(); + symbolLabel = new javax.swing.JLabel(); + + setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); + setResizable(false); + + contentPane.setPreferredSize(new java.awt.Dimension(300, 150)); + contentPane.setLayout(new java.awt.BorderLayout()); + + contentTextArea.setEditable(false); + contentTextArea.setColumns(20); + contentTextArea.setRows(5); + contentScrollPane.setViewportView(contentTextArea); + + contentPane.add(contentScrollPane, java.awt.BorderLayout.CENTER); + + textAreaLabel.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + textAreaLabel.setText(" "); + contentPane.add(textAreaLabel, java.awt.BorderLayout.NORTH); + + getContentPane().add(contentPane, java.awt.BorderLayout.CENTER); + + buttonPane.setLayout(new java.awt.GridBagLayout()); + + okButton.setText("OK"); + okButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + okButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + buttonPane.add(okButton, gridBagConstraints); + + getContentPane().add(buttonPane, java.awt.BorderLayout.SOUTH); + + symbolPane.setPreferredSize(new java.awt.Dimension(100, 100)); + symbolPane.setLayout(new java.awt.GridBagLayout()); + + symbolLabel.setIcon(UIManager.getIcon("OptionPane.errorIcon")); + symbolLabel.setPreferredSize(new java.awt.Dimension(32, 32)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + symbolPane.add(symbolLabel, gridBagConstraints); + + getContentPane().add(symbolPane, java.awt.BorderLayout.WEST); + + pack(); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void okButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_okButtonActionPerformed + setVisible(false); + }//GEN-LAST:event_okButtonActionPerformed + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel buttonPane; + private javax.swing.JPanel contentPane; + private javax.swing.JScrollPane contentScrollPane; + private javax.swing.JTextArea contentTextArea; + private javax.swing.JButton okButton; + private javax.swing.JLabel symbolLabel; + private javax.swing.JPanel symbolPane; + private javax.swing.JLabel textAreaLabel; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/UniqueNameGenerator.java b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/UniqueNameGenerator.java new file mode 100644 index 0000000..5bb5b9b --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/modeleditor/util/UniqueNameGenerator.java @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.util; + +import jakarta.inject.Inject; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.util.UniqueStringGenerator; + +/** + */ +public class UniqueNameGenerator + extends + UniqueStringGenerator> { + + @Inject + @SuppressWarnings("this-escape") + public UniqueNameGenerator(ElementNamingSchemeConfiguration config) { + registerNamePattern(PointModel.class, config.pointPrefix(), config.pointNumberPattern()); + registerNamePattern(PathModel.class, config.pathPrefix(), config.pathNumberPattern()); + registerNamePattern( + LocationTypeModel.class, config.locationTypePrefix(), config.locationTypeNumberPattern() + ); + registerNamePattern( + LocationModel.class, config.locationPrefix(), config.locationNumberPattern() + ); + registerNamePattern(LinkModel.class, config.linkPrefix(), config.linkNumberPattern()); + registerNamePattern(BlockModel.class, config.blockPrefix(), config.blockNumberPattern()); + registerNamePattern(LayoutModel.class, config.layoutPrefix(), config.layoutNumberPattern()); + registerNamePattern(VehicleModel.class, config.vehiclePrefix(), config.vehicleNumberPattern()); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/OpenTCSSDIApplication.java b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/OpenTCSSDIApplication.java new file mode 100644 index 0000000..aba1934 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/OpenTCSSDIApplication.java @@ -0,0 +1,257 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.modeleditor.jhotdraw.application; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.common.PortalManager.ConnectionState.CONNECTED; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import java.awt.Frame; +import java.awt.event.ActionEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.ResourceBundle; +import javax.swing.JFrame; +import org.jhotdraw.app.SDIApplication; +import org.jhotdraw.app.View; +import org.opentcs.common.PortalManager; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.guing.common.event.ModelNameChangeEvent; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.modeleditor.application.OpenTCSView; +import org.opentcs.modeleditor.application.menus.menubar.ApplicationMenuBar; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.file.CloseFileAction; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.event.EventSource; +import org.opentcs.util.gui.Icons; + +/** + * The enclosing SDI application. + */ +public class OpenTCSSDIApplication + extends + SDIApplication + implements + EventHandler { + + private static final ResourceBundle BUNDLE + = ResourceBundle.getBundle(I18nPlantOverviewModeling.SYSTEM_PATH); + /** + * Whether the GUI window should be maximized on startup. + */ + private static final boolean FRAME_MAXIMIZED = false; + /** + * The GUI window's x-coordinate on screen in pixels. + */ + private static final int FRAME_BOUNDS_X = 0; + /** + * The GUI window's y-coordinate on screen in pixels. + */ + private static final int FRAME_BOUNDS_Y = 0; + /** + * The GUI window's height in pixels. + */ + private static final int FRAME_BOUNDS_HEIGHT = 768; + /** + * The GUI window's width in pixels. + */ + private static final int FRAME_BOUNDS_WIDTH = 1024; + /** + * The JFrame in which the OpenTCSView is shown. + */ + private final JFrame contentFrame; + /** + * A provider for the menu bar. + */ + private final Provider menuBarProvider; + /** + * Provides the current system model. + */ + private final ModelManager modelManager; + /** + * Where we register for application events. + */ + private final EventSource eventSource; + /** + * The portal manager. + */ + private final PortalManager portalManager; + + /** + * Creates a new instance. + * + * @param frame The frame in which the OpenTCSView is to be shown. + * @param menuBarProvider Provides the main application menu bar. + * @param modelManager Provides the current system model. + * @param eventSource Where this instance registers for application events. + * @param portalManager The portal manager. + */ + @Inject + public OpenTCSSDIApplication(@ApplicationFrame + JFrame frame, + Provider menuBarProvider, + ModelManager modelManager, + @ApplicationEventBus + EventSource eventSource, + PortalManager portalManager + ) { + this.contentFrame = requireNonNull(frame, "frame"); + this.menuBarProvider = requireNonNull(menuBarProvider, "menuBarProvider"); + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.eventSource = requireNonNull(eventSource, "eventSource"); + this.portalManager = requireNonNull(portalManager, "portalManager"); + } + + @Override + public void show(final View view) { + requireNonNull(view, "view"); + + if (view.isShowing()) { + return; + } + view.setShowing(true); + + eventSource.subscribe(this); + + final OpenTCSView opentcsView = (OpenTCSView) view; + + setupContentFrame(opentcsView); + + TitleUpdater titleUpdater = new TitleUpdater(opentcsView); + opentcsView.addPropertyChangeListener(titleUpdater); + eventSource.subscribe(titleUpdater); + + updateViewTitle(view, contentFrame); + + // The frame should be shown only after the view has been initialized. + opentcsView.start(); + contentFrame.setVisible(true); + } + + @Override + protected void updateViewTitle(View view, JFrame frame) { + ((OpenTCSView) view).updateModelName(); + } + + @Override + public void onEvent(Object event) { + if (event instanceof ModelNameChangeEvent) { + ModelNameChangeEvent modelNameChangeEvent = (ModelNameChangeEvent) event; + updateViewTitle((OpenTCSView) modelNameChangeEvent.getSource(), contentFrame); + } + } + + private void updateViewTitle(OpenTCSView view, JFrame frame) { + String modelName = modelManager.getModel().getName(); + if (view.hasUnsavedChanges()) { + modelName += "*"; + } + + if (frame != null) { + frame.setTitle( + OpenTCSView.NAME + " - \"" + + modelName + "\" - " + + BUNDLE.getString("openTcsSdiApplication.frameTitle_connectedTo.text") + + portalManager.getDescription() + + " (" + portalManager.getHost() + ":" + portalManager.getPort() + ")" + ); + } + } + + private void setupContentFrame(OpenTCSView opentcsView) { + contentFrame.setJMenuBar(menuBarProvider.get()); + + contentFrame.setIconImages(Icons.getOpenTCSIcons()); + contentFrame.setSize(1024, 768); + + // Restore the window's dimensions from the configuration. + contentFrame.setExtendedState(FRAME_MAXIMIZED ? Frame.MAXIMIZED_BOTH : Frame.NORMAL); + + if (contentFrame.getExtendedState() != Frame.MAXIMIZED_BOTH) { + contentFrame.setBounds( + FRAME_BOUNDS_X, + FRAME_BOUNDS_Y, + FRAME_BOUNDS_WIDTH, + FRAME_BOUNDS_HEIGHT + ); + } + + contentFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + contentFrame.addWindowListener(new WindowStatusUpdater(opentcsView)); + } + + private class TitleUpdater + implements + PropertyChangeListener, + EventHandler { + + private final OpenTCSView opentcsView; + + TitleUpdater(OpenTCSView opentcsView) { + this.opentcsView = requireNonNull(opentcsView, "opentcsView"); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + String name = evt.getPropertyName(); + + if (name.equals(View.HAS_UNSAVED_CHANGES_PROPERTY)) { + updateViewTitle(opentcsView, contentFrame); + } + } + + @Override + public void onEvent(Object event) { + if (event instanceof PortalManager.ConnectionState) { + PortalManager.ConnectionState connectionState = (PortalManager.ConnectionState) event; + switch (connectionState) { + case CONNECTED: + break; + case DISCONNECTED: + break; + default: + } + updateViewTitle(opentcsView, contentFrame); + } + } + } + + private class WindowStatusUpdater + extends + WindowAdapter { + + private final OpenTCSView opentcsView; + + WindowStatusUpdater(OpenTCSView opentcsView) { + this.opentcsView = requireNonNull(opentcsView, "opentcsView"); + } + + @Override + public void windowClosing(WindowEvent e) { + // Check if changes to the model still need to be saved. + getAction(opentcsView, CloseFileAction.ID).actionPerformed( + new ActionEvent( + contentFrame, + ActionEvent.ACTION_PERFORMED, + CloseFileAction.ID_WINDOW_CLOSING + ) + ); + } + + @Override + public void windowClosed(WindowEvent e) { + opentcsView.stop(); + } + + @Override + public void windowGainedFocus(WindowEvent e) { + setActiveView(opentcsView); + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/ButtonFactory.java b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/ButtonFactory.java new file mode 100644 index 0000000..b26f2ab --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/ButtonFactory.java @@ -0,0 +1,880 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.modeleditor.jhotdraw.application.action; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Rectangle; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.swing.AbstractAction; +import javax.swing.AbstractButton; +import javax.swing.Action; +import javax.swing.Icon; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JToolBar; +import javax.swing.text.StyledEditorKit; +import org.jhotdraw.app.action.ActionUtil; +import org.jhotdraw.draw.AttributeKey; +import org.jhotdraw.draw.AttributeKeys; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.action.AttributeToggler; +import org.jhotdraw.draw.action.EditorColorIcon; +import org.jhotdraw.draw.action.FontChooserHandler; +import org.jhotdraw.draw.action.LineDecorationIcon; +import org.jhotdraw.draw.action.StrokeIcon; +import org.jhotdraw.draw.decoration.ArrowTip; +import org.jhotdraw.draw.decoration.LineDecoration; +import org.jhotdraw.geom.DoubleStroke; +import org.jhotdraw.gui.JComponentPopup; +import org.jhotdraw.gui.JFontChooser; +import org.jhotdraw.gui.JPopupButton; +import org.jhotdraw.util.Images; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.draw.MoveAction; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.draw.AlignAction; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.draw.ApplyAttributesAction; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.draw.AttributeAction; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.draw.BringToFrontAction; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.draw.ColorIcon; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.draw.DefaultAttributeAction; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.draw.EditorColorChooserAction; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.draw.PickAttributesAction; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.draw.SendToBackAction; + +/** + * ButtonFactory. + *

+ * Design pattern:
Name: Abstract Factory.
Role: + * Abstract Factory.
Partners: {@link org.jhotdraw.samples.draw.DrawApplicationModel} + * as Client, + * {@link org.jhotdraw.samples.draw.DrawView} as Client, + * {@link org.jhotdraw.samples.draw.DrawingPanel} as Client. + * + * FIXME - All buttons created using the ButtonFactory must automatically become + * disabled/enabled, when the DrawingEditor is disabled/enabled. + * + * @author Werner Randelshofer + */ +public class ButtonFactory { + + /** + * Mac OS X 'Apple Color Palette'. This palette has 8 columns. + */ + private static final List DEFAULT_COLOR_ICONS; + private static final int DEFAULT_COLORS_COLUMN_COUNT = 8; + private static final ResourceBundleUtil BUNDLE + = ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.TOOLBAR_PATH); + + static { + List m = new ArrayList<>(); + m.add(new ColorIcon(0x800000, "Cayenne")); + m.add(new ColorIcon(0x808000, "Asparagus")); + m.add(new ColorIcon(0x008000, "Clover")); + m.add(new ColorIcon(0x008080, "Teal")); + m.add(new ColorIcon(0x000080, "Midnight")); + m.add(new ColorIcon(0x800080, "Plum")); + m.add(new ColorIcon(0x7f7f7f, "Tin")); + m.add(new ColorIcon(0x808080, "Nickel")); + m.add(new ColorIcon(0xff0000, "Maraschino")); + m.add(new ColorIcon(0xffff00, "Lemon")); + m.add(new ColorIcon(0x00ff00, "Spring")); + m.add(new ColorIcon(0x00ffff, "Turquoise")); + m.add(new ColorIcon(0x0000ff, "Blueberry")); + m.add(new ColorIcon(0xff00ff, "Magenta")); + m.add(new ColorIcon(0x666666, "Steel")); + m.add(new ColorIcon(0x999999, "Aluminium")); + m.add(new ColorIcon(0xff6666, "Salmon")); + m.add(new ColorIcon(0xffff66, "Banana")); + m.add(new ColorIcon(0x66ff66, "Flora")); + m.add(new ColorIcon(0x66ffff, "Ice")); + m.add(new ColorIcon(0x6666ff, "Orchid")); + m.add(new ColorIcon(0xff66ff, "Bubblegum")); + m.add(new ColorIcon(0x4c4c4c, "Iron")); + m.add(new ColorIcon(0xb3b3b3, "Magnesium")); + m.add(new ColorIcon(0x804000, "Mocha")); + m.add(new ColorIcon(0x408000, "Fern")); + m.add(new ColorIcon(0x008040, "Moss")); + m.add(new ColorIcon(0x004080, "Ocean")); + m.add(new ColorIcon(0x400080, "Eggplant")); + m.add(new ColorIcon(0x800040, "Maroon")); + m.add(new ColorIcon(0x333333, "Tungsten")); + m.add(new ColorIcon(0xcccccc, "Silver")); + m.add(new ColorIcon(0xff8000, "Tangerine")); + m.add(new ColorIcon(0x80ff00, "Lime")); + m.add(new ColorIcon(0x00ff80, "Sea Foam")); + m.add(new ColorIcon(0x0080ff, "Aqua")); + m.add(new ColorIcon(0x8000ff, "Grape")); + m.add(new ColorIcon(0xff0080, "Strawberry")); + m.add(new ColorIcon(0x191919, "Lead")); + m.add(new ColorIcon(0xe6e6e6, "Mercury")); + m.add(new ColorIcon(0xffcc66, "Cantaloupe")); + m.add(new ColorIcon(0xccff66, "Honeydew")); + m.add(new ColorIcon(0x66ffcc, "Spindrift")); + m.add(new ColorIcon(0x66ccff, "Sky")); + m.add(new ColorIcon(0xcc66ff, "Lavender")); + m.add(new ColorIcon(0xff6fcf, "Carnation")); + m.add(new ColorIcon(0x000000, "Licorice")); + m.add(new ColorIcon(0xffffff, "Snow")); + DEFAULT_COLOR_ICONS = Collections.unmodifiableList(m); + } + + /** + * Prevent instance creation. + */ + private ButtonFactory() { + } + + /** + * Creates toolbar buttons and adds them to the specified JToolBar. + * + * @param toolBar The toolbar. + * @param editor The drawing editor. + */ + public static void addAttributesButtonsTo( + JToolBar toolBar, + DrawingEditor editor + ) { + + JButton button; + + button = toolBar.add(new PickAttributesAction(editor)); + button.setFocusable(false); + + button = toolBar.add(new ApplyAttributesAction(editor)); + button.setFocusable(false); + + toolBar.addSeparator(); + + addColorButtonsTo(toolBar, editor); + + toolBar.addSeparator(); + + addStrokeButtonsTo(toolBar, editor); + + toolBar.addSeparator(); + + addFontButtonsTo(toolBar, editor); + } + + private static void addColorButtonsTo(JToolBar bar, DrawingEditor editor) { + bar.add( + createEditorColorButton( + editor, + AttributeKeys.STROKE_COLOR, + BUNDLE.getString("buttonFactory.button_lineColor.tooltipText"), + ImageDirectory.getImageIcon("/toolbar/attributeStrokeColor.png"), + new HashMap() + ) + ); + bar.add( + createEditorColorButton( + editor, + AttributeKeys.FILL_COLOR, + BUNDLE.getString("buttonFactory.button_fillColor.tooltipText"), + ImageDirectory.getImageIcon("/toolbar/attributeFillColor.png"), + new HashMap() + ) + ); + bar.add( + createEditorColorButton( + editor, + AttributeKeys.TEXT_COLOR, + BUNDLE.getString("buttonFactory.button_textlColor.tooltipText"), + ImageDirectory.getImageIcon("/toolbar/attributeTextColor.png"), + new HashMap() + ) + ); + } + + /** + * Creates a color button, with an action region and a popup menu. The button + * works like the color button in Microsoft Office:

  • When the user + * clicks on the action region, the default color of the DrawingEditor is + * applied to the selected figures.
  • When the user opens the popup + * menu, a color palette is displayed. Choosing a color from the palette + * changes the default color of the editor and also changes the color of the + * selected figures.
  • A shape on the color button displays the current + * default color of the DrawingEditor.
+ * + * @param editor The DrawingEditor. + * @param attributeKey The AttributeKey of the color. + * @param toolTipText The tooltip text. + * @param defaultAttributes A set of attributes which are also applied to the + * selected figures, when a color is selected. This can be used, to set + * attributes that otherwise prevent the color from being shown. For example, + * when the color attribute is set, we wan't the gradient attribute of the + * Figure to be cleared. + * @return + */ + private static JPopupButton createEditorColorButton( + DrawingEditor editor, + AttributeKey attributeKey, + String toolTipText, + ImageIcon baseIcon, + Map defaultAttributes + ) { + final JPopupButton popupButton = new JPopupButton(); + popupButton.setPopupAlpha(1f); + + popupButton.setAction( + new DefaultAttributeAction(editor, attributeKey, defaultAttributes), + new Rectangle(0, 0, 22, 22) + ); + popupButton.setColumnCount(DEFAULT_COLORS_COLUMN_COUNT, false); + boolean hasNullColor = false; + + for (ColorIcon swatch : DEFAULT_COLOR_ICONS) { + HashMap attributes = new HashMap<>(defaultAttributes); + Color swatchColor = swatch.getColor(); + attributes.put(attributeKey, swatchColor); + + if (swatchColor == null || swatchColor.getAlpha() == 0) { + hasNullColor = true; + } + + AttributeAction action = new AttributeAction( + editor, + attributes, + toolTipText, + swatch + ); + popupButton.add(action); + action.putValue(Action.SHORT_DESCRIPTION, swatch.getName()); + action.setUpdateEnabledState(false); + } + + // No color + if (!hasNullColor) { + HashMap attributes = new HashMap<>(defaultAttributes); + attributes.put(attributeKey, null); + AttributeAction action + = new AttributeAction( + editor, + attributes, + BUNDLE.getString("buttonFactory.action_noColor.name"), + new ColorIcon( + null, + "", + DEFAULT_COLOR_ICONS.get(0).getIconWidth(), + DEFAULT_COLOR_ICONS.get(0).getIconHeight() + ) + ); + popupButton.add(action); + action.putValue( + Action.SHORT_DESCRIPTION, + BUNDLE.getString("buttonFactory.action_noColor.shortDescription") + ); + action.setUpdateEnabledState(false); + } + + // Color chooser + ImageIcon chooserIcon = new ImageIcon( + Images.createImage( + ButtonFactory.class, + "/org/jhotdraw/draw/action/images/attribute.color.colorChooser.png" + ) + ); + Action action = new EditorColorChooserAction( + editor, + attributeKey, + "color", + chooserIcon, + defaultAttributes + ); + popupButton.add(action); + popupButton.setText(null); + popupButton.setToolTipText(toolTipText); + + Icon icon = new EditorColorIcon( + editor, + attributeKey, + baseIcon.getImage(), + new Rectangle(1, 17, 20, 4) + ); + + popupButton.setIcon(icon); + popupButton.setDisabledIcon(icon); + popupButton.setFocusable(false); + + editor.addPropertyChangeListener(new PropertyChangeListener() { + + @Override + public void propertyChange(PropertyChangeEvent evt) { + popupButton.repaint(); + } + }); + + return popupButton; + } + + private static void addStrokeButtonsTo(JToolBar bar, DrawingEditor editor) { + bar.add(createStrokeDecorationButton(editor)); + bar.add(createStrokeWidthButton(editor)); + bar.add(createStrokeDashesButton(editor)); + bar.add(createStrokeTypeButton(editor)); + bar.add(createStrokePlacementButton(editor)); + bar.add(createStrokeCapButton(editor)); + bar.add(createStrokeJoinButton(editor)); + } + + private static JPopupButton createStrokeDecorationButton(DrawingEditor editor) { + JPopupButton strokeDecorationPopupButton = new JPopupButton(); + + strokeDecorationPopupButton.setIcon( + ImageDirectory.getImageIcon("/toolbar/attributeStrokeDecoration.png") + ); + + strokeDecorationPopupButton.setText(null); + strokeDecorationPopupButton.setToolTipText( + BUNDLE.getString("buttonFactory.button_strokeDecoration.tooltipText") + ); + + strokeDecorationPopupButton.setFocusable(false); + strokeDecorationPopupButton.setColumnCount(2, false); + LineDecoration[] decorations = { + // Arrow + new ArrowTip(0.35, 12, 11.3), + // Arrow + new ArrowTip(0.35, 13, 7), + // Generalization triangle + new ArrowTip(Math.PI / 5, 12, 9.8, true, true, false), + // Dependency arrow + new ArrowTip(Math.PI / 6, 12, 0, false, true, false), + // Link arrow + new ArrowTip(Math.PI / 11, 13, 0, false, true, true), + // Aggregation diamond + new ArrowTip(Math.PI / 6, 10, 18, false, true, false), + // Composition diamond + new ArrowTip(Math.PI / 6, 10, 18, true, true, true), + null + }; + + for (LineDecoration decoration : decorations) { + strokeDecorationPopupButton.add( + new AttributeAction( + editor, + AttributeKeys.START_DECORATION, + decoration, + null, + new LineDecorationIcon(decoration, true) + ) + ); + strokeDecorationPopupButton.add( + new AttributeAction( + editor, + AttributeKeys.END_DECORATION, + decoration, + null, + new LineDecorationIcon(decoration, false) + ) + ); + } + + return strokeDecorationPopupButton; + } + + private static JPopupButton createStrokeWidthButton(DrawingEditor editor) { + JPopupButton strokeWidthPopupButton = new JPopupButton(); + strokeWidthPopupButton.setIcon( + ImageDirectory.getImageIcon("/toolbar/attributeStrokeWidth.png") + ); + strokeWidthPopupButton.setText(null); + strokeWidthPopupButton.setToolTipText( + BUNDLE.getString("buttonFactory.button_strokeWidth.tooltipText") + ); + strokeWidthPopupButton.setFocusable(false); + + NumberFormat formatter = NumberFormat.getInstance(); + + if (formatter instanceof DecimalFormat) { + ((DecimalFormat) formatter).setMaximumFractionDigits(1); + ((DecimalFormat) formatter).setMinimumFractionDigits(0); + } + + double[] widths = new double[]{0.5d, 1d, 2d, 3d, 5d, 9d, 13d}; + + for (int i = 0; i < widths.length; i++) { + String label = Double.toString(widths[i]); + Icon icon = new StrokeIcon( + new BasicStroke( + (float) widths[i], + BasicStroke.CAP_BUTT, + BasicStroke.JOIN_BEVEL + ) + ); + AttributeAction action = new AttributeAction( + editor, AttributeKeys.STROKE_WIDTH, widths[i], label, icon + ); + action.putValue( + ActionUtil.UNDO_PRESENTATION_NAME_KEY, + BUNDLE.getString("buttonFactory.action_strokeWidth.undo.presentationName") + ); + AbstractButton btn = strokeWidthPopupButton.add(action); + btn.setDisabledIcon(icon); + } + + return strokeWidthPopupButton; + } + + private static JPopupButton createStrokeDashesButton(DrawingEditor editor) { + JPopupButton strokeDashesPopupButton = new JPopupButton(); + strokeDashesPopupButton.setIcon( + ImageDirectory.getImageIcon("/toolbar/attributeStrokeDashes.png") + ); + strokeDashesPopupButton.setText(null); + strokeDashesPopupButton.setToolTipText( + BUNDLE.getString("buttonFactory.button_strokeDashes.tooltipText") + ); + strokeDashesPopupButton.setFocusable(false); + + double[][] dashes = new double[][]{ + null, + {4d, 4d}, + {2d, 2d}, + {4d, 2d}, + {2d, 4d}, + {8d, 2d}, + {6d, 2d, 2d, 2d},}; + + for (double[] dash : dashes) { + float[] fdashes; + + if (dash == null) { + fdashes = null; + } + else { + fdashes = new float[dash.length]; + + for (int j = 0; j < dash.length; j++) { + fdashes[j] = (float) dash[j]; + } + } + + Icon icon = new StrokeIcon( + new BasicStroke( + 2f, + BasicStroke.CAP_BUTT, + BasicStroke.JOIN_BEVEL, + 10f, + fdashes, + 0 + ) + ); + AbstractButton button = strokeDashesPopupButton.add( + new AttributeAction(editor, AttributeKeys.STROKE_DASHES, dash, null, icon) + ); + button.setDisabledIcon(icon); + } + + return strokeDashesPopupButton; + } + + private static JPopupButton createStrokeTypeButton(DrawingEditor editor) { + JPopupButton strokeTypePopupButton = new JPopupButton(); + strokeTypePopupButton.setIcon( + ImageDirectory.getImageIcon("/toolbar/attributeStrokeType.png") + ); + strokeTypePopupButton.setText(null); + strokeTypePopupButton.setToolTipText( + BUNDLE.getString("buttonFactory.button_strokeType.tooltipText") + ); + strokeTypePopupButton.setFocusable(false); + + strokeTypePopupButton.add( + new AttributeAction( + editor, + AttributeKeys.STROKE_TYPE, + AttributeKeys.StrokeType.BASIC, + BUNDLE.getString("buttonFactory.action_strokeTypeBasic.name"), + new StrokeIcon( + new BasicStroke( + 1, + BasicStroke.CAP_BUTT, + BasicStroke.JOIN_BEVEL + ) + ) + ) + ); + HashMap attributes = new HashMap<>(); + attributes.put(AttributeKeys.STROKE_TYPE, AttributeKeys.StrokeType.DOUBLE); + attributes.put(AttributeKeys.STROKE_INNER_WIDTH_FACTOR, 2d); + strokeTypePopupButton.add( + new AttributeAction( + editor, + attributes, + BUNDLE.getString("buttonFactory.action_strokeTypeDouble.name"), + new StrokeIcon(new DoubleStroke(2, 1)) + ) + ); + attributes = new HashMap<>(); + attributes.put(AttributeKeys.STROKE_TYPE, AttributeKeys.StrokeType.DOUBLE); + attributes.put(AttributeKeys.STROKE_INNER_WIDTH_FACTOR, 3d); + strokeTypePopupButton.add( + new AttributeAction( + editor, + attributes, + BUNDLE.getString("buttonFactory.action_strokeTypeDouble.name"), + new StrokeIcon(new DoubleStroke(3, 1)) + ) + ); + attributes = new HashMap<>(); + attributes.put(AttributeKeys.STROKE_TYPE, AttributeKeys.StrokeType.DOUBLE); + attributes.put(AttributeKeys.STROKE_INNER_WIDTH_FACTOR, 4d); + strokeTypePopupButton.add( + new AttributeAction( + editor, + attributes, + BUNDLE.getString("buttonFactory.action_strokeTypeDouble.name"), + new StrokeIcon(new DoubleStroke(4, 1)) + ) + ); + + return strokeTypePopupButton; + } + + private static JPopupButton createStrokePlacementButton(DrawingEditor editor) { + JPopupButton strokePlacementPopupButton = new JPopupButton(); + strokePlacementPopupButton.setIcon( + ImageDirectory.getImageIcon("/toolbar/attributeStrokePlacement.png") + ); + strokePlacementPopupButton.setText(null); + strokePlacementPopupButton.setToolTipText( + BUNDLE.getString("buttonFactory.button_strokePlacement.tooltipText") + ); + strokePlacementPopupButton.setFocusable(false); + + HashMap attributes; + attributes = new HashMap<>(); + attributes.put(AttributeKeys.STROKE_PLACEMENT, AttributeKeys.StrokePlacement.CENTER); + attributes.put(AttributeKeys.FILL_UNDER_STROKE, AttributeKeys.Underfill.CENTER); + strokePlacementPopupButton.add( + new AttributeAction( + editor, + attributes, + BUNDLE.getString("buttonFactory.action_strokePlacementCenter.name"), + null + ) + ); + + attributes = new HashMap<>(); + attributes.put(AttributeKeys.STROKE_PLACEMENT, AttributeKeys.StrokePlacement.INSIDE); + attributes.put(AttributeKeys.FILL_UNDER_STROKE, AttributeKeys.Underfill.CENTER); + strokePlacementPopupButton.add( + new AttributeAction( + editor, + attributes, + BUNDLE.getString("buttonFactory.action_strokePlacementInside.name"), + null + ) + ); + + attributes = new HashMap<>(); + attributes.put(AttributeKeys.STROKE_PLACEMENT, AttributeKeys.StrokePlacement.OUTSIDE); + attributes.put(AttributeKeys.FILL_UNDER_STROKE, AttributeKeys.Underfill.CENTER); + strokePlacementPopupButton.add( + new AttributeAction( + editor, + attributes, + BUNDLE.getString("buttonFactory.action_strokePlacementOutside.name"), + null + ) + ); + + attributes = new HashMap<>(); + attributes.put(AttributeKeys.STROKE_PLACEMENT, AttributeKeys.StrokePlacement.CENTER); + attributes.put(AttributeKeys.FILL_UNDER_STROKE, AttributeKeys.Underfill.FULL); + strokePlacementPopupButton.add( + new AttributeAction( + editor, + attributes, + BUNDLE.getString("buttonFactory.action_strokePlacementCenterFilled.name"), + null + ) + ); + + attributes = new HashMap<>(); + attributes.put(AttributeKeys.STROKE_PLACEMENT, AttributeKeys.StrokePlacement.INSIDE); + attributes.put(AttributeKeys.FILL_UNDER_STROKE, AttributeKeys.Underfill.FULL); + strokePlacementPopupButton.add( + new AttributeAction( + editor, + attributes, + BUNDLE.getString("buttonFactory.action_strokePlacementInsideFilled.name"), + null + ) + ); + + attributes = new HashMap<>(); + attributes.put(AttributeKeys.STROKE_PLACEMENT, AttributeKeys.StrokePlacement.OUTSIDE); + attributes.put(AttributeKeys.FILL_UNDER_STROKE, AttributeKeys.Underfill.FULL); + strokePlacementPopupButton.add( + new AttributeAction( + editor, + attributes, + BUNDLE.getString("buttonFactory.action_strokePlacementOutsideFilled.name"), + null + ) + ); + + attributes = new HashMap<>(); + attributes.put(AttributeKeys.STROKE_PLACEMENT, AttributeKeys.StrokePlacement.CENTER); + attributes.put(AttributeKeys.FILL_UNDER_STROKE, AttributeKeys.Underfill.NONE); + strokePlacementPopupButton.add( + new AttributeAction( + editor, + attributes, + BUNDLE.getString("buttonFactory.action_strokePlacementCenterUnfilled.name"), + null + ) + ); + + attributes = new HashMap<>(); + attributes.put(AttributeKeys.STROKE_PLACEMENT, AttributeKeys.StrokePlacement.INSIDE); + attributes.put(AttributeKeys.FILL_UNDER_STROKE, AttributeKeys.Underfill.NONE); + strokePlacementPopupButton.add( + new AttributeAction( + editor, + attributes, + BUNDLE.getString("buttonFactory.action_strokePlacementInsideUnfilled.name"), + null + ) + ); + + attributes = new HashMap<>(); + attributes.put(AttributeKeys.STROKE_PLACEMENT, AttributeKeys.StrokePlacement.OUTSIDE); + attributes.put(AttributeKeys.FILL_UNDER_STROKE, AttributeKeys.Underfill.NONE); + strokePlacementPopupButton.add( + new AttributeAction( + editor, + attributes, + BUNDLE.getString("buttonFactory.action_strokePlacementOutsideUnfilled.name"), + null + ) + ); + + return strokePlacementPopupButton; + } + + private static JPopupButton createStrokeCapButton(DrawingEditor editor) { + JPopupButton popupButton = new JPopupButton(); + popupButton.setIcon(ImageDirectory.getImageIcon("/toolbar/attributeStrokeCap.png")); + popupButton.setText(null); + popupButton.setToolTipText(BUNDLE.getString("buttonFactory.button_strokeCap.tooltipText")); + popupButton.setFocusable(false); + + HashMap attributes; + + attributes = new HashMap<>(); + attributes.put(AttributeKeys.STROKE_CAP, BasicStroke.CAP_BUTT); + AttributeAction action; + action = new AttributeAction( + editor, + attributes, + BUNDLE.getString("buttonFactory.action_strokeCapButt.name"), + null + ); + popupButton.add(action); + + attributes = new HashMap<>(); + attributes.put(AttributeKeys.STROKE_CAP, BasicStroke.CAP_ROUND); + action = new AttributeAction( + editor, + attributes, + BUNDLE.getString("buttonFactory.action_strokeCapRound.name"), + null + ); + popupButton.add(action); + + attributes = new HashMap<>(); + attributes.put(AttributeKeys.STROKE_CAP, BasicStroke.CAP_SQUARE); + action = new AttributeAction( + editor, + attributes, + BUNDLE.getString("buttonFactory.action_strokeCapSquare.name"), + null + ); + popupButton.add(action); + + return popupButton; + } + + private static JPopupButton createStrokeJoinButton(DrawingEditor editor) { + JPopupButton popupButton = new JPopupButton(); + popupButton.setIcon(ImageDirectory.getImageIcon("/toolbar/attributeStrokeJoin.png")); + popupButton.setText(null); + popupButton.setToolTipText(BUNDLE.getString("buttonFactory.button_strokeJoin.tooltipText")); + popupButton.setFocusable(false); + + HashMap attributes; + + attributes = new HashMap<>(); + attributes.put(AttributeKeys.STROKE_JOIN, BasicStroke.JOIN_BEVEL); + AttributeAction action; + action = new AttributeAction( + editor, + attributes, + BUNDLE.getString("buttonFactory.action_strokeJoinBevel.name"), + null + ); + popupButton.add(action); + + attributes = new HashMap<>(); + attributes.put(AttributeKeys.STROKE_JOIN, BasicStroke.JOIN_ROUND); + action = new AttributeAction( + editor, + attributes, + BUNDLE.getString("buttonFactory.action_strokeJoinRound.name"), + null + ); + popupButton.add(action); + + attributes = new HashMap<>(); + attributes.put(AttributeKeys.STROKE_JOIN, BasicStroke.JOIN_MITER); + action = new AttributeAction( + editor, + attributes, + BUNDLE.getString("buttonFactory.action_strokeJoinMiter.name"), + null + ); + popupButton.add(action); + + return popupButton; + } + + private static void addFontButtonsTo(JToolBar bar, DrawingEditor editor) { + bar.add(createFontButton(editor)); + bar.add(createFontStyleBoldButton(editor)); + bar.add(createFontStyleItalicButton(editor)); + bar.add(createFontStyleUnderlineButton(editor)); + } + + private static JPopupButton createFontButton(DrawingEditor editor) { + JPopupButton fontPopupButton = new JPopupButton(); + fontPopupButton.setIcon(ImageDirectory.getImageIcon("/toolbar/attributeFont.png")); + fontPopupButton.setText(null); + fontPopupButton.setToolTipText(BUNDLE.getString("buttonFactory.button_font.tooltipText")); + fontPopupButton.setFocusable(false); + + JComponentPopup popupMenu = new JComponentPopup(); + JFontChooser fontChooser = new JFontChooser(); + + new FontChooserHandler(editor, AttributeKeys.FONT_FACE, fontChooser, popupMenu); + + popupMenu.add(fontChooser); + fontPopupButton.setPopupMenu(popupMenu); + fontPopupButton.setFocusable(false); + + return fontPopupButton; + } + + private static JButton createFontStyleBoldButton(DrawingEditor editor) { + JButton button = new JButton(); + button.setIcon(ImageDirectory.getImageIcon("/toolbar/attributeFontBold.png")); + button.setText(null); + button.setToolTipText(BUNDLE.getString("buttonFactory.button_fontStyleBold.tooltipText")); + button.setFocusable(false); + + AbstractAction action + = new AttributeToggler<>( + editor, + AttributeKeys.FONT_BOLD, + Boolean.TRUE, + Boolean.FALSE, + new StyledEditorKit.BoldAction() + ); + action.putValue( + ActionUtil.UNDO_PRESENTATION_NAME_KEY, + BUNDLE.getString("buttonFactory.action_fontStyleBold.undo.presentationName") + ); + button.addActionListener(action); + + return button; + } + + private static JButton createFontStyleItalicButton(DrawingEditor editor) { + JButton button = new JButton(); + button.setIcon(ImageDirectory.getImageIcon("/toolbar/attributeFontItalic.png")); + button.setText(null); + button.setToolTipText(BUNDLE.getString("buttonFactory.button_fontStyleItalic.tooltipText")); + button.setFocusable(false); + + AbstractAction action + = new AttributeToggler<>( + editor, + AttributeKeys.FONT_ITALIC, + Boolean.TRUE, + Boolean.FALSE, + new StyledEditorKit.BoldAction() + ); + action.putValue( + ActionUtil.UNDO_PRESENTATION_NAME_KEY, + BUNDLE.getString("buttonFactory.action_fontStyleItalic.undo.presentationName") + ); + button.addActionListener(action); + + return button; + } + + private static JButton createFontStyleUnderlineButton(DrawingEditor editor) { + JButton button = new JButton(); + button.setIcon(ImageDirectory.getImageIcon("/toolbar/attributeFontUnderline.png")); + button.setText(null); + button.setToolTipText(BUNDLE.getString("buttonFactory.button_fontStyleUnderline.tooltipText")); + button.setFocusable(false); + + AbstractAction action + = new AttributeToggler<>( + editor, + AttributeKeys.FONT_UNDERLINE, + Boolean.TRUE, + Boolean.FALSE, + new StyledEditorKit.BoldAction() + ); + action.putValue( + ActionUtil.UNDO_PRESENTATION_NAME_KEY, + BUNDLE.getString("buttonFactory.action_fontStyleUnderline.undo.presentationName") + ); + button.addActionListener(action); + + return button; + } + + /** + * Creates toolbar buttons and adds them to the specified JToolBar. + * + * @param bar The toolbar. + * @param editor The drawing editor. + */ + public static void addAlignmentButtonsTo(JToolBar bar, final DrawingEditor editor) { + bar.add(new AlignAction.West(editor)).setFocusable(false); + bar.add(new AlignAction.East(editor)).setFocusable(false); + bar.add(new AlignAction.Horizontal(editor)).setFocusable(false); + bar.add(new AlignAction.North(editor)).setFocusable(false); + bar.add(new AlignAction.South(editor)).setFocusable(false); + bar.add(new AlignAction.Vertical(editor)).setFocusable(false); + + bar.addSeparator(); + + bar.add(new MoveAction.West(editor)).setFocusable(false); + bar.add(new MoveAction.East(editor)).setFocusable(false); + bar.add(new MoveAction.North(editor)).setFocusable(false); + bar.add(new MoveAction.South(editor)).setFocusable(false); + + bar.addSeparator(); + + bar.add(new BringToFrontAction(editor)).setFocusable(false); + bar.add(new SendToBackAction(editor)).setFocusable(false); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/AlignAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/AlignAction.java new file mode 100644 index 0000000..418710c --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/AlignAction.java @@ -0,0 +1,270 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.draw; + +import static javax.swing.Action.SMALL_ICON; + +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.util.Collection; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.action.AbstractSelectedAction; +import org.jhotdraw.draw.event.TransformEdit; +import org.jhotdraw.undo.CompositeEdit; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Aligns the selected figures. + * + * XXX - Fire edit events + * + * @author Werner Randelshofer + */ +public abstract class AlignAction + extends + AbstractSelectedAction { + + protected ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.TOOLBAR_PATH); + + /** + * Creates a new instance. + * + * @param editor The drawing editor + */ + @SuppressWarnings("this-escape") + public AlignAction(DrawingEditor editor) { + super(editor); + updateEnabledState(); + } + + @Override + protected final void updateEnabledState() { + if (getView() != null) { + setEnabled( + getView().isEnabled() + && getView().getSelectionCount() > 1 + ); + } + else { + setEnabled(false); + } + } + + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + CompositeEdit edit = new CompositeEdit(bundle.getString("alignAction.undo.presentationName")); + fireUndoableEditHappened(edit); + alignFigures(getView().getSelectedFigures(), getSelectionBounds()); + fireUndoableEditHappened(edit); + } + + protected abstract void alignFigures( + Collection selectedFigures, + Rectangle2D.Double selectionBounds + ); + + /** + * Returns the bounds of the selected figures. + * + * @return The bounds of the selected figures. + */ + protected Rectangle2D.Double getSelectionBounds() { + Rectangle2D.Double bounds = null; + + for (Figure f : getView().getSelectedFigures()) { + if (bounds == null) { + bounds = f.getBounds(); + } + else { + bounds.add(f.getBounds()); + } + } + + return bounds; + } + + public static class North + extends + AlignAction { + + @SuppressWarnings("this-escape") + public North(DrawingEditor editor) { + super(editor); + + putValue(SHORT_DESCRIPTION, bundle.getString("alignAction.north.shortDescription")); + putValue(SMALL_ICON, ImageDirectory.getImageIcon("/toolbar/align-vertical-top-2.png")); + } + + @Override + protected void alignFigures(Collection selectedFigures, Rectangle2D.Double selectionBounds) { + double y = selectionBounds.y; + + for (Figure f : getView().getSelectedFigures()) { + if (f.isTransformable()) { + f.willChange(); + Rectangle2D.Double b = f.getBounds(); + AffineTransform tx = new AffineTransform(); + tx.translate(0, y - b.y); + f.transform(tx); + f.changed(); + fireUndoableEditHappened(new TransformEdit(f, tx)); + } + } + } + } + + public static class East + extends + AlignAction { + + @SuppressWarnings("this-escape") + public East(DrawingEditor editor) { + super(editor); + + putValue(SHORT_DESCRIPTION, bundle.getString("alignAction.east.shortDescription")); + putValue(SMALL_ICON, ImageDirectory.getImageIcon("/toolbar/align-horizontal-right-2.png")); + } + + @Override + protected void alignFigures(Collection selectedFigures, Rectangle2D.Double selectionBounds) { + double x = selectionBounds.x + selectionBounds.width; + + for (Figure f : getView().getSelectedFigures()) { + if (f.isTransformable()) { + f.willChange(); + Rectangle2D.Double b = f.getBounds(); + AffineTransform tx = new AffineTransform(); + tx.translate(x - b.x - b.width, 0); + f.transform(tx); + f.changed(); + fireUndoableEditHappened(new TransformEdit(f, tx)); + } + } + } + } + + public static class West + extends + AlignAction { + + @SuppressWarnings("this-escape") + public West(DrawingEditor editor) { + super(editor); + + putValue(SHORT_DESCRIPTION, bundle.getString("alignAction.west.shortDescription")); + putValue(SMALL_ICON, ImageDirectory.getImageIcon("/toolbar/align-horizontal-left.png")); + } + + @Override + protected void alignFigures(Collection selectedFigures, Rectangle2D.Double selectionBounds) { + double x = selectionBounds.x; + + for (Figure f : getView().getSelectedFigures()) { + if (f.isTransformable()) { + f.willChange(); + Rectangle2D.Double b = f.getBounds(); + AffineTransform tx = new AffineTransform(); + tx.translate(x - b.x, 0); + f.transform(tx); + f.changed(); + fireUndoableEditHappened(new TransformEdit(f, tx)); + } + } + } + } + + public static class South + extends + AlignAction { + + @SuppressWarnings("this-escape") + public South(DrawingEditor editor) { + super(editor); + + putValue(SHORT_DESCRIPTION, bundle.getString("alignAction.south.shortDescription")); + putValue(SMALL_ICON, ImageDirectory.getImageIcon("/toolbar/align-vertical-bottom-2.png")); + } + + @Override + protected void alignFigures(Collection selectedFigures, Rectangle2D.Double selectionBounds) { + double y = selectionBounds.y + selectionBounds.height; + + for (Figure f : getView().getSelectedFigures()) { + if (f.isTransformable()) { + f.willChange(); + Rectangle2D.Double b = f.getBounds(); + AffineTransform tx = new AffineTransform(); + tx.translate(0, y - b.y - b.height); + f.transform(tx); + f.changed(); + fireUndoableEditHappened(new TransformEdit(f, tx)); + } + } + } + } + + public static class Vertical + extends + AlignAction { + + @SuppressWarnings("this-escape") + public Vertical(DrawingEditor editor) { + super(editor); + + putValue(SHORT_DESCRIPTION, bundle.getString("alignAction.vertical.shortDescription")); + putValue(SMALL_ICON, ImageDirectory.getImageIcon("/toolbar/align-vertical-center-2.png")); + } + + @Override + protected void alignFigures(Collection selectedFigures, Rectangle2D.Double selectionBounds) { + double y = selectionBounds.y + selectionBounds.height / 2; + + for (Figure f : getView().getSelectedFigures()) { + if (f.isTransformable()) { + f.willChange(); + Rectangle2D.Double b = f.getBounds(); + AffineTransform tx = new AffineTransform(); + tx.translate(0, y - b.y - b.height / 2); + f.transform(tx); + f.changed(); + fireUndoableEditHappened(new TransformEdit(f, tx)); + } + } + } + } + + public static class Horizontal + extends + AlignAction { + + @SuppressWarnings("this-escape") + public Horizontal(DrawingEditor editor) { + super(editor); + + putValue(SHORT_DESCRIPTION, bundle.getString("alignAction.horizontal.shortDescription")); + putValue(SMALL_ICON, ImageDirectory.getImageIcon("/toolbar/align-horizontal-center-2.png")); + } + + @Override + protected void alignFigures(Collection selectedFigures, Rectangle2D.Double selectionBounds) { + double x = selectionBounds.x + selectionBounds.width / 2; + + for (Figure f : getView().getSelectedFigures()) { + if (f.isTransformable()) { + f.willChange(); + Rectangle2D.Double b = f.getBounds(); + AffineTransform tx = new AffineTransform(); + tx.translate(x - b.x - b.width / 2, 0); + f.transform(tx); + f.changed(); + fireUndoableEditHappened(new TransformEdit(f, tx)); + } + } + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/ApplyAttributesAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/ApplyAttributesAction.java new file mode 100644 index 0000000..58e3db7 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/ApplyAttributesAction.java @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.draw; + +import static java.util.Objects.requireNonNull; +import static javax.swing.Action.LARGE_ICON_KEY; +import static javax.swing.Action.SMALL_ICON; +import static org.jhotdraw.draw.AttributeKeys.TEXT; +import static org.jhotdraw.draw.AttributeKeys.TRANSFORM; +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.TOOLBAR_PATH; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import javax.swing.ImageIcon; +import org.jhotdraw.draw.AttributeKey; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.action.AbstractSelectedAction; +import org.jhotdraw.draw.event.FigureSelectionEvent; +import org.jhotdraw.undo.CompositeEdit; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * ApplyAttributesAction. + * + * @author Werner Randelshofer + */ +public class ApplyAttributesAction + extends + AbstractSelectedAction { + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(TOOLBAR_PATH); + + private Set> excludedAttributes = new HashSet<>( + Arrays.asList(new AttributeKey[]{TRANSFORM, TEXT}) + ); + + /** + * Creates a new instance. + * + * @param editor The editor. + */ + @SuppressWarnings("this-escape") + public ApplyAttributesAction(DrawingEditor editor) { + super(editor); + + putValue(NAME, BUNDLE.getString("applyAttributesAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("applyAttributesAction.shortDescription")); + + ImageIcon icon = ImageDirectory.getImageIcon("/toolbar/view-media-visualization.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + updateEnabledState(); + } + + /** + * Set of attributes that is excluded when applying default attributes. + * + * @param excludedAttributes The set of attributes to be excluded. + */ + public void setExcludedAttributes(Set> excludedAttributes) { + this.excludedAttributes = requireNonNull(excludedAttributes, "excludedAttributes"); + } + + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + applyAttributes(); + } + + @SuppressWarnings("unchecked") + public void applyAttributes() { + DrawingEditor editor = getEditor(); + + ResourceBundleUtil labels + = ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.TOOLBAR_PATH); + CompositeEdit edit + = new CompositeEdit(labels.getString("applyAttributesAction.undo.presentationName")); + DrawingView view = getView(); + view.getDrawing().fireUndoableEditHappened(edit); + + for (Figure figure : view.getSelectedFigures()) { + figure.willChange(); + + for (Map.Entry entry : editor.getDefaultAttributes().entrySet()) { + if (!excludedAttributes.contains(entry.getKey())) { + figure.set(entry.getKey(), entry.getValue()); + } + } + + figure.changed(); + } + + view.getDrawing().fireUndoableEditHappened(edit); + } + + public void selectionChanged(FigureSelectionEvent evt) { + setEnabled(getView().getSelectionCount() == 1); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/AttributeAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/AttributeAction.java new file mode 100644 index 0000000..c51df00 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/AttributeAction.java @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.draw; + +import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import javax.swing.AbstractAction; +import javax.swing.Icon; +import javax.swing.undo.AbstractUndoableEdit; +import javax.swing.undo.UndoableEdit; +import org.jhotdraw.app.action.ActionUtil; +import org.jhotdraw.draw.AttributeKey; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.action.AbstractSelectedAction; + +/** + * Applies attribute values on the selected figures of the current {@link DrawingView} of a + * {@link DrawingEditor}. + * + * @author Werner Randelshofer + */ +public class AttributeAction + extends + AbstractSelectedAction { + + protected Map attributes; + + @SuppressWarnings("this-escape") + public AttributeAction( + DrawingEditor editor, + AttributeKey key, + Object value, + String name, + Icon icon + ) { + super(editor); + this.attributes = new HashMap<>(); + attributes.put(key, value); + + putValue(AbstractAction.NAME, name); + putValue(AbstractAction.SMALL_ICON, icon); + putValue(ActionUtil.UNDO_PRESENTATION_NAME_KEY, key.getPresentationName()); + updateEnabledState(); + } + + @SuppressWarnings("this-escape") + public AttributeAction( + DrawingEditor editor, + Map attributes, + String name, + Icon icon + ) { + super(editor); + this.attributes = (attributes == null) ? new HashMap<>() : attributes; + + putValue(AbstractAction.NAME, name); + putValue(AbstractAction.SMALL_ICON, icon); + updateEnabledState(); + } + + @Override + public void actionPerformed(ActionEvent evt) { + applyAttributesTo(attributes, getView().getSelectedFigures()); + } + + /** + * Applies the specified attributes to the currently selected figures of the + * drawing. + * + * @param a The attributes. + * @param figures The figures to which the attributes are applied. + */ + @SuppressWarnings("unchecked") + public void applyAttributesTo(final Map a, Set
figures) { + for (Map.Entry entry : a.entrySet()) { + getEditor().setDefaultAttribute(entry.getKey(), entry.getValue()); + } + + final ArrayList
selectedFigures = new ArrayList<>(figures); + final ArrayList restoreData = new ArrayList<>(selectedFigures.size()); + + for (Figure figure : selectedFigures) { + restoreData.add(figure.getAttributesRestoreData()); + figure.willChange(); + + for (Map.Entry entry : a.entrySet()) { + figure.set(entry.getKey(), entry.getValue()); + } + + figure.changed(); + } + + UndoableEdit edit = new AbstractUndoableEdit() { + @Override + public String getPresentationName() { + String name = (String) getValue(ActionUtil.UNDO_PRESENTATION_NAME_KEY); + + if (name == null) { + name = (String) getValue(AbstractAction.NAME); + } + + return name; + } + + @Override + public void undo() { + super.undo(); + Iterator iRestore = restoreData.iterator(); + + for (Figure figure : selectedFigures) { + figure.willChange(); + figure.restoreAttributesTo(iRestore.next()); + figure.changed(); + } + } + + @Override + public void redo() { + super.redo(); + + for (Figure figure : selectedFigures) { + //restoreData.add(figure.getAttributesRestoreData()); + figure.willChange(); + + for (Map.Entry entry : a.entrySet()) { + figure.set(entry.getKey(), entry.getValue()); + } + + figure.changed(); + } + } + }; + + getDrawing().fireUndoableEditHappened(edit); + } + + @Override + protected void updateEnabledState() { + if (getEditor() != null) { + setEnabled(getEditor().isEnabled()); + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/BringToFrontAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/BringToFrontAction.java new file mode 100644 index 0000000..b98f870 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/BringToFrontAction.java @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.draw; + +import static javax.swing.Action.SMALL_ICON; +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.TOOLBAR_PATH; + +import java.util.ArrayList; +import java.util.Collection; +import javax.swing.undo.AbstractUndoableEdit; +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; +import org.jhotdraw.draw.Drawing; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.action.AbstractSelectedAction; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * ToFrontAction. + * + * @author Werner Randelshofer + */ +public class BringToFrontAction + extends + AbstractSelectedAction { + + /** + * This action's ID. + */ + public static final String ID = "edit.bringToFront"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(TOOLBAR_PATH); + + /** + * Creates a new instance. + * + * @param editor The drawing editor + */ + @SuppressWarnings("this-escape") + public BringToFrontAction(DrawingEditor editor) { + super(editor); + + putValue(NAME, BUNDLE.getString("bringToFrontAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("bringToFrontAction.shortDescription")); + putValue(SMALL_ICON, ImageDirectory.getImageIcon("/toolbar/object-order-front.png")); + + updateEnabledState(); + } + + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + final DrawingView view = getView(); + final Collection
figures = new ArrayList<>(view.getSelectedFigures()); + bringToFront(view, figures); + fireUndoableEditHappened(new AbstractUndoableEdit() { + @Override + public String getPresentationName() { + return ResourceBundleUtil.getBundle(TOOLBAR_PATH) + .getString("bringToFrontAction.undo.presentationName"); + } + + @Override + public void redo() + throws CannotRedoException { + super.redo(); + BringToFrontAction.bringToFront(view, figures); + } + + @Override + public void undo() + throws CannotUndoException { + super.undo(); + SendToBackAction.sendToBack(view, figures); + } + }); + } + + public static void bringToFront(DrawingView view, Collection
figures) { + Drawing drawing = view.getDrawing(); + + for (Figure figure : drawing.sort(figures)) { + drawing.bringToFront(figure); + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/ColorIcon.java b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/ColorIcon.java new file mode 100644 index 0000000..30c7265 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/ColorIcon.java @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.draw; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.image.BufferedImage; +import org.jhotdraw.util.Images; + +/** + * ColorIcon. + * + * @author Werner Randelshofer + */ +public class ColorIcon + implements + javax.swing.Icon { + + private static BufferedImage noColorImage; + private Color fillColor; + private int width; + private int height; + private String name; + + public ColorIcon(int rgb, String name) { + this(new Color(rgb), name, 14, 14); + } + + public ColorIcon(Color color, String name) { + this(color, name, 14, 14); + } + + public ColorIcon(Color color, String name, int width, int height) { + this.fillColor = color; + this.name = name; + this.width = width; + this.height = height; + + if (noColorImage == null) { + noColorImage = Images.toBufferedImage( + Images.createImage( + ColorIcon.class, "/org/jhotdraw/draw/action/images/attribute.color.noColor.png" + ) + ); + } + } + + public Color getColor() { + return fillColor; + } + + public String getName() { + return name; + } + + @Override + public int getIconWidth() { + return width; + } + + @Override + public int getIconHeight() { + return height; + } + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + //Graphics2D g = (Graphics2D) gr; + if (fillColor == null || fillColor.getAlpha() == 0) { + if (width == noColorImage.getWidth() && height == noColorImage.getHeight()) { + g.drawImage(noColorImage, x, y, c); + } + else { + g.setColor(Color.WHITE); + g.fillRect(x + 1, y + 1, width - 2, height - 2); + g.setColor(Color.red); + int[] xpoints = new int[]{x + 2, + x + width - 5, + x + width - 3, + x + width - 3, + x + 4, + x + 2}; + int[] ypoints = new int[]{y + height - 5, + y + 2, + y + 2, + y + 4, + y + height - 3, + y + height - 3}; + g.fillPolygon(xpoints, ypoints, xpoints.length); + } + } + else { + // g.setColor(Color.WHITE); + // g.fillRect(x + 1, y + 1, width - 2, height - 2); + g.setColor(fillColor); + // g.fillRect(x + 2, y + 2, width - 4, height - 4); + g.fillRect(x + 1, y + 1, width - 2, height - 2); + } + + g.setColor(new Color(0x666666)); + + // Draw the rectangle using drawLine to work around a drawing bug in + // Apples MRJ for Java 1.5 +// g.drawRect(x, y, getIconWidth() - 1, getIconHeight() - 1); + g.drawLine(x, y, x + width - 1, y); + g.drawLine(x + width - 1, y, x + width - 1, y + width - 1); + g.drawLine(x + width - 1, y + height - 1, x, y + height - 1); + g.drawLine(x, y + height - 1, x, y); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/DefaultAttributeAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/DefaultAttributeAction.java new file mode 100644 index 0000000..a366c0b --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/DefaultAttributeAction.java @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.draw; + +import java.awt.event.ActionEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.HashMap; +import java.util.Map; +import javax.swing.AbstractAction; +import javax.swing.Icon; +import org.jhotdraw.draw.AttributeKey; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.action.AbstractSelectedAction; +import org.jhotdraw.draw.event.FigureSelectionEvent; +import org.jhotdraw.undo.CompositeEdit; + +/** + * DefaultAttributeAction. + * + * @author Werner Randelshofer + */ +public class DefaultAttributeAction + extends + AbstractSelectedAction { + + private AttributeKey[] keys; + private Map fixedAttributes; + + /** + * Creates a new instance. + * + * @param editor The drawing editor + * @param key The attribute key + */ + public DefaultAttributeAction( + DrawingEditor editor, + AttributeKey key + ) { + + this(editor, key, null, null); + } + + public DefaultAttributeAction( + DrawingEditor editor, + AttributeKey key, + Map fixedAttributes + ) { + + this(editor, new AttributeKey[]{key}, null, null, fixedAttributes); + } + + public DefaultAttributeAction( + DrawingEditor editor, + AttributeKey[] keys + ) { + + this(editor, keys, null, null); + } + + /** + * Creates a new instance. + * + * @param editor The drawing editor. + * @param key The attribute key. + * @param icon The icon. + */ + public DefaultAttributeAction(DrawingEditor editor, AttributeKey key, Icon icon) { + this(editor, key, null, icon); + } + + /** + * Creates a new instance. + * + * @param editor The drawing editor. + * @param key The attribute key. + * @param name The name. + */ + public DefaultAttributeAction(DrawingEditor editor, AttributeKey key, String name) { + this(editor, key, name, null); + } + + public DefaultAttributeAction(DrawingEditor editor, AttributeKey key, String name, Icon icon) { + this(editor, new AttributeKey[]{key}, name, icon); + } + + public DefaultAttributeAction( + DrawingEditor editor, + AttributeKey[] keys, + String name, + Icon icon + ) { + + this(editor, keys, name, icon, new HashMap()); + } + + @SuppressWarnings("this-escape") + public DefaultAttributeAction( + DrawingEditor editor, + AttributeKey[] keys, + String name, + Icon icon, + Map fixedAttributes + ) { + + super(editor); + this.keys = keys.clone(); + putValue(AbstractAction.NAME, name); + putValue(AbstractAction.SMALL_ICON, icon); + setEnabled(true); + editor.addPropertyChangeListener(new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getPropertyName().equals(DefaultAttributeAction.this.keys[0].getKey())) { + putValue("attribute_" + DefaultAttributeAction.this.keys[0].getKey(), evt.getNewValue()); + } + } + }); + this.fixedAttributes = fixedAttributes; + updateEnabledState(); + } + + @Override + public void actionPerformed(ActionEvent evt) { + if (getView() != null && getView().getSelectionCount() > 0) { + CompositeEdit edit = new CompositeEdit(""); + fireUndoableEditHappened(edit); + changeAttribute(); + fireUndoableEditHappened(edit); + } + } + + @SuppressWarnings("unchecked") + public void changeAttribute() { + CompositeEdit edit = new CompositeEdit("attributes"); + fireUndoableEditHappened(edit); + DrawingEditor editor = getEditor(); + + for (Figure figure : getView().getSelectedFigures()) { + figure.willChange(); + for (AttributeKey key : keys) { + figure.set(key, editor.getDefaultAttribute(key)); + } + + for (Map.Entry entry : fixedAttributes.entrySet()) { + figure.set(entry.getKey(), entry.getValue()); + + } + + figure.changed(); + } + + fireUndoableEditHappened(edit); + } + + public void selectionChanged(FigureSelectionEvent evt) { + //setEnabled(getView().getSelectionCount() > 0); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/DefaultPathSelectedAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/DefaultPathSelectedAction.java new file mode 100644 index 0000000..ee853d0 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/DefaultPathSelectedAction.java @@ -0,0 +1,162 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.draw; + +import java.awt.event.ActionEvent; +import java.util.Objects; +import javax.swing.AbstractAction; +import javax.swing.ButtonGroup; +import javax.swing.ImageIcon; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.tool.Tool; +import org.jhotdraw.gui.JPopupButton; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.common.components.drawing.figures.PathConnection; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.modeleditor.jhotdraw.application.toolbar.OpenTCSConnectionTool; + +/** + * This action manages the behaviour when the user selects the path button. + */ +public class DefaultPathSelectedAction + extends + org.jhotdraw.draw.action.AbstractSelectedAction { + + private final PathModel.Type pathType; + private final Tool tool; + /** + * The button this action belongs to. + */ + private final JPopupButton popupButton; + /** + * The Icon the popup button uses when this action is selected. + */ + private final ImageIcon largeIcon; + /** + * The ButtonGroup the popupButton belongs to. It is necessary to know it, + * because + * DrawingEditor.setTool() doesn't select or deselect the + * popupButton, so we have to do it manually. + */ + private final ButtonGroup group; + + /** + * Constructor for an action of a button in the toolbar. + * + * @param editor The drawing editor + * @param tool The tool + * @param popupButton The popup button + * @param group The button group + */ + public DefaultPathSelectedAction( + DrawingEditor editor, + Tool tool, + JPopupButton popupButton, + ButtonGroup group + ) { + + super(editor); + this.tool = Objects.requireNonNull(tool); + this.popupButton = Objects.requireNonNull(popupButton); + this.group = Objects.requireNonNull(group); + this.pathType = null; + this.largeIcon = null; + } + + /** + * Constructor for a button inside a drop down menu of another button. + * + * @param editor The drawing editor + * @param tool The tool + * @param pathType The path tzpe + * @param popupButton The popup button + * @param group The button group + */ + @SuppressWarnings("this-escape") + public DefaultPathSelectedAction( + DrawingEditor editor, + Tool tool, + PathModel.Type pathType, + JPopupButton popupButton, + ButtonGroup group + ) { + + super(editor); + this.tool = Objects.requireNonNull(tool); + this.popupButton = Objects.requireNonNull(popupButton); + this.group = Objects.requireNonNull(group); + + this.pathType = Objects.requireNonNull(pathType); + this.largeIcon = getLargeImageIconByType(pathType); + + putValue(AbstractAction.NAME, pathType.getDescription()); + putValue(AbstractAction.SHORT_DESCRIPTION, pathType.getHelptext()); + putValue(AbstractAction.SMALL_ICON, getImageIconByType(pathType)); + } + + @Override + public void actionPerformed(ActionEvent e) { + if (pathType != null) { + OpenTCSConnectionTool connectionTool = (OpenTCSConnectionTool) tool; + PathConnection pathConnection = (PathConnection) connectionTool.getPrototype(); + // Set the type explicitly to make sure that the selected path is displayed correctly. + pathConnection.getModel().getPropertyPathConnType().setValue(pathType); + + popupButton.setText(null); + popupButton.setToolTipText(pathType.getHelptext()); + popupButton.setIcon(largeIcon); + } + + getEditor().setTool(tool); + group.setSelected(popupButton.getModel(), true); + } + + @Override + protected void updateEnabledState() { + if (getView() != null) { + setEnabled(getView().isEnabled()); + } + else { + setEnabled(false); + } + } + + private ImageIcon getImageIconByType(PathModel.Type pathType) { + switch (pathType) { + case DIRECT: + return ImageDirectory.getImageIcon("/toolbar/path-direct.22.png"); + case ELBOW: + return ImageDirectory.getImageIcon("/toolbar/path-elbow.22.png"); + case SLANTED: + return ImageDirectory.getImageIcon("/toolbar/path-slanted.22.png"); + case BEZIER: + return ImageDirectory.getImageIcon("/toolbar/path-bezier.22.png"); + case BEZIER_3: + return ImageDirectory.getImageIcon("/toolbar/path-bezier.22.png"); + case POLYPATH: + return ImageDirectory.getImageIcon("/toolbar/path-polypath.22.png"); + default: + return null; + } + } + + private ImageIcon getLargeImageIconByType(PathModel.Type pathType) { + switch (pathType) { + case DIRECT: + return ImageDirectory.getImageIcon("/toolbar/path-direct-arrow.22.png"); + case ELBOW: + return ImageDirectory.getImageIcon("/toolbar/path-elbow-arrow.22.png"); + case SLANTED: + return ImageDirectory.getImageIcon("/toolbar/path-slanted-arrow.22.png"); + case BEZIER: + return ImageDirectory.getImageIcon("/toolbar/path-bezier-arrow.22.png"); + case BEZIER_3: + return ImageDirectory.getImageIcon("/toolbar/path-bezier-arrow.22.png"); + case POLYPATH: + return ImageDirectory.getImageIcon("/toolbar/path-polypath-arrow.22.png"); + default: + return null; + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/EditorColorChooserAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/EditorColorChooserAction.java new file mode 100644 index 0000000..f5f3e61 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/EditorColorChooserAction.java @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.draw; + +import java.awt.Color; +import java.awt.Component; +import java.util.HashMap; +import java.util.Map; +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.Icon; +import javax.swing.JColorChooser; +import org.jhotdraw.draw.AttributeKey; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.action.EditorColorIcon; +import org.jhotdraw.draw.event.FigureSelectionEvent; +import org.opentcs.modeleditor.util.I18nPlantOverviewModeling; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * EditorColorChooserAction. + *

+ * The behavior for choosing the initial color of + * the JColorChooser matches with + * {@link EditorColorIcon }. + * + * @author Werner Randelshofer + */ +public class EditorColorChooserAction + extends + AttributeAction { + + protected AttributeKey key; + + /** + * Creates a new instance. + * + * @param editor The drawing editor + * @param key The attribute key + * @param name The name + * @param icon The icon + * @param fixedAttributes The fixed attributes + */ + @SuppressWarnings("this-escape") + public EditorColorChooserAction( + DrawingEditor editor, + AttributeKey key, + String name, + Icon icon, + Map fixedAttributes + ) { + + super(editor, fixedAttributes, name, icon); + this.key = key; + putValue(AbstractAction.NAME, name); + putValue( + Action.SHORT_DESCRIPTION, + ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.TOOLBAR_PATH) + .getString("editorColorChooserAction.shortDescription") + ); + putValue(AbstractAction.SMALL_ICON, icon); + updateEnabledState(); + } + + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + Color initialColor = getInitialColor(); + ResourceBundleUtil labels + = ResourceBundleUtil.getBundle(I18nPlantOverviewModeling.TOOLBAR_PATH); + Color chosenColor + = JColorChooser.showDialog( + (Component) e.getSource(), + labels.getString("editorColorChooserAction.dialog_colorSelection.title"), + initialColor + ); + + if (chosenColor != null) { + HashMap attr = new HashMap<>(attributes); + attr.put(key, chosenColor); + applyAttributesTo(attr, getView().getSelectedFigures()); + } + } + + public void selectionChanged(FigureSelectionEvent evt) { + //setEnabled(getView().getSelectionCount() > 0); + } + + protected Color getInitialColor() { + Color initialColor = getEditor().getDefaultAttribute(key); + + if (initialColor == null) { + initialColor = Color.red; + } + + return initialColor; + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/PickAttributesAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/PickAttributesAction.java new file mode 100644 index 0000000..0670d2c --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/PickAttributesAction.java @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.draw; + +import static javax.swing.Action.LARGE_ICON_KEY; +import static javax.swing.Action.SMALL_ICON; +import static org.jhotdraw.draw.AttributeKeys.TEXT; +import static org.jhotdraw.draw.AttributeKeys.TRANSFORM; +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.TOOLBAR_PATH; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import javax.swing.ImageIcon; +import org.jhotdraw.draw.AttributeKey; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.action.AbstractSelectedAction; +import org.jhotdraw.draw.event.FigureSelectionEvent; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * PickAttributesAction. + * + * @author Werner Randelshofer + */ +public class PickAttributesAction + extends + AbstractSelectedAction { + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(TOOLBAR_PATH); + + private Set excludedAttributes = new HashSet<>( + Arrays.asList(new AttributeKey[]{TRANSFORM, TEXT}) + ); + + /** + * Creates a new instance. + * + * @param editor The drawing editor + */ + @SuppressWarnings("this-escape") + public PickAttributesAction(DrawingEditor editor) { + super(editor); + putValue(NAME, BUNDLE.getString("pickAttributesAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("pickAttributesAction.shortDescription")); + + ImageIcon icon = ImageDirectory.getImageIcon("/toolbar/colorpicker.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + + updateEnabledState(); + } + + /** + * Set of attributes that is excluded when applying default attributes. + * By default, the TRANSFORM attribute is excluded. + * + * @param a The attributes to exclude. + */ + public void setExcludedAttributes(Set a) { + this.excludedAttributes = a; + } + + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + pickAttributes(); + } + + @SuppressWarnings("unchecked") + public void pickAttributes() { + DrawingEditor editor = getEditor(); + Collection

selection = getView().getSelectedFigures(); + + if (selection.size() > 0) { + Figure figure = selection.iterator().next(); + + for (Map.Entry entry : figure.getAttributes().entrySet()) { + if (!excludedAttributes.contains(entry.getKey())) { + editor.setDefaultAttribute(entry.getKey(), entry.getValue()); + } + } + } + } + + public void selectionChanged(FigureSelectionEvent evt) { + setEnabled(getView().getSelectionCount() == 1); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/SendToBackAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/SendToBackAction.java new file mode 100644 index 0000000..63cae76 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/draw/SendToBackAction.java @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.draw; + +import static javax.swing.Action.SMALL_ICON; +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.TOOLBAR_PATH; + +import java.util.ArrayList; +import java.util.Collection; +import javax.swing.undo.AbstractUndoableEdit; +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; +import org.jhotdraw.draw.Drawing; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.action.AbstractSelectedAction; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * SendToBackAction. + * + * @author Werner Randelshofer + */ +public class SendToBackAction + extends + AbstractSelectedAction { + + /** + * This action's ID. + */ + public static final String ID = "edit.sendToBack"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(TOOLBAR_PATH); + + /** + * Creates a new instance. + * + * @param editor The drawing editor + */ + @SuppressWarnings("this-escape") + public SendToBackAction(DrawingEditor editor) { + super(editor); + + putValue(NAME, BUNDLE.getString("sendToBackAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("sendToBackAction.shortDescription")); + putValue(SMALL_ICON, ImageDirectory.getImageIcon("/toolbar/object-order-back.png")); + + updateEnabledState(); + } + + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + final DrawingView view = getView(); + final Collection
figures = new ArrayList<>(view.getSelectedFigures()); + sendToBack(view, figures); + fireUndoableEditHappened(new AbstractUndoableEdit() { + @Override + public String getPresentationName() { + return ResourceBundleUtil.getBundle(TOOLBAR_PATH) + .getString("sendToBackAction.undo.presentationName"); + } + + @Override + public void redo() + throws CannotRedoException { + super.redo(); + SendToBackAction.sendToBack(view, figures); + } + + @Override + public void undo() + throws CannotUndoException { + super.undo(); + BringToFrontAction.bringToFront(view, figures); + } + }); + } + + public static void sendToBack(DrawingView view, Collection
figures) { + Drawing drawing = view.getDrawing(); + + for (Figure figure : figures) { + drawing.sendToBack(figure); + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/edit/ClearSelectionAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/edit/ClearSelectionAction.java new file mode 100644 index 0000000..1f731bc --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/edit/ClearSelectionAction.java @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.edit; + +import static javax.swing.Action.ACCELERATOR_KEY; +import static javax.swing.Action.LARGE_ICON_KEY; +import static javax.swing.Action.SMALL_ICON; +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.MENU_PATH; + +import java.awt.Component; +import java.awt.KeyboardFocusManager; +import java.awt.event.ActionEvent; +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.KeyStroke; +import javax.swing.text.JTextComponent; +import org.opentcs.guing.common.components.EditableComponent; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Clears (de-selects) the selected region. + * This action acts on the last {@code JTextComponent} which had the focus + * when the {@code ActionEvent} was generated. + * This action is called when the user selects the "Clear Selection" item + * in the Edit menu. The menu item is automatically created by the application. + * + * @author Werner Randelshofer. + */ +public class ClearSelectionAction + extends + org.jhotdraw.app.action.edit.AbstractSelectionAction { + + /** + * This action's ID. + */ + public static final String ID = "edit.clearSelection"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + + /** + * Creates a new instance which acts on the currently focused component. + */ + public ClearSelectionAction() { + this(null); + } + + /** + * Creates a new instance which acts on the specified component. + * + * @param target The target of the action. Specify null for the currently + * focused component. + */ + @SuppressWarnings("this-escape") + public ClearSelectionAction(JComponent target) { + super(target); + + putValue(NAME, BUNDLE.getString("clearSelectionAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("clearSelectionAction.shortDescription")); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke("shift ctrl A")); + + ImageIcon icon = ImageDirectory.getImageIcon("/menu/edit-clear-2.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + } + + @Override + public void actionPerformed(ActionEvent evt) { + JComponent cTarget = target; + Component cFocusOwner + = KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner(); + + if (cTarget == null && (cFocusOwner instanceof JComponent)) { + cTarget = (JComponent) cFocusOwner; + } + + if (cTarget != null && cTarget.isEnabled()) { + if (cTarget instanceof EditableComponent) { + ((EditableComponent) cTarget).clearSelection(); + } + else if (cTarget instanceof JTextComponent) { + JTextComponent tc = ((JTextComponent) cTarget); + tc.select(tc.getSelectionStart(), tc.getSelectionStart()); + } + else { + cTarget.getToolkit().beep(); + } + } + } + + @Override + protected void updateEnabled() { + if (target != null) { + setEnabled(target.isEnabled()); + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/edit/CopyAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/edit/CopyAction.java new file mode 100644 index 0000000..866b121 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/edit/CopyAction.java @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.edit; + +import static javax.swing.Action.ACCELERATOR_KEY; +import static javax.swing.Action.LARGE_ICON_KEY; +import static javax.swing.Action.SMALL_ICON; +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.MENU_PATH; + +import java.awt.Component; +import java.awt.KeyboardFocusManager; +import java.awt.event.ActionEvent; +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.KeyStroke; +import org.jhotdraw.app.action.edit.AbstractSelectionAction; +import org.opentcs.guing.common.components.EditableComponent; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Copies the selected region and place its contents into the system clipboard. + * This action acts on the last EditableComponent / {@code JTextComponent} + * which had the focus when the {@code ActionEvent} was generated. + * This action is called when the user selects the "Copy" item in the Edit menu. + * The menu item is automatically created by the application. + * + * @author Werner Randelshofer + */ +public class CopyAction + extends + AbstractSelectionAction { + + /** + * This action's ID. + */ + public static final String ID = "edit.copy"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + + /** + * Creates a new instance which acts on the currently focused component. + */ + public CopyAction() { + this(null); + } + + /** + * Creates a new instance which acts on the specified component. + * + * @param target The target of the action. Specify null for the currently + * focused component. + */ + @SuppressWarnings("this-escape") + public CopyAction(JComponent target) { + super(target); + + putValue(NAME, BUNDLE.getString("copyAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("copyAction.shortDescription")); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke("ctrl C")); + + ImageIcon icon = ImageDirectory.getImageIcon("/menu/edit-copy-4.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + } + + @Override + public void actionPerformed(ActionEvent evt) { + Component cFocusOwner + = KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner(); + + if (cFocusOwner instanceof JComponent) { + if (cFocusOwner.isEnabled()) { + // Cut all selected UserObjects from the tree + if (cFocusOwner instanceof EditableComponent) { + ((EditableComponent) cFocusOwner).copySelectedItems(); + } + } + } + + // "Old" version with JHotDraw clipboard +// JComponent cTarget = target; +// +// if (cTarget == null && (cFocusOwner instanceof JComponent)) { +// cTarget = (JComponent) cFocusOwner; +// } +// // Note: copying is allowed for disabled components +// if (cTarget != null && cTarget.getTransferHandler() != null) { +// cTarget.getTransferHandler().exportToClipboard(cTarget, +// ClipboardUtil.getClipboard(), +// TransferHandler.COPY); +// } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/edit/CutAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/edit/CutAction.java new file mode 100644 index 0000000..35864d1 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/edit/CutAction.java @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.edit; + +import static javax.swing.Action.ACCELERATOR_KEY; +import static javax.swing.Action.LARGE_ICON_KEY; +import static javax.swing.Action.SMALL_ICON; +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.MENU_PATH; + +import java.awt.Component; +import java.awt.KeyboardFocusManager; +import java.awt.event.ActionEvent; +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.KeyStroke; +import org.jhotdraw.app.action.edit.AbstractSelectionAction; +import org.opentcs.guing.common.components.EditableComponent; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Cuts the selected region and places its contents into the system clipboard. + * This action acts on the last EditableComponent / {@code JTextComponent} + * which had the focus when the {@code ActionEvent} was generated. + * This action is called when the user selects the "Cut" item in + * the Edit menu. The menu item is automatically created by the application. + * + * @author Werner Randelshofer + */ +public class CutAction + extends + AbstractSelectionAction { + + /** + * This action's ID. + */ + public static final String ID = "edit.cut"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + + /** + * Creates a new instance which acts on the currently focused component. + */ + public CutAction() { + this(null); + } + + /** + * Creates a new instance which acts on the specified component. + * + * @param target The target of the action. Specify null for the currently + * focused component. + */ + @SuppressWarnings("this-escape") + public CutAction(JComponent target) { + super(target); + + putValue(NAME, BUNDLE.getString("cutAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("cutAction.shortDescription")); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke("ctrl X")); + + ImageIcon icon = ImageDirectory.getImageIcon("/menu/edit-cut-4.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + } + + @Override + public void actionPerformed(ActionEvent evt) { + Component cFocusOwner + = KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner(); + + if (cFocusOwner instanceof JComponent) { + if (cFocusOwner.isEnabled()) { + // Cut all selected UserObjects from the tree + if (cFocusOwner instanceof EditableComponent) { + ((EditableComponent) cFocusOwner).cutSelectedItems(); + } + } + } + + // "Old" version with JHotDraw clipboard +// JComponent cTarget = target; +// +// if (cTarget == null && (cFocusOwner instanceof JComponent)) { +// cTarget = (JComponent) cFocusOwner; +// } +// +// if (cTarget != null && cTarget.isEnabled() && cTarget.getTransferHandler() != null) { +// cTarget.getTransferHandler().exportToClipboard(cTarget, +// ClipboardUtil.getClipboard(), +// TransferHandler.MOVE); +// } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/edit/DuplicateAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/edit/DuplicateAction.java new file mode 100644 index 0000000..f273c39 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/edit/DuplicateAction.java @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.edit; + +import static javax.swing.Action.ACCELERATOR_KEY; +import static javax.swing.Action.LARGE_ICON_KEY; +import static javax.swing.Action.SMALL_ICON; +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.MENU_PATH; + +import java.awt.Component; +import java.awt.KeyboardFocusManager; +import java.awt.event.ActionEvent; +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.KeyStroke; +import org.opentcs.guing.common.components.EditableComponent; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Duplicates the selected region. + * This action acts on the last EditableComponent / {@code JTextComponent} + * which had the focus when the {@code ActionEvent} was generated. + * This action is called when the user selects the "Duplicate" item + * in the Edit menu. The menu item is automatically created by the application. + * + * @author Werner Randelshofer. + */ +public class DuplicateAction + extends + org.jhotdraw.app.action.edit.AbstractSelectionAction { + + /** + * This action's ID. + */ + public static final String ID = "edit.duplicate"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + + /** + * Creates a new instance which acts on the currently focused component. + */ + public DuplicateAction() { + this(null); + } + + /** + * Creates a new instance which acts on the specified component. + * + * @param target The target of the action. Specify null for the currently + * focused component. + */ + @SuppressWarnings("this-escape") + public DuplicateAction(JComponent target) { + super(target); + + putValue(NAME, BUNDLE.getString("duplicateAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("duplicateAction.shortDescription")); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke("ctrl D")); + + ImageIcon icon = ImageDirectory.getImageIcon("/menu/edit-copy-3.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + } + + @Override + public void actionPerformed(ActionEvent evt) { + Component cFocusOwner + = KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner(); + + if (cFocusOwner instanceof JComponent) { + if (cFocusOwner.isEnabled()) { + // Cut all selected UserObjects from the tree + if (cFocusOwner instanceof EditableComponent) { + ((EditableComponent) cFocusOwner).duplicate(); + } + } + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/edit/PasteAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/edit/PasteAction.java new file mode 100644 index 0000000..e84cb74 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/edit/PasteAction.java @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.edit; + +import static javax.swing.Action.ACCELERATOR_KEY; +import static javax.swing.Action.LARGE_ICON_KEY; +import static javax.swing.Action.SMALL_ICON; +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.MENU_PATH; + +import java.awt.Component; +import java.awt.KeyboardFocusManager; +import java.awt.event.ActionEvent; +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.KeyStroke; +import org.opentcs.guing.common.components.EditableComponent; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Pastes the contents of the system clipboard at the caret position. + * This action acts on the last EditableComponent / {@code JTextComponent} + * which had the focus when the {@code ActionEvent} was generated. + * This action is called when the user selects the "Paste" item in the Edit menu. + * The menu item is automatically created by the application. + * + * @author Werner Randelshofer + */ +public class PasteAction + extends + org.jhotdraw.app.action.edit.AbstractSelectionAction { + + /** + * This action's ID. + */ + public static final String ID = "edit.paste"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + + /** + * Creates a new instance which acts on the currently focused component. + */ + public PasteAction() { + this(null); + } + + /** + * Creates a new instance which acts on the specified component. + * + * @param target The target of the action. Specify null for the currently + * focused component. + */ + @SuppressWarnings("this-escape") + public PasteAction(JComponent target) { + super(target); + + putValue(NAME, BUNDLE.getString("pasteAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("pasteAction.shortDescription")); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke("ctrl V")); + + ImageIcon icon = ImageDirectory.getImageIcon("/menu/edit-paste.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + } + + @Override + public void actionPerformed(ActionEvent evt) { + Component cFocusOwner + = KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner(); + + if (cFocusOwner instanceof JComponent) { + if (cFocusOwner.isEnabled()) { + if (cFocusOwner instanceof EditableComponent) { + ((EditableComponent) cFocusOwner).pasteBufferedItems(); + } + } + } + + // "Old" version with JHotDraw clipboard +// JComponent cTarget = target; +// +// if (cTarget == null && (cFocusOwner instanceof JComponent)) { +// cTarget = (JComponent) cFocusOwner; +// } +// +// if (cTarget != null && cTarget.isEnabled()) { +// Transferable t = ClipboardUtil.getClipboard().getContents(cTarget); +// +// if (t != null && cTarget.getTransferHandler() != null) { +// cTarget.getTransferHandler().importData(cTarget, t); +// } +// } + } + + @Override + protected void updateEnabled() { + if (target != null) { + setEnabled(target.isEnabled()); + } + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/file/CloseFileAction.java b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/file/CloseFileAction.java new file mode 100644 index 0000000..a2a382a --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/action/file/CloseFileAction.java @@ -0,0 +1,172 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.modeleditor.jhotdraw.application.action.file; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.MENU_PATH; + +import java.awt.event.ActionEvent; +import java.net.URI; +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.ImageIcon; +import javax.swing.JFrame; +import javax.swing.JOptionPane; +import javax.swing.KeyStroke; +import javax.swing.SwingUtilities; +import org.jhotdraw.app.View; +import org.jhotdraw.app.action.file.NewFileAction; +import org.jhotdraw.app.action.file.NewWindowAction; +import org.jhotdraw.app.action.file.OpenDirectoryAction; +import org.jhotdraw.app.action.file.OpenFileAction; +import org.jhotdraw.net.URIUtil; +import org.opentcs.access.Kernel; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.modeleditor.application.OpenTCSView; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Closes the active view after letting the user save unsaved changes. + * {@code DefaultSDIApplication} automatically exits when the user closes the + * last view. + *

+ * This action is called when the user selects the Close item in + * the File menu. The menu item is automatically created by the application. + *

+ * If you want this behavior in your application, you have to create it and put + * it in your {@code ApplicationModel} in method + * {@link org.jhotdraw.app.ApplicationModel#initApplication}. + *

+ * You should + * include this action in applications which use at least one of the following + * actions, so that the user can close views that he/she created: + * {@link NewFileAction}, {@link NewWindowAction}, + * {@link OpenFileAction}, {@link OpenDirectoryAction}. + * + * @author Werner Randelshofer + */ +public class CloseFileAction + extends + AbstractAction { + + /** + * This action's regular ID. + */ + public static final String ID = "file.close"; + /** + * This action's ID for the window being closed. + */ + public static final String ID_WINDOW_CLOSING = "windowClosing"; + /** + * This action's ID for the model being closed. + */ + public static final String ID_MODEL_CLOSING = "modelClosing"; + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + /** + * 0: Save file; 1: Don't save file; 2: Canceled. + */ + private int fileSaved; + private final OpenTCSView view; + + /** + * Creates a new instance. + * + * @param view The openTCS view + */ + @SuppressWarnings("this-escape") + public CloseFileAction(OpenTCSView view) { + this.view = requireNonNull(view, "view"); + + putValue(NAME, BUNDLE.getString("closeFileAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("closeFileAction.shortDescription")); + putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("alt F4")); + putValue(Action.MNEMONIC_KEY, Integer.valueOf('C')); + + ImageIcon icon = ImageDirectory.getImageIcon("/menu/document-close-4.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + } + + public int getFileSavedStatus() { + return fileSaved; + } + + @Override + public void actionPerformed(ActionEvent evt) { + final ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.app.Labels"); + + if (view.hasUnsavedChanges()) { + URI unsavedURI = view.getURI(); + String message + = "" + + labels.getFormatted( + "file.saveBefore.doYouWantToSave.message", + (unsavedURI == null) ? Kernel.DEFAULT_MODEL_NAME : URIUtil.getName(unsavedURI) + ) + + "

" + + labels.getString("file.saveBefore.doYouWantToSave.details") + + "

"; + + Object[] options = { + labels.getString("file.saveBefore.saveOption.text"), + labels.getString("file.saveBefore.dontSaveOption.text"), + labels.getString("file.saveBefore.cancelOption.text") + }; + + int option = JOptionPane.showOptionDialog( + view.getComponent(), + message, + "", + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE, + null, + options, + options[0] + ); + + fileSaved = JOptionPane.CANCEL_OPTION; + + switch (option) { + case JOptionPane.YES_OPTION: // Save + if (view.saveModel()) { + fileSaved = JOptionPane.YES_OPTION; + doIt(evt.getActionCommand(), view); + } + + break; + + case JOptionPane.NO_OPTION: // Don't save + fileSaved = JOptionPane.NO_OPTION; + doIt(evt.getActionCommand(), view); + break; + + default: + case JOptionPane.CANCEL_OPTION: + break; + } + } + else { + fileSaved = JOptionPane.NO_OPTION; + doIt(evt.getActionCommand(), view); + } + } + + protected void doIt(String actionCommand, View view) { + if (!actionCommand.equals(ID_MODEL_CLOSING) && view != null) { + if (view.isShowing()) { + view.setShowing(false); + JFrame f = (JFrame) SwingUtilities.getWindowAncestor(view.getComponent()); + f.setVisible(false); + f.remove(view.getComponent()); + f.dispose(); + } + + view.dispose(); + } + } + + protected void doIt(View view) { + doIt(ID_WINDOW_CLOSING, view); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/toolbar/OpenTCSConnectionTool.java b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/toolbar/OpenTCSConnectionTool.java new file mode 100644 index 0000000..cce71d8 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/application/toolbar/OpenTCSConnectionTool.java @@ -0,0 +1,208 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.modeleditor.jhotdraw.application.toolbar; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.modeleditor.util.I18nPlantOverviewModeling.TOOLBAR_PATH; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.Rectangle; +import java.awt.event.MouseEvent; +import java.awt.geom.Point2D; +import javax.swing.JOptionPane; +import javax.swing.undo.AbstractUndoableEdit; +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; +import org.jhotdraw.draw.ConnectionFigure; +import org.jhotdraw.draw.Drawing; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.tool.ConnectionTool; +import org.opentcs.guing.base.components.layer.LayerWrapper; +import org.opentcs.guing.base.model.AbstractConnectableModelComponent; +import org.opentcs.guing.common.components.drawing.figures.FigureConstants; +import org.opentcs.guing.common.components.drawing.figures.LabeledLocationFigure; +import org.opentcs.guing.common.components.drawing.figures.LinkConnection; +import org.opentcs.guing.common.components.drawing.figures.ModelBasedFigure; +import org.opentcs.guing.common.components.drawing.figures.SimpleLineConnection; +import org.opentcs.modeleditor.components.layer.ActiveLayerProvider; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * A tool to connect two figures with a path for instance. + */ +public class OpenTCSConnectionTool + extends + ConnectionTool { + + /** + * The resource bundle to use. + */ + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(TOOLBAR_PATH); + /** + * Provides the currently active layer. + */ + private final ActiveLayerProvider activeLayerProvider; + /** + * A localized name for this tool. The presentationName is displayed by the + * UndoableEdit. + */ + private final String presentationName; + + /** + * Creates a new instance. + * + * @param activeLayerProvider Provides the currently active layer. + * @param prototype The prototypical figure to be used. + */ + @Inject + public OpenTCSConnectionTool( + ActiveLayerProvider activeLayerProvider, + @Assisted + ConnectionFigure prototype + ) { + super(prototype); + this.activeLayerProvider = requireNonNull(activeLayerProvider, "activeLayerProvider"); + presentationName = BUNDLE.getString("openTcsConnectionTool.undo.presentationName"); + } + + @Override + public void mousePressed(MouseEvent evt) { + if (!activeLayerProvider.getActiveLayer().getLayer().isVisible() + || !activeLayerProvider.getActiveLayer().getLayer().isVisible()) { + JOptionPane.showMessageDialog( + evt.getComponent(), + BUNDLE.getString("openTcsConnectionTool.optionPane_activeLayerNotVisible.message"), + BUNDLE.getString("openTcsConnectionTool.optionPane_activeLayerNotVisible.title"), + JOptionPane.INFORMATION_MESSAGE + ); + return; + } + + super.mousePressed(evt); + } + + @Override // ConnectionTool + public void mouseReleased(MouseEvent event) { + if (!isValidConnection()) { + removeCreatedFigure(); + return; + } + + createdFigure.willChange(); + createdFigure.setStartConnector(startConnector); + createdFigure.setEndConnector(endConnector); + if (createdFigure instanceof SimpleLineConnection) { + ((SimpleLineConnection) createdFigure).getModel().updateName(); + } + + createdFigure.changed(); + + final Figure addedFigure = createdFigure; + final Drawing addedDrawing = getDrawing(); + + getDrawing().fireUndoableEditHappened(new AbstractUndoableEdit() { + + @Override + public String getPresentationName() { + return presentationName; + } + + @Override + public void undo() + throws CannotUndoException { + super.undo(); + addedDrawing.remove(addedFigure); + } + + @Override + public void redo() + throws CannotRedoException { + super.redo(); + addedDrawing.add(addedFigure); + } + }); + + targetFigure = null; + Point2D.Double cAnchor = startConnector.getAnchor(); + Rectangle r = new Rectangle(getView().drawingToView(cAnchor)); + r.grow(getAnchorWidth(), getAnchorWidth()); + fireAreaInvalidated(r); + cAnchor = endConnector.getAnchor(); + r = new Rectangle(getView().drawingToView(cAnchor)); + r.grow(getAnchorWidth(), getAnchorWidth()); + fireAreaInvalidated(r); + startConnector = null; + endConnector = null; + Figure finishedFigure = createdFigure; + createdFigure = null; + creationFinished(finishedFigure); + } + + @Override + protected ConnectionFigure createFigure() { + ConnectionFigure figure = super.createFigure(); + + if (figure instanceof ModelBasedFigure) { + if (figure instanceof LinkConnection) { + // Rather than using the active layer for links as well, get the location's layer instead. + LayerWrapper locationLayer + = ((LabeledLocationFigure) startConnector.getOwner()).getPresentationFigure().getModel() + .getPropertyLayerWrapper().getValue(); + ((ModelBasedFigure) figure).getModel().getPropertyLayerWrapper().setValue(locationLayer); + } + else { + ((ModelBasedFigure) figure).getModel() + .getPropertyLayerWrapper().setValue(activeLayerProvider.getActiveLayer()); + } + } + + return figure; + } + + private void removeCreatedFigure() { + if (createdFigure != null) { + getDrawing().remove(createdFigure); + // TODO: Also remove figure from the model + + Point2D.Double cAnchor = startConnector.getAnchor(); + Rectangle r = new Rectangle(getView().drawingToView(cAnchor)); + r.grow(getAnchorWidth(), getAnchorWidth()); + fireAreaInvalidated(r); + r = new Rectangle(getView().drawingToView(cAnchor)); + r.grow(getAnchorWidth(), getAnchorWidth()); + fireAreaInvalidated(r); + startConnector = null; + endConnector = null; + createdFigure = null; + } + + if (isToolDoneAfterCreation()) { + fireToolDone(); + } + } + + private boolean isValidConnection() { + // Prevent the manual creation of multiple connections with the same start and end connector. + return canConnect() && !connectionAlreadyPresent(); + } + + private boolean connectionAlreadyPresent() { + AbstractConnectableModelComponent startComponent + = (AbstractConnectableModelComponent) startConnector.getOwner().get(FigureConstants.MODEL); + AbstractConnectableModelComponent endComponent + = (AbstractConnectableModelComponent) endConnector.getOwner().get(FigureConstants.MODEL); + + return startComponent != null + && endComponent != null + && startComponent.hasConnectionTo(endComponent); + } + + private boolean canConnect() { + return createdFigure != null + && startConnector != null + && endConnector != null + && createdFigure.canConnect(startConnector, endConnector); + } +} diff --git a/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/components/drawing/OpenTCSDrawingViewModeling.java b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/components/drawing/OpenTCSDrawingViewModeling.java new file mode 100644 index 0000000..67296d7 --- /dev/null +++ b/opentcs-modeleditor/src/main/java/org/opentcs/thirdparty/modeleditor/jhotdraw/components/drawing/OpenTCSDrawingViewModeling.java @@ -0,0 +1,288 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.modeleditor.jhotdraw.components.drawing; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.awt.Graphics2D; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.jhotdraw.draw.DefaultDrawingView; +import org.jhotdraw.draw.Figure; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.application.ApplicationState; +import org.opentcs.guing.common.components.drawing.course.Origin; +import org.opentcs.guing.common.components.drawing.course.OriginChangeListener; +import org.opentcs.guing.common.components.drawing.figures.FigureConstants; +import org.opentcs.guing.common.components.drawing.figures.LabeledFigure; +import org.opentcs.guing.common.components.drawing.figures.ModelBasedFigure; +import org.opentcs.guing.common.components.drawing.figures.OriginFigure; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.modeleditor.components.drawing.BlockChangeHandler; +import org.opentcs.modeleditor.components.drawing.DeleteEdit; +import org.opentcs.modeleditor.components.drawing.PasteEdit; +import org.opentcs.modeleditor.components.layer.ActiveLayerProvider; +import org.opentcs.modeleditor.util.FigureCloner; +import org.opentcs.thirdparty.guing.common.jhotdraw.components.drawing.AbstractOpenTCSDrawingView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A DrawingView implementation for the openTCS plant overview. + */ +public class OpenTCSDrawingViewModeling + extends + AbstractOpenTCSDrawingView { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(OpenTCSDrawingViewModeling.class); + /** + * A helper for cloning figures. + */ + private final FigureCloner figureCloner; + /** + * The active layer provider. + */ + private final ActiveLayerProvider activeLayerProvider; + /** + * Contains figures currently in the buffer (eg when copying or cutting figures). + */ + private List
bufferedFigures = new ArrayList<>(); + /** + * Handles events for blocks. + */ + private final BlockChangeHandler blockChangeHandler; + + /** + * Creates new instance. + * + * @param appState Stores the application's current state. + * @param modelManager Provides the current system model. + * @param figureCloner A helper for cloning figures. + * @param activeLayerProvider The active layer provider. + * @param blockChangeHandler The handler for block changes. + */ + @Inject + public OpenTCSDrawingViewModeling( + ApplicationState appState, + ModelManager modelManager, + FigureCloner figureCloner, + ActiveLayerProvider activeLayerProvider, + BlockChangeHandler blockChangeHandler + ) { + super(appState, modelManager); + this.figureCloner = requireNonNull(figureCloner, "figureCloner"); + this.activeLayerProvider = requireNonNull(activeLayerProvider, "activeLayerProvider"); + this.blockChangeHandler = requireNonNull(blockChangeHandler, "blockChangeHandler"); + } + + @Override + public void cutSelectedItems() { + deleteSelectedFigures(); + } + + @Override + public void copySelectedItems() { + bufferedFigures = getDrawing().sort(getSelectedFigures()); + } + + @Override + public void pasteBufferedItems() { + clearSelection(); + + List
pastedFigures = new ArrayList<>(); + if (getDrawing().getChildren().containsAll(bufferedFigures)) { + pastedFigures.addAll(copyPasteBufferedItems()); + } + else if (Collections.disjoint(getDrawing().getChildren(), bufferedFigures)) { + pastedFigures.addAll(cutPasteBufferedItems()); + } + else { + // The list of buffered figures contains a mix of figures contained in the drawing and + // figures not contained in the drawing. This should never happen. + throw new IllegalStateException( + "Some figures to be pasted are already in the drawing, some " + + "are not." + ); + } + + placeFiguresOnActiveLayer(pastedFigures); + } + + @Override + public void delete() { + deleteSelectedFigures(); + + if (!bufferedFigures.isEmpty()) { + getDrawing().fireUndoableEditHappened(new DeleteEdit(this, bufferedFigures)); + } + } + + @Override + public void duplicate() { + copySelectedItems(); + pasteBufferedItems(); + } + + @Override + public void displayDriveOrders(VehicleModel vehicle, boolean visible) { + // Displaying drive orders is specific to operating mode + } + + @Override + public void followVehicle( + @Nonnull + final VehicleModel model + ) { + // Follwing a vehicle is not possible in modeling mode + } + + @Override + public void stopFollowVehicle() { + // Follwing a vehicle is not possible in modeling mode + } + + @Override + protected void drawTool(Graphics2D g2d) { + super.drawTool(g2d); + + if (getEditor() == null + || getEditor().getTool() == null + || getEditor().getActiveView() != this) { + return; + } + + // Set focus on the selected figure + highlightFocus(g2d); + } + + @Override + protected DefaultDrawingView.EventHandler createEventHandler() { + return new ExtendedEventHandler(); + } + + @Override + public void delete(Set components) { + List
figuresToDelete = components.stream() + .map(component -> getModelManager().getModel().getFigure(component)) + .collect(Collectors.toList()); + deleteFigures(figuresToDelete); + } + + @Override + public void setBlocks(ModelComponent blocks) { + synchronized (this) { + for (ModelComponent blockComp : blocks.getChildComponents()) { + BlockModel block = (BlockModel) blockComp; + block.addBlockChangeListener(blockChangeHandler); + } + } + } + + /** + * Message of the application that a block area was created. + * + * @param block The newly created block. + */ + public void blockAdded(BlockModel block) { + block.addBlockChangeListener(blockChangeHandler); + } + + /** + * Pastes the list of buffered figures by cloning them and adding the clones to the drawing. + * + * @return The list of pasted figures. (The cloned figures.) + */ + private List
copyPasteBufferedItems() { + // Create clones of all buffered figures + List
clonedFigures = figureCloner.cloneFigures(bufferedFigures); + addToSelection(clonedFigures); + getDrawing().fireUndoableEditHappened(new PasteEdit(this, clonedFigures)); + + return clonedFigures; + } + + /** + * Pastes the list of buffered figures by adding them to the drawing. + * + * @return The list of pasted figures. (The buffered figures.) + */ + private List
cutPasteBufferedItems() { + for (Figure deletedFigure : bufferedFigures) { + getDrawing().add(deletedFigure); + } + getDrawing().fireUndoableEditHappened(new PasteEdit(this, bufferedFigures)); + + return bufferedFigures; + } + + private void placeFiguresOnActiveLayer(List
figures) { + for (Figure figure : figures) { + if (figure instanceof ModelBasedFigure) { + ((ModelBasedFigure) figure).getModel() + .getPropertyLayerWrapper().setValue(activeLayerProvider.getActiveLayer()); + } + else if (figure instanceof LabeledFigure) { + ((LabeledFigure) figure).getPresentationFigure().getModel() + .getPropertyLayerWrapper().setValue(activeLayerProvider.getActiveLayer()); + } + } + } + + private void deleteSelectedFigures() { + final List
deletedFigures = getDrawing().sort(getSelectedFigures()); + deleteFigures(deletedFigures); + } + + private void deleteFigures(List
figures) { + // Abort, if not all of the selected figures may be removed from the drawing + for (Figure figure : figures) { + if (!figure.isRemovable()) { + LOG.warn("Figure is not removable: {}. Aborting.", figure); + return; + } + } + + bufferedFigures = figures; + clearSelection(); + + for (Figure figure : figures) { + if (figure instanceof OriginChangeListener) { + Origin ref = figure.get(FigureConstants.ORIGIN); + + if (ref != null) { + ref.removeListener((OriginChangeListener) figure); + figure.set(FigureConstants.ORIGIN, null); + } + } + + getDrawing().remove(figure); + } + } + + private class ExtendedEventHandler + extends + AbstractExtendedEventHandler { + + /** + * Creates a new instance. + */ + ExtendedEventHandler() { + } + + @Override + protected boolean shouldShowFigure(Figure figure) { + return !(figure instanceof OriginFigure); + } + } +} diff --git a/opentcs-modeleditor/src/main/resources/REUSE.toml b/opentcs-modeleditor/src/main/resources/REUSE.toml new file mode 100644 index 0000000..42aab58 --- /dev/null +++ b/opentcs-modeleditor/src/main/resources/REUSE.toml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC0-1.0 + +version = 1 + +[[annotations]] +path = ["**/*.gif", "**/*.jpg", "**/*.png", "**/*.svg"] +precedence = "closest" +SPDX-FileCopyrightText = "The openTCS Authors" +SPDX-License-Identifier = "CC-BY-4.0" diff --git a/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/dialogs/createGroup.properties b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/dialogs/createGroup.properties new file mode 100644 index 0000000..353b4c0 --- /dev/null +++ b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/dialogs/createGroup.properties @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +createGroupPanel.button_addElements.text=Add selected elements +createGroupPanel.button_cancel.text=Cancel +createGroupPanel.button_createGroup.text=Create group +createGroupPanel.button_removeElements.text=Remove selected elements +createGroupPanel.label_locations.text=Locations: +createGroupPanel.label_paths.text=Paths: +createGroupPanel.label_points.text=Points: +createGroupPanel.label_selectMembers.text=Select group elements: +createGroupPanel.label_selectedMembers.text=Selected group elements: +createGroupPanel.optionPane_noElementsSelected.message=No elements selected. +createGroupPanel.title=Group creation diff --git a/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/dialogs/createGroup_de.properties b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/dialogs/createGroup_de.properties new file mode 100644 index 0000000..494d75e --- /dev/null +++ b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/dialogs/createGroup_de.properties @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +createGroupPanel.button_addElements.text=Selektierte Elemente hinzuf\u00fcgen +createGroupPanel.button_cancel.text=Abbrechen +createGroupPanel.button_createGroup.text=Gruppe erstellen +createGroupPanel.button_removeElements.text=Selektierte Elemente l\u00f6schen +createGroupPanel.label_locations.text=Stationen: +createGroupPanel.label_paths.text=Pfade: +createGroupPanel.label_points.text=Punkte: +createGroupPanel.label_selectMembers.text=Gruppenelemente ausw\u00e4hlen: +createGroupPanel.label_selectedMembers.text=Ausgew\u00e4hlte Gruppenelemente: +createGroupPanel.optionPane_noElementsSelected.message=Keine Elemente ausgew\u00e4hlt. +createGroupPanel.title=Gruppenerstellung diff --git a/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/mainMenu.properties b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/mainMenu.properties new file mode 100644 index 0000000..1c95cfa --- /dev/null +++ b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/mainMenu.properties @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +aboutAction.name=About +aboutAction.optionPane_applicationInformation.message.baselineVersion=openTCS baseline version: {0} +aboutAction.optionPane_applicationInformation.message.copyright=Copyright (c) the openTCS authors and contributors +aboutAction.optionPane_applicationInformation.message.customization=openTCS customization: {0} {1} +aboutAction.optionPane_applicationInformation.message.mode=Mode: {0} +aboutAction.optionPane_applicationInformation.message.runningOn=Running on +aboutAction.optionPane_applicationInformation.title=About +actionsMenu.menuItem_ignorePreciseOrientation.text=Ignore orientation angle +actionsMenu.menuItem_ignorePrecisePosition.text=Ignore precise positions +actionsMenu.text=Actions +actionsMenu.tooltipText=Actions +addBitmapAction.name=Add background image... +calculatePathLengthMenuItem.text=Recalculate path lengths +clearSelectionAction.name=Deselect All +clearSelectionAction.shortDescription=Deselect all figures in the drawing +closeFileAction.name=Close +closeFileAction.shortDescription=Close Plant Overview +copyAction.name=Copy +copyAction.shortDescription=Copy the selected figures to the clipboard +cutAction.name=Cut +cutAction.shortDescription=Cut the selected figures +deleteEdit.presentationName=Delete +duplicateAction.name=Duplicate +duplicateAction.shortDescription=Duplicate the selected figures +editMenu.text=Edit +editMenu.tooltipText=Edit +fileExportMenu.text=Export plant model +fileImportMenu.text=Import plant model +fileMenu.text=File +fileMenu.tooltipText=File +helpMenu.text=? +helpMenu.tooltipText=Help +layoutToModelMenuItem.text=Copy layout values to model +loadModelAction.name=Load Model... +loadModelAction.shortDescription=Load an existing model +downloadModelFromKernelAction.name=Download model from kernel +downloadModelFromKernelAction.shortDescription=Connects with the kernel in order to download its model. +modelToLayoutMenuItem.text=Copy model values to layout +newModelAction.name=New Model +newModelAction.shortDescription=Create a new Model +pasteAction.name=Paste +pasteAction.shortDescription=Paste figures from clipboard +pasteEdit.presentationName=Paste +uploadModelToKernelAction.name=Upload model to kernel +uploadModelToKernelAction.shortDescription=Connects with the kernel in order to upload the current (local) model to it. +viewMenu.menuItem_restoreWindowArrangement.text=Reset window arrangement +viewMenu.text=View +viewMenu.tooltipText=View +viewToolBarsMenu.text=Tool Bars diff --git a/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/mainMenu_de.properties b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/mainMenu_de.properties new file mode 100644 index 0000000..32d9169 --- /dev/null +++ b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/mainMenu_de.properties @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +aboutAction.name=Info +aboutAction.optionPane_applicationInformation.message.baselineVersion=openTCS Basisversion: {0} +aboutAction.optionPane_applicationInformation.message.customization=openTCS Erweiterung: {0} {1} +aboutAction.optionPane_applicationInformation.message.mode=Modus: {0} +aboutAction.optionPane_applicationInformation.message.runningOn=Ausgef\u00fchrt durch +aboutAction.optionPane_applicationInformation.title=\u00dcber +actionsMenu.menuItem_ignorePreciseOrientation.text=Orientierungswinkel ignorieren +actionsMenu.menuItem_ignorePrecisePosition.text=Pr\u00e4zise Positionen ignorieren +actionsMenu.text=Aktionen +actionsMenu.tooltipText=Aktionen +addBitmapAction.name=Hintergrundbild hinzuf\u00fcgen... +calculatePathLengthMenuItem.text=Pfadl\u00e4ngen neu berechnen +clearSelectionAction.name=Auswahl aufheben +clearSelectionAction.shortDescription=Auswahl aufheben +closeFileAction.name=Beenden +closeFileAction.shortDescription=Anlagen\u00fcbersicht schlie\u00dfen +copyAction.name=Kopieren +copyAction.shortDescription=Selektierte Figuren in Zwischenablage kopieren +cutAction.name=Ausschneiden +cutAction.shortDescription=Selektierte Figuren ausschneiden +deleteEdit.presentationName=L\u00f6schen +duplicateAction.name=Duplizieren +duplicateAction.shortDescription=Selektierte Figuren duplizieren +editMenu.text=Bearbeiten +editMenu.tooltipText=Bearbeiten +fileExportMenu.text=Anlagenmodell exportieren +fileImportMenu.text=Anlagenmodell importieren +fileMenu.text=Datei +fileMenu.tooltipText=Datei +helpMenu.tooltipText=Hilfe +layoutToModelMenuItem.text=Layout-Werte ins Modell \u00fcbernehmen +loadModelAction.name=Anlagenmodell laden... +loadModelAction.shortDescription=Ein bestehendes Anlagenmodell laden +downloadModelFromKernelAction.name=Modell von Kernel herunterladen +downloadModelFromKernelAction.shortDescription=Stellt eine Verbindung mit dem Kernel her, um dessen aktuelles Modell herunterzuladen. +modelToLayoutMenuItem.text=Modell-Werte ins Layout \u00fcbernehmen +newModelAction.name=Neues Modell +newModelAction.shortDescription=Ein neues Modell erzeugen +pasteAction.name=Einf\u00fcgen +pasteAction.shortDescription=Figuren aus Zwischenablage einf\u00fcgen +pasteEdit.presentationName=Einf\u00fcgen +uploadModelToKernelAction.name=Modell zu Kernel hochladen +uploadModelToKernelAction.shortDescription=Stellt eine Verbindung mit dem Kernel her, um das aktuelle (lokale) Modell dorthin hochzuladen. +viewMenu.menuItem_restoreWindowArrangement.text=Fensteranordnung zur\u00fccksetzen +viewMenu.text=Ansicht +viewMenu.tooltipText=Ansicht +viewToolBarsMenu.text=Werkzeugleisten diff --git a/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/miscellaneous.properties b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/miscellaneous.properties new file mode 100644 index 0000000..b5ea629 --- /dev/null +++ b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/miscellaneous.properties @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +layoutToModelCoordinateUndoActivity.presentationName=Coordinates +modelToLayoutCoordinateUndoActivity.presentationName=Transformation +openTcsModelManagerModeling.message_notExported.text=The model could not be exported due to an error. +openTcsModelManagerModeling.message_notImported.text=The model could not be imported due to an error. +openTcsModelManagerModeling.message_notLoaded.text=Invalid openTCS model file "{0}". +openTcsModelManagerModeling.message_notSaved.text=An error occurred while saving the model. The model was not saved. +openTcsView.applicationName.text=Model Editor +openTcsView.dialog_saveModelConfirmation.message=The Kernel is in modelling mode. Please make sure no one else is editing this model right now, because problems may arise. Do you still want so save it? +openTcsView.dialog_saveModelConfirmation.title=Do you really want to save? +openTcsView.message_kernelConnectionLost.text=The connection to the kernel was lost. Switching to {0}. +openTcsView.message_modelDownloaded.text=Model "{0}" successfully downloaded from kernel. +openTcsView.message_modelUploaded.text=Model "{0}" successfully uploaded to kernel. +openTcsView.optionPane_cannotDeleteLocationType.message=The location type cannot be deleted\nbecause at least one location of this type\nexists in this model. +openTcsView.optionPane_cannotDeleteLocationType.title=Delete not allowed +openTcsView.optionPane_saveModelBeforeUpload.message=The model needs to be saved locally before you can upload it to the kernel. +openTcsView.panel_modellingDrawingView.title=Modelling view +openTcsView.panel_operatingDrawingView.title=Driving course +openTcsView.panel_operatingOrderSequencesView.title=Transport order sequences +openTcsView.panel_operatingTransportOrdersView.title=Transport orders +pathTypeSelection.title=Select path types +pathTypeSelection.allTypes_label.text=All types: diff --git a/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/miscellaneous_de.properties b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/miscellaneous_de.properties new file mode 100644 index 0000000..a420e0a --- /dev/null +++ b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/miscellaneous_de.properties @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +layoutToModelCoordinateUndoActivity.presentationName=Koordinaten +modelToLayoutCoordinateUndoActivity.presentationName=Transformation +openTcsModelManagerModeling.message_notExported.text=Das Modell konnte wegen eines Fehlers nicht exportiert werden. +openTcsModelManagerModeling.message_notImported.text=Das Modell konnte wegen eines Fehlers nicht importiert werden. +openTcsModelManagerModeling.message_notLoaded.text=Ung\u00fcltige openTCS Modelldatei "{0}". +openTcsModelManagerModeling.message_notSaved.text=Ein Fehler ist aufgetreten w\u00e4hrend des Speicherns des Modells. Das Modell wurde nicht gespeichert. +openTcsView.applicationName.text=Model Editor +openTcsView.dialog_saveModelConfirmation.message=Der Kernel ist im Modellierungs-Modus. Bitte stellen Sie sicher, dass sonst niemand an diesem Modell arbeitet, da es sonst zu Problemen kommen kann. M\u00f6chten Sie das Modell dennoch speichern? +openTcsView.dialog_saveModelConfirmation.title=M\u00f6chten Sie wirklich speichern? +openTcsView.message_kernelConnectionLost.text=Die Verbindung zum Kernel ist verloren gegangen. Wechsle in den {0}. +openTcsView.message_modelDownloaded.text=Modell "{0}" erfolgreich vom Kernel heruntergeladen. +openTcsView.message_modelUploaded.text=Modell "{0}" erfolgreich zum Kernel hochgeladen. +openTcsView.optionPane_cannotDeleteLocationType.message=Der Stationstyp kann nicht entfernt werden,\nda im Anlagenmodell noch mindestens eine Station\ndieses Typs existiert. +openTcsView.optionPane_cannotDeleteLocationType.title=L\u00f6schen nicht m\u00f6glich +openTcsView.optionPane_saveModelBeforeUpload.message=Das Modell muss lokal gespeichert werden, bevor es zum Kernel hochgeladen werden kann. +openTcsView.panel_modellingDrawingView.title=Modellierungsansicht +openTcsView.panel_operatingDrawingView.title=Fahrkurs +openTcsView.panel_operatingOrderSequencesView.title=Transportauftragsketten +openTcsView.panel_operatingTransportOrdersView.title=Transportauftr\u00e4ge +pathTypeSelection.title=W\u00e4hle Pfadtypen aus +pathTypeSelection.allTypes_label.text=Alle Typen: diff --git a/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/modelValidation.properties b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/modelValidation.properties new file mode 100644 index 0000000..6606283 --- /dev/null +++ b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/modelValidation.properties @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +modelValidator.dialog_validationWarning.message.loadingError=There were problems while loading/downloading the model: +modelValidator.dialog_validationWarning.message.savingError=There were problems while uploading/exporting the model: +modelValidator.dialog_validationWarning.title=Warning +modelValidator.error_blockElementsBotExisting.text=Block element "{0}" does not exist. +modelValidator.error_blockElementsDuplicate.text=Block element "{0}" is listed multiple times. +modelValidator.error_componentNameExists.text=Component name "{0}" used multiple times. +modelValidator.error_componentNameInvalid.text=Invalid name "{0}". +modelValidator.error_componentNull.text=The model component to validate is null. +modelValidator.error_deserialization.text=Deserialization errors for component {0}: {1} +modelValidator.error_groupElementsDuplicate.text=Group element "{0}" is listed multiple times. +modelValidator.error_groupElementsNotExisting.text=Group element "{0}" does not exist. +modelValidator.error_linkEndComponentNotExisting.text=End component "{0}" does not exist. +modelValidator.error_linkStartComponentNotExisting.text=Start component "{0}" does not exist. +modelValidator.error_locationTypeInvalid.text=Invalid type of location "{0}" - not found. +modelValidator.error_modelNull.text=The system model is null. +modelValidator.error_pathEndComponentNotExisting.text=End component "{0}" does not exist. +modelValidator.error_pathStartComponentNotExisting.text=Start component "{0}" does not exist. +modelValidator.error_vehicleCurrentPointNotExisting.text=Current point "{0}" is invalid. +modelValidator.error_vehicleNextPointNotExisting.text=Next point "{0}" is invalid. diff --git a/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/modelValidation_de.properties b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/modelValidation_de.properties new file mode 100644 index 0000000..4496986 --- /dev/null +++ b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/modelValidation_de.properties @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +modelValidator.dialog_validationWarning.message.loadingError=Problem beim Laden/Herunterladen des Modells: +modelValidator.dialog_validationWarning.message.savingError=Problem beim Hochladen/Exportieren des Modells: +modelValidator.dialog_validationWarning.title=Warnung +modelValidator.error_blockElementsBotExisting.text=Das Element "{0}" existiert mehrfach in dem Block. +modelValidator.error_blockElementsDuplicate.text=Das Element "{0}" existiert mehrfach in dem Block. +modelValidator.error_componentNameExists.text=Der Name der Modellkomponente "{0}" existiert mehrfach. +modelValidator.error_componentNameInvalid.text=Ung\u00fcltiger Name "{0}". +modelValidator.error_componentNull.text=Die Modellkomponente, welche validiert werden soll, ist null. +modelValidator.error_deserialization.text=Deserialization errors for component {0}: {1} +modelValidator.error_groupElementsDuplicate.text=Das Element "{0}" existiert mehrfach in der Gruppe. +modelValidator.error_groupElementsNotExisting.text=Das Element "{0}" existiert nicht im Modell. +modelValidator.error_linkEndComponentNotExisting.text=Die Station mit Namen "{0}" existiert nicht. +modelValidator.error_linkStartComponentNotExisting.text=Der Startpunkt "{0}" existiert nicht. +modelValidator.error_locationTypeInvalid.text=Der Stationstyp ist ung\u00fcltig: "{0}". +modelValidator.error_modelNull.text=Das Anlagenmodell ist null. +modelValidator.error_pathEndComponentNotExisting.text=Der Zielpunkt "{0}" existiert nicht. +modelValidator.error_pathStartComponentNotExisting.text=Der Startpunkt "{0}" existiert nicht. +modelValidator.error_vehicleCurrentPointNotExisting.text=Der aktuelle Punkt "{0}" ist ung\u00fcltig. +modelValidator.error_vehicleNextPointNotExisting.text=Der n\u00e4chste Punkt "{0}" ist ung\u00fcltig. diff --git a/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/panels/dockable.properties b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/panels/dockable.properties new file mode 100644 index 0000000..220a924 --- /dev/null +++ b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/panels/dockable.properties @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +dockingManagerModeling.panel_blocks.title=Blocks +dockingManagerModeling.panel_components.title=Components +dockingManagerModeling.panel_layers.title=Layers +dockingManagerModeling.panel_layerGroups.title=Layer groups +dockingManagerModeling.panel_properties.title=Properties diff --git a/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/panels/dockable_de.properties b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/panels/dockable_de.properties new file mode 100644 index 0000000..d770848 --- /dev/null +++ b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/panels/dockable_de.properties @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +dockingManagerModeling.panel_blocks.title=Blockbereiche +dockingManagerModeling.panel_components.title=Komponenten +dockingManagerModeling.panel_layers.title=Ebenen +dockingManagerModeling.panel_layerGroups.title=Ebenengruppen +dockingManagerModeling.panel_properties.title=Eigenschaften diff --git a/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/panels/layers.properties b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/panels/layers.properties new file mode 100644 index 0000000..5daf79f --- /dev/null +++ b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/panels/layers.properties @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +layerGroupsPanel.button_addGroup.tooltipText=Add layer group +layerGroupsPanel.button_removeGroup.tooltipText=Remove layer group +layerGroupsPanel.optionPane_confirmGroupAndAssignedLayersRemoval.message=Removing a group results in all layers assigned to it to be removed as well.\nRemember: Removing a layer results in the model components it contains to be removed as well.\nAre you sure you want to remove the selected group? +layerGroupsPanel.optionPane_confirmGroupAndAssignedLayersRemoval.title=Confirm group removal +layerGroupsPanel.optionPane_groupRemovalNotPossible.message=Removing a group results in all layers assigned to it to be removed as well.\nAll layers in the model are assigned to the selected group, which means that after removing the group, the model would no longer contain any layers.\nAssign at least one layer to another group.\n +layerGroupsPanel.optionPane_groupRemovalNotPossible.title=Group removal not possible +layersPanel.button_addLayer.tooltipText=Add layer +layersPanel.button_moveLayerDown.tooltipText=Move layer down +layersPanel.button_moveLayerUp.tooltipText=Move layer up +layersPanel.button_removeLayer.tooltipText=Remove layer +layersPanel.optionPane_confirmLayerWithComponentsRemoval.message=The selected layer contains model components.\nRemoving a layer results in the model components it contains to be removed as well.\nAre you sure you want to remove the selected layer? +layersPanel.optionPane_confirmLayerWithComponentsRemoval.title=Confirm layer removal +layersTableModel.column_active.headerText=Active +layersTableModel.column_group.headerText=Group +layersTableModel.column_groupVisible.headerText=Group visible +layersTableModel.column_name.headerText=Name +layersTableModel.column_ordinal.headerText=Ordinal +layersTableModel.column_visible.headerText=Visible \ No newline at end of file diff --git a/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/panels/layers_de.properties b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/panels/layers_de.properties new file mode 100644 index 0000000..103d8c3 --- /dev/null +++ b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/panels/layers_de.properties @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +layerGroupsPanel.button_addGroup.tooltipText=Ebenengruppe hinzuf\u00fcgen +layerGroupsPanel.button_removeGroup.tooltipText=Ebenengruppe entfernen +layerGroupsPanel.optionPane_confirmGroupAndAssignedLayersRemoval.message=Das Entfernen einer Gruppe f\u00fchrt dazu, dass alle ihr zugewiesenen Ebenen ebenfalls entfernt werden.\nZur Erinnerung: Das Entfernen einer Ebene f\u00fchrt dazu, dass die darin enthaltenen Modellkomponenten ebenfalls entfernt werden.\nSind Sie sicher, dass Sie die ausgew\u00e4hlte Gruppe entfernen m\u00f6chten? +layerGroupsPanel.optionPane_confirmGroupAndAssignedLayersRemoval.title=Gruppenentfernung best\u00e4tigen +layerGroupsPanel.optionPane_groupRemovalNotPossible.message=Das Entfernen einer Gruppe f\u00fchrt dazu, dass alle ihr zugewiesenen Ebenen ebenfalls entfernt werden.\nAlle Ebenen im Modell sind der ausgew\u00e4hlten Gruppe zugeordnet, was bedeutet, dass das Modell nach dem Entfernen der Gruppe keine Ebenen mehr enthalten w\u00fcrde.\nWeisen Sie mindestens einer Ebene eine andere Gruppe zu. +layerGroupsPanel.optionPane_groupRemovalNotPossible.title=Gruppenentfernung nicht m\u00f6glich +layersPanel.button_addLayer.tooltipText=Ebene hinzuf\u00fcgen +layersPanel.button_moveLayerDown.tooltipText=Ebene nach unten verschieben +layersPanel.button_moveLayerUp.tooltipText=Ebene nach oben verschieben +layersPanel.button_removeLayer.tooltipText=Ebene entfernen +layersPanel.optionPane_confirmLayerWithComponentsRemoval.message=Die ausgew\u00e4hlte Ebene enth\u00e4lt Modellkomponenten.\nDas Entfernen einer Ebene f\u00fchrt dazu, dass die darin enthaltenen Modellkomponenten ebenfalls entfernt werden.\nSind Sie sicher, dass Sie die ausgew\u00e4hlte Ebene entfernen m\u00f6chten? +layersPanel.optionPane_confirmLayerWithComponentsRemoval.title=Ebenenentfernung best\u00e4tigen +layersTableModel.column_active.headerText=Aktiv +layersTableModel.column_group.headerText=Gruppe +layersTableModel.column_groupVisible.headerText=Gruppe sichtbar +layersTableModel.column_name.headerText=Name +layersTableModel.column_ordinal.headerText=Ordnungszahl +layersTableModel.column_visible.headerText=Sichtbar diff --git a/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/system.properties b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/system.properties new file mode 100644 index 0000000..4f0c7b1 --- /dev/null +++ b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/system.properties @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +openTcsSdiApplication.frameTitle_connectedTo.text=Connected to: diff --git a/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/system_de.properties b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/system_de.properties new file mode 100644 index 0000000..3571670 --- /dev/null +++ b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/system_de.properties @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +openTcsSdiApplication.frameTitle_connectedTo.text=Verbunden mit: diff --git a/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/toolbar.properties b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/toolbar.properties new file mode 100644 index 0000000..ebd50d1 --- /dev/null +++ b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/toolbar.properties @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +alignAction.east.shortDescription=Align right +alignAction.horizontal.shortDescription=Align horizontal +alignAction.north.shortDescription=Align top +alignAction.south.shortDescription=Align bottom +alignAction.undo.presentationName=Align +alignAction.vertical.shortDescription=Align vertical +alignAction.west.shortDescription=Align left +applyAttributesAction.name=Apply attributes +applyAttributesAction.shortDescription=Apply attributes +applyAttributesAction.undo.presentationName=Apply attributes +bringToFrontAction.name=Bring to front +bringToFrontAction.shortDescription=Bring to front +bringToFrontAction.undo.presentationName=Bring to front +buttonFactory.action_fontStyleBold.undo.presentationName=Bold +buttonFactory.action_fontStyleItalic.undo.presentationName=Italic +buttonFactory.action_fontStyleUnderline.undo.presentationName=Underline +buttonFactory.action_noColor.name=No Color +buttonFactory.action_noColor.shortDescription=No Color +buttonFactory.action_strokeCapButt.name=Butt +buttonFactory.action_strokeCapRound.name=Round +buttonFactory.action_strokeCapSquare.name=Square +buttonFactory.action_strokeJoinBevel.name=Bevel +buttonFactory.action_strokeJoinMiter.name=Miter +buttonFactory.action_strokeJoinRound.name=Round +buttonFactory.action_strokePlacementCenter.name=Center +buttonFactory.action_strokePlacementCenterFilled.name=Center filled +buttonFactory.action_strokePlacementCenterUnfilled.name=Center unfilled +buttonFactory.action_strokePlacementInside.name=Inside +buttonFactory.action_strokePlacementInsideFilled.name=Inside filled +buttonFactory.action_strokePlacementInsideUnfilled.name=Inside unfilled +buttonFactory.action_strokePlacementOutside.name=Outside +buttonFactory.action_strokePlacementOutsideFilled.name=Outside filled +buttonFactory.action_strokePlacementOutsideUnfilled.name=Outside unfilled +buttonFactory.action_strokeTypeBasic.name=Basic stroke +buttonFactory.action_strokeTypeDouble.name=Double stroke +buttonFactory.action_strokeWidth.undo.presentationName=Stroke width +buttonFactory.button_fillColor.tooltipText=Fill color +buttonFactory.button_font.tooltipText=Font +buttonFactory.button_fontStyleBold.tooltipText=Bold +buttonFactory.button_fontStyleItalic.tooltipText=Italic +buttonFactory.button_fontStyleUnderline.tooltipText=Underline +buttonFactory.button_lineColor.tooltipText=Line color +buttonFactory.button_strokeCap.tooltipText=Line caps +buttonFactory.button_strokeDashes.tooltipText=Line dashes +buttonFactory.button_strokeDecoration.tooltipText=Arrow tips +buttonFactory.button_strokeJoin.tooltipText=Stroke joint +buttonFactory.button_strokePlacement.tooltipText=Stroke placement +buttonFactory.button_strokeType.tooltipText=Stroke type +buttonFactory.button_strokeWidth.tooltipText=Stroke width +buttonFactory.button_textlColor.tooltipText=Text color +createBlockAction.name=Block +createBlockAction.shortDescription=Creates a block area +createLocationTypeAction.name=Location type +createLocationTypeAction.shortDescription=Creates a location type +createVehicleAction.name=Vehicle +createVehicleAction.shortDescription=Creates a vehicle +editorColorChooserAction.dialog_colorSelection.title=Choose color +editorColorChooserAction.shortDescription=Open color chooser +openTcsConnectionTool.optionPane_activeLayerNotVisible.message=Creation of model components on hidden layers or layers in a hidden layer group is not possible. +openTcsConnectionTool.optionPane_activeLayerNotVisible.title=Set active layer visible +openTcsConnectionTool.undo.presentationName=Create Connection +openTcsCreationTool.optionPane_activeLayerNotVisible.message=Creation of model components on hidden layers or layers in a hidden layer group is not possible. +openTcsCreationTool.optionPane_activeLayerNotVisible.title=Set active layer visible +pickAttributesAction.name=Pick attributes +pickAttributesAction.shortDescription=Pick attributes +sendToBackAction.name=Send to back +sendToBackAction.shortDescription=Send to back +sendToBackAction.undo.presentationName=Send to back +toolBarManager.button_createLink.tooltipText=Creates a link between a point and a location +toolBarManager.button_createLocation.tooltipText=Creates a location +toolBarManager.button_dragTool.tooltipText=Moves the model view +toolBarManager.button_selectionTool.tooltipText=Selects a component +toolBarManager.toolbar_alignment.title=Align +toolBarManager.toolbar_drawing.title=Draw diff --git a/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/toolbar_de.properties b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/toolbar_de.properties new file mode 100644 index 0000000..5ff28b6 --- /dev/null +++ b/opentcs-modeleditor/src/main/resources/i18n/org/opentcs/plantoverview/modeling/toolbar_de.properties @@ -0,0 +1,70 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +alignAction.east.shortDescription=Nach rechts ausrichten +alignAction.horizontal.shortDescription=Horizontal ausrichten +alignAction.north.shortDescription=Nach oben ausrichten +alignAction.south.shortDescription=Nach unten ausrichten +alignAction.undo.presentationName=Ausrichten +alignAction.vertical.shortDescription=Vertikal ausrichten +alignAction.west.shortDescription=Nach links ausrichten +applyAttributesAction.shortDescription=Attribute anwenden +bringToFrontAction.name=In den Vordergrund +bringToFrontAction.shortDescription=In den Vordergrund +bringToFrontAction.undo.presentationName=In den Vordergrund +buttonFactory.action_noColor.name=Keine Farbe +buttonFactory.action_strokeCapButt.name=Abgeschnitten +buttonFactory.action_strokeCapRound.name=Rund +buttonFactory.action_strokeCapSquare.name=Quadratisch +buttonFactory.action_strokeJoinBevel.name=Abgeschnitten +buttonFactory.action_strokeJoinMiter.name=Gerade +buttonFactory.action_strokeJoinRound.name=Rund +buttonFactory.action_strokePlacementCenter.name=Mitte +buttonFactory.action_strokePlacementCenterFilled.name=Mitte unterf\u00fcllt +buttonFactory.action_strokePlacementCenterUnfilled.name=Mitte nicht unterf\u00fcllt +buttonFactory.action_strokePlacementInside.name=Innen +buttonFactory.action_strokePlacementInsideFilled.name=Innen unterf\u00fcllt +buttonFactory.action_strokePlacementInsideUnfilled.name=Innen nicht unterf\u00fcllt +buttonFactory.action_strokePlacementOutside.name=Aussen +buttonFactory.action_strokePlacementOutsideFilled.name=Aussen unterf\u00fcllt +buttonFactory.action_strokePlacementOutsideUnfilled.name=Aussen nicht unterf\u00fcllt +buttonFactory.action_strokeTypeBasic.name=Einfache Linie +buttonFactory.action_strokeTypeDouble.name=Doppelte Linie +buttonFactory.action_strokeWidth.undo.presentationName=Linient\u00e4rke +buttonFactory.button_fillColor.tooltipText=F\u00fcllfarbe +buttonFactory.button_font.tooltipText=Schriftart +buttonFactory.button_fontStyleBold.tooltipText=Fett +buttonFactory.button_fontStyleItalic.tooltipText=Kursiv +buttonFactory.button_fontStyleUnderline.tooltipText=Unterstreichen +buttonFactory.button_lineColor.tooltipText=Linienfarbe +buttonFactory.button_strokeCap.tooltipText=Linienenden +buttonFactory.button_strokeDashes.tooltipText=Linienmuster +buttonFactory.button_strokeDecoration.tooltipText=Pfeilspitzen +buttonFactory.button_strokeJoin.tooltipText=Linienverbindungen +buttonFactory.button_strokePlacement.tooltipText=Linien-Platzierung +buttonFactory.button_strokeType.tooltipText=Linientyp +buttonFactory.button_strokeWidth.tooltipText=Linienst\u00e4rke +buttonFactory.button_textlColor.tooltipText=Textfarbe +createBlockAction.name=Block +createBlockAction.shortDescription=Erstellt eine Blockstrecke +createLocationTypeAction.name=Stations-Typ +createLocationTypeAction.shortDescription=Erstellt einen Stations-Typ +createVehicleAction.name=Fahrzeug +createVehicleAction.shortDescription=Erstellt ein Fahrzeug +editorColorChooserAction.dialog_colorSelection.title=Bitte Farbe w\u00e4hlen +editorColorChooserAction.shortDescription=Farbdialog \u00f6ffnen +openTcsConnectionTool.optionPane_activeLayerNotVisible.message=Die Erstellung von Modellkomponenten auf ausgeblendeten Ebenen oder Ebenen, die sich in einer ausgeblendeten Ebenengruppe befinden, ist nicht m\u00f6glich. +openTcsConnectionTool.optionPane_activeLayerNotVisible.title=Aktive Ebene einblenden +openTcsConnectionTool.undo.presentationName=Verbindung erzeugen +openTcsCreationTool.optionPane_activeLayerNotVisible.message=Die Erstellung von Modellkomponenten auf ausgeblendeten Ebenen oder Ebenen, die sich in einer ausgeblendeten Ebenengruppe befinden, ist nicht m\u00f6glich. +openTcsCreationTool.optionPane_activeLayerNotVisible.title=Aktive Ebene einblenden +pickAttributesAction.shortDescription=Attribute kopieren +sendToBackAction.name=In den Hintergrund +sendToBackAction.shortDescription=In den Hintergrund +sendToBackAction.undo.presentationName=In den Hintergrund +toolBarManager.button_createLink.tooltipText=Erstellt eine Verbindung zwischen einem Punkt und einer Station +toolBarManager.button_createLocation.tooltipText=Erstellt eine Station +toolBarManager.button_dragTool.tooltipText=Verschiebt die Modellansicht +toolBarManager.button_selectionTool.tooltipText=W\u00e4hlt eine Komponente aus +toolBarManager.toolbar_alignment.title=Ausrichten +toolBarManager.toolbar_drawing.title=Zeichnen diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/layer/create-layer-group.16.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/layer/create-layer-group.16.png new file mode 100644 index 0000000..f7d92f1 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/layer/create-layer-group.16.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/layer/create-layer.16.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/layer/create-layer.16.png new file mode 100644 index 0000000..f7d92f1 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/layer/create-layer.16.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/layer/delete-layer-group.16.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/layer/delete-layer-group.16.png new file mode 100644 index 0000000..681ee60 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/layer/delete-layer-group.16.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/layer/delete-layer.16.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/layer/delete-layer.16.png new file mode 100644 index 0000000..681ee60 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/layer/delete-layer.16.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/layer/move-layer-down.16.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/layer/move-layer-down.16.png new file mode 100644 index 0000000..be85f55 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/layer/move-layer-down.16.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/layer/move-layer-up.16.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/layer/move-layer-up.16.png new file mode 100644 index 0000000..cb931e0 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/layer/move-layer-up.16.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/arrow-down-3.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/arrow-down-3.png new file mode 100644 index 0000000..f5276c1 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/arrow-down-3.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/arrow-up-3.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/arrow-up-3.png new file mode 100644 index 0000000..77b58d8 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/arrow-up-3.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/document-close-4.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/document-close-4.png new file mode 100644 index 0000000..063f2f6 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/document-close-4.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/document-import-2.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/document-import-2.png new file mode 100644 index 0000000..239cd3b Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/document-import-2.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/document-new.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/document-new.png new file mode 100644 index 0000000..bc9cffe Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/document-new.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-clear-2.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-clear-2.png new file mode 100644 index 0000000..3e30f3f Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-clear-2.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-copy-3.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-copy-3.png new file mode 100644 index 0000000..8eadda4 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-copy-3.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-copy-4.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-copy-4.png new file mode 100644 index 0000000..d5e5624 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-copy-4.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-cut-4.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-cut-4.png new file mode 100644 index 0000000..ae166df Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-cut-4.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-paste.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-paste.png new file mode 100644 index 0000000..3ed7f8b Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-paste.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/help-contents.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/help-contents.png new file mode 100644 index 0000000..a6a986e Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/menu/help-contents.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/openTCS/openTCS.300x132.gif b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/openTCS/openTCS.300x132.gif new file mode 100644 index 0000000..17a9130 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/openTCS/openTCS.300x132.gif differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/align-horizontal-center-2.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/align-horizontal-center-2.png new file mode 100644 index 0000000..81f201a Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/align-horizontal-center-2.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/align-horizontal-left.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/align-horizontal-left.png new file mode 100644 index 0000000..9e8f2eb Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/align-horizontal-left.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/align-horizontal-right-2.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/align-horizontal-right-2.png new file mode 100644 index 0000000..c666345 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/align-horizontal-right-2.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/align-vertical-bottom-2.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/align-vertical-bottom-2.png new file mode 100644 index 0000000..d5dd022 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/align-vertical-bottom-2.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/align-vertical-center-2.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/align-vertical-center-2.png new file mode 100644 index 0000000..7626365 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/align-vertical-center-2.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/align-vertical-top-2.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/align-vertical-top-2.png new file mode 100644 index 0000000..e2e012b Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/align-vertical-top-2.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeFillColor.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeFillColor.png new file mode 100644 index 0000000..02cd40b Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeFillColor.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeFont.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeFont.png new file mode 100644 index 0000000..5b8a8d9 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeFont.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeFontBold.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeFontBold.png new file mode 100644 index 0000000..771dc05 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeFontBold.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeFontItalic.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeFontItalic.png new file mode 100644 index 0000000..066508d Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeFontItalic.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeFontUnderline.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeFontUnderline.png new file mode 100644 index 0000000..b54a780 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeFontUnderline.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeCap.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeCap.png new file mode 100644 index 0000000..192c55b Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeCap.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeColor.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeColor.png new file mode 100644 index 0000000..aa852cc Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeColor.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeDashes.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeDashes.png new file mode 100644 index 0000000..5f0b625 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeDashes.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeDecoration.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeDecoration.png new file mode 100644 index 0000000..2c1d36f Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeDecoration.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeJoin.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeJoin.png new file mode 100644 index 0000000..3fc789b Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeJoin.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokePlacement.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokePlacement.png new file mode 100644 index 0000000..f607172 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokePlacement.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeType.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeType.png new file mode 100644 index 0000000..42e1ebd Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeType.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeWidth.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeWidth.png new file mode 100644 index 0000000..fc4a806 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeStrokeWidth.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeTextColor.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeTextColor.png new file mode 100644 index 0000000..f646005 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/attributeTextColor.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/blockdevice-3.16.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/blockdevice-3.16.png new file mode 100644 index 0000000..1ebe695 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/blockdevice-3.16.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/blockdevice-3.22.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/blockdevice-3.22.png new file mode 100644 index 0000000..480db09 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/blockdevice-3.22.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/car.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/car.png new file mode 100644 index 0000000..959677f Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/car.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/colorpicker.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/colorpicker.png new file mode 100644 index 0000000..e8da9c4 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/colorpicker.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/cursor-opened-hand.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/cursor-opened-hand.png new file mode 100644 index 0000000..70fd2ff Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/cursor-opened-hand.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-clear-2.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-clear-2.png new file mode 100644 index 0000000..34a4cec Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-clear-2.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-copy-3.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-copy-3.png new file mode 100644 index 0000000..4126897 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-copy-3.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-copy-4.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-copy-4.png new file mode 100644 index 0000000..046e780 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-copy-4.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-cut-4.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-cut-4.png new file mode 100644 index 0000000..e21b524 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-cut-4.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-paste.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-paste.png new file mode 100644 index 0000000..26198d4 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-paste.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/link.22.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/link.22.png new file mode 100644 index 0000000..c8f47ca Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/link.22.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/location.22.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/location.22.png new file mode 100644 index 0000000..28ea75f Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/location.22.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/locationType.22.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/locationType.22.png new file mode 100644 index 0000000..c11a755 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/locationType.22.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/object-order-back.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/object-order-back.png new file mode 100644 index 0000000..8146d24 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/object-order-back.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/object-order-front.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/object-order-front.png new file mode 100644 index 0000000..95b0e49 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/object-order-front.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-bezier-arrow.22.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-bezier-arrow.22.png new file mode 100644 index 0000000..8325bbd Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-bezier-arrow.22.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-bezier.22.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-bezier.22.png new file mode 100644 index 0000000..10e7a19 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-bezier.22.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-direct-arrow.22.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-direct-arrow.22.png new file mode 100644 index 0000000..ca9e52d Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-direct-arrow.22.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-direct.22.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-direct.22.png new file mode 100644 index 0000000..c9c3038 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-direct.22.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-elbow-arrow.22.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-elbow-arrow.22.png new file mode 100644 index 0000000..56e53e3 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-elbow-arrow.22.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-elbow.22.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-elbow.22.png new file mode 100644 index 0000000..f9a5727 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-elbow.22.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-polypath-arrow.22.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-polypath-arrow.22.png new file mode 100644 index 0000000..df608d3 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-polypath-arrow.22.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-polypath.22.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-polypath.22.png new file mode 100644 index 0000000..eeb111a Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-polypath.22.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-slanted-arrow.22.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-slanted-arrow.22.png new file mode 100644 index 0000000..9bcab9a Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-slanted-arrow.22.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-slanted.22.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-slanted.22.png new file mode 100644 index 0000000..04fbaa1 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/path-slanted.22.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/point-halt-arrow.22.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/point-halt-arrow.22.png new file mode 100644 index 0000000..950ce3b Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/point-halt-arrow.22.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/point-halt.22.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/point-halt.22.png new file mode 100644 index 0000000..e7ebe85 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/point-halt.22.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/point-park-arrow.22.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/point-park-arrow.22.png new file mode 100644 index 0000000..1aa5b88 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/point-park-arrow.22.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/point-park.22.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/point-park.22.png new file mode 100644 index 0000000..9325398 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/point-park.22.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/select-2.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/select-2.png new file mode 100644 index 0000000..ae01970 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/select-2.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/view-media-visualization.png b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/view-media-visualization.png new file mode 100644 index 0000000..effc797 Binary files /dev/null and b/opentcs-modeleditor/src/main/resources/org/opentcs/guing/res/symbols/toolbar/view-media-visualization.png differ diff --git a/opentcs-modeleditor/src/main/resources/org/opentcs/modeleditor/distribution/config/opentcs-modeleditor-defaults-baseline.properties b/opentcs-modeleditor/src/main/resources/org/opentcs/modeleditor/distribution/config/opentcs-modeleditor-defaults-baseline.properties new file mode 100644 index 0000000..720b500 --- /dev/null +++ b/opentcs-modeleditor/src/main/resources/org/opentcs/modeleditor/distribution/config/opentcs-modeleditor-defaults-baseline.properties @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +# This file contains default configuration values and should not be modified. +# To adjust the application configuration, override values in a separate file. + +modeleditor.locale = en +modeleditor.connectionBookmarks = Localhost|localhost|1099 +modeleditor.useBookmarksWhenConnecting = true +modeleditor.locationThemeClass = org.opentcs.guing.plugins.themes.DefaultLocationTheme + +ssl.enable = false +ssl.truststoreFile = ./config/truststore.p12 +ssl.truststorePassword = password + +statisticspanel.enable = true + +elementnamingscheme.pointPrefix = Point- +elementnamingscheme.pointNumberPattern = 0000 +elementnamingscheme.pathPrefix = Path- +elementnamingscheme.pathNumberPattern = 0000 +elementnamingscheme.locationTypePrefix = LType- +elementnamingscheme.locationTypeNumberPattern = 0000 +elementnamingscheme.locationPrefix = Location- +elementnamingscheme.locationNumberPattern = 0000 +elementnamingscheme.linkPrefix = Link- +elementnamingscheme.linkNumberPattern = 0000 +elementnamingscheme.blockPrefix = Block- +elementnamingscheme.blockNumberPattern = 0000 +elementnamingscheme.layoutPrefix = Layout- +elementnamingscheme.layoutNumberPattern = 0000 +elementnamingscheme.vehiclePrefix = Vehicle- +elementnamingscheme.vehicleNumberPattern = 0000 diff --git a/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/math/path/BezierLengthTest.java b/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/math/path/BezierLengthTest.java new file mode 100644 index 0000000..24aeea7 --- /dev/null +++ b/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/math/path/BezierLengthTest.java @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.math.path; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * Test for {@link BezierLength}. + */ +class BezierLengthTest { + + private ModelManager manager; + private SystemModel systemModel; + private PathLengthMath pathLengthMath; + + @BeforeEach + void setUp() { + manager = mock(); + systemModel = mock(); + pathLengthMath = mock(); + given(manager.getModel()).willReturn(systemModel); + } + + @Test + void testApplyAsDouble() { + PointModel pointModelStart = new PointModel(); + pointModelStart.getPropertyModelPositionX().setValueAndUnit(10, LengthProperty.Unit.MM); + pointModelStart.getPropertyModelPositionY().setValueAndUnit(10, LengthProperty.Unit.MM); + + PointModel pointModelEnd = new PointModel(); + pointModelEnd.getPropertyModelPositionX().setValueAndUnit(60, LengthProperty.Unit.MM); + pointModelEnd.getPropertyModelPositionY().setValueAndUnit(60, LengthProperty.Unit.MM); + + PathModel pathModel = new PathModel(); + pathModel.getPropertyPathControlPoints().setText("0.4,-0.6;0.8,-1"); + pathModel.setConnectedComponents(pointModelStart, pointModelEnd); + + LayoutModel layoutModel = new LayoutModel(); + layoutModel.getPropertyScaleX().setValueAndUnit(50.0, LengthProperty.Unit.MM); + layoutModel.getPropertyScaleY().setValueAndUnit(50.0, LengthProperty.Unit.MM); + given(systemModel.getLayoutModel()).willReturn(layoutModel); + + given(pathLengthMath.approximateCubicBezierCurveLength(any(), any(), any(), any(), anyDouble())) + .willReturn(47.11); + + BezierLength function = new BezierLength(manager, pathLengthMath); + double calculatedLength = function.applyAsDouble(pathModel); + + assertThat(calculatedLength).isEqualTo(47.11); + verify(pathLengthMath).approximateCubicBezierCurveLength( + new Coordinate(10, 10), + new Coordinate(20, 30), + new Coordinate(40, 50), + new Coordinate(60, 60), + 1000 + ); + } +} diff --git a/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/math/path/BezierThreeLengthTest.java b/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/math/path/BezierThreeLengthTest.java new file mode 100644 index 0000000..e653f84 --- /dev/null +++ b/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/math/path/BezierThreeLengthTest.java @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.math.path; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * Test for {@link BezierThreeLength}. + */ +class BezierThreeLengthTest { + + private ModelManager manager; + private SystemModel systemModel; + private PathLengthMath pathLengthMath; + + @BeforeEach + void setUp() { + manager = mock(); + systemModel = mock(); + pathLengthMath = mock(); + given(manager.getModel()).willReturn(systemModel); + } + + @Test + void testApplyAsDouble() { + PointModel pointModelStart = new PointModel(); + pointModelStart.getPropertyModelPositionX().setValueAndUnit(0, LengthProperty.Unit.MM); + pointModelStart.getPropertyModelPositionY().setValueAndUnit(0, LengthProperty.Unit.MM); + + PointModel pointModelEnd = new PointModel(); + pointModelEnd.getPropertyModelPositionX().setValueAndUnit(10000, LengthProperty.Unit.MM); + pointModelEnd.getPropertyModelPositionY().setValueAndUnit(10000, LengthProperty.Unit.MM); + + PathModel pathModel = new PathModel(); + pathModel.getPropertyPathControlPoints().setText("36,-35;68,-67;100,-99;132,-131;164,-163"); + pathModel.setConnectedComponents(pointModelStart, pointModelEnd); + + LayoutModel layoutModel = new LayoutModel(); + layoutModel.getPropertyScaleX().setValueAndUnit(50.0, LengthProperty.Unit.MM); + layoutModel.getPropertyScaleY().setValueAndUnit(50.0, LengthProperty.Unit.MM); + given(systemModel.getLayoutModel()).willReturn(layoutModel); + + given(pathLengthMath.approximateCubicBezierCurveLength(any(), any(), any(), any(), anyDouble())) + .willReturn(47.11); + + BezierThreeLength function = new BezierThreeLength(manager, pathLengthMath); + double calculatedLength = function.applyAsDouble(pathModel); + + assertThat(calculatedLength).isEqualTo(94.22); + verify(pathLengthMath, times(2)) + .approximateCubicBezierCurveLength(any(), any(), any(), any(), anyDouble()); + verify(pathLengthMath).approximateCubicBezierCurveLength( + new Coordinate(0, 0), + new Coordinate(1800, 1750), + new Coordinate(3400, 3350), + new Coordinate(5000, 4950), + 1000 + ); + verify(pathLengthMath).approximateCubicBezierCurveLength( + new Coordinate(5000, 4950), + new Coordinate(6600, 6550), + new Coordinate(8200, 8150), + new Coordinate(10000, 10000), + 1000 + ); + } +} diff --git a/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/math/path/CompositePathLengthFunctionTest.java b/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/math/path/CompositePathLengthFunctionTest.java new file mode 100644 index 0000000..376ae25 --- /dev/null +++ b/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/math/path/CompositePathLengthFunctionTest.java @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.math.path; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.guing.base.model.elements.PathModel; + +/** + * Test for {@link CompositePathlengthFunction}. + */ +class CompositePathLengthFunctionTest { + + private CompositePathLengthFunction compPLF; + private PathLengthFunction directFunction; + private PathLengthFunction bezierFunction; + private EuclideanDistance defaultFunction; + + @BeforeEach + void setUp() { + directFunction = mock(); + bezierFunction = mock(); + defaultFunction = mock(); + + Map pathlengthfunctions = new HashMap<>(); + pathlengthfunctions.put(PathModel.Type.DIRECT, directFunction); + pathlengthfunctions.put(PathModel.Type.BEZIER, bezierFunction); + + compPLF = new CompositePathLengthFunction(pathlengthfunctions, defaultFunction); + } + + @Test + void shouldUseDirectFunction() { + PathModel pathModel = new PathModel(); + pathModel.getPropertyPathConnType().setValue(PathModel.Type.DIRECT); + + compPLF.applyAsDouble(pathModel); + + verify(directFunction).applyAsDouble(pathModel); + } + + @Test + void shouldUseBezierFunction() { + PathModel pathModel = new PathModel(); + pathModel.getPropertyPathConnType().setValue(PathModel.Type.BEZIER); + + compPLF.applyAsDouble(pathModel); + + verify(bezierFunction).applyAsDouble(pathModel); + } + + @Test + void shouldUseDefaultFunctionAsFallback() { + PathModel pathModel = new PathModel(); + pathModel.getPropertyPathConnType().setValue(PathModel.Type.POLYPATH); + + compPLF.applyAsDouble(pathModel); + + verify(defaultFunction).applyAsDouble(pathModel); + } +} diff --git a/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/math/path/EuclideanDistanceTest.java b/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/math/path/EuclideanDistanceTest.java new file mode 100644 index 0000000..ea50dfc --- /dev/null +++ b/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/math/path/EuclideanDistanceTest.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.math.path; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; + +/** + * Unit tests for {@link EuclideanDistance}. + */ +class EuclideanDistanceTest { + + private PointModel startPoint; + private PointModel endPoint; + private PathModel pathModel; + private EuclideanDistance function; + private PathLengthMath pathLengthMath; + + @BeforeEach + void setUp() { + startPoint = new PointModel(); + endPoint = new PointModel(); + pathModel = new PathModel(); + pathModel.setConnectedComponents(startPoint, endPoint); + + pathLengthMath = mock(); + function = new EuclideanDistance(pathLengthMath); + } + + @Test + void verifyEuclideanDistanceIsUsed() { + startPoint.getPropertyModelPositionX().setValueAndUnit(1000, LengthProperty.Unit.MM); + startPoint.getPropertyModelPositionY().setValueAndUnit(1000, LengthProperty.Unit.MM); + endPoint.getPropertyModelPositionX().setValueAndUnit(1000, LengthProperty.Unit.MM); + endPoint.getPropertyModelPositionY().setValueAndUnit(1000, LengthProperty.Unit.MM); + + function.applyAsDouble(pathModel); + + verify(pathLengthMath).euclideanDistance( + new Coordinate(1000, 1000), + new Coordinate(1000, 1000) + ); + } +} diff --git a/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/math/path/PathLengthMathTest.java b/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/math/path/PathLengthMathTest.java new file mode 100644 index 0000000..c7c5c02 --- /dev/null +++ b/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/math/path/PathLengthMathTest.java @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.math.path; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.BDDAssertions.within; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link PathLengthMath}. + */ +public class PathLengthMathTest { + + private PathLengthMath pathLengthMath; + + @BeforeEach + void setUp() { + pathLengthMath = new PathLengthMath(); + } + + @Test + void approximateCubicBezierCurveLength() { + double result = pathLengthMath.approximateCubicBezierCurveLength( + new Coordinate(10, 10), + new Coordinate(20, 30), + new Coordinate(40, 50), + new Coordinate(60, 60), + 1000 + ); + + assertThat(result).isEqualTo(71.8, within(0.5)); + } + + @Test + void testTZeroEqualsStartPointOfCubicBezier() { + Coordinate result = pathLengthMath + .evaluatePointOnCubicBezierCurve( + 0, + new Coordinate(10, 10), + new Coordinate(0.4, -0.6), + new Coordinate(0.8, -1), + new Coordinate(60, 60) + ); + + assertThat(result, is(new Coordinate(10, 10))); + } + + @Test + void testTOneEqualsEndPointOfCubicBezier() { + Coordinate result = pathLengthMath + .evaluatePointOnCubicBezierCurve( + 1, + new Coordinate(10, 10), + new Coordinate(0.4, -0.6), + new Coordinate(0.8, -1), + new Coordinate(60, 60) + ); + + assertThat(result, is(new Coordinate(60, 60))); + } + + @Test + void testTBetweenZeroAndOneForCubicBezier() { + Coordinate result = pathLengthMath + .evaluatePointOnCubicBezierCurve( + 0.75, + new Coordinate(10, 10), + new Coordinate(0.4, -0.6), + new Coordinate(0.8, -1), + new Coordinate(60, 60) + ); + + assertThat(result.getX(), is(closeTo(25.8625, 0.5))); + assertThat(result.getY(), is(closeTo(24.9625, 0.5))); + } + + @Test + void returnZeroForSamePosition() { + double distance = pathLengthMath + .euclideanDistance(new Coordinate(1000, 1000), new Coordinate(1000, 1000)); + + assertThat(distance, is(0.0)); + } + + @Test + void returnDistanceXForSameY() { + double distance = pathLengthMath + .euclideanDistance(new Coordinate(2000, 1000), new Coordinate(4500, 1000)); + + assertThat(distance, is(2500.0)); + } + + @Test + void returnDistanceYForSameX() { + double distance = pathLengthMath + .euclideanDistance(new Coordinate(1000, 3000), new Coordinate(1000, 7654)); + + assertThat(distance, is(4654.0)); + } + + @Test + void returnEuclideanDistance() { + double distance = pathLengthMath + .euclideanDistance(new Coordinate(2000, 1000), new Coordinate(4000, 5000)); + + assertThat(distance, is(closeTo(4472.0, 0.5))); + } +} diff --git a/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/math/path/PolyPathLengthTest.java b/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/math/path/PolyPathLengthTest.java new file mode 100644 index 0000000..957ab07 --- /dev/null +++ b/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/math/path/PolyPathLengthTest.java @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.math.path; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * Test for {@link PolyPathLength}. + */ +class PolyPathLengthTest { + + private ModelManager manager; + private SystemModel systemModel; + + @BeforeEach + void setUp() { + manager = mock(); + systemModel = mock(); + given(manager.getModel()).willReturn(systemModel); + + LayoutModel layoutModel = new LayoutModel(); + layoutModel.getPropertyScaleX().setValueAndUnit(50.0, LengthProperty.Unit.MM); + layoutModel.getPropertyScaleY().setValueAndUnit(50.0, LengthProperty.Unit.MM); + given(systemModel.getLayoutModel()).willReturn(layoutModel); + } + + @Test + void testApplyAsDoubleOneControlPoint() { + PointModel pointModelStart = new PointModel(); + pointModelStart.getPropertyModelPositionX().setValueAndUnit(10, LengthProperty.Unit.MM); + pointModelStart.getPropertyModelPositionY().setValueAndUnit(10, LengthProperty.Unit.MM); + + PointModel pointModelEnd = new PointModel(); + pointModelEnd.getPropertyModelPositionX().setValueAndUnit(30, LengthProperty.Unit.MM); + pointModelEnd.getPropertyModelPositionY().setValueAndUnit(20, LengthProperty.Unit.MM); + + PathModel pathModel = new PathModel(); + pathModel.getPropertyPathControlPoints().setText("0.2,-0.4"); + pathModel.setConnectedComponents(pointModelStart, pointModelEnd); + + PolyPathLength function = new PolyPathLength(manager, new PathLengthMath()); + double calculatedLength = function.applyAsDouble(pathModel); + + assertThat(calculatedLength, is(30.0)); + } + + @Test + void testApplyAsDoubleTwoControlPoints() { + PointModel pointModelStart = new PointModel(); + pointModelStart.getPropertyModelPositionX().setValueAndUnit(10, LengthProperty.Unit.MM); + pointModelStart.getPropertyModelPositionY().setValueAndUnit(10, LengthProperty.Unit.MM); + + PointModel pointModelEnd = new PointModel(); + pointModelEnd.getPropertyModelPositionX().setValueAndUnit(50, LengthProperty.Unit.MM); + pointModelEnd.getPropertyModelPositionY().setValueAndUnit(50, LengthProperty.Unit.MM); + + PathModel pathModel = new PathModel(); + pathModel.getPropertyPathControlPoints().setText("0.2,-0.4;1,-0.4"); + pathModel.setConnectedComponents(pointModelStart, pointModelEnd); + + PolyPathLength function = new PolyPathLength(manager, new PathLengthMath()); + double calculatedLength = function.applyAsDouble(pathModel); + + assertThat(calculatedLength, is(80.0)); + } + + @Test + void testApplyAsDoubleThreeControlPoints() { + PointModel pointModelStart = new PointModel(); + pointModelStart.getPropertyModelPositionX().setValueAndUnit(10, LengthProperty.Unit.MM); + pointModelStart.getPropertyModelPositionY().setValueAndUnit(10, LengthProperty.Unit.MM); + + PointModel pointModelEnd = new PointModel(); + pointModelEnd.getPropertyModelPositionX().setValueAndUnit(70, LengthProperty.Unit.MM); + pointModelEnd.getPropertyModelPositionY().setValueAndUnit(50, LengthProperty.Unit.MM); + + PathModel pathModel = new PathModel(); + pathModel.getPropertyPathControlPoints().setText("0.2,-0.4;1,-0.4;1,-1"); + pathModel.setConnectedComponents(pointModelStart, pointModelEnd); + + PolyPathLength function = new PolyPathLength(manager, new PathLengthMath()); + double calculatedLength = function.applyAsDouble(pathModel); + + assertThat(calculatedLength, is(100.0)); + } +} diff --git a/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/persistence/ModelValidatorTest.java b/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/persistence/ModelValidatorTest.java new file mode 100644 index 0000000..9e94001 --- /dev/null +++ b/opentcs-modeleditor/src/test/java/org/opentcs/modeleditor/persistence/ModelValidatorTest.java @@ -0,0 +1,434 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.modeleditor.persistence; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.invocation.InvocationOnMock; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Triple; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.model.visualization.ElementPropKeys; +import org.opentcs.guing.base.components.properties.type.AbstractProperty; +import org.opentcs.guing.base.components.properties.type.AngleProperty; +import org.opentcs.guing.base.components.properties.type.BooleanProperty; +import org.opentcs.guing.base.components.properties.type.BoundingBoxProperty; +import org.opentcs.guing.base.components.properties.type.CoordinateProperty; +import org.opentcs.guing.base.components.properties.type.EnergyLevelThresholdSetModel; +import org.opentcs.guing.base.components.properties.type.EnergyLevelThresholdSetProperty; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.components.properties.type.LocationTypeProperty; +import org.opentcs.guing.base.components.properties.type.PercentProperty; +import org.opentcs.guing.base.components.properties.type.SelectionProperty; +import org.opentcs.guing.base.components.properties.type.SpeedProperty; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.base.components.properties.type.StringSetProperty; +import org.opentcs.guing.base.components.properties.type.TripleProperty; +import org.opentcs.guing.base.model.BoundingBoxModel; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.model.SystemModel; + +/** + * Unit tests for {@link ModelValidator}. + */ +class ModelValidatorTest { + + private static final String LAYOUT_NAME = "VLayout-001"; + + private static final String LOCATION_THEME_NAME = "Loc-Theme-001"; + + private static final String VEHICLE_THEME_NAME = "Vehicle-Theme-001"; + + private static final String POINT_NAME = "Point-001"; + + private static final String POINT_NAME_2 = "Point-002"; + + private static final String PATH_NAME = "Point-001 --- Point-002"; + + private static final String LOCATION_TYPE_NAME = "Ladestation"; + + private static final String LOCATION_NAME = "Location-001"; + + private static final String LINK_NAME = POINT_NAME + " --- " + LOCATION_NAME; + + private static final String VEHICLE_NAME = "Vehicle-001"; + + /** + * The system model for this test. + */ + private SystemModel model; + + /** + * The validator for this test. + */ + private ModelValidator validator; + + /** + * Stores the properties for every model component. + */ + private Map> objectPropertiesMap; + + /** + * Stores all model components returned by {@link SystemModel#getAll()}. + */ + private Map components; + + @BeforeEach + void setUp() { + model = mock(SystemModel.class); + validator = new ModelValidator(); + objectPropertiesMap = new HashMap<>(); + components = new HashMap<>(); + + when(model.getAll()).thenAnswer((InvocationOnMock invocation) -> { + return new ArrayList<>(components.values()); + }); + when(model.getLocationTypeModels()).thenAnswer((InvocationOnMock invocation) -> { + return components.values().stream() + .filter(o -> o instanceof LocationTypeModel) + .collect(Collectors.toList()); + }); + } + + @Test + void shouldInvalidateIfNull() { + assertFalse(validator.isValidWith(model, null)); + + assertFalse(validator.isValidWith(null, mock(ModelComponent.class))); + } + + @Test + void shouldInvalidateWhenEmptyName() { + assertFalse(validator.isValidWith(model, createComponentWithName(ModelComponent.class, ""))); + } + + @Test + void shouldInvalidateIfExists() { + ModelComponent component = createComponentWithName(ModelComponent.class, POINT_NAME); + components.put(POINT_NAME, component); + when(model.getModelComponent(POINT_NAME)).thenReturn(component); + + assertFalse( + validator.isValidWith(model, createComponentWithName(ModelComponent.class, POINT_NAME)) + ); + } + + @Test + void testPointNegativeOrientationAngle() { + PointModel point = createPointModel(POINT_NAME); + addProperty(point, AngleProperty.class, PointModel.VEHICLE_ORIENTATION_ANGLE, -5d); + assertTrue(validator.isValidWith(model, point)); + } + + @Test + void testPathNegativeLength() { + PathModel path = createPathModel(PATH_NAME, POINT_NAME, POINT_NAME_2); + addProperty(path, LengthProperty.class, PathModel.LENGTH, -5d); + assertFalse(validator.isValidWith(model, path)); + } + + @Test + void testPathInvalidStartPoint() { + PathModel path = createPathModel(PATH_NAME, POINT_NAME, POINT_NAME_2); + when(model.getModelComponent(POINT_NAME_2)).thenReturn(components.get(POINT_NAME_2)); + components.remove(POINT_NAME); + assertFalse(validator.isValidWith(model, path)); + } + + @Test + void testPathInvalidEndPoint() { + PathModel path = createPathModel(PATH_NAME, POINT_NAME, POINT_NAME_2); + when(model.getModelComponent(POINT_NAME)).thenReturn(components.get(POINT_NAME)); + components.remove(POINT_NAME_2); + assertFalse(validator.isValidWith(model, path)); + } + + @Test + void testPathValid() { + PathModel path = createPathModel(PATH_NAME, POINT_NAME, POINT_NAME_2); + when(model.getModelComponent(POINT_NAME)).thenReturn(components.get(POINT_NAME)); + when(model.getModelComponent(POINT_NAME_2)).thenReturn(components.get(POINT_NAME_2)); + assertTrue(validator.isValidWith(model, path)); + } + + @Test + void testLocationInvalidType() { + LocationModel location = createLocation(LOCATION_NAME); + components.remove(LOCATION_TYPE_NAME); + assertFalse(validator.isValidWith(model, location)); + } + + @Test + void testLocationValid() { + LocationModel location = createLocation(LOCATION_NAME); + assertTrue(validator.isValidWith(model, location)); + } + + @Test + void testLinkInvalidEndPoint() { + LinkModel link = createLink(LINK_NAME); + when(model.getModelComponent(POINT_NAME)).thenReturn(components.get(POINT_NAME)); + assertFalse(validator.isValidWith(model, link)); + } + + @Test + void testLinkInvalidStartPoint() { + LinkModel link = createLink(LINK_NAME); + when(model.getModelComponent(LOCATION_NAME)).thenReturn(components.get(LOCATION_NAME)); + assertFalse(validator.isValidWith(model, link)); + } + + @Test + void testLinkValid() { + LinkModel link = createLink(LINK_NAME); + when(model.getModelComponent(POINT_NAME)).thenReturn(components.get(POINT_NAME)); + when(model.getModelComponent(LOCATION_NAME)).thenReturn(components.get(LOCATION_NAME)); + assertTrue(validator.isValidWith(model, link)); + } + + @Test + void testVehicleInvalidNextPosition() { + VehicleModel vehicle = createVehicle(VEHICLE_NAME); + when(model.getModelComponent(POINT_NAME)).thenReturn(components.get(POINT_NAME)); + assertFalse(validator.isValidWith(model, vehicle)); + } + + @Test + void testVehicleValidWithNullPoints() { + VehicleModel vehicle = createVehicle(VEHICLE_NAME); + addProperty(vehicle, StringProperty.class, VehicleModel.POINT, "null"); + addProperty(vehicle, StringProperty.class, VehicleModel.NEXT_POINT, "null"); + assertTrue(validator.isValidWith(model, vehicle)); + } + + @Test + void testVehicleValid() { + VehicleModel vehicle = createVehicle(VEHICLE_NAME); + when(model.getModelComponent(POINT_NAME)).thenReturn(components.get(POINT_NAME)); + when(model.getModelComponent(POINT_NAME_2)).thenReturn(components.get(POINT_NAME_2)); + assertTrue(validator.isValidWith(model, vehicle)); + } + + /** + * Creates a mock for a given class with the given name and registers Mockito Stubs on it + * to return from the properties map defined at the beginning. + * + * @param the type of the model component + * @param clazz the class of the model component + * @param name the name of the model component + * @return the mocked model component + */ + private T createComponentWithName(Class clazz, String name) { + T comp = mock(clazz); + when(comp.getName()).thenReturn(name); + when(comp.getProperty(anyString())).thenAnswer((InvocationOnMock invocation) -> { + String propName = invocation.getArguments()[0].toString(); + objectPropertiesMap.putIfAbsent(comp, new HashMap<>()); + return objectPropertiesMap.get(comp).get(propName); + }); + return comp; + } + + /** + * Creates a layout model with all properties set. + * + * @param name the name of the layout model + * @return the layout model + */ + private LayoutModel createLayoutModel(String name) { + LayoutModel layoutModel = createComponentWithName(LayoutModel.class, name); + addProperty(layoutModel, LengthProperty.class, LayoutModel.SCALE_X, 0d); + addProperty(layoutModel, LengthProperty.class, LayoutModel.SCALE_Y, 0d); + return layoutModel; + } + + /** + * Creates a point model with all properties set. + * + * @param name the name of the point model + * @return the point model + */ + private PointModel createPointModel(String name) { + PointModel point = createComponentWithName(PointModel.class, name); + addProperty(point, AngleProperty.class, PointModel.VEHICLE_ORIENTATION_ANGLE, "5"); + addProperty(point, SelectionProperty.class, PointModel.TYPE, PointModel.Type.HALT); + addProperty(point, CoordinateProperty.class, PointModel.MODEL_X_POSITION, "0"); + addProperty(point, CoordinateProperty.class, PointModel.MODEL_Y_POSITION, "0"); + addProperty(point, StringProperty.class, ElementPropKeys.POINT_POS_X, "0"); + addProperty(point, StringProperty.class, ElementPropKeys.POINT_POS_Y, "0"); + addProperty( + point, + BoundingBoxProperty.class, + PointModel.MAX_VEHICLE_BOUNDING_BOX, + new BoundingBoxModel(Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE, new Couple(0, 0)) + ); + return point; + } + + /** + * Creates a path model with all properties set. + * + * @param name the name of the path model + * @return the path model + */ + private PathModel createPathModel(String name, String pointName1, String pointName2) { + PathModel path = createComponentWithName(PathModel.class, name); + addProperty(path, LengthProperty.class, PathModel.LENGTH, 0d); + addProperty(path, SpeedProperty.class, PathModel.MAX_VELOCITY, 0d); + addProperty(path, SpeedProperty.class, PathModel.MAX_REVERSE_VELOCITY, 0d); + addProperty( + path, SelectionProperty.class, ElementPropKeys.PATH_CONN_TYPE, PathModel.Type.DIRECT + ); + addProperty(path, StringProperty.class, ElementPropKeys.PATH_CONTROL_POINTS, ""); + addProperty(path, StringProperty.class, PathModel.START_COMPONENT, pointName1); + addProperty(path, StringProperty.class, PathModel.END_COMPONENT, pointName2); + addProperty(path, BooleanProperty.class, PathModel.LOCKED, false); + components.put(pointName1, createPointModel(pointName1)); + components.put(pointName2, createPointModel(pointName2)); + return path; + } + + /** + * Creates a location type model with all properties set. + * + * @param name the name of the location type model + * @return the location type model + */ + private LocationTypeModel createLocationType(String name) { + LocationTypeModel locationType = createComponentWithName(LocationTypeModel.class, name); + addProperty( + locationType, StringSetProperty.class, LocationTypeModel.ALLOWED_OPERATIONS, new HashSet<>() + ); + return locationType; + } + + /** + * Creates a location model with all properties set. + * + * @param name the name of the location model + * @return the location model + */ + private LocationModel createLocation(String name) { + LocationModel location = createComponentWithName(LocationModel.class, name); + addProperty(location, CoordinateProperty.class, LocationModel.MODEL_X_POSITION, "0"); + addProperty(location, CoordinateProperty.class, LocationModel.MODEL_Y_POSITION, "0"); + addProperty(location, StringProperty.class, ElementPropKeys.LOC_POS_X, "0"); + addProperty(location, StringProperty.class, ElementPropKeys.LOC_POS_Y, "0"); + addProperty(location, LocationTypeProperty.class, LocationModel.TYPE, LOCATION_TYPE_NAME); + addProperty(location, StringProperty.class, ElementPropKeys.LOC_LABEL_OFFSET_X, "0"); + addProperty(location, StringProperty.class, ElementPropKeys.LOC_LABEL_OFFSET_Y, "0"); + addProperty(location, StringProperty.class, ElementPropKeys.LOC_LABEL_ORIENTATION_ANGLE, ""); + components.put(LOCATION_TYPE_NAME, createLocationType(LOCATION_TYPE_NAME)); + return location; + } + + /** + * Creates a link model with all properties set. + * + * @param name The name of the link + * @return The link model + */ + private LinkModel createLink(String name) { + LinkModel link = createComponentWithName(LinkModel.class, name); + addProperty(link, StringProperty.class, LinkModel.START_COMPONENT, POINT_NAME); + addProperty(link, StringProperty.class, LinkModel.END_COMPONENT, LOCATION_NAME); + components.put(POINT_NAME, createPointModel(POINT_NAME)); + components.put(LOCATION_NAME, createLocation(LOCATION_NAME)); + + return link; + } + + /** + * Creates a vehicle model with all properties set. + * + * @param name The name of the vehicle + * @return The vehicle model + */ + private VehicleModel createVehicle(String name) { + VehicleModel vehicle = createComponentWithName(VehicleModel.class, name); + addProperty( + vehicle, + BoundingBoxProperty.class, + VehicleModel.BOUNDING_BOX, + new BoundingBoxModel(1000, 1000, 1000, new Couple(0, 0)) + ); + addProperty( + vehicle, + EnergyLevelThresholdSetProperty.class, + VehicleModel.ENERGY_LEVEL_THRESHOLD_SET, + new EnergyLevelThresholdSetModel(30, 80, 0, 0) + ); + addProperty(vehicle, PercentProperty.class, VehicleModel.ENERGY_LEVEL, 60); + addProperty(vehicle, SelectionProperty.class, VehicleModel.PROC_STATE, Vehicle.ProcState.IDLE); + addProperty(vehicle, SelectionProperty.class, VehicleModel.STATE, Vehicle.State.IDLE); + addProperty( + vehicle, + SelectionProperty.class, + VehicleModel.INTEGRATION_LEVEL, + Vehicle.IntegrationLevel.TO_BE_RESPECTED + ); + addProperty(vehicle, BooleanProperty.class, VehicleModel.LOADED, Boolean.FALSE); + addProperty(vehicle, StringProperty.class, VehicleModel.POINT, POINT_NAME); + addProperty(vehicle, StringProperty.class, VehicleModel.NEXT_POINT, POINT_NAME_2); + addProperty(vehicle, TripleProperty.class, VehicleModel.PRECISE_POSITION, new Triple(0, 0, 0)); + addProperty(vehicle, AngleProperty.class, VehicleModel.ORIENTATION_ANGLE, 0.0); + + components.put(POINT_NAME, createPointModel(POINT_NAME)); + components.put(POINT_NAME_2, createPointModel(POINT_NAME_2)); + return vehicle; + } + + /** + * Adds a property to the given model component. + * + * @param the type of the property + * @param component the model component + * @param clazz the class of the property + * @param propName the property key + * @param propValue the property value + */ + private void addProperty( + ModelComponent component, + Class clazz, + String propName, + Object propValue + ) { + objectPropertiesMap.putIfAbsent(component, new HashMap<>()); + T property = mock(clazz); + when(property.getValue()).thenReturn(propValue); + when(property.getComparableValue()).thenReturn(propValue); + if (clazz.isAssignableFrom(StringProperty.class)) { + when(((StringProperty) property).getText()).thenReturn(propValue.toString()); + } + objectPropertiesMap.get(component).put(propName, property); + } + + /** + * Removes a property from a model component. + * + * @param component the model component + * @param propName the property key + */ + private void removeProperty(ModelComponent component, String propName) { + objectPropertiesMap.putIfAbsent(component, new HashMap<>()); + objectPropertiesMap.get(component).remove(propName); + } +} diff --git a/opentcs-operationsdesk/build.gradle b/opentcs-operationsdesk/build.gradle new file mode 100644 index 0000000..ea2d742 --- /dev/null +++ b/opentcs-operationsdesk/build.gradle @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply from: "${rootDir}/gradle/java-project.gradle" +apply from: "${rootDir}/gradle/java-codequality.gradle" +apply from: "${rootDir}/gradle/guice-application.gradle" +apply from: "${rootDir}/gradle/publishing-java.gradle" + +if (!hasProperty('mainClass')) { + ext.mainClass = 'org.opentcs.operationsdesk.RunOperationsDesk' +} +application.mainClass = ext.mainClass + +ext.collectableDistDir = new File(buildDir, 'install') + +dependencies { + api project(':opentcs-common') + api project(':opentcs-impl-configuration-gestalt') + api project(':opentcs-plantoverview-common') + api project(':opentcs-plantoverview-panel-loadgenerator') + api project(':opentcs-plantoverview-panel-resourceallocation') + api project(':opentcs-plantoverview-themes-default') + + runtimeOnly group: 'org.slf4j', name: 'slf4j-jdk14', version: '2.0.16' +} + +compileJava { + options.compilerArgs << "-Xlint:-rawtypes" +} + +distributions { + main { + contents { + from "${sourceSets.main.resources.srcDirs[0]}/org/opentcs/operationsdesk/distribution" + } + } +} + +// For now, we're using hand-crafted start scripts, so disable the application +// plugin's start script generation. +startScripts.enabled = false + +distTar.enabled = false + +task release { + dependsOn build + dependsOn installDist +} + +run { + systemProperties(['java.util.logging.config.file':'./config/logging.config',\ + 'sun.java2d.d3d':'false',\ + 'opentcs.base':'.',\ + 'opentcs.home':'.',\ + 'opentcs.configuration.reload.interval':'10000',\ + 'opentcs.configuration.provider':'gestalt']) + jvmArgs('-XX:-OmitStackTraceInFastThrow',\ + '-splash:bin/splash-image.gif') +} diff --git a/opentcs-operationsdesk/gradle.properties b/opentcs-operationsdesk/gradle.properties new file mode 100644 index 0000000..14431ac --- /dev/null +++ b/opentcs-operationsdesk/gradle.properties @@ -0,0 +1,40 @@ +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineMethodParams=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAfterDotInChainedMethodCalls=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineDisjunctiveCatchTypes=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineFor=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineImplements=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.sortMembersByVisibility=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.visibilityOrder=PUBLIC;PROTECTED;DEFAULT;PRIVATE +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.enable-indent=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineArrayInit=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineCallArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.keepGettersAndSettersTogether=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classMembersOrder=STATIC FIELD;FIELD;STATIC_INIT;CONSTRUCTOR;INSTANCE_INIT;STATIC METHOD;METHOD;STATIC CLASS;CLASS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapCommentText=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.importGroupsOrder=* +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize=4 +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineAnnotationArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineTryResources=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.preserveNewLinesInComments=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineParenthesized=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineThrows=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=100 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project diff --git a/opentcs-operationsdesk/src/dist/bin/splash-image.gif b/opentcs-operationsdesk/src/dist/bin/splash-image.gif new file mode 100644 index 0000000..9e6f131 Binary files /dev/null and b/opentcs-operationsdesk/src/dist/bin/splash-image.gif differ diff --git a/opentcs-operationsdesk/src/dist/bin/splash-image.gif.license b/opentcs-operationsdesk/src/dist/bin/splash-image.gif.license new file mode 100644 index 0000000..777faa6 --- /dev/null +++ b/opentcs-operationsdesk/src/dist/bin/splash-image.gif.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 diff --git a/opentcs-operationsdesk/src/dist/config/logging.config b/opentcs-operationsdesk/src/dist/config/logging.config new file mode 100644 index 0000000..badca62 --- /dev/null +++ b/opentcs-operationsdesk/src/dist/config/logging.config @@ -0,0 +1,66 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +############################################################ +# Default Logging Configuration File +# +# You can use a different file by specifying a filename +# with the java.util.logging.config.file system property. +# For example java -Djava.util.logging.config.file=myfile +############################################################ + +############################################################ +# Global properties +############################################################ + +# "handlers" specifies a comma separated list of log Handler +# classes. These handlers will be installed during VM startup. +# Note that these classes must be on the system classpath. +# By default we only configure a ConsoleHandler, which will only +# show messages at the INFO and above levels. +#handlers= java.util.logging.ConsoleHandler + +# To also add the FileHandler, use the following line instead. +handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler + +# Default global logging level. +# This specifies which kinds of events are logged across +# all loggers. For any given facility this global level +# can be overriden by a facility specific level +# Note that the ConsoleHandler also has a separate level +# setting to limit messages printed to the console. +.level= INFO + +############################################################ +# Handler specific properties. +# Describes specific configuration info for Handlers. +############################################################ + +# default file output is in user's home directory. +java.util.logging.FileHandler.pattern = ./log/opentcs-operationsdesk.%g.log +java.util.logging.FileHandler.limit = 500000 +java.util.logging.FileHandler.count = 10 +#java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.FileHandler.formatter = org.opentcs.util.logging.SingleLineFormatter +java.util.logging.FileHandler.append = true +java.util.logging.FileHandler.level = FINE + +# Limit the message that are printed on the console to INFO and above. +java.util.logging.ConsoleHandler.level = INFO +#java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.ConsoleHandler.formatter = org.opentcs.util.logging.SingleLineFormatter + +############################################################ +# Facility specific properties. +# Provides extra control for each logger. +############################################################ + +# For example, set the com.xyz.foo logger to only log SEVERE +# messages: +#com.xyz.foo.level = SEVERE + +# Logging configuration for single classes. Remember that you might also have to +# adjust handler levels! + +#org.opentcs.guing.*.level = FINE +#org.opentcs.access.rmi.ProxyInvocationHandler.level = FINE diff --git a/opentcs-operationsdesk/src/dist/config/opentcs-operationsdesk.properties b/opentcs-operationsdesk/src/dist/config/opentcs-operationsdesk.properties new file mode 100644 index 0000000..777faa6 --- /dev/null +++ b/opentcs-operationsdesk/src/dist/config/opentcs-operationsdesk.properties @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 diff --git a/opentcs-operationsdesk/src/dist/lib/openTCS-extensions/.keepme b/opentcs-operationsdesk/src/dist/lib/openTCS-extensions/.keepme new file mode 100644 index 0000000..e69de29 diff --git a/opentcs-operationsdesk/src/dist/log/.keepme b/opentcs-operationsdesk/src/dist/log/.keepme new file mode 100644 index 0000000..e69de29 diff --git a/opentcs-operationsdesk/src/dist/startOperationsDesk.bat b/opentcs-operationsdesk/src/dist/startOperationsDesk.bat new file mode 100644 index 0000000..8ac9068 --- /dev/null +++ b/opentcs-operationsdesk/src/dist/startOperationsDesk.bat @@ -0,0 +1,38 @@ +@echo off +rem SPDX-FileCopyrightText: The openTCS Authors +rem SPDX-License-Identifier: MIT +rem +rem Start the openTCS Operations Desk application. +rem + +rem Set window title +title Operations Desk (openTCS) + +rem Don't export variables to the parent shell +setlocal + +rem Set base directory names. +set OPENTCS_BASE=. +set OPENTCS_HOME=. +set OPENTCS_CONFIGDIR=%OPENTCS_HOME%\config +set OPENTCS_LIBDIR=%OPENTCS_BASE%\lib + +rem Set the class path +set OPENTCS_CP=%OPENTCS_LIBDIR%\*; +set OPENTCS_CP=%OPENTCS_CP%;%OPENTCS_LIBDIR%\openTCS-extensions\*; + +rem XXX Be a bit more clever to find out the name of the JVM runtime. +set JAVA=javaw + +rem Start plant overview +start /b %JAVA% -enableassertions ^ + -Dopentcs.base="%OPENTCS_BASE%" ^ + -Dopentcs.home="%OPENTCS_HOME%" ^ + -Dopentcs.configuration.provider=gestalt ^ + -Dopentcs.configuration.reload.interval=10000 ^ + -Djava.util.logging.config.file="%OPENTCS_CONFIGDIR%\logging.config" ^ + -Dsun.java2d.d3d=false ^ + -XX:-OmitStackTraceInFastThrow ^ + -classpath "%OPENTCS_CP%" ^ + -splash:bin/splash-image.gif ^ + org.opentcs.operationsdesk.RunOperationsDesk diff --git a/opentcs-operationsdesk/src/dist/startOperationsDesk.sh b/opentcs-operationsdesk/src/dist/startOperationsDesk.sh new file mode 100644 index 0000000..3d2d68b --- /dev/null +++ b/opentcs-operationsdesk/src/dist/startOperationsDesk.sh @@ -0,0 +1,35 @@ +#!/bin/sh +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT +# +# Start the openTCS Operations Desk application. +# + +# Set base directory names. +export OPENTCS_BASE=. +export OPENTCS_HOME=. +export OPENTCS_CONFIGDIR="${OPENTCS_HOME}/config" +export OPENTCS_LIBDIR="${OPENTCS_BASE}/lib" + +# Set the class path +export OPENTCS_CP="${OPENTCS_LIBDIR}/*" +export OPENTCS_CP="${OPENTCS_CP}:${OPENTCS_LIBDIR}/openTCS-extensions/*" + +if [ -n "${OPENTCS_JAVAVM}" ]; then + export JAVA="${OPENTCS_JAVAVM}" +else + # XXX Be a bit more clever to find out the name of the JVM runtime. + export JAVA="java" +fi + +# Start plant overview +${JAVA} -enableassertions \ + -Dopentcs.base="${OPENTCS_BASE}" \ + -Dopentcs.home="${OPENTCS_HOME}" \ + -Dopentcs.configuration.provider=gestalt \ + -Dopentcs.configuration.reload.interval=10000 \ + -Djava.util.logging.config.file=${OPENTCS_CONFIGDIR}/logging.config \ + -XX:-OmitStackTraceInFastThrow \ + -classpath "${OPENTCS_CP}" \ + -splash:bin/splash-image.gif \ + org.opentcs.operationsdesk.RunOperationsDesk diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/DefaultImportersExportersModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/DefaultImportersExportersModule.java new file mode 100644 index 0000000..32d072e --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/DefaultImportersExportersModule.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk; + +import org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule; + +/** + * Configures/binds the default importers and exporters of the openTCS plant overview. + */ +public class DefaultImportersExportersModule + extends + PlantOverviewInjectionModule { + + /** + * Creates a new instance. + */ + public DefaultImportersExportersModule() { + } + + @Override + protected void configure() { + plantModelImporterBinder(); + plantModelExporterBinder(); + } +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/DefaultPlantOverviewInjectionModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/DefaultPlantOverviewInjectionModule.java new file mode 100644 index 0000000..e2a39f0 --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/DefaultPlantOverviewInjectionModule.java @@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk; + +import com.google.inject.TypeLiteral; +import jakarta.inject.Singleton; +import java.io.File; +import java.util.List; +import java.util.Locale; +import javax.swing.ToolTipManager; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.SslParameterSet; +import org.opentcs.access.rmi.KernelServicePortalBuilder; +import org.opentcs.access.rmi.factories.NullSocketFactoryProvider; +import org.opentcs.access.rmi.factories.SecureSocketFactoryProvider; +import org.opentcs.access.rmi.factories.SocketFactoryProvider; +import org.opentcs.common.GuestUserCredentials; +import org.opentcs.components.plantoverview.LocationTheme; +import org.opentcs.components.plantoverview.VehicleTheme; +import org.opentcs.customizations.ApplicationHome; +import org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule; +import org.opentcs.drivers.LowLevelCommunicationEvent; +import org.opentcs.guing.common.exchange.ApplicationPortalProviderConfiguration; +import org.opentcs.guing.common.exchange.SslConfiguration; +import org.opentcs.operationsdesk.application.ApplicationInjectionModule; +import org.opentcs.operationsdesk.components.ComponentsInjectionModule; +import org.opentcs.operationsdesk.exchange.ExchangeInjectionModule; +import org.opentcs.operationsdesk.model.ModelInjectionModule; +import org.opentcs.operationsdesk.notifications.NotificationInjectionModule; +import org.opentcs.operationsdesk.peripherals.jobs.PeripheralJobInjectionModule; +import org.opentcs.operationsdesk.persistence.DefaultPersistenceInjectionModule; +import org.opentcs.operationsdesk.transport.TransportInjectionModule; +import org.opentcs.operationsdesk.util.OperationsDeskConfiguration; +import org.opentcs.operationsdesk.util.UtilInjectionModule; +import org.opentcs.util.ClassMatcher; +import org.opentcs.util.gui.dialog.ConnectionParamSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A Guice module for the openTCS plant overview application. + */ +public class DefaultPlantOverviewInjectionModule + extends + PlantOverviewInjectionModule { + + /** + * This class's logger. + */ + private static final Logger LOG + = LoggerFactory.getLogger(DefaultPlantOverviewInjectionModule.class); + + /** + * Creates a new instance. + */ + public DefaultPlantOverviewInjectionModule() { + } + + @Override + protected void configure() { + File applicationHome = new File(System.getProperty("opentcs.home", ".")); + bind(File.class) + .annotatedWith(ApplicationHome.class) + .toInstance(applicationHome); + + configurePlantOverviewDependencies(); + install(new ApplicationInjectionModule()); + install(new ComponentsInjectionModule()); + install(new ExchangeInjectionModule()); + install(new ModelInjectionModule()); + install(new DefaultPersistenceInjectionModule()); + install(new TransportInjectionModule()); + install(new NotificationInjectionModule()); + install(new PeripheralJobInjectionModule()); + install(new UtilInjectionModule()); + + // Ensure there is at least an empty binder for pluggable panels. + pluggablePanelFactoryBinder(); + // Ensure there is at least an empty binder for history entry formatters. + objectHistoryEntryFormatterBinder(); + } + + private void configurePlantOverviewDependencies() { + OperationsDeskConfiguration configuration + = getConfigBindingProvider().get( + OperationsDeskConfiguration.PREFIX, + OperationsDeskConfiguration.class + ); + bind(ApplicationPortalProviderConfiguration.class) + .toInstance(configuration); + bind(OperationsDeskConfiguration.class) + .toInstance(configuration); + configurePlantOverview(configuration); + configureThemes(configuration); + configureSocketConnections(); + + bind(new TypeLiteral>() { + }) + .toInstance(configuration.connectionBookmarks()); + } + + private void configureSocketConnections() { + SslConfiguration sslConfiguration = getConfigBindingProvider().get( + SslConfiguration.PREFIX, + SslConfiguration.class + ); + + //Create the data object for the ssl configuration + SslParameterSet sslParamSet = new SslParameterSet( + SslParameterSet.DEFAULT_KEYSTORE_TYPE, + null, + null, + new File(sslConfiguration.truststoreFile()), + sslConfiguration.truststorePassword() + ); + bind(SslParameterSet.class).toInstance(sslParamSet); + + SocketFactoryProvider socketFactoryProvider; + if (sslConfiguration.enable()) { + socketFactoryProvider = new SecureSocketFactoryProvider(sslParamSet); + } + else { + LOG.warn("SSL encryption disabled, connections will not be secured!"); + socketFactoryProvider = new NullSocketFactoryProvider(); + } + + //Bind socket provider to the kernel portal + bind(KernelServicePortal.class) + .toInstance( + new KernelServicePortalBuilder( + GuestUserCredentials.USER, + GuestUserCredentials.PASSWORD + ) + .setSocketFactoryProvider(socketFactoryProvider) + .setEventFilter(new ClassMatcher(LowLevelCommunicationEvent.class).negate()) + .build() + ); + } + + private void configureThemes(OperationsDeskConfiguration configuration) { + bind(LocationTheme.class) + .to(configuration.locationThemeClass()) + .in(Singleton.class); + bind(VehicleTheme.class) + .to(configuration.vehicleThemeClass()) + .in(Singleton.class); + } + + private void configurePlantOverview( + OperationsDeskConfiguration configuration + ) { + Locale.setDefault(Locale.forLanguageTag(configuration.locale())); + + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } + catch (ClassNotFoundException | InstantiationException | IllegalAccessException + | UnsupportedLookAndFeelException ex) { + LOG.warn("Could not set look-and-feel", ex); + } + // Show tooltips for 30 seconds (Default: 4 sec) + ToolTipManager.sharedInstance().setDismissDelay(30 * 1000); + } +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/DefaultPropertySuggestions.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/DefaultPropertySuggestions.java new file mode 100644 index 0000000..6827c19 --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/DefaultPropertySuggestions.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk; + +import java.util.HashSet; +import java.util.Set; +import org.opentcs.common.LoopbackAdapterConstants; +import org.opentcs.components.kernel.Dispatcher; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.components.plantoverview.PropertySuggestions; + +/** + * The default property suggestions of the baseline plant overview. + */ +public class DefaultPropertySuggestions + implements + PropertySuggestions { + + private final Set keySuggestions = new HashSet<>(); + private final Set valueSuggestions = new HashSet<>(); + + /** + * Creates a new instance. + */ + public DefaultPropertySuggestions() { + keySuggestions.add(Scheduler.PROPKEY_BLOCK_ENTRY_DIRECTION); + keySuggestions.add(Router.PROPKEY_ROUTING_GROUP); + keySuggestions.add(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY); + keySuggestions.add(Dispatcher.PROPKEY_ASSIGNED_PARKING_POSITION); + keySuggestions.add(Dispatcher.PROPKEY_PREFERRED_PARKING_POSITION); + keySuggestions.add(Dispatcher.PROPKEY_ASSIGNED_RECHARGE_LOCATION); + keySuggestions.add(Dispatcher.PROPKEY_PREFERRED_RECHARGE_LOCATION); + keySuggestions.add(LoopbackAdapterConstants.PROPKEY_INITIAL_POSITION); + keySuggestions.add(LoopbackAdapterConstants.PROPKEY_OPERATING_TIME); + keySuggestions.add(LoopbackAdapterConstants.PROPKEY_LOAD_OPERATION); + keySuggestions.add(LoopbackAdapterConstants.PROPKEY_UNLOAD_OPERATION); + keySuggestions.add(LoopbackAdapterConstants.PROPKEY_ACCELERATION); + keySuggestions.add(LoopbackAdapterConstants.PROPKEY_DECELERATION); + } + + @Override + public Set getKeySuggestions() { + return keySuggestions; + } + + @Override + public Set getValueSuggestions() { + return valueSuggestions; + } + +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/PropertySuggestionsModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/PropertySuggestionsModule.java new file mode 100644 index 0000000..bc6579b --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/PropertySuggestionsModule.java @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk; + +import jakarta.inject.Singleton; +import org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule; + +/** + * This module configures the multibinder used to suggest key value properties in the editor. + */ +public class PropertySuggestionsModule + extends + PlantOverviewInjectionModule { + + /** + * Creates a new instance. + */ + public PropertySuggestionsModule() { + } + + @Override + protected void configure() { + propertySuggestionsBinder().addBinding() + .to(DefaultPropertySuggestions.class) + .in(Singleton.class); + } +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/RunOperationsDesk.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/RunOperationsDesk.java new file mode 100644 index 0000000..3fbb51b --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/RunOperationsDesk.java @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.util.Modules; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; +import org.opentcs.configuration.ConfigurationBindingProvider; +import org.opentcs.configuration.gestalt.GestaltConfigurationBindingProvider; +import org.opentcs.customizations.ConfigurableInjectionModule; +import org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule; +import org.opentcs.guing.common.util.CompatibilityChecker; +import org.opentcs.operationsdesk.application.PlantOverviewStarter; +import org.opentcs.util.Environment; +import org.opentcs.util.logging.UncaughtExceptionLogger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The plant overview process's default entry point. + */ +public class RunOperationsDesk { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(RunOperationsDesk.class); + + /** + * Prevents external instantiation. + */ + private RunOperationsDesk() { + } + + /** + * The plant overview client's main entry point. + * + * @param args the command line arguments + */ + public static void main(final String[] args) { + Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionLogger(false)); + + Environment.logSystemInfo(); + ensureVersionCompatibility(); + + Injector injector = Guice.createInjector(customConfigurationModule()); + injector.getInstance(PlantOverviewStarter.class).startPlantOverview(); + } + + private static void ensureVersionCompatibility() { + String version = System.getProperty("java.version"); + if (!CompatibilityChecker.versionCompatibleWithDockingFrames(version)) { + LOG.error("Version incompatible with Docking Frames: '{}'", version); + CompatibilityChecker.showVersionIncompatibleWithDockingFramesMessage(); + System.exit(1); + } + } + + /** + * Builds and returns a Guice module containing the custom configuration for the plant overview + * application, including additions and overrides by the user. + * + * @return The custom configuration module. + */ + private static Module customConfigurationModule() { + ConfigurationBindingProvider bindingProvider = configurationBindingProvider(); + ConfigurableInjectionModule plantOverviewInjectionModule + = new DefaultPlantOverviewInjectionModule(); + plantOverviewInjectionModule.setConfigBindingProvider(bindingProvider); + return Modules.override(plantOverviewInjectionModule) + .with(findRegisteredModules(bindingProvider)); + } + + /** + * Finds and returns all Guice modules registered via ServiceLoader. + * + * @return The registered/found modules. + */ + private static List findRegisteredModules( + ConfigurationBindingProvider bindingProvider + ) { + List registeredModules = new ArrayList<>(); + for (PlantOverviewInjectionModule module : ServiceLoader.load( + PlantOverviewInjectionModule.class + )) { + LOG.info( + "Integrating injection module {} (source: {})", + module.getClass().getName(), + module.getClass().getProtectionDomain().getCodeSource() + ); + module.setConfigBindingProvider(bindingProvider); + registeredModules.add(module); + } + return registeredModules; + } + + private static ConfigurationBindingProvider configurationBindingProvider() { + String chosenProvider = System.getProperty("opentcs.configuration.provider", "gestalt"); + switch (chosenProvider) { + case "gestalt": + default: + LOG.info("Using gestalt as the configuration provider."); + return gestaltConfigurationBindingProvider(); + } + } + + private static ConfigurationBindingProvider gestaltConfigurationBindingProvider() { + return new GestaltConfigurationBindingProvider( + Paths.get( + System.getProperty("opentcs.base", "."), + "config", + "opentcs-operationsdesk-defaults-baseline.properties" + ) + .toAbsolutePath(), + Paths.get( + System.getProperty("opentcs.base", "."), + "config", + "opentcs-operationsdesk-defaults-custom.properties" + ) + .toAbsolutePath(), + Paths.get( + System.getProperty("opentcs.home", "."), + "config", + "opentcs-operationsdesk.properties" + ) + .toAbsolutePath() + ); + } + +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/application/ApplicationInjectionModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/application/ApplicationInjectionModule.java new file mode 100644 index 0000000..1dba12f --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/application/ApplicationInjectionModule.java @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application; + +import com.google.inject.AbstractModule; +import jakarta.inject.Singleton; +import java.awt.Component; +import javax.swing.JFrame; +import org.jhotdraw.app.Application; +import org.opentcs.common.KernelClientApplication; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.guing.common.application.ApplicationState; +import org.opentcs.guing.common.application.ComponentsManager; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.application.PluginPanelManager; +import org.opentcs.guing.common.application.ProgressIndicator; +import org.opentcs.guing.common.application.SplashFrame; +import org.opentcs.guing.common.application.StatusPanel; +import org.opentcs.guing.common.application.ViewManager; +import org.opentcs.operationsdesk.application.action.ActionInjectionModule; +import org.opentcs.operationsdesk.application.menus.MenusInjectionModule; +import org.opentcs.operationsdesk.application.toolbar.ToolBarInjectionModule; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.UndoRedoManager; +import org.opentcs.thirdparty.operationsdesk.jhotdraw.application.OpenTCSSDIApplication; + +/** + * An injection module for this package. + */ +public class ApplicationInjectionModule + extends + AbstractModule { + + /** + * The application's main frame. + */ + private final JFrame applicationFrame = new JFrame(); + + /** + * Creates a new instance. + */ + public ApplicationInjectionModule() { + } + + @Override + protected void configure() { + install(new ActionInjectionModule()); + install(new MenusInjectionModule()); + install(new ToolBarInjectionModule()); + + bind(ApplicationState.class).in(Singleton.class); + + bind(UndoRedoManager.class).in(Singleton.class); + + bind(ProgressIndicator.class) + .to(SplashFrame.class) + .in(Singleton.class); + bind(StatusPanel.class).in(Singleton.class); + + bind(JFrame.class) + .annotatedWith(ApplicationFrame.class) + .toInstance(applicationFrame); + bind(Component.class) + .annotatedWith(ApplicationFrame.class) + .toInstance(applicationFrame); + + bind(ViewManagerOperating.class) + .in(Singleton.class); + bind(ViewManager.class).to(ViewManagerOperating.class); + + bind(Application.class) + .to(OpenTCSSDIApplication.class) + .in(Singleton.class); + + bind(OpenTCSView.class).in(Singleton.class); + bind(GuiManager.class).to(OpenTCSView.class); + bind(ComponentsManager.class).to(OpenTCSView.class); + bind(PluginPanelManager.class).to(OpenTCSView.class); + bind(KernelClientApplication.class) + .to(OperationsDeskApplication.class) + .in(Singleton.class); + } + +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/application/action/ActionInjectionModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/application/action/ActionInjectionModule.java new file mode 100644 index 0000000..d6af39c --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/application/action/ActionInjectionModule.java @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action; + +import com.google.inject.AbstractModule; +import com.google.inject.assistedinject.FactoryModuleBuilder; +import jakarta.inject.Singleton; + +/** + * An injection module for this package. + */ +public class ActionInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public ActionInjectionModule() { + } + + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(ActionFactory.class)); + + bind(ViewActionMap.class).in(Singleton.class); + } + +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/application/menus/MenusInjectionModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/application/menus/MenusInjectionModule.java new file mode 100644 index 0000000..91a32bc --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/application/menus/MenusInjectionModule.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.menus; + +import com.google.inject.AbstractModule; +import com.google.inject.assistedinject.FactoryModuleBuilder; + +/** + */ +public class MenusInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public MenusInjectionModule() { + } + + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(MenuFactory.class)); + } + +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/application/toolbar/ToolBarInjectionModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/application/toolbar/ToolBarInjectionModule.java new file mode 100644 index 0000000..7a04270 --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/application/toolbar/ToolBarInjectionModule.java @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.toolbar; + +import com.google.inject.AbstractModule; +import com.google.inject.assistedinject.FactoryModuleBuilder; +import jakarta.inject.Singleton; +import org.jhotdraw.draw.tool.DragTracker; +import org.jhotdraw.draw.tool.SelectAreaTracker; +import org.opentcs.operationsdesk.application.action.ToolBarManager; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.toolbar.OpenTCSDragTracker; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.toolbar.OpenTCSSelectAreaTracker; + +/** + */ +public class ToolBarInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public ToolBarInjectionModule() { + } + + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(SelectionToolFactory.class)); + + bind(ToolBarManager.class).in(Singleton.class); + + bind(SelectAreaTracker.class).to(OpenTCSSelectAreaTracker.class); + bind(DragTracker.class).to(OpenTCSDragTracker.class); + } + +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/ComponentsInjectionModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/ComponentsInjectionModule.java new file mode 100644 index 0000000..6aed34f --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/ComponentsInjectionModule.java @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components; + +import com.google.inject.AbstractModule; +import com.google.inject.PrivateModule; +import jakarta.inject.Singleton; +import java.awt.event.MouseListener; +import org.opentcs.guing.common.components.tree.AbstractTreeViewPanel; +import org.opentcs.guing.common.components.tree.BlockMouseListener; +import org.opentcs.guing.common.components.tree.BlocksTreeViewManager; +import org.opentcs.guing.common.components.tree.BlocksTreeViewPanel; +import org.opentcs.guing.common.components.tree.ComponentsTreeViewManager; +import org.opentcs.guing.common.components.tree.ComponentsTreeViewPanel; +import org.opentcs.guing.common.components.tree.TreeMouseAdapter; +import org.opentcs.guing.common.components.tree.TreeView; +import org.opentcs.operationsdesk.components.dialogs.DialogsInjectionModule; +import org.opentcs.operationsdesk.components.dockable.DockableInjectionModule; +import org.opentcs.operationsdesk.components.drawing.DrawingInjectionModule; +import org.opentcs.operationsdesk.components.layer.LayersInjectionModule; +import org.opentcs.operationsdesk.components.properties.PropertiesInjectionModule; +import org.opentcs.operationsdesk.components.tree.elements.TreeElementsInjectionModule; + +/** + * A Guice module for this package. + */ +public class ComponentsInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public ComponentsInjectionModule() { + } + + @Override + protected void configure() { + install(new DialogsInjectionModule()); + install(new DockableInjectionModule()); + install(new DrawingInjectionModule()); + install(new PropertiesInjectionModule()); + install(new TreeElementsInjectionModule()); + + install(new ComponentsTreeViewModule()); + install(new BlocksTreeViewModule()); + + install(new LayersInjectionModule()); + } + + private static class ComponentsTreeViewModule + extends + PrivateModule { + + ComponentsTreeViewModule() { + } + + @Override + protected void configure() { + // Within this (private) module, there should only be a single tree panel. + bind(ComponentsTreeViewPanel.class) + .in(Singleton.class); + + // Bind the tree panel annotated with the given annotation to our single + // instance and expose only this annotated version. + bind(AbstractTreeViewPanel.class) + .to(ComponentsTreeViewPanel.class); + expose(ComponentsTreeViewPanel.class); + + // Bind TreeView to the single tree panel, too. + bind(TreeView.class) + .to(ComponentsTreeViewPanel.class); + + // Bind and expose a single manager for the single tree view/panel. + bind(ComponentsTreeViewManager.class) + .in(Singleton.class); + expose(ComponentsTreeViewManager.class); + + bind(MouseListener.class) + .to(TreeMouseAdapter.class); + } + } + + private static class BlocksTreeViewModule + extends + PrivateModule { + + BlocksTreeViewModule() { + } + + @Override + protected void configure() { + // Within this (private) module, there should only be a single tree panel. + bind(BlocksTreeViewPanel.class) + .in(Singleton.class); + + // Bind the tree panel annotated with the given annotation to our single + // instance and expose only this annotated version. + bind(AbstractTreeViewPanel.class) + .to(BlocksTreeViewPanel.class); + expose(BlocksTreeViewPanel.class); + + // Bind TreeView to the single tree panel, too. + bind(TreeView.class) + .to(BlocksTreeViewPanel.class); + + // Bind and expose a single manager for the single tree view/panel. + bind(BlocksTreeViewManager.class) + .in(Singleton.class); + expose(BlocksTreeViewManager.class); + + bind(MouseListener.class) + .to(BlockMouseListener.class); + } + } +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/dialogs/DialogsInjectionModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/dialogs/DialogsInjectionModule.java new file mode 100644 index 0000000..5e697ac --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/dialogs/DialogsInjectionModule.java @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.dialogs; + +import com.google.inject.AbstractModule; +import com.google.inject.assistedinject.FactoryModuleBuilder; +import jakarta.inject.Singleton; + +/** + * A Guice module for this package. + */ +public class DialogsInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public DialogsInjectionModule() { + } + + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(SingleVehicleViewFactory.class)); + install(new FactoryModuleBuilder().build(FindVehiclePanelFactory.class)); + + bind(VehiclesPanel.class).in(Singleton.class); + } +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/dockable/DockableInjectionModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/dockable/DockableInjectionModule.java new file mode 100644 index 0000000..4368340 --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/dockable/DockableInjectionModule.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.dockable; + +import com.google.inject.AbstractModule; +import com.google.inject.assistedinject.FactoryModuleBuilder; +import jakarta.inject.Singleton; +import org.opentcs.guing.common.components.dockable.DockableHandlerFactory; +import org.opentcs.guing.common.components.dockable.DockingManager; + +/** + * A Guice module for this package. + */ +public class DockableInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public DockableInjectionModule() { + } + + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(DockableHandlerFactory.class)); + + bind(DockingManagerOperating.class).in(Singleton.class); + bind(DockingManager.class).to(DockingManagerOperating.class); + } +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/drawing/DrawingInjectionModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/drawing/DrawingInjectionModule.java new file mode 100644 index 0000000..f884a3c --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/drawing/DrawingInjectionModule.java @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.drawing; + +import com.google.inject.AbstractModule; +import com.google.inject.assistedinject.FactoryModuleBuilder; +import jakarta.inject.Singleton; +import org.jhotdraw.draw.DrawingEditor; +import org.opentcs.guing.common.components.drawing.DrawingOptions; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingView; +import org.opentcs.guing.common.components.drawing.figures.ToolTipTextGenerator; +import org.opentcs.guing.common.util.CourseObjectFactory; +import org.opentcs.operationsdesk.components.drawing.figures.ToolTipTextGeneratorOperationsDesk; +import org.opentcs.operationsdesk.components.drawing.figures.VehicleFigureFactory; +import org.opentcs.operationsdesk.util.VehicleCourseObjectFactory; +import org.opentcs.thirdparty.operationsdesk.components.drawing.OpenTCSDrawingViewOperating; + +/** + * A Guice module for this package. + */ +public class DrawingInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public DrawingInjectionModule() { + } + + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(VehicleFigureFactory.class)); + bind(CourseObjectFactory.class).to(VehicleCourseObjectFactory.class); + + bind(OpenTCSDrawingEditorOperating.class).in(Singleton.class); + bind(OpenTCSDrawingEditor.class).to(OpenTCSDrawingEditorOperating.class); + bind(DrawingEditor.class).to(OpenTCSDrawingEditorOperating.class); + + bind(OpenTCSDrawingView.class).to(OpenTCSDrawingViewOperating.class); + + bind(DrawingOptions.class).in(Singleton.class); + + bind(ToolTipTextGeneratorOperationsDesk.class).in(Singleton.class); + bind(ToolTipTextGenerator.class).to(ToolTipTextGeneratorOperationsDesk.class); + } +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/layer/LayersInjectionModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/layer/LayersInjectionModule.java new file mode 100644 index 0000000..3e6ed79 --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/layer/LayersInjectionModule.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.layer; + +import jakarta.inject.Singleton; +import org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule; +import org.opentcs.guing.common.components.layer.DefaultLayerManager; +import org.opentcs.guing.common.components.layer.LayerEditor; +import org.opentcs.guing.common.components.layer.LayerGroupEditor; +import org.opentcs.guing.common.components.layer.LayerGroupManager; +import org.opentcs.guing.common.components.layer.LayerManager; + +/** + * A Guice module for this package. + */ +public class LayersInjectionModule + extends + PlantOverviewInjectionModule { + + /** + * Creates a new instance. + */ + public LayersInjectionModule() { + } + + @Override + protected void configure() { + bind(DefaultLayerManager.class).in(Singleton.class); + bind(LayerManager.class).to(DefaultLayerManager.class); + bind(LayerEditor.class).to(DefaultLayerManager.class); + bind(LayersPanel.class).in(Singleton.class); + + bind(LayerGroupManager.class).to(DefaultLayerManager.class); + bind(LayerGroupEditor.class).to(DefaultLayerManager.class); + bind(LayerGroupsPanel.class).in(Singleton.class); + } +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/properties/PropertiesInjectionModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/properties/PropertiesInjectionModule.java new file mode 100644 index 0000000..c564123 --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/properties/PropertiesInjectionModule.java @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.properties; + +import com.google.inject.TypeLiteral; +import com.google.inject.assistedinject.FactoryModuleBuilder; +import com.google.inject.multibindings.MapBinder; +import jakarta.inject.Singleton; +import org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule; +import org.opentcs.guing.base.components.properties.type.AbstractComplexProperty; +import org.opentcs.guing.base.components.properties.type.EnergyLevelThresholdSetProperty; +import org.opentcs.guing.base.components.properties.type.KeyValueProperty; +import org.opentcs.guing.base.components.properties.type.KeyValueSetProperty; +import org.opentcs.guing.base.components.properties.type.LinkActionsProperty; +import org.opentcs.guing.base.components.properties.type.LocationTypeActionsProperty; +import org.opentcs.guing.base.components.properties.type.OrderTypesProperty; +import org.opentcs.guing.base.components.properties.type.ResourceProperty; +import org.opentcs.guing.base.components.properties.type.SymbolProperty; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; +import org.opentcs.guing.common.components.properties.PropertiesComponentsFactory; +import org.opentcs.guing.common.components.properties.SelectionPropertiesComponent; +import org.opentcs.guing.common.components.properties.panel.EnergyLevelThresholdSetPropertyEditorPanel; +import org.opentcs.guing.common.components.properties.panel.KeyValuePropertyEditorPanel; +import org.opentcs.guing.common.components.properties.panel.KeyValueSetPropertyViewerEditorPanel; +import org.opentcs.guing.common.components.properties.panel.LinkActionsEditorPanel; +import org.opentcs.guing.common.components.properties.panel.LocationTypeActionsEditorPanel; +import org.opentcs.guing.common.components.properties.panel.OrderTypesPropertyEditorPanel; +import org.opentcs.guing.common.components.properties.panel.PropertiesPanelFactory; +import org.opentcs.guing.common.components.properties.panel.ResourcePropertyViewerEditorPanel; +import org.opentcs.guing.common.components.properties.panel.SymbolPropertyEditorPanel; +import org.opentcs.guing.common.components.properties.table.CellEditorFactory; + +/** + * A Guice module for this package. + */ +public class PropertiesInjectionModule + extends + PlantOverviewInjectionModule { + + /** + * Creates a new instance. + */ + public PropertiesInjectionModule() { + } + + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(PropertiesPanelFactory.class)); + install(new FactoryModuleBuilder().build(CellEditorFactory.class)); + install(new FactoryModuleBuilder().build(PropertiesComponentsFactory.class)); + + MapBinder, DetailsDialogContent> dialogContentMapBinder + = MapBinder.newMapBinder( + binder(), + new TypeLiteral>() { + }, + new TypeLiteral() { + } + ); + dialogContentMapBinder + .addBinding(KeyValueProperty.class) + .to(KeyValuePropertyEditorPanel.class); + dialogContentMapBinder + .addBinding(KeyValueSetProperty.class) + .to(KeyValueSetPropertyViewerEditorPanel.class); + dialogContentMapBinder + .addBinding(LocationTypeActionsProperty.class) + .to(LocationTypeActionsEditorPanel.class); + dialogContentMapBinder + .addBinding(LinkActionsProperty.class) + .to(LinkActionsEditorPanel.class); + dialogContentMapBinder + .addBinding(SymbolProperty.class) + .to(SymbolPropertyEditorPanel.class); + dialogContentMapBinder + .addBinding(OrderTypesProperty.class) + .to(OrderTypesPropertyEditorPanel.class); + dialogContentMapBinder + .addBinding(EnergyLevelThresholdSetProperty.class) + .to(EnergyLevelThresholdSetPropertyEditorPanel.class); + dialogContentMapBinder + .addBinding(ResourceProperty.class) + .to(ResourcePropertyViewerEditorPanel.class); + + bind(SelectionPropertiesComponent.class) + .in(Singleton.class); + + } + +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/tree/elements/TreeElementsInjectionModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/tree/elements/TreeElementsInjectionModule.java new file mode 100644 index 0000000..6a11fd1 --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/components/tree/elements/TreeElementsInjectionModule.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.tree.elements; + +import com.google.inject.AbstractModule; +import com.google.inject.assistedinject.FactoryModuleBuilder; +import org.opentcs.guing.common.components.tree.elements.UserObjectFactory; +import org.opentcs.guing.common.components.tree.elements.VehicleUserObject; + +/** + * A Guice module for this package. + */ +public class TreeElementsInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public TreeElementsInjectionModule() { + } + + @Override + protected void configure() { + install( + new FactoryModuleBuilder() + .implement(VehicleUserObject.class, VehicleUserObjectOperating.class) + .build(UserObjectFactory.class) + ); + } +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/exchange/ExchangeInjectionModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/exchange/ExchangeInjectionModule.java new file mode 100644 index 0000000..fc81f76 --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/exchange/ExchangeInjectionModule.java @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.exchange; + +import com.google.inject.AbstractModule; +import jakarta.inject.Singleton; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.common.DefaultPortalManager; +import org.opentcs.common.PortalManager; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.guing.common.exchange.AllocationHistory; +import org.opentcs.guing.common.exchange.ApplicationPortalProvider; +import org.opentcs.guing.common.exchange.adapter.VehicleAdapter; +import org.opentcs.operationsdesk.exchange.adapter.OpsDeskVehicleAdapter; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.event.EventSource; +import org.opentcs.util.event.SimpleEventBus; + +/** + * A Guice configuration module for this package. + */ +public class ExchangeInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public ExchangeInjectionModule() { + } + + @Override + protected void configure() { + bind(PortalManager.class) + .to(DefaultPortalManager.class) + .in(Singleton.class); + + bind(KernelEventFetcher.class) + .in(Singleton.class); + + EventBus eventBus = new SimpleEventBus(); + bind(EventSource.class) + .annotatedWith(ApplicationEventBus.class) + .toInstance(eventBus); + bind(EventHandler.class) + .annotatedWith(ApplicationEventBus.class) + .toInstance(eventBus); + bind(EventBus.class) + .annotatedWith(ApplicationEventBus.class) + .toInstance(eventBus); + + bind(SharedKernelServicePortalProvider.class) + .to(ApplicationPortalProvider.class) + .in(Singleton.class); + + bind(AttributeAdapterRegistry.class) + .in(Singleton.class); + bind(OpenTCSEventDispatcher.class) + .in(Singleton.class); + + bind(AllocationHistory.class) + .in(Singleton.class); + + bind(VehicleAdapter.class) + .to(OpsDeskVehicleAdapter.class); + } +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/model/ModelInjectionModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/model/ModelInjectionModule.java new file mode 100644 index 0000000..9fcf804 --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/model/ModelInjectionModule.java @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.model; + +import com.google.inject.AbstractModule; +import org.opentcs.guing.common.model.SystemModel; + +/** + * A Guice module for the model package. + */ +public class ModelInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public ModelInjectionModule() { + } + + @Override + protected void configure() { + bind(SystemModel.class).to(CachedSystemModel.class); + } + +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/notifications/NotificationInjectionModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/notifications/NotificationInjectionModule.java new file mode 100644 index 0000000..81b9950 --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/notifications/NotificationInjectionModule.java @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.notifications; + +import com.google.inject.AbstractModule; +import com.google.inject.assistedinject.FactoryModuleBuilder; +import jakarta.inject.Singleton; + +/** + * A Guice module for this package. + */ +public class NotificationInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public NotificationInjectionModule() { + } + + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(UserNotificationViewFactory.class)); + + bind(UserNotificationsContainer.class) + .in(Singleton.class); + } + +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobInjectionModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobInjectionModule.java new file mode 100644 index 0000000..e3e46cd --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobInjectionModule.java @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.peripherals.jobs; + +import com.google.inject.AbstractModule; +import com.google.inject.assistedinject.FactoryModuleBuilder; +import jakarta.inject.Singleton; + +/** + * A Guice module for this package. + */ +public class PeripheralJobInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public PeripheralJobInjectionModule() { + } + + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(PeripheralJobViewFactory.class)); + bind(PeripheralJobsContainer.class).in(Singleton.class); + } +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/persistence/DefaultPersistenceInjectionModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/persistence/DefaultPersistenceInjectionModule.java new file mode 100644 index 0000000..e1a0488 --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/persistence/DefaultPersistenceInjectionModule.java @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.persistence; + +import com.google.inject.AbstractModule; +import com.google.inject.Singleton; +import org.opentcs.guing.common.persistence.ModelFilePersistor; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.persistence.OpenTCSModelManager; +import org.opentcs.guing.common.persistence.unified.UnifiedModelPersistor; + +/** + * Default bindings for model readers and persistors. + */ +public class DefaultPersistenceInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public DefaultPersistenceInjectionModule() { + } + + @Override + protected void configure() { + bind(ModelManager.class).to(OpenTCSModelManager.class).in(Singleton.class); + + bind(ModelFilePersistor.class).to(UnifiedModelPersistor.class); + } + +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/transport/DefaultOrderTypeSuggestions.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/transport/DefaultOrderTypeSuggestions.java new file mode 100644 index 0000000..13c6a8f --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/transport/DefaultOrderTypeSuggestions.java @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport; + +import java.util.HashSet; +import java.util.Set; +import org.opentcs.components.plantoverview.OrderTypeSuggestions; +import org.opentcs.data.order.OrderConstants; + +/** + * The default suggestions for transport order types. + */ +public class DefaultOrderTypeSuggestions + implements + OrderTypeSuggestions { + + /** + * The transport order type suggestions. + */ + private final Set typeSuggestions = new HashSet<>(); + + /** + * Creates a new instance. + */ + public DefaultOrderTypeSuggestions() { + typeSuggestions.add(OrderConstants.TYPE_NONE); + typeSuggestions.add(OrderConstants.TYPE_CHARGE); + typeSuggestions.add(OrderConstants.TYPE_PARK); + typeSuggestions.add(OrderConstants.TYPE_TRANSPORT); + } + + @Override + public Set getTypeSuggestions() { + return typeSuggestions; + } +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/transport/OrderTypeSuggestionsModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/transport/OrderTypeSuggestionsModule.java new file mode 100644 index 0000000..abeaa62 --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/transport/OrderTypeSuggestionsModule.java @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport; + +import jakarta.inject.Singleton; +import org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule; +import org.opentcs.guing.common.transport.OrderTypeSuggestionsPool; + +/** + * A Guice module for the transport order type suggestions. + */ +public class OrderTypeSuggestionsModule + extends + PlantOverviewInjectionModule { + + /** + * Creates a new instance. + */ + public OrderTypeSuggestionsModule() { + } + + @Override + protected void configure() { + orderTypeSuggestionsBinder().addBinding() + .to(DefaultOrderTypeSuggestions.class) + .in(Singleton.class); + + bind(OrderTypeSuggestionsPool.class).in(Singleton.class); + } +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/transport/TransportInjectionModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/transport/TransportInjectionModule.java new file mode 100644 index 0000000..643be09 --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/transport/TransportInjectionModule.java @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport; + +import com.google.inject.AbstractModule; +import com.google.inject.assistedinject.FactoryModuleBuilder; +import jakarta.inject.Singleton; +import org.opentcs.operationsdesk.transport.orders.TransportOrdersContainer; +import org.opentcs.operationsdesk.transport.orders.TransportViewFactory; +import org.opentcs.operationsdesk.transport.sequences.OrderSequencesContainer; + +/** + * A Guice module for this package. + */ +public class TransportInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public TransportInjectionModule() { + } + + @Override + protected void configure() { + install(new FactoryModuleBuilder().build(TransportViewFactory.class)); + + bind(TransportOrdersContainer.class) + .in(Singleton.class); + + bind(OrderSequencesContainer.class) + .in(Singleton.class); + } +} diff --git a/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/util/UtilInjectionModule.java b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/util/UtilInjectionModule.java new file mode 100644 index 0000000..f79384d --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/java/org/opentcs/operationsdesk/util/UtilInjectionModule.java @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.util; + +import com.google.inject.AbstractModule; +import jakarta.inject.Singleton; +import org.opentcs.guing.common.util.PanelRegistry; + +/** + * A default Guice module for this package. + */ +public class UtilInjectionModule + extends + AbstractModule { + + /** + * Creates a new instance. + */ + public UtilInjectionModule() { + } + + @Override + protected void configure() { + bind(PanelRegistry.class).in(Singleton.class); + } +} diff --git a/opentcs-operationsdesk/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule b/opentcs-operationsdesk/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule new file mode 100644 index 0000000..c9c8c9c --- /dev/null +++ b/opentcs-operationsdesk/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT + +org.opentcs.operationsdesk.DefaultImportersExportersModule +org.opentcs.operationsdesk.PropertySuggestionsModule +org.opentcs.operationsdesk.transport.OrderTypeSuggestionsModule diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/OpenTCSView.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/OpenTCSView.java new file mode 100644 index 0000000..dc7e52b --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/OpenTCSView.java @@ -0,0 +1,1911 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application; + +import static java.util.Objects.requireNonNull; + +import bibliothek.gui.dock.common.DefaultSingleCDockable; +import bibliothek.gui.dock.common.SingleCDockable; +import bibliothek.gui.dock.common.event.CVetoClosingEvent; +import bibliothek.gui.dock.common.event.CVetoClosingListener; +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.FocusEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JToggleButton; +import javax.swing.JToolBar; +import javax.swing.SwingUtilities; +import javax.swing.border.EtchedBorder; +import org.jhotdraw.app.AbstractView; +import org.jhotdraw.draw.Drawing; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.gui.URIChooser; +import org.jhotdraw.util.ReversedList; +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.components.kernel.services.ServiceUnavailableException; +import org.opentcs.components.plantoverview.PlantModelExporter; +import org.opentcs.components.plantoverview.PlantModelImporter; +import org.opentcs.components.plantoverview.PluggablePanel; +import org.opentcs.components.plantoverview.PluggablePanelFactory; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.data.model.Vehicle; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.event.AttributesChangeListener; +import org.opentcs.guing.base.components.properties.type.AbstractProperty; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.event.BlockChangeEvent; +import org.opentcs.guing.base.event.BlockChangeListener; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.PropertiesCollection; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.application.ApplicationState; +import org.opentcs.guing.common.application.ComponentsManager; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.application.ModelRestorationProgressStatus; +import org.opentcs.guing.common.application.OperationMode; +import org.opentcs.guing.common.application.PluginPanelManager; +import org.opentcs.guing.common.application.ProgressIndicator; +import org.opentcs.guing.common.application.StartupProgressStatus; +import org.opentcs.guing.common.application.StatusPanel; +import org.opentcs.guing.common.components.dockable.DockableHandlerFactory; +import org.opentcs.guing.common.components.dockable.DrawingViewFocusHandler; +import org.opentcs.guing.common.components.drawing.DrawingViewScrollPane; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingView; +import org.opentcs.guing.common.components.drawing.course.Origin; +import org.opentcs.guing.common.components.drawing.course.OriginChangeListener; +import org.opentcs.guing.common.components.drawing.figures.FigureConstants; +import org.opentcs.guing.common.components.drawing.figures.LabeledFigure; +import org.opentcs.guing.common.components.drawing.figures.LabeledPointFigure; +import org.opentcs.guing.common.components.layer.LayerManager; +import org.opentcs.guing.common.components.properties.SelectionPropertiesComponent; +import org.opentcs.guing.common.components.properties.panel.PropertiesPanelFactory; +import org.opentcs.guing.common.components.tree.BlocksTreeViewManager; +import org.opentcs.guing.common.components.tree.ComponentsTreeViewManager; +import org.opentcs.guing.common.components.tree.TreeViewManager; +import org.opentcs.guing.common.components.tree.elements.UserObject; +import org.opentcs.guing.common.components.tree.elements.VehicleUserObject; +import org.opentcs.guing.common.event.DrawingEditorEvent; +import org.opentcs.guing.common.event.DrawingEditorListener; +import org.opentcs.guing.common.event.ModelNameChangeEvent; +import org.opentcs.guing.common.event.OperationModeChangeEvent; +import org.opentcs.guing.common.event.ResetInteractionToolCommand; +import org.opentcs.guing.common.event.SystemModelTransitionEvent; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.PanelRegistry; +import org.opentcs.guing.common.util.UserMessageHelper; +import org.opentcs.operationsdesk.application.action.ToolBarManager; +import org.opentcs.operationsdesk.application.action.ViewActionMap; +import org.opentcs.operationsdesk.components.dialogs.VehiclesPanel; +import org.opentcs.operationsdesk.components.dockable.DockingManagerOperating; +import org.opentcs.operationsdesk.components.drawing.DrawingViewFactory; +import org.opentcs.operationsdesk.components.drawing.OpenTCSDrawingEditorOperating; +import org.opentcs.operationsdesk.event.KernelStateChangeEvent; +import org.opentcs.operationsdesk.exchange.TransportOrderUtil; +import org.opentcs.operationsdesk.notifications.UserNotificationsContainerPanel; +import org.opentcs.operationsdesk.peripherals.jobs.PeripheralJobsContainerPanel; +import org.opentcs.operationsdesk.transport.orders.TransportOrdersContainerPanel; +import org.opentcs.operationsdesk.transport.sequences.OrderSequencesContainerPanel; +import org.opentcs.operationsdesk.util.Cursors; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.operationsdesk.util.OperationsDeskConfiguration; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.UndoRedoManager; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.toolbar.PaletteToolBarBorder; +import org.opentcs.thirdparty.guing.common.jhotdraw.components.drawing.AbstractOpenTCSDrawingView; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.thirdparty.operationsdesk.jhotdraw.application.action.file.CloseFileAction; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Visualizes the driving course and other kernel objects as well as messages + * received by the kernel. + * (Contains everything underneath the tool bars.) + */ +public class OpenTCSView + extends + AbstractView + implements + GuiManager, + ComponentsManager, + PluginPanelManager, + EventHandler { + + /** + * The name/title of this application. + */ + public static final String NAME + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.MISC_PATH) + .getString("openTcsView.applicationName.text"); + /** + * Property key for the currently loaded driving course model. + * The corresponding value contains a "*" if the model has been modified. + */ + public static final String MODELNAME_PROPERTY = "modelName"; + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(OpenTCSView.class); + /** + * This instance's resource bundle. + */ + private final ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.MISC_PATH); + /** + * Provides/manages the application's current state. + */ + private final ApplicationState appState; + /** + * Allows for undoing and redoing actions. + */ + private final UndoRedoManager fUndoRedoManager; + /** + * The drawing editor. + */ + private final OpenTCSDrawingEditorOperating fDrawingEditor; + /** + * The JFrame. + */ + private final JFrame fFrame; + /** + * Utility to manage the views. + */ + private final ViewManagerOperating viewManager; + /** + * A manager for the components tree view. + */ + private final TreeViewManager fComponentsTreeManager; + /** + * A manager for the blocks tree view. + */ + private final TreeViewManager fBlocksTreeManager; + /** + * Displays properties of the currently selected model component(s). + */ + private final SelectionPropertiesComponent fPropertiesComponent; + /** + * Manages driving course models. + */ + private final ModelManager fModelManager; + /** + * Indicates the progress for lengthy operations. + */ + private final ProgressIndicator progressIndicator; + /** + * Manages docking frames. + */ + private final DockingManagerOperating dockingManager; + /** + * Registry for plugin panels. + */ + private final PanelRegistry panelRegistry; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * The shared portal to be used. + */ + private SharedKernelServicePortal sharedPortal; + /** + * A panel for mouse position/status. + */ + private final StatusPanel statusPanel; + /** + * A panel showing all vehicles in the system model. + */ + private final VehiclesPanel vehiclesPanel; + /** + * Shows messages to the user. + */ + private final UserMessageHelper userMessageHelper; + /** + * A factory for drawing views. + */ + private final DrawingViewFactory drawingViewFactory; + /** + * Handles block events. + */ + private final BlockChangeListener blockEventHandler = new BlockEventHandler(); + /** + * Handles events for changes of properties. + */ + private final AttributesChangeListener attributesEventHandler = new AttributesEventHandler(); + /** + * A provider for ActionMaps. + */ + private final Provider actionMapProvider; + /** + * A provider for the tool bar manager. + */ + private final Provider toolBarManagerProvider; + /** + * A factory for properties-related panels. + */ + private final PropertiesPanelFactory propertiesPanelFactory; + /** + * A provider for panels showing the user notifications. + */ + private final Provider unContainerPanelProvider; + /** + * A provider for panels showing the transport orders. + */ + private final Provider toContainerPanelProvider; + /** + * A provider for panels showing the order sequences. + */ + private final Provider osContainerPanelProvider; + /** + * A provider for panels showing the peripheral jobs. + */ + private final Provider pjContainerPanelProvider; + /** + * A helper for creating transport orders with the kernel. + */ + private final TransportOrderUtil orderUtil; + /** + * The application's event bus. + */ + private final EventBus eventBus; + /** + * Handles focussing of dockables. + */ + private final DrawingViewFocusHandler drawingViewFocusHandler; + /** + * A factory for handlers related to dockables. + */ + private final DockableHandlerFactory dockableHandlerFactory; + /** + * The layer manager. + */ + private final LayerManager layerManager; + /** + * The operations desk application's configuration. + */ + private final OperationsDeskConfiguration configuration; + /** + * Provides the application's tool bars. + */ + private ToolBarManager toolBarManager; + + /** + * Creates a new instance. + * + * @param appState Provides/manages the application's current state. + * @param appFrame The JFrame this view is wrapped in. + * @param progressIndicator The progress indicator to be used. + * @param portalProvider Provides a access to a portal. + * @param viewManager The view manager to be used. + * @param tcsDrawingEditor The drawing editor to be used. + * @param modelManager The model manager to be used. + * @param statusPanel The status panel to be used. + * @param panelRegistry The plugin panel registry to be used. + * @param userMessageHelper An UserMessageHelper + * @param drawingViewFactory A factory for drawing views. + * @param undoRedoManager Allows for undoing and redoing actions. + * @param componentsTreeManager Manages the components tree view. + * @param blocksTreeManager Manages the blocks tree view. + * @param propertiesComponent Displays properties of the currently selected model component(s). + * @param vehiclesPanel A panel showing all the vehicles in the system model. + * @param actionMapProvider A provider for ActionMaps. + * @param toolBarManagerProvider A provider for the tool bar manager. + * @param propertiesPanelFactory A factory for properties-related panels. + * @param unContainerPanelProvider A provider for panels showing the user notifications. + * @param toContainerPanelProvider A provider for panels showing the transport orders. + * @param osContainerPanelProvider A provider for panels showing the order sequences. + * @param pjContainerPanelProvider A provider for panels showing the peripheral jobs. + * @param orderUtil A helper for creating transport orders with the kernel. + * @param eventBus The application's event bus. + * @param dockingManager Manages docking frames. + * @param drawingViewFocusHandler Handles focussing of dockables. + * @param dockableHandlerFactory A factory for handlers related to dockables. + * @param layerManager The layer manager. + * @param configuration The operations desk application's configuration. + */ + @Inject + public OpenTCSView( + ApplicationState appState, + @ApplicationFrame + JFrame appFrame, + ProgressIndicator progressIndicator, + SharedKernelServicePortalProvider portalProvider, + ViewManagerOperating viewManager, + OpenTCSDrawingEditorOperating tcsDrawingEditor, + ModelManager modelManager, + StatusPanel statusPanel, + PanelRegistry panelRegistry, + UserMessageHelper userMessageHelper, + DrawingViewFactory drawingViewFactory, + UndoRedoManager undoRedoManager, + ComponentsTreeViewManager componentsTreeManager, + BlocksTreeViewManager blocksTreeManager, + SelectionPropertiesComponent propertiesComponent, + VehiclesPanel vehiclesPanel, + Provider actionMapProvider, + Provider toolBarManagerProvider, + PropertiesPanelFactory propertiesPanelFactory, + Provider unContainerPanelProvider, + Provider toContainerPanelProvider, + Provider osContainerPanelProvider, + Provider pjContainerPanelProvider, + TransportOrderUtil orderUtil, + @ApplicationEventBus + EventBus eventBus, + DockingManagerOperating dockingManager, + DrawingViewFocusHandler drawingViewFocusHandler, + DockableHandlerFactory dockableHandlerFactory, + LayerManager layerManager, + OperationsDeskConfiguration configuration + ) { + this.appState = requireNonNull(appState, "appState"); + this.fFrame = requireNonNull(appFrame, "appFrame"); + this.progressIndicator = requireNonNull(progressIndicator, "progressIndicator"); + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.viewManager = requireNonNull(viewManager, "viewManager"); + this.fDrawingEditor = requireNonNull(tcsDrawingEditor, "tcsDrawingEditor"); + this.fModelManager = requireNonNull(modelManager, "modelManager"); + this.statusPanel = requireNonNull(statusPanel, "statusPanel"); + this.panelRegistry = requireNonNull(panelRegistry, "panelRegistry"); + this.userMessageHelper = requireNonNull(userMessageHelper, "userMessageHelper"); + this.drawingViewFactory = requireNonNull(drawingViewFactory, "drawingViewFactory"); + this.fUndoRedoManager = requireNonNull(undoRedoManager, "undoRedoManager"); + this.fComponentsTreeManager = requireNonNull(componentsTreeManager, "componentsTreeManager"); + this.fBlocksTreeManager = requireNonNull(blocksTreeManager, "blocksTreeManager"); + this.fPropertiesComponent = requireNonNull(propertiesComponent, "propertiesComponent"); + this.vehiclesPanel = requireNonNull(vehiclesPanel, "vehiclesPanel"); + this.actionMapProvider = requireNonNull(actionMapProvider, "actionMapProvider"); + this.toolBarManagerProvider = requireNonNull(toolBarManagerProvider, "toolBarManagerProvider"); + this.propertiesPanelFactory = requireNonNull(propertiesPanelFactory, "propertiesPanelFactory"); + this.unContainerPanelProvider = requireNonNull( + unContainerPanelProvider, + "unContainerPanelProvider" + ); + this.toContainerPanelProvider = requireNonNull( + toContainerPanelProvider, + "toContainerPanelProvider" + ); + this.osContainerPanelProvider = requireNonNull( + osContainerPanelProvider, + "osContainerPanelProvider" + ); + this.pjContainerPanelProvider = requireNonNull( + pjContainerPanelProvider, + "pjContainerPanelProvider" + ); + this.orderUtil = requireNonNull(orderUtil, "orderUtil"); + this.eventBus = requireNonNull(eventBus, "eventBus"); + this.dockingManager = requireNonNull(dockingManager, "dockingManager"); + this.drawingViewFocusHandler = requireNonNull( + drawingViewFocusHandler, + "drawingViewFocusHandler" + ); + this.dockableHandlerFactory = requireNonNull(dockableHandlerFactory, "dockableHandlerFactory"); + this.layerManager = requireNonNull(layerManager, "layerManager"); + this.configuration = requireNonNull(configuration, "configuration"); + } + + @Override // AbstractView + public void init() { + eventBus.subscribe(this); + + progressIndicator.setProgress(StartupProgressStatus.INITIALIZED); + + fDrawingEditor.addDrawingEditorListener(new DrawingEditorEventHandler(fModelManager)); + + eventBus.subscribe(fComponentsTreeManager); + + progressIndicator.setProgress(StartupProgressStatus.INITIALIZE_MODEL); + setSystemModel(fModelManager.getModel()); + + // Properties view (lower left corner) + fPropertiesComponent.setPropertiesContent( + propertiesPanelFactory.createPropertiesTableContent(this) + ); + + // Register a listener for dragging vehicles around. + VehicleDragHandler listener = new VehicleDragHandler(Cursors.getDragVehicleCursor()); + fComponentsTreeManager.addMouseListener(listener); + fComponentsTreeManager.addMouseMotionListener(listener); + + setActionMap(actionMapProvider.get()); + this.toolBarManager = toolBarManagerProvider.get(); + eventBus.subscribe(toolBarManager); + + eventBus.subscribe(fPropertiesComponent); + eventBus.subscribe(vehiclesPanel); + eventBus.subscribe(fUndoRedoManager); + eventBus.subscribe(fDrawingEditor); + + layerManager.initialize(); + + initializeFrame(); + viewManager.init(); + createEmptyModel(); + + setOperatingState(); + } + + @Override // AbstractView + public void stop() { + LOG.info("GUI terminating..."); + eventBus.unsubscribe(this); + System.exit(0); + } + + @Override // AbstractView + public void clear() { + } + + @Override + public void onEvent(Object event) { + if (event instanceof SystemModelTransitionEvent) { + handleSystemModelTransition((SystemModelTransitionEvent) event); + } + if (event instanceof KernelStateChangeEvent) { + handleKernelStateChangeEvent((KernelStateChangeEvent) event); + } + } + + private void handleSystemModelTransition(SystemModelTransitionEvent evt) { + switch (evt.getStage()) { + case LOADED: + setHasUnsavedChanges(false); + // Update model name in title + setModelNameProperty(fModelManager.getModel().getName()); + break; + default: + // Do nada. + } + } + + @Override + public void showPluginPanel(PluggablePanelFactory factory, boolean visible) { + String id = factory.getClass().getName(); + SingleCDockable dockable = dockingManager.getCControl().getSingleDockable(id); + if (dockable != null) { + // dockable is not null at this point when the user hides the plugin + // panel by clicking on its menu entry + PluggablePanel panel = (PluggablePanel) dockable.getFocusComponent(); + panel.terminate(); + if (!dockingManager.getCControl().removeDockable(dockable)) { + LOG.warn("Couldn't remove dockable for plugin panel factory '{}'", factory); + return; + } + } + if (!visible) { + return; + } + if (factory.providesPanel(OperationMode.equivalent(appState.getOperationMode()))) { + PluggablePanel panel + = factory.createPanel(OperationMode.equivalent(appState.getOperationMode())); + DefaultSingleCDockable factoryDockable = dockingManager.createFloatingDockable( + factory.getClass().getName(), + factory.getPanelDescription(), + panel + ); + factoryDockable.addVetoClosingListener(new CVetoClosingListener() { + + @Override + public void closing(CVetoClosingEvent event) { + } + + @Override + public void closed(CVetoClosingEvent event) { + panel.terminate(); + dockingManager.getCControl().removeDockable(factoryDockable); + } + }); + panel.initialize(); + } + } + + /** + * Adds a new drawing view to the tabbed wrappingPanel. + * + * @return The newly created Dockable. + */ + public DefaultSingleCDockable addDrawingView() { + DrawingViewScrollPane newScrollPane + = drawingViewFactory.createDrawingView( + fModelManager.getModel(), + toolBarManager.getSelectionToolButton(), + toolBarManager.getDragToolButton() + ); + + int drawingViewIndex = viewManager.getNextDrawingViewIndex(); + + String title + = bundle.getString("openTcsView.panel_operatingDrawingView.title") + " " + drawingViewIndex; + DefaultSingleCDockable newDockable + = dockingManager.createDockable( + "drivingCourse" + drawingViewIndex, + title, + newScrollPane, + true + ); + viewManager.addDrawingView(newDockable, newScrollPane); + + int lastIndex = Math.max(0, drawingViewIndex - 1); + dockingManager.addTabTo(newDockable, DockingManagerOperating.COURSE_TAB_PANE_ID, lastIndex); + + newDockable.addVetoClosingListener(new DrawingViewClosingListener(newDockable)); + newDockable.addFocusListener(drawingViewFocusHandler); + + newScrollPane.getDrawingView().getComponent() + .dispatchEvent(new FocusEvent(this, FocusEvent.FOCUS_GAINED)); + firePropertyChange(AbstractOpenTCSDrawingView.FOCUS_GAINED, null, newDockable); + + return newDockable; + } + + /** + * Adds a new transport order view. + */ + public void addTransportOrderView() { + int biggestIndex = viewManager.getNextTransportOrderViewIndex(); + DefaultSingleCDockable lastTOView = viewManager.getLastTransportOrderView(); + TransportOrdersContainerPanel panel = toContainerPanelProvider.get(); + DefaultSingleCDockable newDockable + = dockingManager.createDockable( + "transportOrders" + biggestIndex, + bundle.getString( + "openTcsView.panel_operatingTransportOrdersView.title" + ) + + " " + biggestIndex, panel, true + ); + viewManager.addTransportOrderView(newDockable, panel); + + panel.initView(); + + newDockable.addVetoClosingListener( + dockableHandlerFactory.createDockableClosingHandler(newDockable) + ); + + final int indexToInsert; + + if (lastTOView != null) { + indexToInsert = dockingManager + .getTabPane(DockingManagerOperating.COURSE_TAB_PANE_ID) + .getStation() + .indexOf(lastTOView.intern()) + 1; + } + else { + indexToInsert = viewManager.getDrawingViewMap().size(); + } + + dockingManager.addTabTo(newDockable, DockingManagerOperating.COURSE_TAB_PANE_ID, indexToInsert); + } + + /** + * Adds a new order sequence view. + */ + public void addTransportOrderSequenceView() { + int biggestIndex = viewManager.getNextOrderSequenceViewIndex(); + DefaultSingleCDockable lastOSView = viewManager.getLastOrderSequenceView(); + + OrderSequencesContainerPanel panel = osContainerPanelProvider.get(); + DefaultSingleCDockable newDockable + = dockingManager.createDockable( + "orderSequences" + biggestIndex, + bundle.getString( + "openTcsView.panel_operatingOrderSequencesView.title" + ) + + " " + biggestIndex, + panel, true + ); + viewManager.addOrderSequenceView(newDockable, panel); + + panel.initView(); + + newDockable.addVetoClosingListener( + dockableHandlerFactory.createDockableClosingHandler(newDockable) + ); + + final int indexToInsert; + if (lastOSView != null) { + indexToInsert = dockingManager + .getTabPane(DockingManagerOperating.COURSE_TAB_PANE_ID) + .getStation() + .indexOf(lastOSView.intern()) + 1; + } + else { + indexToInsert = viewManager.getTransportOrderMap().size() + + viewManager.getDrawingViewMap().size(); + } + dockingManager.addTabTo(newDockable, DockingManagerOperating.COURSE_TAB_PANE_ID, indexToInsert); + } + + /** + * Adds a new peripheral jobs view. + */ + public void addPeripheralJobsView() { + int biggestIndex = viewManager.getNextPeripheralJobViewIndex(); + DefaultSingleCDockable lastView = viewManager.getLastPeripheralJobView(); + + PeripheralJobsContainerPanel panel = pjContainerPanelProvider.get(); + DefaultSingleCDockable newDockable + = dockingManager.createDockable( + "peripheralJobs" + biggestIndex, + bundle.getString( + "openTcsView.panel_peripheralJobsView.title" + ) + + " " + biggestIndex, + panel, true + ); + viewManager.addPeripheralJobView(newDockable, panel); + + panel.initView(); + + newDockable.addVetoClosingListener( + dockableHandlerFactory.createDockableClosingHandler(newDockable) + ); + + final int indexToInsert; + if (lastView != null) { + indexToInsert = dockingManager + .getTabPane(DockingManagerOperating.COURSE_TAB_PANE_ID) + .getStation() + .indexOf(lastView.intern()) + 1; + } + else { + indexToInsert = viewManager.getTransportOrderMap().size() + + viewManager.getOrderSequenceMap().size() + + viewManager.getDrawingViewMap().size(); + } + dockingManager.addTabTo(newDockable, DockingManagerOperating.COURSE_TAB_PANE_ID, indexToInsert); + } + + /** + * Adds a new user notification view. + */ + public void addUserNotificationView() { + int biggestIndex = viewManager.getNextUserNotificationViewIndex(); + DefaultSingleCDockable lastUNView = viewManager.getLastUserNotificationView(); + UserNotificationsContainerPanel panel = unContainerPanelProvider.get(); + DefaultSingleCDockable newDockable + = dockingManager.createDockable( + "userNotifications" + biggestIndex, + bundle.getString( + "openTcsView.panel_operatingUserNotificationsView.title" + ) + + " " + biggestIndex, panel, true + ); + viewManager.addUserNotificationView(newDockable, panel); + + panel.initView(); + + newDockable.addVetoClosingListener( + dockableHandlerFactory.createDockableClosingHandler(newDockable) + ); + + final int indexToInsert; + + if (lastUNView != null) { + indexToInsert = dockingManager + .getTabPane(DockingManagerOperating.COURSE_TAB_PANE_ID) + .getStation() + .indexOf(lastUNView.intern()) + 1; + } + else { + indexToInsert = viewManager.getDrawingViewMap().size() + + viewManager.getOrderSequenceMap().size() + + viewManager.getTransportOrderMap().size() + + viewManager.getDrawingViewMap().size(); + } + + dockingManager.addTabTo(newDockable, DockingManagerOperating.COURSE_TAB_PANE_ID, indexToInsert); + } + + /** + * Restores the layout to default. + */ + public void resetWindowArrangement() { + for (DefaultSingleCDockable dock : new ArrayList<>(viewManager.getDrawingViewMap().keySet())) { + removeDrawingView(dock); + } + for (DefaultSingleCDockable dock : new ArrayList<>( + viewManager.getUserNotificationMap().keySet() + )) { + dockingManager.removeDockable(dock); + } + for (DefaultSingleCDockable dock : new ArrayList<>( + viewManager.getTransportOrderMap().keySet() + )) { + dockingManager.removeDockable(dock); + } + for (DefaultSingleCDockable dock : new ArrayList<>( + viewManager.getOrderSequenceMap().keySet() + )) { + dockingManager.removeDockable(dock); + } + for (DefaultSingleCDockable dock : new ArrayList<>( + viewManager.getPeripheralJobMap().keySet() + )) { + dockingManager.removeDockable(dock); + } + dockingManager.reset(); + closeOpenedPluginPanels(); + viewManager.reset(); + + initializeFrame(); + viewManager.init(); + + // Depending on the current kernel state there may exist panels, now, that shouldn't be visible. + new Thread(() -> setOperatingState()).start(); + } + + @Override // GuiManager + public void createEmptyModel() { + CloseFileAction action = (CloseFileAction) getActionMap().get(CloseFileAction.ID); + if (action != null) { + action.actionPerformed( + new ActionEvent( + this, + ActionEvent.ACTION_PERFORMED, + CloseFileAction.ID_MODEL_CLOSING + ) + ); + if (action.getFileSavedStatus() == JOptionPane.CANCEL_OPTION) { + return; + } + } + + // Clean up first... + eventBus.onEvent( + new SystemModelTransitionEvent( + this, + SystemModelTransitionEvent.Stage.UNLOADING, + fModelManager.getModel() + ) + ); + + eventBus.onEvent( + new SystemModelTransitionEvent( + this, + SystemModelTransitionEvent.Stage.UNLOADED, + fModelManager.getModel() + ) + ); + + // Create the new, empty model. + LOG.debug("Creating new driving course model..."); + fModelManager.createEmptyModel(); + + eventBus.onEvent( + new SystemModelTransitionEvent( + this, + SystemModelTransitionEvent.Stage.LOADING, + fModelManager.getModel() + ) + ); + + // Now let components set themselves up for the new model. + setSystemModel(fModelManager.getModel()); + + eventBus.onEvent( + new SystemModelTransitionEvent( + this, + SystemModelTransitionEvent.Stage.LOADED, + fModelManager.getModel() + ) + ); + + // makes sure the origin is on the lower left side and the ruler + // are correctly drawn + fDrawingEditor.initializeViewport(); + } + + public void loadCurrentKernelModel() { + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + loadCurrentKernelModel(sharedPortal.getPortal()); + } + catch (ServiceUnavailableException exc) { + LOG.info("Kernel unavailable, aborting.", exc); + } + } + + /** + * Loads the current kernel model. + */ + private void loadCurrentKernelModel(KernelServicePortal portal) { + if (hasUnsavedChanges()) { + if (!showUnsavedChangesDialog()) { + return; + } + } + if (portal.getState() == Kernel.State.MODELLING) { + handleKernelInModellingMode(); + return; + } + restoreModel(portal); + } + + /** + * Initializes the model stored in the kernel or in the model manager. + * + * @param portal If not null, the model from the given kernel will be loaded, else the model from + * the model manager + */ + private void restoreModel( + @Nullable + KernelServicePortal portal + ) { + progressIndicator.initialize(); + + progressIndicator.setProgress(ModelRestorationProgressStatus.CLEANUP); + eventBus.onEvent( + new SystemModelTransitionEvent( + this, + SystemModelTransitionEvent.Stage.UNLOADING, + fModelManager.getModel() + ) + ); + + progressIndicator.setProgress(ModelRestorationProgressStatus.START_LOADING_MODEL); + eventBus.onEvent( + new SystemModelTransitionEvent( + this, + SystemModelTransitionEvent.Stage.UNLOADED, + fModelManager.getModel() + ) + ); + + if (portal == null) { + fModelManager.restoreModel(); + } + else { + fModelManager.restoreModel(portal); + statusPanel.setLogMessage( + Level.INFO, + bundle.getFormatted( + "openTcsView.message_modelLoaded.text", + fModelManager.getModel().getName() + ) + ); + } + + progressIndicator.setProgress(ModelRestorationProgressStatus.SET_UP_MODEL_VIEW); + eventBus.onEvent( + new SystemModelTransitionEvent( + this, + SystemModelTransitionEvent.Stage.LOADING, + fModelManager.getModel() + ) + ); + + setSystemModel(fModelManager.getModel()); + + progressIndicator.setProgress(ModelRestorationProgressStatus.SET_UP_DIRECTORY_TREE); + + progressIndicator.setProgress(ModelRestorationProgressStatus.SET_UP_WORKING_AREA); + + ModelComponent layoutComponent + = fModelManager.getModel().getMainFolder(SystemModel.FolderKey.LAYOUT); + layoutComponent.addAttributesChangeListener(attributesEventHandler); + + eventBus.onEvent( + new SystemModelTransitionEvent( + this, SystemModelTransitionEvent.Stage.LOADED, + fModelManager.getModel() + ) + ); + updateModelName(); + + progressIndicator.terminate(); + } + + /** + * Shows a dialog telling the user the plant overview state can't be switched + * as long as it has unsaved changes. + * + * @return true if the user saved the model or + * discarded the changes (the program can + * continue normally), false if the user pressed cancel. + */ + private boolean showSwitchStateUnsavedChangesDialog() { + String title = bundle.getString("openTcsView.dialog_unsavedChanges.title"); + String text = bundle.getString("openTcsView.dialog_unsavedChanges.message"); + String[] options = {bundle.getString("openTcsView.dialog_unsavedChanges.option_upload.text"), + bundle.getString("openTcsView.dialog_unsavedChanges.option_discard.text"), + bundle.getString("openTcsView.dialog_unsavedChanges.option_cancel.text")}; + switch (userMessageHelper.showOptionsDialog( + title, + text, + UserMessageHelper.Type.ERROR, + options + )) { + case 0: + return uploadModelToKernel(); + case 1: + setHasUnsavedChanges(false); + return true; + default: + return false; + } + } + + private void handleKernelStateChangeEvent(KernelStateChangeEvent event) { + closeOpenedPluginPanels(); + switch (event.getNewState()) { + case LOGGED_IN: + handleKernelConnection(); + break; + case MODELLING: + handleKernelInModellingMode(); + break; + case OPERATING: + if (hasUnsavedChanges()) { + if (!showSwitchStateUnsavedChangesDialog()) { + return; + } + } + SwingUtilities.invokeLater(() -> loadCurrentKernelModel()); + break; + case DISCONNECTED: + case SHUTDOWN: + default: + handleNoKernelConnection(); + handleKernelInModellingMode(); + } + } + + private void handleNoKernelConnection() { + if (sharedPortal != null) { + sharedPortal.close(); + sharedPortal = null; + } + statusPanel.setLogMessage( + Level.INFO, + bundle.getFormatted( + "openTcsView.message_disconnectedFromKernel.text", + fModelManager.getModel().getName() + ) + ); + } + + private void handleKernelConnection() { + try { + sharedPortal = portalProvider.register(); + } + catch (ServiceUnavailableException exc) { + handleNoKernelConnection(); + } + } + + private void handleKernelInModellingMode() { + createEmptyModel(); + } + + private void setModelNameProperty(String modelName) { + fModelManager.getModel().setName(modelName); + eventBus.onEvent(new ModelNameChangeEvent(this, modelName)); + } + + public void updateModelName() { + String newName = fModelManager.getModel().getName(); + eventBus.onEvent(new ModelNameChangeEvent(this, newName)); + } + + /** + * Adds a background image to the currently active drawing view. + * + * @param file The file with the image. + */ + public void addBackgroundBitmap(File file) { + getActiveDrawingView().addBackgroundBitmap(file); + } + + @Override + public List restoreModelComponents(List userObjects) { + // This method is specific to modeling mode + return new ArrayList<>(); + } + + @Override // View + public void write(URI f, URIChooser chooser) + throws IOException { + } + + @Override // View + public void read(URI f, URIChooser chooser) + throws IOException { + } + + @Override // AbstractView + public boolean canSaveTo(URI file) { + return new File(file).getName().endsWith(".xml"); + } + + @Override // AbstractView + public URI getURI() { + String modelName = fModelManager.getModel().getName(); + + try { + uri = new URI(modelName); + } + catch (URISyntaxException ex) { + LOG.warn("URISyntaxException in getURI({})", modelName, ex); + } + + return uri; + } + + /** + * Returns all drawing views (including the modelling view). + * + * @return List with all known OpenTCSDrawingViews. + */ + private List getDrawingViews() { + List views = new ArrayList<>(); + + for (DrawingViewScrollPane scrollPane : viewManager.getDrawingViewMap().values()) { + views.add(scrollPane.getDrawingView()); + } + + return views; + } + + @Override + public void selectModelComponent(ModelComponent modelComponent) { + fPropertiesComponent.setModel(modelComponent); + DrawingView drawingView = fDrawingEditor.getActiveView(); + drawingView.clearSelection(); + Figure figure = findFigure(modelComponent); + // LocationType hat keine Figur + if (figure != null) { + drawingView.toggleSelection(figure); + } + } + + @Override// GuiManager + public void addSelectedModelComponent(ModelComponent modelComponent) { + Set components = fComponentsTreeManager.getSelectedItems(); + + if (components.size() > 1) { + components.add(modelComponent); + + DrawingView drawingView = fDrawingEditor.getActiveView(); + drawingView.clearSelection(); + + Collection
figures = new ArrayList<>(components.size()); + for (ModelComponent comp : components) { + Figure figure = findFigure(comp); + + // At least LocationTypes do not have a Figure! + if (figure != null) { + figures.add(figure); + } + } + drawingView.addToSelection(figures); + + fPropertiesComponent.setModel(new PropertiesCollection(components)); + // Re-select all originally selected objects in the tree. + fComponentsTreeManager.selectItems(components); + } + else { + // In operating mode, only one component can be selected. + selectModelComponent(modelComponent); + } + } + + @Override// GuiManager + public boolean treeComponentRemoved(ModelComponent model) { + return false; + } + + @Override // GuiManager + public void figureSelected(ModelComponent modelComponent) { + modelComponent.addAttributesChangeListener(attributesEventHandler); + fPropertiesComponent.setModel(modelComponent); + + Figure figure = findFigure(modelComponent); + OpenTCSDrawingView drawingView = fDrawingEditor.getActiveView(); + + if (figure != null) { + drawingView.clearSelection(); + drawingView.addToSelection(figure); + // Scroll view to this figure. + drawingView.scrollTo(figure); + } + } + + @Override // GuiManager + public void loadModel() { + } + + @Override + public void importModel(PlantModelImporter importer) { + } + + /** + * Shows a dialog to save unsaved changes. + * + * @return true if the user pressed yes or no, false + * if the user pressed cancel. + */ + private boolean showUnsavedChangesDialog() { + CloseFileAction action = (CloseFileAction) getActionMap().get(CloseFileAction.ID); + action.actionPerformed( + new ActionEvent( + this, + ActionEvent.ACTION_PERFORMED, + CloseFileAction.ID_MODEL_CLOSING + ) + ); + switch (action.getFileSavedStatus()) { + case JOptionPane.YES_OPTION: + super.setHasUnsavedChanges(false); + return true; + case JOptionPane.NO_OPTION: + return true; + case JOptionPane.CANCEL_OPTION: + return false; + default: + return false; + } + } + + /** + * Uploads the current (local) model to the kernel. + * + * @return Whether the model was actually uploaded. + */ + private boolean uploadModelToKernel() { + return false; + } + + @Override + public boolean saveModel() { + boolean saved = fModelManager.saveModelToFile(false); + if (saved) { + String modelName = fModelManager.getModel().getName(); + setModelNameProperty(modelName); + setHasUnsavedChanges(false); + } + return saved; + } + + @Override // GuiManager + public boolean saveModelAs() { + boolean saved = fModelManager.saveModelToFile(true); + if (saved) { + String modelName = fModelManager.getModel().getName(); + setModelNameProperty(modelName); + setHasUnsavedChanges(false); + } + return saved; + } + + @Override + public void exportModel(PlantModelExporter exporter) { + } + + private OpenTCSDrawingView getActiveDrawingView() { + return fDrawingEditor.getActiveView(); + } + + private void removeDrawingView(DefaultSingleCDockable dock) { + if (!viewManager.getDrawingViewMap().containsKey(dock)) { + return; + } + + fDrawingEditor.remove(viewManager.getDrawingViewMap().get(dock).getDrawingView()); + viewManager.removeDockable(dock); + dockingManager.removeDockable(dock); + } + + /** + * Combines the OpenTCSView panel and the panel for the tool bars to a new + * panel. + * + * @return The resulting panel. + */ + private JPanel wrapViewComponent() { + // Add a dummy toolbar for dragging. + // (Preview to see how the tool bar would look like after dragging?) + final JToolBar toolBar = new JToolBar(); + // A wholeComponentPanel for toolbars above the OpenTCSView wholeComponentPanel. + final JPanel toolBarPanel = new JPanel(); + toolBarPanel.setLayout(new BoxLayout(toolBarPanel, BoxLayout.LINE_AXIS)); + toolBar.setBorder(new PaletteToolBarBorder()); + + final List lToolBars = new ArrayList<>(); + + // The new wholeComponentPanel for the whole component. + JPanel wholeComponentPanel = new JPanel(new BorderLayout()); + wholeComponentPanel.add(toolBarPanel, BorderLayout.NORTH); + wholeComponentPanel.add(getComponent()); + lToolBars.add(toolBar); + + JPanel viewComponent = wholeComponentPanel; + + // XXX Why is this list iterated in *reverse* order? + for (JToolBar curToolBar : new ReversedList<>(toolBarManager.getToolBars())) { + // A panel that wraps the toolbar. + final JPanel curToolBarPanel = new JPanel(); + curToolBarPanel.setLayout(new BoxLayout(curToolBarPanel, BoxLayout.LINE_AXIS)); + // A panel that wraps the (wrapped) toolbar and the previous component + // (the whole view and the nested/wrapped toolbars). + JPanel wrappingPanel = new JPanel(new BorderLayout()); + curToolBar.setBorder(new PaletteToolBarBorder()); + + curToolBarPanel.add(curToolBar); + wrappingPanel.add(curToolBarPanel, BorderLayout.NORTH); + wrappingPanel.add(viewComponent); + + lToolBars.add(curToolBar); + viewComponent = wrappingPanel; + } + + for (JToolBar bar : lToolBars) { + configureToolBarButtons(bar); + } + + return viewComponent; + } + + private void configureToolBarButtons(JToolBar bar) { + final Dimension dimButton = new Dimension(32, 34); + for (Component comp : bar.getComponents()) { + if (comp instanceof JButton || comp instanceof JToggleButton) { + JComponent tbButton = (JComponent) comp; + tbButton.setMaximumSize(dimButton); + tbButton.setPreferredSize(dimButton); + tbButton.setBorder(new EtchedBorder()); + } + } + } + + private void closeOpenedPluginPanels() { + for (PluggablePanelFactory factory : panelRegistry.getFactories()) { + showPluginPanel(factory, false); + } + } + + /** + * Initializes operating state. + */ + private void setOperatingState() { + appState.setOperationMode(OperationMode.OPERATING); + Runnable run = new Runnable() { + + @Override + public void run() { + // XXX The event should probably be emitted in ApplicationState now. + eventBus.onEvent( + new OperationModeChangeEvent( + this, + OperationMode.UNDEFINED, + OperationMode.OPERATING + ) + ); + } + }; + + if (SwingUtilities.isEventDispatchThread()) { + // Called from File -> Mode + SwingUtilities.invokeLater(run); + } + else { + try { + // Called from Main.connectKernel() + SwingUtilities.invokeAndWait(run); + } + catch (InterruptedException | InvocationTargetException ex) { + LOG.error("Unexpected exception ", ex); + } + } + + // Switch to selection tool. + eventBus.onEvent(new ResetInteractionToolCommand(this)); + } + + /** + * Removes the given model component from the given folder. + * + * @param folder The folder. + * @param model The component to be removed. + */ + private boolean removeModelComponent( + ModelComponent folder, + ModelComponent model + ) { + if (!folder.contains(model)) { + return false; + } + + // This method is being called by command objects that use undo/redo, so + // avoid calling commands via undo/redo here. + boolean componentRemoved = false; + + synchronized (model) { + if (!BlockModel.class.isInstance(folder)) { + // don't delete objects from a Blocks folder + synchronized (folder) { + folder.remove(model); + } + + model.removeAttributesChangeListener(attributesEventHandler); + componentRemoved = true; + } + + fPropertiesComponent.reset(); + + if (model instanceof BlockModel) { + BlockModel blockModel = (BlockModel) model; + // Remove Blocks from the Blocks tree + fBlocksTreeManager.removeItem(blockModel); + blockModel.blockRemoved(); + blockModel.removeBlockChangeListener(blockEventHandler); + } + else if (componentRemoved) { + fComponentsTreeManager.removeItem(model); + } + + if (model instanceof LocationTypeModel) { + for (LocationModel location : fModelManager.getModel().getLocationModels()) { + location.updateTypeProperty(fModelManager.getModel().getLocationTypeModels()); + } + } + + setHasUnsavedChanges(true); + } + + return componentRemoved; + } + + /** + * Returns the figure that belongs to the given model component. + * + * @param model The model component. + * @return The figure that belongs to the given model component, or + * null, if there isn't any. + */ + private Figure findFigure(ModelComponent model) { + return fModelManager.getModel().getFigure(model); + } + + private void setSystemModel(SystemModel systemModel) { + requireNonNull(systemModel, "systemModel"); + + long timeBefore = System.currentTimeMillis(); + + // Notify the view's scroll panes about the new systemModel and therefore about the new/changed + // origin. This way they can handle changes made to the origin's scale. + for (DrawingViewScrollPane scrollPane : viewManager.getDrawingViewMap().values()) { + scrollPane.originChanged(systemModel.getDrawingMethod().getOrigin()); + } + + fDrawingEditor.setSystemModel(systemModel); + + // --- Undo, Redo, Clipboard --- + Drawing drawing = fDrawingEditor.getDrawing(); + drawing.addUndoableEditListener(fUndoRedoManager); + + fComponentsTreeManager.restoreTreeView(systemModel); + fComponentsTreeManager.sortItems(); + fComponentsTreeManager.getTreeView().getTree().scrollRowToVisible(0); + fBlocksTreeManager.restoreTreeView(systemModel.getMainFolder(SystemModel.FolderKey.BLOCKS)); + fBlocksTreeManager.getTreeView().sortRoot(); + fBlocksTreeManager.getTreeView().getTree().scrollRowToVisible(0); + + // Add Attribute Change Listeners to all objects + for (VehicleModel vehicle : systemModel.getVehicleModels()) { + vehicle.addAttributesChangeListener(attributesEventHandler); + } + + systemModel.getLayoutModel().addAttributesChangeListener(attributesEventHandler); + + for (PointModel point : systemModel.getPointModels()) { + point.addAttributesChangeListener(attributesEventHandler); + } + + for (PathModel path : systemModel.getPathModels()) { + path.addAttributesChangeListener(attributesEventHandler); + } + + for (LocationTypeModel locationType : systemModel.getLocationTypeModels()) { + locationType.addAttributesChangeListener(attributesEventHandler); + } + + for (LocationModel location : systemModel.getLocationModels()) { + location.addAttributesChangeListener(attributesEventHandler); + } + + for (LinkModel link : systemModel.getLinkModels()) { + link.addAttributesChangeListener(attributesEventHandler); + } + + for (BlockModel block : systemModel.getBlockModels()) { + block.addAttributesChangeListener(attributesEventHandler); + block.addBlockChangeListener(blockEventHandler); + } + + LOG.debug("setSystemModel() took {} ms.", System.currentTimeMillis() - timeBefore); + } + + /** + * Initializes the frame with the toolbars and the dockable elements. + */ + private void initializeFrame() { + if (!SwingUtilities.isEventDispatchThread()) { + try { + SwingUtilities.invokeAndWait(() -> initializeFrame()); + } + catch (InterruptedException | InvocationTargetException e) { + LOG.warn("Exception initializing frame", e); + } + return; + } + + fFrame.getContentPane().removeAll(); + dockingManager.initializeDockables(); + // Frame + fFrame.setLayout(new BorderLayout()); + fFrame.add(wrapViewComponent(), BorderLayout.NORTH); + fFrame.add(dockingManager.getCControl().getContentArea()); + fFrame.add(statusPanel, BorderLayout.SOUTH); + restoreDockables(); + // Ensure that, after initialization, the selection tool is active. + // This needs to be done after the initial drawing views have been set + // up so they reflect the behaviour of the selected tool. + // XXX Maybe there is a better way to ensure this... + toolBarManager.getDragToolButton().doClick(); + toolBarManager.getSelectionToolButton().doClick(); + } + + private void restoreDockables() { + addDrawingView(); + + addTransportOrderView(); + addTransportOrderSequenceView(); + addPeripheralJobsView(); + addUserNotificationView(); + + dockingManager.getTabPane(DockingManagerOperating.COURSE_TAB_PANE_ID) + .getStation() + .setFrontDockable(viewManager.evaluateFrontDockable()); + } + + private class AttributesEventHandler + implements + AttributesChangeListener { + + /** + * Creates a new instance. + */ + AttributesEventHandler() { + } + + @Override // AttributesChangeListener + public void propertiesChanged(AttributesChangeEvent event) { + if (event.getInitiator() == this) { + return; + } + + ModelComponent model = event.getModel(); + + // If a model component's name changed, update the blocks this component is a member of + if (model.getPropertyName() != null && model.getPropertyName().hasChanged()) { + fComponentsTreeManager.itemChanged(model); + + fModelManager.getModel().getBlockModels().stream() + .filter(block -> blockAffectedByNameChange(block, model)) + .forEach(block -> updateBlockMembers(block)); + } + + if (model instanceof LayoutModel) { + // Handle scale changes. + LengthProperty pScaleX = (LengthProperty) model.getProperty(LayoutModel.SCALE_X); + LengthProperty pScaleY = (LengthProperty) model.getProperty(LayoutModel.SCALE_Y); + + if (pScaleX.hasChanged() || pScaleY.hasChanged()) { + double scaleX = (double) pScaleX.getValue(); + double scaleY = (double) pScaleY.getValue(); + + if (scaleX != 0.0 && scaleY != 0.0) { + fModelManager.getModel().getDrawingMethod().getOrigin().setScale(scaleX, scaleY); + } + } + } + + if (model instanceof LocationModel) { + if (model.getProperty(LocationModel.TYPE).hasChanged()) { + AbstractProperty p = (AbstractProperty) model.getProperty(LocationModel.TYPE); + LocationTypeModel type + = fModelManager.getModel().getLocationTypeModel((String) p.getValue()); + ((LocationModel) model).setLocationType(type); + if (model != event.getInitiator()) { + model.propertiesChanged(this); + } + } + } + + if (model instanceof LocationTypeModel) { + for (LocationModel locModel : fModelManager.getModel().getLocationModels()) { + locModel.updateTypeProperty(fModelManager.getModel().getLocationTypeModels()); + } + } + } + + private boolean blockAffectedByNameChange(BlockModel block, ModelComponent model) { + return block.getChildComponents().stream().anyMatch(member -> member.equals(model)); + } + + private void updateBlockMembers(BlockModel block) { + List members = new ArrayList<>(); + for (ModelComponent component : block.getChildComponents()) { + members.add(component.getName()); + } + block.getPropertyElements().setItems(members); + } + } + + /** + * Handles events emitted for changes of blocks. + */ + private class BlockEventHandler + implements + BlockChangeListener { + + /** + * Creates a new instance. + */ + BlockEventHandler() { + } + + @Override // BlockChangeListener + public void courseElementsChanged(BlockChangeEvent event) { + BlockModel block = (BlockModel) event.getSource(); + // Remove all children from the block and re-add those that are still there. + fBlocksTreeManager.removeChildren(block); + for (ModelComponent component : block.getChildComponents()) { + fBlocksTreeManager.addItem(block, component); + } + + setHasUnsavedChanges(true); + } + + @Override + public void colorChanged(BlockChangeEvent event) { + } + + @Override // BlockChangeListener + public void blockRemoved(BlockChangeEvent event) { + } + } + + /** + * Handles events emitted by the drawing editor. + */ + private class DrawingEditorEventHandler + implements + DrawingEditorListener { + + /** + * Provides access to the current system model. + */ + private final ModelManager modelManager; + + /** + * Creates a new instance. + * + * @param modelManager Provides access to the current system model. + */ + DrawingEditorEventHandler(ModelManager modelManager) { + this.modelManager = requireNonNull(modelManager, "modelManager"); + } + + @Override // DrawingEditorListener + public void figureAdded(DrawingEditorEvent event) { + Figure figure = event.getFigure(); + ModelComponent model = figure.get(FigureConstants.MODEL); + + // Some figures do not have a model - OriginFigure, for instance. + // XXX Check if we can't unify all figures to have a model. + if (model == null) { + return; + } + + if (figure instanceof AttributesChangeListener) { + model.addAttributesChangeListener((AttributesChangeListener) figure); + } + + // The added figure shall react on changes of the layout's scale. + if (figure instanceof OriginChangeListener) { + Origin ref = modelManager.getModel().getDrawingMethod().getOrigin(); + + if (ref != null) { + ref.addListener((OriginChangeListener) figure); + figure.set(FigureConstants.ORIGIN, ref); + } + } + + fModelManager.getModel().registerFigure(model, figure); + +// ModelComponent folder = modelManager.getModel().getFolder(model); +// addModelComponent(folder, model); + if (figure instanceof LabeledFigure) { + ((LabeledFigure) figure).updateModel(); + } + } + + @Override// DrawingEditorListener + public void figureRemoved(DrawingEditorEvent e) { + Figure figure = e.getFigure(); + + if (figure == null) { + return; + } + + ModelComponent model = figure.get(FigureConstants.MODEL); + + if (model == null) { + return; + } + + synchronized (model) { + // The removed figure shouldn't react on changes of the origin any more. + if (figure instanceof OriginChangeListener) { + Origin ref = figure.get(FigureConstants.ORIGIN); + + if (ref != null) { + ref.removeListener((OriginChangeListener) figure); + figure.set(FigureConstants.ORIGIN, null); + } + } + // Disassociate from blocks... + removeFromAllBlocks(model); + // ...and remove the object itself. + ModelComponent folder = modelManager.getModel().getFolder(model); + synchronized (folder) { + removeModelComponent(folder, model); + } + } + } + + @Override + public void figureSelected(DrawingEditorEvent event) { + if (event.getCount() == 0) { + fComponentsTreeManager.selectItems(null); + fBlocksTreeManager.selectItems(null); + } + else if (event.getCount() == 1) { + // Single figure selected. + Figure figure = event.getFigure(); + + if (figure != null) { + ModelComponent model = figure.get(FigureConstants.MODEL); + + if (model != null) { + model.addAttributesChangeListener(attributesEventHandler); + fPropertiesComponent.setModel(model); + fComponentsTreeManager.selectItem(model); + fBlocksTreeManager.selectItem(model); + } + } + } + else { + // Multiple figures selected. + List models = new ArrayList<>(); + Set components = new HashSet<>(); + + for (Figure figure : event.getFigures()) { + ModelComponent model = figure.get(FigureConstants.MODEL); + if (model != null) { + models.add(model); + components.add(model); + } + } + + // Display shared properties of the selected figures. + ModelComponent model = new PropertiesCollection(models); + fComponentsTreeManager.selectItems(components); + fBlocksTreeManager.selectItems(components); + fPropertiesComponent.setModel(model); + } + } + + /** + * Removes a component from all blocks in the model. + * + * @param model The component to be removed. + */ + private void removeFromAllBlocks(ModelComponent model) { + // The (invisible?) root folder of the "Blocks" tree... + ModelComponent mainFolder + = modelManager.getModel().getMainFolder(SystemModel.FolderKey.BLOCKS); + + synchronized (mainFolder) { + // ... contains one folder for each Block + + for (ModelComponent blockModelComp : mainFolder.getChildComponents()) { + BlockModel block = (BlockModel) blockModelComp; + + List elementsToRemove = new ArrayList<>(); + // All child components (Points, Paths) of one Block + for (ModelComponent blockChildComp : block.getChildComponents()) { + if (model == blockChildComp) { + elementsToRemove.add(blockChildComp); + } + } + + if (!elementsToRemove.isEmpty()) { + // At least one component found + for (ModelComponent mc : elementsToRemove) { + block.removeCourseElement(mc); + } + + block.courseElementsChanged(); + } + } + } + } + } + + /** + * MouseListener for vehicle dragging events in the tree view. + */ + private class VehicleDragHandler + extends + MouseAdapter { + + /** + * The cursor to be used when a vehicle is dragged. + */ + private final Cursor dragCursor; + /** + * The currently selected/dragged vehicle model. + */ + private VehicleModel vehicleModel; + + /** + * Creates a new instance. + * + * @param dragCursor The cursor to be used when a vehicle is dragged. + */ + VehicleDragHandler(Cursor dragCursor) { + this.dragCursor = requireNonNull(dragCursor, "dragCursor"); + } + + @Override + public void mousePressed(MouseEvent e) { + UserObject object = fComponentsTreeManager.getDraggedUserObject(e); + + if (object instanceof VehicleUserObject) { + vehicleModel = ((VehicleUserObject) object).getModelComponent(); + } + else { + vehicleModel = null; + } + } + + @Override + public void mouseDragged(MouseEvent e) { + if (vehicleModel == null) { + return; + } + Point eOnScreen = e.getLocationOnScreen(); + for (OpenTCSDrawingView drawView : getDrawingViews()) { + if (drawView.getComponent().isShowing()) { + if (drawView.containsPointOnScreen(eOnScreen)) { + drawView.setCursor(dragCursor); + } + } + } + + fComponentsTreeManager.setCursor(dragCursor); + setCursor(dragCursor); + } + + @Override + public void mouseReleased(MouseEvent event) { + // Reset cursors to the default ones. + fComponentsTreeManager.setCursor(Cursor.getDefaultCursor()); + setCursor(Cursor.getDefaultCursor()); + vehicleModel = null; + + if (vehicleModel != null + && (Vehicle.ProcState) vehicleModel.getPropertyProcState() + .getValue() == Vehicle.ProcState.IDLE) { + createOrderToPointOnScreen(event.getLocationOnScreen()); + } + } + + // Class-specific methods start here. + private void createOrderToPointOnScreen(Point locationOnScreen) { + for (OpenTCSDrawingView drawView : getDrawingViews()) { + drawView.setCursor(Cursor.getDefaultCursor()); + + if (drawView.getComponent().isShowing() + && drawView.containsPointOnScreen(locationOnScreen)) { + Figure figure = getFigureAtPointInView(locationOnScreen, drawView); + if (figure instanceof LabeledPointFigure) { + createOrderToPointFigure((LabeledPointFigure) figure); + } + } + } + } + + private Figure getFigureAtPointInView( + Point locationOnScreen, + OpenTCSDrawingView drawView + ) { + Point drawingViewOnScreen = drawView.getComponent().getLocationOnScreen(); + Point drawingViewPoint + = new Point( + locationOnScreen.x - drawingViewOnScreen.x, + locationOnScreen.y - drawingViewOnScreen.y + ); + return drawView.findFigure(drawingViewPoint); + } + + private void createOrderToPointFigure(LabeledPointFigure figure) { + PointModel model = (PointModel) figure.get(FigureConstants.MODEL); + orderUtil.createTransportOrder(model, vehicleModel); + } + } + + private class DrawingViewClosingListener + implements + CVetoClosingListener { + + private final DefaultSingleCDockable newDockable; + + DrawingViewClosingListener(DefaultSingleCDockable newDockable) { + this.newDockable = newDockable; + } + + @Override + public void closing(CVetoClosingEvent event) { + } + + @Override + public void closed(CVetoClosingEvent event) { + // A dockable is closeable by default. It isn't closeable + // when switching kernel states and we want to hide additional views + if (newDockable.isCloseable()) { + removeDrawingView(newDockable); + } + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/OperationsDeskApplication.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/OperationsDeskApplication.java new file mode 100644 index 0000000..dab52c9 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/OperationsDeskApplication.java @@ -0,0 +1,176 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import org.opentcs.common.ClientConnectionMode; +import org.opentcs.common.KernelClientApplication; +import org.opentcs.common.PortalManager; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.operationsdesk.util.OperationsDeskConfiguration; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The application for kernel connections. + */ +public class OperationsDeskApplication + implements + KernelClientApplication, + EventHandler { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory + .getLogger(OperationsDeskApplication.class); + /** + * The portal manager. + */ + private final PortalManager portalManager; + /** + * The application's event bus. + */ + private final EventBus eventBus; + /** + * The application's configuration. + */ + private final OperationsDeskConfiguration configuration; + /** + * Whether this application is online or not. + */ + private ConnectionState connectionState = ConnectionState.OFFLINE; + /** + * Whether this application is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param portalManager The service portal manager. + * @param eventBus The application's event bus. + * @param configuration The application's configuration. + */ + @Inject + @SuppressWarnings("this-escape") + public OperationsDeskApplication( + @Nonnull + PortalManager portalManager, + @ApplicationEventBus + EventBus eventBus, + OperationsDeskConfiguration configuration + ) { + this.portalManager = requireNonNull(portalManager, "portalManager"); + this.eventBus = requireNonNull(eventBus, "eventBus"); + this.configuration = requireNonNull(configuration, "configuration"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + eventBus.subscribe(this); + + online(configuration.useBookmarksWhenConnecting()); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + eventBus.unsubscribe(this); + // If we want to terminate but are still online, go offline first + offline(); + + initialized = false; + } + + @Override + public void onEvent(Object event) { + } + + @Override + public void online(boolean autoConnect) { + + if (isOnline() || isConnecting()) { + return; + } + + connectionState = ConnectionState.CONNECTING; + if (portalManager.connect(toConnectionMode(autoConnect))) { + LOG.info("Switching application state to online..."); + connectionState = ConnectionState.ONLINE; + eventBus.onEvent(ClientConnectionMode.ONLINE); + } + else { + connectionState = ConnectionState.OFFLINE; + } + } + + @Override + public void offline() { + if (!isOnline() && !isConnecting()) { + return; + } + + portalManager.disconnect(); + + LOG.info("Switching application state to offline..."); + connectionState = ConnectionState.OFFLINE; + eventBus.onEvent(ClientConnectionMode.OFFLINE); + } + + @Override + public boolean isOnline() { + return connectionState == ConnectionState.ONLINE; + } + + /** + * Returns true if, and only if the operations desk is trying to establish a + * connection to the kernel. + * + * @return true if, and only if the operations desk is trying to establish a + * connection to the kernel + */ + public boolean isConnecting() { + return connectionState == ConnectionState.CONNECTING; + } + + private PortalManager.ConnectionMode toConnectionMode(boolean autoConnect) { + return autoConnect ? PortalManager.ConnectionMode.AUTO : PortalManager.ConnectionMode.MANUAL; + } + + /** + * An enum to display the different states of the operations desk application connection to the + * kernel. + */ + private enum ConnectionState { + /** + * The operations desk is not connected to the kernel and is not trying to. + */ + OFFLINE, + /** + * The operations desk is currently trying to connect to the kernel. + */ + CONNECTING, + /** + * The operations desk is connected to the kernel. + */ + ONLINE + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/PlantOverviewStarter.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/PlantOverviewStarter.java new file mode 100644 index 0000000..d763c12 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/PlantOverviewStarter.java @@ -0,0 +1,162 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.jhotdraw.app.Application; +import org.opentcs.common.KernelClientApplication; +import org.opentcs.guing.common.application.ProgressIndicator; +import org.opentcs.guing.common.application.StartupProgressStatus; +import org.opentcs.guing.common.event.EventLogger; +import org.opentcs.operationsdesk.exchange.AttributeAdapterRegistry; +import org.opentcs.operationsdesk.exchange.KernelEventFetcher; +import org.opentcs.operationsdesk.exchange.OpenTCSEventDispatcher; +import org.opentcs.operationsdesk.notifications.UserNotificationsContainer; +import org.opentcs.operationsdesk.peripherals.jobs.PeripheralJobsContainer; +import org.opentcs.operationsdesk.transport.orders.TransportOrdersContainer; +import org.opentcs.operationsdesk.transport.sequences.OrderSequencesContainer; + +/** + * The plant overview application's entry point. + */ +public class PlantOverviewStarter { + + /** + * Our startup progress indicator. + */ + private final ProgressIndicator progressIndicator; + /** + * The enclosing application. + */ + private final Application application; + /** + * The actual document view. + */ + private final OpenTCSView opentcsView; + /** + * + */ + private final EventLogger eventLogger; + /** + * Fetches events from the kernel, if connected, and publishes them via the local event bus. + */ + private final KernelEventFetcher kernelEventFetcher; + /** + * Dispatches openTCS event from kernel objects to corresponding model components. + */ + private final OpenTCSEventDispatcher eventDispatcher; + /** + * Responsible for connections to a kernel. + */ + private final KernelClientApplication kernelClientApplication; + /** + * Handles registering of model attribute adapters. + */ + private final AttributeAdapterRegistry attributeAdapterRegistry; + /** + * Maintains a set of all transport orders existing on the kernel side. + */ + private final TransportOrdersContainer transportOrdersContainer; + /** + * Maintains a list of the most recent user notifications. + */ + private final UserNotificationsContainer userNotificationsContainer; + /** + * Maintains a set of all peripheral jobs existing on the kernel side. + */ + private final PeripheralJobsContainer peripheralJobsContainer; + /** + * Maintains a set of all order sequences existing on the kernel side. + */ + private final OrderSequencesContainer orderSequencesContainer; + + /** + * Creates a new instance. + * + * @param progressIndicator The progress indicator to be used. + * @param application The application to be used. + * @param opentcsView The view to be used. + * @param eventLogger The event logger. + * @param kernelEventFetcher Fetches events from the kernel, if connected, and publishes them via + * the local event bus. + * @param eventDispatcher Dispatches openTCS event from kernel objects to corresponding model + * components. + * @param kernelClientApplication Responsible for connections to a kernel. + * @param attributeAdapterRegistry Handles registering of model attribute adapters. + * @param transportOrdersContainer Maintains a set of all transport orders existing on the kernel + * side. + * @param peripheralJobsContainer Maintains a set of all peripheral jobs existing on the kernel + * side. + * @param orderSequencesContainer Maintains a set of all peripheral jobs existing on the kernel + * side. + * @param userNotificationsContainer Maintains a list of the most recent user notifications. + */ + @Inject + public PlantOverviewStarter( + ProgressIndicator progressIndicator, + Application application, + OpenTCSView opentcsView, + EventLogger eventLogger, + KernelEventFetcher kernelEventFetcher, + OpenTCSEventDispatcher eventDispatcher, + KernelClientApplication kernelClientApplication, + AttributeAdapterRegistry attributeAdapterRegistry, + TransportOrdersContainer transportOrdersContainer, + PeripheralJobsContainer peripheralJobsContainer, + OrderSequencesContainer orderSequencesContainer, + UserNotificationsContainer userNotificationsContainer + ) { + this.progressIndicator = requireNonNull(progressIndicator, "progressIndicator"); + this.application = requireNonNull(application, "application"); + this.opentcsView = requireNonNull(opentcsView, "opentcsView"); + this.eventLogger = requireNonNull(eventLogger, "eventLogger"); + this.kernelEventFetcher = requireNonNull(kernelEventFetcher, "kernelEventFetcher"); + this.eventDispatcher = requireNonNull(eventDispatcher, "eventDispatcher"); + this.kernelClientApplication = requireNonNull( + kernelClientApplication, "kernelClientApplication" + ); + this.attributeAdapterRegistry = requireNonNull( + attributeAdapterRegistry, + "attributeAdapterRegistry" + ); + this.transportOrdersContainer = requireNonNull( + transportOrdersContainer, + "transportOrdersContainer" + ); + this.peripheralJobsContainer = requireNonNull( + peripheralJobsContainer, + "peripheralJobsContainer" + ); + this.orderSequencesContainer = requireNonNull( + orderSequencesContainer, + "orderSequencesContainer" + ); + this.userNotificationsContainer = requireNonNull( + userNotificationsContainer, + "userNotificationsContainer" + ); + } + + public void startPlantOverview() { + eventLogger.initialize(); + kernelEventFetcher.initialize(); + eventDispatcher.initialize(); + attributeAdapterRegistry.initialize(); + transportOrdersContainer.initialize(); + peripheralJobsContainer.initialize(); + orderSequencesContainer.initialize(); + userNotificationsContainer.initialize(); + + opentcsView.init(); + progressIndicator.initialize(); + progressIndicator.setProgress(StartupProgressStatus.START_PLANT_OVERVIEW); + progressIndicator.setProgress(StartupProgressStatus.SHOW_PLANT_OVERVIEW); + opentcsView.setApplication(application); + // Start the view. + application.show(opentcsView); + kernelClientApplication.initialize(); + progressIndicator.terminate(); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/ViewManagerOperating.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/ViewManagerOperating.java new file mode 100644 index 0000000..b885864 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/ViewManagerOperating.java @@ -0,0 +1,353 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application; + +import static java.util.Objects.requireNonNull; + +import bibliothek.gui.Dockable; +import bibliothek.gui.dock.common.DefaultSingleCDockable; +import bibliothek.gui.dock.common.intern.DefaultCommonDockable; +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.guing.common.application.AbstractViewManager; +import org.opentcs.guing.common.components.dockable.CStackDockStation; +import org.opentcs.guing.common.components.dockable.DockableTitleComparator; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingView; +import org.opentcs.operationsdesk.components.dockable.DockingManagerOperating; +import org.opentcs.operationsdesk.notifications.UserNotificationsContainerPanel; +import org.opentcs.operationsdesk.peripherals.jobs.PeripheralJobsContainerPanel; +import org.opentcs.operationsdesk.transport.orders.TransportOrdersContainerPanel; +import org.opentcs.operationsdesk.transport.sequences.OrderSequencesContainerPanel; +import org.opentcs.util.event.EventSource; + +/** + * Manages the mapping of dockables to drawing views, transport order views and + * order sequence views. + */ +public class ViewManagerOperating + extends + AbstractViewManager { + + /** + * Manages the application's docking frames. + */ + private final DockingManagerOperating dockingManager; + /** + * Where we register event listeners. + */ + private final EventSource eventSource; + /** + * Map for user notification dockable -> user notification container panel. + */ + private final Map userNotificationViews; + /** + * Map for transport order dockable -> transport order container panel. + */ + private final Map transportOrderViews; + /** + * Map for order sequences dockable -> order sequences container panel. + */ + private final Map orderSequenceViews; + /** + * Map for peripheral job dockable -> peripheral job container panel. + */ + private final Map peripheralJobViews; + + /** + * Creates a new instance. + * + * @param dockingManager Manages the application's docking frames. + * @param eventSource Where this instance registers event listeners. + */ + @Inject + public ViewManagerOperating( + DockingManagerOperating dockingManager, + @ApplicationEventBus + EventSource eventSource + ) { + super(eventSource); + this.dockingManager = requireNonNull(dockingManager, "dockingManager"); + this.eventSource = requireNonNull(eventSource, "eventSource"); + userNotificationViews = new TreeMap<>(new DockableTitleComparator()); + transportOrderViews = new TreeMap<>(new DockableTitleComparator()); + orderSequenceViews = new TreeMap<>(new DockableTitleComparator()); + peripheralJobViews = new TreeMap<>(new DockableTitleComparator()); + } + + public void init() { + setPlantOverviewStateOperating(); + } + + /** + * Resets all components. + */ + public void reset() { + super.reset(); + userNotificationViews.clear(); + transportOrderViews.clear(); + orderSequenceViews.clear(); + peripheralJobViews.clear(); + } + + public Map getUserNotificationMap() { + return userNotificationViews; + } + + public Map getTransportOrderMap() { + return transportOrderViews; + } + + public Map getOrderSequenceMap() { + return orderSequenceViews; + } + + public Map getPeripheralJobMap() { + return peripheralJobViews; + } + + /** + * Returns all drawing views (excluding the modelling view) + * + * @return List with all known OpenTCSDrawingViews, but not + * the modelling view. + */ + public List getOperatingDrawingViews() { + return getDrawingViewMap().entrySet().stream() + .map(entry -> entry.getValue().getDrawingView()) + .collect(Collectors.toList()); + } + + public int getNextUserNotificationViewIndex() { + return nextAvailableIndex(userNotificationViews.keySet()); + } + + public int getNextTransportOrderViewIndex() { + return nextAvailableIndex(transportOrderViews.keySet()); + } + + public int getNextOrderSequenceViewIndex() { + return nextAvailableIndex(orderSequenceViews.keySet()); + } + + public int getNextPeripheralJobViewIndex() { + return nextAvailableIndex(peripheralJobViews.keySet()); + } + + public DefaultSingleCDockable getLastUserNotificationView() { + int biggestIndex = getNextUserNotificationViewIndex(); + DefaultSingleCDockable lastUNView = null; + Iterator userNotificationViewIterator + = userNotificationViews.keySet().iterator(); + for (int i = 0; i < biggestIndex; i++) { + if (userNotificationViewIterator.hasNext()) { + lastUNView = userNotificationViewIterator.next(); + } + } + + return lastUNView; + } + + public DefaultSingleCDockable getLastTransportOrderView() { + int biggestIndex = getNextTransportOrderViewIndex(); + DefaultSingleCDockable lastTOView = null; + Iterator tranportOrderViewIterator + = transportOrderViews.keySet().iterator(); + for (int i = 0; i < biggestIndex; i++) { + if (tranportOrderViewIterator.hasNext()) { + lastTOView = tranportOrderViewIterator.next(); + } + } + + return lastTOView; + } + + public DefaultSingleCDockable getLastOrderSequenceView() { + int biggestIndex = getNextOrderSequenceViewIndex(); + DefaultSingleCDockable lastOSView = null; + Iterator orderSequencesViewIterator + = orderSequenceViews.keySet().iterator(); + for (int i = 0; i < biggestIndex; i++) { + if (orderSequencesViewIterator.hasNext()) { + lastOSView = orderSequencesViewIterator.next(); + } + } + + return lastOSView; + } + + public DefaultSingleCDockable getLastPeripheralJobView() { + int biggestIndex = getNextPeripheralJobViewIndex(); + DefaultSingleCDockable lastView = null; + Iterator peripheralJobViewIterator + = peripheralJobViews.keySet().iterator(); + for (int i = 0; i < biggestIndex; i++) { + if (peripheralJobViewIterator.hasNext()) { + lastView = peripheralJobViewIterator.next(); + } + } + + return lastView; + } + + /** + * Puts a UserNotificationsContainerPanel with a key dockable + * into the user notification view map. + * + * @param dockable The dockable the panel is wrapped into. Used as the key. + * @param panel The panel. + */ + public void addUserNotificationView( + DefaultSingleCDockable dockable, + UserNotificationsContainerPanel panel + ) { + requireNonNull(dockable, "dockable"); + requireNonNull(panel, "panel"); + + userNotificationViews.put(dockable, panel); + } + + /** + * Puts a TransportOrdersContainerPanel with a key dockable + * into the transport order view map. + * + * @param dockable The dockable the panel is wrapped into. Used as the key. + * @param panel The panel. + */ + public void addTransportOrderView( + DefaultSingleCDockable dockable, + TransportOrdersContainerPanel panel + ) { + requireNonNull(dockable, "dockable"); + requireNonNull(panel, "panel"); + + transportOrderViews.put(dockable, panel); + } + + /** + * Puts a OrderSequencesContainerPanel with a key dockable + * into the order sequence view map. + * + * @param dockable The dockable the panel is wrapped into. Used as the key. + * @param panel The panel. + */ + public void addOrderSequenceView( + DefaultSingleCDockable dockable, + OrderSequencesContainerPanel panel + ) { + requireNonNull(dockable, "dockable"); + requireNonNull(panel, "panel"); + + orderSequenceViews.put(dockable, panel); + } + + /** + * Puts a OrderSequencesContainerPanel with a key dockable + * into the order sequence view map. + * + * @param dockable The dockable the panel is wrapped into. Used as the key. + * @param panel The panel. + */ + public void addPeripheralJobView( + DefaultSingleCDockable dockable, + PeripheralJobsContainerPanel panel + ) { + requireNonNull(dockable, "dockable"); + requireNonNull(panel, "panel"); + + peripheralJobViews.put(dockable, panel); + } + + /** + * Forgets the given dockable. + * + * @param dockable The dockable. + */ + @Override + public void removeDockable(DefaultSingleCDockable dockable) { + super.removeDockable(dockable); + + transportOrderViews.remove(dockable); + orderSequenceViews.remove(dockable); + peripheralJobViews.remove(dockable); + } + + /** + * Evaluates which dockable should be the front dockable. + * + * @return The dockable that should be the front dockable. null + * if no dockables exist. + */ + @Override + public DefaultCommonDockable evaluateFrontDockable() { + if (!getDrawingViewMap().isEmpty()) { + return getDrawingViewMap().keySet().iterator().next().intern(); + } + if (!transportOrderViews.isEmpty()) { + return transportOrderViews.keySet().iterator().next().intern(); + } + if (!orderSequenceViews.isEmpty()) { + return orderSequenceViews.keySet().iterator().next().intern(); + } + if (!peripheralJobViews.isEmpty()) { + return peripheralJobViews.keySet().iterator().next().intern(); + } + return null; + } + + /** + * Sets visibility states of all dockables to operating. + */ + private void setPlantOverviewStateOperating() { + CStackDockStation station + = dockingManager.getTabPane(DockingManagerOperating.COURSE_TAB_PANE_ID).getStation(); + Dockable frontDock = station.getFrontDockable(); + int i = 0; + for (DefaultSingleCDockable dock : new ArrayList<>(getDrawingViewMap().keySet())) { + // Restore to default + dock.setCloseable(true); + dockingManager.showDockable(station, dock, i); + i++; + } + i = getDrawingViewMap().size(); + for (DefaultSingleCDockable dock : new ArrayList<>(getDrawingViewMap().keySet())) { + // OpenTCSDrawingViews can be undocked when switching states, so + // we make sure they aren't counted as docked + if (dockingManager.isDockableDocked(station, dock)) { + i--; + } + } + + int dockedDrawingViews = i; + + for (DefaultSingleCDockable dock : new ArrayList<>(transportOrderViews.keySet())) { + dockingManager.showDockable(station, dock, i); + i++; + } + + i = dockedDrawingViews + transportOrderViews.size(); + + for (DefaultSingleCDockable dock : new ArrayList<>(orderSequenceViews.keySet())) { + dockingManager.showDockable(station, dock, i); + i++; + } + + for (DefaultSingleCDockable dock : new ArrayList<>(peripheralJobViews.keySet())) { + dockingManager.showDockable(station, dock, i); + i++; + } + + if (frontDock != null && frontDock.isDockableShowing()) { + station.setFrontDockable(frontDock); + } + else { + station.setFrontDockable(station.getDockable(0)); + } + dockingManager.setDockableVisibility(DockingManagerOperating.VEHICLES_DOCKABLE_ID, true); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/ActionFactory.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/ActionFactory.java new file mode 100644 index 0000000..9da0891 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/ActionFactory.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action; + +import java.util.Collection; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.operationsdesk.application.action.course.FollowVehicleAction; +import org.opentcs.operationsdesk.application.action.course.IntegrationLevelChangeAction; +import org.opentcs.operationsdesk.application.action.course.PauseAction; +import org.opentcs.operationsdesk.application.action.course.RerouteAction; +import org.opentcs.operationsdesk.application.action.course.ScrollToVehicleAction; +import org.opentcs.operationsdesk.application.action.course.SendVehicleToLocationAction; +import org.opentcs.operationsdesk.application.action.course.SendVehicleToPointAction; +import org.opentcs.operationsdesk.application.action.course.WithdrawAction; + +/** + * A factory for various actions. + */ +public interface ActionFactory { + + ScrollToVehicleAction createScrollToVehicleAction(VehicleModel vehicleModel); + + FollowVehicleAction createFollowVehicleAction(VehicleModel vehicleModel); + + SendVehicleToPointAction createSendVehicleToPointAction(VehicleModel vehicleModel); + + SendVehicleToLocationAction createSendVehicleToLocationAction(VehicleModel vehicleModel); + + WithdrawAction createWithdrawAction(Collection vehicles, boolean immediateAbort); + + IntegrationLevelChangeAction createIntegrationLevelChangeAction( + Collection vehicles, + Vehicle.IntegrationLevel level + ); + + PauseAction createPauseAction(Collection vehicles, boolean pause); + + RerouteAction createRerouteAction( + Collection vehicles, + ReroutingType reroutingType + ); +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/ToolBarManager.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/ToolBarManager.java new file mode 100644 index 0000000..5e416fe --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/ToolBarManager.java @@ -0,0 +1,305 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.operationsdesk.event.KernelStateChangeEvent.State.LOGGED_IN; + +import jakarta.inject.Inject; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.swing.Action; +import javax.swing.ButtonGroup; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JToggleButton; +import javax.swing.JToolBar; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.event.ToolAdapter; +import org.jhotdraw.draw.event.ToolEvent; +import org.jhotdraw.draw.event.ToolListener; +import org.opentcs.guing.common.application.action.ToolButtonListener; +import org.opentcs.guing.common.application.toolbar.DragTool; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; +import org.opentcs.guing.common.event.ResetInteractionToolCommand; +import org.opentcs.guing.common.util.CourseObjectFactory; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.operationsdesk.application.action.actions.CreateTransportOrderAction; +import org.opentcs.operationsdesk.application.action.actions.FindVehicleAction; +import org.opentcs.operationsdesk.application.action.actions.PauseAllVehiclesAction; +import org.opentcs.operationsdesk.application.action.actions.ResumeAllVehiclesAction; +import org.opentcs.operationsdesk.application.toolbar.MultipleSelectionTool; +import org.opentcs.operationsdesk.application.toolbar.SelectionToolFactory; +import org.opentcs.operationsdesk.event.KernelStateChangeEvent; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.draw.SelectSameAction; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.util.event.EventHandler; + +/** + * Sets up and manages a list of tool bars in the graphical user interface. + */ +public class ToolBarManager + implements + EventHandler { + + /** + * A factory for selectiont tools. + */ + private final SelectionToolFactory selectionToolFactory; + /** + * A list of all toolbars. + */ + private final List toolBarList = Collections.synchronizedList(new ArrayList<>()); + /** + * A tool bar for actions creating new items. + */ + private final JToolBar toolBarCreation = new JToolBar(); + /** + * A toggle button for the selection tool. + */ + private final JToggleButton selectionToolButton; + /** + * A toggle button for the drag tool. + */ + private final JToggleButton dragToolButton; + /** + * The actual drag tool. + */ + private DragTool dragTool; + /** + * A button for creating transport orders. + * Available in operating mode. + */ + private final JButton buttonCreateOrder; + /** + * A button for finding vehicles. + * Available in operating mode. + */ + private final JButton buttonFindVehicle; + /** + * A button for pausing all vehicles. + * Available in operating mode. + */ + private final JButton buttonPauseAllVehicles; + /** + * A button for resuming all vehicles. + * Available in operating mode. + */ + private final JButton buttonResumeAllVehicles; + + /** + * Creates a new instance. + * + * @param actionMap The action map to be used + * @param crsObjFactory A factory for course objects + * @param editor The drawing editor + * @param selectionToolFactory The selection tool factory + */ + @Inject + public ToolBarManager( + ViewActionMap actionMap, + CourseObjectFactory crsObjFactory, + OpenTCSDrawingEditor editor, + SelectionToolFactory selectionToolFactory + ) { + requireNonNull(actionMap, "actionMap"); + requireNonNull(crsObjFactory, "crsObjFactory"); + requireNonNull(editor, "editor"); + this.selectionToolFactory = requireNonNull( + selectionToolFactory, + "selectionToolFactory" + ); + + ResourceBundleUtil labels + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TOOLBAR_PATH); + + // --- 1. ToolBar: Creation --- + // Selection, Drag | Create Point, Location, Path, Link | + // Create Location Type, Vehicle, Block, Static Route | + // Create Transport Order | Find, Show Vehicles + toolBarCreation.setActionMap(actionMap); + // --- Selection Tool --- + selectionToolButton = addSelectionToolButton(toolBarCreation, editor); + // --- Drag Tool --- + dragToolButton = addDragToolButton(toolBarCreation, editor); + + toolBarCreation.addSeparator(); + + // --- Create Transport Order (only in Operating mode) --- + buttonCreateOrder = new JButton(actionMap.get(CreateTransportOrderAction.ID)); + buttonCreateOrder.setText(null); + buttonCreateOrder.setEnabled(false); + toolBarCreation.add(buttonCreateOrder); + + toolBarCreation.addSeparator(); + + // --- Find Vehicle (only in Operating mode) --- + buttonFindVehicle = new JButton(actionMap.get(FindVehicleAction.ID)); + buttonFindVehicle.setText(null); + buttonFindVehicle.setEnabled(false); + toolBarCreation.add(buttonFindVehicle); + + toolBarCreation.addSeparator(); + + // --- Pause All Vehicles (only in Operating mode) --- + buttonPauseAllVehicles = new JButton(actionMap.get(PauseAllVehiclesAction.ID)); + buttonPauseAllVehicles.setText(null); + buttonPauseAllVehicles.setEnabled(false); + toolBarCreation.add(buttonPauseAllVehicles); + + // --- Resume All Vehicles (only in Operating mode) --- + buttonResumeAllVehicles = new JButton(actionMap.get(ResumeAllVehiclesAction.ID)); + buttonResumeAllVehicles.setText(null); + buttonResumeAllVehicles.setEnabled(false); + toolBarCreation.add(buttonResumeAllVehicles); + + toolBarCreation.setName(labels.getString("toolBarManager.toolbar_drawing.title")); + toolBarList.add(toolBarCreation); + } + + public List getToolBars() { + return toolBarList; + } + + public JToolBar getToolBarCreation() { + return toolBarCreation; + } + + public JToggleButton getSelectionToolButton() { + return selectionToolButton; + } + + public JToggleButton getDragToolButton() { + return dragToolButton; + } + + @Override + public void onEvent(Object event) { + if (event instanceof ResetInteractionToolCommand resetInteractionToolCommand) { + handleToolReset(resetInteractionToolCommand); + } + else if (event instanceof KernelStateChangeEvent kernelStateChangeEvent) { + handleKernelStateChangeEvent(kernelStateChangeEvent); + } + } + + private void handleToolReset(ResetInteractionToolCommand evt) { + selectionToolButton.setSelected(true); + } + + private void handleKernelStateChangeEvent(KernelStateChangeEvent event) { + switch (event.getNewState()) { + case LOGGED_IN: + buttonCreateOrder.setEnabled(true); + buttonFindVehicle.setEnabled(true); + buttonPauseAllVehicles.setEnabled(true); + buttonResumeAllVehicles.setEnabled(true); + break; + case DISCONNECTED: + buttonCreateOrder.setEnabled(false); + buttonFindVehicle.setEnabled(false); + buttonPauseAllVehicles.setEnabled(false); + buttonResumeAllVehicles.setEnabled(false); + break; + default: + // Do nothing. + } + } + + /** + * Adds the selection tool to the given toolbar. + * + * @param toolBar The toolbar to add to. + * @param editor The DrawingEditor. + */ + private JToggleButton addSelectionToolButton( + JToolBar toolBar, + DrawingEditor editor + ) { + List drawingActions = new ArrayList<>(); + // Drawing Actions + drawingActions.add(new SelectSameAction(editor)); + + MultipleSelectionTool selectionTool + = selectionToolFactory.createMultipleSelectionTool(drawingActions, new ArrayList<>()); + + ButtonGroup buttonGroup; + + if (toolBar.getClientProperty("toolButtonGroup") instanceof ButtonGroup) { + buttonGroup = (ButtonGroup) toolBar.getClientProperty("toolButtonGroup"); + } + else { + buttonGroup = new ButtonGroup(); + toolBar.putClientProperty("toolButtonGroup", buttonGroup); + } + + // Selection tool + editor.setTool(selectionTool); + final JToggleButton toggleButton = new JToggleButton(); + + if (!(toolBar.getClientProperty("toolHandler") instanceof ToolListener)) { + ToolListener toolHandler = new ToolAdapter() { + @Override + public void toolDone(ToolEvent event) { + toggleButton.setSelected(true); + } + }; + + toolBar.putClientProperty("toolHandler", toolHandler); + } + + toggleButton.setIcon(ImageDirectory.getImageIcon("/toolbar/select-2.png")); + toggleButton.setText(null); + toggleButton.setToolTipText( + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TOOLBAR_PATH) + .getString("toolBarManager.button_selectionTool.tooltipText") + ); + + toggleButton.setSelected(true); + toggleButton.addItemListener(new ToolButtonListener(selectionTool, editor)); + buttonGroup.add(toggleButton); + toolBar.add(toggleButton); + + return toggleButton; + } + + /** + * + * @param toolBar + * @param editor + */ + private JToggleButton addDragToolButton(JToolBar toolBar, DrawingEditor editor) { + final JToggleButton button = new JToggleButton(); + dragTool = new DragTool(); + editor.setTool(dragTool); + + if (!(toolBar.getClientProperty("toolHandler") instanceof ToolListener)) { + ToolListener toolHandler = new ToolAdapter() { + @Override + public void toolDone(ToolEvent event) { + button.setSelected(true); + } + }; + toolBar.putClientProperty("toolHandler", toolHandler); + } + + URL url = getClass().getResource(ImageDirectory.DIR + "/toolbar/cursor-opened-hand.png"); + button.setIcon(new ImageIcon(url)); + button.setText(null); + button.setToolTipText( + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TOOLBAR_PATH) + .getString("toolBarManager.button_dragTool.tooltipText") + ); + + button.setSelected(false); + button.addItemListener(new ToolButtonListener(dragTool, editor)); + + ButtonGroup group = (ButtonGroup) toolBar.getClientProperty("toolButtonGroup"); + group.add(button); + toolBar.add(button); + return button; + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/ViewActionMap.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/ViewActionMap.java new file mode 100644 index 0000000..9e98f84 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/ViewActionMap.java @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import javax.swing.ActionMap; +import org.opentcs.common.KernelClientApplication; +import org.opentcs.guing.common.application.action.file.ModelPropertiesAction; +import org.opentcs.guing.common.application.action.file.SaveModelAction; +import org.opentcs.guing.common.application.action.file.SaveModelAsAction; +import org.opentcs.operationsdesk.application.OpenTCSView; +import org.opentcs.operationsdesk.application.action.actions.ConnectToKernelAction; +import org.opentcs.operationsdesk.application.action.actions.CreatePeripheralJobAction; +import org.opentcs.operationsdesk.application.action.actions.CreateTransportOrderAction; +import org.opentcs.operationsdesk.application.action.actions.DisconnectFromKernelAction; +import org.opentcs.operationsdesk.application.action.actions.FindVehicleAction; +import org.opentcs.operationsdesk.application.action.actions.PauseAllVehiclesAction; +import org.opentcs.operationsdesk.application.action.actions.ResumeAllVehiclesAction; +import org.opentcs.operationsdesk.application.action.app.AboutAction; +import org.opentcs.operationsdesk.application.action.view.AddDrawingViewAction; +import org.opentcs.operationsdesk.application.action.view.AddPeripheralJobViewAction; +import org.opentcs.operationsdesk.application.action.view.AddTransportOrderSequenceViewAction; +import org.opentcs.operationsdesk.application.action.view.AddTransportOrderViewAction; +import org.opentcs.operationsdesk.application.action.view.RestoreDockingLayoutAction; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.DeleteAction; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.SelectAllAction; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.UndoRedoManager; +import org.opentcs.thirdparty.operationsdesk.jhotdraw.application.action.file.CloseFileAction; + +/** + * A custom ActionMap for the plant overview application. + */ +public class ViewActionMap + extends + ActionMap { + + /** + * Creates a new instance. + * + * @param view The openTCS view + * @param undoRedoManager The undo redo manager + * @param kernelClientApplication The kernel-client application + * @param actionFactory The action factory + * @param createTransportOrderAction The action to create transport orders + * @param findVehicleAction The action to find vehicles + * @param pauseAllVehiclesAction The action to pause all vehicles + * @param resumeAllVehiclesAction The action to resume all vehicles + * @param aboutAction The action to show the about window + * @param modelPropertiesAction The action to show some model properties. + * @param createPeripheralJobAction The action to create peripheral jobs. + */ + @Inject + @SuppressWarnings("this-escape") + public ViewActionMap( + OpenTCSView view, + UndoRedoManager undoRedoManager, + KernelClientApplication kernelClientApplication, + ActionFactory actionFactory, + CreateTransportOrderAction createTransportOrderAction, + FindVehicleAction findVehicleAction, + PauseAllVehiclesAction pauseAllVehiclesAction, + ResumeAllVehiclesAction resumeAllVehiclesAction, + AboutAction aboutAction, + ModelPropertiesAction modelPropertiesAction, + CreatePeripheralJobAction createPeripheralJobAction + ) { + requireNonNull(view, "view"); + requireNonNull(undoRedoManager, "undoRedoManager"); + requireNonNull(kernelClientApplication, "kernelClientApplication"); + requireNonNull(actionFactory, "actionFactory"); + requireNonNull(createTransportOrderAction, "createTransportOrderAction"); + requireNonNull(findVehicleAction, "findVehicleAction"); + requireNonNull(pauseAllVehiclesAction, "pauseAllVehiclesAction"); + requireNonNull(resumeAllVehiclesAction, "resumeAllVehiclesAction"); + requireNonNull(aboutAction, "aboutAction"); + requireNonNull(createPeripheralJobAction, "createPeripheralJobAction"); + + // --- Menu File --- + put(SaveModelAction.ID, new SaveModelAction(view)); + put(SaveModelAsAction.ID, new SaveModelAsAction(view)); + put(ModelPropertiesAction.ID, modelPropertiesAction); + put(CloseFileAction.ID, new CloseFileAction(view)); + put(ConnectToKernelAction.ID, new ConnectToKernelAction(kernelClientApplication)); + put(DisconnectFromKernelAction.ID, new DisconnectFromKernelAction(kernelClientApplication)); + + // --- Menu Edit --- + // Undo, Redo + put(UndoRedoManager.UNDO_ACTION_ID, undoRedoManager.getUndoAction()); + put(UndoRedoManager.REDO_ACTION_ID, undoRedoManager.getRedoAction()); + // Cut, Copy, Paste, Duplicate, Delete + put(DeleteAction.ID, new DeleteAction()); + // Select all + put(SelectAllAction.ID, new SelectAllAction()); + + // --- Menu Actions --- + // Menu item Actions -> Create ... + put(CreateTransportOrderAction.ID, createTransportOrderAction); + put(CreatePeripheralJobAction.ID, createPeripheralJobAction); + + // --- Menu View --- + // Menu View -> Add drawing view + put(AddDrawingViewAction.ID, new AddDrawingViewAction(view)); + + // Menu View -> Add transport order view + put(AddTransportOrderViewAction.ID, new AddTransportOrderViewAction(view)); + + // Menu View -> Add transport order sequence view + put(AddTransportOrderSequenceViewAction.ID, new AddTransportOrderSequenceViewAction(view)); + + put(AddPeripheralJobViewAction.ID, new AddPeripheralJobViewAction(view)); + + put(RestoreDockingLayoutAction.ID, new RestoreDockingLayoutAction(view)); + + put(FindVehicleAction.ID, findVehicleAction); + put(PauseAllVehiclesAction.ID, pauseAllVehiclesAction); + put(ResumeAllVehiclesAction.ID, resumeAllVehiclesAction); + + // --- Menu Help --- + put(AboutAction.ID, aboutAction); + } + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/ConnectToKernelAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/ConnectToKernelAction.java new file mode 100644 index 0000000..02be2fe --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/ConnectToKernelAction.java @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.actions; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.MENU_PATH; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import org.opentcs.common.KernelClientApplication; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * An action to connect to a kernel. + */ +public class ConnectToKernelAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "file.connectToKernel"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + + private final KernelClientApplication application; + + /** + * Creates a new instance. + * + * @param application The kernel client application. + */ + @SuppressWarnings("this-escape") + public ConnectToKernelAction(KernelClientApplication application) { + this.application = requireNonNull(application, "application"); + + putValue(NAME, BUNDLE.getString("connectToKernelAction.name")); + } + + @Override + public void actionPerformed(ActionEvent evt) { + application.online(false); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/CreatePeripheralJobAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/CreatePeripheralJobAction.java new file mode 100644 index 0000000..7122cc6 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/CreatePeripheralJobAction.java @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.actions; + +import static java.util.Objects.requireNonNull; +import static javax.swing.Action.MNEMONIC_KEY; +import static javax.swing.Action.NAME; +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.MENU_PATH; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import java.awt.Component; +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.access.to.peripherals.PeripheralJobCreationTO; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.guing.common.components.dialogs.StandardContentDialog; +import org.opentcs.operationsdesk.transport.CreatePeripheralJobPanel; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An action to trigger the creation of a peripheral job. + */ +public class CreatePeripheralJobAction + extends + AbstractAction { + + /** + * This action class's ID. + */ + public static final String ID = "actions.createPeripheralJob"; + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(CreatePeripheralJobAction.class); + /** + * Access to the resource bundle. + */ + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + /** + * The parent component for dialogs show by this action. + */ + private final Component dialogParent; + /** + * Provides panels for entering a new peripheral job. + */ + private final Provider jobPanelProvider; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + + /** + * Creates a new instance. + * + * @param dialogParent The parent for dialogs shown by this action. + * @param peripheralJobPanel Provides panels for entering new peripheral jobs. + * @param portalProvider Provides access to the kernel service portal. + */ + @Inject + @SuppressWarnings("this-escape") + public CreatePeripheralJobAction( + @ApplicationFrame + Component dialogParent, + Provider peripheralJobPanel, + SharedKernelServicePortalProvider portalProvider + ) { + this.dialogParent = requireNonNull(dialogParent, "dialogParent"); + this.jobPanelProvider = requireNonNull(peripheralJobPanel, "peripheralJobPanel"); + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + + putValue(NAME, BUNDLE.getString("createPeripheralJobAction.name")); + putValue(MNEMONIC_KEY, Integer.valueOf('P')); + } + + @Override + public void actionPerformed(ActionEvent e) { + CreatePeripheralJobPanel contentPanel = jobPanelProvider.get(); + StandardContentDialog dialog = new StandardContentDialog(dialogParent, contentPanel); + dialog.setVisible(true); + + if (dialog.getReturnStatus() != StandardContentDialog.RET_OK) { + return; + } + + PeripheralJobCreationTO job + = new PeripheralJobCreationTO( + "Job-", + contentPanel.getReservationToken(), + contentPanel.getPeripheralOperation() + ) + .withIncompleteName(true); + + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + sharedPortal.getPortal().getPeripheralJobService().createPeripheralJob(job); + sharedPortal.getPortal().getPeripheralDispatcherService().dispatch(); + } + catch (KernelRuntimeException exception) { + LOG.warn("Unexpected exception", exception); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/CreateTransportOrderAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/CreateTransportOrderAction.java new file mode 100644 index 0000000..4af8385 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/CreateTransportOrderAction.java @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.actions; + +import static java.util.Objects.requireNonNull; +import static javax.swing.Action.LARGE_ICON_KEY; +import static javax.swing.Action.MNEMONIC_KEY; +import static javax.swing.Action.SMALL_ICON; +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.MENU_PATH; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import java.awt.Component; +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import javax.swing.ImageIcon; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.guing.common.components.dialogs.StandardContentDialog; +import org.opentcs.guing.common.transport.OrderTypeSuggestionsPool; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.operationsdesk.exchange.TransportOrderUtil; +import org.opentcs.operationsdesk.transport.CreateTransportOrderPanel; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * An action to trigger the creation of a transport order. + */ +public class CreateTransportOrderAction + extends + AbstractAction { + + /** + * This action class's ID. + */ + public static final String ID = "actions.createTransportOrder"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + /** + * A helper for creating transport orders with the kernel. + */ + private final TransportOrderUtil orderUtil; + /** + * The parent component for dialogs shown by this action. + */ + private final Component dialogParent; + /** + * Provides panels for entering new transport orders. + */ + private final Provider orderPanelProvider; + /** + * The pool of suggested transport order types. + */ + private final OrderTypeSuggestionsPool typeSuggestionsPool; + + /** + * Creates a new instance. + * + * @param orderUtil A helper for creating transport orders with the kernel. + * @param dialogParent The parent component for dialogs shown by this action. + * @param orderPanelProvider Provides panels for entering new transport orders. + * @param typeSuggestionsPool The pool of suggested transport order types. + */ + @Inject + @SuppressWarnings("this-escape") + public CreateTransportOrderAction( + TransportOrderUtil orderUtil, + @ApplicationFrame + Component dialogParent, + Provider orderPanelProvider, + OrderTypeSuggestionsPool typeSuggestionsPool + ) { + this.orderUtil = requireNonNull(orderUtil, "orderUtil"); + this.dialogParent = requireNonNull(dialogParent, "dialogParent"); + this.orderPanelProvider = requireNonNull(orderPanelProvider, "orderPanelProvider"); + this.typeSuggestionsPool = requireNonNull(typeSuggestionsPool, "typeSuggestionsPool"); + + putValue(NAME, BUNDLE.getString("createTransportOrderAction.name")); + putValue(MNEMONIC_KEY, Integer.valueOf('T')); + + ImageIcon icon = ImageDirectory.getImageIcon("/toolbar/create-order.22.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + } + + @Override + public void actionPerformed(ActionEvent evt) { + createTransportOrder(); + } + + public void createTransportOrder() { + CreateTransportOrderPanel contentPanel = orderPanelProvider.get(); + StandardContentDialog dialog = new StandardContentDialog(dialogParent, contentPanel); + dialog.setVisible(true); + + if (dialog.getReturnStatus() != StandardContentDialog.RET_OK) { + return; + } + orderUtil.createTransportOrder( + contentPanel.getDestinationModels(), + contentPanel.getActions(), + contentPanel.getSelectedDeadline(), + contentPanel.getSelectedVehicle(), + contentPanel.getSelectedType() + ); + + typeSuggestionsPool.addTypeSuggestion(contentPanel.getSelectedType()); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/DisconnectFromKernelAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/DisconnectFromKernelAction.java new file mode 100644 index 0000000..66ca981 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/DisconnectFromKernelAction.java @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.actions; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.MENU_PATH; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import org.opentcs.common.KernelClientApplication; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * An action to disconnect from a kernel. + */ +public class DisconnectFromKernelAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "file.disconnectFromKernel"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + + private final KernelClientApplication application; + + /** + * Creates a new instance. + * + * @param application The kernel client application. + */ + @SuppressWarnings("this-escape") + public DisconnectFromKernelAction(KernelClientApplication application) { + this.application = requireNonNull(application, "application"); + + putValue(NAME, BUNDLE.getString("disconnectFromKernelAction.name")); + } + + @Override + public void actionPerformed(ActionEvent evt) { + application.offline(); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/FindVehicleAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/FindVehicleAction.java new file mode 100644 index 0000000..26c4ace --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/FindVehicleAction.java @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.actions; + +import static java.util.Objects.requireNonNull; +import static javax.swing.Action.LARGE_ICON_KEY; +import static javax.swing.Action.MNEMONIC_KEY; +import static javax.swing.Action.SMALL_ICON; +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.MENU_PATH; + +import jakarta.inject.Inject; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import javax.swing.AbstractAction; +import javax.swing.ImageIcon; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.dialogs.ClosableDialog; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.operationsdesk.components.dialogs.FindVehiclePanel; +import org.opentcs.operationsdesk.components.dialogs.FindVehiclePanelFactory; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * An action to find a vehicle on the drawing. + */ +public class FindVehicleAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "actions.findVehicle"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + /** + * Provides the current system model. + */ + private final ModelManager modelManager; + /** + * The drawing editor. + */ + private final OpenTCSDrawingEditor drawingEditor; + /** + * The parent component for dialogs shown by this action. + */ + private final Component dialogParent; + /** + * The panel factory. + */ + private final FindVehiclePanelFactory panelFactory; + + /** + * Creates a new instance. + * + * @param modelManager Provides the current system model. + * @param drawingEditor The drawing editor. + * @param dialogParent The parent component for dialogs shown by this action. + * @param panelFactory The panel factory. + */ + @Inject + @SuppressWarnings("this-escape") + public FindVehicleAction( + ModelManager modelManager, + OpenTCSDrawingEditor drawingEditor, + @ApplicationFrame + Component dialogParent, + FindVehiclePanelFactory panelFactory + ) { + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.drawingEditor = requireNonNull(drawingEditor, "drawingEditor"); + this.dialogParent = requireNonNull(dialogParent, "dialogParent"); + this.panelFactory = requireNonNull(panelFactory, "panelFactory"); + + putValue(NAME, BUNDLE.getString("findVehicleAction.name")); + putValue(MNEMONIC_KEY, Integer.valueOf('F')); + + ImageIcon icon = ImageDirectory.getImageIcon("/toolbar/find-vehicle.22.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + } + + @Override + public void actionPerformed(ActionEvent evt) { + findVehicle(); + } + + private void findVehicle() { + List vehicles = new ArrayList<>(modelManager.getModel().getVehicleModels()); + if (vehicles.isEmpty()) { + return; + } + + Collections.sort(vehicles, Comparator.comparing(VehicleModel::getName)); + FindVehiclePanel content = panelFactory.createFindVehiclesPanel( + vehicles, + drawingEditor.getActiveView() + ); + String title = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.FINDVEHICLE_PATH) + .getString("findVehicleAction.dialog_findVehicle.title"); + ClosableDialog dialog = new ClosableDialog(dialogParent, true, content, title); + dialog.setLocationRelativeTo(dialogParent); + dialog.setVisible(true); + } + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/PauseAllVehiclesAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/PauseAllVehiclesAction.java new file mode 100644 index 0000000..13f4233 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/PauseAllVehiclesAction.java @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.actions; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.TOOLBAR_PATH; + +import jakarta.inject.Inject; +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import javax.swing.ImageIcon; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.components.kernel.services.ServiceUnavailableException; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Action for pausing all vehicles. + */ +public class PauseAllVehiclesAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "openTCS.pauseAllVehicles"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(TOOLBAR_PATH); + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PauseAllVehiclesAction.class); + /** + * Provides the current system model. + */ + private final ModelManager modelManager; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + + /** + * Creates a new instance. + * + * @param modelManager Provides the current system model. + * @param portalProvider Provides access to a portal. + */ + @Inject + @SuppressWarnings("this-escape") + public PauseAllVehiclesAction( + ModelManager modelManager, + SharedKernelServicePortalProvider portalProvider + ) { + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + + putValue(NAME, BUNDLE.getString("pauseAllVehiclesAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("pauseAllVehiclesAction.shortDescription")); + + ImageIcon iconSmall = ImageDirectory.getImageIcon("/toolbar/pause-vehicles.16.png"); + ImageIcon iconLarge = ImageDirectory.getImageIcon("/toolbar/pause-vehicles.22.png"); + putValue(SMALL_ICON, iconSmall); + putValue(LARGE_ICON_KEY, iconLarge); + } + + @Override + public void actionPerformed(ActionEvent evt) { + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + pauseVehicles(sharedPortal.getPortal()); + } + catch (ServiceUnavailableException exc) { + LOG.warn("Could not connect to kernel", exc); + } + } + + private void pauseVehicles(KernelServicePortal portal) { + ModelComponent folder = modelManager.getModel().getMainFolder(SystemModel.FolderKey.VEHICLES); + + for (ModelComponent component : folder.getChildComponents()) { + VehicleModel vModel = (VehicleModel) component; + LOG.info("Pausing vehicle {}...", vModel.getVehicle().getName()); + portal.getVehicleService().updateVehiclePaused(vModel.getVehicle().getReference(), true); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/ResumeAllVehiclesAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/ResumeAllVehiclesAction.java new file mode 100644 index 0000000..426bda1 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/actions/ResumeAllVehiclesAction.java @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.actions; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.TOOLBAR_PATH; + +import jakarta.inject.Inject; +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import javax.swing.ImageIcon; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.components.kernel.services.ServiceUnavailableException; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Action for resuming all vehicles. + */ +public class ResumeAllVehiclesAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "openTCS.resumeAllVehicles"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(TOOLBAR_PATH); + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ResumeAllVehiclesAction.class); + /** + * Provides the current system model. + */ + private final ModelManager modelManager; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + + /** + * Creates a new instance. + * + * @param modelManager Provides the current system model. + * @param portalProvider Provides access to a portal. + */ + @Inject + @SuppressWarnings("this-escape") + public ResumeAllVehiclesAction( + ModelManager modelManager, + SharedKernelServicePortalProvider portalProvider + ) { + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + + putValue(NAME, BUNDLE.getString("resumeAllVehiclesAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("resumeAllVehiclesAction.shortDescription")); + + ImageIcon iconSmall = ImageDirectory.getImageIcon("/toolbar/resume-vehicles.16.png"); + ImageIcon iconLarge = ImageDirectory.getImageIcon("/toolbar/resume-vehicles.22.png"); + putValue(SMALL_ICON, iconSmall); + putValue(LARGE_ICON_KEY, iconLarge); + } + + @Override + public void actionPerformed(ActionEvent evt) { + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + resumeVehicles(sharedPortal.getPortal()); + } + catch (ServiceUnavailableException exc) { + LOG.warn("Could not connect to kernel", exc); + } + } + + private void resumeVehicles(KernelServicePortal portal) { + ModelComponent folder = modelManager.getModel().getMainFolder(SystemModel.FolderKey.VEHICLES); + + for (ModelComponent component : folder.getChildComponents()) { + VehicleModel vModel = (VehicleModel) component; + LOG.info("Resuming vehicle {}...", vModel.getVehicle().getName()); + portal.getVehicleService().updateVehiclePaused(vModel.getVehicle().getReference(), false); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/app/AboutAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/app/AboutAction.java new file mode 100644 index 0000000..b19153a --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/app/AboutAction.java @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.app; + +import static java.util.Objects.requireNonNull; +import static javax.swing.Action.LARGE_ICON_KEY; +import static javax.swing.Action.MNEMONIC_KEY; +import static javax.swing.Action.SMALL_ICON; +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.MENU_PATH; + +import jakarta.inject.Inject; +import java.awt.Component; +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import javax.swing.ImageIcon; +import javax.swing.JOptionPane; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.guing.common.application.ApplicationState; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.operationsdesk.application.OpenTCSView; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.util.Environment; + +/** + * Displays a dialog showing information about the application. + */ +public class AboutAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "application.about"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + /** + * Stores the application's current state. + */ + private final ApplicationState appState; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * The parent component for dialogs shown by this action. + */ + private final Component dialogParent; + + /** + * Creates a new instance. + * + * @param appState Stores the application's current state. + * @param portalProvider Provides access to a portal. + * @param dialogParent The parent component for dialogs shown by this action. + */ + @Inject + @SuppressWarnings("this-escape") + public AboutAction( + ApplicationState appState, + SharedKernelServicePortalProvider portalProvider, + @ApplicationFrame + Component dialogParent + ) { + this.appState = requireNonNull(appState, "appState"); + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.dialogParent = requireNonNull(dialogParent, "dialogParent"); + + putValue(NAME, BUNDLE.getString("aboutAction.name")); + putValue(MNEMONIC_KEY, Integer.valueOf('A')); + + ImageIcon icon = ImageDirectory.getImageIcon("/menu/help-contents.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + } + + @Override + public void actionPerformed(ActionEvent evt) { + JOptionPane.showMessageDialog( + dialogParent, + "

" + OpenTCSView.NAME + "
" + + BUNDLE.getFormatted( + "aboutAction.optionPane_applicationInformation.message.baselineVersion", + Environment.getBaselineVersion() + ) + + "
" + + BUNDLE.getFormatted( + "aboutAction.optionPane_applicationInformation.message.customization", + Environment.getCustomizationName(), + Environment.getCustomizationVersion() + ) + + "
" + + BUNDLE.getString("aboutAction.optionPane_applicationInformation.message.copyright") + + "
" + + BUNDLE.getString("aboutAction.optionPane_applicationInformation.message.runningOn") + + "
" + + "Java: " + + System.getProperty("java.version") + + ", " + + System.getProperty("java.vendor") + + "
" + + "JVM: " + + System.getProperty("java.vm.version") + + ", " + + System.getProperty("java.vm.vendor") + + "
" + + "OS: " + + System.getProperty("os.name") + + " " + + System.getProperty("os.version") + + ", " + + System.getProperty("os.arch") + + "
" + + "Kernel
" + + portalProvider.getPortalDescription() + + "
" + + BUNDLE.getFormatted( + "aboutAction.optionPane_applicationInformation.message.mode", + appState.getOperationMode() + ) + + "

", + BUNDLE.getString("aboutAction.optionPane_applicationInformation.title"), + JOptionPane.PLAIN_MESSAGE, + new ImageIcon( + getClass().getResource("/org/opentcs/guing/res/symbols/openTCS/openTCS.300x132.gif") + ) + ); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/FollowVehicleAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/FollowVehicleAction.java new file mode 100644 index 0000000..d8bef9d --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/FollowVehicleAction.java @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.course; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.VEHICLEPOPUP_PATH; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import javax.swing.JCheckBoxMenuItem; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingView; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + */ +public class FollowVehicleAction + extends + AbstractAction { + + /** + * Automatically moves the drawing so a vehicle is always visible. + */ + public static final String ID = "course.vehicle.follow"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(VEHICLEPOPUP_PATH); + /** + * The vehicle. + */ + private final VehicleModel vehicleModel; + /** + * The drawing editor. + */ + private final OpenTCSDrawingEditor drawingEditor; + + /** + * Creates a new instance. + * + * @param vehicle The selected vehicle. + * @param drawingEditor The application's drawing editor. + */ + @Inject + @SuppressWarnings("this-escape") + public FollowVehicleAction( + @Assisted + VehicleModel vehicle, + OpenTCSDrawingEditor drawingEditor + ) { + this.vehicleModel = requireNonNull(vehicle, "vehicle"); + this.drawingEditor = requireNonNull(drawingEditor, "drawingEditor"); + + putValue(NAME, BUNDLE.getString("followVehicleAction.name")); + } + + @Override + public void actionPerformed(ActionEvent evt) { + JCheckBoxMenuItem checkBox = (JCheckBoxMenuItem) evt.getSource(); + OpenTCSDrawingView drawingView = drawingEditor.getActiveView(); + + if (drawingView != null) { + if (checkBox.isSelected()) { + drawingView.followVehicle(vehicleModel); + } + else { + drawingView.stopFollowVehicle(); + } + } + } + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/IntegrationLevelChangeAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/IntegrationLevelChangeAction.java new file mode 100644 index 0000000..52d13da --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/IntegrationLevelChangeAction.java @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.course; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.VEHICLEPOPUP_PATH; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.event.ActionEvent; +import java.util.Collection; +import javax.swing.AbstractAction; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.data.model.Vehicle; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + */ +public class IntegrationLevelChangeAction + extends + AbstractAction { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(IntegrationLevelChangeAction.class); + /** + * This instance's resource bundle. + */ + private final ResourceBundleUtil bundle = ResourceBundleUtil.getBundle(VEHICLEPOPUP_PATH); + /** + * The vehicles to change the level of. + */ + private final Collection vehicles; + /** + * Sets the level to to change the vehicles to. + */ + private final Vehicle.IntegrationLevel level; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + + /** + * Creates a new instance. + * + * @param vehicles The selected vehicles. + * @param level The level to to change the vehicles to. + * @param portalProvider Provides access to a shared portal. + */ + @Inject + @SuppressWarnings("this-escape") + public IntegrationLevelChangeAction( + @Assisted + Collection vehicles, + @Assisted + Vehicle.IntegrationLevel level, + SharedKernelServicePortalProvider portalProvider + ) { + this.vehicles = requireNonNull(vehicles, "vehicles"); + this.level = requireNonNull(level, "level"); + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + + String actionName; + switch (level) { + case TO_BE_NOTICED: + actionName = bundle.getString("integrationLevelChangeAction.notice.name"); + break; + case TO_BE_RESPECTED: + actionName = bundle.getString("integrationLevelChangeAction.respect.name"); + break; + case TO_BE_UTILIZED: + actionName = bundle.getString("integrationLevelChangeAction.utilize.name"); + break; + default: + actionName = bundle.getString("integrationLevelChangeAction.ignore.name"); + break; + } + putValue(NAME, actionName); + } + + @Override + public void actionPerformed(ActionEvent evt) { + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + + for (VehicleModel vehicle : vehicles) { + sharedPortal.getPortal().getVehicleService().updateVehicleIntegrationLevel( + vehicle.getVehicle().getReference(), level + ); + } + + } + catch (KernelRuntimeException e) { + LOG.warn("Unexpected exception", e); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/PauseAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/PauseAction.java new file mode 100644 index 0000000..a1a15b8 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/PauseAction.java @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.course; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.VEHICLEPOPUP_PATH; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.event.ActionEvent; +import java.util.Collection; +import javax.swing.AbstractAction; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + */ +public class PauseAction + extends + AbstractAction { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PauseAction.class); + /** + * This instance's resource bundle. + */ + private final ResourceBundleUtil bundle = ResourceBundleUtil.getBundle(VEHICLEPOPUP_PATH); + /** + * The vehicles to change the level of. + */ + private final Collection vehicles; + /** + * Indicates whether to pause or unpause the vehicles. + */ + private final boolean pause; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + + /** + * Creates a new instance. + * + * @param vehicles The selected vehicles. + * @param pause The paused state to set the vehicles to. + * @param portalProvider Provides access to a shared portal. + */ + @Inject + @SuppressWarnings("this-escape") + public PauseAction( + @Assisted + Collection vehicles, + @Assisted + boolean pause, + SharedKernelServicePortalProvider portalProvider + ) { + this.vehicles = requireNonNull(vehicles, "vehicles"); + this.pause = requireNonNull(pause, "pause"); + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + + putValue( + NAME, + pause + ? bundle.getString("pauseAction.pause.name") + : bundle.getString("pauseAction.resume.name") + ); + } + + @Override + public void actionPerformed(ActionEvent evt) { + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + + for (VehicleModel vehicle : vehicles) { + sharedPortal.getPortal().getVehicleService().updateVehiclePaused( + vehicle.getVehicle().getReference(), pause + ); + } + + } + catch (KernelRuntimeException e) { + LOG.warn("Unexpected exception", e); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/RerouteAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/RerouteAction.java new file mode 100644 index 0000000..5d840ca --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/RerouteAction.java @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.course; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.VEHICLEPOPUP_PATH; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.util.Collection; +import javax.swing.AbstractAction; +import javax.swing.JOptionPane; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An action for triggering a rerouting of a selected set of vehicles. + */ +public class RerouteAction + extends + AbstractAction { + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(VEHICLEPOPUP_PATH); + private static final Logger LOG = LoggerFactory.getLogger(RerouteAction.class); + private final Collection vehicles; + private final ReroutingType reroutingType; + private final SharedKernelServicePortalProvider portalProvider; + private final Component dialogParent; + + /** + * Creates a new instance. + * + * @param vehicles The selected vehicles. + * @param reroutingType The selected rerouting type. + * @param portalProvider Provides access to a shared kernel service portal. + * @param dialogParent The parent component for dialogs shown by this action. + */ + @Inject + @SuppressWarnings("this-escape") + public RerouteAction( + @Assisted + Collection vehicles, + @Assisted + ReroutingType reroutingType, + SharedKernelServicePortalProvider portalProvider, + @ApplicationFrame + Component dialogParent + ) { + this.vehicles = requireNonNull(vehicles, "vehicles"); + this.reroutingType = requireNonNull(reroutingType, "reroutingType"); + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.dialogParent = requireNonNull(dialogParent, "dialogParent"); + + switch (reroutingType) { + case REGULAR: + putValue(NAME, BUNDLE.getString("rerouteAction.regularRerouting.name")); + break; + case FORCED: + putValue(NAME, BUNDLE.getString("rerouteAction.forcedRerouting.name")); + break; + default: + putValue(NAME, reroutingType.name()); + } + } + + @Override + public void actionPerformed(ActionEvent evt) { + if (reroutingType == ReroutingType.FORCED) { + int dialogResult = JOptionPane.showConfirmDialog( + dialogParent, + BUNDLE.getString("rerouteAction.optionPane_confirmForcedRerouting.message"), + BUNDLE.getString("rerouteAction.optionPane_confirmForcedRerouting.title"), + JOptionPane.OK_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE + ); + + if (dialogResult != JOptionPane.OK_OPTION) { + return; + } + } + + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + for (VehicleModel vehicle : vehicles) { + sharedPortal.getPortal().getDispatcherService().reroute( + vehicle.getVehicle().getReference(), + reroutingType + ); + } + } + catch (KernelRuntimeException e) { + LOG.warn("Unexpected exception", e); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/ScrollToVehicleAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/ScrollToVehicleAction.java new file mode 100644 index 0000000..2f487c3 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/ScrollToVehicleAction.java @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.course; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.VEHICLEPOPUP_PATH; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import org.jhotdraw.draw.Figure; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingView; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + */ +public class ScrollToVehicleAction + extends + AbstractAction { + + /** + * Scrolls to a vehicle in the drawing. + */ + public static final String ID = "course.vehicle.scrollTo"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(VEHICLEPOPUP_PATH); + /** + * The vehicle. + */ + private final VehicleModel vehicleModel; + /** + * The drawing editor. + */ + private final OpenTCSDrawingEditor drawingEditor; + /** + * The model manager. + */ + private final ModelManager modelManager; + + /** + * Creates a new instance. + * + * @param vehicle The selected vehicle. + * @param drawingEditor The application's drawing editor. + * @param modelManager The model manager. + */ + @Inject + @SuppressWarnings("this-escape") + public ScrollToVehicleAction( + @Assisted + VehicleModel vehicle, + OpenTCSDrawingEditor drawingEditor, + ModelManager modelManager + ) { + this.vehicleModel = requireNonNull(vehicle, "vehicle"); + this.drawingEditor = requireNonNull(drawingEditor, "drawingEditor"); + this.modelManager = requireNonNull(modelManager, "modelManager"); + + putValue(NAME, BUNDLE.getString("scrollToVehicleAction.name")); + } + + @Override + public void actionPerformed(ActionEvent e) { + Figure figure = modelManager.getModel().getFigure(vehicleModel); + OpenTCSDrawingView drawingView = drawingEditor.getActiveView(); + + if (drawingView != null && figure != null) { + drawingView.clearSelection(); + drawingView.addToSelection(figure); + drawingView.scrollTo(figure); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/SendVehicleToLocationAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/SendVehicleToLocationAction.java new file mode 100644 index 0000000..1687ea7 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/SendVehicleToLocationAction.java @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.course; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.VEHICLEPOPUP_PATH; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.List; +import javax.swing.AbstractAction; +import javax.swing.JFrame; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.data.order.OrderConstants; +import org.opentcs.guing.base.model.AbstractConnectableModelComponent; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.dialogs.StandardContentDialog; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.operationsdesk.exchange.TransportOrderUtil; +import org.opentcs.operationsdesk.transport.LocationActionPanel; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + */ +public class SendVehicleToLocationAction + extends + AbstractAction { + + /** + * Sends a vehicle directly to a location. + */ + public static final String ID = "course.vehicle.sendToLocation"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(VEHICLEPOPUP_PATH); + /** + * The vehicle. + */ + private final VehicleModel fVehicle; + /** + * The application's main frame. + */ + private final JFrame applicationFrame; + /** + * Provides the current system model. + */ + private final ModelManager modelManager; + /** + * A helper for creating transport orders with the kernel. + */ + private final TransportOrderUtil orderUtil; + + /** + * Creates a new instance. + * + * @param vehicle The selected vehicle. + * @param applicationFrame The application's main view. + * @param modelManager Provides the current system model. + * @param orderUtil A helper for creating transport orders with the kernel. + */ + @Inject + @SuppressWarnings("this-escape") + public SendVehicleToLocationAction( + @Assisted + VehicleModel vehicle, + @ApplicationFrame + JFrame applicationFrame, + ModelManager modelManager, + TransportOrderUtil orderUtil + ) { + this.fVehicle = requireNonNull(vehicle, "vehicle"); + this.applicationFrame = requireNonNull(applicationFrame, "applicationFrame"); + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.orderUtil = requireNonNull(orderUtil, "orderUtil"); + + putValue(NAME, BUNDLE.getString("sendVehicleToLocationAction.name")); + } + + @Override + public void actionPerformed(ActionEvent evt) { + List locModels = locationModels(); + + if (!locModels.isEmpty()) { + LocationActionPanel contentPanel = new LocationActionPanel(locModels); + StandardContentDialog fDialog = new StandardContentDialog(applicationFrame, contentPanel); + fDialog.setTitle(evt.getActionCommand()); + fDialog.setVisible(true); + + if (fDialog.getReturnStatus() == StandardContentDialog.RET_OK) { + LocationModel location = contentPanel.getSelectedLocation(); + List destinationModels = new ArrayList<>(); + destinationModels.add(location); + List actions = new ArrayList<>(); + actions.add(contentPanel.getSelectedAction()); + orderUtil.createTransportOrder( + destinationModels, + actions, + System.currentTimeMillis(), + fVehicle, + OrderConstants.TYPE_NONE + ); + } + } + } + + private List locationModels() { + return modelManager.getModel().getLocationModels(); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/SendVehicleToPointAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/SendVehicleToPointAction.java new file mode 100644 index 0000000..c2b0aea --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/SendVehicleToPointAction.java @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.course; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.VEHICLEPOPUP_PATH; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.event.ActionEvent; +import java.util.List; +import javax.swing.AbstractAction; +import javax.swing.JFrame; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.dialogs.StandardContentDialog; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.operationsdesk.exchange.TransportOrderUtil; +import org.opentcs.operationsdesk.transport.PointPanel; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + */ +public class SendVehicleToPointAction + extends + AbstractAction { + + /** + * Sends a vehicle directly to a point. + */ + public static final String ID = "course.vehicle.sendToPoint"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(VEHICLEPOPUP_PATH); + /** + * The vehicle. + */ + private final VehicleModel vehicleModel; + /** + * The application's main frame. + */ + private final JFrame applicationFrame; + /** + * Provides the current system model. + */ + private final ModelManager modelManager; + /** + * A helper for creating transport orders with the kernel. + */ + private final TransportOrderUtil orderUtil; + + /** + * Creates a new instance. + * + * @param vehicle The selected vehicle. + * @param applicationFrame The application's main view. + * @param modelManager Provides the current system model. + * @param orderUtil A helper for creating transport orders with the kernel. + */ + @Inject + @SuppressWarnings("this-escape") + public SendVehicleToPointAction( + @Assisted + VehicleModel vehicle, + @ApplicationFrame + JFrame applicationFrame, + ModelManager modelManager, + TransportOrderUtil orderUtil + ) { + this.vehicleModel = requireNonNull(vehicle, "vehicle"); + this.applicationFrame = requireNonNull(applicationFrame, "applicationFrame"); + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.orderUtil = requireNonNull(orderUtil, "orderUtil"); + + putValue(NAME, BUNDLE.getString("sendVehicleToPointAction.name")); + } + + @Override + public void actionPerformed(ActionEvent evt) { + List pointModels = pointModels(); + + if (!pointModels.isEmpty()) { + PointPanel contentPanel = new PointPanel(pointModels); + StandardContentDialog fDialog = new StandardContentDialog(applicationFrame, contentPanel); + contentPanel.addInputValidationListener(fDialog); + fDialog.setTitle(evt.getActionCommand()); + fDialog.setVisible(true); + + if (fDialog.getReturnStatus() == StandardContentDialog.RET_OK) { + PointModel point = (PointModel) contentPanel.getSelectedItem(); + orderUtil.createTransportOrder(point, vehicleModel); + } + } + } + + private List pointModels() { + return modelManager.getModel().getPointModels(); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/WithdrawAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/WithdrawAction.java new file mode 100644 index 0000000..224977f --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/course/WithdrawAction.java @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.course; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.VEHICLEPOPUP_PATH; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.util.Collection; +import javax.swing.AbstractAction; +import javax.swing.JOptionPane; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + */ +public class WithdrawAction + extends + AbstractAction { + + /** + * The ID for the 'withdraw regularly' action. + */ + public static final String ID = "course.vehicle.withdrawTransportOrder"; + /** + * The ID for the 'withdraw forcibly' action. + */ + public static final String IMMEDIATELY_ID = "course.vehicle.withdrawTransportOrderImmediately"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(VEHICLEPOPUP_PATH); + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(WithdrawAction.class); + /** + * The vehicles. + */ + private final Collection vehicles; + /** + * Resource path to the correct lables. + */ + private final boolean immediateAbort; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * The parent component for dialogs shown by this action. + */ + private final Component dialogParent; + + /** + * Creates a new instance. + * + * @param vehicles The selected vehicles. + * @param immediateAbort Whether or not to abort immediately + * @param portalProvider Provides access to a shared portal. + * @param dialogParent The parent component for dialogs shown by this action. + */ + @Inject + @SuppressWarnings("this-escape") + public WithdrawAction( + @Assisted + Collection vehicles, + @Assisted + boolean immediateAbort, + SharedKernelServicePortalProvider portalProvider, + @ApplicationFrame + Component dialogParent + ) { + this.vehicles = requireNonNull(vehicles, "vehicles"); + this.immediateAbort = requireNonNull(immediateAbort, "immediateAbort"); + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.dialogParent = requireNonNull(dialogParent, "dialogParent"); + + if (immediateAbort) { + putValue(NAME, BUNDLE.getString("withdrawAction.withdrawImmediately.name")); + } + else { + putValue(NAME, BUNDLE.getString("withdrawAction.withdraw.name")); + } + + } + + @Override + public void actionPerformed(ActionEvent evt) { + if (immediateAbort) { + int dialogResult + = JOptionPane.showConfirmDialog( + dialogParent, + BUNDLE.getString("withdrawAction.optionPane_confirmWithdraw.message"), + BUNDLE.getString("withdrawAction.optionPane_confirmWithdraw.title"), + JOptionPane.OK_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE + ); + + if (dialogResult != JOptionPane.OK_OPTION) { + return; + } + } + + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + + for (VehicleModel vehicle : vehicles) { + sharedPortal.getPortal().getDispatcherService().withdrawByVehicle( + vehicle.getVehicle().getReference(), immediateAbort + ); + } + + } + catch (KernelRuntimeException e) { + LOG.warn("Unexpected exception", e); + } + } + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/view/AddDrawingViewAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/view/AddDrawingViewAction.java new file mode 100644 index 0000000..785d6be --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/view/AddDrawingViewAction.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.view; + +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.MENU_PATH; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import org.opentcs.operationsdesk.application.OpenTCSView; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * An action for adding new drawing views. + */ +public class AddDrawingViewAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "view.addDrawingView"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + + private final OpenTCSView view; + + /** + * Creates a new instance. + * + * @param view The openTCS view + */ + @SuppressWarnings("this-escape") + public AddDrawingViewAction(OpenTCSView view) { + this.view = view; + + putValue(NAME, BUNDLE.getString("addDrawingViewAction.name")); + } + + @Override + public void actionPerformed(ActionEvent evt) { + view.addDrawingView(); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/view/AddPeripheralJobViewAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/view/AddPeripheralJobViewAction.java new file mode 100644 index 0000000..e0c6975 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/view/AddPeripheralJobViewAction.java @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.view; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.MENU_PATH; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import org.opentcs.operationsdesk.application.OpenTCSView; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * An action for adding new peripheral job views. + */ +public class AddPeripheralJobViewAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "view.addPeripheralJobView"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + + private final OpenTCSView view; + + /** + * Creates a new instance. + * + * @param view The openTCS view + */ + @SuppressWarnings("this-escape") + public AddPeripheralJobViewAction(OpenTCSView view) { + this.view = requireNonNull(view, "view"); + + putValue(NAME, BUNDLE.getString("addPeripheralJobViewAction.name")); + } + + @Override + public void actionPerformed(ActionEvent e) { + view.addPeripheralJobsView(); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/view/AddTransportOrderSequenceViewAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/view/AddTransportOrderSequenceViewAction.java new file mode 100644 index 0000000..7ab08b5 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/view/AddTransportOrderSequenceViewAction.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.view; + +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.MENU_PATH; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import org.opentcs.operationsdesk.application.OpenTCSView; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * An action for adding new transport order sequence views. + */ +public class AddTransportOrderSequenceViewAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "view.addOrderSequenceView"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + + private final OpenTCSView view; + + /** + * Creates a new instance. + * + * @param view The openTCS view + */ + @SuppressWarnings("this-escape") + public AddTransportOrderSequenceViewAction(OpenTCSView view) { + this.view = view; + + putValue(NAME, BUNDLE.getString("addTransportOrderSequenceViewAction.name")); + } + + @Override + public void actionPerformed(ActionEvent e) { + view.addTransportOrderSequenceView(); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/view/AddTransportOrderViewAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/view/AddTransportOrderViewAction.java new file mode 100644 index 0000000..7be2a92 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/view/AddTransportOrderViewAction.java @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.view; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.MENU_PATH; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import org.opentcs.operationsdesk.application.OpenTCSView; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * An action for adding new transport order views. + */ +public class AddTransportOrderViewAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "view.addTransportOrderView"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + + private final OpenTCSView view; + + /** + * Creates a new instance. + * + * @param view The openTCS view + */ + @SuppressWarnings("this-escape") + public AddTransportOrderViewAction(OpenTCSView view) { + this.view = requireNonNull(view, "view"); + + putValue(NAME, BUNDLE.getString("addTransportOrderViewAction.name")); + } + + @Override + public void actionPerformed(ActionEvent e) { + view.addTransportOrderView(); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/view/RestoreDockingLayoutAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/view/RestoreDockingLayoutAction.java new file mode 100644 index 0000000..cf30c2f --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/action/view/RestoreDockingLayoutAction.java @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.action.view; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import org.opentcs.operationsdesk.application.OpenTCSView; + +/** + * Action for resetting the docking layout. + */ +public class RestoreDockingLayoutAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "openTCS.restoreDockingLayout"; + private final OpenTCSView view; + + /** + * Creates a new instance. + * + * @param view The openTCS view + */ + public RestoreDockingLayoutAction(OpenTCSView view) { + this.view = view; + } + + @Override + public void actionPerformed(ActionEvent e) { + view.resetWindowArrangement(); + } + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/MenuFactory.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/MenuFactory.java new file mode 100644 index 0000000..b220c6c --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/MenuFactory.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.menus; + +import java.util.Collection; +import org.opentcs.guing.base.model.elements.VehicleModel; + +/** + * A factory for various menus and menu items. + */ +public interface MenuFactory { + + /** + * Creates a popup menu with actions for a set of vehicles. + * + * @param vehicles The vehicle models for which to create the popup menu. + * @return A popup menu with actions for the given vehicle. + */ + VehiclePopupMenu createVehiclePopupMenu(Collection vehicles); +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/VehiclePopupMenu.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/VehiclePopupMenu.java new file mode 100644 index 0000000..158ced4 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/VehiclePopupMenu.java @@ -0,0 +1,232 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.menus; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.util.Collection; +import javax.swing.Action; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.operationsdesk.application.action.ActionFactory; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.operationsdesk.util.OperationsDeskConfiguration; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * A popup menu for actions for multiple selected vehicles. + */ +public class VehiclePopupMenu + extends + JPopupMenu { + + /** + * Creates a new instance. + * + * @param modelManager Provides access to the current system model. + * @param actionFactory A factory for menu actions. + * @param configuration The application configuration. + * @param vehicles a set of all currently selected Vehicles. + */ + @Inject + @SuppressWarnings("this-escape") + public VehiclePopupMenu( + ModelManager modelManager, + ActionFactory actionFactory, + OperationsDeskConfiguration configuration, + @Assisted + Collection vehicles + ) { + requireNonNull(modelManager, "modelManager"); + requireNonNull(actionFactory, "actionFactory"); + requireNonNull(vehicles, "vehicles"); + + final ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.VEHICLEPOPUP_PATH); + VehicleModel singleVehicle = vehicles.stream().findFirst().get(); + JCheckBoxMenuItem checkBoxMenuItem; + Action action; + + JMenuItem mi = new JMenuItem(); + mi.setEnabled(false); + if (vehicles.size() == 1) { + mi.setText( + bundle.getString("vehiclePopupMenu.menuItem_singleVehicle.text") + singleVehicle.getName() + ); + } + else { + mi.setText(bundle.getString("vehiclePopupMenu.menuItem_multipleVehicles.text")); + } + add(mi); + + addSeparator(); + + if (vehicles.size() == 1) { + action = actionFactory.createScrollToVehicleAction(singleVehicle); + add(action); + + action = actionFactory.createFollowVehicleAction(singleVehicle); + JCheckBoxMenuItem followCheckBox = new JCheckBoxMenuItem(); + followCheckBox.setAction(action); + followCheckBox.setSelected(singleVehicle.isViewFollows()); + add(followCheckBox); + + addSeparator(); + } + + if (vehicles.size() == 1) { + action = actionFactory.createSendVehicleToPointAction(singleVehicle); + action.setEnabled( + singleVehicle.isAvailableForOrder() + && !modelManager.getModel().getPointModels().isEmpty() + ); + add(action); + + action = actionFactory.createSendVehicleToLocationAction(singleVehicle); + action.setEnabled( + singleVehicle.isAvailableForOrder() + && !modelManager.getModel().getLocationModels().isEmpty() + ); + add(action); + + addSeparator(); + } + + JMenu pauseSubMenu = new JMenu(bundle.getString("vehiclePopupMenu.subMenu_pause.text")); + + action = actionFactory.createPauseAction(vehicles, true); + checkBoxMenuItem = new JCheckBoxMenuItem(action); + checkBoxMenuItem.setSelected(allPaused(vehicles)); + pauseSubMenu.add(checkBoxMenuItem); + + action = actionFactory.createPauseAction(vehicles, false); + checkBoxMenuItem = new JCheckBoxMenuItem(action); + checkBoxMenuItem.setSelected(nonePaused(vehicles)); + pauseSubMenu.add(checkBoxMenuItem); + + add(pauseSubMenu); + + addSeparator(); + + JMenu integrateSubMenu + = new JMenu(bundle.getString("vehiclePopupMenu.subMenu_integrate.text")); + + action = actionFactory.createIntegrationLevelChangeAction( + vehicles, + Vehicle.IntegrationLevel.TO_BE_IGNORED + ); + action.setEnabled(!isAnyProcessingOrder(vehicles)); + checkBoxMenuItem = new JCheckBoxMenuItem(action); + checkBoxMenuItem.setSelected( + isAnyAtIntegrationLevel( + vehicles, + Vehicle.IntegrationLevel.TO_BE_IGNORED + ) + ); + integrateSubMenu.add(checkBoxMenuItem); + + action = actionFactory.createIntegrationLevelChangeAction( + vehicles, + Vehicle.IntegrationLevel.TO_BE_NOTICED + ); + action.setEnabled(!isAnyProcessingOrder(vehicles)); + checkBoxMenuItem = new JCheckBoxMenuItem(action); + checkBoxMenuItem.setSelected( + isAnyAtIntegrationLevel( + vehicles, + Vehicle.IntegrationLevel.TO_BE_NOTICED + ) + ); + integrateSubMenu.add(checkBoxMenuItem); + + action = actionFactory.createIntegrationLevelChangeAction( + vehicles, + Vehicle.IntegrationLevel.TO_BE_RESPECTED + ); + checkBoxMenuItem = new JCheckBoxMenuItem(action); + checkBoxMenuItem.setSelected( + isAnyAtIntegrationLevel(vehicles, Vehicle.IntegrationLevel.TO_BE_RESPECTED) + ); + integrateSubMenu.add(checkBoxMenuItem); + + action = actionFactory.createIntegrationLevelChangeAction( + vehicles, + Vehicle.IntegrationLevel.TO_BE_UTILIZED + ); + checkBoxMenuItem = new JCheckBoxMenuItem(action); + checkBoxMenuItem.setSelected( + isAnyAtIntegrationLevel(vehicles, Vehicle.IntegrationLevel.TO_BE_UTILIZED) + ); + integrateSubMenu.add(checkBoxMenuItem); + + add(integrateSubMenu); + + addSeparator(); + + JMenu rerouteSubMenu + = new JMenu(bundle.getString("vehiclePopupMenu.subMenu_reroute.text")); + + action = actionFactory.createRerouteAction(vehicles, ReroutingType.REGULAR); + action.setEnabled(isAnyProcessingOrder(vehicles)); + rerouteSubMenu.add(action); + + action = actionFactory.createRerouteAction(vehicles, ReroutingType.FORCED); + action.setEnabled(isAnyProcessingOrder(vehicles)); + rerouteSubMenu.add(action); + + add(rerouteSubMenu); + + addSeparator(); + + JMenu withdrawSubMenu + = new JMenu(bundle.getString("vehiclePopupMenu.subMenu_withdraw.text")); + + action = actionFactory.createWithdrawAction(vehicles, false); + action.setEnabled(isAnyProcessingOrder(vehicles)); + withdrawSubMenu.add(action); + + action = actionFactory.createWithdrawAction(vehicles, true); + action.setEnabled(configuration.allowForcedWithdrawal() && isAnyProcessingOrder(vehicles)); + withdrawSubMenu.add(action); + + add(withdrawSubMenu); + } + + private boolean allPaused(Collection vehicles) { + return vehicles.stream().allMatch(vehicle -> isPaused(vehicle)); + } + + private boolean nonePaused(Collection vehicles) { + return vehicles.stream().noneMatch(vehicle -> isPaused(vehicle)); + } + + private boolean isAnyProcessingOrder(Collection vehicles) { + return vehicles.stream().anyMatch(vehicle -> isProcessingOrder(vehicle)); + } + + private boolean isPaused(VehicleModel vehicle) { + return Boolean.TRUE.equals(vehicle.getPropertyPaused().getValue()); + } + + private boolean isProcessingOrder(VehicleModel vehicle) { + return vehicle.getPropertyProcState().getValue() == Vehicle.ProcState.PROCESSING_ORDER + || vehicle.getPropertyProcState().getValue() == Vehicle.ProcState.AWAITING_ORDER; + } + + private boolean isAnyAtIntegrationLevel( + Collection vehicles, + Vehicle.IntegrationLevel level + ) { + return vehicles.stream().anyMatch( + vehicle -> vehicle.getPropertyIntegrationLevel().getComparableValue().equals(level) + ); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/menubar/ActionsMenu.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/menubar/ActionsMenu.java new file mode 100644 index 0000000..7b31463 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/menubar/ActionsMenu.java @@ -0,0 +1,163 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.menus.menubar; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.operationsdesk.event.KernelStateChangeEvent.State.LOGGED_IN; + +import jakarta.inject.Inject; +import java.awt.event.ActionEvent; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import org.jhotdraw.draw.Figure; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; +import org.opentcs.operationsdesk.application.action.ViewActionMap; +import org.opentcs.operationsdesk.application.action.actions.CreatePeripheralJobAction; +import org.opentcs.operationsdesk.application.action.actions.CreateTransportOrderAction; +import org.opentcs.operationsdesk.application.action.actions.FindVehicleAction; +import org.opentcs.operationsdesk.application.menus.MenuFactory; +import org.opentcs.operationsdesk.components.drawing.figures.VehicleFigure; +import org.opentcs.operationsdesk.event.KernelStateChangeEvent; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.operationsdesk.util.OperationsDeskConfiguration; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.event.EventSource; + +/** + * The application's menu for run-time actions. + */ +public class ActionsMenu + extends + JMenu + implements + EventHandler { + + /** + * A menu item for creating new transport orders. + */ + private final JMenuItem menuItemCreateTransportOrder; + /** + * A menu item for creating new peripheral jobs. + */ + private final JMenuItem menuItemCreatePeripheralJob; + /** + * A menu item for finding a vehicle in the driving course. + */ + private final JMenuItem menuItemFindVehicle; + /** + * A check box for ignoring the vehicles' precise positions. + */ + private final JCheckBoxMenuItem cbiIgnorePrecisePosition; + /** + * A check box for ignoring the vehicles' orientation angles. + */ + private final JCheckBoxMenuItem cbiIgnoreOrientationAngle; + + /** + * Creates a new instance. + * + * @param actionMap The application's action map. + * @param drawingEditor The application's drawing editor. + * @param menuFactory A factory for menu items. + * @param appConfig The application's configuration. + */ + @Inject + @SuppressWarnings("this-escape") + public ActionsMenu( + ViewActionMap actionMap, + OpenTCSDrawingEditor drawingEditor, + MenuFactory menuFactory, + OperationsDeskConfiguration appConfig, + @ApplicationEventBus + EventSource eventSource + ) { + requireNonNull(actionMap, "actionMap"); + requireNonNull(drawingEditor, "drawingEditor"); + requireNonNull(menuFactory, "menuFactory"); + requireNonNull(appConfig, "appConfig"); + requireNonNull(eventSource, "eventSource"); + + final ResourceBundleUtil labels + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.MENU_PATH); + + this.setText(labels.getString("actionsMenu.text")); + this.setToolTipText(labels.getString("actionsMenu.tooltipText")); + this.setMnemonic('A'); + + // Menu item Actions -> Create Transport Order + menuItemCreateTransportOrder = new JMenuItem(actionMap.get(CreateTransportOrderAction.ID)); + menuItemCreateTransportOrder.setEnabled(false); + add(menuItemCreateTransportOrder); + //Menu item Actions -> Create Peripheral Job. + menuItemCreatePeripheralJob = new JMenuItem(actionMap.get(CreatePeripheralJobAction.ID)); + menuItemCreatePeripheralJob.setEnabled(false); + add(menuItemCreatePeripheralJob); + addSeparator(); + + // Menu item Actions -> Find Vehicle + menuItemFindVehicle = new JMenuItem(actionMap.get(FindVehicleAction.ID)); + menuItemFindVehicle.setEnabled(false); + add(menuItemFindVehicle); + + // Menu item Actions -> Ignore precise position + cbiIgnorePrecisePosition = new JCheckBoxMenuItem( + labels.getString("actionsMenu.menuItem_ignorePrecisePosition.text") + ); + + add(cbiIgnorePrecisePosition); + cbiIgnorePrecisePosition.setSelected(appConfig.ignoreVehiclePrecisePosition()); + cbiIgnorePrecisePosition.addActionListener((ActionEvent e) -> { + for (Figure figure : drawingEditor.getDrawing().getChildren()) { + if (figure instanceof VehicleFigure) { + ((VehicleFigure) figure).setIgnorePrecisePosition(cbiIgnorePrecisePosition.isSelected()); + } + } + }); + + // Menu item Actions -> Ignore orientation angle + cbiIgnoreOrientationAngle = new JCheckBoxMenuItem( + labels.getString("actionsMenu.menuItem_ignorePreciseOrientation.text") + ); + + add(cbiIgnoreOrientationAngle); + cbiIgnoreOrientationAngle.setSelected(appConfig.ignoreVehicleOrientationAngle()); + cbiIgnoreOrientationAngle.addActionListener((ActionEvent e) -> { + for (Figure figure : drawingEditor.getDrawing().getChildren()) { + if (figure instanceof VehicleFigure) { + ((VehicleFigure) figure).setIgnoreOrientationAngle( + cbiIgnoreOrientationAngle.isSelected() + ); + } + } + }); + + eventSource.subscribe(this); + } + + @Override + public void onEvent(Object event) { + if (event instanceof KernelStateChangeEvent kernelStateChangeEvent) { + handleKernelStateChangeEvent(kernelStateChangeEvent); + } + } + + private void handleKernelStateChangeEvent(KernelStateChangeEvent event) { + switch (event.getNewState()) { + case LOGGED_IN: + menuItemCreateTransportOrder.setEnabled(true); + menuItemCreatePeripheralJob.setEnabled(true); + menuItemFindVehicle.setEnabled(true); + break; + case DISCONNECTED: + menuItemCreateTransportOrder.setEnabled(false); + menuItemCreatePeripheralJob.setEnabled(false); + menuItemFindVehicle.setEnabled(false); + break; + default: + // Do nothing. + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/menubar/ApplicationMenuBar.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/menubar/ApplicationMenuBar.java new file mode 100644 index 0000000..d54bab4 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/menubar/ApplicationMenuBar.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.menus.menubar; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import javax.swing.JMenuBar; + +/** + * The plant overview's main menu bar. + */ +public class ApplicationMenuBar + extends + JMenuBar { + + private final FileMenu menuFile; + private final ActionsMenu menuActions; + private final ViewMenu menuView; + private final HelpMenu menuHelp; + + /** + * Creates a new instance. + * + * @param menuFile The "File" menu. + * @param menuActions The "Actions" menu. + * @param menuView The "View" menu. + * @param menuHelp The "Help menu. + */ + @Inject + @SuppressWarnings("this-escape") + public ApplicationMenuBar( + FileMenu menuFile, + ActionsMenu menuActions, + ViewMenu menuView, + HelpMenu menuHelp + ) { + requireNonNull(menuFile, "menuFile"); + requireNonNull(menuActions, "menuActions"); + requireNonNull(menuView, "menuView"); + requireNonNull(menuHelp, "menuHelp"); + + this.menuFile = menuFile; + add(menuFile); + + this.menuActions = menuActions; + add(menuActions); + + this.menuView = menuView; + add(menuView); + + this.menuHelp = menuHelp; + add(menuHelp); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/menubar/FileMenu.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/menubar/FileMenu.java new file mode 100644 index 0000000..4a9daf2 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/menubar/FileMenu.java @@ -0,0 +1,137 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.menus.menubar; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.operationsdesk.event.KernelStateChangeEvent.State.LOGGED_IN; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.guing.common.application.action.file.ModelPropertiesAction; +import org.opentcs.guing.common.application.action.file.SaveModelAction; +import org.opentcs.guing.common.application.action.file.SaveModelAsAction; +import org.opentcs.operationsdesk.application.action.ViewActionMap; +import org.opentcs.operationsdesk.application.action.actions.ConnectToKernelAction; +import org.opentcs.operationsdesk.application.action.actions.DisconnectFromKernelAction; +import org.opentcs.operationsdesk.event.KernelStateChangeEvent; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.thirdparty.operationsdesk.jhotdraw.application.action.file.CloseFileAction; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.event.EventSource; + +/** + * The application's "File" menu. + */ +public class FileMenu + extends + JMenu + implements + EventHandler { + + /** + * A menu item for persisting the kernel's current model. + */ + private final JMenuItem menuItemSaveModel; + /** + * A menu item for persisting the kernel's current model with a new name. + */ + private final JMenuItem menuItemSaveModelAs; + /** + * A menu item for connecting to a kernel. + */ + private final JMenuItem menuItemConnect; + /** + * A menu item for disconnecting from the kernel. + */ + private final JMenuItem menuItemDisconnect; + /** + * A menu item for showing the current model's properties. + */ + private final JMenuItem menuItemModelProperties; + /** + * A menu item for closing the application. + */ + private final JMenuItem menuItemClose; + + /** + * Creates a new instance. + * + * @param actionMap The application's action map. + * @param eventSource Where this instance registers for application events. + */ + @Inject + @SuppressWarnings("this-escape") + public FileMenu( + ViewActionMap actionMap, + @Nonnull + @ApplicationEventBus + EventSource eventSource + ) { + requireNonNull(actionMap, "actionMap"); + requireNonNull(eventSource, "eventSource"); + + final ResourceBundleUtil labels + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.MENU_PATH); + + this.setText(labels.getString("fileMenu.text")); + this.setToolTipText(labels.getString("fileMenu.tooltipText")); + this.setMnemonic('F'); + + // Menu item File -> Save Model + menuItemSaveModel = new JMenuItem(actionMap.get(SaveModelAction.ID)); + add(menuItemSaveModel); + + // Menu item File -> Save Model As + menuItemSaveModelAs = new JMenuItem(actionMap.get(SaveModelAsAction.ID)); + add(menuItemSaveModelAs); + + addSeparator(); + + menuItemConnect = new JMenuItem(actionMap.get(ConnectToKernelAction.ID)); + menuItemConnect.setEnabled(true); + add(menuItemConnect); + + menuItemDisconnect = new JMenuItem(actionMap.get(DisconnectFromKernelAction.ID)); + menuItemDisconnect.setEnabled(false); + add(menuItemDisconnect); + + addSeparator(); + + menuItemModelProperties = new JMenuItem(actionMap.get(ModelPropertiesAction.ID)); + add(menuItemModelProperties); + + addSeparator(); + + // Menu item File -> Close + menuItemClose = new JMenuItem(actionMap.get(CloseFileAction.ID)); + add(menuItemClose); // TODO: Nur bei "Stand-Alone" Frame + + eventSource.subscribe(this); + } + + @Override + public void onEvent(Object event) { + if (event instanceof KernelStateChangeEvent kernelStateChangeEvent) { + handleKernelStateChangeEvent(kernelStateChangeEvent); + } + } + + private void handleKernelStateChangeEvent(KernelStateChangeEvent event) { + switch (event.getNewState()) { + case LOGGED_IN: + menuItemConnect.setEnabled(false); + menuItemDisconnect.setEnabled(true); + break; + case DISCONNECTED: + menuItemConnect.setEnabled(true); + menuItemDisconnect.setEnabled(false); + break; + default: + // Do nothing. + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/menubar/HelpMenu.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/menubar/HelpMenu.java new file mode 100644 index 0000000..bd09b03 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/menubar/HelpMenu.java @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.menus.menubar; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import org.opentcs.operationsdesk.application.action.ViewActionMap; +import org.opentcs.operationsdesk.application.action.app.AboutAction; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * The application's "Help" menu. + */ +public class HelpMenu + extends + JMenu { + + /** + * A menu item for showing the application's "about" panel. + */ + private final JMenuItem menuItemAbout; + + /** + * Creates a new instance. + * + * @param actionMap The application's action map. + */ + @Inject + @SuppressWarnings("this-escape") + public HelpMenu(ViewActionMap actionMap) { + requireNonNull(actionMap, "actionMap"); + + final ResourceBundleUtil labels + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.MENU_PATH); + + this.setText(labels.getString("helpMenu.text")); + this.setToolTipText(labels.getString("helpMenu.tooltipText")); + this.setMnemonic('?'); + + menuItemAbout = add(actionMap.get(AboutAction.ID)); + } + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/menubar/ViewMenu.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/menubar/ViewMenu.java new file mode 100644 index 0000000..837fb42 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/menus/menubar/ViewMenu.java @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.menus.menubar; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.operationsdesk.event.KernelStateChangeEvent.State.LOGGED_IN; + +import jakarta.inject.Inject; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.guing.common.application.OperationMode; +import org.opentcs.guing.common.application.menus.menubar.ViewPluginPanelsMenu; +import org.opentcs.operationsdesk.application.action.ViewActionMap; +import org.opentcs.operationsdesk.application.action.view.AddDrawingViewAction; +import org.opentcs.operationsdesk.application.action.view.AddPeripheralJobViewAction; +import org.opentcs.operationsdesk.application.action.view.AddTransportOrderSequenceViewAction; +import org.opentcs.operationsdesk.application.action.view.AddTransportOrderViewAction; +import org.opentcs.operationsdesk.application.action.view.RestoreDockingLayoutAction; +import org.opentcs.operationsdesk.event.KernelStateChangeEvent; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.event.EventSource; + +/** + * The application's menu for view-related operations. + */ +public class ViewMenu + extends + JMenu + implements + EventHandler { + + /** + * A menu item for adding a drawing view. + */ + private final JMenuItem menuAddDrawingView; + /** + * A menu item for adding a transport order view. + */ + private final JMenuItem menuTransportOrderView; + /** + * A menu item for adding an order sequence view. + */ + private final JMenuItem menuOrderSequenceView; + /** + * A menu item for adding a peripheral job view. + */ + private final JMenuItem menuPeripheralJobView; + /** + * A menu for showing/hiding plugin panels. + */ + private final ViewPluginPanelsMenu menuPluginPanels; + /** + * A menu item for restoring the default GUI layout. + */ + private final JMenuItem menuItemRestoreDockingLayout; + + /** + * Creates a new instance. + * + * @param actionMap The application's action map. + * @param menuPluginPanels A menu for showing/hiding plugin panels. + */ + @Inject + @SuppressWarnings("this-escape") + public ViewMenu( + ViewActionMap actionMap, + ViewPluginPanelsMenu menuPluginPanels, + @ApplicationEventBus + EventSource eventSource + ) { + requireNonNull(actionMap, "actionMap"); + requireNonNull(menuPluginPanels, "menuPluginPanels"); + + final ResourceBundleUtil labels + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.MENU_PATH); + + this.setText(labels.getString("viewMenu.text")); + this.setToolTipText(labels.getString("viewMenu.tooltipText")); + this.setMnemonic('V'); + + // Menu item View -> Add course view + menuAddDrawingView = new JMenuItem(actionMap.get(AddDrawingViewAction.ID)); + add(menuAddDrawingView); + + // Menu item View -> Add transport order view + menuTransportOrderView = new JMenuItem(actionMap.get(AddTransportOrderViewAction.ID)); + add(menuTransportOrderView); + + // Menu item View -> Add transport order sequence view + menuOrderSequenceView = new JMenuItem(actionMap.get(AddTransportOrderSequenceViewAction.ID)); + add(menuOrderSequenceView); + + menuPeripheralJobView = new JMenuItem(actionMap.get(AddPeripheralJobViewAction.ID)); + add(menuPeripheralJobView); + + addSeparator(); + + // Menu item View -> Plugins + this.menuPluginPanels = menuPluginPanels; + menuPluginPanels.setOperationMode(OperationMode.OPERATING); + menuPluginPanels.setEnabled(false); + add(menuPluginPanels); + + // Menu item View -> Restore docking layout + menuItemRestoreDockingLayout = new JMenuItem(actionMap.get(RestoreDockingLayoutAction.ID)); + menuItemRestoreDockingLayout.setText( + labels.getString("viewMenu.menuItem_restoreWindowArrangement.text") + ); + add(menuItemRestoreDockingLayout); + + eventSource.subscribe(this); + } + + @Override + public void onEvent(Object event) { + if (event instanceof KernelStateChangeEvent kernelStateChangeEvent) { + handleKernelStateChangeEvent(kernelStateChangeEvent); + } + } + + private void handleKernelStateChangeEvent(KernelStateChangeEvent event) { + switch (event.getNewState()) { + case LOGGED_IN: + menuPluginPanels.setEnabled(true); + break; + case DISCONNECTED: + menuPluginPanels.setEnabled(false); + break; + default: + // Do nothing. + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/toolbar/MultipleSelectionTool.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/toolbar/MultipleSelectionTool.java new file mode 100644 index 0000000..2b17532 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/toolbar/MultipleSelectionTool.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.toolbar; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javax.swing.Action; +import javax.swing.JMenuItem; +import org.jhotdraw.draw.tool.DragTracker; +import org.jhotdraw.draw.tool.SelectAreaTracker; +import org.opentcs.guing.common.application.ApplicationState; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.toolbar.AbstractMultipleSelectionTool; + +/** + * The default selection tool. + */ +public class MultipleSelectionTool + extends + AbstractMultipleSelectionTool { + + /** + * Creates a new instance. + * + * @param appState Stores the application's current state. + * @param selectAreaTracker The tracker to be used for area selections in the drawing. + * @param dragTracker The tracker to be used for dragging figures. + * @param drawingActions Drawing-related actions for the popup menus created by this tool. + * @param selectionActions Selection-related actions for the popup menus created by this tool. + */ + @Inject + public MultipleSelectionTool( + ApplicationState appState, + SelectAreaTracker selectAreaTracker, + DragTracker dragTracker, + @Assisted("drawingActions") + Collection drawingActions, + @Assisted("selectionActions") + Collection selectionActions + ) { + super(appState, selectAreaTracker, dragTracker, drawingActions, selectionActions); + } + + @Override + public List customPopupMenuItems() { + return new ArrayList<>(); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/toolbar/SelectionToolFactory.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/toolbar/SelectionToolFactory.java new file mode 100644 index 0000000..9d3ac10 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/application/toolbar/SelectionToolFactory.java @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.application.toolbar; + +import com.google.inject.assistedinject.Assisted; +import java.util.Collection; +import javax.swing.Action; + +/** + */ +public interface SelectionToolFactory { + + MultipleSelectionTool createMultipleSelectionTool( + @Assisted("drawingActions") + Collection drawingActions, + @Assisted("selectionActions") + Collection selectionActions + ); +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/EditDriveOrderPanel.form b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/EditDriveOrderPanel.form new file mode 100644 index 0000000..ae88cab --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/EditDriveOrderPanel.form @@ -0,0 +1,506 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/EditDriveOrderPanel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/EditDriveOrderPanel.java new file mode 100644 index 0000000..9c004c4 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/EditDriveOrderPanel.java @@ -0,0 +1,249 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.dialogs; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import javax.swing.DefaultComboBoxModel; +import org.opentcs.guing.base.components.properties.type.StringSetProperty; +import org.opentcs.guing.base.model.AbstractConnectableModelComponent; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.common.components.dialogs.DialogContent; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Panel to edit a drive order. + */ +public class EditDriveOrderPanel + extends + DialogContent { + + /** + * Available locations. + */ + private final List fLocations; + /** + * The selected location. + */ + private AbstractConnectableModelComponent fSelectedLocation; + /** + * Selected action. + */ + private String fSelectedAction; + + /** + * Creates new form EditDriveOrderPanel + * + * @param locations available locations. + */ + @SuppressWarnings("this-escape") + public EditDriveOrderPanel(List locations) { + initComponents(); + fLocations = sortLocations(locations); + setDialogTitle( + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.CREATETO_PATH) + .getString("editDriverOrderPanel.create.title") + ); + } + + /** + * Creates new form EditDriveOrderPanel + * + * @param locations available locations. + * @param location selected location + * @param action selected action. + */ + @SuppressWarnings("this-escape") + public EditDriveOrderPanel( + List locations, + AbstractConnectableModelComponent location, String action + ) { + checkArgument( + location instanceof PointModel || location instanceof LocationModel, + String.format( + "Selected location has to be of type PointModel or LocationModel " + + "and not \"%s\".", location.getClass().getName() + ) + ); + initComponents(); + fLocations = sortLocations(locations); + fSelectedLocation = location; + fSelectedAction = action; + setDialogTitle( + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.CREATETO_PATH) + .getString("editDriverOrderPanel.edit.title") + ); + } + + /** + * Sorts a list of locations based on their name. + * + * @param locations list of locations to sort. + * @return The list of sorted locations. + */ + private List sortLocations(List locations) { + Comparator c = new Comparator() { + + @Override + public int compare(LocationModel o1, LocationModel o2) { + String s1 = o1.getName().toLowerCase(); + String s2 = o2.getName().toLowerCase(); + return s1.compareTo(s2); + } + }; + + List result = new ArrayList<>(locations); + Collections.sort(result, c); + + return result; + } + + @Override + public void update() { + } + + @Override + public void initFields() { + for (LocationModel s : fLocations) { + locationComboBox.addItem(s.getName()); + } + + if (fSelectedLocation != null) { + locationComboBox.setSelectedItem(fSelectedLocation.getName()); + } + else if (locationComboBox.getItemCount() > 0) { + locationComboBox.setSelectedIndex(0); + } + + if (fSelectedAction != null) { + actionComboBox.setSelectedItem(fSelectedAction); + } + } + + /** + * Returns the selected location. + * + * @return The selected location + */ + public Optional getSelectedLocation() { + int index = locationComboBox.getSelectedIndex(); + return index == -1 ? Optional.empty() : Optional.ofNullable(fLocations.get(index)); + } + + /** + * Returns the selected action. + * + * @return The selected action. + */ + public Optional getSelectedAction() { + return Optional.ofNullable((String) actionComboBox.getSelectedItem()); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + stationLabel = new javax.swing.JLabel(); + locationComboBox = new javax.swing.JComboBox<>(); + actionLabel = new javax.swing.JLabel(); + actionComboBox = new javax.swing.JComboBox<>(); + + java.awt.GridBagLayout layout = new java.awt.GridBagLayout(); + layout.columnWidths = new int[] {0, 5, 0}; + layout.rowHeights = new int[] {0, 5, 0}; + setLayout(layout); + + stationLabel.setFont(stationLabel.getFont()); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/operating/dialogs/createTransportOrder"); // NOI18N + stationLabel.setText(bundle.getString("editDriverOrderPanel.label_location.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 3); + add(stationLabel, gridBagConstraints); + + locationComboBox.setFont(locationComboBox.getFont()); + locationComboBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + locationComboBoxActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 0.5; + add(locationComboBox, gridBagConstraints); + + actionLabel.setFont(actionLabel.getFont()); + actionLabel.setText(bundle.getString("editDriverOrderPanel.label_action.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 3); + add(actionLabel, gridBagConstraints); + + actionComboBox.setFont(actionComboBox.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 0.5; + add(actionComboBox, gridBagConstraints); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + /** + * Updates the contents of the ComboBox with the allowed operations. + * + * @param evt the event. + */ + private void locationComboBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_locationComboBoxActionPerformed + DefaultComboBoxModel model + = (DefaultComboBoxModel) actionComboBox.getModel(); + model.removeAllElements(); + + getSelectedLocation().ifPresent(location -> { + LocationTypeModel type = location.getLocationType(); + StringSetProperty p = type.getPropertyAllowedOperations(); + for (String item : new ArrayList<>(type.getPropertyAllowedOperations().getItems())) { + model.addElement(item); + } + + if (model.getSize() > 0) { + actionComboBox.setSelectedIndex(0); + } + }); + }//GEN-LAST:event_locationComboBoxActionPerformed + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JComboBox actionComboBox; + private javax.swing.JLabel actionLabel; + private javax.swing.JComboBox locationComboBox; + private javax.swing.JLabel stationLabel; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/FindVehiclePanel.form b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/FindVehiclePanel.form new file mode 100644 index 0000000..5442288 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/FindVehiclePanel.form @@ -0,0 +1,59 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/FindVehiclePanel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/FindVehiclePanel.java new file mode 100644 index 0000000..a93b97e --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/FindVehiclePanel.java @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.dialogs; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javax.swing.JPanel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingView; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.operationsdesk.components.drawing.figures.VehicleFigure; + +/** + * Panel to select a Vehicle that will be searched for in the view. + */ +public class FindVehiclePanel + extends + JPanel { + + /** + * The list of existing vehicles. + */ + private final List fVehicles; + /** + * The view to show the found vehicle in. + */ + private final OpenTCSDrawingView fDrawingView; + /** + * The model manager. + */ + private final ModelManager modelManager; + + /** + * Creates a new instance. + * + * @param vehicles A list of existing vehicles. + * @param drawingView The view to show the found vehicle in. + * @param modelManager The model manager. + */ + @Inject + @SuppressWarnings("this-escape") + public FindVehiclePanel( + @Assisted + Collection vehicles, + @Assisted + OpenTCSDrawingView drawingView, + ModelManager modelManager + ) { + fVehicles = new ArrayList<>(requireNonNull(vehicles, "vehicles")); + fDrawingView = requireNonNull(drawingView, "drawingView"); + this.modelManager = requireNonNull(modelManager, "modelManager"); + + initComponents(); + + for (VehicleModel vehicle : vehicles) { + comboBoxVehicles.addItem(vehicle.getName()); + } + } + + /** + * Returns the selected vehicle. + * + * @return The selected vehicle. + */ + public VehicleModel getSelectedVehicle() { + int index = comboBoxVehicles.getSelectedIndex(); + + if (index == -1) { + return null; + } + + return fVehicles.get(index); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + + labelVehicles = new javax.swing.JLabel(); + comboBoxVehicles = new javax.swing.JComboBox<>(); + buttonFind = new javax.swing.JButton(); + + labelVehicles.setFont(labelVehicles.getFont()); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/operating/dialogs/findVehicle"); // NOI18N + labelVehicles.setText(bundle.getString("findVehiclePanel.label_vehicles.text")); // NOI18N + add(labelVehicles); + + comboBoxVehicles.setFont(comboBoxVehicles.getFont()); + add(comboBoxVehicles); + + buttonFind.setFont(buttonFind.getFont()); + buttonFind.setText(bundle.getString("findVehiclePanel.button_find.text")); // NOI18N + buttonFind.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + buttonFindActionPerformed(evt); + } + }); + add(buttonFind); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + /** + * Starts the search for the vehicle. + */ + private void buttonFindActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_buttonFindActionPerformed + VehicleModel vehicle = getSelectedVehicle(); + if (vehicle == null) { + return; + } + + VehicleFigure figure = (VehicleFigure) modelManager.getModel().getFigure(vehicle); + if (figure != null) { + fDrawingView.scrollTo(figure); + } + }//GEN-LAST:event_buttonFindActionPerformed + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton buttonFind; + private javax.swing.JComboBox comboBoxVehicles; + private javax.swing.JLabel labelVehicles; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/FindVehiclePanelFactory.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/FindVehiclePanelFactory.java new file mode 100644 index 0000000..9f1bb85 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/FindVehiclePanelFactory.java @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.dialogs; + +import java.util.Collection; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingView; + +/** + */ +public interface FindVehiclePanelFactory { + + /** + * Create a {@link FindVehiclePanel} for the given vehicle models. + * + * @param vehicles The vehicle models. + * @param drawingView The drawing view. + * @return A {@link FindVehiclePanel}. + */ + FindVehiclePanel createFindVehiclesPanel( + Collection vehicles, + OpenTCSDrawingView drawingView + ); +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/SingleVehicleView.form b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/SingleVehicleView.form new file mode 100644 index 0000000..5d335c2 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/SingleVehicleView.form @@ -0,0 +1,222 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/SingleVehicleView.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/SingleVehicleView.java new file mode 100644 index 0000000..ae23f25 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/SingleVehicleView.java @@ -0,0 +1,523 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.dialogs; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.geom.Point2D; +import java.util.Arrays; +import java.util.ResourceBundle; +import javax.swing.ImageIcon; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import org.jhotdraw.draw.Figure; +import org.opentcs.data.model.Vehicle; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.event.AttributesChangeListener; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; +import org.opentcs.guing.common.components.properties.SelectionPropertiesComponent; +import org.opentcs.guing.common.components.tree.ComponentsTreeViewManager; +import org.opentcs.guing.common.components.tree.TreeViewManager; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.operationsdesk.application.menus.MenuFactory; +import org.opentcs.operationsdesk.components.drawing.figures.VehicleFigure; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.operationsdesk.util.VehicleCourseObjectFactory; + +/** + * A single vehicle in the {@link VehiclesPanel}. + */ +public class SingleVehicleView + extends + JPanel + implements + AttributesChangeListener, + Comparable { + + /** + * The resource bundle this component uses. + */ + private static final ResourceBundle BUNDLE + = ResourceBundle.getBundle(I18nPlantOverviewOperating.VEHICLEVIEW_PATH); + /** + * The color definition for orange. + */ + private static final Color ORANGE = new Color(0xff, 0xdd, 0x75); + /** + * The color definition for green. + */ + private static final Color GREEN = new Color(0x77, 0xdb, 0x6c); + /** + * The Vehicle to be displayed. + */ + private final VehicleModel fVehicleModel; + /** + * The tree view's manager (for selecting the vehicle when it's clicked on). + */ + private final TreeViewManager treeViewManager; + /** + * The properties component (for displaying properties of the vehicle when + * it's clicked on). + */ + private final SelectionPropertiesComponent propertiesComponent; + /** + * The drawing editor (for accessing the currently active drawing view). + */ + private final OpenTCSDrawingEditor drawingEditor; + /** + * A factory for popup menus. + */ + private final MenuFactory menuFactory; + /** + * The model manager. + */ + private final ModelManager modelManager; + /** + * Panel to draw on. + */ + private final JPanel fVehicleView; + + /** + * Creates new instance. + * + * @param vehicle The vehicle to be displayed. + * @param treeViewManager The tree view's manager (for selecting the vehicle + * when it's clicked on). + * @param propertiesComponent The properties component (for displaying + * properties of the vehicle when it's clicked on). + * @param drawingEditor The drawing editor (for accessing the currently active + * drawing view). + * @param crsObjFactory A factory to create vehicle figures. + * @param menuFactory A factory for popup menus. + * @param modelManager The model manager. + */ + @Inject + @SuppressWarnings("this-escape") + public SingleVehicleView( + @Assisted + VehicleModel vehicle, + ComponentsTreeViewManager treeViewManager, + SelectionPropertiesComponent propertiesComponent, + OpenTCSDrawingEditor drawingEditor, + VehicleCourseObjectFactory crsObjFactory, + MenuFactory menuFactory, + ModelManager modelManager + ) { + this.fVehicleModel = requireNonNull(vehicle, "vehicle"); + this.treeViewManager = requireNonNull(treeViewManager, "treeViewManager"); + this.propertiesComponent = requireNonNull( + propertiesComponent, + "propertiesComponent" + ); + this.drawingEditor = requireNonNull(drawingEditor, "drawingEditor"); + this.menuFactory = requireNonNull(menuFactory, "menuFactory"); + this.modelManager = requireNonNull(modelManager, "modelManager"); + requireNonNull(crsObjFactory, "crsObjFactory"); + this.fVehicleView = new VehicleView( + fVehicleModel, + crsObjFactory.createVehicleFigure(fVehicleModel) + ); + + initComponents(); + + vehiclePanel.add(fVehicleView, BorderLayout.CENTER); + + vehicle.addAttributesChangeListener(this); + + vehicleLabel.setText(vehicle.getName()); + updateVehicle(); + } + + private void showPopup(int x, int y) { + menuFactory.createVehiclePopupMenu(Arrays.asList(fVehicleModel)).show(this, x, y); + } + + private void updateVehicle() { + updateVehicleIntegrationLevel(); + updateVehicleState(); + updateVehiclePosition(); + updateEnergyLevel(); + updateVehicleDestination(); + + revalidate(); + } + + private void updateVehicleDestination() { + PointModel destinationPoint = getVehicleModel().getDriveOrderDestination(); + if (destinationPoint != null) { + destinationValueLabel.setText(destinationPoint.getName()); + } + else { + destinationValueLabel.setText("-"); + } + } + + private void updateVehicleIntegrationLevel() { + Vehicle.IntegrationLevel integrationLevel + = (Vehicle.IntegrationLevel) fVehicleModel.getPropertyIntegrationLevel().getValue(); + switch (integrationLevel) { + case TO_BE_IGNORED: + case TO_BE_NOTICED: + integratedStateLabel.setText( + BUNDLE.getString("singleVehicleView.label_integratedState.no.text") + ); + integratedStateLabel.setOpaque(false); + break; + case TO_BE_RESPECTED: + integratedStateLabel.setText( + BUNDLE.getString("singleVehicleView.label_integratedState.partially.text") + ); + integratedStateLabel.setOpaque(true); + integratedStateLabel.setBackground(ORANGE); + break; + case TO_BE_UTILIZED: + integratedStateLabel.setText( + BUNDLE.getString("singleVehicleView.label_integratedState.fully.text") + ); + integratedStateLabel.setOpaque(true); + integratedStateLabel.setBackground(GREEN); + break; + default: + integratedStateLabel.setText(integrationLevel.name()); + integratedStateLabel.setOpaque(false); + } + } + + private void updateVehicleState() { + Vehicle.State state = (Vehicle.State) fVehicleModel.getPropertyState().getValue(); + + vehicleStateValueLabel.setText(state.toString()); + + switch (state) { + case ERROR: + case UNAVAILABLE: + case UNKNOWN: + vehicleStateValueLabel.setBackground(ORANGE); + vehicleStateValueLabel.setOpaque(true); + break; + default: + vehicleStateValueLabel.setOpaque(false); + } + } + + private void updateVehiclePosition() { + positionValueLabel.setText(fVehicleModel.getPropertyPoint().getText()); + } + + private void updateEnergyLevel() { + batteryLabel.setText(fVehicleModel.getPropertyEnergyLevel().getValue() + " %"); + Vehicle vehicle = fVehicleModel.getVehicle(); + + if (vehicle.isEnergyLevelCritical()) { + batteryIcon.setIcon( + new ImageIcon( + getToolkit().getImage( + getClass().getClassLoader().getResource( + "org/opentcs/guing/res/symbols/panel/battery-caution-3.png" + ) + ) + ) + ); + } + else if (vehicle.isEnergyLevelDegraded()) { + batteryIcon.setIcon( + new ImageIcon( + getToolkit().getImage( + getClass().getClassLoader().getResource( + "org/opentcs/guing/res/symbols/panel/battery-060-2.png" + ) + ) + ) + ); + } + else if (vehicle.isEnergyLevelGood()) { + batteryIcon.setIcon( + new ImageIcon( + getToolkit().getImage( + getClass().getClassLoader().getResource( + "org/opentcs/guing/res/symbols/panel/battery-100-2.png" + ) + ) + ) + ); + } + } + + public VehicleModel getVehicleModel() { + return fVehicleModel; + } + + @Override + public void propertiesChanged(AttributesChangeEvent e) { + updateVehicle(); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + statusPanel = new javax.swing.JPanel(); + vehiclePanel = new javax.swing.JPanel(); + vehicleLabel = new javax.swing.JLabel(); + batteryPanel = new javax.swing.JPanel(); + batteryIcon = new javax.swing.JLabel(); + batteryLabel = new javax.swing.JLabel(); + propertiesPanel = new javax.swing.JPanel(); + integratedLabel = new javax.swing.JLabel(); + integratedStateLabel = new javax.swing.JLabel(); + vehicleStateLabel = new javax.swing.JLabel(); + vehicleStateValueLabel = new javax.swing.JLabel(); + positionLabel = new javax.swing.JLabel(); + positionValueLabel = new javax.swing.JLabel(); + destinationLabel = new javax.swing.JLabel(); + destinationValueLabel = new javax.swing.JLabel(); + fillLabel = new javax.swing.JLabel(); + + setMinimumSize(new java.awt.Dimension(200, 59)); + setLayout(new java.awt.GridBagLayout()); + + statusPanel.setLayout(new java.awt.BorderLayout()); + + vehiclePanel.setLayout(new java.awt.BorderLayout()); + + vehicleLabel.setFont(vehicleLabel.getFont()); + vehicleLabel.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + vehiclePanel.add(vehicleLabel, java.awt.BorderLayout.NORTH); + + statusPanel.add(vehiclePanel, java.awt.BorderLayout.CENTER); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + add(statusPanel, gridBagConstraints); + + batteryPanel.setMinimumSize(new java.awt.Dimension(20, 14)); + batteryPanel.setPreferredSize(new java.awt.Dimension(45, 14)); + batteryPanel.setLayout(new java.awt.GridBagLayout()); + batteryPanel.add(batteryIcon, new java.awt.GridBagConstraints()); + + batteryLabel.setText("battery"); + batteryLabel.setPreferredSize(new java.awt.Dimension(45, 14)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.insets = new java.awt.Insets(0, 5, 0, 0); + batteryPanel.add(batteryLabel, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(14, 0, 0, 0); + add(batteryPanel, gridBagConstraints); + + propertiesPanel.setLayout(new java.awt.GridBagLayout()); + + integratedLabel.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/operating/panels/vehicleView"); // NOI18N + integratedLabel.setText(bundle.getString("singleVehicleView.label_integrated.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + propertiesPanel.add(integratedLabel, gridBagConstraints); + + integratedStateLabel.setText(bundle.getString("singleVehicleView.label_integratedState.no.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + propertiesPanel.add(integratedStateLabel, gridBagConstraints); + + vehicleStateLabel.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + vehicleStateLabel.setText(bundle.getString("singleVehicleView.label_state.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 0); + propertiesPanel.add(vehicleStateLabel, gridBagConstraints); + + vehicleStateValueLabel.setText("UNAVAILABLE"); + vehicleStateValueLabel.setToolTipText(""); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 0); + propertiesPanel.add(vehicleStateValueLabel, gridBagConstraints); + + positionLabel.setText(bundle.getString("singleVehicleView.label_position.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 0); + propertiesPanel.add(positionLabel, gridBagConstraints); + + positionValueLabel.setText("-"); + positionValueLabel.setMaximumSize(new java.awt.Dimension(68, 14)); + positionValueLabel.setMinimumSize(new java.awt.Dimension(68, 14)); + positionValueLabel.setPreferredSize(new java.awt.Dimension(68, 14)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 0); + propertiesPanel.add(positionValueLabel, gridBagConstraints); + + destinationLabel.setText(bundle.getString("singleVehicleView.label_destination.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 0); + propertiesPanel.add(destinationLabel, gridBagConstraints); + + destinationValueLabel.setText("-"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 0); + propertiesPanel.add(destinationValueLabel, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + add(propertiesPanel, gridBagConstraints); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.weightx = 1.0; + add(fillLabel, gridBagConstraints); + }// //GEN-END:initComponents + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel batteryIcon; + private javax.swing.JLabel batteryLabel; + private javax.swing.JPanel batteryPanel; + private javax.swing.JLabel destinationLabel; + private javax.swing.JLabel destinationValueLabel; + private javax.swing.JLabel fillLabel; + private javax.swing.JLabel integratedLabel; + private javax.swing.JLabel integratedStateLabel; + private javax.swing.JLabel positionLabel; + private javax.swing.JLabel positionValueLabel; + private javax.swing.JPanel propertiesPanel; + private javax.swing.JPanel statusPanel; + private javax.swing.JLabel vehicleLabel; + private javax.swing.JPanel vehiclePanel; + private javax.swing.JLabel vehicleStateLabel; + private javax.swing.JLabel vehicleStateValueLabel; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON + + @Override + public int compareTo(SingleVehicleView o) { + return fVehicleModel.getName().compareTo(o.getVehicleModel().getName()); + } + + private class VehicleView + extends + JPanel + implements + AttributesChangeListener { + + private final VehicleFigure figure; + + VehicleView(VehicleModel vehicleModel, VehicleFigure figure) { + this.figure = requireNonNull(figure, "figure"); + requireNonNull(vehicleModel, "vehicleModel"); + + vehicleModel.addAttributesChangeListener(this); + + setBackground(Color.WHITE); + + Rectangle r = figure.getBounds().getBounds(); + r.grow(10, 10); + setPreferredSize(new Dimension(r.width, r.height)); + + addMouseListener(new VehicleMouseAdapter(vehicleModel)); + } + + @Override + public void propertiesChanged(AttributesChangeEvent e) { + figure.propertiesChanged(e); + + // Because the figure is not part of any drawing it does not automatically redraw itself. + SwingUtilities.invokeLater(() -> { + this.repaint(); + }); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + drawVehicle((Graphics2D) g); + } + + /** + * Draws the vehicle figure into the dialog. + * + * @param g2d The graphics context. + */ + private void drawVehicle(Graphics2D g2d) { + figure.setIgnorePrecisePosition(true); + Point2D.Double posDialog = new Point2D.Double( + fVehicleView.getWidth() / 2, + fVehicleView.getHeight() / 2 + ); + figure.setBounds(posDialog, null); + figure.setAngle(0.0); + figure.forcedDraw(g2d); + } + } + + private class VehicleMouseAdapter + extends + MouseAdapter { + + private final VehicleModel vehicleModel; + + VehicleMouseAdapter(VehicleModel vehicleModel) { + this.vehicleModel = requireNonNull(vehicleModel, "vehicleModel"); + } + + @Override + public void mouseClicked(MouseEvent evt) { + if (evt.getButton() == MouseEvent.BUTTON1) { + treeViewManager.selectItem(vehicleModel); + propertiesComponent.setModel(vehicleModel); + } + + if (evt.getClickCount() == 2) { + Figure vehicleFigure = modelManager.getModel().getFigure(vehicleModel); + drawingEditor.getActiveView().scrollTo(vehicleFigure); + } + + if (evt.getButton() == MouseEvent.BUTTON3) { + showPopup(evt.getX(), evt.getY()); + } + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/SingleVehicleViewFactory.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/SingleVehicleViewFactory.java new file mode 100644 index 0000000..c66cb9c --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/SingleVehicleViewFactory.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.dialogs; + +import org.opentcs.guing.base.model.elements.VehicleModel; + +/** + */ +public interface SingleVehicleViewFactory { + + /** + * Creates a new SingleVehicleView for the given model. + * + * @param vehicleModel The vehicle model. + * @return A new SingleVehicleView for the given model. + */ + SingleVehicleView createSingleVehicleView(VehicleModel vehicleModel); +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/VehiclesPanel.form b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/VehiclesPanel.form new file mode 100644 index 0000000..13dcc59 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/VehiclesPanel.form @@ -0,0 +1,42 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/VehiclesPanel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/VehiclesPanel.java new file mode 100644 index 0000000..64ddb15 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dialogs/VehiclesPanel.java @@ -0,0 +1,180 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.dialogs; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.util.Collection; +import java.util.SortedSet; +import java.util.TreeSet; +import javax.swing.JPanel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.dialogs.ModifiedFlowLayout; +import org.opentcs.guing.common.event.OperationModeChangeEvent; +import org.opentcs.guing.common.event.SystemModelTransitionEvent; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.util.event.EventHandler; + +/** + * Shows every vehicle available in the system in a panel. + */ +public class VehiclesPanel + extends + JPanel + implements + EventHandler { + + /** + * Provides the current system model. + */ + private final ModelManager modelManager; + /** + * A factory for vehicle views. + */ + private final SingleVehicleViewFactory vehicleViewFactory; + /** + * The vehicle views sorted. + */ + private final SortedSet vehicleViews = new TreeSet<>(); + + /** + * Creates a new instance. + * + * @param modelManager Provides the current system model. + * @param vehicleViewFactory A factory for vehicle views. + */ + @Inject + VehiclesPanel( + ModelManager modelManager, + SingleVehicleViewFactory vehicleViewFactory + ) { + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.vehicleViewFactory = requireNonNull( + vehicleViewFactory, + "vehicleViewFactory" + ); + + initComponents(); + setPreferredSize(new Dimension(0, 97)); + setMinimumSize(new Dimension(140, 120)); + panelVehicles.setLayout(new ModifiedFlowLayout(FlowLayout.LEFT, 10, 10)); + } + + @Override + public void onEvent(Object event) { + if (event instanceof OperationModeChangeEvent) { + handleModeChange((OperationModeChangeEvent) event); + } + if (event instanceof SystemModelTransitionEvent) { + handleSystemModelTransition((SystemModelTransitionEvent) event); + } + } + + private void handleModeChange(OperationModeChangeEvent evt) { + switch (evt.getNewMode()) { + case OPERATING: + setVehicleModels(modelManager.getModel().getVehicleModels()); + break; + case MODELLING: + default: + clearVehicles(); + } + } + + /** + * Initializes this panel with the current vehicles. + * + * @param vehicleModels The vehicle models. + */ + public void setVehicleModels(Collection vehicleModels) { + // Remove vehicles of the previous model from panel + for (SingleVehicleView vehicleView : vehicleViews) { + panelVehicles.remove(vehicleView); + } + + // Remove vehicles of the previous model from list + vehicleViews.clear(); + // Add vehicles of actual model to list + for (VehicleModel vehicle : vehicleModels) { + vehicleViews.add(vehicleViewFactory.createSingleVehicleView(vehicle)); + } + + // Add vehicles of actual model to panel, sorted by name + for (SingleVehicleView vehicleView : vehicleViews) { + panelVehicles.add(vehicleView); + } + + panelVehicles.revalidate(); + } + + /** + * Clears the vehicles in this panel. + */ + public void clearVehicles() { + for (SingleVehicleView vehicleView : vehicleViews) { + panelVehicles.remove(vehicleView); + } + vehicleViews.clear(); + repaint(); + } + + @Override + public void repaint() { + super.repaint(); + + if (vehicleViews != null) { + for (SingleVehicleView view : vehicleViews) { + view.repaint(); + } + } + } + + private void handleSystemModelTransition(SystemModelTransitionEvent evt) { + switch (evt.getStage()) { + case UNLOADING: + clearVehicles(); + break; + case LOADED: + setVehicleModels(modelManager.getModel().getVehicleModels()); + break; + default: + // Do nada. + } + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + + scrollPaneVehicles = new javax.swing.JScrollPane(); + panelVehicles = new javax.swing.JPanel(); + + setName("VehiclesPanel"); // NOI18N + setLayout(new java.awt.GridLayout(1, 0)); + + scrollPaneVehicles.setViewportView(panelVehicles); + + add(scrollPaneVehicles); + + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/operating/panels/vehicleView"); // NOI18N + getAccessibleContext().setAccessibleName(bundle.getString("vehiclesPanel.title")); // NOI18N + getAccessibleContext().setAccessibleDescription(""); + }// //GEN-END:initComponents + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel panelVehicles; + private javax.swing.JScrollPane scrollPaneVehicles; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dockable/DockingManagerOperating.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dockable/DockingManagerOperating.java new file mode 100644 index 0000000..7a59fa8 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/dockable/DockingManagerOperating.java @@ -0,0 +1,220 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.dockable; + +import static java.util.Objects.requireNonNull; + +import bibliothek.gui.dock.common.CControl; +import bibliothek.gui.dock.common.CGrid; +import bibliothek.gui.dock.common.DefaultSingleCDockable; +import bibliothek.gui.dock.common.group.CGroupBehavior; +import jakarta.inject.Inject; +import javax.swing.JComponent; +import javax.swing.JFrame; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.guing.common.components.dockable.AbstractDockingManager; +import org.opentcs.guing.common.components.dockable.CStack; +import org.opentcs.guing.common.components.properties.SelectionPropertiesComponent; +import org.opentcs.guing.common.components.tree.BlocksTreeViewManager; +import org.opentcs.guing.common.components.tree.ComponentsTreeViewManager; +import org.opentcs.operationsdesk.components.dialogs.VehiclesPanel; +import org.opentcs.operationsdesk.components.layer.LayerGroupsPanel; +import org.opentcs.operationsdesk.components.layer.LayersPanel; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Utility class for working with dockables. + */ +public class DockingManagerOperating + extends + AbstractDockingManager { + + /** + * ID for the tab pane that contains the course, transport orders and order sequences. + */ + public static final String COURSE_TAB_PANE_ID = "course_tab_pane"; + /** + * ID for the dockable that contains the VehiclePanel. + */ + public static final String VEHICLES_DOCKABLE_ID = "vehicles_dock"; + /** + * ID for the tab pane that contains the components, blocks and groups. + */ + private static final String TREE_TAB_PANE_ID = "tree_tab_pane"; + private static final String LAYER_TAB_PANE_ID = "layer_tab_pane"; + private static final String COMPONENTS_ID = "comp_dock"; + private static final String BLOCKS_ID = "block_dock"; + private static final String PROPERTIES_ID = "properties_id"; + private static final String STATUS_ID = "status_id"; + private static final String LAYERS_ID = "layers_id"; + private static final String LAYER_GROUPS_ID = "layer_groups_id"; + /** + * The panel showing every vehicle available in the system. + */ + private final VehiclesPanel vehiclesPanel; + /** + * The tree view manager for components. + */ + private final ComponentsTreeViewManager componentsTreeViewManager; + /** + * The tree view manager for blocks. + */ + private final BlocksTreeViewManager blocksTreeViewManager; + /** + * The panel displaying the properties of the currently selected driving course components. + */ + private final SelectionPropertiesComponent selectionPropertiesComponent; + /** + * The panel displaying the layers in the plant model. + */ + private final LayersPanel layersPanel; + /** + * The panel displaying the layer groups in the plant model. + */ + private final LayerGroupsPanel layerGroupsPanel; + /** + * Tab pane that contains the components, blocks and groups. + */ + private CStack treeTabPane; + /** + * Tab pane that contains the course, transport orders and order sequences. + */ + private CStack courseTabPane; + /** + * Tab pane that contains layers and layer groups. + */ + private CStack layerTabPane; + + /** + * Creates a new instance. + * + * @param applicationFrame The application's main frame. + * @param vehiclesPanel The panel showing every vehicle available in the system. + * @param componentsTreeViewManager The tree view manager for components. + * @param blocksTreeViewManager The tree view manager for blocks. + * @param selectionPropertiesComponent The panel displaying the properties of the currently + * selected driving course components. + * @param layersPanel The panel displaying the layers in the plant model. + * @param layerGroupsPanel The panel displaying the layer groups in the plant model. + */ + @Inject + public DockingManagerOperating( + @ApplicationFrame + JFrame applicationFrame, + VehiclesPanel vehiclesPanel, + ComponentsTreeViewManager componentsTreeViewManager, + BlocksTreeViewManager blocksTreeViewManager, + SelectionPropertiesComponent selectionPropertiesComponent, + LayersPanel layersPanel, + LayerGroupsPanel layerGroupsPanel + ) { + super(new CControl(applicationFrame)); + this.vehiclesPanel = requireNonNull(vehiclesPanel, "vehiclesPanel"); + this.componentsTreeViewManager = requireNonNull( + componentsTreeViewManager, + "componentsTreeViewManager" + ); + this.blocksTreeViewManager = requireNonNull(blocksTreeViewManager, "blocksTreeViewManager"); + this.selectionPropertiesComponent = requireNonNull( + selectionPropertiesComponent, + "selectionPropertiesComponent" + ); + this.layersPanel = requireNonNull(layersPanel, "layersPanel"); + this.layerGroupsPanel = requireNonNull(layerGroupsPanel, "layerGroupsPanel"); + } + + @Override + public void reset() { + removeDockable(VEHICLES_DOCKABLE_ID); + removeDockable(BLOCKS_ID); + removeDockable(COMPONENTS_ID); + removeDockable(PROPERTIES_ID); + removeDockable(STATUS_ID); + removeDockable(LAYERS_ID); + removeDockable(LAYER_GROUPS_ID); + getCControl().removeStation(getTabPane(COURSE_TAB_PANE_ID)); + getCControl().removeStation(getTabPane(TREE_TAB_PANE_ID)); + getCControl().removeStation(getTabPane(LAYER_TAB_PANE_ID)); + } + + @Override + public void initializeDockables() { + getCControl().setGroupBehavior(CGroupBehavior.TOPMOST); + + // Disable keyboard shortcuts to avoid collisions. + getCControl().putProperty(CControl.KEY_GOTO_NORMALIZED, null); + getCControl().putProperty(CControl.KEY_GOTO_EXTERNALIZED, null); + getCControl().putProperty(CControl.KEY_GOTO_MAXIMIZED, null); + getCControl().putProperty(CControl.KEY_MAXIMIZE_CHANGE, null); + + ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.DOCKABLE_PATH); + CGrid grid = new CGrid(getCControl()); + courseTabPane = new CStack(COURSE_TAB_PANE_ID); + addTabPane(COURSE_TAB_PANE_ID, courseTabPane); + DefaultSingleCDockable vehiclesDockable + = createDockable( + VEHICLES_DOCKABLE_ID, + vehiclesPanel.getAccessibleContext().getAccessibleName(), + vehiclesPanel, + false + ); + treeTabPane = new CStack(TREE_TAB_PANE_ID); + addTabPane(TREE_TAB_PANE_ID, treeTabPane); + layerTabPane = new CStack(LAYER_TAB_PANE_ID); + addTabPane(LAYER_TAB_PANE_ID, layerTabPane); + DefaultSingleCDockable treeViewDock + = createDockable( + COMPONENTS_ID, + bundle.getString("dockingManagerOperating.panel_components.title"), + (JComponent) componentsTreeViewManager.getTreeView(), + false + ); + DefaultSingleCDockable treeBlocks + = createDockable( + BLOCKS_ID, + bundle.getString("dockingManagerOperating.panel_blocks.title"), + (JComponent) blocksTreeViewManager.getTreeView(), + false + ); + + grid.add(0, 0, 250, 400, treeTabPane); + grid.add( + 0, 400, 250, 300, + createDockable( + PROPERTIES_ID, + bundle.getString("dockingManagerOperating.panel_properties.title"), + selectionPropertiesComponent, + false + ) + ); + DefaultSingleCDockable layersDock + = createDockable( + LAYERS_ID, + bundle.getString("dockingManagerOperating.panel_layers.title"), + layersPanel, + false + ); + DefaultSingleCDockable layerGroupsDock + = createDockable( + LAYER_GROUPS_ID, + bundle.getString("dockingManagerOperating.panel_layerGroups.title"), + layerGroupsPanel, + false + ); + grid.add(0, 900, 250, 300, layerTabPane); + grid.add(250, 0, 150, 500, vehiclesDockable); + grid.add(400, 0, 1000, 500, courseTabPane); + + getCControl().getContentArea().deploy(grid); + + // init tab panes + addTabTo(treeViewDock, TREE_TAB_PANE_ID, 0); + addTabTo(treeBlocks, TREE_TAB_PANE_ID, 1); + addTabTo(layersDock, LAYER_TAB_PANE_ID, 0); + addTabTo(layerGroupsDock, LAYER_TAB_PANE_ID, 1); + treeTabPane.getStation().setFrontDockable(treeViewDock.intern()); + layerTabPane.getStation().setFrontDockable(layersDock.intern()); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/DrawingViewFactory.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/DrawingViewFactory.java new file mode 100644 index 0000000..964b9a2 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/DrawingViewFactory.java @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.drawing; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import javax.swing.JToggleButton; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.application.StatusPanel; +import org.opentcs.guing.common.components.drawing.DrawingOptions; +import org.opentcs.guing.common.components.drawing.DrawingViewPlacardPanel; +import org.opentcs.guing.common.components.drawing.DrawingViewScrollPane; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingView; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.operationsdesk.exchange.TransportOrderUtil; + +/** + * A factory for drawing views. + */ +public class DrawingViewFactory { + + /** + * A provider for drawing views. + */ + private final Provider drawingViewProvider; + /** + * The drawing editor. + */ + private final OpenTCSDrawingEditor drawingEditor; + /** + * The status panel to display the current mouse position in. + */ + private final StatusPanel statusPanel; + /** + * The manager keeping/providing the currently loaded model. + */ + private final ModelManager modelManager; + /** + * A helper for creating transport orders. + */ + private final TransportOrderUtil orderUtil; + /** + * The drawing options. + */ + private final DrawingOptions drawingOptions; + + @Inject + public DrawingViewFactory( + Provider drawingViewProvider, + OpenTCSDrawingEditor drawingEditor, + StatusPanel statusPanel, + ModelManager modelManager, + TransportOrderUtil orderUtil, + DrawingOptions drawingOptions + ) { + this.drawingViewProvider = requireNonNull(drawingViewProvider, "drawingViewProvider"); + this.drawingEditor = requireNonNull(drawingEditor, "drawingEditor"); + this.statusPanel = requireNonNull(statusPanel, "statusPanel"); + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.orderUtil = requireNonNull(orderUtil, "orderUtil"); + this.drawingOptions = requireNonNull(drawingOptions, "drawingOptions"); + } + + /** + * Creates and returns a new drawing view along with its placard panel, both + * wrapped in a scroll pane. + * + * @param systemModel The system model. + * @param selectionToolButton The selection tool button in the tool bar. + * @param dragToolButton The drag tool button in the tool bar. + * @return A new drawing view, wrapped in a scroll pane. + */ + public DrawingViewScrollPane createDrawingView( + SystemModel systemModel, + JToggleButton selectionToolButton, + JToggleButton dragToolButton + ) { + requireNonNull(systemModel, "systemModel"); + requireNonNull(selectionToolButton, "selectionToolButton"); + requireNonNull(dragToolButton, "dragToolButton"); + + OpenTCSDrawingView drawingView = drawingViewProvider.get(); + drawingEditor.add(drawingView); + drawingEditor.setActiveView(drawingView); + for (VehicleModel vehicle : systemModel.getVehicleModels()) { + drawingView.displayDriveOrders(vehicle, vehicle.getDisplayDriveOrders()); + } + + DrawingViewPlacardPanel placardPanel = new DrawingViewPlacardPanel(drawingView, drawingOptions); + + DrawingViewScrollPane scrollPane = new DrawingViewScrollPane(drawingView, placardPanel); + scrollPane.originChanged(systemModel.getDrawingMethod().getOrigin()); + + // --- Listens to draggings in the drawing --- + ViewDragScrollListener dragScrollListener + = new ViewDragScrollListener( + scrollPane, + placardPanel.getZoomComboBox(), + selectionToolButton, + dragToolButton, + statusPanel, + modelManager, + orderUtil + ); + drawingView.addMouseListener(dragScrollListener); + drawingView.addMouseMotionListener(dragScrollListener); + drawingView.getComponent().addMouseWheelListener(dragScrollListener); + + return scrollPane; + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/OpenTCSDrawingEditorOperating.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/OpenTCSDrawingEditorOperating.java new file mode 100644 index 0000000..b72724f --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/OpenTCSDrawingEditorOperating.java @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.drawing; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.List; +import javax.swing.SwingUtilities; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.event.NullAttributesChangeListener; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingView; +import org.opentcs.guing.common.event.SystemModelTransitionEvent; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.operationsdesk.components.drawing.figures.NamedVehicleFigure; +import org.opentcs.operationsdesk.util.VehicleCourseObjectFactory; + +/** + * The DrawingEditor coordinates DrawingViews + * and the Drawing. + * It also offers methods to add specific unique figures to the + * Drawing. + */ +public class OpenTCSDrawingEditorOperating + extends + OpenTCSDrawingEditor { + + /** + * Provides the current system model. + */ + private final ModelManager modelManager; + /** + * A factory for course objects. + */ + private final VehicleCourseObjectFactory courseObjectFactory; + + /** + * Creates a new instance. + * + * @param courseObjectFactory A factory for course objects. + * @param modelManager Provides the current system model. + */ + @Inject + public OpenTCSDrawingEditorOperating( + VehicleCourseObjectFactory courseObjectFactory, + ModelManager modelManager + ) { + super(courseObjectFactory); + this.courseObjectFactory = requireNonNull(courseObjectFactory, "courseObjectFactory"); + this.modelManager = requireNonNull(modelManager, "modelManager"); + } + + @Override + public void onEvent(Object event) { + if (event instanceof SystemModelTransitionEvent) { + handleSystemModelTransition((SystemModelTransitionEvent) event); + } + } + + private void handleSystemModelTransition(SystemModelTransitionEvent evt) { + switch (evt.getStage()) { + case UNLOADING: + // XXX Remove vehicles? + break; + case LOADED: + setVehicles(modelManager.getModel().getVehicleModels()); +// initializeOffsetFigures(); + break; + default: + // Do nada. + } + } + + /** + * Adds the given vehicles to the drawing. + * + * @param vehicleModels The VehicleModels to add. + */ + public void setVehicles(List vehicleModels) { + for (VehicleModel vehicleComp : vehicleModels) { + addVehicle(vehicleComp); + } + } + + /** + * Adds a vehicle to the drawing. + * + * @param vehicleModel The vehicle model to add. + */ + public void addVehicle(VehicleModel vehicleModel) { + NamedVehicleFigure vehicleFigure + = courseObjectFactory.createNamedVehicleFigure(vehicleModel); + + SwingUtilities.invokeLater(() -> getDrawing().add(vehicleFigure)); + + vehicleModel.addAttributesChangeListener(vehicleFigure); + modelManager.getModel().registerFigure(vehicleModel, vehicleFigure); + + vehicleModel.setDisplayDriveOrders(true); + for (OpenTCSDrawingView view : getAllViews()) { + view.displayDriveOrders(vehicleModel, true); + } + vehicleFigure.propertiesChanged( + new AttributesChangeEvent(new NullAttributesChangeListener(), vehicleModel) + ); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/ViewDragScrollListener.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/ViewDragScrollListener.java new file mode 100644 index 0000000..b41476d --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/ViewDragScrollListener.java @@ -0,0 +1,408 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.drawing; + +import static java.util.Objects.requireNonNull; + +import java.awt.Container; +import java.awt.Cursor; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseWheelEvent; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.Set; +import javax.swing.JComboBox; +import javax.swing.JToggleButton; +import javax.swing.JViewport; +import javax.swing.SwingUtilities; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.Figure; +import org.opentcs.data.model.Vehicle; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.application.StatusPanel; +import org.opentcs.guing.common.components.drawing.DrawingViewScrollPane; +import org.opentcs.guing.common.components.drawing.ZoomItem; +import org.opentcs.guing.common.components.drawing.figures.FigureConstants; +import org.opentcs.guing.common.components.drawing.figures.LabeledLocationFigure; +import org.opentcs.guing.common.components.drawing.figures.LabeledPointFigure; +import org.opentcs.guing.common.components.drawing.figures.PathConnection; +import org.opentcs.guing.common.components.drawing.figures.liner.TripleBezierLiner; +import org.opentcs.guing.common.components.drawing.figures.liner.TupelBezierLiner; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.operationsdesk.components.drawing.figures.VehicleFigure; +import org.opentcs.operationsdesk.exchange.TransportOrderUtil; +import org.opentcs.operationsdesk.util.Cursors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A listener for dragging of the drawing view and single objects inside the + * view. + */ +public class ViewDragScrollListener + extends + MouseAdapter { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ViewDragScrollListener.class); + /** + * The scroll pane enclosing the drawing view. + */ + private final DrawingViewScrollPane scrollPane; + /** + * The combo box for selecting the zoom level. + */ + private final JComboBox zoomComboBox; + /** + * The button for enabling object selection. + */ + private final JToggleButton selectionTool; + /** + * The button for enabling dragging. + */ + private final JToggleButton dragTool; + /** + * The status panel to display the current mouse position in. + */ + private final StatusPanel statusPanel; + /** + * The manager keeping/providing the currently loaded model. + */ + private final ModelManager modelManager; + /** + * A helper for creating transport orders. + */ + private final TransportOrderUtil orderUtil; + /** + * A default cursor for the drawing view. + */ + private final Cursor defaultCursor; + /** + * The start position of drag movements. + */ + private final Point startPoint = new Point(); + /** + * Start coordinate for measuring. + * XXX Is this redundant, or is it used for something different than startPoint? + */ + private final Point2D.Double fMouseStartPoint = new Point2D.Double(); + /** + * Current coordinate for measuring. + */ + private final Point2D.Double fMouseCurrentPoint = new Point2D.Double(); + /** + * End coordinate for measuring. + */ + private final Point2D.Double fMouseEndPoint = new Point2D.Double(); + /** + * The figure a user may have pressed on / want to drag. + */ + private Figure pressedFigure; + + /** + * Creates a new instance. + * + * @param scrollPane The scroll pane enclosing the drawing view. + * @param zoomComboBox The combo box for selecting the zoom level. + * @param selectionTool The button for enabling object selection. + * @param dragTool The button for enabling dragging. + * @param statusPanel The status panel to display the current mouse position in. + * @param modelManager The manager keeping/providing the currently loaded model. + * @param orderUtil A helper for creating transport orders. + */ + public ViewDragScrollListener( + DrawingViewScrollPane scrollPane, + JComboBox zoomComboBox, + JToggleButton selectionTool, + JToggleButton dragTool, + StatusPanel statusPanel, + ModelManager modelManager, + TransportOrderUtil orderUtil + ) { + this.scrollPane = requireNonNull(scrollPane, "scrollPane"); + this.zoomComboBox = requireNonNull(zoomComboBox, "zoomComboBox"); + this.selectionTool = requireNonNull(selectionTool, "selectionTool"); + this.dragTool = requireNonNull(dragTool, "dragTool"); + this.statusPanel = requireNonNull(statusPanel, "statusPanel"); + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.orderUtil = requireNonNull(orderUtil, "orderUtil"); + this.defaultCursor = scrollPane.getDrawingView().getComponent().getCursor(); + } + + @Override + public void mouseDragged(final MouseEvent evt) { + final DrawingView drawingView = scrollPane.getDrawingView(); + if (vehicleDragged()) { + drawingView.setCursor(Cursors.getDragVehicleCursor()); + } + + if (!(drawingView.getComponent().getParent() instanceof JViewport)) { + return; + } + + final JViewport viewport = (JViewport) drawingView.getComponent().getParent(); + Point cp = SwingUtilities.convertPoint(drawingView.getComponent(), evt.getPoint(), viewport); + + if (dragTool.isSelected()) { + int dx = startPoint.x - cp.x; + int dy = startPoint.y - cp.y; + Point vp = viewport.getViewPosition(); + vp.translate(dx, dy); + drawingView.getComponent().scrollRectToVisible(new Rectangle(vp, viewport.getSize())); + } + else { // The selection tool is selected + viewport.revalidate(); + + if (isMovableFigure(pressedFigure)) { + if (!isFigureCompletelyInView(pressedFigure, viewport, drawingView)) { + // If the figure exceeds the current view, start scrolling as soon as the mouse is + // hitting the view bounds. + drawingView.getComponent().scrollRectToVisible( + new Rectangle(evt.getX(), evt.getY(), 1, 1) + ); + } + + fMouseCurrentPoint.setLocation(drawingView.viewToDrawing(evt.getPoint())); + showPositionStatus(false); + startPoint.setLocation(cp); + } + } + SwingUtilities.invokeLater(() -> { + Rectangle2D.Double drawingArea = drawingView.getDrawing().getDrawingArea(); + scrollPane.getHorizontalRuler().setPreferredWidth((int) drawingArea.width); + scrollPane.getVerticalRuler().setPreferredHeight((int) drawingArea.height); + }); + } + + private boolean isMovableFigure(Figure figure) { + return (figure instanceof LabeledPointFigure) + || (figure instanceof LabeledLocationFigure) + || ((figure instanceof PathConnection) + && (((PathConnection) figure).getLiner() instanceof TupelBezierLiner)) + || ((figure instanceof PathConnection) + && (((PathConnection) figure).getLiner() instanceof TripleBezierLiner)); + } + + private boolean isFigureCompletelyInView( + Figure figure, + JViewport viewport, + DrawingView drawingView + ) { + Rectangle viewPortBounds = viewport.getViewRect(); + Rectangle figureBounds = drawingView.drawingToView(figure.getDrawingArea()); + + return (figureBounds.getMinX() > viewPortBounds.getMinX()) + && (figureBounds.getMinY() > viewPortBounds.getMinY()) + && (figureBounds.getMaxX() < viewPortBounds.getMaxX()) + && (figureBounds.getMaxY() < viewPortBounds.getMaxY()); + } + + @Override + public void mousePressed(MouseEvent evt) { + final DrawingView drawingView = scrollPane.getDrawingView(); + if (dragIsSelected()) { + drawingView.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); + } + Container c = drawingView.getComponent().getParent(); + if (c instanceof JViewport) { + JViewport viewPort = (JViewport) c; + Point cp = SwingUtilities.convertPoint(drawingView.getComponent(), evt.getPoint(), viewPort); + startPoint.setLocation(cp); + } + pressedFigure = drawingView.findFigure(evt.getPoint()); + fMouseCurrentPoint.setLocation(drawingView.viewToDrawing(evt.getPoint())); + fMouseStartPoint.setLocation(drawingView.viewToDrawing(evt.getPoint())); + showPositionStatus(false); + } + + @Override + public void mouseReleased(MouseEvent evt) { + if (dragIsSelected()) { + return; + } + + final DrawingView drawingView = scrollPane.getDrawingView(); + Figure fig = drawingView.findFigure(evt.getPoint()); + if (fig instanceof LabeledPointFigure) { + createPossibleTransportOrder((LabeledPointFigure) fig, drawingView.getSelectedFigures()); + } + pressedFigure = null; + fMouseEndPoint.setLocation(drawingView.viewToDrawing(evt.getPoint())); + if (evt.getButton() != 2) { + showPositionStatus(true); + } + else { + showPositionStatus(false); + } + } + + @Override + public void mouseExited(MouseEvent evt) { + dragIsSelected(); + clearPositionStatus(); + } + + @Override + public void mouseEntered(MouseEvent evt) { + dragIsSelected(); + } + + @Override + public void mouseMoved(MouseEvent evt) { + final DrawingView drawingView = scrollPane.getDrawingView(); + fMouseCurrentPoint.setLocation(drawingView.viewToDrawing(evt.getPoint())); + showPositionStatus(false); + } + + @Override + public void mouseClicked(MouseEvent evt) { + if (evt.getButton() == 2) { + if (dragTool.isSelected()) { + selectionTool.setSelected(true); + } + else if (selectionTool.isSelected()) { + dragTool.setSelected(true); + } + // Sets the correct cursor + dragIsSelected(); + } + } + + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + if (e.isControlDown()) { + int zoomLevel = zoomComboBox.getSelectedIndex(); + int notches = e.getWheelRotation(); + if (zoomLevel != -1) { + if (notches < 0) { + if (zoomLevel > 0) { + zoomLevel--; + zoomComboBox.setSelectedIndex(zoomLevel); + } + } + else { + if (zoomLevel < zoomComboBox.getItemCount() - 1) { + zoomLevel++; + zoomComboBox.setSelectedIndex(zoomLevel); + } + } + } + } + } + + /** + * Checks whether the drag tool is selected. + * + * @return true if the drag tool is selected, false otherwise. + */ + private boolean dragIsSelected() { + final DrawingView drawingView = scrollPane.getDrawingView(); + if (!selectionTool.isSelected() && dragTool.isSelected()) { + drawingView.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + return true; + } + else if (selectionTool.isSelected() && !dragTool.isSelected()) { + drawingView.setCursor(defaultCursor); + return false; + } + else { + return false; + } + } + + /** + * Clears the mouse position information in the status panel. + */ + private void clearPositionStatus() { + statusPanel.setPositionText(""); + } + + /** + * Displays the current mouse position or covered area in the status panel. + * + * @param showCoveredArea Whether to display the dimensions of the covered + * area instead of the current mouse coordinates. + */ + private void showPositionStatus(boolean showCoveredArea) { + double x = fMouseCurrentPoint.x; + double y = -fMouseCurrentPoint.y; + + if (showCoveredArea) { + double w = Math.abs(fMouseEndPoint.x - fMouseStartPoint.x); + double h = Math.abs(fMouseEndPoint.y - fMouseStartPoint.y); + statusPanel.setPositionText( + String.format("X %.0f Y %.0f W %.0f H %.0f", x, y, w, h) + ); + } + else { + LayoutModel layout = modelManager.getModel().getLayoutModel(); + + double scaleX = (double) layout.getPropertyScaleX().getValue(); + double scaleY = (double) layout.getPropertyScaleY().getValue(); + double xmm = x * scaleX; + double ymm = y * scaleY; + statusPanel.setPositionText( + String.format("X %.0f (%.0fmm) Y %.0f (%.0fmm)", x, xmm, y, ymm) + ); + } + } + + /** + * Creates a transport order, assuming a single vehicle was selected before. + * + * @param figure A point figure. + */ + private void createPossibleTransportOrder( + LabeledPointFigure figure, + Set
selectedFigures + ) { + if (selectedFigures.size() != 1) { + LOG.debug("More than one figure selected, skipping."); + return; + } + + Figure nextFigure = selectedFigures.iterator().next(); + + if (!(nextFigure instanceof VehicleFigure)) { + LOG.debug("Selected figure is not a VehicleFigure, skipping."); + return; + } + + PointModel model = figure.getPresentationFigure().getModel(); + VehicleModel vehicleModel = (VehicleModel) nextFigure.get(FigureConstants.MODEL); + + if (vehicleModel == null) { + LOG.warn("Selected VehicleFigure does not have a model, skipping."); + return; + } + if ((Vehicle.ProcState) vehicleModel.getPropertyProcState() + .getValue() != Vehicle.ProcState.IDLE) { + LOG.debug("Selected vehicle already has an order, skipping."); + return; + } + + orderUtil.createTransportOrder(model, vehicleModel); + } + + /** + * Returns if a vehicle is currently being dragged. + * + * @return True if yes, false otherwise. + */ + private boolean vehicleDragged() { + Set
selectedFigures = scrollPane.getDrawingView().getSelectedFigures(); + if (selectedFigures.size() != 1) { + return false; + } + + return selectedFigures.iterator().next() instanceof VehicleFigure; + } + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/figures/NamedVehicleFigure.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/figures/NamedVehicleFigure.java new file mode 100644 index 0000000..21bf78e --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/figures/NamedVehicleFigure.java @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.drawing.figures; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.Graphics2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import javax.swing.SwingUtilities; +import org.jhotdraw.draw.Figure; +import org.opentcs.components.plantoverview.VehicleTheme; +import org.opentcs.data.model.Triple; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.application.ApplicationState; +import org.opentcs.guing.common.components.drawing.figures.FigureOrdinals; +import org.opentcs.guing.common.components.drawing.figures.ToolTipTextGenerator; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.operationsdesk.application.menus.MenuFactory; +import org.opentcs.operationsdesk.util.OperationsDeskConfiguration; + +/** + * A vehicle figure that adds the name of the vehicle into the image. + */ +public class NamedVehicleFigure + extends + VehicleFigure { + + @Inject + public NamedVehicleFigure( + VehicleTheme vehicleTheme, + MenuFactory menuFactory, + OperationsDeskConfiguration appConfig, + @Assisted + VehicleModel model, + ToolTipTextGenerator textGenerator, + ModelManager modelManager, + ApplicationState applicationState + ) { + super( + vehicleTheme, + menuFactory, + appConfig, + model, + textGenerator, + modelManager, + applicationState + ); + } + + @Override + protected void drawFill(Graphics2D g2d) { + super.drawFill(g2d); + g2d.setFont(getVehicleTheme().labelFont()); + g2d.setPaint(getVehicleTheme().labelColor()); + g2d.drawString( + getVehicleTheme().label(getModel().getVehicle()), + (int) displayBox().getCenterX() + getVehicleTheme().labelOffsetX(), + (int) displayBox().getCenterY() + getVehicleTheme().labelOffsetY() + ); + } + + @Override + protected void updateFigureDetails(VehicleModel model) { + super.updateFigureDetails(model); + + PointModel point = model.getPoint(); + Triple precisePosition = model.getPrecisePosition(); + + if (point == null && precisePosition == null) { + // If neither the point nor the precise position is known, don't draw the figure. + SwingUtilities.invokeLater(() -> setVisible(false)); + } + else if (precisePosition != null && !isIgnorePrecisePosition()) { + // If a precise position exists, it is set in setBounds(), so it doesn't need any coordinates. + SwingUtilities.invokeLater(() -> { + setVisible(true); + setBounds(new Point2D.Double(), null); + }); + + setFigureDetailsChanged(true); + } + else if (point != null) { + SwingUtilities.invokeLater(() -> { + setVisible(true); + Figure pointFigure = getModelManager().getModel().getFigure(point); + Rectangle2D.Double r = pointFigure.getBounds(); + Point2D.Double pCenter = new Point2D.Double(r.getCenterX(), r.getCenterY()); + // Draw figure in the center of the node. + // Angle is set in setBounds(). + setBounds(pCenter, null); + }); + + setFigureDetailsChanged(true); + } + else { + SwingUtilities.invokeLater(() -> setVisible(false)); + } + } + + @Override + public boolean isVisible() { + if (getModel().getPoint() == null) { + return super.isVisible(); + } + + return super.isVisible() + && getModelManager().getModel().getFigure(getModel().getPoint()).isVisible(); + } + + @Override + public int getLayer() { + return FigureOrdinals.VEHICLE_FIGURE_ORDINAL; + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDesk.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDesk.java new file mode 100644 index 0000000..d44bf02 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDesk.java @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.drawing.figures; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.data.model.Vehicle.State.ERROR; +import static org.opentcs.data.model.Vehicle.State.UNAVAILABLE; +import static org.opentcs.data.model.Vehicle.State.UNKNOWN; + +import jakarta.inject.Inject; +import java.util.Comparator; +import java.util.List; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.stream.Collectors; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.guing.base.AllocationState; +import org.opentcs.guing.base.model.FigureDecorationDetails; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.drawing.figures.ToolTipTextGenerator; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.operationsdesk.peripherals.jobs.PeripheralJobsContainer; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * A tool tip generator integrating information about a vehicle's state. + */ +public class ToolTipTextGeneratorOperationsDesk + extends + ToolTipTextGenerator { + + /** + * A unicode play symbol. + */ + private static final String PLAY_SYMBOL = "\u23f5"; + /** + * A unicode hourglass symbol. + */ + private static final String HOURGLASS_SYMBOL = "\u29d6"; + + /** + * Collection of peripheral jobs. + */ + private final PeripheralJobsContainer peripheralJobContainer; + + /** + * Creates a new instance. + * + * @param modelManager The model manager to use. + * @param peripheralJobContainer The peripheral job container to use. + */ + @Inject + public ToolTipTextGeneratorOperationsDesk( + ModelManager modelManager, + PeripheralJobsContainer peripheralJobContainer + ) { + super(modelManager); + this.peripheralJobContainer = requireNonNull(peripheralJobContainer, "peripheralJobContainer"); + } + + @Override + public String getToolTipText(VehicleModel model) { + requireNonNull(model, "model"); + + StringBuilder sb = new StringBuilder("\n"); + + sb.append(model.getDescription()) + .append(" ") + .append("") + .append(model.getName()) + .append("\n"); + + appendVehicleState(sb, model); + appendMiscProps(sb, model); + appendPeripheralInformationVehicle(sb, model); + + sb.append("\n"); + return sb.toString(); + } + + @Override + protected void appendAllocatingVehicle(StringBuilder sb, FigureDecorationDetails figure) { + List> allocationStates = figure.getAllocationStates() + .entrySet() + .stream() + .filter(entry -> entry.getValue() == AllocationState.ALLOCATED) + .collect(Collectors.toList()); + + if (allocationStates.isEmpty()) { + return; + } + + sb.append("
\n"); + allocationStates + .forEach(entry -> { + sb.append(getResourceAllocatedByText()) + .append(" ") + .append(entry.getKey().getName()) + .append("
"); + }); + } + + private void appendPeripheralInformationVehicle(StringBuilder sb, VehicleModel vehicle) { + sb.append("
\n"); + sb.append(getRelatedPeripheralJobHeadingText()).append('\n'); + sb.append("
    \n"); + peripheralJobContainer.getPeripheralJobs().stream() + .filter(job -> !job.getState().isFinalState()) + .filter(job -> job.getPeripheralOperation().isCompletionRequired()) + .filter(job -> Objects.equals(job.getRelatedVehicle(), vehicle.getVehicle().getReference())) + .sorted(Comparator.comparing(PeripheralJob::getCreationTime)) + .forEach(job -> appendPeripheralJobListItem(sb, job)); + sb.append("
\n"); + } + + @SuppressWarnings("checkstyle:LineLength") + private String getRelatedPeripheralJobHeadingText() { + return ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.MISC_PATH) + .getString( + "toolTipTextGeneratorOperationsDesk.vehicleModel.awaitPeripheralJobCompletion.text" + ); + } + + private void appendPeripheralJobListItem(StringBuilder sb, PeripheralJob job) { + sb.append("
  • ") + .append( + job.getState() == PeripheralJob.State.BEING_PROCESSED + ? PLAY_SYMBOL + : HOURGLASS_SYMBOL + ) + .append(job.getPeripheralOperation().getLocation().getName()) + .append(": ") + .append(job.getPeripheralOperation().getOperation()) + .append(" (") + .append(job.getName()) + .append(")
  • \n"); + } + + private void appendVehicleState(StringBuilder sb, VehicleModel model) { + sb.append("
    \n"); + sb.append(model.getPropertyPoint().getDescription()).append(": ") + .append(model.getPoint() != null ? model.getPoint().getName() : "?") + .append('\n'); + sb.append("
    \n"); + sb.append(model.getPropertyNextPoint().getDescription()).append(": ") + .append(model.getNextPoint() != null ? model.getNextPoint().getName() : "?") + .append('\n'); + + sb.append("
    \n"); + sb.append(model.getPropertyState().getDescription()) + .append(": ") + .append(model.getPropertyState().getValue()) + .append("\n"); + + sb.append("
    \n"); + sb.append(model.getPropertyProcState().getDescription()).append(": ") + .append(model.getPropertyProcState().getValue()) + .append('\n'); + sb.append("
    \n"); + sb.append(model.getPropertyIntegrationLevel().getDescription()).append(": ") + .append(model.getPropertyIntegrationLevel().getValue()) + .append('\n'); + + sb.append("
    \n"); + sb.append(model.getPropertyEnergyLevel().getDescription()) + .append(": ") + .append(model.getPropertyEnergyLevel().getValue()) + .append("%\n"); + } + + private String energyColorString(Vehicle vehicle) { + if (vehicle.isEnergyLevelCritical()) { + return "red"; + } + else if (vehicle.isEnergyLevelDegraded()) { + return "orange"; + } + else if (vehicle.isEnergyLevelGood()) { + return "green"; + } + else { + return "black"; + } + } + + private String stateColorString(Vehicle vehicle) { + switch (vehicle.getState()) { + case ERROR: + return "red"; + case UNAVAILABLE: + case UNKNOWN: + return "orange"; + default: + return "black"; + } + } + + @SuppressWarnings("checkstyle:LineLength") + private String getResourceAllocatedByText() { + return ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.MISC_PATH) + .getString( + "toolTipTextGeneratorOperationsDesk.figureDecorationDetails.resourceAllocatedBy.text" + ); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/figures/VehicleFigure.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/figures/VehicleFigure.java new file mode 100644 index 0000000..6b06718 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/figures/VehicleFigure.java @@ -0,0 +1,537 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.drawing.figures; + +import static java.awt.image.ImageObserver.ABORT; +import static java.awt.image.ImageObserver.ALLBITS; +import static java.awt.image.ImageObserver.FRAMEBITS; +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.event.MouseEvent; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.ImageObserver; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Objects; +import javax.swing.SwingUtilities; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.handle.Handle; +import org.jhotdraw.geom.BezierPath; +import org.opentcs.components.plantoverview.VehicleTheme; +import org.opentcs.data.model.Triple; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.event.AttributesChangeListener; +import org.opentcs.guing.base.components.properties.type.AngleProperty; +import org.opentcs.guing.base.model.elements.AbstractConnection; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.application.ApplicationState; +import org.opentcs.guing.common.components.drawing.ZoomPoint; +import org.opentcs.guing.common.components.drawing.course.Origin; +import org.opentcs.guing.common.components.drawing.figures.FigureConstants; +import org.opentcs.guing.common.components.drawing.figures.LabeledPointFigure; +import org.opentcs.guing.common.components.drawing.figures.PathConnection; +import org.opentcs.guing.common.components.drawing.figures.PointFigure; +import org.opentcs.guing.common.components.drawing.figures.TCSFigure; +import org.opentcs.guing.common.components.drawing.figures.ToolTipTextGenerator; +import org.opentcs.guing.common.components.drawing.figures.liner.TripleBezierLiner; +import org.opentcs.guing.common.components.drawing.figures.liner.TupelBezierLiner; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.operationsdesk.application.menus.MenuFactory; +import org.opentcs.operationsdesk.application.menus.VehiclePopupMenu; +import org.opentcs.operationsdesk.components.dialogs.SingleVehicleView; +import org.opentcs.operationsdesk.components.drawing.figures.decoration.VehicleOutlineHandle; +import org.opentcs.operationsdesk.util.OperationsDeskConfiguration; + +/** + * The graphical representation of a vehicle. + */ +public class VehicleFigure + extends + TCSFigure + implements + AttributesChangeListener, + ImageObserver { + + /** + * When the position of the vehicle changed. + */ + public static final String POSITION_CHANGED = "position_changed"; + /** + * The figure's length in drawing units. + */ + private static final double LENGTH = 30.0; + /** + * The figure's width in drawing units. + */ + private static final double WIDTH = 20.0; + /** + * The vehicle theme to be used. + */ + private final VehicleTheme vehicleTheme; + /** + * A factory for popup menus. + */ + private final MenuFactory menuFactory; + /** + * The tool tip text generator. + */ + private final ToolTipTextGenerator textGenerator; + /** + * The model manager. + */ + private final ModelManager modelManager; + /** + * The application's current state. + */ + private final ApplicationState applicationState; + /** + * The angle at which the image is to be drawn. + */ + private double fAngle; + /** + * The image. + */ + private transient Image fImage; + /** + * Whether to ignore the vehicle's precise position or not. + */ + private boolean ignorePrecisePosition; + /** + * Whether to ignore the vehicle's orientation angle or not. + */ + private boolean ignoreOrientationAngle; + /** + * Indicates whether figure details changed. + */ + private boolean figureDetailsChanged; + + /** + * Creates a new instance. + * + * @param vehicleTheme The vehicle theme to be used. + * @param menuFactory A factory for popup menus. + * @param appConfig The application's configuration. + * @param model The model corresponding to this graphical object. + * @param textGenerator The tool tip text generator. + * @param modelManager The model manager. + * @param applicationState The application's current state. + */ + @Inject + @SuppressWarnings("this-escape") + public VehicleFigure( + VehicleTheme vehicleTheme, + MenuFactory menuFactory, + OperationsDeskConfiguration appConfig, + @Assisted + VehicleModel model, + ToolTipTextGenerator textGenerator, + ModelManager modelManager, + ApplicationState applicationState + ) { + super(model); + this.vehicleTheme = requireNonNull(vehicleTheme, "vehicleTheme"); + this.menuFactory = requireNonNull(menuFactory, "menuFactory"); + this.textGenerator = requireNonNull(textGenerator, "textGenerator"); + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.applicationState = requireNonNull(applicationState, "applicationState"); + + fDisplayBox = new Rectangle((int) LENGTH, (int) WIDTH); + fZoomPoint = new ZoomPoint(0.5 * LENGTH, 0.5 * WIDTH); + + setIgnorePrecisePosition(appConfig.ignoreVehiclePrecisePosition()); + setIgnoreOrientationAngle(appConfig.ignoreVehicleOrientationAngle()); + + fImage = vehicleTheme.statelessImage(model.getVehicle()); + } + + @Override + public VehicleModel getModel() { + return (VehicleModel) get(FigureConstants.MODEL); + } + + public void setAngle(double angle) { + fAngle = angle; + } + + public double getAngle() { + return fAngle; + } + + @Override + public Rectangle2D.Double getBounds() { + Rectangle2D.Double r2d = new Rectangle2D.Double(); + r2d.setRect(fDisplayBox.getBounds2D()); + + return r2d; + } + + @Override + public Object getTransformRestoreData() { + return fDisplayBox.clone(); + } + + @Override + public void restoreTransformTo(Object restoreData) { + Rectangle r = (Rectangle) restoreData; + fDisplayBox.x = r.x; + fDisplayBox.y = r.y; + fDisplayBox.width = r.width; + fDisplayBox.height = r.height; + fZoomPoint.setX(r.getCenterX()); + fZoomPoint.setY(r.getCenterY()); + } + + @Override + public void transform(AffineTransform tx) { + Point2D center = fZoomPoint.getPixelLocationExactly(); + setBounds((Point2D.Double) tx.transform(center, center), null); + } + + @Override + public String getToolTipText(Point2D.Double p) { + return textGenerator.getToolTipText(getModel()); + } + + /** + * Sets the ignore flag for the vehicle's reported orientation angle. + * + * @param doIgnore Whether to ignore the reported orientation angle. + */ + public final void setIgnoreOrientationAngle(boolean doIgnore) { + ignoreOrientationAngle = doIgnore; + PointModel point = getModel().getPoint(); + + if (point == null) { + // Only draw the vehicle if the point is known or the precise position is to be used. + setVisible(!ignorePrecisePosition); + } + else { + Figure pointFigure = modelManager.getModel().getFigure(point); + Rectangle2D.Double r = pointFigure.getBounds(); + Point2D.Double pCenter = new Point2D.Double(r.getCenterX(), r.getCenterY()); + setBounds(pCenter, null); + fireFigureChanged(); + } + } + + /** + * Sets the ignore flag for the vehicle's precise position. + * + * @param doIgnore Whether to ignore the reported precise position of the + * vehicle. + */ + public final void setIgnorePrecisePosition(boolean doIgnore) { + ignorePrecisePosition = doIgnore; + PointModel point = getModel().getPoint(); + + if (point == null) { + // Only draw the vehicle if the point is known or the precise position is to be used. + setVisible(!ignorePrecisePosition); + } + else { + Figure pointFigure = modelManager.getModel().getFigure(point); + Rectangle2D.Double r = pointFigure.getBounds(); + Point2D.Double pCenter = new Point2D.Double(r.getCenterX(), r.getCenterY()); + setBounds(pCenter, null); + fireFigureChanged(); + } + } + + /** + * Draws the center of the figure at anchor; the size does not + * change. + * + * @param anchor Center of the figure + * @param lead Not used + */ + @Override + public void setBounds(Point2D.Double anchor, Point2D.Double lead) { + VehicleModel model = getModel(); + Rectangle2D.Double oldBounds = getBounds(); + setVisible(false); + + Triple precisePosition = model.getPrecisePosition(); + + if (!ignorePrecisePosition) { + if (precisePosition != null) { + setVisible(true); + Origin origin = modelManager.getModel().getDrawingMethod().getOrigin(); + + if (origin.getScaleX() != 0.0 && origin.getScaleY() != 0.0) { + anchor.x = precisePosition.getX() / origin.getScaleX(); + anchor.y = -precisePosition.getY() / origin.getScaleY(); + } + } + } + + fZoomPoint.setX(anchor.x); + fZoomPoint.setY(anchor.y); + fDisplayBox.x = (int) (anchor.x - 0.5 * LENGTH); + fDisplayBox.y = (int) (anchor.y - 0.5 * WIDTH); + firePropertyChange(POSITION_CHANGED, oldBounds, getBounds()); + + updateVehicleOrientation(); + } + + private void updateVehicleOrientation() { + VehicleModel model = getModel(); + // orientation: + // 1. Use exact orientation from vehicle adapter. + // 2. Use orientation from current point. + // 3. Use direction to next point. + // 4. Use last known orientation. + double angle = model.getOrientationAngle(); + PointModel currentPoint = model.getPoint(); + + if (currentPoint != null) { + setVisible(true); + } + + if (!Double.isNaN(angle) && !ignoreOrientationAngle) { + fAngle = angle; + } + else if (currentPoint != null) { + // Use orientation from current point. + AngleProperty ap = currentPoint.getPropertyVehicleOrientationAngle(); + angle = (double) ap.getValue(); + + if (!Double.isNaN(angle)) { + fAngle = angle; + } + else { + alignVehicleToNextPoint(); + } + } + } + + private void alignVehicleToNextPoint() { + VehicleModel model = getModel(); + PointModel nextPoint = model.getNextPoint(); + PointModel currentPoint = model.getPoint(); + + AbstractConnection connection; + if (model.getDriveOrderState() == TransportOrder.State.BEING_PROCESSED) { + connection = model.getCurrentDriveOrderPath(); + } + else { + if (nextPoint != null) { + connection = currentPoint.getConnectionTo(nextPoint); + } + else { + // No destination point, use a random point connected to the current point. + connection = currentPoint.getConnections().stream() + .filter(con -> con instanceof PathModel) + .filter(con -> Objects.equals(con.getStartComponent(), currentPoint)) + .findFirst() + .orElse(null); + } + } + + if (connection != null) { + fAngle = calculateAngle(connection); + } + } + + private double calculateAngle(AbstractConnection connection) { + PointModel currentPoint = (PointModel) connection.getStartComponent(); + PointModel nextPoint = (PointModel) connection.getEndComponent(); + + PathConnection pathFigure + = (PathConnection) modelManager.getModel().getFigure(connection); + LabeledPointFigure clpf + = (LabeledPointFigure) modelManager.getModel().getFigure(currentPoint); + PointFigure cpf = clpf.getPresentationFigure(); + + if (pathFigure.getLiner() instanceof TupelBezierLiner + || pathFigure.getLiner() instanceof TripleBezierLiner) { + BezierPath bezierPath = pathFigure.getBezierPath(); + Point2D.Double cp = bezierPath.get(0, BezierPath.C2_MASK); + double dx = cp.getX() - cpf.getZoomPoint().getX(); + double dy = cp.getY() - cpf.getZoomPoint().getY(); + return Math.toDegrees(Math.atan2(-dy, dx)); + } + else { + LabeledPointFigure nlpf + = (LabeledPointFigure) modelManager.getModel().getFigure(nextPoint); + PointFigure npf = nlpf.getPresentationFigure(); + double dx = npf.getZoomPoint().getX() - cpf.getZoomPoint().getX(); + double dy = npf.getZoomPoint().getY() - cpf.getZoomPoint().getY(); + return Math.toDegrees(Math.atan2(-dy, dx)); + } + } + + /** + * Forces the vehicle figure to be drawn. (Used primarily for {@link SingleVehicleView}.) + * + * @param g2d The graphics context. + */ + public void forcedDraw(Graphics2D g2d) { + drawFill(g2d); + } + + @Override + protected void drawFigure(Graphics2D g2d) { + VehicleModel model = getModel(); + PointModel currentPoint = model.getPoint(); + Triple precisePosition = model.getPrecisePosition(); + if (currentPoint != null || precisePosition != null) { + drawFill(g2d); + } + } + + @Override + protected void drawFill(Graphics2D g2d) { + if (g2d == null) { + return; + } + + int dx; + int dy; + Rectangle r = displayBox(); + + if (fImage != null) { + dx = (r.width - fImage.getWidth(this)) / 2; + dy = (r.height - fImage.getHeight(this)) / 2; + int x = r.x + dx; + int y = r.y + dy; + AffineTransform oldAF = g2d.getTransform(); + g2d.translate(r.getCenterX(), r.getCenterY()); + g2d.rotate(-Math.toRadians(fAngle)); + g2d.translate(-r.getCenterX(), -r.getCenterY()); + g2d.drawImage(fImage, x, y, null); + g2d.setTransform(oldAF); + } + else { + // TODO: Draw an outline, e.g. a rectangle. + } + } + + @Override + protected void drawStroke(Graphics2D g2d) { + // Nothing to do here - Vehicle Figure is completely drawn in drawFill() + } + + @Override + public Collection createHandles(int detailLevel) { + Collection handles = new ArrayList<>(); + + switch (detailLevel) { + case -1: // Mouse Moved + handles.add(new VehicleOutlineHandle(this)); + break; + + case 0: // Mouse clicked +// handles.add(new VehicleOutlineHandle(this)); + break; + + case 1: // Double-Click +// handles.add(new VehicleOutlineHandle(this)); + break; + + default: + break; + } + + return handles; + } + + @Override + public boolean handleMouseClick( + Point2D.Double p, + MouseEvent evt, + DrawingView drawingView + ) { + // This gets executed on a double click AND a right click on the figure + VehicleModel model = getModel(); + VehiclePopupMenu menu = menuFactory.createVehiclePopupMenu(Arrays.asList(model)); + menu.show(drawingView.getComponent(), evt.getX(), evt.getY()); + + return false; + } + + @Override + public void propertiesChanged(AttributesChangeEvent e) { + if (e.getInitiator().equals(this) + || e.getModel() == null) { + return; + } + + updateFigureDetails((VehicleModel) e.getModel()); + + if (isFigureDetailsChanged()) { + SwingUtilities.invokeLater(() -> { + // Only call if the figure is visible - will cause NPE in BoundsOutlineHandle otherwise. + if (isVisible()) { + fireFigureChanged(); + } + }); + + setFigureDetailsChanged(false); + } + } + + /** + * Updates the figure details based on the given vehicle model. + *

    + * If figure details do change, call {@link #setFigureDetailsChanged(boolean)} to set the + * corresponding flag to {@code true}. + * When overriding this method, always remember to call the super-implementation. + *

    + * + * @param model The updated vehicle model. + */ + protected void updateFigureDetails(VehicleModel model) { + fImage = getVehicleTheme().statefulImage(model.getVehicle()); + setFigureDetailsChanged(true); + } + + @Override + public boolean imageUpdate( + Image img, int infoflags, + int x, int y, + int width, int height + ) { + if ((infoflags & (FRAMEBITS | ALLBITS)) != 0) { + invalidate(); + } + + return (infoflags & (ALLBITS | ABORT)) == 0; + } + + /** + * Returns the vehicle theme. + * + * @return The vehicle theme. + */ + protected VehicleTheme getVehicleTheme() { + return vehicleTheme; + } + + public boolean isFigureDetailsChanged() { + return figureDetailsChanged; + } + + public void setFigureDetailsChanged(boolean figureDetailsChanged) { + this.figureDetailsChanged = figureDetailsChanged; + } + + public boolean isIgnorePrecisePosition() { + return ignorePrecisePosition; + } + + public ModelManager getModelManager() { + return modelManager; + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/figures/VehicleFigureFactory.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/figures/VehicleFigureFactory.java new file mode 100644 index 0000000..6a4f7a8 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/figures/VehicleFigureFactory.java @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.drawing.figures; + +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.drawing.figures.FigureFactory; + +/** + */ +public interface VehicleFigureFactory + extends + FigureFactory { + + VehicleFigure createVehicleFigure(VehicleModel model); + + NamedVehicleFigure createNamedVehicleFigure(VehicleModel model); +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/figures/VehicleLabelFigure.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/figures/VehicleLabelFigure.java new file mode 100644 index 0000000..7392526 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/figures/VehicleLabelFigure.java @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.drawing.figures; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.font.TextLayout; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; +import org.jhotdraw.draw.event.FigureEvent; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.drawing.figures.LabeledFigure; +import org.opentcs.guing.common.components.drawing.figures.TCSFigure; +import org.opentcs.guing.common.components.drawing.figures.TCSLabelFigure; + +/** + */ +public class VehicleLabelFigure + extends + TCSLabelFigure { + + private static final Color COLOR_BACKGROUND = new Color(0xFFFFF0); // beige + private static final int MARGIN = 4; + + public VehicleLabelFigure(String vehicleName) { + super(vehicleName); + } + + @Override + protected void drawFill(Graphics2D g) { + if (getText() != null) { + TextLayout layout = getTextLayout(); + Rectangle2D bounds = layout.getBounds(); + RoundRectangle2D.Double rr = new RoundRectangle2D.Double( + bounds.getX() + origin.x - MARGIN, + bounds.getY() + origin.y + layout.getAscent() - MARGIN, + bounds.getWidth() + 2 * MARGIN, + bounds.getHeight() + 2 + MARGIN, + MARGIN, MARGIN + ); + g.setPaint(COLOR_BACKGROUND); + g.fill(rr); + } + } + + @Override + protected void drawStroke(Graphics2D g) { + } + + @Override + protected void drawText(Graphics2D g) { + if (getText() != null || isEditable()) { + TextLayout layout = getTextLayout(); + g.setPaint(Color.BLUE.darker()); + layout.draw(g, (float) origin.x, (float) (origin.y + layout.getAscent())); + } + } + + @Override + public void figureChanged(FigureEvent event) { + if (event.getFigure() instanceof LabeledFigure) { + LabeledFigure lf = (LabeledFigure) event.getFigure(); + TCSFigure figure = lf.getPresentationFigure(); + VehicleModel model = (VehicleModel) figure.getModel(); + String name = model.getName(); + + if (model.getPoint() != null) { + name += "@" + model.getPoint().getName(); + } + + setText(name); + invalidate(); + validate(); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/figures/decoration/VehicleOutlineHandle.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/figures/decoration/VehicleOutlineHandle.java new file mode 100644 index 0000000..f39c30d --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/drawing/figures/decoration/VehicleOutlineHandle.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.drawing.figures.decoration; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; +import java.awt.geom.Path2D; +import java.awt.geom.Rectangle2D; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.handle.BoundsOutlineHandle; +import org.opentcs.operationsdesk.components.drawing.figures.VehicleFigure; + +/** + */ +public class VehicleOutlineHandle + extends + BoundsOutlineHandle { + + public VehicleOutlineHandle(Figure owner) { + super(owner); + } + + @Override + public void draw(Graphics2D g) { + VehicleFigure vf = (VehicleFigure) getOwner(); + Rectangle2D bounds = vf.getBounds(); + + if (view != null) { + AffineTransform at = view.getDrawingToViewTransform(); + at.translate(bounds.getCenterX(), bounds.getCenterY()); + at.rotate(-Math.toRadians(vf.getAngle())); + at.translate(-bounds.getCenterX(), -bounds.getCenterY()); + Path2D shape = (Path2D) at.createTransformedShape(bounds); + g.setClip(shape); + g.setStroke(new BasicStroke(2.0f)); + g.draw(shape); + Color c = new Color(127, 0, 127, 127); + g.setPaint(c); + g.fill(shape); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/layer/LayerGroupsPanel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/layer/LayerGroupsPanel.java new file mode 100644 index 0000000..032f120 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/layer/LayerGroupsPanel.java @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.layer; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.awt.BorderLayout; +import java.util.Arrays; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; +import javax.swing.RowSorter; +import javax.swing.SortOrder; +import javax.swing.table.TableRowSorter; +import org.opentcs.guing.common.components.layer.LayerGroupEditor; +import org.opentcs.guing.common.components.layer.LayerGroupManager; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * A panel to display and edit layer groups. + */ +public class LayerGroupsPanel + extends + JPanel { + + /** + * The model manager. + */ + private final ModelManager modelManager; + /** + * The layer manager. + */ + private final LayerGroupManager layerGroupManager; + /** + * The layer editor. + */ + private final LayerGroupEditor layerGroupEditor; + /** + * The table to display available layers. + */ + private JTable table; + /** + * The table model. + */ + private LayerGroupsTableModel tableModel; + + @Inject + @SuppressWarnings("this-escape") + public LayerGroupsPanel( + ModelManager modelManager, + LayerGroupManager layerGroupManager, + LayerGroupEditor layerGroupEditor + ) { + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.layerGroupManager = requireNonNull(layerGroupManager, "layerGroupManager"); + this.layerGroupEditor = requireNonNull(layerGroupEditor, "layerGroupEditor"); + + initComponents(); + } + + private void initComponents() { + setLayout(new BorderLayout()); + + tableModel = new LayerGroupsTableModel(modelManager, layerGroupEditor); + layerGroupManager.addLayerGroupChangeListener(tableModel); + table = new JTable(tableModel); + initTable(); + + add(new JScrollPane(table), BorderLayout.CENTER); + } + + private void initTable() { + table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + TableRowSorter sorter = new TableRowSorter<>(tableModel); + // Sort the table by the layer ordinals... + sorter.setSortKeys( + Arrays.asList( + new RowSorter.SortKey(LayerGroupsTableModel.COLUMN_ID, SortOrder.DESCENDING) + ) + ); + // ...but prevent manual sorting. + for (int i = 0; i < table.getColumnCount(); i++) { + sorter.setSortable(i, false); + } + sorter.setSortsOnUpdates(true); + table.setRowSorter(sorter); + + // Hide the column that shows the layer group IDs. + table.removeColumn( + table.getColumnModel() + .getColumn(table.convertColumnIndexToView(LayerGroupsTableModel.COLUMN_ID)) + ); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/layer/LayerGroupsTableModel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/layer/LayerGroupsTableModel.java new file mode 100644 index 0000000..5ab881b --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/layer/LayerGroupsTableModel.java @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.layer; + +import javax.swing.SwingUtilities; +import org.opentcs.guing.common.components.layer.AbstractLayerGroupsTableModel; +import org.opentcs.guing.common.components.layer.LayerGroupEditor; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * The table model for layer groups for the Operations Desk application. + */ +class LayerGroupsTableModel + extends + AbstractLayerGroupsTableModel { + + /** + * Creates a new instance. + * + * @param modelManager The model manager. + * @param layerGroupEditor The layer group editor. + */ + LayerGroupsTableModel(ModelManager modelManager, LayerGroupEditor layerGroupEditor) { + super(modelManager, layerGroupEditor); + } + + @Override + protected boolean isNameColumnEditable() { + return false; + } + + @Override + protected boolean isVisibleColumnEditable() { + return true; + } + + @Override + public void groupsInitialized() { + // Once the layers are initialized we want to redraw the entire table to avoid any + // display errors. + executeOnEventDispatcherThread(() -> fireTableDataChanged()); + } + + @Override + public void groupsChanged() { + // Update the entire table but don't use fireTableDataChanged() to preserve the current + // selection. + executeOnEventDispatcherThread(() -> fireTableRowsUpdated(0, getRowCount() - 1)); + } + + @Override + public void groupAdded() { + } + + @Override + public void groupRemoved() { + } + + /** + * Ensures the given runnable is executed on the EDT. + * If the runnable is already being called on the EDT, the runnable is executed immediately. + * Otherwise it is scheduled for execution on the EDT. + *

    + * Note: Deferring a runnable by scheduling it for execution on the EDT even though it would + * have already been executed on the EDT may lead to exceptions due to data inconsistency. + *

    + * + * @param runnable The runnable. + */ + private void executeOnEventDispatcherThread(Runnable runnable) { + if (SwingUtilities.isEventDispatchThread()) { + runnable.run(); + } + else { + SwingUtilities.invokeLater(runnable); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/layer/LayersPanel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/layer/LayersPanel.java new file mode 100644 index 0000000..9ba547a --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/layer/LayersPanel.java @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.layer; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.awt.BorderLayout; +import java.util.Arrays; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; +import javax.swing.RowSorter; +import javax.swing.SortOrder; +import javax.swing.table.TableRowSorter; +import org.opentcs.guing.common.components.layer.DisabledCheckBoxCellRenderer; +import org.opentcs.guing.common.components.layer.LayerEditor; +import org.opentcs.guing.common.components.layer.LayerGroupCellRenderer; +import org.opentcs.guing.common.components.layer.LayerGroupManager; +import org.opentcs.guing.common.components.layer.LayerManager; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * A panel to display and edit layers. + */ +public class LayersPanel + extends + JPanel { + + /** + * The model manager. + */ + private final ModelManager modelManager; + /** + * The layer manager. + */ + private final LayerManager layerManager; + /** + * The layer group manager. + */ + private final LayerGroupManager layerGroupManager; + /** + * The layer editor. + */ + private final LayerEditor layerEditor; + /** + * The table to display available layers. + */ + private JTable table; + /** + * The table model. + */ + private LayersTableModel tableModel; + + @Inject + @SuppressWarnings("this-escape") + public LayersPanel( + ModelManager modelManager, + LayerManager layerManager, + LayerGroupManager layerGroupManager, + LayerEditor layerEditor + ) { + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.layerManager = requireNonNull(layerManager, "layerManager"); + this.layerGroupManager = requireNonNull(layerGroupManager, "layerGroupManager"); + this.layerEditor = requireNonNull(layerEditor, "layerEditor"); + + initComponents(); + } + + private void initComponents() { + setLayout(new BorderLayout()); + + tableModel = new LayersTableModel(modelManager, layerEditor); + layerManager.setLayerChangeListener(tableModel); + layerGroupManager.addLayerGroupChangeListener(tableModel); + table = new JTable(tableModel); + initTable(); + + JScrollPane scrollPane = new JScrollPane(table); + add(scrollPane, BorderLayout.CENTER); + } + + private void initTable() { + table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + TableRowSorter sorter = new TableRowSorter<>(tableModel); + // Sort the table by the layer ordinals... + sorter.setSortKeys( + Arrays.asList( + new RowSorter.SortKey(LayersTableModel.COLUMN_ORDINAL, SortOrder.DESCENDING) + ) + ); + // ...but prevent manual sorting. + for (int i = 0; i < table.getColumnCount(); i++) { + sorter.setSortable(i, false); + } + sorter.setSortsOnUpdates(true); + table.setRowSorter(sorter); + + // Hide the column that shows the layer ordinals. + table.removeColumn( + table.getColumnModel() + .getColumn(table.convertColumnIndexToView(LayersTableModel.COLUMN_ORDINAL)) + ); + + table.getColumnModel() + .getColumn(table.convertColumnIndexToView(LayersTableModel.COLUMN_GROUP)) + .setCellRenderer(new LayerGroupCellRenderer()); + table.getColumnModel() + .getColumn(table.convertColumnIndexToView(LayersTableModel.COLUMN_GROUP_VISIBLE)) + .setCellRenderer(new DisabledCheckBoxCellRenderer()); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/layer/LayersTableModel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/layer/LayersTableModel.java new file mode 100644 index 0000000..4bf5fc3 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/layer/LayersTableModel.java @@ -0,0 +1,266 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.layer; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.stream.Collectors; +import javax.swing.SwingUtilities; +import javax.swing.table.AbstractTableModel; +import org.opentcs.data.model.visualization.Layer; +import org.opentcs.data.model.visualization.LayerGroup; +import org.opentcs.guing.base.components.layer.LayerWrapper; +import org.opentcs.guing.common.components.layer.LayerChangeListener; +import org.opentcs.guing.common.components.layer.LayerEditor; +import org.opentcs.guing.common.components.layer.LayerGroupChangeListener; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; + +/** + * A table model for layers. + */ +class LayersTableModel + extends + AbstractTableModel + implements + LayerChangeListener, + LayerGroupChangeListener { + + /** + * The number of the "Ordinal" column. + */ + public static final int COLUMN_ORDINAL = 0; + /** + * The number of the "Visible" column. + */ + public static final int COLUMN_VISIBLE = 1; + /** + * The number of the "Name" column. + */ + public static final int COLUMN_NAME = 2; + /** + * The number of the "Group" column. + */ + public static final int COLUMN_GROUP = 3; + /** + * The number of the "Group visible" column. + */ + public static final int COLUMN_GROUP_VISIBLE = 4; + /** + * The resource bundle to use. + */ + private static final ResourceBundle BUNDLE + = ResourceBundle.getBundle(I18nPlantOverviewOperating.LAYERS_PATH); + /** + * The column names. + */ + private static final String[] COLUMN_NAMES = new String[]{ + BUNDLE.getString("layersTableModel.column_ordinal.headerText"), + BUNDLE.getString("layersTableModel.column_visible.headerText"), + BUNDLE.getString("layersTableModel.column_name.headerText"), + BUNDLE.getString("layersTableModel.column_group.headerText"), + BUNDLE.getString("layersTableModel.column_groupVisible.headerText") + }; + /** + * The column classes. + */ + private static final Class[] COLUMN_CLASSES = new Class[]{ + Integer.class, + Boolean.class, + String.class, + LayerGroup.class, + Boolean.class + }; + /** + * The model manager. + */ + private final ModelManager modelManager; + /** + * The layer editor. + */ + private final LayerEditor layerEditor; + + /** + * Creates a new instance. + * + * @param modelManager The model manager. + * @param layerEditor The layer editor. + */ + LayersTableModel( + ModelManager modelManager, + LayerEditor layerEditor + ) { + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.layerEditor = requireNonNull(layerEditor, "layerEditor"); + } + + @Override + public int getRowCount() { + return getLayers().size(); + } + + @Override + public int getColumnCount() { + return COLUMN_NAMES.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= getRowCount()) { + return null; + } + + Layer entry = getLayers().get(rowIndex); + switch (columnIndex) { + case COLUMN_ORDINAL: + return entry.getOrdinal(); + case COLUMN_VISIBLE: + return entry.isVisible(); + case COLUMN_NAME: + return entry.getName(); + case COLUMN_GROUP: + return getLayerGroups().get(entry.getGroupId()); + case COLUMN_GROUP_VISIBLE: + return getLayerGroups().get(entry.getGroupId()).isVisible(); + default: + throw new IllegalArgumentException("Invalid column index: " + columnIndex); + } + } + + @Override + public String getColumnName(int columnIndex) { + return COLUMN_NAMES[columnIndex]; + } + + @Override + public Class getColumnClass(int columnIndex) { + return COLUMN_CLASSES[columnIndex]; + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + switch (columnIndex) { + case COLUMN_ORDINAL: + return false; + case COLUMN_VISIBLE: + return true; + case COLUMN_NAME: + return false; + case COLUMN_GROUP: + return false; + case COLUMN_GROUP_VISIBLE: + return false; + default: + throw new IllegalArgumentException("Invalid column index: " + columnIndex); + } + } + + @Override + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= getRowCount()) { + return; + } + + if (aValue == null) { + return; + } + + Layer entry = getLayers().get(rowIndex); + switch (columnIndex) { + case COLUMN_ORDINAL: + // Do nothing. + break; + case COLUMN_VISIBLE: + layerEditor.setLayerVisible(entry.getId(), (boolean) aValue); + break; + case COLUMN_NAME: + // Do nothing. + break; + case COLUMN_GROUP: + // Do nothing. + break; + case COLUMN_GROUP_VISIBLE: + // Do nothing. + break; + default: + throw new IllegalArgumentException("Invalid column index: " + columnIndex); + } + } + + @Override + public void layersInitialized() { + // Once the layers are initialized we want to redraw the entire table to avoid any + // display errors. + executeOnEventDispatcherThread(() -> fireTableDataChanged()); + } + + @Override + public void layersChanged() { + // Update the entire table but don't use fireTableDataChanged() to preserve the current + // selection. + executeOnEventDispatcherThread(() -> fireTableRowsUpdated(0, getRowCount() - 1)); + } + + @Override + public void layerAdded() { + } + + @Override + public void layerRemoved() { + } + + @Override + public void groupsInitialized() { + } + + @Override + public void groupsChanged() { + // The visibility of a group, which we display as well, may have changed. Update the table. + executeOnEventDispatcherThread(() -> fireTableRowsUpdated(0, getRowCount() - 1)); + } + + @Override + public void groupAdded() { + } + + @Override + public void groupRemoved() { + } + + private List getLayers() { + return getLayerWrappers().values().stream() + .map(wrapper -> wrapper.getLayer()) + .collect(Collectors.toList()); + } + + private Map getLayerWrappers() { + return modelManager.getModel().getLayoutModel().getPropertyLayerWrappers().getValue(); + } + + private Map getLayerGroups() { + return modelManager.getModel().getLayoutModel().getPropertyLayerGroups().getValue(); + } + + /** + * Ensures the given runnable is executed on the EDT. + * If the runnable is already being called on the EDT, the runnable is executed immediately. + * Otherwise it is scheduled for execution on the EDT. + *

    + * Note: Deferring a runnable by scheduling it for execution on the EDT even though it would + * have already been executed on the EDT may lead to exceptions due to data inconsistency. + *

    + * + * @param runnable The runnable. + */ + private void executeOnEventDispatcherThread(Runnable runnable) { + if (SwingUtilities.isEventDispatchThread()) { + runnable.run(); + } + else { + SwingUtilities.invokeLater(runnable); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/tree/elements/VehicleUserObjectOperating.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/tree/elements/VehicleUserObjectOperating.java new file mode 100644 index 0000000..4b095c9 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/components/tree/elements/VehicleUserObjectOperating.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.tree.elements; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import javax.swing.JPopupMenu; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.components.tree.elements.VehicleUserObject; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.operationsdesk.application.menus.MenuFactory; + +/** + * A Vehicle in the tree view. + */ +public class VehicleUserObjectOperating + extends + VehicleUserObject { + + /** + * A factory for popup menus. + */ + private final MenuFactory menuFactory; + + /** + * Creates a new instance. + * + * @param model The corresponding vehicle object. + * @param guiManager The gui manager. + * @param modelManager Provides the current system model. + * @param menuFactory A factory for popup menus. + */ + @Inject + public VehicleUserObjectOperating( + @Assisted + VehicleModel model, + GuiManager guiManager, + ModelManager modelManager, + MenuFactory menuFactory + ) { + super(model, guiManager, modelManager); + this.menuFactory = requireNonNull(menuFactory, "menuFactory"); + } + + @Override + public JPopupMenu getPopupMenu() { + return menuFactory.createVehiclePopupMenu(selectedVehicles); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/event/KernelStateChangeEvent.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/event/KernelStateChangeEvent.java new file mode 100644 index 0000000..c42a7c6 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/event/KernelStateChangeEvent.java @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.event; + +import static java.util.Objects.requireNonNull; + +import java.util.EventObject; +import org.opentcs.access.Kernel; + +/** + * Informs listeners about a change of the kernel's state. + */ +public class KernelStateChangeEvent + extends + EventObject { + + /** + * The new/current kernel state. + */ + private final State newState; + + /** + * Creates a new instance. + * + * @param source The source of this event. + * @param newState The new/current kernel state. + */ + public KernelStateChangeEvent(Object source, State newState) { + super(source); + this.newState = requireNonNull(newState, "newState"); + } + + /** + * Returns the new/current kernel state. + * + * @return The new/current kernel state. + */ + public State getNewState() { + return newState; + } + + @Override + public String toString() { + return "KernelStateChangeEvent{" + + "newState=" + newState + + ", source=" + getSource() + + '}'; + } + + public static State convertKernelState(Kernel.State kernelState) { + switch (kernelState) { + case MODELLING: + return State.MODELLING; + case OPERATING: + return State.OPERATING; + case SHUTDOWN: + return State.SHUTDOWN; + default: + throw new IllegalArgumentException("Unhandled state: " + kernelState); + } + } + + /** + * The potential kernel states. + */ + public enum State { + /** + * Modelling mode. + */ + MODELLING, + /** + * Operating. + */ + OPERATING, + /** + * Shutting down. + */ + SHUTDOWN, + /** + * Logged in. + */ + LOGGED_IN, + /** + * Disconnected. + */ + DISCONNECTED; + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/AttributeAdapterRegistry.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/AttributeAdapterRegistry.java new file mode 100644 index 0000000..f9befb7 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/AttributeAdapterRegistry.java @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.exchange; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.components.Lifecycle; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.event.SystemModelTransitionEvent; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.operationsdesk.exchange.adapter.LocationLockAdapter; +import org.opentcs.operationsdesk.exchange.adapter.PathLockAdapter; +import org.opentcs.operationsdesk.exchange.adapter.VehicleAllowedOrderTypesAdapter; +import org.opentcs.operationsdesk.exchange.adapter.VehicleEnergyLevelThresholdSetAdapter; +import org.opentcs.operationsdesk.exchange.adapter.VehicleEnvelopeKeyAdapter; +import org.opentcs.operationsdesk.exchange.adapter.VehiclePausedAdapter; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.event.EventSource; + +/** + * Handles registering of model attribute adapters that update a model component's attribute with + * the kernel when it changes. + */ +public class AttributeAdapterRegistry + implements + EventHandler, + Lifecycle { + + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * The model manager. + */ + private final ModelManager modelManager; + /** + * The event soruce we're registering with. + */ + private final EventSource eventSource; + /** + * Whether this instance is initialized or not. + */ + private boolean initialized; + + @Inject + public AttributeAdapterRegistry( + SharedKernelServicePortalProvider portalProvider, + ModelManager modelManager, + @ApplicationEventBus + EventSource eventSource + ) { + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.eventSource = requireNonNull(eventSource, "eventSource"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + eventSource.subscribe(this); + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + eventSource.unsubscribe(this); + initialized = false; + } + + @Override + public void onEvent(Object event) { + if (event instanceof SystemModelTransitionEvent) { + SystemModelTransitionEvent evt = (SystemModelTransitionEvent) event; + switch (evt.getStage()) { + case LOADED: + registerAdapters(); + break; + default: + } + } + } + + private void registerAdapters() { + for (VehicleModel model : modelManager.getModel().getVehicleModels()) { + model.addAttributesChangeListener( + new VehicleAllowedOrderTypesAdapter( + portalProvider, + model + ) + ); + model.addAttributesChangeListener(new VehiclePausedAdapter(portalProvider, model)); + model.addAttributesChangeListener(new VehicleEnvelopeKeyAdapter(portalProvider, model)); + model.addAttributesChangeListener( + new VehicleEnergyLevelThresholdSetAdapter(portalProvider, model) + ); + } + for (PathModel model : modelManager.getModel().getPathModels()) { + model.addAttributesChangeListener(new PathLockAdapter(portalProvider, model)); + } + for (LocationModel model : modelManager.getModel().getLocationModels()) { + model.addAttributesChangeListener(new LocationLockAdapter(portalProvider, model)); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/KernelEventFetcher.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/KernelEventFetcher.java new file mode 100644 index 0000000..4290a6a --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/KernelEventFetcher.java @@ -0,0 +1,219 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.exchange; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkInRange; + +import jakarta.inject.Inject; +import java.util.List; +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.KernelStateTransitionEvent; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.common.KernelClientApplication; +import org.opentcs.components.Lifecycle; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.operationsdesk.event.KernelStateChangeEvent; +import org.opentcs.util.CyclicTask; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Periodically fetches events from the kernel, if connected, and publishes them via the local event + * bus. + */ +public class KernelEventFetcher + implements + Lifecycle, + EventHandler { + + /** + * This class' logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(KernelEventFetcher.class); + /** + * The time to wait between event fetches with the service portal (in ms). + */ + private final long eventFetchInterval = 100; + /** + * The time to wait for events to arrive when fetching (in ms). + */ + private final long eventFetchTimeout = 1000; + /** + * Where we send events and receive them from. + */ + private final EventBus eventBus; + /** + * Provides a shared portal instance. + */ + private final SharedKernelServicePortalProvider servicePortalProvider; + /** + * The kernel client application. + */ + private final KernelClientApplication kernelClientApplication; + /** + * The shared portal + */ + private SharedKernelServicePortal sharedServicePortal; + /** + * The portal. + */ + private KernelServicePortal servicePortal; + /** + * The task fetching the service portal for new events. + */ + private EventFetcherTask eventFetcherTask; + /** + * Whether this event hub is initialized or not. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param eventBus Where this instance sends events. + * @param servicePortalProvider Provides a shared portal instance. + * @param kernelClientApplication The kernel client application. + */ + @Inject + public KernelEventFetcher( + @ApplicationEventBus + EventBus eventBus, + SharedKernelServicePortalProvider servicePortalProvider, + KernelClientApplication kernelClientApplication + ) { + this.eventBus = requireNonNull(eventBus, "eventBus"); + this.servicePortalProvider = requireNonNull(servicePortalProvider, "servicePortalProvider"); + this.kernelClientApplication + = requireNonNull(kernelClientApplication, "kernelClientApplication"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + LOG.info("Initializing..."); + eventBus.subscribe(this); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + LOG.info("Terminating..."); + eventBus.unsubscribe(this); + + initialized = false; + } + + @Override + public void onEvent(Object event) { + if (event instanceof KernelStateChangeEvent kernelStateChangeEvent) { + switch (kernelStateChangeEvent.getNewState()) { + case LOGGED_IN: + handleKernelConnect(); + break; + case DISCONNECTED: + handleKernelDisconnect(); + break; + default: + // Do nothing. + } + } + } + + private void handleKernelConnect() { + if (eventFetcherTask != null) { + return; + } + sharedServicePortal = servicePortalProvider.register(); + servicePortal = sharedServicePortal.getPortal(); + + eventFetcherTask = new EventFetcherTask(eventFetchInterval, eventFetchTimeout); + Thread eventFetcherThread = new Thread(eventFetcherTask, "KernelEventFetcher"); + eventFetcherThread.start(); + } + + private void handleKernelDisconnect() { + if (eventFetcherTask == null) { + return; + } + // Stop polling for events. + eventFetcherTask.terminate(); + eventFetcherTask = null; + + sharedServicePortal.close(); + servicePortal = null; + } + + /** + * A task fetching the service portal for events in regular intervals. + */ + private class EventFetcherTask + extends + CyclicTask { + + /** + * The poll timeout. + */ + private final long timeout; + + /** + * Creates a new instance. + * + * @param interval The time to wait between polls in ms. + * @param timeout The timeout in ms for which to wait for events to arrive with each polling + * call. + */ + private EventFetcherTask(long interval, long timeout) { + super(interval); + this.timeout = checkInRange(timeout, 1, Long.MAX_VALUE, "timeout"); + } + + @Override + protected void runActualTask() { + boolean shutDown = false; + try { + LOG.debug("Fetching remote kernel for events"); + List events = servicePortal.fetchEvents(timeout); + for (Object event : events) { + LOG.debug("Processing fetched event: {}", event); + // Forward received events to all registered listeners. + eventBus.onEvent(event); + + // Check if the kernel notifies us about a state change. + if (event instanceof KernelStateTransitionEvent) { + KernelStateTransitionEvent stateEvent = (KernelStateTransitionEvent) event; + // If the kernel switches to SHUTDOWN, remember to shut down. + shutDown = stateEvent.getEnteredState() == Kernel.State.SHUTDOWN; + } + } + } + catch (KernelRuntimeException exc) { + LOG.error("Exception fetching events, logging out", exc); + // Remember the connection problem by shutting it down properly. + shutDown = true; + } + + if (shutDown) { + kernelClientApplication.offline(); + } + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/OpenTCSEventDispatcher.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/OpenTCSEventDispatcher.java new file mode 100644 index 0000000..8be8c29 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/OpenTCSEventDispatcher.java @@ -0,0 +1,229 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.exchange; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.data.TCSObjectEvent.Type.OBJECT_MODIFIED; + +import jakarta.inject.Inject; +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelStateTransitionEvent; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.common.ClientConnectionMode; +import org.opentcs.components.Lifecycle; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.common.exchange.adapter.ProcessAdapter; +import org.opentcs.guing.common.exchange.adapter.ProcessAdapterUtil; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.operationsdesk.event.KernelStateChangeEvent; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A central event dispatcher between the kernel and the plant overview. + */ +public class OpenTCSEventDispatcher + implements + Lifecycle, + EventHandler { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(OpenTCSEventDispatcher.class); + /** + * Where we get events from and send them to. + */ + private final EventBus eventBus; + /** + * The process adapter util. + */ + private final ProcessAdapterUtil processAdapterUtil; + /** + * The model manager. + */ + private final ModelManager modelManager; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * A reference to a shared portal instance. + */ + private SharedKernelServicePortal sharedPortal; + /** + * Whether this component is initialized. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param portalProvider Provides a access to a portal. + * @param eventBus Where this instance gets events from and sends them to. + * @param processAdapterUtil The process adapter util. + * @param modelManager The model manager. + */ + @Inject + public OpenTCSEventDispatcher( + SharedKernelServicePortalProvider portalProvider, + @ApplicationEventBus + EventBus eventBus, + ProcessAdapterUtil processAdapterUtil, + ModelManager modelManager + ) { + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.eventBus = requireNonNull(eventBus, "eventBus"); + this.processAdapterUtil = requireNonNull(processAdapterUtil, "processAdapterUtil"); + this.modelManager = requireNonNull(modelManager, "modelManager"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + eventBus.subscribe(this); + + initialized = true; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + eventBus.unsubscribe(this); + + initialized = false; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + private void register() { + if (sharedPortal != null) { + return; + } + + LOG.debug("EventDispatcher {} registering with portal...", this); + sharedPortal = portalProvider.register(); + } + + private void release() { + if (sharedPortal == null) { + return; + } + + LOG.debug("EventDispatcher {} unregistering with portal...", this); + sharedPortal.close(); + sharedPortal = null; + } + + @Override + public void onEvent(Object event) { + if (event instanceof TCSObjectEvent tcsObjectEvent) { + processObjectEvent(tcsObjectEvent); + } + else if (event instanceof KernelStateTransitionEvent kernelStateTransitionEvent) { + // React instantly on SHUTDOWN of the kernel, otherwise wait for + // the transition to finish + if (kernelStateTransitionEvent.isTransitionFinished() + || kernelStateTransitionEvent.getEnteredState() == Kernel.State.SHUTDOWN) { + eventBus.onEvent( + new KernelStateChangeEvent( + this, + KernelStateChangeEvent.convertKernelState( + kernelStateTransitionEvent.getEnteredState() + ) + ) + ); + } + } + else if (event instanceof ClientConnectionMode connectionMode) { + switch (connectionMode) { + case ONLINE: + handleKernelConnect(); + break; + case OFFLINE: + handleKernelDisconnect(); + break; + default: + // Do nothing. + } + } + } + + private void handleKernelConnect() { + register(); + + eventBus.onEvent( + new KernelStateChangeEvent(this, KernelStateChangeEvent.State.LOGGED_IN) + ); + eventBus.onEvent( + new KernelStateChangeEvent( + this, + KernelStateChangeEvent.convertKernelState(sharedPortal.getPortal().getState()) + ) + ); + } + + private void handleKernelDisconnect() { + release(); + + eventBus.onEvent( + new KernelStateChangeEvent( + this, + KernelStateChangeEvent.State.DISCONNECTED + ) + ); + } + + private void processObjectEvent(TCSObjectEvent objectEvent) { + LOG.debug("TCSObjectEvent received: {}", objectEvent); + + if (sharedPortal == null) { + return; + } + + if (objectEvent.getType() == OBJECT_MODIFIED) { + processObjectModifiedEvent(objectEvent.getCurrentObjectState()); + } + } + + private void processObjectModifiedEvent(TCSObject tcsObject) { + if (tcsObject instanceof TransportOrder + || tcsObject instanceof OrderSequence) { + // We only care about model objects (with ProcessAdapters) here, not transport orders. + return; + } + + ModelComponent modelComponent = modelManager.getModel() + .getModelComponent(tcsObject.getReference().getName()); + if (modelComponent == null) { + LOG.debug("No model component found for {}", tcsObject.getName()); + return; + } + + ProcessAdapter adapter = processAdapterUtil.processAdapterFor(modelComponent); + adapter.updateModelProperties( + tcsObject, + modelComponent, + modelManager.getModel(), + sharedPortal.getPortal().getPlantModelService() + ); + } + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/TransportOrderUtil.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/TransportOrderUtil.java new file mode 100644 index 0000000..034737e --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/TransportOrderUtil.java @@ -0,0 +1,229 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.exchange; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.access.to.order.DestinationCreationTO; +import org.opentcs.access.to.order.TransportOrderCreationTO; +import org.opentcs.components.kernel.services.TransportOrderService; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Point; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.guing.base.model.AbstractConnectableModelComponent; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A helper class for creating transport orders with the kernel. + */ +public class TransportOrderUtil { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(TransportOrderUtil.class); + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + + /** + * Creates a new instance. + * + * @param portalProvider Provides a access to a portal. + */ + @Inject + public TransportOrderUtil(SharedKernelServicePortalProvider portalProvider) { + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + } + + /** + * Creates a new transport order. + * + * @param destModels The locations or points to visit. + * @param actions The actions to execute. + * @param deadline The deadline. + * @param vModel The vehicle that shall execute this order. Pass null to let the + * kernel determine one. + * @param category The category. + */ + public void createTransportOrder( + List destModels, + List actions, + long deadline, + VehicleModel vModel, + String category + ) { + createTransportOrder(destModels, actions, new ArrayList<>(), deadline, vModel, category); + } + + /** + * Creates a new transport order. + * + * @param destModels The locations or points to visit. + * @param actions The actions to execute. + * @param propertiesList The properties for each destination. + * @param deadline The deadline. + * @param vModel The vehicle that shall execute this order. Pass null to let the + * kernel determine one. + * @param type The type. + */ + public void createTransportOrder( + List destModels, + List actions, + List> propertiesList, + long deadline, + VehicleModel vModel, + String type + ) { + requireNonNull(destModels, "locations"); + requireNonNull(actions, "actions"); + requireNonNull(propertiesList, "propertiesList"); + checkArgument( + !destModels.stream() + .anyMatch(o -> !(o instanceof PointModel || o instanceof LocationModel)), + "destModels have to be a PointModel or a Locationmodel" + ); + + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + TransportOrderService transportOrderService + = sharedPortal.getPortal().getTransportOrderService(); + List destinations = new ArrayList<>(); + for (int i = 0; i < destModels.size(); i++) { + AbstractConnectableModelComponent locModel = destModels.get(i); + String action = actions.get(i); + Map properties = new HashMap<>(); + if (!propertiesList.isEmpty()) { + properties = propertiesList.get(i); + } + Location location = transportOrderService.fetchObject(Location.class, locModel.getName()); + DestinationCreationTO destination; + if (location == null) { + Point point = transportOrderService.fetchObject(Point.class, locModel.getName()); + destination = new DestinationCreationTO(point.getName(), action) + .withDestLocationName(point.getName()); + } + else { + destination = new DestinationCreationTO(location.getName(), action) + .withDestLocationName(location.getName()) + .withProperties(properties); + } + destinations.add(destination); + } + + transportOrderService.createTransportOrder( + new TransportOrderCreationTO("TOrder-", destinations) + .withIncompleteName(true) + .withDeadline(Instant.ofEpochMilli(deadline)) + .withIntendedVehicleName(vModel == null ? null : vModel.getName()) + .withType(type) + ); + + sharedPortal.getPortal().getDispatcherService().dispatch(); + } + catch (KernelRuntimeException | IllegalArgumentException e) { + LOG.warn("Unexpected exception", e); + } + } + + /** + * Creates a new transport order by copying the given one. + * + * @param pattern The transport order that server as a pattern. + */ + public void createTransportOrder(TransportOrder pattern) { + requireNonNull(pattern, "pattern"); + + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + + sharedPortal.getPortal().getTransportOrderService().createTransportOrder( + new TransportOrderCreationTO("TOrder-", copyDestinations(pattern)) + .withIncompleteName(true) + .withDeadline(pattern.getDeadline()) + .withIntendedVehicleName( + pattern.getIntendedVehicle() == null + ? null + : pattern.getIntendedVehicle().getName() + ) + .withType(pattern.getType()) + .withProperties(pattern.getProperties()) + ); + + sharedPortal.getPortal().getDispatcherService().dispatch(); + } + catch (KernelRuntimeException e) { + LOG.warn("Unexpected exception", e); + } + } + + /** + * Creates a new transport order for the purpose to drive to a point. + * + * @param pointModel The point that shall be driven to. + * @param vModel The vehicle to execute this order. + */ + public void createTransportOrder(PointModel pointModel, VehicleModel vModel) { + requireNonNull(pointModel, "point"); + requireNonNull(vModel, "vehicle"); + + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + // This is only allowed in operating mode. + if (sharedPortal.getPortal().getState() != Kernel.State.OPERATING) { + return; + } + + sharedPortal.getPortal().getTransportOrderService().createTransportOrder( + new TransportOrderCreationTO( + "Move-", + Collections.singletonList( + new DestinationCreationTO( + pointModel.getName(), + DriveOrder.Destination.OP_MOVE + ) + ) + ) + .withIncompleteName(true) + .withDeadline(Instant.now()) + .withIntendedVehicleName(vModel.getName()) + ); + + sharedPortal.getPortal().getDispatcherService().dispatch(); + } + catch (KernelRuntimeException e) { + LOG.warn("Unexpected exception", e); + } + } + + private List copyDestinations(TransportOrder original) { + List result = new ArrayList<>(); + for (DriveOrder driveOrder : original.getAllDriveOrders()) { + result.add(copyDestination(driveOrder)); + } + return result; + } + + private DestinationCreationTO copyDestination(DriveOrder driveOrder) { + return new DestinationCreationTO( + driveOrder.getDestination().getDestination().getName(), + driveOrder.getDestination().getOperation() + ) + .withProperties(driveOrder.getDestination().getProperties()); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/LocationLockAdapter.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/LocationLockAdapter.java new file mode 100644 index 0000000..ae0c118 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/LocationLockAdapter.java @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.exchange.adapter; + +import static java.util.Objects.requireNonNull; + +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.components.kernel.services.ServiceUnavailableException; +import org.opentcs.data.model.Location; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.event.AttributesChangeListener; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Updates a location's lock state with the kernel when it changes. + */ +public class LocationLockAdapter + implements + AttributesChangeListener { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(LocationLockAdapter.class); + /** + * The location model. + */ + private final LocationModel model; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * Indicates whether the location was locked the last time we checked. + */ + private boolean lockedPreviously; + + /** + * Creates a new instance. + * + * @param portalProvider A portal provider. + * @param model The location model. + */ + public LocationLockAdapter( + SharedKernelServicePortalProvider portalProvider, + LocationModel model + ) { + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.model = requireNonNull(model, "model"); + this.lockedPreviously = isLocationLocked(); + } + + @Override + public void propertiesChanged(AttributesChangeEvent e) { + if (e.getModel() != model) { + return; + } + + boolean locked = isLocationLocked(); + if (locked == lockedPreviously) { + return; + } + lockedPreviously = locked; + + new Thread(() -> updateLockInKernel(locked)).start(); + } + + private boolean isLocationLocked() { + return (Boolean) model.getPropertyLocked().getValue(); + } + + private void updateLockInKernel(boolean locked) { + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + KernelServicePortal portal = sharedPortal.getPortal(); + // Check if the kernel is in operating mode, too. + if (portal.getState() == Kernel.State.OPERATING) { + // Update the path in the kernel if it exists and its locked state is different. + Location location = portal.getPlantModelService().fetchObject( + Location.class, + model.getName() + ); + if (location != null && location.isLocked() != locked) { + portal.getPlantModelService().updateLocationLock(location.getReference(), locked); + } + } + + } + catch (ServiceUnavailableException exc) { + LOG.warn("Could not connect to kernel", exc); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/OpsDeskVehicleAdapter.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/OpsDeskVehicleAdapter.java new file mode 100644 index 0000000..510cdc4 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/OpsDeskVehicleAdapter.java @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.exchange.adapter; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.data.order.TransportOrder.State.WITHDRAWN; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.opentcs.access.CredentialsException; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.guing.base.AllocationState; +import org.opentcs.guing.base.model.FigureDecorationDetails; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.exchange.AllocationHistory; +import org.opentcs.guing.common.exchange.adapter.VehicleAdapter; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.operationsdesk.transport.orders.TransportOrdersContainer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An adapter for vehicles specific to the Operations Desk application. + */ +public class OpsDeskVehicleAdapter + extends + VehicleAdapter { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(OpsDeskVehicleAdapter.class); + /** + * Keeps track of the resources claimed and allocated by vehicles. + */ + private final AllocationHistory allocationHistory; + /** + * Maintains a set of all transport orders. + */ + private final TransportOrdersContainer transportOrdersContainer; + + @Inject + public OpsDeskVehicleAdapter( + AllocationHistory allocationHistory, + @Nonnull + TransportOrdersContainer transportOrdersContainer + ) { + this.allocationHistory = requireNonNull(allocationHistory, "allocationHistory"); + this.transportOrdersContainer = requireNonNull( + transportOrdersContainer, + "transportOrdersContainer" + ); + } + + @Override + protected void updateModelDriveOrder( + TCSObjectService objectService, + Vehicle vehicle, + VehicleModel vehicleModel, + SystemModel systemModel + ) + throws CredentialsException { + TransportOrder transportOrder = getTransportOrder(objectService, vehicle.getTransportOrder()); + + if (transportOrder != null) { + vehicleModel.setCurrentDriveOrderPath(getCurrentDriveOrderPath(vehicle, systemModel)); + vehicleModel.setDriveOrderDestination( + getCurrentDriveOrderDestination( + transportOrder.getCurrentDriveOrder(), + systemModel + ) + ); + + vehicleModel.setDriveOrderState(transportOrder.getState()); + } + else { + vehicleModel.setCurrentDriveOrderPath(null); + vehicleModel.setDriveOrderDestination(null); + } + + updateAllocationStates(vehicle, systemModel, vehicleModel); + } + + @Nullable + private TransportOrder getTransportOrder( + TCSObjectService objectService, + TCSObjectReference ref + ) + throws CredentialsException { + if (ref == null) { + return null; + } + return transportOrdersContainer.getTransportOrder(ref.getName()).orElse(null); + } + + private PathModel getCurrentDriveOrderPath(Vehicle vehicle, SystemModel systemModel) { + if (!vehicle.isProcessingOrder()) { + return null; + } + + return Stream.concat( + vehicle.getAllocatedResources().stream(), + vehicle.getClaimedResources().stream() + ) + .dropWhile( + resources -> !containsPointWithName(resources, vehicle.getCurrentPosition().getName()) + ) + // Skip the resource set containing the vehicle's current position. + .skip(1) + // Get the resource set after the one containing the vehicle's current position. + .findFirst() + .map(resourceSet -> extractPath(resourceSet)) + .map(path -> systemModel.getPathModel(path.getName())) + .orElse(null); + } + + private boolean containsPointWithName(Set> resources, String pointName) { + return resources.stream() + .filter(resource -> resource.getReferentClass().isAssignableFrom(Point.class)) + .anyMatch(resource -> Objects.equals(resource.getName(), pointName)); + } + + private TCSResourceReference extractPath(Set> resources) { + return resources.stream() + .filter(resource -> resource.getReferentClass().isAssignableFrom(Path.class)) + .findFirst() + .orElse(null); + } + + private PointModel getCurrentDriveOrderDestination( + @Nullable + DriveOrder driveOrder, + SystemModel systemModel + ) { + if (driveOrder == null) { + return null; + } + + List routeSteps = driveOrder.getRoute().getSteps(); + return systemModel.getPointModel( + routeSteps.get(routeSteps.size() - 1).getDestinationPoint().getName() + ); + } + + private void updateAllocationStates( + Vehicle vehicle, + SystemModel systemModel, + VehicleModel vehicleModel + ) { + AllocationHistory.Entry entry = allocationHistory.updateHistory(vehicle); + + for (FigureDecorationDetails component : toFigureDecorationDetails( + entry.getCurrentClaimedResources(), systemModel + )) { + component.updateAllocationState(vehicleModel, AllocationState.CLAIMED); + } + + for (FigureDecorationDetails component : toFigureDecorationDetails( + entry.getCurrentAllocatedResourcesAhead(), systemModel + )) { + if (vehicleModel.getDriveOrderState() == WITHDRAWN) { + component.updateAllocationState(vehicleModel, AllocationState.ALLOCATED_WITHDRAWN); + } + else { + component.updateAllocationState(vehicleModel, AllocationState.ALLOCATED); + } + } + + for (FigureDecorationDetails component : toFigureDecorationDetails( + entry.getCurrentAllocatedResourcesBehind(), systemModel + )) { + component.updateAllocationState(vehicleModel, AllocationState.ALLOCATED); + } + + for (FigureDecorationDetails component : toFigureDecorationDetails( + entry.getPreviouslyClaimedOrAllocatedResources(), + systemModel + )) { + component.clearAllocationState(vehicleModel); + } + } + + private Set toFigureDecorationDetails( + Set> resources, + SystemModel systemModel + ) { + return resources.stream() + .map(res -> systemModel.getModelComponent(res.getName())) + .filter(modelComponent -> modelComponent instanceof FigureDecorationDetails) + .map(modelComponent -> (FigureDecorationDetails) modelComponent) + .collect(Collectors.toSet()); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/PathLockAdapter.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/PathLockAdapter.java new file mode 100644 index 0000000..4bca57c --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/PathLockAdapter.java @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.exchange.adapter; + +import static java.util.Objects.requireNonNull; + +import java.util.Set; +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.components.kernel.services.ServiceUnavailableException; +import org.opentcs.data.model.Path; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.event.AttributesChangeListener; +import org.opentcs.guing.base.model.elements.PathModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Updates a path's lock state with the kernel when it changes. + */ +public class PathLockAdapter + implements + AttributesChangeListener { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PathLockAdapter.class); + /** + * The path model. + */ + private final PathModel model; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * Indicates whether the path was locked the last time we checked. + */ + private boolean lockedPreviously; + + /** + * Creates a new instance. + * + * @param portalProvider A portal provider. + * @param model The path model. + */ + public PathLockAdapter( + SharedKernelServicePortalProvider portalProvider, + PathModel model + ) { + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.model = requireNonNull(model, "model"); + this.lockedPreviously = isPathLocked(); + } + + @Override + public void propertiesChanged(AttributesChangeEvent e) { + if (e.getModel() != model) { + return; + } + + boolean locked = isPathLocked(); + if (locked == lockedPreviously) { + return; + } + lockedPreviously = locked; + + new Thread(() -> updateLockInKernel(locked)).start(); + } + + private boolean isPathLocked() { + return (Boolean) model.getPropertyLocked().getValue(); + } + + private void updateLockInKernel(boolean locked) { + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + KernelServicePortal portal = sharedPortal.getPortal(); + // Check if the kernel is in operating mode, too. + if (portal.getState() == Kernel.State.OPERATING) { + // Update the path in the kernel if it exists and its locked state is different. + Path path = portal.getPlantModelService().fetchObject(Path.class, model.getName()); + if (path != null && path.isLocked() != locked) { + portal.getPlantModelService().updatePathLock(path.getReference(), locked); + portal.getRouterService().updateRoutingTopology(Set.of(path.getReference())); + portal.getDispatcherService().rerouteAll(ReroutingType.REGULAR); + } + } + + } + catch (ServiceUnavailableException exc) { + LOG.warn("Could not connect to kernel", exc); + } + } + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/VehicleAllowedOrderTypesAdapter.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/VehicleAllowedOrderTypesAdapter.java new file mode 100644 index 0000000..2ab2b7f --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/VehicleAllowedOrderTypesAdapter.java @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.exchange.adapter; + +import static java.util.Objects.requireNonNull; + +import java.util.Set; +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.components.kernel.services.ServiceUnavailableException; +import org.opentcs.data.model.Vehicle; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.event.AttributesChangeListener; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Updates a vehicle's allowed order types with the kernel when it changes. + */ +public class VehicleAllowedOrderTypesAdapter + implements + AttributesChangeListener { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(VehicleAllowedOrderTypesAdapter.class); + /** + * The vehicle model. + */ + private final VehicleModel model; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * The vehicle's allowed order types the last time we checked them. + */ + private Set previousAllowedOrderTypes; + + /** + * Creates a new instance. + * + * @param portalProvider A kernel provider. + * @param model The vehicle model. + */ + public VehicleAllowedOrderTypesAdapter( + SharedKernelServicePortalProvider portalProvider, + VehicleModel model + ) { + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.model = requireNonNull(model, "model"); + this.previousAllowedOrderTypes = getAllowedOrderTypes(); + } + + @Override + public void propertiesChanged(AttributesChangeEvent e) { + if (e.getModel() != model) { + return; + } + + Set allowedOrderTypes = getAllowedOrderTypes(); + if (previousAllowedOrderTypes.equals(allowedOrderTypes)) { + LOG.debug("Ignoring vehicle properties update as the allowed order types did not change"); + return; + } + + previousAllowedOrderTypes = allowedOrderTypes; + new Thread(() -> updateAllowedOrderTypesInKernel(allowedOrderTypes)).start(); + } + + private Set getAllowedOrderTypes() { + return model.getPropertyAllowedOrderTypes().getItems(); + } + + private void updateAllowedOrderTypesInKernel(Set allowedOrderTypes) { + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + KernelServicePortal portal = sharedPortal.getPortal(); + // Check if the kernel is in operating mode, too. + if (portal.getState() == Kernel.State.OPERATING) { + Vehicle vehicle = portal.getVehicleService().fetchObject(Vehicle.class, model.getName()); + if (vehicle.getAllowedOrderTypes().size() == allowedOrderTypes.size() + && vehicle.getAllowedOrderTypes().containsAll(allowedOrderTypes)) { + LOG.debug("Ignoring vehicle properties update. Already up do date."); + return; + } + portal.getVehicleService().updateVehicleAllowedOrderTypes( + vehicle.getReference(), + allowedOrderTypes + ); + } + + } + catch (ServiceUnavailableException exc) { + LOG.warn("Could not connect to kernel", exc); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/VehicleEnergyLevelThresholdSetAdapter.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/VehicleEnergyLevelThresholdSetAdapter.java new file mode 100644 index 0000000..9594526 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/VehicleEnergyLevelThresholdSetAdapter.java @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.exchange.adapter; + +import static java.util.Objects.requireNonNull; + +import java.util.Objects; +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.components.kernel.services.ServiceUnavailableException; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.model.Vehicle.EnergyLevelThresholdSet; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.event.AttributesChangeListener; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Updates a vehicle's energy level threshold set. + */ +public class VehicleEnergyLevelThresholdSetAdapter + implements + AttributesChangeListener { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger( + VehicleEnergyLevelThresholdSetAdapter.class + ); + /** + * The vehicle model. + */ + private final VehicleModel model; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * The vehicle's energy level threshold set the last time we checked it. + */ + private Vehicle.EnergyLevelThresholdSet previousEnergyLevelThresholdSet; + + public VehicleEnergyLevelThresholdSetAdapter( + SharedKernelServicePortalProvider portalProvider, + VehicleModel model + ) { + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.model = requireNonNull(model, "model"); + this.previousEnergyLevelThresholdSet = getEnergyLevelThresholdSet(); + } + + @Override + public void propertiesChanged(AttributesChangeEvent e) { + if (e.getModel() != model) { + return; + } + + EnergyLevelThresholdSet energyLevelThresholdSet = getEnergyLevelThresholdSet(); + if (Objects.equals(previousEnergyLevelThresholdSet, energyLevelThresholdSet)) { + LOG.debug( + "Ignoring vehicle properties update as the energy level threshold set did not change." + ); + return; + } + + previousEnergyLevelThresholdSet = energyLevelThresholdSet; + new Thread(() -> updateEnergyLevelThresholdSetInKernel(energyLevelThresholdSet)).start(); + } + + private EnergyLevelThresholdSet getEnergyLevelThresholdSet() { + return new EnergyLevelThresholdSet( + model.getPropertyEnergyLevelThresholdSet().getValue().getEnergyLevelCritical(), + model.getPropertyEnergyLevelThresholdSet().getValue().getEnergyLevelGood(), + model.getPropertyEnergyLevelThresholdSet().getValue().getEnergyLevelSufficientlyRecharged(), + model.getPropertyEnergyLevelThresholdSet().getValue().getEnergyLevelFullyRecharged() + ); + } + + private void updateEnergyLevelThresholdSetInKernel( + EnergyLevelThresholdSet energyLevelThresholdSet + ) { + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + KernelServicePortal portal = sharedPortal.getPortal(); + // Check if the kernel is in operating mode, too. + if (portal.getState() == Kernel.State.OPERATING) { + portal.getVehicleService().updateVehicleEnergyLevelThresholdSet( + model.getVehicle().getReference(), + energyLevelThresholdSet + ); + } + + } + catch (ServiceUnavailableException exc) { + LOG.warn("Could not connect to kernel", exc); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/VehicleEnvelopeKeyAdapter.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/VehicleEnvelopeKeyAdapter.java new file mode 100644 index 0000000..6ff1a96 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/VehicleEnvelopeKeyAdapter.java @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.exchange.adapter; + +import static java.util.Objects.requireNonNull; + +import java.util.Objects; +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.components.kernel.services.ServiceUnavailableException; +import org.opentcs.data.model.Vehicle; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.event.AttributesChangeListener; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Updates a vehicle's envelope key with the kernel when it changes. + */ +public class VehicleEnvelopeKeyAdapter + implements + AttributesChangeListener { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(VehicleEnvelopeKeyAdapter.class); + /** + * The vehicle model. + */ + private final VehicleModel model; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * The vehicle's envelope key the last time we checked it. + */ + private String previousEnvelopeKey; + + /** + * Creates a new instance. + * + * @param portalProvider A kernel provider. + * @param model The vehicle model. + */ + public VehicleEnvelopeKeyAdapter( + SharedKernelServicePortalProvider portalProvider, + VehicleModel model + ) { + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.model = requireNonNull(model, "model"); + this.previousEnvelopeKey = getEnvelopeKey(); + } + + @Override + public void propertiesChanged(AttributesChangeEvent e) { + if (e.getModel() != model) { + return; + } + + String envelopeKey = getEnvelopeKey(); + if (Objects.equals(previousEnvelopeKey, envelopeKey)) { + LOG.debug("Ignoring vehicle properties update as the envelope key did not change."); + return; + } + + previousEnvelopeKey = envelopeKey; + new Thread(() -> updateEnvelopeKeyInKernel(envelopeKey)).start(); + } + + private String getEnvelopeKey() { + return model.getPropertyEnvelopeKey().getText(); + } + + private void updateEnvelopeKeyInKernel(String envelopeKey) { + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + KernelServicePortal portal = sharedPortal.getPortal(); + // Check if the kernel is in operating mode, too. + if (portal.getState() == Kernel.State.OPERATING) { + Vehicle vehicle = portal.getVehicleService().fetchObject(Vehicle.class, model.getName()); + if (Objects.equals(vehicle.getEnvelopeKey(), envelopeKey)) { + LOG.debug("Ignoring vehicle properties update. Already up do date."); + return; + } + portal.getVehicleService().updateVehicleEnvelopeKey( + vehicle.getReference(), + envelopeKey + ); + } + + } + catch (ServiceUnavailableException exc) { + LOG.warn("Could not connect to kernel", exc); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/VehiclePausedAdapter.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/VehiclePausedAdapter.java new file mode 100644 index 0000000..b2d7389 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/exchange/adapter/VehiclePausedAdapter.java @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.exchange.adapter; + +import static java.util.Objects.requireNonNull; + +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.components.kernel.services.ServiceUnavailableException; +import org.opentcs.data.model.Vehicle; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.event.AttributesChangeListener; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Updates a vehicle's paused state with the kernel when it changes. + */ +public class VehiclePausedAdapter + implements + AttributesChangeListener { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(VehiclePausedAdapter.class); + /** + * The vehicle model. + */ + private final VehicleModel model; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * The vehicle's paused state the last time we checked it. + */ + private boolean pausedPreviously; + + /** + * Creates a new instance. + * + * @param portalProvider A kernel provider. + * @param model The vehicle model. + */ + public VehiclePausedAdapter( + SharedKernelServicePortalProvider portalProvider, + VehicleModel model + ) { + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.model = requireNonNull(model, "model"); + this.pausedPreviously = isVehiclePaused(); + } + + @Override + public void propertiesChanged(AttributesChangeEvent e) { + if (e.getModel() != model) { + return; + } + + boolean paused = isVehiclePaused(); + if (paused == pausedPreviously) { + return; + } + pausedPreviously = paused; + + new Thread(() -> updatePausedInKernel(paused)).start(); + } + + private boolean isVehiclePaused() { + return Boolean.TRUE.equals(model.getPropertyPaused().getValue()); + } + + private void updatePausedInKernel(boolean paused) { + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + KernelServicePortal portal = sharedPortal.getPortal(); + + // Check if the kernel is in operating mode, too. + if (portal.getState() == Kernel.State.OPERATING) { + Vehicle vehicle = portal.getVehicleService().fetchObject(Vehicle.class, model.getName()); + if (vehicle != null && vehicle.isPaused() != paused) { + portal.getVehicleService().updateVehiclePaused(vehicle.getReference(), paused); + } + } + } + catch (ServiceUnavailableException exc) { + LOG.warn("Could not connect to kernel", exc); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/model/CachedSystemModel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/model/CachedSystemModel.java new file mode 100644 index 0000000..fb5ed28 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/model/CachedSystemModel.java @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.model; + +import jakarta.inject.Inject; +import java.util.HashMap; +import java.util.Map; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.model.StandardSystemModel; +import org.opentcs.guing.common.util.ModelComponentFactory; + +/** + * Extends the standard system model with a cache of model components + * to provide a more efficient lookup of components by name. + */ +public class CachedSystemModel + extends + StandardSystemModel { + + /** + * A map from component name to the actual component. + */ + private final Map components = new HashMap<>(); + + @Inject + public CachedSystemModel(ModelComponentFactory modelComponentFactory) { + super(modelComponentFactory); + } + + @Override + public void onRestorationComplete() { + components.clear(); + for (ModelComponent component : getAll()) { + components.put(component.getName(), component); + } + } + + @Override + public BlockModel getBlockModel(String name) { + if (components.isEmpty()) { + return super.getBlockModel(name); + } + + ModelComponent block = components.get(name); + if (block instanceof BlockModel) { + return (BlockModel) block; + } + return null; + } + + @Override + public LocationModel getLocationModel(String name) { + if (components.isEmpty()) { + return super.getLocationModel(name); + } + + ModelComponent location = components.get(name); + if (location instanceof LocationModel) { + return (LocationModel) location; + } + return null; + } + + @Override + public LocationTypeModel getLocationTypeModel(String name) { + if (components.isEmpty()) { + return super.getLocationTypeModel(name); + } + + ModelComponent locationType = components.get(name); + if (locationType instanceof LocationTypeModel) { + return (LocationTypeModel) locationType; + } + return null; + } + + @Override + public ModelComponent getModelComponent(String name) { + if (components.isEmpty()) { + return super.getModelComponent(name); + } + return components.get(name); + } + + @Override + public PathModel getPathModel(String name) { + if (components.isEmpty()) { + return super.getPathModel(name); + } + + ModelComponent path = components.get(name); + if (path instanceof PathModel) { + return (PathModel) path; + } + return null; + } + + @Override + public PointModel getPointModel(String name) { + if (components.isEmpty()) { + return super.getPointModel(name); + } + + ModelComponent point = components.get(name); + if (point instanceof PointModel) { + return (PointModel) point; + } + return null; + } + + @Override + public VehicleModel getVehicleModel(String name) { + if (components.isEmpty()) { + return super.getVehicleModel(name); + } + + ModelComponent vehicle = components.get(name); + if (vehicle instanceof VehicleModel) { + return (VehicleModel) vehicle; + } + return null; + } + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationContainerListener.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationContainerListener.java new file mode 100644 index 0000000..fd78ec8 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationContainerListener.java @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.notifications; + +import java.util.List; +import org.opentcs.data.notification.UserNotification; + +/** + * Listener for changes in the {@link UserNotificationsContainerPanel}. + */ +public interface UserNotificationContainerListener { + + /** + * Notifies the listener that the container has been initialized. + * + * @param notifications The notifications the container has been initialized with. + */ + void containerInitialized(List notifications); + + /** + * Notifies the listener that a user notification has been added. + * + * @param notification The user notification that has been added. + */ + void userNotificationAdded(UserNotification notification); + + /** + * Notifies the listener that a user notification has been removed. + * + * @param notification The removed notification. + */ + void userNotificationRemoved(UserNotification notification); + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationTableModel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationTableModel.java new file mode 100644 index 0000000..4d17380 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationTableModel.java @@ -0,0 +1,168 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.notifications; + +import static java.util.Objects.requireNonNull; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.ResourceBundle; +import javax.swing.SwingUtilities; +import javax.swing.table.AbstractTableModel; +import org.opentcs.data.notification.UserNotification; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; + +/** + * A table model for user notifications. + */ +public class UserNotificationTableModel + extends + AbstractTableModel + implements + UserNotificationContainerListener { + + /** + * The indexes of the time column. + */ + public static final int COLUMN_TIME = 0; + /** + * The indexes of the level column. + */ + public static final int COLUMN_LEVEL = 1; + /** + * The indexes of the source column. + */ + public static final int COLUMN_SOURCE = 2; + /** + * The indexes of the text column. + */ + public static final int COLUMN_TEXT = 3; + + /** + * The resource bundle to use. + */ + private static final ResourceBundle BUNDLE + = ResourceBundle.getBundle(I18nPlantOverviewOperating.USERNOTIFICATION_PATH); + + private static final String[] COLUMN_NAMES = new String[]{ + BUNDLE.getString("userNotificationTableModel.column_time.headerText"), + BUNDLE.getString("userNotificationTableModel.column_level.headerText"), + BUNDLE.getString("userNotificationTableModel.column_source.headerText"), + BUNDLE.getString("userNotificationTableModel.column_text.headerText") + }; + + /** + * The column classes. + */ + private static final Class[] COLUMN_CLASSES = new Class[]{ + Instant.class, + String.class, + String.class, + String.class + }; + + private final List entries = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public UserNotificationTableModel() { + } + + @Override + public int getRowCount() { + return entries.size(); + } + + @Override + public int getColumnCount() { + return COLUMN_NAMES.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= getRowCount()) { + return null; + } + + UserNotification entry = entries.get(rowIndex); + + switch (columnIndex) { + case COLUMN_TIME: + return entry.getTimestamp(); + case COLUMN_LEVEL: + return entry.getLevel().name(); + case COLUMN_SOURCE: + if (entry.getSource() != null) { + return entry.getSource(); + } + else { + return "-"; + } + case COLUMN_TEXT: + return entry.getText(); + default: + throw new IllegalArgumentException("Invalid column index: " + columnIndex); + } + } + + @Override + public String getColumnName(int columnIndex) { + return COLUMN_NAMES[columnIndex]; + } + + @Override + public Class getColumnClass(int columnIndex) { + return COLUMN_CLASSES[columnIndex]; + } + + @Override + public void containerInitialized(List notifications) { + requireNonNull(notifications, "notifications"); + + SwingUtilities.invokeLater(() -> { + // Notifications of any change listeners must happen at the same time/in the same thread the + // data behind the model is updated. Otherwise, there is a risk that listeners work with/ + // refer to outdated data, which can lead to runtime exceptions. + entries.clear(); + entries.addAll(notifications); + fireTableDataChanged(); + }); + } + + @Override + public void userNotificationAdded(UserNotification notification) { + requireNonNull(notification, "notification"); + + SwingUtilities.invokeLater(() -> { + entries.add(notification); + fireTableRowsInserted(entries.size() - 1, entries.size() - 1); + }); + } + + @Override + public void userNotificationRemoved(UserNotification notification) { + SwingUtilities.invokeLater(() -> { + int row = entries.indexOf(notification); + if (row != -1) { + entries.remove(notification); + fireTableRowsDeleted(row, row); + } + }); + } + + /** + * Returns the user notification at the specified index. + * + * @param index the index to return. + * @return the user notification at that index. + */ + public UserNotification getEntryAt(int index) { + if (index < 0 || index >= entries.size()) { + return null; + } + + return entries.get(index); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationView.form b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationView.form new file mode 100644 index 0000000..8183a0c --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationView.form @@ -0,0 +1,176 @@ + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationView.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationView.java new file mode 100644 index 0000000..3a52b8c --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationView.java @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.notifications; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import org.opentcs.data.notification.UserNotification; +import org.opentcs.guing.common.components.dialogs.DialogContent; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * A view of a user notification. + */ +public class UserNotificationView + extends + DialogContent { + + /** + * A formatter for timestamps. + */ + private static final DateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + /** + * The user notification to be shown. + */ + private final UserNotification fUserNotification; + + /** + * Creates new instance. + * + * @param notification The user notification. + */ + @Inject + @SuppressWarnings("this-escape") + public UserNotificationView( + @Assisted + UserNotification notification + ) { + this.fUserNotification = requireNonNull(notification, "notification"); + + initComponents(); + setDialogTitle( + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.UNDETAIL_PATH) + .getString("userNotificationView.title") + ); + } + + @Override + public void update() { + } + + @Override + public final void initFields() { + createdTextField.setText(TIMESTAMP_FORMAT.format(Date.from(fUserNotification.getTimestamp()))); + levelTextField.setText(fUserNotification.getLevel().name()); + sourceTextField.setText( + fUserNotification.getSource() == null ? "-" : fUserNotification.getSource() + ); + textTextArea.setText(fUserNotification.getText()); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + generalPanel = new javax.swing.JPanel(); + createdLabel = new javax.swing.JLabel(); + createdTextField = new javax.swing.JTextField(); + sourceLabel = new javax.swing.JLabel(); + sourceTextField = new javax.swing.JTextField(); + levelLabel = new javax.swing.JLabel(); + levelTextField = new javax.swing.JTextField(); + textPanel = new javax.swing.JPanel(); + jScrollPane1 = new javax.swing.JScrollPane(); + textTextArea = new javax.swing.JTextArea(); + + setLayout(new java.awt.GridBagLayout()); + + generalPanel.setLayout(new java.awt.GridBagLayout()); + + createdLabel.setFont(createdLabel.getFont()); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/operating/dialogs/userNotificationDetail"); // NOI18N + createdLabel.setText(bundle.getString("userNotificationView.label_created.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + generalPanel.add(createdLabel, gridBagConstraints); + + createdTextField.setEditable(false); + createdTextField.setColumns(10); + createdTextField.setFont(createdTextField.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 3, 0); + generalPanel.add(createdTextField, gridBagConstraints); + + sourceLabel.setFont(sourceLabel.getFont()); + sourceLabel.setText(bundle.getString("userNotificationView.label_source.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 8, 0, 4); + generalPanel.add(sourceLabel, gridBagConstraints); + + sourceTextField.setEditable(false); + sourceTextField.setColumns(10); + sourceTextField.setFont(sourceTextField.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 3; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 3, 0); + generalPanel.add(sourceTextField, gridBagConstraints); + + levelLabel.setFont(levelLabel.getFont()); + levelLabel.setText(bundle.getString("userNotificationView.label_level.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + generalPanel.add(levelLabel, gridBagConstraints); + + levelTextField.setEditable(false); + levelTextField.setColumns(10); + levelTextField.setFont(levelTextField.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 3, 0); + generalPanel.add(levelTextField, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 0.1; + add(generalPanel, gridBagConstraints); + + textPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("userNotificationView.label_text.text"))); // NOI18N + textPanel.setLayout(new java.awt.GridBagLayout()); + + textTextArea.setEditable(false); + textTextArea.setColumns(40); + textTextArea.setLineWrap(true); + textTextArea.setRows(5); + textTextArea.setWrapStyleWord(true); + jScrollPane1.setViewportView(textTextArea); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + textPanel.add(jScrollPane1, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weighty = 1.0; + add(textPanel, gridBagConstraints); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel createdLabel; + private javax.swing.JTextField createdTextField; + private javax.swing.JPanel generalPanel; + private javax.swing.JScrollPane jScrollPane1; + private javax.swing.JLabel levelLabel; + private javax.swing.JTextField levelTextField; + private javax.swing.JLabel sourceLabel; + private javax.swing.JTextField sourceTextField; + private javax.swing.JPanel textPanel; + private javax.swing.JTextArea textTextArea; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationViewFactory.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationViewFactory.java new file mode 100644 index 0000000..a96bb9c --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationViewFactory.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.notifications; + +import org.opentcs.data.notification.UserNotification; + +/** + * Creates user notification related GUI components. + */ +public interface UserNotificationViewFactory { + + /** + * Creates a new view for a user notification. + * + * @param notification The user notification to be shown. + * @return A new view for a user notification. + */ + UserNotificationView createUserNotificationView(UserNotification notification); + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationsContainer.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationsContainer.java new file mode 100644 index 0000000..c088207 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationsContainer.java @@ -0,0 +1,201 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.notifications; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.NotificationPublicationEvent; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.common.KernelClientApplication; +import org.opentcs.components.Lifecycle; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.notification.UserNotification; +import org.opentcs.guing.common.event.OperationModeChangeEvent; +import org.opentcs.guing.common.event.SystemModelTransitionEvent; +import org.opentcs.operationsdesk.event.KernelStateChangeEvent; +import org.opentcs.operationsdesk.util.OperationsDeskConfiguration; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Maintains a list of the most recent user notifications. + */ +public class UserNotificationsContainer + implements + EventHandler, + Lifecycle { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(UserNotificationsContainer.class); + /** + * Where we get events from. + */ + private final EventBus eventBus; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * The kernel client application. + */ + private final KernelClientApplication kernelClientApplication; + /** + * The user notifications. + */ + private final List userNotifications = new LinkedList<>(); + /** + * This container's listeners. + */ + private final Set listeners = new HashSet<>(); + /** + * The amount of user notifications to be kept in the container. + * Configurable through the operation desk's configuration. + */ + private final int capacity; + /** + * Whether this component is initialized. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param eventBus Where this instance subscribes for events. + * @param portalProvider Provides access to a portal. + * @param kernelClientApplication The kernel client application. + * @param configuration The operations desk application's configuration. + */ + @Inject + public UserNotificationsContainer( + @ApplicationEventBus + EventBus eventBus, + SharedKernelServicePortalProvider portalProvider, + KernelClientApplication kernelClientApplication, + OperationsDeskConfiguration configuration + ) { + this.eventBus = requireNonNull(eventBus, "eventBus"); + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.kernelClientApplication + = requireNonNull(kernelClientApplication, "kernelClientApplication"); + this.capacity = requireNonNull(configuration, "configuration").userNotificationDisplayCount(); + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + eventBus.subscribe(this); + + initialized = true; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + eventBus.unsubscribe(this); + + initialized = false; + } + + @Override + public void onEvent(Object event) { + if (event instanceof NotificationPublicationEvent) { + handleNotificationEvent((NotificationPublicationEvent) event); + } + else if (event instanceof OperationModeChangeEvent) { + initNotifications(); + } + else if (event instanceof SystemModelTransitionEvent) { + initNotifications(); + } + else if (event instanceof KernelStateChangeEvent) { + initNotifications(); + } + } + + public void addListener(UserNotificationContainerListener listener) { + listeners.add(listener); + } + + public void removeListener(UserNotificationContainerListener listener) { + listeners.remove(listener); + } + + /** + * Returns the user notification with the given index, if it exists. + * + * @param index The index of the user notification. + * @return The user notification with the given index, if it exists. + */ + public Optional getUserNotification(int index) { + return Optional.ofNullable(userNotifications.get(index)); + } + + /** + * Returns all currently stored user notifications. + * + * @return The collection of user notifications. + */ + public List getUserNotifications() { + return userNotifications; + } + + private void initNotifications() { + setUserNotifications(fetchNotificationsIfOnLine()); + while (userNotifications.size() > capacity) { + userNotifications.remove(0); + } + listeners.forEach(listener -> listener.containerInitialized(userNotifications)); + } + + private List fetchNotificationsIfOnLine() { + if (kernelClientApplication.isOnline()) { + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + return sharedPortal.getPortal().getNotificationService() + .fetchUserNotifications(null); + } + catch (KernelRuntimeException exc) { + LOG.warn("Exception fetching user notifications", exc); + } + } + + return List.of(); + } + + private void handleNotificationEvent(NotificationPublicationEvent evt) { + userNotifications.add(evt.getNotification()); + listeners.forEach(listener -> listener.userNotificationAdded(evt.getNotification())); + + while (userNotifications.size() > capacity) { + UserNotification removedNotification = userNotifications.remove(0); + listeners.forEach(listener -> listener.userNotificationRemoved(removedNotification)); + } + } + + private void setUserNotifications(List newNotifications) { + userNotifications.clear(); + userNotifications.addAll(newNotifications); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationsContainerPanel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationsContainerPanel.java new file mode 100644 index 0000000..13a4a3d --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/notifications/UserNotificationsContainerPanel.java @@ -0,0 +1,219 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.notifications; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.Arrays; +import java.util.Date; +import java.util.Optional; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; +import javax.swing.RowSorter; +import javax.swing.SortOrder; +import javax.swing.table.TableCellRenderer; +import org.opentcs.data.notification.UserNotification; +import org.opentcs.guing.common.components.dialogs.DialogContent; +import org.opentcs.guing.common.components.dialogs.StandardContentDialog; +import org.opentcs.operationsdesk.transport.FilteredRowSorter; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.util.gui.StringTableCellRenderer; + +/** + * Shows a table of the most recent user notifications. + */ +public class UserNotificationsContainerPanel + extends + JPanel { + + /** + * A formatter for timestamps. + */ + private static final DateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + /** + * A factory for creating user notification views. + */ + private final UserNotificationViewFactory notificationViewFactory; + /** + * The table showing the user notifications. + */ + private JTable fTable; + /** + * The table's model. + */ + private UserNotificationTableModel tableModel; + /** + * The sorter for the table. + */ + private FilteredRowSorter sorter; + /** + * Maintains a list of the most recent user notifications. + */ + private final UserNotificationsContainer userNotificationsContainer; + + /** + * Creates a new instance. + * + * @param notificationViewFactory A factory for creating user notification views. + * @param userNotificationsContainer Maintains a list of the most recent user notifications. + */ + @Inject + @SuppressWarnings("this-escape") + public UserNotificationsContainerPanel( + UserNotificationViewFactory notificationViewFactory, + UserNotificationsContainer userNotificationsContainer + ) { + this.notificationViewFactory = requireNonNull( + notificationViewFactory, + "notificationViewFactory" + ); + this.userNotificationsContainer = requireNonNull( + userNotificationsContainer, + "userNotificationsContainer" + ); + + initComponents(); + } + + /** + * Initializes this panel's contents. + */ + public void initView() { + tableModel.containerInitialized(userNotificationsContainer.getUserNotifications()); + } + + private void initComponents() { + setLayout(new BorderLayout()); + + tableModel = new UserNotificationTableModel(); + userNotificationsContainer.addListener(tableModel); + fTable = new JTable(tableModel); + fTable.setFocusable(false); + fTable.setRowSelectionAllowed(true); + fTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + + sorter = new FilteredRowSorter<>(tableModel); + // Sort the table by the creation instant. + sorter.setSortKeys( + Arrays.asList( + new RowSorter.SortKey(UserNotificationTableModel.COLUMN_TIME, SortOrder.DESCENDING) + ) + ); + // ...but prevent manual sorting. + for (int i = 0; i < fTable.getColumnCount(); i++) { + sorter.setSortable(i, false); + } + sorter.setSortsOnUpdates(true); + fTable.setRowSorter(sorter); + + JScrollPane scrollPane = new JScrollPane(fTable); + add(scrollPane, BorderLayout.CENTER); + + TableCellRenderer timeRenderer = new StringTableCellRenderer(instant -> { + if (instant == null) { + return "-"; + } + return TIMESTAMP_FORMAT.format(Date.from(instant)); + }); + fTable.getColumnModel() + .getColumn(fTable.convertColumnIndexToView(UserNotificationTableModel.COLUMN_TIME)) + .setCellRenderer(timeRenderer); + + fTable.getColumnModel() + .getColumn(fTable.convertColumnIndexToView(UserNotificationTableModel.COLUMN_TIME)) + .setMaxWidth(130); + fTable.getColumnModel() + .getColumn(fTable.convertColumnIndexToView(UserNotificationTableModel.COLUMN_TIME)) + .setPreferredWidth(130); + fTable.getColumnModel() + .getColumn(fTable.convertColumnIndexToView(UserNotificationTableModel.COLUMN_LEVEL)) + .setMaxWidth(100); + fTable.getColumnModel() + .getColumn(fTable.convertColumnIndexToView(UserNotificationTableModel.COLUMN_LEVEL)) + .setPreferredWidth(100); + fTable.getColumnModel() + .getColumn(fTable.convertColumnIndexToView(UserNotificationTableModel.COLUMN_SOURCE)) + .setMaxWidth(130); + fTable.getColumnModel() + .getColumn(fTable.convertColumnIndexToView(UserNotificationTableModel.COLUMN_SOURCE)) + .setPreferredWidth(130); + fTable.getColumnModel() + .getColumn(fTable.convertColumnIndexToView(UserNotificationTableModel.COLUMN_TEXT)) + .setPreferredWidth(300); + fTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); + + fTable.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent evt) { + if (evt.getButton() == MouseEvent.BUTTON1) { + if (evt.getClickCount() == 2) { + showSelectedUserNotification(); + } + } + + if (evt.getButton() == MouseEvent.BUTTON3) { + if (fTable.getSelectedRow() != -1) { + showPopupMenuForSelectedUserNotification(evt.getX(), evt.getY()); + } + } + } + }); + } + + private void showSelectedUserNotification() { + getSelectedUserNotification().ifPresent(userNotification -> { + DialogContent content = notificationViewFactory.createUserNotificationView(userNotification); + StandardContentDialog dialog + = new StandardContentDialog( + JOptionPane.getFrameForComponent(this), + content, + true, + StandardContentDialog.CLOSE + ); + dialog.setTitle( + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.UNDETAIL_PATH) + .getString("userNotificationView.title") + ); + dialog.setVisible(true); + }); + } + + private void showPopupMenuForSelectedUserNotification(int x, int y) { + boolean singleRowSelected = fTable.getSelectedRowCount() <= 1; + ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.USERNOTIFICATION_PATH); + JPopupMenu menu = new JPopupMenu(); + JMenuItem item = menu.add( + bundle.getString( + "userNotificationsContainerPanel.table_notifications.popupMenuItem_showDetails.text" + ) + ); + item.setEnabled(singleRowSelected); + item.addActionListener((ActionEvent evt) -> showSelectedUserNotification()); + + menu.show(fTable, x, y); + } + + private Optional getSelectedUserNotification() { + int row = fTable.convertRowIndexToModel(fTable.getSelectedRow()); + if (row == -1) { + return Optional.empty(); + } + + return Optional.of(tableModel.getEntryAt(row)); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobHistoryEntryFormatter.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobHistoryEntryFormatter.java new file mode 100644 index 0000000..a26d28b --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobHistoryEntryFormatter.java @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.peripherals.jobs; + +import static java.util.Objects.requireNonNull; + +import java.util.Optional; +import org.opentcs.components.plantoverview.ObjectHistoryEntryFormatter; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.data.peripherals.PeripheralJobHistoryCodes; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * A formatter for history events/entries related to {@link PeripheralJob}s. + */ +public class PeripheralJobHistoryEntryFormatter + implements + ObjectHistoryEntryFormatter { + + /** + * A bundle providing localized strings. + */ + private final ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.PJDETAIL_PATH); + + /** + * Creates a new instance. + */ + public PeripheralJobHistoryEntryFormatter() { + } + + @Override + public Optional apply(ObjectHistory.Entry entry) { + requireNonNull(entry, "entry"); + + switch (entry.getEventCode()) { + case PeripheralJobHistoryCodes.JOB_CREATED: + return Optional.of( + bundle.getString("peripheralJobHistoryEntryFormatter.code_jobCreated.text") + ); + case PeripheralJobHistoryCodes.JOB_REACHED_FINAL_STATE: + return Optional.of( + bundle.getString("peripheralJobHistoryEntryFormatter.code_jobReachedFinalState.text") + ); + + default: + return Optional.empty(); + } + } + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobTableModel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobTableModel.java new file mode 100644 index 0000000..927bc16 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobTableModel.java @@ -0,0 +1,213 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.peripherals.jobs; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.ResourceBundle; +import javax.swing.SwingUtilities; +import javax.swing.table.AbstractTableModel; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A table model for peripheral jobs. + */ +class PeripheralJobTableModel + extends + AbstractTableModel + implements + PeripheralJobsContainerListener { + + /** + * The ID for the 'name' column. + */ + public static final int COLUMN_NAME = 0; + /** + * The ID for the 'location' column. + */ + public static final int COLUMN_LOCATION = 1; + /** + * The ID for the 'operation' column. + */ + public static final int COLUMN_OPERATION = 2; + /** + * The ID for the 'related vehicle' column. + */ + public static final int COLUMN_RELATED_VEHICLE = 3; + /** + * The ID for the 'related order' column. + */ + public static final int COLUMN_RELATED_ORDER = 4; + /** + * The ID for the 'state' column. + */ + public static final int COLUMN_STATE = 5; + /** + * The ID for the 'creation time' column. + */ + public static final int COLUMN_CREATION_TIME = 6; + + private static final Logger LOG = LoggerFactory.getLogger(PeripheralJobTableModel.class); + /** + * The resource bundle to use. + */ + private static final ResourceBundle BUNDLE + = ResourceBundle.getBundle(I18nPlantOverviewOperating.PERIPHERALJOB_PATH); + + /** + * The column names. + */ + private static final String[] COLUMN_NAMES = new String[]{ + BUNDLE.getString("peripheralJobTableModel.column_name.headerText"), + BUNDLE.getString("peripheralJobTableModel.column_location.headerText"), + BUNDLE.getString("peripheralJobTableModel.column_operation.headerText"), + BUNDLE.getString("peripheralJobTableModel.column_relatedVehicle.headerText"), + BUNDLE.getString("peripheralJobTableModel.column_relatedTransportOrder.headerText"), + BUNDLE.getString("peripheralJobTableModel.column_state.headerText"), + BUNDLE.getString("peripheralJobTableModel.column_creationTime.headerText") + }; + /** + * The column classes. + */ + private static final Class[] COLUMN_CLASSES = new Class[]{ + String.class, + String.class, + String.class, + TCSObjectReference.class, + TCSObjectReference.class, + String.class, + Instant.class + }; + /** + * The entries in the table. + */ + private final List entries = new ArrayList<>(); + + /** + * Creates a new instance. + */ + PeripheralJobTableModel() { + } + + @Override + public int getRowCount() { + return entries.size(); + } + + @Override + public int getColumnCount() { + return COLUMN_NAMES.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= getRowCount()) { + return null; + } + + PeripheralJob entry = entries.get(rowIndex); + switch (columnIndex) { + case COLUMN_NAME: + return entry.getName(); + case COLUMN_LOCATION: + return entry.getPeripheralOperation().getLocation().getName(); + case COLUMN_OPERATION: + return entry.getPeripheralOperation().getOperation(); + case COLUMN_RELATED_VEHICLE: + return entry.getRelatedVehicle(); + case COLUMN_RELATED_ORDER: + return entry.getRelatedTransportOrder(); + case COLUMN_STATE: + return entry.getState().name(); + case COLUMN_CREATION_TIME: + return entry.getCreationTime(); + default: + throw new IllegalArgumentException("Invalid column index: " + columnIndex); + } + } + + @Override + public String getColumnName(int columnIndex) { + return COLUMN_NAMES[columnIndex]; + } + + @Override + public Class getColumnClass(int columnIndex) { + return COLUMN_CLASSES[columnIndex]; + } + + @Override + public void containerInitialized(Collection jobs) { + requireNonNull(jobs, "jobs"); + + SwingUtilities.invokeLater(() -> { + // Notifiations of any change listeners must happen at the same time/in the same thread the + // data behind the model is updated. Otherwise, there is a risk that listeners work with/ + // refer to outdated data, which can lead to runtime exceptions. + entries.clear(); + entries.addAll(jobs); + fireTableDataChanged(); + }); + } + + @Override + public void peripheralJobAdded(PeripheralJob job) { + requireNonNull(job, "job"); + + SwingUtilities.invokeLater(() -> { + entries.add(job); + fireTableRowsInserted(entries.size() - 1, entries.size() - 1); + }); + } + + @Override + public void peripheralJobUpdated(PeripheralJob job) { + requireNonNull(job, "job"); + + SwingUtilities.invokeLater(() -> { + int jobIndex = entries.indexOf(job); + if (jobIndex == -1) { + LOG.warn("Unknown job: {}. Ignoring job update.", job.getName()); + return; + } + + entries.set(jobIndex, job); + fireTableRowsUpdated(jobIndex, jobIndex); + }); + } + + @Override + public void peripheralJobRemoved(PeripheralJob job) { + requireNonNull(job, "job"); + + SwingUtilities.invokeLater(() -> { + int jobIndex = entries.indexOf(job); + if (jobIndex == -1) { + LOG.warn("Unknown job: {}. Ignoring job removal.", job.getName()); + return; + } + + entries.remove(jobIndex); + fireTableRowsDeleted(jobIndex, jobIndex); + }); + } + + public PeripheralJob getEntryAt(int index) { + checkArgument( + index >= 0 && index < entries.size(), + "index must be in 0..%d: %d", + entries.size(), + index + ); + return entries.get(index); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobView.form b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobView.form new file mode 100644 index 0000000..fa202ee --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobView.form @@ -0,0 +1,434 @@ + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobView.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobView.java new file mode 100644 index 0000000..5abc9dd --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobView.java @@ -0,0 +1,550 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.peripherals.jobs; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.awt.Component; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.Date; +import javax.swing.JComponent; +import javax.swing.JTable; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.DefaultTableModel; +import javax.swing.table.TableModel; +import org.opentcs.components.plantoverview.ObjectHistoryEntryFormatter; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.guing.common.components.dialogs.DialogContent; +import org.opentcs.operationsdesk.transport.CompositeObjectHistoryEntryFormatter; +import org.opentcs.operationsdesk.transport.UneditableTableModel; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * A view on a peripheral job. + */ +public class PeripheralJobView + extends + DialogContent { + + /** + * A formatter for timestamps. + */ + private static final DateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + /** + * A formatter for history entries. + */ + private final ObjectHistoryEntryFormatter historyEntryFormatter; + /** + * The peripheral job to be shown. + */ + private final PeripheralJob peripheralJob; + + /** + * Creates new instance. + * + * @param job The peripheral job. + * @param historyEntryFormatter A formatter for history entries. + */ + @Inject + @SuppressWarnings("this-escape") + public PeripheralJobView( + @Nonnull + @Assisted + PeripheralJob job, + @Nonnull + CompositeObjectHistoryEntryFormatter historyEntryFormatter + ) { + this.peripheralJob = requireNonNull(job, "job"); + this.historyEntryFormatter = requireNonNull(historyEntryFormatter, "historyEntryFormatter"); + + initComponents(); + setDialogTitle( + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.PJDETAIL_PATH) + .getString("peripheralJobView.title") + ); + } + + @Override + public void update() { + } + + @Override + public final void initFields() { + nameTextField.setText(peripheralJob.getName()); + + stateTextField.setText(peripheralJob.getState().name()); + + createdTextField.setText(TIMESTAMP_FORMAT.format(Date.from(peripheralJob.getCreationTime()))); + + finishedTextField.setText( + !peripheralJob.getFinishedTime().equals(Instant.MAX) + ? TIMESTAMP_FORMAT.format(Date.from(peripheralJob.getFinishedTime())) + : "-" + ); + + if (peripheralJob.getRelatedVehicle() != null) { + vehicleTextField.setText(peripheralJob.getRelatedVehicle().getName()); + } + else { + vehicleTextField.setText("-"); + } + + reservationTokenTextField.setText(peripheralJob.getReservationToken()); + + if (peripheralJob.getRelatedTransportOrder() != null) { + relatedTransportOrderTextField.setText(peripheralJob.getRelatedTransportOrder().getName()); + } + else { + relatedTransportOrderTextField.setText("-"); + } + + locationTextField.setText(peripheralJob.getPeripheralOperation().getLocation().getName()); + operationTextField.setText(peripheralJob.getPeripheralOperation().getOperation()); + triggerTextField.setText(peripheralJob.getPeripheralOperation().getExecutionTrigger().name()); + requireCompletionTextField.setText( + String.valueOf(peripheralJob.getPeripheralOperation().isCompletionRequired()) + ); + + propertiesTable.setModel(createPropertiesTableModel()); + + historyTable.setModel(createHistoryTableModel()); + historyTable.getColumnModel().getColumn(0).setPreferredWidth(100); + historyTable.getColumnModel().getColumn(1).setPreferredWidth(300); + historyTable.getColumnModel().getColumn(1).setCellRenderer(new ToolTipCellRenderer()); + } + + private TableModel createPropertiesTableModel() { + DefaultTableModel tableModel = new UneditableTableModel(); + + tableModel.setColumnIdentifiers( + new String[]{ + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.PJDETAIL_PATH) + .getString( + "peripheralJobView.table_properties.column_propertiesKey.headerText" + ), + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.PJDETAIL_PATH) + .getString( + "peripheralJobView.table_properties.column_propertiesValue.headerText" + ) + } + ); + peripheralJob.getProperties().entrySet().stream() + .sorted((e1, e2) -> e1.getKey().compareTo(e2.getKey())) + .forEach(entry -> { + tableModel.addRow(new String[]{entry.getKey(), entry.getValue()}); + }); + + return tableModel; + } + + private TableModel createHistoryTableModel() { + DefaultTableModel tableModel = new UneditableTableModel(); + + tableModel.setColumnIdentifiers( + new String[]{ + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.PJDETAIL_PATH) + .getString("peripheralJobView.table_history.column_timestamp.headerText"), + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.PJDETAIL_PATH) + .getString("peripheralJobView.table_history.column_event.headerText") + } + ); + + for (ObjectHistory.Entry entry : peripheralJob.getHistory().getEntries()) { + tableModel.addRow( + new String[]{ + TIMESTAMP_FORMAT.format(Date.from(entry.getTimestamp())), + historyEntryFormatter.apply(entry).get() + } + ); + } + + return tableModel; + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + generalPanel = new javax.swing.JPanel(); + nameLabel = new javax.swing.JLabel(); + nameTextField = new javax.swing.JTextField(); + createdLabel = new javax.swing.JLabel(); + createdTextField = new javax.swing.JTextField(); + finishedLabel = new javax.swing.JLabel(); + finishedTextField = new javax.swing.JTextField(); + vehicleLabel = new javax.swing.JLabel(); + vehicleTextField = new javax.swing.JTextField(); + reservationTokenLabel = new javax.swing.JLabel(); + reservationTokenTextField = new javax.swing.JTextField(); + relatedTransportOrderLabel = new javax.swing.JLabel(); + relatedTransportOrderTextField = new javax.swing.JTextField(); + stateLabel = new javax.swing.JLabel(); + stateTextField = new javax.swing.JTextField(); + propertiesPanel = new javax.swing.JPanel(); + propertiesScrollPane = new javax.swing.JScrollPane(); + propertiesTable = new javax.swing.JTable(); + historyPanel = new javax.swing.JPanel(); + historyScrollPane = new javax.swing.JScrollPane(); + historyTable = new javax.swing.JTable(); + operationPanel = new javax.swing.JPanel(); + locationLabel = new javax.swing.JLabel(); + locationTextField = new javax.swing.JTextField(); + triggerLabel = new javax.swing.JLabel(); + triggerTextField = new javax.swing.JTextField(); + requireCompletionLabel = new javax.swing.JLabel(); + requireCompletionTextField = new javax.swing.JTextField(); + operationLabel = new javax.swing.JLabel(); + operationTextField = new javax.swing.JTextField(); + + setLayout(new java.awt.GridBagLayout()); + + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/operating/dialogs/peripheralJobDetail"); // NOI18N + generalPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("peripheralJobView.panel_general.border.title"))); // NOI18N + generalPanel.setLayout(new java.awt.GridBagLayout()); + + nameLabel.setFont(nameLabel.getFont()); + nameLabel.setText(bundle.getString("peripheralJobView.panel_general.label_name.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + generalPanel.add(nameLabel, gridBagConstraints); + + nameTextField.setEditable(false); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 3, 0); + generalPanel.add(nameTextField, gridBagConstraints); + + createdLabel.setFont(createdLabel.getFont()); + createdLabel.setText(bundle.getString("peripheralJobView.panel_general.label_created.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + generalPanel.add(createdLabel, gridBagConstraints); + + createdTextField.setEditable(false); + createdTextField.setColumns(12); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 3, 0); + generalPanel.add(createdTextField, gridBagConstraints); + + finishedLabel.setFont(finishedLabel.getFont()); + finishedLabel.setText(bundle.getString("peripheralJobView.panel_general.label_finished.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 8, 0, 4); + generalPanel.add(finishedLabel, gridBagConstraints); + + finishedTextField.setEditable(false); + finishedTextField.setColumns(12); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 3; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 3, 0); + generalPanel.add(finishedTextField, gridBagConstraints); + + vehicleLabel.setFont(vehicleLabel.getFont()); + vehicleLabel.setText(bundle.getString("peripheralJobView.panel_general.label_vehicle.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + generalPanel.add(vehicleLabel, gridBagConstraints); + + vehicleTextField.setEditable(false); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 3; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 3, 0); + generalPanel.add(vehicleTextField, gridBagConstraints); + + reservationTokenLabel.setText(bundle.getString("peripheralJobView.panel_general.label_reservationToken.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + generalPanel.add(reservationTokenLabel, gridBagConstraints); + + reservationTokenTextField.setEditable(false); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 3, 0); + generalPanel.add(reservationTokenTextField, gridBagConstraints); + + relatedTransportOrderLabel.setText(bundle.getString("peripheralJobView.panel_general.label_transportOrder.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + generalPanel.add(relatedTransportOrderLabel, gridBagConstraints); + + relatedTransportOrderTextField.setEditable(false); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 4; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 3, 0); + generalPanel.add(relatedTransportOrderTextField, gridBagConstraints); + + stateLabel.setText(bundle.getString("peripheralJobView.panel_general.label_state.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 8, 0, 4); + generalPanel.add(stateLabel, gridBagConstraints); + + stateTextField.setEditable(false); + stateTextField.setColumns(14); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 3; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 3, 0); + generalPanel.add(stateTextField, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 0.1; + add(generalPanel, gridBagConstraints); + + propertiesPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("peripheralJobView.panel_properties.border.title"))); // NOI18N + propertiesPanel.setLayout(new java.awt.BorderLayout()); + + propertiesScrollPane.setPreferredSize(new java.awt.Dimension(150, 100)); + + propertiesTable.setFont(propertiesTable.getFont()); + propertiesScrollPane.setViewportView(propertiesTable); + + propertiesPanel.add(propertiesScrollPane, java.awt.BorderLayout.CENTER); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.weighty = 0.5; + add(propertiesPanel, gridBagConstraints); + + historyPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("peripheralJobView.panel_history.border.title"))); // NOI18N + historyPanel.setLayout(new java.awt.BorderLayout()); + + historyScrollPane.setPreferredSize(new java.awt.Dimension(150, 100)); + + historyTable.setModel(new javax.swing.table.DefaultTableModel( + new Object [][] { + + }, + new String [] { + + } + )); + historyScrollPane.setViewportView(historyTable); + + historyPanel.add(historyScrollPane, java.awt.BorderLayout.CENTER); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.weighty = 0.5; + add(historyPanel, gridBagConstraints); + + operationPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("peripheralJobView.panel_operation.border.title"))); // NOI18N + operationPanel.setLayout(new java.awt.GridBagLayout()); + + locationLabel.setFont(locationLabel.getFont()); + locationLabel.setText(bundle.getString("peripheralJobView.panel_operation.lable_location.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + operationPanel.add(locationLabel, gridBagConstraints); + + locationTextField.setEditable(false); + locationTextField.setColumns(10); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 3, 0); + operationPanel.add(locationTextField, gridBagConstraints); + + triggerLabel.setFont(triggerLabel.getFont()); + triggerLabel.setText(bundle.getString("peripheralJobView.panel_operation.lable_trigger.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + operationPanel.add(triggerLabel, gridBagConstraints); + + triggerTextField.setEditable(false); + triggerTextField.setColumns(14); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 3, 0); + operationPanel.add(triggerTextField, gridBagConstraints); + + requireCompletionLabel.setFont(requireCompletionLabel.getFont()); + requireCompletionLabel.setText(bundle.getString("peripheralJobView.panel_operation.lable_requireCompletion.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 8, 0, 4); + operationPanel.add(requireCompletionLabel, gridBagConstraints); + + requireCompletionTextField.setEditable(false); + requireCompletionTextField.setColumns(10); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 3; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 3, 0); + operationPanel.add(requireCompletionTextField, gridBagConstraints); + + operationLabel.setText(bundle.getString("peripheralJobView.panel_operation.lable_operation.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 8, 0, 4); + operationPanel.add(operationLabel, gridBagConstraints); + + operationTextField.setEditable(false); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 3; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 3, 0); + operationPanel.add(operationTextField, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 0.1; + add(operationPanel, gridBagConstraints); + }// //GEN-END:initComponents + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel createdLabel; + private javax.swing.JTextField createdTextField; + private javax.swing.JLabel finishedLabel; + private javax.swing.JTextField finishedTextField; + private javax.swing.JPanel generalPanel; + private javax.swing.JPanel historyPanel; + private javax.swing.JScrollPane historyScrollPane; + private javax.swing.JTable historyTable; + private javax.swing.JLabel locationLabel; + private javax.swing.JTextField locationTextField; + private javax.swing.JLabel nameLabel; + private javax.swing.JTextField nameTextField; + private javax.swing.JLabel operationLabel; + private javax.swing.JPanel operationPanel; + private javax.swing.JTextField operationTextField; + private javax.swing.JPanel propertiesPanel; + private javax.swing.JScrollPane propertiesScrollPane; + private javax.swing.JTable propertiesTable; + private javax.swing.JLabel relatedTransportOrderLabel; + private javax.swing.JTextField relatedTransportOrderTextField; + private javax.swing.JLabel requireCompletionLabel; + private javax.swing.JTextField requireCompletionTextField; + private javax.swing.JLabel reservationTokenLabel; + private javax.swing.JTextField reservationTokenTextField; + private javax.swing.JLabel stateLabel; + private javax.swing.JTextField stateTextField; + private javax.swing.JLabel triggerLabel; + private javax.swing.JTextField triggerTextField; + private javax.swing.JLabel vehicleLabel; + private javax.swing.JTextField vehicleTextField; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON + + /** + * A cell renderer that adds a tool tip with the cell's value. + */ + private static class ToolTipCellRenderer + extends + DefaultTableCellRenderer { + + /** + * Creates a new instance. + */ + ToolTipCellRenderer() { + } + + @Override + public Component getTableCellRendererComponent( + JTable table, + Object value, + boolean isSelected, + boolean hasFocus, + int row, + int column + ) { + Component component = super.getTableCellRendererComponent( + table, + value, + isSelected, + hasFocus, + row, + column + ); + + ((JComponent) component).setToolTipText(value.toString()); + + return component; + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobViewFactory.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobViewFactory.java new file mode 100644 index 0000000..35c3753 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobViewFactory.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.peripherals.jobs; + +import org.opentcs.data.peripherals.PeripheralJob; + +/** + * Creates peripheral job-related GUI elements. + */ +public interface PeripheralJobViewFactory { + + /** + * Creates a peripheral job details panel. + * + * @param job The job to create a panel for. + * @return The panel. + */ + PeripheralJobView createPeripheralJobView(PeripheralJob job); + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobsContainer.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobsContainer.java new file mode 100644 index 0000000..e70847d --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobsContainer.java @@ -0,0 +1,198 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.peripherals.jobs; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.common.KernelClientApplication; +import org.opentcs.components.Lifecycle; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.guing.common.event.OperationModeChangeEvent; +import org.opentcs.guing.common.event.SystemModelTransitionEvent; +import org.opentcs.operationsdesk.event.KernelStateChangeEvent; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Maintains a set of all peripheral jobs existing on the kernel side. + */ +public class PeripheralJobsContainer + implements + EventHandler, + Lifecycle { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PeripheralJobsContainer.class); + /** + * Where we get events from. + */ + private final EventBus eventBus; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * The kernel client application. + */ + private final KernelClientApplication kernelClientApplication; + /** + * The peripheral jobs. + */ + private final Map peripheralJobs = new HashMap<>(); + /** + * This container's listeners. + */ + private final Set listeners = new HashSet<>(); + /** + * Whether this component is initialized. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param eventBus Where this instance subscribes for events. + * @param portalProvider Provides access to a portal. + * @param kernelClientApplication The kernel client application. + */ + @Inject + public PeripheralJobsContainer( + @ApplicationEventBus + EventBus eventBus, + SharedKernelServicePortalProvider portalProvider, + KernelClientApplication kernelClientApplication + ) { + this.eventBus = requireNonNull(eventBus, "eventBus"); + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.kernelClientApplication + = requireNonNull(kernelClientApplication, "kernelClientApplication"); + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + eventBus.subscribe(this); + + initialized = true; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + eventBus.unsubscribe(this); + + initialized = false; + } + + @Override + public void onEvent(Object event) { + if (event instanceof TCSObjectEvent) { + handleObjectEvent((TCSObjectEvent) event); + } + else if (event instanceof OperationModeChangeEvent) { + initJobs(); + } + else if (event instanceof SystemModelTransitionEvent) { + initJobs(); + } + else if (event instanceof KernelStateChangeEvent) { + initJobs(); + } + } + + public void addListener(PeripheralJobsContainerListener listener) { + listeners.add(listener); + } + + public void removeListener(PeripheralJobsContainerListener listener) { + listeners.remove(listener); + } + + public Collection getPeripheralJobs() { + return peripheralJobs.values(); + } + + private void initJobs() { + setPeripheralJobs(fetchJobsIfOnline()); + listeners.forEach(listener -> listener.containerInitialized(peripheralJobs.values())); + } + + private void handleObjectEvent(TCSObjectEvent evt) { + if (evt.getCurrentOrPreviousObjectState() instanceof PeripheralJob) { + switch (evt.getType()) { + case OBJECT_CREATED: + peripheralJobAdded((PeripheralJob) evt.getCurrentOrPreviousObjectState()); + break; + case OBJECT_MODIFIED: + peripheralJobChanged((PeripheralJob) evt.getCurrentOrPreviousObjectState()); + break; + case OBJECT_REMOVED: + peripheralJobRemoved((PeripheralJob) evt.getCurrentOrPreviousObjectState()); + break; + default: + LOG.warn("Unhandled event type: {}", evt.getType()); + } + } + } + + private void peripheralJobAdded(PeripheralJob job) { + peripheralJobs.put(job.getName(), job); + listeners.forEach(listener -> listener.peripheralJobAdded(job)); + } + + private void peripheralJobChanged(PeripheralJob job) { + peripheralJobs.put(job.getName(), job); + listeners.forEach(listener -> listener.peripheralJobUpdated(job)); + } + + private void peripheralJobRemoved(PeripheralJob job) { + peripheralJobs.remove(job.getName()); + listeners.forEach(listener -> listener.peripheralJobRemoved(job)); + } + + private void setPeripheralJobs(Set newJobs) { + peripheralJobs.clear(); + for (PeripheralJob job : newJobs) { + peripheralJobs.put(job.getName(), job); + } + } + + private Set fetchJobsIfOnline() { + if (kernelClientApplication.isOnline()) { + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + return sharedPortal.getPortal().getPeripheralJobService().fetchObjects(PeripheralJob.class); + } + catch (KernelRuntimeException exc) { + LOG.warn("Exception fetching peripheral jobs", exc); + } + } + + return new HashSet<>(); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobsContainerListener.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobsContainerListener.java new file mode 100644 index 0000000..402d663 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobsContainerListener.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.peripherals.jobs; + +import java.util.Collection; +import org.opentcs.data.peripherals.PeripheralJob; + +/** + * Listens for changes to the {@link PeripheralJobsContainer}. + */ +public interface PeripheralJobsContainerListener { + + /** + * Notifies the listener that the container has been initialized. + * + * @param jobs The jobs the container has been initialized with. + */ + void containerInitialized(Collection jobs); + + /** + * Notifies the listener that a job has been added. + * + * @param job The job that has been added. + */ + void peripheralJobAdded(PeripheralJob job); + + /** + * Notifies the listener that a job has been updated. + * + * @param job The job that has been updated. + */ + void peripheralJobUpdated(PeripheralJob job); + + /** + * Notifies the listener that a job has been removed. + * + * @param job The job that has been removed. + */ + void peripheralJobRemoved(PeripheralJob job); +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobsContainerPanel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobsContainerPanel.java new file mode 100644 index 0000000..50b6066 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/peripherals/jobs/PeripheralJobsContainerPanel.java @@ -0,0 +1,251 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.peripherals.jobs; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.PERIPHERALJOB_PATH; + +import jakarta.inject.Inject; +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import javax.swing.JButton; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.JToolBar; +import javax.swing.RowSorter; +import javax.swing.SortOrder; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableRowSorter; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.guing.common.components.dialogs.DialogContent; +import org.opentcs.guing.common.components.dialogs.StandardContentDialog; +import org.opentcs.guing.common.util.IconToolkit; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.util.gui.StringTableCellRenderer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Shows a table of the kernel's peripheral jobs. + */ +public class PeripheralJobsContainerPanel + extends + JPanel { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PeripheralJobsContainerPanel.class); + /** + * The path containing the icons. + */ + private static final String ICON_PATH = "/org/opentcs/guing/res/symbols/panel/"; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * Maintains a set of all peripheral jobs existing on the kernel side. + */ + private final PeripheralJobsContainer peripheralJobsContainer; + + /** + * Factory for creating peripheral job views. + */ + private final PeripheralJobViewFactory peripheralJobViewFactory; + /** + * The table showing the peripheral jobs. + */ + private JTable table; + /** + * The table's model. + */ + private PeripheralJobTableModel tableModel; + + /** + * Creates a new instance. + * + * @param portalProvider Provides access to a kernel service portal. + * @param peripheralJobsContainer Maintains a set of all peripheral jobs existing on the kernel + * side. + * @param peripheralJobViewFactory The factory for creating peripheral job views. + */ + @Inject + @SuppressWarnings("this-escape") + public PeripheralJobsContainerPanel( + SharedKernelServicePortalProvider portalProvider, + PeripheralJobsContainer peripheralJobsContainer, + PeripheralJobViewFactory peripheralJobViewFactory + ) { + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.peripheralJobsContainer = requireNonNull( + peripheralJobsContainer, + "peripheralJobsContainer" + ); + this.peripheralJobViewFactory = requireNonNull( + peripheralJobViewFactory, + "peripheralJobViewFactory" + ); + + initComponents(); + } + + /** + * Initializes this panel's contents. + */ + public void initView() { + tableModel.containerInitialized(peripheralJobsContainer.getPeripheralJobs()); + } + + private void initComponents() { + setLayout(new BorderLayout()); + initPeripheralJobTable(); + add(new JScrollPane(table), BorderLayout.CENTER); + + add(createToolBar(), BorderLayout.NORTH); + } + + private void initPeripheralJobTable() { + tableModel = new PeripheralJobTableModel(); + peripheralJobsContainer.addListener(tableModel); + table = new JTable(tableModel); + + TableRowSorter sorter = new TableRowSorter<>(tableModel); + // Sort the table by the creation instant. + sorter.setSortKeys( + Arrays.asList( + new RowSorter.SortKey( + PeripheralJobTableModel.COLUMN_CREATION_TIME, SortOrder.DESCENDING + ) + ) + ); + // ...but prevent manual sorting. + for (int i = 0; i < table.getColumnCount(); i++) { + sorter.setSortable(i, false); + } + sorter.setSortsOnUpdates(true); + table.setRowSorter(sorter); + + // Hide the column that shows the creation time. + table.removeColumn( + table.getColumnModel() + .getColumn(table.convertColumnIndexToView(PeripheralJobTableModel.COLUMN_CREATION_TIME)) + ); + + TableCellRenderer renderer = new StringTableCellRenderer(reference -> { + if (reference == null) { + return "-"; + } + + return reference.getName(); + }); + table.getColumnModel().getColumn(PeripheralJobTableModel.COLUMN_RELATED_VEHICLE) + .setCellRenderer(renderer); + table.getColumnModel().getColumn(PeripheralJobTableModel.COLUMN_RELATED_ORDER) + .setCellRenderer(renderer); + + table.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent evt) { + if (evt.getButton() == MouseEvent.BUTTON1) { + if (evt.getClickCount() == 2) { + showSelectedJob(); + } + } + + if (evt.getButton() == MouseEvent.BUTTON3) { + if (table.getSelectedRow() != -1) { + showPopupMenuForSelectedJob(evt.getX(), evt.getY()); + } + } + } + }); + } + + private JToolBar createToolBar() { + JToolBar toolBar = new JToolBar(); + + JButton button = new JButton( + IconToolkit.instance().getImageIconByFullPath(ICON_PATH + "table-row-delete-2.16x16.png") + ); + button.addActionListener((ActionEvent e) -> withdrawSelectedJobs()); + button.setToolTipText( + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.PERIPHERALJOB_PATH) + .getString("peripheralJobsContainerPanel.button_withdrawSelectedJobs.tooltipText") + ); + toolBar.add(button); + + return toolBar; + } + + private void showSelectedJob() { + getSelectedJob().ifPresent(job -> { + DialogContent content = peripheralJobViewFactory.createPeripheralJobView(job); + StandardContentDialog dialog + = new StandardContentDialog( + JOptionPane.getFrameForComponent(this), + content, + true, + StandardContentDialog.CLOSE + ); + dialog.setVisible(true); + }); + } + + private Optional getSelectedJob() { + int rowIndex = table.getSelectedRow(); + if (rowIndex == -1) { + return Optional.empty(); + } + return Optional.of(tableModel.getEntryAt(table.convertRowIndexToModel(rowIndex))); + } + + private void showPopupMenuForSelectedJob(int x, int y) { + boolean singleRowSelected = table.getSelectedRowCount() <= 1; + ResourceBundleUtil bundle = ResourceBundleUtil.getBundle(PERIPHERALJOB_PATH); + JPopupMenu menu = new JPopupMenu(); + JMenuItem item = menu.add( + bundle.getString( + "peripheralJobsContainerPanel.table_peripheralJobs.popupMenuItem_showDetails.text" + ) + ); + item.setEnabled(singleRowSelected); + item.addActionListener(event -> showSelectedJob()); + + menu.show(table, x, y); + } + + private void withdrawSelectedJobs() { + List toBeWithdrawn = new ArrayList<>(); + + for (int i : table.getSelectedRows()) { + toBeWithdrawn.add(tableModel.getEntryAt(table.convertRowIndexToModel(i))); + } + + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + for (PeripheralJob job : toBeWithdrawn) { + sharedPortal.getPortal().getPeripheralDispatcherService() + .withdrawByPeripheralJob(job.getReference()); + } + } + catch (IllegalArgumentException | KernelRuntimeException exc) { + LOG.warn("Exception withdrawing transport order", exc); + } + } + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/CompositeObjectHistoryEntryFormatter.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/CompositeObjectHistoryEntryFormatter.java new file mode 100644 index 0000000..0820045 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/CompositeObjectHistoryEntryFormatter.java @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.opentcs.components.plantoverview.ObjectHistoryEntryFormatter; +import org.opentcs.data.ObjectHistory; +import org.opentcs.operationsdesk.peripherals.jobs.PeripheralJobHistoryEntryFormatter; + +/** + * A composite formatter for history entries that first tries to apply all registered formatters, + * then a set of standard formatters, then a fallback function to ensure that the returned value is + * never empty. + *

    + * The set of standard formatters is composed of: + *

      + *
    • {@link TransportOrderHistoryEntryFormatter}
    • + *
    • {@link PeripheralJobHistoryEntryFormatter}
    • + *
    + */ +public class CompositeObjectHistoryEntryFormatter + implements + ObjectHistoryEntryFormatter { + + /** + * The actual formatters. + */ + private final List formatters = new ArrayList<>(); + + /** + * Creates a new instance. + * + * @param customFormatters The set of custom formatters. + */ + @Inject + public CompositeObjectHistoryEntryFormatter(Set customFormatters) { + for (ObjectHistoryEntryFormatter formatter : customFormatters) { + formatters.add(formatter); + } + + formatters.add(new TransportOrderHistoryEntryFormatter()); + formatters.add(new PeripheralJobHistoryEntryFormatter()); + formatters.add(new OrderSequenceHistoryEntryFormatter()); + formatters.add(this::fallbackFormat); + } + + @Override + public Optional apply(ObjectHistory.Entry entry) { + return formatters.stream() + .map(formatter -> formatter.apply(entry)) + .filter(result -> result.isPresent()) + .map(result -> result.get()) + .findFirst(); + } + + private Optional fallbackFormat(ObjectHistory.Entry entry) { + return Optional.of( + "eventCode: '" + entry.getEventCode() + + "', supplement: '" + entry.getSupplement().toString() + '\'' + ); + } + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/CreatePeripheralJobPanel.form b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/CreatePeripheralJobPanel.form new file mode 100644 index 0000000..07f3186 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/CreatePeripheralJobPanel.form @@ -0,0 +1,119 @@ + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/CreatePeripheralJobPanel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/CreatePeripheralJobPanel.java new file mode 100644 index 0000000..3d61e5a --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/CreatePeripheralJobPanel.java @@ -0,0 +1,212 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.awt.event.ItemEvent; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.ResourceBundle; +import java.util.stream.Collectors; +import javax.swing.JOptionPane; +import org.opentcs.access.to.peripherals.PeripheralOperationCreationTO; +import org.opentcs.data.model.PeripheralInformation; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.common.components.dialogs.DialogContent; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.util.gui.StringListCellRenderer; + +/** + */ +public class CreatePeripheralJobPanel + extends + DialogContent { + + private static final Comparator BY_NAME + = (o1, o2) -> o1.getName().toLowerCase().compareTo(o2.getName().toLowerCase()); + /** + * This instance's resource bundle. + */ + private final ResourceBundle bundle + = ResourceBundle.getBundle(I18nPlantOverviewOperating.CREATE_PERIPHERAL_JOB_PATH); + /** + * List of locations to choose from. + */ + private final List locations; + + /** + * Creates a new instance. + * + * @param modelManager The model manager. + */ + @Inject + @SuppressWarnings("this-escape") + public CreatePeripheralJobPanel(ModelManager modelManager) { + requireNonNull(modelManager, "modelManager"); + locations = modelManager.getModel().getLocationModels().stream() + .filter( + location -> !Objects.equals( + location.getPropertyPeripheralState().getText(), + PeripheralInformation.State.NO_PERIPHERAL.name() + ) + ) + .sorted(BY_NAME) + .collect(Collectors.toList()); + + initComponents(); + setDialogTitle(bundle.getString("createPeripheralJobPanel.title")); + } + + @Override + public void initFields() { + locations.stream().forEach(locationCombobox::addItem); + loadOperations(); + } + + @Override + public void update() { + updateFailed = false; + if (reservationTokenTextField.getText().isEmpty()) { + updateFailed = true; + JOptionPane.showMessageDialog( + this, + bundle.getString("createPeripheralJobPanel.optionPane_reserveTokenEmpty.message"), + bundle.getString("createPeripheralJobPanel.optionPane_reserveTokenEmpty.title"), + JOptionPane.ERROR_MESSAGE + ); + + } + if (locationCombobox.getSelectedItem() == null + || operationCombobox.getSelectedItem() == null) { + updateFailed = true; + JOptionPane.showMessageDialog( + this, + bundle.getString("createPeripheralJobPanel.optionPane_invalidOperation.message"), + bundle.getString("createPeripheralJobPanel.optionPane_invalidOperation.title"), + JOptionPane.ERROR_MESSAGE + ); + } + } + + private void loadOperations() { + LocationModel location = (LocationModel) locationCombobox.getSelectedItem(); + if (location == null) { + return; + } + + operationCombobox.removeAllItems(); + for (String op : location.getLocationType().getPropertyAllowedPeripheralOperations() + .getItems()) { + operationCombobox.addItem(op); + } + } + + public String getReservationToken() { + return reservationTokenTextField.getText(); + } + + public PeripheralOperationCreationTO getPeripheralOperation() { + return new PeripheralOperationCreationTO( + (String) operationCombobox.getSelectedItem(), + ((LocationModel) locationCombobox.getSelectedItem()).getName() + ); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + reservationTokenLabel = new javax.swing.JLabel(); + reservationTokenTextField = new javax.swing.JTextField(); + locationLabel = new javax.swing.JLabel(); + locationCombobox = new javax.swing.JComboBox<>(); + operationLabel = new javax.swing.JLabel(); + operationCombobox = new javax.swing.JComboBox<>(); + + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/operating/dialogs/createPeripheralJob"); // NOI18N + setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("createPeripheralJobPanel.border.title"))); // NOI18N + setLayout(new java.awt.GridBagLayout()); + + reservationTokenLabel.setText(bundle.getString("createPeripheralJobPanel.label_reservationToken.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + add(reservationTokenLabel, gridBagConstraints); + + reservationTokenTextField.setPreferredSize(new java.awt.Dimension(150, 20)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(reservationTokenTextField, gridBagConstraints); + + locationLabel.setText(bundle.getString("createPeripheralJobPanel.label_location.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + add(locationLabel, gridBagConstraints); + + locationCombobox.setPreferredSize(new java.awt.Dimension(150, 20)); + locationCombobox.setRenderer(new StringListCellRenderer(location -> (location!=null)?location.getName():"")); + locationCombobox.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + locationComboboxItemStateChanged(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(locationCombobox, gridBagConstraints); + + operationLabel.setText(bundle.getString("createPeripheralJobPanel.label_operation.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + add(operationLabel, gridBagConstraints); + + operationCombobox.setPreferredSize(new java.awt.Dimension(150, 20)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(operationCombobox, gridBagConstraints); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void locationComboboxItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_locationComboboxItemStateChanged + if (evt.getStateChange() == ItemEvent.SELECTED) { + loadOperations(); + } + }//GEN-LAST:event_locationComboboxItemStateChanged + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JComboBox locationCombobox; + private javax.swing.JLabel locationLabel; + private javax.swing.JComboBox operationCombobox; + private javax.swing.JLabel operationLabel; + private javax.swing.JLabel reservationTokenLabel; + private javax.swing.JTextField reservationTokenTextField; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/CreateTransportOrderPanel.form b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/CreateTransportOrderPanel.form new file mode 100644 index 0000000..211ac3c --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/CreateTransportOrderPanel.form @@ -0,0 +1,1885 @@ + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/CreateTransportOrderPanel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/CreateTransportOrderPanel.java new file mode 100644 index 0000000..4a5e73f --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/CreateTransportOrderPanel.java @@ -0,0 +1,678 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.stream.Collectors; +import javax.swing.JOptionPane; +import javax.swing.event.ListSelectionEvent; +import javax.swing.table.DefaultTableModel; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.OrderConstants; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.guing.base.model.AbstractConnectableModelComponent; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.dialogs.DialogContent; +import org.opentcs.guing.common.components.dialogs.StandardContentDialog; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.transport.OrderTypeSuggestionsPool; +import org.opentcs.operationsdesk.components.dialogs.EditDriveOrderPanel; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; + +/** + * Allows creation of transport orders. + */ +public class CreateTransportOrderPanel + extends + DialogContent { + + /** + * This instance's resource bundle. + */ + private final ResourceBundle bundle + = ResourceBundle.getBundle(I18nPlantOverviewOperating.CREATETO_PATH); + /** + * The selected deadline. + */ + private long fSelectedDeadline; + /** + * The destinations to drive to. + */ + private final List fDestinationModels = new ArrayList<>(); + /** + * The actions to perform at the destinations. + */ + private final List fActions = new ArrayList<>(); + /** + * The transport order's properties. + */ + private final List> fPropertiesList = new ArrayList<>(); + /** + * The possible types for the transport order. + */ + private final List fPossibleTypes; + /** + * The available vehicles. + */ + private final List fVehicles; + /** + * The manager for accessing the current system model. + */ + private final ModelManager fModelManager; + /** + * The transport order used as a template. + */ + private TransportOrder fPattern; + + /** + * Creates new instance. + * + * @param modelManager The manager for accessing the current system model. + * @param orderTypeSuggestionsPool The transport order types to suggest. + */ + @Inject + @SuppressWarnings("this-escape") + public CreateTransportOrderPanel( + ModelManager modelManager, + OrderTypeSuggestionsPool orderTypeSuggestionsPool + ) { + this.fModelManager = requireNonNull(modelManager, "modelManager"); + requireNonNull(orderTypeSuggestionsPool, "orderTypeSuggestionsPool"); + + initComponents(); + Object[] columnNames = { + bundle.getString( + "createTransportOrderPanel.table_driveOrdersTable.column_location.headerText" + ), + bundle.getString( + "createTransportOrderPanel.table_driveOrdersTable.column_action.headerText" + ) + }; + DefaultTableModel model = (DefaultTableModel) driveOrdersTable.getModel(); + model.setColumnIdentifiers(columnNames); + + driveOrdersTable.getSelectionModel().addListSelectionListener((ListSelectionEvent evt) -> { + if (!evt.getValueIsAdjusting()) { + updateButtons(); + } + }); + + fVehicles = fModelManager.getModel().getVehicleModels(); + Collections.sort(fVehicles, (v1, v2) -> v1.getName().compareToIgnoreCase(v2.getName())); + + fPossibleTypes = new ArrayList<>(orderTypeSuggestionsPool.getTypeSuggestions()); + initTitle(); + } + + private void initTitle() { + setDialogTitle(bundle.getString("transportOrdersContainerPanel.dialog.title")); + } + + public List getDestinationModels() { + return fDestinationModels; + } + + public List getActions() { + return fActions; + } + + public List> getPropertiesList() { + return fPropertiesList; + } + + public long getSelectedDeadline() { + return fSelectedDeadline; + } + + public VehicleModel getSelectedVehicle() { + if (vehicleComboBox.getSelectedIndex() == 0) { + return null; + } + + return fVehicles.get(vehicleComboBox.getSelectedIndex() - 1); + } + + public String getSelectedType() { + if (typeComboBox.getSelectedItem() == null) { + return OrderConstants.TYPE_NONE; + } + + return typeComboBox.getSelectedItem().toString(); + } + + @Override + public void update() { + try { + updateFailed = false; + SimpleDateFormat deadlineFormat = new SimpleDateFormat("dd.MM.yyyyHH:mm"); + Date date = deadlineFormat.parse(dateTextField.getText() + timeTextField.getText()); + ZonedDateTime deadline = ZonedDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); + fSelectedDeadline = deadline.toInstant().toEpochMilli(); + } + catch (ParseException e) { + JOptionPane.showMessageDialog( + this, + bundle.getString("createTransportOrderPanel.optionPane_dateTimeParseError.message"), + bundle.getString("createTransportOrderPanel.optionPane_dateTimeParseError.title"), + JOptionPane.ERROR_MESSAGE + ); + updateFailed = true; + } + + if (fDestinationModels.isEmpty()) { + JOptionPane.showMessageDialog( + this, + bundle.getString("createTransportOrderPanel.optionPane_noOrderError.message"), + bundle.getString("createTransportOrderPanel.optionPane_noOrderError.title"), + JOptionPane.ERROR_MESSAGE + ); + updateFailed = true; + } + } + + @Override + public void initFields() { + vehicleComboBox.addItem(bundle.getString("createTransportOrderPanel.comboBox_automatic.text")); + + for (VehicleModel vehicleModel : fVehicles) { + vehicleComboBox.addItem(vehicleModel.getName()); + } + + for (String tag : fPossibleTypes) { + typeComboBox.addItem(tag); + } + + ZonedDateTime newDeadline = ZonedDateTime.now(ZoneId.systemDefault()).plusHours(1); + dateTextField.setText(newDeadline.format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))); + timeTextField.setText(newDeadline.format(DateTimeFormatter.ofPattern("HH:mm"))); + + if (fPattern != null) { + newDeadline = ZonedDateTime.ofInstant(fPattern.getDeadline(), ZoneId.systemDefault()); + dateTextField.setText(newDeadline.format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))); + timeTextField.setText(newDeadline.format(DateTimeFormatter.ofPattern("HH:mm"))); + + if (fPattern.getIntendedVehicle() != null) { + vehicleComboBox.setSelectedItem(fPattern.getIntendedVehicle().getName()); + } + + typeComboBox.setSelectedItem(fPattern.getType()); + + List driveOrders = new ArrayList<>(fPattern.getAllDriveOrders()); + + DefaultTableModel model = (DefaultTableModel) driveOrdersTable.getModel(); + for (DriveOrder o : driveOrders) { + String destination = o.getDestination().getDestination().getName(); + String action = o.getDestination().getOperation(); + Map properties = o.getDestination().getProperties(); + + String[] row = new String[2]; + row[0] = destination; + row[1] = action; + model.addRow(row); + AbstractConnectableModelComponent destModel + = fModelManager.getModel().getLocationModel(destination); + if (destModel == null) { + destModel = fModelManager.getModel().getPointModel(destination); + } + fDestinationModels.add(destModel); + fActions.add(action); + fPropertiesList.add(properties); + } + } + + updateButtons(); + } + + public void setPattern(TransportOrder t) { + fPattern = t; + } + + private void updateButtons() { + boolean state = driveOrdersTable.getSelectedRow() != -1; + + editButton.setEnabled(state); + removeButton.setEnabled(state); + moveUpButton.setEnabled(state); + moveDownButton.setEnabled(state); + + if (driveOrdersTable.getRowCount() == driveOrdersTable.getSelectedRow() + 1) { + moveDownButton.setEnabled(false); + } + + if (driveOrdersTable.getSelectedRow() == 0) { + moveUpButton.setEnabled(false); + } + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + stationsPanel = new javax.swing.JPanel(); + driveOrdersScrollPane = new javax.swing.JScrollPane(); + driveOrdersTable = new javax.swing.JTable(); + addButton = new javax.swing.JButton(); + editButton = new javax.swing.JButton(); + removeButton = new javax.swing.JButton(); + moveUpButton = new javax.swing.JButton(); + moveDownButton = new javax.swing.JButton(); + deadlinePanel = new javax.swing.JPanel(); + dateLabel = new javax.swing.JLabel(); + dateTextField = new javax.swing.JTextField(); + timeLabel = new javax.swing.JLabel(); + timeTextField = new javax.swing.JTextField(); + typePanel = new javax.swing.JPanel(); + typeLabel = new javax.swing.JLabel(); + typeComboBox = new javax.swing.JComboBox<>(); + vehiclePanel = new javax.swing.JPanel(); + vehicleLabel = new javax.swing.JLabel(); + vehicleComboBox = new javax.swing.JComboBox<>(); + + setLayout(new javax.swing.BoxLayout(this, javax.swing.BoxLayout.Y_AXIS)); + + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/operating/dialogs/createTransportOrder"); // NOI18N + stationsPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("createTransportOrderPanel.panel_stations.border.title"))); // NOI18N + java.awt.GridBagLayout stationsPanelLayout = new java.awt.GridBagLayout(); + stationsPanelLayout.columnWidths = new int[] {0, 5, 0}; + stationsPanelLayout.rowHeights = new int[] {0, 5, 0, 5, 0, 5, 0, 5, 0, 5, 0}; + stationsPanel.setLayout(stationsPanelLayout); + + driveOrdersScrollPane.setPreferredSize(new java.awt.Dimension(200, 200)); + + driveOrdersTable.setModel(new javax.swing.table.DefaultTableModel( + new Object [][] { + + }, + new String [] { + "Station", "Aktion" + } + ) { + boolean[] canEdit = new boolean [] { + false, false + }; + + public boolean isCellEditable(int rowIndex, int columnIndex) { + return canEdit [columnIndex]; + } + }); + driveOrdersScrollPane.setViewportView(driveOrdersTable); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridheight = 11; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + stationsPanel.add(driveOrdersScrollPane, gridBagConstraints); + + addButton.setFont(addButton.getFont()); + addButton.setText(bundle.getString("createTransportOrderPanel.button_add.text")); // NOI18N + addButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(5, 0, 0, 5); + stationsPanel.add(addButton, gridBagConstraints); + + editButton.setFont(editButton.getFont()); + editButton.setText(bundle.getString("createTransportOrderPanel.button_edit.text")); // NOI18N + editButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + editButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 5); + stationsPanel.add(editButton, gridBagConstraints); + + removeButton.setFont(removeButton.getFont()); + removeButton.setText(bundle.getString("createTransportOrderPanel.button_delete.text")); // NOI18N + removeButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + removeButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 5); + stationsPanel.add(removeButton, gridBagConstraints); + + moveUpButton.setFont(moveUpButton.getFont()); + moveUpButton.setText(bundle.getString("createTransportOrderPanel.button_up.text")); // NOI18N + moveUpButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + moveUpButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 6; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 5); + stationsPanel.add(moveUpButton, gridBagConstraints); + + moveDownButton.setFont(moveDownButton.getFont()); + moveDownButton.setText(bundle.getString("createTransportOrderPanel.button_moveDown.text")); // NOI18N + moveDownButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + moveDownButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 8; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 5); + stationsPanel.add(moveDownButton, gridBagConstraints); + + add(stationsPanel); + + deadlinePanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("createTransportOrderPanel.panel_deadline.border.title"))); // NOI18N + java.awt.GridBagLayout deadlinePanelLayout = new java.awt.GridBagLayout(); + deadlinePanelLayout.columnWidths = new int[] {0, 5, 0, 5, 0, 5, 0}; + deadlinePanelLayout.rowHeights = new int[] {0}; + deadlinePanel.setLayout(deadlinePanelLayout); + + dateLabel.setFont(dateLabel.getFont()); + dateLabel.setText(bundle.getString("createTransportOrderPanel.label_date.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 5, 0, 0); + deadlinePanel.add(dateLabel, gridBagConstraints); + + dateTextField.setColumns(10); + dateTextField.setFont(dateTextField.getFont()); + dateTextField.setText("31.12.2099"); + dateTextField.setToolTipText("Geben Sie das Datum im Format TT.MM.JJJJ ein!"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 0.5; + deadlinePanel.add(dateTextField, gridBagConstraints); + + timeLabel.setFont(timeLabel.getFont()); + timeLabel.setText(bundle.getString("createTransportOrderPanel.label_time.text")); // NOI18N + timeLabel.setToolTipText(""); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 4; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 5, 0, 0); + deadlinePanel.add(timeLabel, gridBagConstraints); + + timeTextField.setColumns(10); + timeTextField.setFont(timeTextField.getFont()); + timeTextField.setText("23:59"); + timeTextField.setToolTipText("Geben Sie die Uhrzeit im Format HH:MM ein!"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 6; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 5); + deadlinePanel.add(timeTextField, gridBagConstraints); + + add(deadlinePanel); + + typePanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("createTransportOrderPanel.panel_type.border.title"))); // NOI18N + java.awt.GridBagLayout typePanelLayout = new java.awt.GridBagLayout(); + typePanelLayout.columnWidths = new int[] {0, 5, 0}; + typePanelLayout.rowHeights = new int[] {0}; + typePanel.setLayout(typePanelLayout); + + typeLabel.setFont(typeLabel.getFont()); + typeLabel.setText(bundle.getString("createTransportOrderPanel.label_type.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 5, 0, 0); + typePanel.add(typeLabel, gridBagConstraints); + + typeComboBox.setEditable(true); + typeComboBox.setFont(typeComboBox.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 5); + typePanel.add(typeComboBox, gridBagConstraints); + + add(typePanel); + + vehiclePanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("createTransportOrderPanel.panel_vehicle.border.title"))); // NOI18N + java.awt.GridBagLayout vehiclePanelLayout = new java.awt.GridBagLayout(); + vehiclePanelLayout.columnWidths = new int[] {0, 5, 0}; + vehiclePanelLayout.rowHeights = new int[] {0}; + vehiclePanel.setLayout(vehiclePanelLayout); + + vehicleLabel.setFont(vehicleLabel.getFont()); + vehicleLabel.setText(bundle.getString("createTransportOrderPanel.label_vehicle.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 5, 0, 0); + vehiclePanel.add(vehicleLabel, gridBagConstraints); + + vehicleComboBox.setFont(vehicleComboBox.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 5); + vehiclePanel.add(vehicleComboBox, gridBagConstraints); + + add(vehiclePanel); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void moveDownButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_moveDownButtonActionPerformed + int index = driveOrdersTable.getSelectedRow(); + + if (index == -1) { + return; + } + + if (index == driveOrdersTable.getRowCount() - 1) { + return; + } + + DefaultTableModel model = (DefaultTableModel) driveOrdersTable.getModel(); + model.moveRow(index, index, index + 1); + driveOrdersTable.getSelectionModel().setSelectionInterval(index + 1, index + 1); + + AbstractConnectableModelComponent location = fDestinationModels.remove(index); + fDestinationModels.add(index + 1, location); + + String action = fActions.remove(index); + fActions.add(index + 1, action); + + Map properties = fPropertiesList.remove(index); + fPropertiesList.add(index + 1, properties); + + updateButtons(); + }//GEN-LAST:event_moveDownButtonActionPerformed + + private void moveUpButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_moveUpButtonActionPerformed + int index = driveOrdersTable.getSelectedRow(); + + if (index <= 0) { + return; + } + + DefaultTableModel model = (DefaultTableModel) driveOrdersTable.getModel(); + model.moveRow(index, index, index - 1); + driveOrdersTable.getSelectionModel().setSelectionInterval(index - 1, index - 1); + + AbstractConnectableModelComponent location = fDestinationModels.remove(index); + fDestinationModels.add(index - 1, location); + + String action = fActions.remove(index); + fActions.add(index - 1, action); + + Map properties = fPropertiesList.remove(index); + fPropertiesList.add(index - 1, properties); + + updateButtons(); + }//GEN-LAST:event_moveUpButtonActionPerformed + + private void removeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removeButtonActionPerformed + int index = driveOrdersTable.getSelectedRow(); + + if (index == -1) { + return; + } + + fDestinationModels.remove(index); + fActions.remove(index); + fPropertiesList.remove(index); + + DefaultTableModel model = (DefaultTableModel) driveOrdersTable.getModel(); + model.removeRow(index); + updateButtons(); + }//GEN-LAST:event_removeButtonActionPerformed + + private void editButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_editButtonActionPerformed + int index = driveOrdersTable.getSelectedRow(); + + if (index == -1) { + return; + } + + AbstractConnectableModelComponent location = fDestinationModels.get(index); + String action = fActions.get(index); + EditDriveOrderPanel contentPanel + = new EditDriveOrderPanel(fetchSuitableLocations(), location, action); + StandardContentDialog dialog + = new StandardContentDialog( + JOptionPane.getFrameForComponent(this), + contentPanel + ); + dialog.setVisible(true); + + Optional locModel = contentPanel.getSelectedLocation(); + Optional act = contentPanel.getSelectedAction(); + if (dialog.getReturnStatus() == StandardContentDialog.RET_OK + && locModel.isPresent() && act.isPresent()) { + location = locModel.get(); + action = act.get(); + + driveOrdersTable.setValueAt(location.getName(), index, 0); + driveOrdersTable.setValueAt(action, index, 1); + + fDestinationModels.set(index, location); + fActions.set(index, action); + } + }//GEN-LAST:event_editButtonActionPerformed + + private void addButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addButtonActionPerformed + EditDriveOrderPanel contentPanel + = new EditDriveOrderPanel(fetchSuitableLocations()); + StandardContentDialog dialog + = new StandardContentDialog(JOptionPane.getFrameForComponent(this), contentPanel); + dialog.setVisible(true); + + Optional locModel = contentPanel.getSelectedLocation(); + Optional act = contentPanel.getSelectedAction(); + if (dialog.getReturnStatus() == StandardContentDialog.RET_OK + && locModel.isPresent() && act.isPresent()) { + int index = driveOrdersTable.getRowCount(); + + LocationModel location = locModel.get(); + String action = act.get(); + + String[] row = new String[2]; + row[0] = location.getName(); + row[1] = action; + + DefaultTableModel model = (DefaultTableModel) driveOrdersTable.getModel(); + model.addRow(row); + + fDestinationModels.add(location); + fActions.add(action); + fPropertiesList.add(new HashMap<>()); + + driveOrdersTable.setRowSelectionInterval(index, index); + updateButtons(); + } + }//GEN-LAST:event_addButtonActionPerformed + + private List fetchSuitableLocations() { + return fModelManager.getModel().getLocationModels().stream() + .filter(lm -> !lm.getLocation().getAttachedLinks().isEmpty()) + .filter(lm -> !lm.getLocationType().getLocationType().getAllowedOperations().isEmpty()) + .collect(Collectors.toList()); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton addButton; + private javax.swing.JLabel dateLabel; + private javax.swing.JTextField dateTextField; + private javax.swing.JPanel deadlinePanel; + private javax.swing.JScrollPane driveOrdersScrollPane; + private javax.swing.JTable driveOrdersTable; + private javax.swing.JButton editButton; + private javax.swing.JButton moveDownButton; + private javax.swing.JButton moveUpButton; + private javax.swing.JButton removeButton; + private javax.swing.JPanel stationsPanel; + private javax.swing.JLabel timeLabel; + private javax.swing.JTextField timeTextField; + private javax.swing.JComboBox typeComboBox; + private javax.swing.JLabel typeLabel; + private javax.swing.JPanel typePanel; + private javax.swing.JComboBox vehicleComboBox; + private javax.swing.JLabel vehicleLabel; + private javax.swing.JPanel vehiclePanel; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/FilterButton.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/FilterButton.java new file mode 100644 index 0000000..7e664ee --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/FilterButton.java @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport; + +import java.awt.event.ActionEvent; +import javax.swing.ImageIcon; +import javax.swing.JToggleButton; +import javax.swing.RowFilter; +import javax.swing.table.TableModel; + +/** + * A button for filtering table entries. + */ +public class FilterButton + extends + JToggleButton { + + /** + * The sorter to apply the filter to. + */ + private final FilteredRowSorter sorter; + /** + * The filter to apply. + */ + private final RowFilter filter; + + /** + * Creates a new instance. + * + * @param icon The image that the button should display + * @param filter The Filter to apply. + * @param sorter The row sorter to apply the filter to. + */ + @SuppressWarnings("this-escape") + public FilterButton( + ImageIcon icon, + RowFilter filter, + FilteredRowSorter sorter + ) { + super(icon); + this.sorter = sorter; + this.filter = filter; + + addActionListener((ActionEvent e) -> changed()); + setSelected(true); + } + + /** + * Called when the button has changed. + */ + private void changed() { + if (isSelected()) { + sorter.removeRowFilter(filter); + } + else { + sorter.addRowFilter(filter); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/FilteredRowSorter.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/FilteredRowSorter.java new file mode 100644 index 0000000..cea1c87 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/FilteredRowSorter.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; +import javax.swing.RowFilter; +import javax.swing.table.TableModel; +import javax.swing.table.TableRowSorter; + +/** + * A table model sorter that implements some convinience methods for easier filtering. + * + * @param The table model class + */ +public class FilteredRowSorter + extends + TableRowSorter { + + /** + * Keeps all the active filters. + */ + private final List> filters = new ArrayList<>(); + + public FilteredRowSorter(T model) { + super(model); + } + + /** + * Add a new filter to the sorter. + * + * @param filter Filter to add. + */ + public void addRowFilter(RowFilter filter) { + requireNonNull(filter, "filter"); + filters.add(filter); + setRowFilter(RowFilter.andFilter(filters)); + } + + /** + * Removes a filter from the sorter. + * + * @param filter Filter to remove. + */ + public void removeRowFilter(RowFilter filter) { + requireNonNull(filter, "filter"); + filters.remove(filter); + setRowFilter(RowFilter.andFilter(filters)); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/IntendedVehiclesPanel.form b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/IntendedVehiclesPanel.form new file mode 100644 index 0000000..b42aca4 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/IntendedVehiclesPanel.form @@ -0,0 +1,48 @@ + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/IntendedVehiclesPanel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/IntendedVehiclesPanel.java new file mode 100644 index 0000000..06971f8 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/IntendedVehiclesPanel.java @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.Vector; +import java.util.stream.Collectors; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JTextField; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import org.opentcs.data.model.Vehicle; +import org.opentcs.guing.common.components.dialogs.DialogContent; +import org.opentcs.guing.common.components.dialogs.InputValidationListener; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * A UI to select an intended vehicle for a transport order. + */ +public class IntendedVehiclesPanel + extends + DialogContent { + + /** + * Entry for the automatic intended vehicle. + */ + private final String automaticEntry; + /** + * Available vehicles. + */ + private final List vehicles; + /** + * Listeners to be notified about the validity of user input. + */ + private final List validationListeners = new ArrayList<>(); + + /** + * Creates new instance. + * + * @param items possible vehicles + */ + @SuppressWarnings("this-escape") + public IntendedVehiclesPanel(Set items) { + requireNonNull(items, "items"); + + initComponents(); + automaticEntry = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TRANSPORTORDER_PATH) + .getString("intendedVehiclesPanel.automatic_entry.text"); + + vehicles = items.stream() + .sorted(Comparator.comparing(Vehicle::getName, String.CASE_INSENSITIVE_ORDER)) + .collect(Collectors.toList()); + + Vector names = new Vector<>(); + names.add(automaticEntry); + for (Vehicle vehicle : vehicles) { + names.add(vehicle.getName()); + } + + itemsComboBox.setModel(new DefaultComboBoxModel<>(names)); + JTextField textField = (JTextField) (itemsComboBox.getEditor().getEditorComponent()); + textField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + verify(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + verify(); + + } + + @Override + public void changedUpdate(DocumentEvent e) { + verify(); + } + }); + } + + public void addInputValidationListener(InputValidationListener listener) { + requireNonNull(listener, "listener"); + + this.validationListeners.add(listener); + verify(); + } + + /** + * Returns the selected vehicle. + * + * @return The selected vehicle. + */ + public Optional getSelectedVehicle() { + int index = itemsComboBox.getSelectedIndex(); + + // The automatic entry exists on position 0 for which case we want to return empty aswell. + if (index <= 0 || index > vehicles.size()) { + return Optional.empty(); + } + return Optional.of(vehicles.get(index - 1)); + } + + @Override + public void update() { + } + + @Override + public void initFields() { + } + + private void verify() { + JTextField textField = (JTextField) (itemsComboBox.getEditor().getEditorComponent()); + String inputText = textField.getText(); + + inputValidationSuccessful( + automaticEntry.equals(inputText) + || vehicles.stream().anyMatch(v -> v.getName().equals(inputText)) + ); + } + + private void inputValidationSuccessful(boolean success) { + for (InputValidationListener valListener : validationListeners) { + valListener.inputValidationSuccessful(success); + } + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + + itemsLabel = new javax.swing.JLabel(); + itemsComboBox = new javax.swing.JComboBox<>(); + + setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.LEFT, 10, 5)); + + itemsLabel.setFont(itemsLabel.getFont()); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/operating/panels/transportOrders"); // NOI18N + itemsLabel.setText(bundle.getString("intendedVehiclesPanel.items_label.text")); // NOI18N + add(itemsLabel); + + itemsComboBox.setEditable(true); + itemsComboBox.setFont(itemsComboBox.getFont()); + add(itemsComboBox); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JComboBox itemsComboBox; + private javax.swing.JLabel itemsLabel; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/LocationActionPanel.form b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/LocationActionPanel.form new file mode 100644 index 0000000..3e9566c --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/LocationActionPanel.form @@ -0,0 +1,95 @@ + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/LocationActionPanel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/LocationActionPanel.java new file mode 100644 index 0000000..c7900fa --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/LocationActionPanel.java @@ -0,0 +1,196 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Vector; +import java.util.stream.Collectors; +import javax.swing.DefaultComboBoxModel; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.common.components.dialogs.DialogContent; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * A UI to select a point or location as a destination for a vehicle. + */ +public class LocationActionPanel + extends + DialogContent { + + /** + * Available locations. + */ + protected List fLocations; + /** + * Available actions. + */ + protected List fActions; + + /** + * Creates new instance. + * + * @param locations The location models. + */ + @SuppressWarnings("this-escape") + public LocationActionPanel(List locations) { + this.fLocations = requireNonNull(locations, "locations"); + + initComponents(); + + setDialogTitle( + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.VEHICLEPOPUP_PATH) + .getString("locationActionPanel.title") + ); + + Collections.sort(fLocations, getComparator()); + + List names = fLocations.stream() + .map(location -> location.getName()) + .collect(Collectors.toList()); + + locationsComboBox.setModel(new DefaultComboBoxModel<>(new Vector<>(names))); + + updateActions(); + } + + /** + * Returns a Comparator to sort model components by their name. + * + * @return The Comparator + */ + protected final Comparator getComparator() { + return (ModelComponent item1, ModelComponent item2) -> { + String s1 = item1.getName(); + String s2 = item2.getName(); + s1 = s1.toLowerCase(); + s2 = s2.toLowerCase(); + + return s1.compareTo(s2); + }; + } + + /** + * Returns the selected location. + * + * @return the selected location or null. + */ + public LocationModel getSelectedLocation() { + int index = locationsComboBox.getSelectedIndex(); + + if (index == -1) { + return null; + } + + return fLocations.get(index); + } + + public String getSelectedAction() { + int index = actionsComboBox.getSelectedIndex(); + + if (index == -1) { + return null; + } + + return fActions.get(index); + } + + private void updateActions() { + LocationModel selectedLocation = getSelectedLocation(); + LocationTypeModel locationType = selectedLocation.getLocationType(); + List actions = locationType.getPropertyAllowedOperations().getItems(); + Collections.sort(actions); + fActions = new ArrayList<>(); + fActions.add(DriveOrder.Destination.OP_NOP); + fActions.addAll(actions); + actionsComboBox.setModel(new DefaultComboBoxModel<>(new Vector<>(fActions))); + } + + @Override + public void update() { + } + + @Override + public void initFields() { + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + locationsLabel = new javax.swing.JLabel(); + locationsComboBox = new javax.swing.JComboBox<>(); + actionsLabel = new javax.swing.JLabel(); + actionsComboBox = new javax.swing.JComboBox<>(); + + setPreferredSize(new java.awt.Dimension(150, 40)); + setLayout(new java.awt.GridBagLayout()); + + locationsLabel.setFont(locationsLabel.getFont()); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/operating/dialogs/vehiclePopup"); // NOI18N + locationsLabel.setText(bundle.getString("locationActionPanel.label_location.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + add(locationsLabel, gridBagConstraints); + + locationsComboBox.setFont(locationsComboBox.getFont()); + locationsComboBox.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + locationsComboBoxItemStateChanged(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 0.5; + add(locationsComboBox, gridBagConstraints); + + actionsLabel.setFont(actionsLabel.getFont()); + actionsLabel.setText(bundle.getString("locationActionPanel.label_action.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + add(actionsLabel, gridBagConstraints); + + actionsComboBox.setFont(actionsComboBox.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + add(actionsComboBox, gridBagConstraints); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void locationsComboBoxItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_locationsComboBoxItemStateChanged + updateActions(); + }//GEN-LAST:event_locationsComboBoxItemStateChanged + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JComboBox actionsComboBox; + private javax.swing.JLabel actionsLabel; + private javax.swing.JComboBox locationsComboBox; + private javax.swing.JLabel locationsLabel; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/OrderSequenceHistoryEntryFormatter.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/OrderSequenceHistoryEntryFormatter.java new file mode 100644 index 0000000..1f7fd32 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/OrderSequenceHistoryEntryFormatter.java @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport; + +import static java.util.Objects.requireNonNull; + +import java.util.Optional; +import org.opentcs.components.plantoverview.ObjectHistoryEntryFormatter; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.OrderSequenceHistoryCodes; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * A formatter for history events/entries related to {@link OrderSequence}s. + */ +public class OrderSequenceHistoryEntryFormatter + implements + ObjectHistoryEntryFormatter { + + /** + * A bundle providing localized strings. + */ + private final ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.OSDETAIL_PATH); + + /** + * Creates a new instance. + */ + public OrderSequenceHistoryEntryFormatter() { + } + + @Override + public Optional apply(ObjectHistory.Entry entry) { + requireNonNull(entry, "entry"); + + switch (entry.getEventCode()) { + case OrderSequenceHistoryCodes.SEQUENCE_CREATED: + return Optional.of( + bundle.getString("orderSequenceHistoryEntryFormatter.code_sequenceCreated.text") + ); + + case OrderSequenceHistoryCodes.SEQUENCE_ORDER_APPENDED: + return Optional.of( + bundle.getString( + "orderSequenceHistoryEntryFormatter.code_sequenceOrderAppended.text" + ) + + " '" + entry.getSupplement().toString() + "'" + ); + + case OrderSequenceHistoryCodes.SEQUENCE_PROCESSING_VEHICLE_CHANGED: + return Optional.of( + bundle.getString( + "orderSequenceHistoryEntryFormatter.code_sequenceProcVehicleChanged.text" + ) + + " '" + entry.getSupplement().toString() + "'" + ); + + case OrderSequenceHistoryCodes.SEQUENCE_COMPLETED: + return Optional.of( + bundle.getString("orderSequenceHistoryEntryFormatter.code_sequenceCompleted.text") + ); + + case OrderSequenceHistoryCodes.SEQUENCE_FINISHED: + return Optional.of( + bundle.getString( + "orderSequenceHistoryEntryFormatter.code_sequenceFinished.text" + ) + ); + + default: + return Optional.empty(); + } + } + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/OrdersTable.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/OrdersTable.java new file mode 100644 index 0000000..84f3069 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/OrdersTable.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport; + +import javax.swing.JTable; +import javax.swing.table.TableCellEditor; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableModel; + +/** + * A table for transport orders. + */ +public class OrdersTable + extends + JTable { + + /** + * Creates a new instance of OrdersTable. + * + * @param tableModel das Tabellenmodell + */ + @SuppressWarnings("this-escape") + public OrdersTable(TableModel tableModel) { + super(tableModel); + + setRowSelectionAllowed(true); + setFocusable(false); + } + + @Override + public boolean isCellEditable(int row, int column) { + return false; + } + + @Override + public TableCellEditor getCellEditor(int row, int column) { + TableModel tableModel = getModel(); + Object value = tableModel.getValueAt(row, column); + TableCellEditor editor = getDefaultEditor(value.getClass()); + + return editor; + } + + @Override + public TableCellRenderer getCellRenderer(int row, int column) { + TableModel tableModel = getModel(); + Object value = tableModel.getValueAt(row, column); + TableCellRenderer renderer = getDefaultRenderer(value.getClass()); + + return renderer; + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/PointPanel.form b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/PointPanel.form new file mode 100644 index 0000000..89733ba --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/PointPanel.form @@ -0,0 +1,48 @@ + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/PointPanel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/PointPanel.java new file mode 100644 index 0000000..7cac6f4 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/PointPanel.java @@ -0,0 +1,174 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Vector; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JTextField; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.common.components.dialogs.DialogContent; +import org.opentcs.guing.common.components.dialogs.InputValidationListener; + +/** + * A UI to select a location or a point as a vehicle destination. + */ +public class PointPanel + extends + DialogContent { + + /** + * Available locations. + */ + protected List fItems; + /** + * List of Listeners to be notified about the validity of user input. + */ + private final List validationListeners = new ArrayList<>(); + + /** + * Creates new instance. + * + * @param items possible destination points + */ + @SuppressWarnings("this-escape") + public PointPanel(List items) { + initComponents(); + fItems = items; + + Collections.sort(fItems, getComparator()); + List names = new ArrayList<>(); + + for (PointModel pointModel : fItems) { + names.add(pointModel.getName()); + } + + itemsComboBox.setModel(new DefaultComboBoxModel<>(new Vector<>(names))); + JTextField textField = (JTextField) (itemsComboBox.getEditor().getEditorComponent()); + textField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + verify(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + verify(); + + } + + @Override + public void changedUpdate(DocumentEvent e) { + verify(); + + } + + }); + } + + public void addInputValidationListener(InputValidationListener listener) { + requireNonNull(listener, "listener"); + + this.validationListeners.add(listener); + verify(); + } + + /** + * Returns the selected element. + * + * @return The selected model component. + */ + public ModelComponent getSelectedItem() { + int index = itemsComboBox.getSelectedIndex(); + + if (index == -1) { + return null; + } + return fItems.get(index); + } + + @Override + public void update() { + } + + @Override + public void initFields() { + } + + protected final Comparator getComparator() { + return new Comparator() { + + @Override + public int compare(ModelComponent item1, ModelComponent item2) { + String s1 = item1.getName(); + String s2 = item2.getName(); + s1 = s1.toLowerCase(); + s2 = s2.toLowerCase(); + + return s1.compareTo(s2); + } + }; + } + + private void verify() { + JTextField textField = (JTextField) (itemsComboBox.getEditor().getEditorComponent()); + String inputText = textField.getText(); + for (PointModel pointModel : fItems) { + if (pointModel.getName().equals(inputText)) { + inputValidationSuccessful(true); + return; + } + } + inputValidationSuccessful(false); + } + + private void inputValidationSuccessful(boolean success) { + for (InputValidationListener valListener : validationListeners) { + valListener.inputValidationSuccessful(success); + } + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + + itemsLabel = new javax.swing.JLabel(); + itemsComboBox = new javax.swing.JComboBox<>(); + + setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.LEFT, 10, 5)); + + itemsLabel.setFont(itemsLabel.getFont()); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/operating/dialogs/vehiclePopup"); // NOI18N + itemsLabel.setText(bundle.getString("pointPanel.label_points.text")); // NOI18N + add(itemsLabel); + + itemsComboBox.setEditable(true); + itemsComboBox.setFont(itemsComboBox.getFont()); + add(itemsComboBox); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JComboBox itemsComboBox; + private javax.swing.JLabel itemsLabel; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/TransportOrderHistoryEntryFormatter.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/TransportOrderHistoryEntryFormatter.java new file mode 100644 index 0000000..3467596 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/TransportOrderHistoryEntryFormatter.java @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport; + +import static java.util.Objects.requireNonNull; + +import java.util.Optional; +import org.opentcs.components.plantoverview.ObjectHistoryEntryFormatter; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.order.TransportOrderHistoryCodes; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * A formatter for history events/entries related to {@link TransportOrder}s. + */ +public class TransportOrderHistoryEntryFormatter + implements + ObjectHistoryEntryFormatter { + + /** + * A bundle providing localized strings. + */ + private final ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TODETAIL_PATH); + + /** + * Creates a new instance. + */ + public TransportOrderHistoryEntryFormatter() { + } + + @Override + public Optional apply(ObjectHistory.Entry entry) { + requireNonNull(entry, "entry"); + + switch (entry.getEventCode()) { + case TransportOrderHistoryCodes.ORDER_CREATED: + return Optional.of( + bundle.getString("transportOrderHistoryEntryFormatter.code_orderCreated.text") + ); + + case TransportOrderHistoryCodes.ORDER_DISPATCHING_DEFERRED: + return Optional.of( + bundle.getString( + "transportOrderHistoryEntryFormatter.code_orderDispatchingDeferred.text" + ) + + " " + entry.getSupplement().toString() + ); + + case TransportOrderHistoryCodes.ORDER_DISPATCHING_RESUMED: + return Optional.of( + bundle.getString( + "transportOrderHistoryEntryFormatter.code_orderDispatchingResumed.text" + ) + ); + + case TransportOrderHistoryCodes.ORDER_ASSIGNED_TO_VEHICLE: + return Optional.of( + bundle.getString("transportOrderHistoryEntryFormatter.code_orderAssignedToVehicle.text") + + " '" + entry.getSupplement().toString() + "'" + ); + + case TransportOrderHistoryCodes.ORDER_RESERVED_FOR_VEHICLE: + return Optional.of( + bundle.getString( + "transportOrderHistoryEntryFormatter.code_orderReservedForVehicle.text" + ) + + " '" + entry.getSupplement().toString() + "'" + ); + + case TransportOrderHistoryCodes.ORDER_PROCESSING_VEHICLE_CHANGED: + return Optional.of( + bundle.getString( + "transportOrderHistoryEntryFormatter.code_orderProcVehicleChanged.text" + ) + + " '" + entry.getSupplement().toString() + "'" + ); + + case TransportOrderHistoryCodes.ORDER_DRIVE_ORDER_FINISHED: + return Optional.of( + bundle.getString("transportOrderHistoryEntryFormatter.code_driveOrderFinished.text") + ); + + case TransportOrderHistoryCodes.ORDER_REACHED_FINAL_STATE: + return Optional.of( + bundle.getString("transportOrderHistoryEntryFormatter.code_orderReachedFinalState.text") + ); + + default: + return Optional.empty(); + } + } + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/UneditableTableModel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/UneditableTableModel.java new file mode 100644 index 0000000..38b626f --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/UneditableTableModel.java @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport; + +import javax.swing.table.DefaultTableModel; + +/** + * A table model in which each cell is uneditable. + */ +public class UneditableTableModel + extends + DefaultTableModel { + + /** + * Creates a new instance. + */ + public UneditableTableModel() { + } + + @Override + public boolean isCellEditable(int row, int column) { + return false; + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportOrderContainerListener.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportOrderContainerListener.java new file mode 100644 index 0000000..2559fc8 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportOrderContainerListener.java @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport.orders; + +import java.util.Collection; +import org.opentcs.data.order.TransportOrder; + +/** + * Listener for changes in the {@link TransportOrdersContainerPanel}. + */ +public interface TransportOrderContainerListener { + + /** + * Notifies the listener that the container has been initialized. + * + * @param orders The orders the container has been initialized with. + */ + void containerInitialized(Collection orders); + + /** + * Notifies the listener that a transport order has been added. + * + * @param order The transport order that has been added. + */ + void transportOrderAdded(TransportOrder order); + + /** + * Notifies the listener that a transport order has been updated. + * + * @param order The transport order that has been updated. + */ + void transportOrderUpdated(TransportOrder order); + + /** + * Notifies the listener that a transport order has been removed. + * + * @param order The transport order that has been removed. + */ + void transportOrderRemoved(TransportOrder order); + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportOrderTableModel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportOrderTableModel.java new file mode 100644 index 0000000..82837aa --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportOrderTableModel.java @@ -0,0 +1,243 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport.orders; + +import static java.util.Objects.requireNonNull; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.ResourceBundle; +import java.util.Vector; +import javax.swing.SwingUtilities; +import javax.swing.table.AbstractTableModel; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A table model for transport orders. + */ +public class TransportOrderTableModel + extends + AbstractTableModel + implements + TransportOrderContainerListener { + + /** + * The index of the 'name' column. + */ + public static final int COLUMN_NAME = 0; + /** + * The index of the 'source' column. + */ + public static final int COLUMN_SOURCE = 1; + /** + * The index of the 'destination' column. + */ + public static final int COLUMN_DESTINATION = 2; + /** + * The index of the 'intended vehicle' column. + */ + public static final int COLUMN_INTENDED_VEHICLE = 3; + /** + * The index of the 'executing vehicle' column. + */ + public static final int COLUMN_EXECUTING_VEHICLE = 4; + /** + * The index of the 'status' column. + */ + public static final int COLUMN_STATUS = 5; + /** + * The index of the 'order sequence' column. + */ + public static final int COLUMN_ORDER_SEQUENCE = 6; + /** + * The index of the 'creation time' column. + */ + public static final int COLUMN_CREATION_TIME = 7; + + private static final Logger LOG = LoggerFactory.getLogger(TransportOrderTableModel.class); + /** + * The resource bundle to use. + */ + private static final ResourceBundle BUNDLE + = ResourceBundle.getBundle(I18nPlantOverviewOperating.TRANSPORTORDER_PATH); + + private static final String[] COLUMN_NAMES = new String[]{ + "Name", + BUNDLE.getString("transportOrderTableModel.column_source.headerText"), + BUNDLE.getString("transportOrderTableModel.column_destination.headerText"), + BUNDLE.getString("transportOrderTableModel.column_intendedVehicle.headerText"), + BUNDLE.getString("transportOrderTableModel.column_executingVehicle.headerText"), + "Status", + BUNDLE.getString("transportOrderTableModel.column_orderSequence.headerText"), + BUNDLE.getString("transportOrderTableModel.column_creationTime.headerText") + }; + + /** + * The column classes. + */ + private static final Class[] COLUMN_CLASSES = new Class[]{ + String.class, + String.class, + String.class, + String.class, + String.class, + String.class, + String.class, + Instant.class + }; + + private final List entries = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public TransportOrderTableModel() { + } + + @Override + public int getRowCount() { + return entries.size(); + } + + @Override + public int getColumnCount() { + return COLUMN_NAMES.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= getRowCount()) { + return null; + } + + TransportOrder entry = entries.get(rowIndex); + Vector driveOrders = new Vector<>(entry.getAllDriveOrders()); + switch (columnIndex) { + case COLUMN_NAME: + return entry.getName(); + case COLUMN_SOURCE: + if (driveOrders.size() == 1) { + return ""; + } + else { + return driveOrders.firstElement().getDestination().getDestination().getName(); + } + case COLUMN_DESTINATION: + return driveOrders.lastElement().getDestination().getDestination().getName(); + case COLUMN_INTENDED_VEHICLE: + if (entry.getIntendedVehicle() != null) { + return entry.getIntendedVehicle().getName(); + } + else { + return BUNDLE.getString( + "transportOrderTableModel.column_intendedVehicle.determinedAutomatic.text" + ); + } + case COLUMN_EXECUTING_VEHICLE: + if (entry.getProcessingVehicle() != null) { + return entry.getProcessingVehicle().getName(); + } + else { + return "?"; + } + case COLUMN_STATUS: + return entry.getState().toString(); + case COLUMN_ORDER_SEQUENCE: + if (entry.getWrappingSequence() != null) { + return entry.getWrappingSequence().getName(); + } + else { + return "-"; + } + case COLUMN_CREATION_TIME: + return entry.getCreationTime(); + default: + throw new IllegalArgumentException("Invalid column index: " + columnIndex); + } + } + + @Override + public String getColumnName(int columnIndex) { + return COLUMN_NAMES[columnIndex]; + } + + @Override + public Class getColumnClass(int columnIndex) { + return COLUMN_CLASSES[columnIndex]; + } + + @Override + public void containerInitialized(Collection orders) { + requireNonNull(orders, "orders"); + + SwingUtilities.invokeLater(() -> { + // Notifiations of any change listeners must happen at the same time/in the same thread the + // data behind the model is updated. Otherwise, there is a risk that listeners work with/ + // refer to outdated data, which can lead to runtime exceptions. + entries.clear(); + entries.addAll(orders); + fireTableDataChanged(); + }); + } + + @Override + public void transportOrderAdded(TransportOrder order) { + requireNonNull(order, "order"); + + SwingUtilities.invokeLater(() -> { + entries.add(order); + fireTableRowsInserted(entries.size() - 1, entries.size() - 1); + }); + } + + @Override + public void transportOrderUpdated(TransportOrder order) { + requireNonNull(order, "order"); + + SwingUtilities.invokeLater(() -> { + int orderIndex = entries.indexOf(order); + if (orderIndex == -1) { + LOG.warn("Unknown transport order: {}. Ignoring order update.", order.getName()); + return; + } + entries.set(orderIndex, order); + fireTableRowsUpdated(orderIndex, orderIndex); + }); + } + + @Override + public void transportOrderRemoved(TransportOrder order) { + requireNonNull(order, "order"); + + SwingUtilities.invokeLater(() -> { + int orderIndex = entries.indexOf(order); + if (orderIndex == -1) { + LOG.warn("Unknown transport order: {}. Ignoring order removal.", order.getName()); + return; + } + entries.remove(orderIndex); + fireTableRowsDeleted(orderIndex, orderIndex); + }); + + } + + /** + * Returns the transport order at the specified index. + * + * @param index the index to return. + * @return the transport order at that index. + */ + public TransportOrder getEntryAt(int index) { + if (index < 0 || index >= entries.size()) { + return null; + } + + return entries.get(index); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportOrderView.form b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportOrderView.form new file mode 100644 index 0000000..747e2a8 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportOrderView.form @@ -0,0 +1,585 @@ + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportOrderView.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportOrderView.java new file mode 100644 index 0000000..356dc30 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportOrderView.java @@ -0,0 +1,753 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport.orders; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.Component; +import java.awt.Dimension; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.Date; +import java.util.Map.Entry; +import javax.swing.JComponent; +import javax.swing.JTable; +import javax.swing.event.ListSelectionEvent; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.DefaultTableModel; +import javax.swing.table.TableModel; +import org.opentcs.components.plantoverview.ObjectHistoryEntryFormatter; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route.Step; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.guing.common.components.dialogs.DialogContent; +import org.opentcs.operationsdesk.transport.CompositeObjectHistoryEntryFormatter; +import org.opentcs.operationsdesk.transport.UneditableTableModel; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * A view on a transport order. + */ +public class TransportOrderView + extends + DialogContent { + + /** + * A formatter for timestamps. + */ + private static final DateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + /** + * A formatter for history entries. + */ + private final ObjectHistoryEntryFormatter historyEntryFormatter; + /** + * The transport order to be shown. + */ + private final TransportOrder fTransportOrder; + + /** + * Creates new instance. + * + * @param order The transport order. + * @param historyEntryFormatter A formatter for history entries. + */ + @Inject + @SuppressWarnings("this-escape") + public TransportOrderView( + @Assisted + TransportOrder order, + CompositeObjectHistoryEntryFormatter historyEntryFormatter + ) { + this.fTransportOrder = requireNonNull(order, "order"); + this.historyEntryFormatter = requireNonNull(historyEntryFormatter, "historyEntryFormatter"); + + initComponents(); + setDialogTitle( + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TODETAIL_PATH) + .getString("transportOrderView.title") + ); + } + + @Override + public void update() { + } + + @Override + public final void initFields() { + nameTextField.setText(fTransportOrder.getName()); + + createdTextField.setText(TIMESTAMP_FORMAT.format(Date.from(fTransportOrder.getCreationTime()))); + + finishedTextField.setText( + !fTransportOrder.getFinishedTime().equals(Instant.MAX) + ? TIMESTAMP_FORMAT.format(Date.from(fTransportOrder.getFinishedTime())) + : "-" + ); + + deadlineTextField.setText( + !fTransportOrder.getDeadline().equals(Instant.MAX) + ? TIMESTAMP_FORMAT.format(Date.from(fTransportOrder.getDeadline())) + : "-" + ); + + dispensableTextField.setText(Boolean.toString(fTransportOrder.isDispensable())); + + if (fTransportOrder.getProcessingVehicle() != null) { + vehicleTextField.setText(fTransportOrder.getProcessingVehicle().getName()); + } + + typeTextField.setText(fTransportOrder.getType()); + + reservationTokenTextField.setText(fTransportOrder.getPeripheralReservationToken()); + + propertiesTable.setModel(createPropertiesTableModel()); + + driveOrdersTable.setModel(createDriveOrdersTableModel()); + driveOrdersTable.getSelectionModel().addListSelectionListener((ListSelectionEvent evt) -> { + if (!evt.getValueIsAdjusting()) { + driveOrdersTableSelectionChanged(); + } + }); + + driveOrdersScrollPane.setPreferredSize(new Dimension(200, 150)); + + driveOrderPropertiesTable.setModel(createDriveOrderPropertiesTableModel()); + + routeTable.setModel(createRouteTableModel()); + + dependenciesTable.setModel(createDependenciesTableModel()); + + historyTable.setModel(createHistoryTableModel()); + historyTable.getColumnModel().getColumn(0).setPreferredWidth(100); + historyTable.getColumnModel().getColumn(1).setPreferredWidth(300); + historyTable.getColumnModel().getColumn(1).setCellRenderer(new ToolTipCellRenderer()); + } + + private TableModel createPropertiesTableModel() { + DefaultTableModel tableModel = new UneditableTableModel(); + + tableModel.setColumnIdentifiers( + new String[]{ + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TODETAIL_PATH) + .getString( + "transportOrderView.table_properties.column_propertiesKey.headerText" + ), + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TODETAIL_PATH) + .getString( + "transportOrderView.table_properties.column_propertiesValue.headerText" + ) + } + ); + fTransportOrder.getProperties().entrySet().stream() + .sorted((e1, e2) -> e1.getKey().compareTo(e2.getKey())) + .forEach(entry -> { + tableModel.addRow(new String[]{entry.getKey(), entry.getValue()}); + }); + + return tableModel; + } + + private TableModel createDriveOrdersTableModel() { + DefaultTableModel tableModel = new UneditableTableModel(); + + tableModel.setColumnIdentifiers( + new String[]{ + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TODETAIL_PATH) + .getString( + "transportOrderView.table_driveOrderProperties.column_target.headerText" + ), + "Operation", + "Status" + } + ); + + for (DriveOrder o : fTransportOrder.getAllDriveOrders()) { + String[] row = new String[3]; + row[0] = o.getDestination().getDestination().getName(); + row[1] = o.getDestination().getOperation(); + row[2] = o.getState().toString(); + tableModel.addRow(row); + } + + return tableModel; + } + + private TableModel createDriveOrderPropertiesTableModel() { + DefaultTableModel tableModel = new UneditableTableModel(); + + // We could put these long resource bundle keys somewhere else with less indentation to make + // them fit into the maximum line length, but then we would deviate from the pattern used in + // all the other places in this file, so we make an exception and disable Checkstyle here. + // -- S. Walter, 2023-02-25 + // CHECKSTYLE:OFF + tableModel.setColumnIdentifiers( + new String[]{ + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TODETAIL_PATH) + .getString( + "transportOrderView.table_driveOrderProperties.column_driveOrderPropertiesKey.headerText" + ), + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TODETAIL_PATH) + .getString( + "transportOrderView.table_driveOrderProperties.column_driveOrderPropertiesValue.headerText" + ) + } + ); + // CHECKSTYLE:ON + + return tableModel; + } + + private TableModel createRouteTableModel() { + DefaultTableModel tableModel = new UneditableTableModel(); + + tableModel.setColumnIdentifiers( + new String[]{ + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TODETAIL_PATH) + .getString("transportOrderView.table_route.column_route.headerText"), + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TODETAIL_PATH) + .getString("transportOrderView.table_routeTable.column_destination.headerText") + } + ); + + return tableModel; + } + + @SuppressWarnings("checkstyle:LineLength") + private TableModel createDependenciesTableModel() { + DefaultTableModel tableModel = new UneditableTableModel(); + + tableModel.setColumnIdentifiers( + new String[]{ + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TODETAIL_PATH) + .getString( + "transportOrderView.table_dependencies.column_dependentTransportOrder.headerText" + ) + } + ); + + for (TCSObjectReference refTransportOrder : fTransportOrder.getDependencies()) { + String[] row = new String[1]; + row[0] = refTransportOrder.getName(); + tableModel.addRow(row); + } + + return tableModel; + } + + private TableModel createHistoryTableModel() { + DefaultTableModel tableModel = new UneditableTableModel(); + + tableModel.setColumnIdentifiers( + new String[]{ + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TODETAIL_PATH) + .getString("transportOrderView.table_history.column_timestamp.headerText"), + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TODETAIL_PATH) + .getString("transportOrderView.table_history.column_event.headerText") + } + ); + + for (ObjectHistory.Entry entry : fTransportOrder.getHistory().getEntries()) { + tableModel.addRow( + new String[]{ + TIMESTAMP_FORMAT.format(Date.from(entry.getTimestamp())), + historyEntryFormatter.apply(entry).get() + } + ); + } + + return tableModel; + } + + private void driveOrdersTableSelectionChanged() { + DriveOrder driveOrder + = fTransportOrder.getAllDriveOrders().get(driveOrdersTable.getSelectedRow()); + DefaultTableModel routeTableModel = (DefaultTableModel) routeTable.getModel(); + DefaultTableModel driveOrderPropsTableModel + = (DefaultTableModel) driveOrderPropertiesTable.getModel(); + + while (routeTableModel.getRowCount() > 0) { + routeTableModel.removeRow(0); + } + while (driveOrderPropsTableModel.getRowCount() > 0) { + driveOrderPropsTableModel.removeRow(0); + } + + for (Entry entry : driveOrder.getDestination().getProperties().entrySet()) { + driveOrderPropsTableModel.addRow(new String[]{entry.getKey(), entry.getValue()}); + } + + if (driveOrder.getRoute() == null) { + return; + } + + costsTextField.setText(Long.toString(driveOrder.getRoute().getCosts())); + + for (Step step : driveOrder.getRoute().getSteps()) { + routeTableModel.addRow( + new String[]{ + step.getPath() == null ? "" : step.getPath().getName(), + step.getDestinationPoint().getName() + } + ); + } + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + generalPanel = new javax.swing.JPanel(); + nameLabel = new javax.swing.JLabel(); + nameTextField = new javax.swing.JTextField(); + createdLabel = new javax.swing.JLabel(); + createdTextField = new javax.swing.JTextField(); + finishedLabel = new javax.swing.JLabel(); + finishedTextField = new javax.swing.JTextField(); + deadlineLabel = new javax.swing.JLabel(); + deadlineTextField = new javax.swing.JTextField(); + vehicleLabel = new javax.swing.JLabel(); + vehicleTextField = new javax.swing.JTextField(); + dispensableLabel = new javax.swing.JLabel(); + dispensableTextField = new javax.swing.JTextField(); + typeLabel = new javax.swing.JLabel(); + typeTextField = new javax.swing.JTextField(); + reservationTokenLabel = new javax.swing.JLabel(); + reservationTokenTextField = new javax.swing.JTextField(); + dependenciesPanel = new javax.swing.JPanel(); + dependenciesScrollPane = new javax.swing.JScrollPane(); + dependenciesTable = new javax.swing.JTable(); + propertiesPanel = new javax.swing.JPanel(); + propertiesScrollPane = new javax.swing.JScrollPane(); + propertiesTable = new javax.swing.JTable(); + historyPanel = new javax.swing.JPanel(); + historyScrollPane = new javax.swing.JScrollPane(); + historyTable = new javax.swing.JTable(); + driveOrdersPanel = new javax.swing.JPanel(); + driveOrdersScrollPane = new javax.swing.JScrollPane(); + driveOrdersTable = new javax.swing.JTable(); + driveOrdersPropertiesPanel = new javax.swing.JPanel(); + driveOrdersPropertiesScrollPane = new javax.swing.JScrollPane(); + driveOrderPropertiesTable = new javax.swing.JTable(); + routePanel = new javax.swing.JPanel(); + routeScrollPane = new javax.swing.JScrollPane(); + routeTable = new javax.swing.JTable(); + costsLabel = new javax.swing.JLabel(); + costsTextField = new javax.swing.JTextField(); + + setLayout(new java.awt.GridBagLayout()); + + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/operating/dialogs/transportOrderDetail"); // NOI18N + generalPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("transportOrderView.panel_general.border.title"))); // NOI18N + generalPanel.setLayout(new java.awt.GridBagLayout()); + + nameLabel.setFont(nameLabel.getFont()); + nameLabel.setText("Name:"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + generalPanel.add(nameLabel, gridBagConstraints); + + nameTextField.setEditable(false); + nameTextField.setColumns(10); + nameTextField.setFont(nameTextField.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 0.5; + generalPanel.add(nameTextField, gridBagConstraints); + + createdLabel.setFont(createdLabel.getFont()); + createdLabel.setText(bundle.getString("transportOrderView.label_created.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + generalPanel.add(createdLabel, gridBagConstraints); + + createdTextField.setEditable(false); + createdTextField.setColumns(10); + createdTextField.setFont(createdTextField.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 0.5; + generalPanel.add(createdTextField, gridBagConstraints); + + finishedLabel.setFont(finishedLabel.getFont()); + finishedLabel.setText(bundle.getString("transportOrderView.label_finished.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 8, 0, 4); + generalPanel.add(finishedLabel, gridBagConstraints); + + finishedTextField.setEditable(false); + finishedTextField.setColumns(10); + finishedTextField.setFont(finishedTextField.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 3; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 0.5; + generalPanel.add(finishedTextField, gridBagConstraints); + + deadlineLabel.setFont(deadlineLabel.getFont()); + deadlineLabel.setText(bundle.getString("transportOrderView.label_deadline.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + generalPanel.add(deadlineLabel, gridBagConstraints); + + deadlineTextField.setEditable(false); + deadlineTextField.setColumns(10); + deadlineTextField.setFont(deadlineTextField.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 0.5; + generalPanel.add(deadlineTextField, gridBagConstraints); + + vehicleLabel.setFont(vehicleLabel.getFont()); + vehicleLabel.setText(bundle.getString("transportOrderView.label_vehicle.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 8, 0, 4); + generalPanel.add(vehicleLabel, gridBagConstraints); + + vehicleTextField.setEditable(false); + vehicleTextField.setColumns(10); + vehicleTextField.setFont(vehicleTextField.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 3; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 0.5; + generalPanel.add(vehicleTextField, gridBagConstraints); + + dispensableLabel.setFont(dispensableLabel.getFont()); + dispensableLabel.setText(bundle.getString("transportOrderView.label_dispensable.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + generalPanel.add(dispensableLabel, gridBagConstraints); + + dispensableTextField.setEditable(false); + dispensableTextField.setColumns(10); + dispensableTextField.setFont(dispensableTextField.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 0.5; + generalPanel.add(dispensableTextField, gridBagConstraints); + + typeLabel.setFont(typeLabel.getFont()); + typeLabel.setText(bundle.getString("transportOrderView.label_type.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 4; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 8, 0, 4); + generalPanel.add(typeLabel, gridBagConstraints); + + typeTextField.setEditable(false); + typeTextField.setColumns(10); + typeTextField.setFont(typeTextField.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 3; + gridBagConstraints.gridy = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 0.5; + generalPanel.add(typeTextField, gridBagConstraints); + + reservationTokenLabel.setText(bundle.getString("transportOrderView.label_reservationToken.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + generalPanel.add(reservationTokenLabel, gridBagConstraints); + + reservationTokenTextField.setEditable(false); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + generalPanel.add(reservationTokenTextField, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 0.1; + add(generalPanel, gridBagConstraints); + + dependenciesPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("transportOrderView.panel_dependencies.border.title"))); // NOI18N + dependenciesPanel.setLayout(new java.awt.BorderLayout()); + + dependenciesScrollPane.setPreferredSize(new java.awt.Dimension(150, 100)); + + dependenciesTable.setFont(dependenciesTable.getFont()); + dependenciesTable.setModel(new javax.swing.table.DefaultTableModel( + new Object[][]{ + + }, + new String[]{ + + } + )); + dependenciesScrollPane.setViewportView(dependenciesTable); + + dependenciesPanel.add(dependenciesScrollPane, java.awt.BorderLayout.CENTER); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.weighty = 0.5; + add(dependenciesPanel, gridBagConstraints); + + propertiesPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("transportOrderView.panel_properties.border.title"))); // NOI18N + propertiesPanel.setLayout(new java.awt.BorderLayout()); + + propertiesScrollPane.setPreferredSize(new java.awt.Dimension(150, 100)); + + propertiesTable.setFont(propertiesTable.getFont()); + propertiesScrollPane.setViewportView(propertiesTable); + + propertiesPanel.add(propertiesScrollPane, java.awt.BorderLayout.CENTER); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.weighty = 0.5; + add(propertiesPanel, gridBagConstraints); + + historyPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("transportOrderView.panel_history.border.title"))); // NOI18N + historyPanel.setLayout(new java.awt.BorderLayout()); + + historyScrollPane.setPreferredSize(new java.awt.Dimension(150, 100)); + + historyTable.setModel(new javax.swing.table.DefaultTableModel( + new Object[][]{ + + }, + new String[]{ + + } + )); + historyScrollPane.setViewportView(historyTable); + + historyPanel.add(historyScrollPane, java.awt.BorderLayout.CENTER); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.weighty = 0.5; + add(historyPanel, gridBagConstraints); + + driveOrdersPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("transportOrderView.panel_driveOrders.border.title"))); // NOI18N + driveOrdersPanel.setLayout(new java.awt.GridBagLayout()); + + driveOrdersScrollPane.setPreferredSize(new java.awt.Dimension(300, 100)); + driveOrdersScrollPane.setViewportView(driveOrdersTable); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.gridheight = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + driveOrdersPanel.add(driveOrdersScrollPane, gridBagConstraints); + + driveOrdersPropertiesPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("transportOrderView.panel_driveOrderProperties.border.title"))); // NOI18N + driveOrdersPropertiesPanel.setPreferredSize(new java.awt.Dimension(162, 140)); + driveOrdersPropertiesPanel.setLayout(new java.awt.BorderLayout()); + + driveOrdersPropertiesScrollPane.setPreferredSize(new java.awt.Dimension(150, 50)); + + driveOrderPropertiesTable.setFont(driveOrderPropertiesTable.getFont()); + driveOrdersPropertiesScrollPane.setViewportView(driveOrderPropertiesTable); + + driveOrdersPropertiesPanel.add(driveOrdersPropertiesScrollPane, java.awt.BorderLayout.CENTER); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + driveOrdersPanel.add(driveOrdersPropertiesPanel, gridBagConstraints); + + routePanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("transportOrderView.panel_route.border.title"))); // NOI18N + routePanel.setPreferredSize(new java.awt.Dimension(300, 140)); + routePanel.setLayout(new java.awt.BorderLayout()); + + routeScrollPane.setViewportView(routeTable); + + routePanel.add(routeScrollPane, java.awt.BorderLayout.CENTER); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + driveOrdersPanel.add(routePanel, gridBagConstraints); + + costsLabel.setFont(costsLabel.getFont()); + costsLabel.setText(bundle.getString("transportOrderView.label_costs.title")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + driveOrdersPanel.add(costsLabel, gridBagConstraints); + + costsTextField.setEditable(false); + costsTextField.setColumns(5); + costsTextField.setFont(costsTextField.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 0.5; + driveOrdersPanel.add(costsTextField, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 0.5; + add(driveOrdersPanel, gridBagConstraints); + }// //GEN-END:initComponents + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel costsLabel; + private javax.swing.JTextField costsTextField; + private javax.swing.JLabel createdLabel; + private javax.swing.JTextField createdTextField; + private javax.swing.JLabel deadlineLabel; + private javax.swing.JTextField deadlineTextField; + private javax.swing.JPanel dependenciesPanel; + private javax.swing.JScrollPane dependenciesScrollPane; + private javax.swing.JTable dependenciesTable; + private javax.swing.JLabel dispensableLabel; + private javax.swing.JTextField dispensableTextField; + private javax.swing.JTable driveOrderPropertiesTable; + private javax.swing.JPanel driveOrdersPanel; + private javax.swing.JPanel driveOrdersPropertiesPanel; + private javax.swing.JScrollPane driveOrdersPropertiesScrollPane; + private javax.swing.JScrollPane driveOrdersScrollPane; + private javax.swing.JTable driveOrdersTable; + private javax.swing.JLabel finishedLabel; + private javax.swing.JTextField finishedTextField; + private javax.swing.JPanel generalPanel; + private javax.swing.JPanel historyPanel; + private javax.swing.JScrollPane historyScrollPane; + private javax.swing.JTable historyTable; + private javax.swing.JLabel nameLabel; + private javax.swing.JTextField nameTextField; + private javax.swing.JPanel propertiesPanel; + private javax.swing.JScrollPane propertiesScrollPane; + private javax.swing.JTable propertiesTable; + private javax.swing.JLabel reservationTokenLabel; + private javax.swing.JTextField reservationTokenTextField; + private javax.swing.JPanel routePanel; + private javax.swing.JScrollPane routeScrollPane; + private javax.swing.JTable routeTable; + private javax.swing.JLabel typeLabel; + private javax.swing.JTextField typeTextField; + private javax.swing.JLabel vehicleLabel; + private javax.swing.JTextField vehicleTextField; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON + + /** + * A cell renderer that adds a tool tip with the cell's value. + */ + private static class ToolTipCellRenderer + extends + DefaultTableCellRenderer { + + /** + * Creates a new instance. + */ + ToolTipCellRenderer() { + } + + @Override + public Component getTableCellRendererComponent( + JTable table, + Object value, + boolean isSelected, + boolean hasFocus, + int row, + int column + ) { + Component component = super.getTableCellRendererComponent( + table, + value, + isSelected, + hasFocus, + row, + column + ); + + ((JComponent) component).setToolTipText(value.toString()); + + return component; + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportOrdersContainer.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportOrdersContainer.java new file mode 100644 index 0000000..d1fb015 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportOrdersContainer.java @@ -0,0 +1,222 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport.orders; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.common.KernelClientApplication; +import org.opentcs.components.Lifecycle; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.guing.common.event.OperationModeChangeEvent; +import org.opentcs.guing.common.event.SystemModelTransitionEvent; +import org.opentcs.operationsdesk.event.KernelStateChangeEvent; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Maintains a set of all transport orders existing on the kernel side. + */ +public class TransportOrdersContainer + implements + EventHandler, + Lifecycle { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(TransportOrdersContainer.class); + /** + * Where we get events from. + */ + private final EventBus eventBus; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * The kernel client application. + */ + private final KernelClientApplication kernelClientApplication; + /** + * The transport orders. + */ + private final Map transportOrders = new HashMap<>(); + /** + * This container's listeners. + */ + private final Set listeners = new HashSet<>(); + /** + * Whether this component is initialized. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param eventBus Where this instance subscribes for events. + * @param portalProvider Provides access to a portal. + * @param kernelClientApplication The kernel client application. + */ + @Inject + public TransportOrdersContainer( + @ApplicationEventBus + EventBus eventBus, + SharedKernelServicePortalProvider portalProvider, + KernelClientApplication kernelClientApplication + ) { + this.eventBus = requireNonNull(eventBus, "eventBus"); + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.kernelClientApplication + = requireNonNull(kernelClientApplication, "kernelClientApplication"); + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + eventBus.subscribe(this); + + initialized = true; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + eventBus.unsubscribe(this); + + initialized = false; + } + + @Override + public void onEvent(Object event) { + if (event instanceof TCSObjectEvent) { + handleObjectEvent((TCSObjectEvent) event); + } + else if (event instanceof OperationModeChangeEvent) { + initOrders(); + } + else if (event instanceof SystemModelTransitionEvent) { + initOrders(); + } + else if (event instanceof KernelStateChangeEvent) { + initOrders(); + } + } + + public void addListener(TransportOrderContainerListener listener) { + listeners.add(listener); + } + + public void removeListener(TransportOrderContainerListener listener) { + listeners.remove(listener); + } + + /** + * Returns the transport order with the given name, if it exists. + * + * @param name The name of the transport order. + * @return The transport order with the given name, if it exists. + */ + public Optional getTransportOrder( + @Nonnull + String name + ) { + requireNonNull(name, "name"); + + return Optional.ofNullable(transportOrders.get(name)); + } + + /** + * Returns all currently stored transport orders. + * + * @return The collection of transport orders. + */ + public Collection getTransportOrders() { + return transportOrders.values(); + } + + private void initOrders() { + setTransportOrders(fetchOrdersIfOnline()); + listeners.forEach(listener -> listener.containerInitialized(transportOrders.values())); + } + + private void handleObjectEvent(TCSObjectEvent evt) { + if (evt.getCurrentOrPreviousObjectState() instanceof TransportOrder) { + switch (evt.getType()) { + case OBJECT_CREATED: + transportOrderAdded((TransportOrder) evt.getCurrentOrPreviousObjectState()); + break; + case OBJECT_MODIFIED: + transportOrderChanged((TransportOrder) evt.getCurrentOrPreviousObjectState()); + break; + case OBJECT_REMOVED: + transportOrderRemoved((TransportOrder) evt.getCurrentOrPreviousObjectState()); + break; + default: + LOG.warn("Unhandled event type: {}", evt.getType()); + } + } + } + + private void transportOrderAdded(TransportOrder order) { + transportOrders.put(order.getName(), order); + listeners.forEach(listener -> listener.transportOrderAdded(order)); + } + + private void transportOrderChanged(TransportOrder order) { + transportOrders.put(order.getName(), order); + listeners.forEach(listener -> listener.transportOrderUpdated(order)); + } + + private void transportOrderRemoved(TransportOrder order) { + transportOrders.remove(order.getName()); + listeners.forEach(listener -> listener.transportOrderRemoved(order)); + } + + private void setTransportOrders(Set newOrders) { + transportOrders.clear(); + for (TransportOrder order : newOrders) { + transportOrders.put(order.getName(), order); + } + } + + private Set fetchOrdersIfOnline() { + if (kernelClientApplication.isOnline()) { + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + return sharedPortal.getPortal().getTransportOrderService() + .fetchObjects(TransportOrder.class); + } + catch (KernelRuntimeException exc) { + LOG.warn("Exception fetching transport orders", exc); + } + } + + return new HashSet<>(); + } + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportOrdersContainerPanel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportOrdersContainerPanel.java new file mode 100644 index 0000000..2f669f0 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportOrdersContainerPanel.java @@ -0,0 +1,615 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport.orders; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.data.order.TransportOrder.State.ACTIVE; +import static org.opentcs.data.order.TransportOrder.State.BEING_PROCESSED; +import static org.opentcs.data.order.TransportOrder.State.DISPATCHABLE; +import static org.opentcs.data.order.TransportOrder.State.FAILED; +import static org.opentcs.data.order.TransportOrder.State.FINISHED; +import static org.opentcs.data.order.TransportOrder.State.RAW; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import javax.swing.JButton; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JSeparator; +import javax.swing.JTable; +import javax.swing.JToggleButton; +import javax.swing.JToolBar; +import javax.swing.ListSelectionModel; +import javax.swing.RowFilter; +import javax.swing.RowSorter; +import javax.swing.SortOrder; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.components.kernel.dipatching.TransportOrderAssignmentException; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.guing.common.components.dialogs.DialogContent; +import org.opentcs.guing.common.components.dialogs.StandardContentDialog; +import org.opentcs.guing.common.util.IconToolkit; +import org.opentcs.operationsdesk.exchange.TransportOrderUtil; +import org.opentcs.operationsdesk.transport.CreateTransportOrderPanel; +import org.opentcs.operationsdesk.transport.FilterButton; +import org.opentcs.operationsdesk.transport.FilteredRowSorter; +import org.opentcs.operationsdesk.transport.IntendedVehiclesPanel; +import org.opentcs.operationsdesk.transport.OrdersTable; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Shows a table of the kernel's transport orders. + */ +public class TransportOrdersContainerPanel + extends + JPanel { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(TransportOrdersContainerPanel.class); + /** + * The path containing the icons. + */ + private static final String ICON_PATH = "/org/opentcs/guing/res/symbols/panel/"; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * A helper for creating transport orders with the kernel. + */ + private final TransportOrderUtil orderUtil; + /** + * Provides panels for entering new transport orders. + */ + private final Provider orderPanelProvider; + /** + * A factory for creating transport order views. + */ + private final TransportViewFactory transportViewFactory; + /** + * The table showing the transport orders. + */ + private JTable fTable; + /** + * The table's model. + */ + private TransportOrderTableModel tableModel; + /** + * The sorter for the table. + */ + private FilteredRowSorter sorter; + /** + * Holds the transport orders. + */ + private final TransportOrdersContainer transportOrdersContainer; + + /** + * Creates a new instance. + * + * @param portalProvider Provides a access to a portal. + * @param orderUtil A helper for creating transport orders with the kernel. + * @param orderPanelProvider Provides panels for entering new transport orders. + * @param transportViewFactory A factory for creating transport order views. + * @param transportOrderContainer Maintains a set of transport order on the kernel side. + */ + @Inject + @SuppressWarnings("this-escape") + public TransportOrdersContainerPanel( + SharedKernelServicePortalProvider portalProvider, + TransportOrderUtil orderUtil, + Provider orderPanelProvider, + TransportViewFactory transportViewFactory, + TransportOrdersContainer transportOrderContainer + ) { + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.orderUtil = requireNonNull(orderUtil, "orderUtil"); + this.orderPanelProvider = requireNonNull(orderPanelProvider, "orderPanelProvider"); + this.transportViewFactory = requireNonNull(transportViewFactory, "transportViewFactory"); + this.transportOrdersContainer = requireNonNull( + transportOrderContainer, + "transportOrderContainer" + ); + + initComponents(); + } + + /** + * Initializes this panel's contents. + */ + public void initView() { + tableModel.containerInitialized(transportOrdersContainer.getTransportOrders()); + } + + private void initComponents() { + setLayout(new BorderLayout()); + + tableModel = new TransportOrderTableModel(); + transportOrdersContainer.addListener(tableModel); + fTable = new OrdersTable(tableModel); + fTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + + sorter = new FilteredRowSorter<>(tableModel); + // Sort the table by the creation instant. + sorter.setSortKeys( + Arrays.asList( + new RowSorter.SortKey( + TransportOrderTableModel.COLUMN_CREATION_TIME, SortOrder.DESCENDING + ) + ) + ); + // ...but prevent manual sorting. + for (int i = 0; i < fTable.getColumnCount(); i++) { + sorter.setSortable(i, false); + } + sorter.setSortsOnUpdates(true); + fTable.setRowSorter(sorter); + + // Hide the column that shows the creation time. + fTable.removeColumn( + fTable.getColumnModel() + .getColumn( + fTable.convertColumnIndexToView(TransportOrderTableModel.COLUMN_CREATION_TIME) + ) + ); + + JScrollPane scrollPane = new JScrollPane(fTable); + add(scrollPane, BorderLayout.CENTER); + + JToolBar toolBar = createToolBar(createFilterButtons()); + addControlButtons(toolBar); + add(toolBar, BorderLayout.NORTH); + + fTable.addMouseListener(new MouseAdapter() { + + @Override + public void mouseClicked(MouseEvent evt) { + if (evt.getButton() == MouseEvent.BUTTON1) { + if (evt.getClickCount() == 2) { + showSelectedTransportOrder(); + } + } + + if (evt.getButton() == MouseEvent.BUTTON3) { + if (fTable.getSelectedRow() != -1) { + showPopupMenuForSelectedTransportOrder(evt.getX(), evt.getY()); + } + } + } + }); + } + + private void showSelectedTransportOrder() { + getSelectedTransportOrder().ifPresent(transportOrder -> { + DialogContent content = transportViewFactory.createTransportOrderView(transportOrder); + StandardContentDialog dialog + = new StandardContentDialog( + JOptionPane.getFrameForComponent(this), + content, + true, + StandardContentDialog.CLOSE + ); + dialog.setTitle( + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TODETAIL_PATH) + .getString("transportOrdersContainerPanel.dialog_createTransportOrder.title") + ); + dialog.setVisible(true); + }); + } + + private void createTransportOrderWithPattern() { + getSelectedTransportOrder().ifPresent(transportOrder -> { + CreateTransportOrderPanel content = orderPanelProvider.get(); + content.setPattern(transportOrder); + StandardContentDialog dialog + = new StandardContentDialog( + JOptionPane.getFrameForComponent(this), + content + ); + dialog.setVisible(true); + + if (dialog.getReturnStatus() == StandardContentDialog.RET_OK) { + orderUtil.createTransportOrder( + content.getDestinationModels(), + content.getActions(), + content.getPropertiesList(), + content.getSelectedDeadline(), + content.getSelectedVehicle(), + content.getSelectedType() + ); + } + }); + } + + private void createCopyOfSelectedTransportOrder() { + getSelectedTransportOrder().ifPresent( + transportOrder -> orderUtil.createTransportOrder(transportOrder) + ); + } + + private void setIntendedVehicle() { + getSelectedTransportOrder().ifPresent(transportOrder -> { + ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TRANSPORTORDER_PATH); + + Set vehicles = null; + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + vehicles = sharedPortal.getPortal().getVehicleService().fetchObjects(Vehicle.class); + } + catch (KernelRuntimeException exc) { + LOG.warn("Exception retrieving vehicles", exc); + JOptionPane.showMessageDialog( + this, + bundle.getString( + "transportOrdersContainerPanel.table_orders." + + "popupMenuItem_setIntendedVehicle.error.message" + ), + bundle.getString( + "transportOrdersContainerPanel.table_orders." + + "popupMenuItem_setIntendedVehicle.error.title" + ), + ERROR + ); + return; + } + + IntendedVehiclesPanel panel = new IntendedVehiclesPanel(vehicles); + StandardContentDialog dialog = new StandardContentDialog(this, panel); + panel.addInputValidationListener(dialog); + dialog.setTitle( + bundle.getString( + "transportOrdersContainerPanel.table_orders.popupMenuItem_setIntendedVehicle.text" + ) + ); + dialog.setVisible(true); + + if (dialog.getReturnStatus() != StandardContentDialog.RET_OK) { + return; + } + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + sharedPortal.getPortal().getTransportOrderService().updateTransportOrderIntendedVehicle( + transportOrder.getReference(), + panel.getSelectedVehicle() + .map(v -> v.getReference()) + .orElse(null) + ); + } + catch (KernelRuntimeException exc) { + LOG.warn( + "Exception setting intended vehicle {} on transport order {}", + panel.getSelectedVehicle() + .map(v -> v.getName()) + .orElse("null"), + transportOrder.getName(), + exc + ); + JOptionPane.showMessageDialog( + this, + bundle.getString( + "transportOrdersContainerPanel.table_orders." + + "popupMenuItem_setIntendedVehicle.error.message" + ), + bundle.getString( + "transportOrdersContainerPanel.table_orders." + + "popupMenuItem_setIntendedVehicle.error.title" + ), + ERROR + ); + } + }); + } + + private void assignTransportOrderToVehicle() { + getSelectedTransportOrder().ifPresent(transportOrder -> { + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + sharedPortal.getPortal().getDispatcherService().assignNow(transportOrder.getReference()); + } + catch (TransportOrderAssignmentException exc) { + LOG.warn("Exception assigning transport order: {}", transportOrder.getName(), exc); + ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TRANSPORTORDER_PATH); + JOptionPane.showMessageDialog( + this, + mapOrderAssignmentVetoToReason(exc), + bundle.getString( + "transportOrdersContainerPanel.table_orders." + + "popupMenuItem_assignToVehicle.error.title" + ), + ERROR + ); + } + catch (KernelRuntimeException exc) { + LOG.warn("Exception assigning transport order: {}", transportOrder.getName(), exc); + ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TRANSPORTORDER_PATH); + JOptionPane.showMessageDialog( + this, + bundle.getString( + "transportOrdersContainerPanel.table_orders." + + "popupMenuItem_assignToVehicle.error.genericMessage" + ), + bundle.getString( + "transportOrdersContainerPanel.table_orders." + + "popupMenuItem_assignToVehicle.error.title" + ), + ERROR + ); + } + }); + } + + private String mapOrderAssignmentVetoToReason(TransportOrderAssignmentException exc) { + ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TRANSPORTORDER_PATH); + switch (exc.getTransportOrderAssignmentVeto()) { + case TRANSPORT_ORDER_STATE_INVALID: + return bundle.getString( + "transportOrdersContainerPanel.table_orders." + + "popupMenuItem_assignToVehicle.error.transportOrderStateInvalid" + ); + case TRANSPORT_ORDER_PART_OF_ORDER_SEQUENCE: + return bundle.getString( + "transportOrdersContainerPanel.table_orders." + + "popupMenuItem_assignToVehicle.error.transportOrderIsPartOfSequence" + ); + case TRANSPORT_ORDER_INTENDED_VEHICLE_NOT_SET: + return bundle.getString( + "transportOrdersContainerPanel.table_orders." + + "popupMenuItem_assignToVehicle.error.transportOrderIntendedVehicleNotSet" + ); + case VEHICLE_PROCESSING_STATE_INVALID: + return bundle.getString( + "transportOrdersContainerPanel.table_orders." + + "popupMenuItem_assignToVehicle.error.vehicleProcessingStateInvalid" + ); + case VEHICLE_STATE_INVALID: + return bundle.getString( + "transportOrdersContainerPanel.table_orders." + + "popupMenuItem_assignToVehicle.error.vehicleStateInvalid" + ); + case VEHICLE_INTEGRATION_LEVEL_INVALID: + return bundle.getString( + "transportOrdersContainerPanel.table_orders." + + "popupMenuItem_assignToVehicle.error.vehicleIntegrationLevelInvalid" + ); + case VEHICLE_CURRENT_POSITION_UNKNOWN: + return bundle.getString( + "transportOrdersContainerPanel.table_orders." + + "popupMenuItem_assignToVehicle.error.vehiclePositionUnknown" + ); + case VEHICLE_PROCESSING_ORDER_SEQUENCE: + return bundle.getString( + "transportOrdersContainerPanel.table_orders." + + "popupMenuItem_assignToVehicle.error.vehicleIsProcessingOrderSequence" + ); + case GENERIC_VETO: + return bundle.getString( + "transportOrdersContainerPanel.table_orders." + + "popupMenuItem_assignToVehicle.error.genericMessage" + ); + case NO_VETO: + default: + LOG.warn( + "TransportOrderAssignmentException with unmapped veto: {}, caused by: {}", + exc.getTransportOrderAssignmentVeto().name(), + exc + ); + return exc.getTransportOrderAssignmentVeto().name(); + } + } + + private void showPopupMenuForSelectedTransportOrder(int x, int y) { + boolean singleRowSelected = fTable.getSelectedRowCount() <= 1; + ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TRANSPORTORDER_PATH); + JPopupMenu menu = new JPopupMenu(); + JMenuItem item = menu.add( + bundle.getString( + "transportOrdersContainerPanel.table_orders.popupMenuItem_showDetails.text" + ) + ); + item.setEnabled(singleRowSelected); + item.addActionListener((ActionEvent evt) -> showSelectedTransportOrder()); + + menu.add(new JSeparator()); + + item = menu.add( + bundle.getString( + "transportOrdersContainerPanel.table_orders.popupMenuItem_orderAsTemplate.text" + ) + ); + item.setEnabled(singleRowSelected); + item.addActionListener((ActionEvent evt) -> createTransportOrderWithPattern()); + + item = menu.add( + bundle.getString( + "transportOrdersContainerPanel.table_orders.popupMenuItem_copyOrder.text" + ) + ); + item.setEnabled(singleRowSelected); + item.addActionListener((ActionEvent evt) -> createCopyOfSelectedTransportOrder()); + + menu.add(new JSeparator()); + + Optional maybeTransportOrder = getSelectedTransportOrder(); + maybeTransportOrder.ifPresent(transportOrder -> { + boolean statePreparing = (transportOrder.getState() == RAW + || transportOrder.getState() == ACTIVE + || transportOrder.getState() == DISPATCHABLE); + + JMenuItem menuItem = menu.add( + bundle.getString( + "transportOrdersContainerPanel.table_orders.popupMenuItem_setIntendedVehicle.text" + ) + ); + menuItem.setEnabled(singleRowSelected && statePreparing); + menuItem.addActionListener((ActionEvent evt) -> setIntendedVehicle()); + + menuItem = menu.add( + bundle.getString( + "transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.text" + ) + ); + menuItem.setEnabled( + singleRowSelected + && statePreparing + && transportOrder.getIntendedVehicle() != null + ); + menuItem.addActionListener((ActionEvent evt) -> assignTransportOrderToVehicle()); + }); + + menu.show(fTable, x, y); + } + + private void addControlButtons(JToolBar toolBar) { + ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TRANSPORTORDER_PATH); + + toolBar.add(new JToolBar.Separator()); + + JButton button = new JButton( + IconToolkit.instance().getImageIconByFullPath(ICON_PATH + "table-row-delete-2.16x16.png") + ); + button.addActionListener((ActionEvent e) -> withdrawTransportOrder()); + button.setToolTipText( + bundle.getString("transportOrdersContainerPanel.button_withdrawSelectedOrders.tooltipText") + ); + toolBar.add(button); + } + + private Optional getSelectedTransportOrder() { + int row = fTable.convertRowIndexToModel(fTable.getSelectedRow()); + if (row == -1) { + return Optional.empty(); + } + + return Optional.of(tableModel.getEntryAt(row)); + } + + private List createFilterButtons() { + ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TRANSPORTORDER_PATH); + JToggleButton button; + List buttons = new ArrayList<>(); + IconToolkit iconkit = IconToolkit.instance(); + + button = new FilterButton( + iconkit.getImageIconByFullPath(ICON_PATH + "filterRaw.16x16.gif"), + createFilterForState(RAW), + sorter + ); + + button.setToolTipText( + bundle.getString("transportOrdersContainerPanel.button_filterRawOrders.tooltipText") + ); + buttons.add(button); + + button + = new FilterButton( + iconkit.getImageIconByFullPath(ICON_PATH + "filterActivated.16x16.gif"), + createFilterForState(DISPATCHABLE), + sorter + ); + button.setToolTipText( + bundle.getString( + "transportOrdersContainerPanel.button_filterDispatchableOrders.tooltipText" + ) + ); + buttons.add(button); + + button + = new FilterButton( + iconkit.getImageIconByFullPath(ICON_PATH + "filterProcessing.16x16.gif"), + createFilterForState(BEING_PROCESSED), + sorter + ); + button.setToolTipText( + bundle.getString("transportOrdersContainerPanel.button_filterProcessedOrders.tooltipText") + ); + buttons.add(button); + + button + = new FilterButton( + iconkit.getImageIconByFullPath(ICON_PATH + "filterFinished.16x16.gif"), + createFilterForState(FINISHED), + sorter + ); + button.setToolTipText( + bundle.getString("transportOrdersContainerPanel.button_filterFinishedOrders.tooltipText") + ); + buttons.add(button); + + button = new FilterButton( + iconkit.getImageIconByFullPath(ICON_PATH + "filterFailed.16x16.gif"), + createFilterForState(FAILED), + sorter + ); + button.setToolTipText( + bundle.getString("transportOrdersContainerPanel.button_filterFailedOrders.tooltipText") + ); + buttons.add(button); + + return buttons; + } + + private RowFilter createFilterForState(TransportOrder.State state) { + return new RowFilter() { + @Override + public boolean include(Entry entry) { + TransportOrder order + = ((TransportOrderTableModel) entry.getModel()).getEntryAt((int) entry.getIdentifier()); + return order.getState() != state; + } + }; + } + + private JToolBar createToolBar(List filterButtons) { + JToolBar toolBar = new JToolBar(); + + for (JToggleButton button : filterButtons) { + toolBar.add(button); + } + + return toolBar; + } + + private void withdrawTransportOrder() { + int[] indices = fTable.getSelectedRows(); + List toWithdraw = new ArrayList<>(); + + for (int i = 0; i < indices.length; i++) { + int modelIndex = fTable.convertRowIndexToModel(indices[i]); + TransportOrder order = tableModel.getEntryAt(modelIndex); + toWithdraw.add(order); + } + + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + for (TransportOrder order : toWithdraw) { + sharedPortal.getPortal().getDispatcherService() + .withdrawByTransportOrder(order.getReference(), false); + } + } + catch (KernelRuntimeException exc) { + LOG.warn("Exception withdrawing transport order", exc); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportViewFactory.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportViewFactory.java new file mode 100644 index 0000000..4abfba0 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/orders/TransportViewFactory.java @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport.orders; + +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.operationsdesk.transport.sequences.OrderSequenceView; + +/** + * Creates transport order-related GUI components. + */ +public interface TransportViewFactory { + + /** + * Creates a new view for a transport order. + * + * @param order The transport order to be shown. + * @return A new view for a transport order. + */ + TransportOrderView createTransportOrderView(TransportOrder order); + + /** + * Creates a new view for an order sequence. + * + * @param sequence The order sequence to be shown. + * @return A new view for an order sequence. + */ + OrderSequenceView createOrderSequenceView(OrderSequence sequence); +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/sequences/OrderSequenceContainerListener.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/sequences/OrderSequenceContainerListener.java new file mode 100644 index 0000000..fef9657 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/sequences/OrderSequenceContainerListener.java @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport.sequences; + +import java.util.Collection; +import org.opentcs.data.order.OrderSequence; + +/** + * + * Listener for changes in the {@link OrderSequencesContainerPanel}. + */ +public interface OrderSequenceContainerListener { + + /** + * Notifies the listener that the container has been initialized. + * + * @param sequences The sequences the container has been initialized with. + */ + void containerInitialized(Collection sequences); + + /** + * Notifies the listener that an order sequence has been added. + * + * @param sequence The order sequence that has been added. + */ + void orderSequenceAdded(OrderSequence sequence); + + /** + * Notifies the listener that an order sequence has been updated. + * + * @param sequence The order sequence that has been updated. + */ + void orderSequenceUpdated(OrderSequence sequence); + + /** + * Notifies the listener that an order sequence has been removed. + * + * @param sequence The order sequence that has been removed. + */ + void orderSequenceRemoved(OrderSequence sequence); +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/sequences/OrderSequenceTableModel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/sequences/OrderSequenceTableModel.java new file mode 100644 index 0000000..20bdfb4 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/sequences/OrderSequenceTableModel.java @@ -0,0 +1,204 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport.sequences; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.ResourceBundle; +import javax.swing.SwingUtilities; +import javax.swing.table.AbstractTableModel; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + */ +public class OrderSequenceTableModel + extends + AbstractTableModel + implements + OrderSequenceContainerListener { + + private static final Logger LOG = LoggerFactory.getLogger(OrderSequenceTableModel.class); + /** + * The resource bundle to use. + */ + private static final ResourceBundle BUNDLE + = ResourceBundle.getBundle(I18nPlantOverviewOperating.TO_SEQUENCE_PATH); + + private static final int COLUMN_NAME = 0; + private static final int COLUMN_INTENDED_VEHICLE = 1; + private static final int COLUMN_EXECUTING_VEHICLE = 2; + private static final int COLUMN_INDEX = 3; + private static final int COLUMN_COMPLETED = 4; + private static final int COLUMN_FINISHED = 5; + private static final int COLUMN_FAILURE = 6; + + private static final String[] COLUMN_NAMES = { + "Name", + BUNDLE.getString("orderSequenceTableModel.column_intendedVehicle.headerText"), + BUNDLE.getString("orderSequenceTableModel.column_executingVehicle.headerText"), + "Index", + BUNDLE.getString("orderSequenceTableModel.column_complete.headerText"), + BUNDLE.getString("orderSequenceTableModel.column_finished.headerText"), + BUNDLE.getString("orderSequenceTableModel.column_failureFatal.headerText") + }; + + /** + * The column classes. + */ + private static final Class[] COLUMN_CLASSES = new Class[]{ + String.class, + String.class, + String.class, + String.class, + String.class, + String.class, + String.class + }; + + private final List entries = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public OrderSequenceTableModel() { + } + + @Override + public int getRowCount() { + return entries.size(); + } + + @Override + public int getColumnCount() { + return COLUMN_NAMES.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= entries.size()) { + return null; + } + + OrderSequence entry = entries.get(rowIndex); + switch (columnIndex) { + case COLUMN_NAME: + return entry.getName(); + + case COLUMN_INTENDED_VEHICLE: + if (entry.getIntendedVehicle() != null) { + return entry.getIntendedVehicle().getName(); + } + else { + return BUNDLE.getString( + "orderSequenceTableModel.column_intendedVehicle.determinedAutomatic.text" + ); + } + case COLUMN_EXECUTING_VEHICLE: + + if (entry.getProcessingVehicle() != null) { + return entry.getProcessingVehicle().getName(); + } + else { + return BUNDLE.getString( + "orderSequenceTableModel.column_intendedVehicle.determinedAutomatic.text" + ); + } + case COLUMN_INDEX: + return entry.getFinishedIndex(); + case COLUMN_COMPLETED: + return entry.isComplete(); + case COLUMN_FINISHED: + return entry.isFinished(); + case COLUMN_FAILURE: + return entry.isFailureFatal(); + default: + throw new IllegalArgumentException("Invalid column index: " + columnIndex); + } + } + + @Override + public String getColumnName(int columnIndex) { + return COLUMN_NAMES[columnIndex]; + } + + @Override + public Class getColumnClass(int columnIndex) { + return COLUMN_CLASSES[columnIndex]; + } + + @Override + public void containerInitialized(Collection sequences) { + requireNonNull(sequences, "sequences"); + + SwingUtilities.invokeLater(() -> { + // Notifiations of any change listeners must happen at the same time/in the same thread the + // data behind the model is updated. Otherwise, there is a risk that listeners work with/ + // refer to outdated data, which can lead to runtime exceptions. + entries.clear(); + entries.addAll(sequences); + fireTableDataChanged(); + }); + } + + @Override + public void orderSequenceAdded(OrderSequence sequence) { + requireNonNull(sequence, "sequence"); + + SwingUtilities.invokeLater(() -> { + entries.add(sequence); + fireTableRowsInserted(entries.size() - 1, entries.size() - 1); + }); + } + + @Override + public void orderSequenceUpdated(OrderSequence sequence) { + requireNonNull(sequence, "sequence"); + + SwingUtilities.invokeLater(() -> { + int sequenceIndex = entries.indexOf(sequence); + if (sequenceIndex == -1) { + LOG.warn("Unknown order sequence: {}. Ignoring order sequence update.", sequence.getName()); + return; + } + entries.set(sequenceIndex, sequence); + fireTableRowsUpdated(sequenceIndex, sequenceIndex); + }); + } + + @Override + public void orderSequenceRemoved(OrderSequence sequence) { + requireNonNull(sequence, "sequence"); + SwingUtilities.invokeLater(() -> { + int sequenceIndex = entries.indexOf(sequence); + if (sequenceIndex == -1) { + LOG.warn( + "Unknown order sequence: {}. Ignoring order sequence removal.", + sequence.getName() + ); + return; + } + entries.remove(sequenceIndex); + fireTableRowsDeleted(sequenceIndex, sequenceIndex); + }); + } + + /** + * Returns the order sequence at the specified index. + * + * @param index the index to return. + * @return the order sequence at that index. + */ + public OrderSequence getEntryAt(int index) { + if (index < 0 || index >= entries.size()) { + return null; + } + + return entries.get(index); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/sequences/OrderSequenceView.form b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/sequences/OrderSequenceView.form new file mode 100644 index 0000000..64db437 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/sequences/OrderSequenceView.form @@ -0,0 +1,410 @@ + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/sequences/OrderSequenceView.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/sequences/OrderSequenceView.java new file mode 100644 index 0000000..0e60c01 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/sequences/OrderSequenceView.java @@ -0,0 +1,576 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport.sequences; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.awt.Component; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import javax.swing.JComponent; +import javax.swing.JOptionPane; +import javax.swing.JTable; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.DefaultTableModel; +import javax.swing.table.TableModel; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.components.plantoverview.ObjectHistoryEntryFormatter; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.guing.common.components.dialogs.DialogContent; +import org.opentcs.operationsdesk.transport.CompositeObjectHistoryEntryFormatter; +import org.opentcs.operationsdesk.transport.UneditableTableModel; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Displays an order sequence. + */ +public class OrderSequenceView + extends + DialogContent { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(OrderSequenceView.class); + /** + * A formatter for timestamps. + */ + private static final DateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + /** + * The order sequence to be shown. + */ + private final OrderSequence fOrderSequence; + /** + * The portal provider to be used. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * A formatter for history entries. + */ + private final ObjectHistoryEntryFormatter historyEntryFormatter; + + /** + * Creates new instance. + * + * @param sequence The order sequence. + * @param historyEntryFormatter A formatter for history entries. + * @param portalProvider Provides access to a portal. + */ + @Inject + @SuppressWarnings("this-escape") + public OrderSequenceView( + @Assisted + OrderSequence sequence, + @Nonnull + CompositeObjectHistoryEntryFormatter historyEntryFormatter, + SharedKernelServicePortalProvider portalProvider + ) { + this.fOrderSequence = requireNonNull(sequence, "sequence"); + this.historyEntryFormatter = requireNonNull(historyEntryFormatter, "historyEntryFormatter"); + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + initComponents(); + initFields(); + } + + /** + * Returns the order sequence. + * + * @return The order sequence. + */ + public OrderSequence getOrderSequence() { + return fOrderSequence; + } + + @Override + public void update() { + // Do nada. + } + + @Override + public final void initFields() { + setDialogTitle( + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.OSDETAIL_PATH) + .getString("orderSequenceView.title") + ); + // Name + String name = getOrderSequence().getName(); + textFieldName.setText(name); + + boolean complete = getOrderSequence().isComplete(); + checkBoxComplete.setSelected(complete); + checkBoxComplete.setEnabled(!complete); + + boolean finished = getOrderSequence().isFinished(); + checkBoxFinished.setSelected(finished); + + boolean failureFatal = getOrderSequence().isFailureFatal(); + checkBoxFailureFatal.setSelected(failureFatal); + + int finishedIndex = getOrderSequence().getFinishedIndex(); + textFieldFinishedIndex.setText("" + finishedIndex); + + TCSObjectReference intendedVehicle = getOrderSequence().getIntendedVehicle(); + + if (intendedVehicle != null) { + textFieldIntendedVehicle.setText(intendedVehicle.getName()); + } + + TCSObjectReference processingVehicle = getOrderSequence().getProcessingVehicle(); + + if (processingVehicle != null) { + textFieldProcessingVehicle.setText(processingVehicle.getName()); + } + + textType.setText(getOrderSequence().getType()); + + DefaultTableModel tableModel = new UneditableTableModel(); + tableModel.setColumnIdentifiers(new String[]{"Name"}); + + for (TCSObjectReference to : getOrderSequence().getOrders()) { + String[] row = new String[1]; + row[0] = to.getName(); + tableModel.addRow(row); + } + + transportOrdersTable.setModel(tableModel); + + propertiesTable.setModel(createPropertiesTableModel()); + + historyTable.setModel(createHistoryTableModel()); + historyTable.getColumnModel().getColumn(0).setPreferredWidth(100); + historyTable.getColumnModel().getColumn(1).setPreferredWidth(300); + historyTable.getColumnModel().getColumn(1).setCellRenderer(new ToolTipCellRenderer()); + } + + private TableModel createPropertiesTableModel() { + DefaultTableModel tableModel = new UneditableTableModel(); + + tableModel.setColumnIdentifiers( + new String[]{ + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.OSDETAIL_PATH) + .getString( + "orderSequenceView.table_properties.column_propertiesKey.headerText" + ), + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.OSDETAIL_PATH) + .getString( + "orderSequenceView.table_properties.column_propertiesValue.headerText" + ) + } + ); + fOrderSequence.getProperties().entrySet().stream() + .sorted((e1, e2) -> e1.getKey().compareTo(e2.getKey())) + .forEach(entry -> { + tableModel.addRow(new String[]{entry.getKey(), entry.getValue()}); + }); + + return tableModel; + } + + private TableModel createHistoryTableModel() { + DefaultTableModel tableModel = new UneditableTableModel(); + + tableModel.setColumnIdentifiers( + new String[]{ + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.OSDETAIL_PATH) + .getString("orderSequenceView.table_history.column_timestamp.headerText"), + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.OSDETAIL_PATH) + .getString("orderSequenceView.table_history.column_event.headerText") + } + ); + + for (ObjectHistory.Entry entry : fOrderSequence.getHistory().getEntries()) { + tableModel.addRow( + new String[]{ + TIMESTAMP_FORMAT.format(Date.from(entry.getTimestamp())), + historyEntryFormatter.apply(entry).get() + } + ); + } + + return tableModel; + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + generalPanel = new javax.swing.JPanel(); + jPanel1 = new javax.swing.JPanel(); + labelName = new javax.swing.JLabel(); + textFieldName = new javax.swing.JTextField(); + labelFinishedIndex = new javax.swing.JLabel(); + textFieldFinishedIndex = new javax.swing.JTextField(); + labelIntendedVehicle = new javax.swing.JLabel(); + textFieldIntendedVehicle = new javax.swing.JTextField(); + labelProcessingVehicle = new javax.swing.JLabel(); + textFieldProcessingVehicle = new javax.swing.JTextField(); + labelType = new javax.swing.JLabel(); + textType = new javax.swing.JTextField(); + jPanel2 = new javax.swing.JPanel(); + checkBoxComplete = new javax.swing.JCheckBox(); + checkBoxFinished = new javax.swing.JCheckBox(); + checkBoxFailureFatal = new javax.swing.JCheckBox(); + transportOrdersPanel = new javax.swing.JPanel(); + transportOrdersScrollPane = new javax.swing.JScrollPane(); + transportOrdersTable = new javax.swing.JTable(); + propertiesPanel = new javax.swing.JPanel(); + propertiesScrollPane = new javax.swing.JScrollPane(); + propertiesTable = new javax.swing.JTable(); + historyPanel = new javax.swing.JPanel(); + historyScrollPane = new javax.swing.JScrollPane(); + historyTable = new javax.swing.JTable(); + + setLayout(new java.awt.GridBagLayout()); + + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/operating/dialogs/orderSequenceDetail"); // NOI18N + generalPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("orderSequenceView.panel_general.border.title"))); // NOI18N + generalPanel.setLayout(new java.awt.GridBagLayout()); + + jPanel1.setLayout(new java.awt.GridBagLayout()); + + labelName.setFont(labelName.getFont()); + labelName.setText(bundle.getString("orderSequenceView.panel_general.label_name.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + jPanel1.add(labelName, gridBagConstraints); + + textFieldName.setEditable(false); + textFieldName.setColumns(10); + textFieldName.setFont(textFieldName.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 0.5; + jPanel1.add(textFieldName, gridBagConstraints); + + labelFinishedIndex.setFont(labelFinishedIndex.getFont()); + labelFinishedIndex.setText(bundle.getString("orderSequenceView.panel_general.label_finishedIndex.text")); // NOI18N + labelFinishedIndex.setToolTipText(bundle.getString("orderSequenceView.panel_general.label_finishedIndex.tooltipText")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + jPanel1.add(labelFinishedIndex, gridBagConstraints); + + textFieldFinishedIndex.setEditable(false); + textFieldFinishedIndex.setColumns(10); + textFieldFinishedIndex.setFont(textFieldFinishedIndex.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 0.5; + jPanel1.add(textFieldFinishedIndex, gridBagConstraints); + + labelIntendedVehicle.setFont(labelIntendedVehicle.getFont()); + labelIntendedVehicle.setText(bundle.getString("orderSequenceView.panel_general.label_intendedVehicle.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + jPanel1.add(labelIntendedVehicle, gridBagConstraints); + + textFieldIntendedVehicle.setEditable(false); + textFieldIntendedVehicle.setColumns(10); + textFieldIntendedVehicle.setFont(textFieldIntendedVehicle.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 0.5; + jPanel1.add(textFieldIntendedVehicle, gridBagConstraints); + + labelProcessingVehicle.setFont(labelProcessingVehicle.getFont()); + labelProcessingVehicle.setText(bundle.getString("orderSequenceView.panel_general.label_processingVehicle.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + jPanel1.add(labelProcessingVehicle, gridBagConstraints); + + textFieldProcessingVehicle.setEditable(false); + textFieldProcessingVehicle.setColumns(10); + textFieldProcessingVehicle.setFont(textFieldProcessingVehicle.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 0.5; + jPanel1.add(textFieldProcessingVehicle, gridBagConstraints); + + labelType.setFont(labelType.getFont()); + labelType.setText(bundle.getString("orderSequenceView.panel_general.label_type.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + jPanel1.add(labelType, gridBagConstraints); + + textType.setEditable(false); + textType.setColumns(10); + textType.setFont(textType.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 0.5; + jPanel1.add(textType, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START; + gridBagConstraints.weightx = 0.5; + generalPanel.add(jPanel1, gridBagConstraints); + + jPanel2.setLayout(new java.awt.GridBagLayout()); + + checkBoxComplete.setText(bundle.getString("orderSequenceView.panel_general.checkBox_complete.text")); // NOI18N + checkBoxComplete.setToolTipText(bundle.getString("orderSequenceView.panel_general.checkBox_complete.tooltipText")); // NOI18N + checkBoxComplete.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + checkBoxCompleteActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + jPanel2.add(checkBoxComplete, gridBagConstraints); + + checkBoxFinished.setText(bundle.getString("orderSequenceView.panel_general.checkBox_finished.text")); // NOI18N + checkBoxFinished.setToolTipText(bundle.getString("orderSequenceView.panel_general.checkBox_finished.tooltipText")); // NOI18N + checkBoxFinished.setEnabled(false); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + jPanel2.add(checkBoxFinished, gridBagConstraints); + + checkBoxFailureFatal.setText(bundle.getString("orderSequenceView.panel_general.checkBox_failureFatal.text")); // NOI18N + checkBoxFailureFatal.setToolTipText(bundle.getString("orderSequenceView.panel_general.checkBox_failureFatal.tooltipText")); // NOI18N + checkBoxFailureFatal.setEnabled(false); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + jPanel2.add(checkBoxFailureFatal, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.anchor = java.awt.GridBagConstraints.FIRST_LINE_START; + generalPanel.add(jPanel2, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 0.1; + add(generalPanel, gridBagConstraints); + + transportOrdersPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("orderSequenceView.panel_transportOrders.border.title"))); // NOI18N + transportOrdersPanel.setLayout(new java.awt.GridBagLayout()); + + transportOrdersScrollPane.setPreferredSize(new java.awt.Dimension(100, 100)); + transportOrdersScrollPane.setViewportView(transportOrdersTable); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + transportOrdersPanel.add(transportOrdersScrollPane, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.weighty = 0.5; + add(transportOrdersPanel, gridBagConstraints); + + propertiesPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("orderSequenceView.panel_properties.border.text"))); // NOI18N + propertiesPanel.setLayout(new java.awt.BorderLayout()); + + propertiesScrollPane.setPreferredSize(new java.awt.Dimension(150, 100)); + + propertiesTable.setModel(new javax.swing.table.DefaultTableModel( + new Object [][] { + {}, + {}, + {}, + {} + }, + new String [] { + + } + )); + propertiesScrollPane.setViewportView(propertiesTable); + + propertiesPanel.add(propertiesScrollPane, java.awt.BorderLayout.CENTER); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.weighty = 0.5; + add(propertiesPanel, gridBagConstraints); + + historyPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("orderSequenceView.panel_history.border.text"))); // NOI18N + historyPanel.setLayout(new java.awt.BorderLayout()); + + historyScrollPane.setPreferredSize(new java.awt.Dimension(150, 100)); + + historyTable.setModel(new javax.swing.table.DefaultTableModel( + new Object [][] { + {}, + {}, + {}, + {} + }, + new String [] { + + } + )); + historyScrollPane.setViewportView(historyTable); + + historyPanel.add(historyScrollPane, java.awt.BorderLayout.CENTER); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.weighty = 0.5; + add(historyPanel, gridBagConstraints); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void checkBoxCompleteActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_checkBoxCompleteActionPerformed + ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.OSDETAIL_PATH); + int n = JOptionPane.showConfirmDialog( + this, + bundle.getString("orderSequenceView.optionPane_markSequenceCompleteConfirmation.message"), + bundle.getString("orderSequenceView.optionPane_markSequenceCompleteConfirmation.title"), + JOptionPane.YES_NO_OPTION + ); + + if (n == JOptionPane.OK_OPTION) { + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + sharedPortal.getPortal().getTransportOrderService() + .markOrderSequenceComplete(fOrderSequence.getReference()); + checkBoxComplete.setEnabled(false); + } + catch (KernelRuntimeException exc) { + LOG.warn("Exception setting order sequence complete", exc); + } + } + else { + checkBoxComplete.setSelected(false); + } + }//GEN-LAST:event_checkBoxCompleteActionPerformed + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JCheckBox checkBoxComplete; + private javax.swing.JCheckBox checkBoxFailureFatal; + private javax.swing.JCheckBox checkBoxFinished; + private javax.swing.JPanel generalPanel; + private javax.swing.JPanel historyPanel; + private javax.swing.JScrollPane historyScrollPane; + private javax.swing.JTable historyTable; + private javax.swing.JPanel jPanel1; + private javax.swing.JPanel jPanel2; + private javax.swing.JLabel labelFinishedIndex; + private javax.swing.JLabel labelIntendedVehicle; + private javax.swing.JLabel labelName; + private javax.swing.JLabel labelProcessingVehicle; + private javax.swing.JLabel labelType; + private javax.swing.JPanel propertiesPanel; + private javax.swing.JScrollPane propertiesScrollPane; + private javax.swing.JTable propertiesTable; + private javax.swing.JTextField textFieldFinishedIndex; + private javax.swing.JTextField textFieldIntendedVehicle; + private javax.swing.JTextField textFieldName; + private javax.swing.JTextField textFieldProcessingVehicle; + private javax.swing.JTextField textType; + private javax.swing.JPanel transportOrdersPanel; + private javax.swing.JScrollPane transportOrdersScrollPane; + private javax.swing.JTable transportOrdersTable; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON + + /** + * A cell renderer that adds a tool tip with the cell's value. + */ + private static class ToolTipCellRenderer + extends + DefaultTableCellRenderer { + + /** + * Creates a new instance. + */ + ToolTipCellRenderer() { + } + + @Override + public Component getTableCellRendererComponent( + JTable table, + Object value, + boolean isSelected, + boolean hasFocus, + int row, + int column + ) { + Component component = super.getTableCellRendererComponent( + table, + value, + isSelected, + hasFocus, + row, + column + ); + + ((JComponent) component).setToolTipText(value.toString()); + + return component; + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/sequences/OrderSequencesContainer.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/sequences/OrderSequencesContainer.java new file mode 100644 index 0000000..8a5f446 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/sequences/OrderSequencesContainer.java @@ -0,0 +1,191 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport.sequences; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.common.KernelClientApplication; +import org.opentcs.components.Lifecycle; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.guing.common.event.OperationModeChangeEvent; +import org.opentcs.guing.common.event.SystemModelTransitionEvent; +import org.opentcs.operationsdesk.event.KernelStateChangeEvent; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Maintains a set of all order sequences existing on the kernel. + */ +public class OrderSequencesContainer + implements + Lifecycle, + EventHandler { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(OrderSequencesContainer.class); + /** + * Event bus. + */ + private final EventBus eventBus; + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * The kernel client application. + */ + private final KernelClientApplication kernelClientApplication; + /** + * The order sequences. + */ + private final Map orderSequences = new HashMap<>(); + /** + * This container's listeners. + */ + private final Set listeners = new HashSet<>(); + /** + * Whether this component is initialized. + */ + private boolean initialized; + + @Inject + public OrderSequencesContainer( + @ApplicationEventBus + EventBus eventBus, + SharedKernelServicePortalProvider portalProvider, + KernelClientApplication kernelClientApplication + ) { + this.eventBus = requireNonNull(eventBus, "eventBus"); + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.kernelClientApplication + = requireNonNull(kernelClientApplication, "kernelClientApplication"); + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + eventBus.subscribe(this); + initialized = true; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + eventBus.unsubscribe(this); + initialized = false; + } + + @Override + public void onEvent(Object event) { + if (event instanceof TCSObjectEvent) { + handleObjectEvent((TCSObjectEvent) event); + } + else if (event instanceof OperationModeChangeEvent) { + initSequences(); + } + else if (event instanceof SystemModelTransitionEvent) { + initSequences(); + } + else if (event instanceof KernelStateChangeEvent) { + initSequences(); + } + } + + private void initSequences() { + setOrderSequences(fetchSequencesIfOnline()); + listeners.forEach(listener -> listener.containerInitialized(orderSequences.values())); + } + + private Set fetchSequencesIfOnline() { + if (kernelClientApplication.isOnline()) { + try (SharedKernelServicePortal sharedPortal = portalProvider.register()) { + return sharedPortal.getPortal().getTransportOrderService() + .fetchObjects(OrderSequence.class); + } + catch (KernelRuntimeException exc) { + LOG.warn("Exception fetching order sequences", exc); + } + } + + return new HashSet<>(); + } + + public void addListener(OrderSequenceContainerListener listener) { + listeners.add(listener); + } + + public void removeListener(OrderSequenceContainerListener listener) { + listeners.remove(listener); + } + + public Collection getOrderSequences() { + return orderSequences.values(); + } + + private void handleObjectEvent(TCSObjectEvent evt) { + if (evt.getCurrentOrPreviousObjectState() instanceof OrderSequence) { + switch (evt.getType()) { + case OBJECT_CREATED: + orderSequenceAdded((OrderSequence) evt.getCurrentOrPreviousObjectState()); + break; + case OBJECT_MODIFIED: + orderSequencesChanged((OrderSequence) evt.getCurrentOrPreviousObjectState()); + break; + case OBJECT_REMOVED: + orderSequenceRemoved((OrderSequence) evt.getCurrentOrPreviousObjectState()); + break; + default: + LOG.warn("Unhandled event type: {}", evt.getType()); + } + } + } + + private void orderSequenceAdded(OrderSequence seq) { + orderSequences.put(seq.getName(), seq); + listeners.forEach(listener -> listener.orderSequenceAdded(seq)); + } + + private void orderSequencesChanged(OrderSequence seq) { + orderSequences.put(seq.getName(), seq); + listeners.forEach(listener -> listener.orderSequenceUpdated(seq)); + } + + private void orderSequenceRemoved(OrderSequence seq) { + orderSequences.remove(seq.getName()); + listeners.forEach(listener -> listener.orderSequenceRemoved(seq)); + } + + private void setOrderSequences(Set sequences) { + orderSequences.clear(); + for (OrderSequence seq : sequences) { + orderSequences.put(seq.getName(), seq); + } + } + +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/sequences/OrderSequencesContainerPanel.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/sequences/OrderSequencesContainerPanel.java new file mode 100644 index 0000000..3c01403 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/transport/sequences/OrderSequencesContainerPanel.java @@ -0,0 +1,222 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.transport.sequences; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.JToolBar; +import javax.swing.RowFilter; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.guing.common.components.dialogs.DialogContent; +import org.opentcs.guing.common.components.dialogs.StandardContentDialog; +import org.opentcs.guing.common.util.IconToolkit; +import org.opentcs.operationsdesk.transport.FilterButton; +import org.opentcs.operationsdesk.transport.FilteredRowSorter; +import org.opentcs.operationsdesk.transport.OrdersTable; +import org.opentcs.operationsdesk.transport.orders.TransportViewFactory; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Shows a table of the kernel's order sequences. + */ +public class OrderSequencesContainerPanel + extends + JPanel { + + /** + * The path to the icons. + */ + private static final String ICON_PATH = "/org/opentcs/guing/res/symbols/panel/"; + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(OrderSequencesContainerPanel.class); + /** + * A factory for order sequence views. + */ + private final TransportViewFactory transportViewFactory; + /** + * The parent component for dialogs shown by this instance. + */ + private final Component dialogParent; + /** + * The table showing the order sequences. + */ + private JTable fTable; + /** + * The table's model. + */ + private OrderSequenceTableModel tableModel; + /** + * The sorter for the table. + */ + private FilteredRowSorter sorter; + /** + * Holds the order sequences. + */ + private final OrderSequencesContainer orderSequencesContainer; + + /** + * Creates a new instance. + * + * @param transportViewFactory A factory for order sequence views. + * @param dialogParent The parent component for dialogs shown by this instance. + * @param orderSequencesContainer Maintains a set of order sequences on the kernel side. + */ + @Inject + @SuppressWarnings("this-escape") + public OrderSequencesContainerPanel( + TransportViewFactory transportViewFactory, + @ApplicationFrame + Component dialogParent, + OrderSequencesContainer orderSequencesContainer + ) { + this.transportViewFactory = requireNonNull(transportViewFactory, "transportViewFactory"); + this.dialogParent = requireNonNull(dialogParent, "dialogParent"); + this.orderSequencesContainer = requireNonNull( + orderSequencesContainer, + "orderSequencesContainer" + ); + + initComponents(); + } + + /** + * Initializes this panel's contents. + */ + public void initView() { + tableModel.containerInitialized(orderSequencesContainer.getOrderSequences()); + } + + private void initComponents() { + setLayout(new BorderLayout()); + + tableModel = new OrderSequenceTableModel(); + orderSequencesContainer.addListener(tableModel); + fTable = new OrdersTable(tableModel); + + sorter = new FilteredRowSorter<>(tableModel); + // Prevent manual sorting. + for (int i = 0; i < fTable.getColumnCount(); i++) { + sorter.setSortable(i, false); + } + sorter.setSortsOnUpdates(true); + fTable.setRowSorter(sorter); + + JScrollPane scrollPane = new JScrollPane(fTable); + add(scrollPane, BorderLayout.CENTER); + + JToolBar toolBar = createToolBar(createFilterButtons()); + add(toolBar, BorderLayout.NORTH); + + fTable.addMouseListener(new MouseAdapter() { + + @Override + public void mouseClicked(MouseEvent evt) { + if (evt.getButton() == MouseEvent.BUTTON1) { + if (evt.getClickCount() == 2) { + showOrderSequence(); + } + } + + if (evt.getButton() == MouseEvent.BUTTON3) { + showPopupMenu(evt.getX(), evt.getY()); + } + } + }); + } + + private void showOrderSequence() { + getSelectedOrderSequence().ifPresent(os -> { + DialogContent content = transportViewFactory.createOrderSequenceView(os); + StandardContentDialog dialog + = new StandardContentDialog(dialogParent, content, true, StandardContentDialog.CLOSE); + dialog.setVisible(true); + }); + } + + private void showPopupMenu(int x, int y) { + int row = fTable.rowAtPoint(new Point(x, y)); + fTable.setRowSelectionInterval(row, row); + + JPopupMenu menu = new JPopupMenu(); + JMenuItem item = menu.add( + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TO_SEQUENCE_PATH) + .getString( + "orderSequencesContainerPanel.table_sequences.popupMenuItem_showDetails.text" + ) + ); + item.addActionListener((ActionEvent evt) -> showOrderSequence()); + + menu.show(fTable, x, y); + } + + private Optional getSelectedOrderSequence() { + int row = fTable.getSelectedRow(); + + if (row == -1) { + return Optional.empty(); + } + + return Optional.of(tableModel.getEntryAt(fTable.convertRowIndexToModel(row))); + } + + private List createFilterButtons() { + List buttons = new ArrayList<>(); + + FilterButton b1 = new FilterButton( + IconToolkit.instance().getImageIconByFullPath(ICON_PATH + "filterFinished.16x16.gif"), + createFilter(), + sorter + ); + buttons.add(b1); + b1.setToolTipText( + ResourceBundleUtil.getBundle(I18nPlantOverviewOperating.TO_SEQUENCE_PATH) + .getString( + "orderSequencesContainerPanel.button_filterFinishedOrderSequences.tooltipText" + ) + ); + + return buttons; + } + + private RowFilter createFilter() { + return new RowFilter() { + @Override + public boolean include(Entry entry) { + OrderSequence os + = ((OrderSequenceTableModel) entry.getModel()).getEntryAt((int) entry.getIdentifier()); + return os.isComplete(); + } + }; + } + + private JToolBar createToolBar(List filterButtons) { + JToolBar toolBar = new JToolBar(); + + for (FilterButton button : filterButtons) { + toolBar.add(button); + } + + return toolBar; + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/util/Cursors.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/util/Cursors.java new file mode 100644 index 0000000..607658b --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/util/Cursors.java @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.util; + +import java.awt.Cursor; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.image.BufferedImage; +import javax.swing.ImageIcon; + +/** + * Provides cursors for various situations. + */ +public final class Cursors { + + /** + * A cursor suitable for dragging a vehicle to a destination point. + */ + private static final Cursor DRAG_VEHICLE_CURSOR = createDragVehicleCursor(); + + /** + * Prevents instantiation. + */ + private Cursors() { + } + + /** + * Returns a cursor suitable for dragging a vehicle to a destination point. + * + * @return A cursor suitable for dragging a vehicle to a destination point. + */ + public static Cursor getDragVehicleCursor() { + return DRAG_VEHICLE_CURSOR; + } + + private static Cursor createDragVehicleCursor() { + // Load an image for the vehicle drag cursor. + BufferedImage bi = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB); + bi.createGraphics().drawImage( + new ImageIcon( + Cursors.class.getClassLoader().getResource( + "org/opentcs/guing/res/symbols/toolbar/create-order.22.png" + ) + ).getImage(), + 0, + 0, + null + ); + return Toolkit.getDefaultToolkit().createCustomCursor(bi, new Point(0, 0), "toCursor"); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/util/I18nPlantOverviewOperating.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/util/I18nPlantOverviewOperating.java new file mode 100644 index 0000000..ab3478f --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/util/I18nPlantOverviewOperating.java @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.util; + +/** + * Defines application-specific constants regarding internationalization. + */ +public interface I18nPlantOverviewOperating { + + /** + * Path to the resources related to the create transport order dialog. + */ + String CREATETO_PATH = "i18n.org.opentcs.plantoverview.operating.dialogs.createTransportOrder"; + /** + * Path to the resources related to dockables. + */ + String DOCKABLE_PATH = "i18n.org.opentcs.plantoverview.operating.panels.dockable"; + /** + * Path to the resources related to the find vehicle dialog. + */ + String FINDVEHICLE_PATH = "i18n.org.opentcs.plantoverview.operating.dialogs.findVehicle"; + /** + * Path to the resources related to layers. + */ + String LAYERS_PATH = "i18n.org.opentcs.plantoverview.operating.panels.layers"; + /** + * Path to the resources related the menu bar. + */ + String MENU_PATH = "i18n.org.opentcs.plantoverview.operating.mainMenu"; + /** + * Path to miscellaneous resources. + */ + String MISC_PATH = "i18n.org.opentcs.plantoverview.operating.miscellaneous"; + /** + * Path to the resources related to the peripheral job panel. + */ + String PERIPHERALJOB_PATH = "i18n.org.opentcs.plantoverview.operating.panels.peripheralJobs"; + /** + * Path to the resources related to the system. + */ + String SYSTEM_PATH = "i18n.org.opentcs.plantoverview.operating.system"; + /** + * Path to the resources related to the user notifications details dialog. + */ + String UNDETAIL_PATH = "i18n.org.opentcs.plantoverview.operating.dialogs.userNotificationDetail"; + /** + * Path to the resources related to the transport order details dialog. + */ + String TODETAIL_PATH = "i18n.org.opentcs.plantoverview.operating.dialogs.transportOrderDetail"; + /** + * Path to the resources related to the peripheral job details dialog. + */ + String PJDETAIL_PATH = "i18n.org.opentcs.plantoverview.operating.dialogs.peripheralJobDetail"; + /** + * Path to the resources related to the order sequence details dialog. + */ + String OSDETAIL_PATH = "i18n.org.opentcs.plantoverview.operating.dialogs.orderSequenceDetail"; + /** + * Path to the resources related to the transportorder sequence panel. + */ + String TO_SEQUENCE_PATH = "i18n.org.opentcs.plantoverview.operating.panels.orderSequences"; + /** + * Path to the resources related to toolbars. + */ + String TOOLBAR_PATH = "i18n.org.opentcs.plantoverview.operating.toolbar"; + /** + * Path to the resources related to the userNotification panel. + */ + String USERNOTIFICATION_PATH + = "i18n.org.opentcs.plantoverview.operating.panels.userNotifications"; + /** + * Path to the resources related to the transportorder panel. + */ + String TRANSPORTORDER_PATH = "i18n.org.opentcs.plantoverview.operating.panels.transportOrders"; + /** + * Path to the resources related to the vehicle popup. + */ + String VEHICLEPOPUP_PATH = "i18n.org.opentcs.plantoverview.operating.dialogs.vehiclePopup"; + /** + * Path to the resources related to the vehicle view. + */ + String VEHICLEVIEW_PATH = "i18n.org.opentcs.plantoverview.operating.panels.vehicleView"; + /** + * Path to the resources related to the create peripheral job panel. + */ + String CREATE_PERIPHERAL_JOB_PATH + = "i18n.org.opentcs.plantoverview.operating.dialogs.createPeripheralJob"; +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/util/OperationsDeskConfiguration.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/util/OperationsDeskConfiguration.java new file mode 100644 index 0000000..63ba277 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/util/OperationsDeskConfiguration.java @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.util; + +import org.opentcs.components.plantoverview.LocationTheme; +import org.opentcs.components.plantoverview.VehicleTheme; +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; +import org.opentcs.guing.common.exchange.ApplicationPortalProviderConfiguration; + +/** + * Provides methods to configure the Operations Desk application. + */ +@ConfigurationPrefix(OperationsDeskConfiguration.PREFIX) +public interface OperationsDeskConfiguration + extends + ApplicationPortalProviderConfiguration { + + /** + * This configuration's prefix. + */ + String PREFIX = "operationsdesk"; + + @ConfigurationEntry( + type = "String", + description = {"The plant overview application's locale, as a BCP 47 language tag.", + "Examples: 'en', 'de', 'zh'"}, + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_init_0" + ) + String locale(); + + @ConfigurationEntry( + type = "Boolean", + description = "Whether the GUI window should be maximized on startup.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "2_size_0" + ) + boolean frameMaximized(); + + @ConfigurationEntry( + type = "Integer", + description = "The GUI window's configured width in pixels.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "2_size_1" + ) + int frameBoundsWidth(); + + @ConfigurationEntry( + type = "Integer", + description = "The GUI window's configured height in pixels.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "2_size_2" + ) + int frameBoundsHeight(); + + @ConfigurationEntry( + type = "Integer", + description = "The GUI window's configured x-coordinate on screen in pixels.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "2_size_3" + ) + int frameBoundsX(); + + @ConfigurationEntry( + type = "Integer", + description = "The GUI window's configured y-coordinate on screen in pixels.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "2_size_4" + ) + int frameBoundsY(); + + @ConfigurationEntry( + type = "Class name", + description = { + "The name of the class to be used for the location theme.", + "Must be a class extending org.opentcs.components.plantoverview.LocationTheme" + }, + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "3_themes_0" + ) + Class locationThemeClass(); + + @ConfigurationEntry( + type = "Class name", + description = {"The name of the class to be used for the vehicle theme.", + "Must be a class extending org.opentcs.components.plantoverview.VehicleTheme"}, + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "3_themes_0" + ) + Class vehicleThemeClass(); + + @ConfigurationEntry( + type = "Boolean", + description = "Whether reported precise positions should be ignored displaying vehicles.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "4_behaviour_0" + ) + boolean ignoreVehiclePrecisePosition(); + + @ConfigurationEntry( + type = "Boolean", + description = "Whether reported orientation angles should be ignored displaying vehicles.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "4_behaviour_1" + ) + boolean ignoreVehicleOrientationAngle(); + + @ConfigurationEntry( + type = "Integer", + description = "The maximum number of most recent user notifications to be displayed.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "9_misc_1" + ) + int userNotificationDisplayCount(); + + @ConfigurationEntry( + type = "Boolean", + description = "Whether the forced withdrawal context menu entry should be enabled.", + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "9_misc_2" + ) + boolean allowForcedWithdrawal(); +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/util/VehicleCourseObjectFactory.java b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/util/VehicleCourseObjectFactory.java new file mode 100644 index 0000000..3f229cd --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/operationsdesk/util/VehicleCourseObjectFactory.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.util; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.util.CourseObjectFactory; +import org.opentcs.operationsdesk.components.drawing.figures.NamedVehicleFigure; +import org.opentcs.operationsdesk.components.drawing.figures.VehicleFigure; +import org.opentcs.operationsdesk.components.drawing.figures.VehicleFigureFactory; + +/** + * A factory for Figures and ModelComponents. + */ +public class VehicleCourseObjectFactory + extends + CourseObjectFactory { + + /** + * A factory for figures. + */ + private final VehicleFigureFactory figureFactory; + + /** + * Creates a new instance. + * + * @param figureFactory A factory for figures. + */ + @Inject + public VehicleCourseObjectFactory(VehicleFigureFactory figureFactory) { + super(figureFactory); + this.figureFactory = requireNonNull(figureFactory, "figureFactory"); + } + + public VehicleFigure createVehicleFigure(VehicleModel model) { + return figureFactory.createVehicleFigure(model); + } + + public NamedVehicleFigure createNamedVehicleFigure(VehicleModel model) { + return figureFactory.createNamedVehicleFigure(model); + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/thirdparty/operationsdesk/components/drawing/OpenTCSDrawingViewOperating.java b/opentcs-operationsdesk/src/main/java/org/opentcs/thirdparty/operationsdesk/components/drawing/OpenTCSDrawingViewOperating.java new file mode 100644 index 0000000..273f135 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/thirdparty/operationsdesk/components/drawing/OpenTCSDrawingViewOperating.java @@ -0,0 +1,316 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.operationsdesk.components.drawing; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.MultipleGradientPaint; +import java.awt.Point; +import java.awt.RadialGradientPaint; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.beans.PropertyChangeEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.jhotdraw.draw.DefaultDrawingView; +import org.jhotdraw.draw.Figure; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.application.ApplicationState; +import org.opentcs.guing.common.components.drawing.figures.OriginFigure; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.operationsdesk.components.drawing.figures.VehicleFigure; +import org.opentcs.thirdparty.guing.common.jhotdraw.components.drawing.AbstractOpenTCSDrawingView; + +/** + * A DrawingView implementation for the openTCS plant overview. + */ +public class OpenTCSDrawingViewOperating + extends + AbstractOpenTCSDrawingView { + + /** + * Contains the vehicle on the drawing, for which transport order shall be drawn. + */ + private final List fVehicles = new ArrayList<>(); + /** + * The vehicle the view should highlight and follow. + */ + private VehicleModel fFocusVehicle; + + /** + * Creates new instance. + * + * @param appState Stores the application's current state. + * @param modelManager Provides the current system model. + */ + @Inject + public OpenTCSDrawingViewOperating(ApplicationState appState, ModelManager modelManager) { + super(appState, modelManager); + } + + @Override + public void removeAll() { + fVehicles.clear(); + super.removeAll(); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + super.propertyChange(evt); + + if (evt.getPropertyName().equals(VehicleFigure.POSITION_CHANGED)) { + scrollTo((VehicleFigure) getModelManager().getModel().getFigure(fFocusVehicle)); + } + } + + @Override + public void cutSelectedItems() { + } + + @Override + public void copySelectedItems() { + } + + @Override + public void pasteBufferedItems() { + } + + @Override + public void delete() { + } + + @Override + public void duplicate() { + } + + @Override + public void displayDriveOrders(VehicleModel vehicle, boolean visible) { + requireNonNull(vehicle, "vehicle"); + + if (visible) { + if (!fVehicles.contains(vehicle)) { + fVehicles.add(vehicle); + } + } + else { + fVehicles.remove(vehicle); + } + } + + @Override + public void followVehicle( + @Nonnull + final VehicleModel model + ) { + requireNonNull(model, "model"); + + stopFollowVehicle(); + fFocusVehicle = model; + fFocusVehicle.setViewFollows(true); + VehicleFigure vFigure = (VehicleFigure) getModelManager().getModel().getFigure(fFocusVehicle); + if (vFigure != null) { + vFigure.addPropertyChangeListener(this); + scrollTo(vFigure); + } + } + + @Override + public void stopFollowVehicle() { + if (fFocusVehicle == null) { + return; + } + + fFocusVehicle.setViewFollows(false); + VehicleFigure vFigure = (VehicleFigure) getModelManager().getModel().getFigure(fFocusVehicle); + if (vFigure != null) { + vFigure.removePropertyChangeListener(this); + } + fFocusVehicle = null; + repaint(); + } + + @Override + public void setBlocks(ModelComponent blocks) { + } + + @Override + protected void drawTool(Graphics2D g2d) { + super.drawTool(g2d); + + if (getEditor() == null + || getEditor().getTool() == null + || getEditor().getActiveView() != this) { + return; + } + + if (fFocusVehicle != null) { + // Set focus on the selected vehicle and its destination point + highlightVehicle(g2d); + } + else { + // Set focus on the selected figure + highlightFocus(g2d); + } + } + + @Override + protected DefaultDrawingView.EventHandler createEventHandler() { + return new ExtendedEventHandler(); + } + + @Override + protected Rectangle2D.Double computeBounds(Figure figure) { + Rectangle2D.Double bounds = super.computeBounds(figure); + + if (figure instanceof VehicleFigure) { + // Also show the target point + VehicleModel vehicleModel = ((VehicleFigure) figure).getModel(); + PointModel pointModel = vehicleModel.getNextPoint(); + + if (pointModel != null) { + Figure pointFigure = getModelManager().getModel().getFigure(pointModel); + bounds.add(pointFigure.getBounds()); + } + } + + return bounds; + } + + @Override + public void delete(Set components) { + } + + /** + * Sets a radial gradient for the vehicle, its current and next position. + * + * @param g2d + */ + private void highlightVehicle(Graphics2D g2d) { + if (fFocusVehicle == null) { + return; + } + + final Figure currentVehicleFigure = getModelManager().getModel().getFigure(fFocusVehicle); + if (currentVehicleFigure == null) { + return; + } + + Rectangle2D.Double bounds = currentVehicleFigure.getBounds(); + double xCenter = bounds.getCenterX(); + double yCenter = bounds.getCenterY(); + Point2D.Double pCenterView = new Point2D.Double(xCenter, yCenter); + Point pCenterDrawing = drawingToView(pCenterView); + + // radial gradient for the vehicle + Point2D center + = new Point2D.Float((float) pCenterDrawing.x, (float) pCenterDrawing.y); + float radius = 30; + float[] dist = {0.0f, 0.7f, 0.8f, 1.0f}; + Color[] colors = { + new Color(1.0f, 1.0f, 1.0f, 0.0f), // Focus: 100% transparent + new Color(1.0f, 1.0f, 1.0f, 0.0f), + new Color(1.0f, 0.0f, 0.0f, 0.7f), // Circle: red + new Color(0f, 0f, 0f, 0f) // Background + }; + RadialGradientPaint paint + = new RadialGradientPaint( + center, radius, dist, colors, + MultipleGradientPaint.CycleMethod.NO_CYCLE + ); + + Graphics2D gVehicle = (Graphics2D) g2d.create(); + gVehicle.setPaint(paint); + gVehicle.fillRect(0, 0, getWidth(), getHeight()); + gVehicle.dispose(); + + // radial gradient for the next position + PointModel pointModel = fFocusVehicle.getNextPoint(); + + if (pointModel != null) { + Figure nextPoint = getModelManager().getModel().getFigure(pointModel); + bounds = nextPoint.getBounds(); + xCenter = bounds.getCenterX(); + yCenter = bounds.getCenterY(); + pCenterView = new Point2D.Double(xCenter, yCenter); + pCenterDrawing = drawingToView(pCenterView); + center = new Point2D.Float((float) pCenterDrawing.x, (float) pCenterDrawing.y); + + radius = 20; + Color[] colorsGreen = { + new Color(1.0f, 1.0f, 1.0f, 0.0f), // Focus: 100% transparent + new Color(1.0f, 1.0f, 1.0f, 0.0f), + new Color(0.0f, 1.0f, 0.0f, 0.7f), // Circle: green + new Color(0f, 0f, 0f, 0f) // Background + }; + paint = new RadialGradientPaint( + center, radius, dist, colorsGreen, + MultipleGradientPaint.CycleMethod.NO_CYCLE + ); + + Graphics2D gNextPosition = (Graphics2D) g2d.create(); + gNextPosition.setPaint(paint); + gNextPosition.fillRect(0, 0, getWidth(), getHeight()); + gNextPosition.dispose(); + } + + // radial gradient for last position + pointModel = fFocusVehicle.getPoint(); + + if (pointModel != null && fFocusVehicle.getPrecisePosition() != null) { + Figure lastPoint = getModelManager().getModel().getFigure(pointModel); + bounds = lastPoint.getBounds(); + xCenter = bounds.getCenterX(); + yCenter = bounds.getCenterY(); + pCenterView = new Point2D.Double(xCenter, yCenter); + pCenterDrawing = drawingToView(pCenterView); + center = new Point2D.Float((float) pCenterDrawing.x, (float) pCenterDrawing.y); + + radius = 20; + Color[] colorsBlue = { + new Color(1.0f, 1.0f, 1.0f, 0.0f), // Focus: 100% transparent + new Color(1.0f, 1.0f, 1.0f, 0.0f), + new Color(0.0f, 0.0f, 1.0f, 0.7f), // Circle: blue + new Color(0f, 0f, 0f, 0f) // Background + }; + paint = new RadialGradientPaint( + center, radius, dist, colorsBlue, + MultipleGradientPaint.CycleMethod.NO_CYCLE + ); + + Graphics2D gCurrentPosition = (Graphics2D) g2d.create(); + gCurrentPosition.setPaint(paint); + gCurrentPosition.fillRect(0, 0, getWidth(), getHeight()); + gCurrentPosition.dispose(); + } + + // After drawing the RadialGradientPaint the drawing area needs to + // repainted, otherwise the GradientPaint isn't drawn correctly or + // the old one isn't removed. We make sure the repaint() call doesn't + // end in an infinite loop. + loopProofRepaintDrawingArea(); + } + + private class ExtendedEventHandler + extends + AbstractExtendedEventHandler { + + /** + * Creates a new instance. + */ + ExtendedEventHandler() { + } + + @Override + protected boolean shouldShowFigure(Figure figure) { + return !(figure instanceof VehicleFigure) && !(figure instanceof OriginFigure); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/thirdparty/operationsdesk/jhotdraw/application/OpenTCSSDIApplication.java b/opentcs-operationsdesk/src/main/java/org/opentcs/thirdparty/operationsdesk/jhotdraw/application/OpenTCSSDIApplication.java new file mode 100644 index 0000000..2a01a29 --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/thirdparty/operationsdesk/jhotdraw/application/OpenTCSSDIApplication.java @@ -0,0 +1,246 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.operationsdesk.jhotdraw.application; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.common.PortalManager.ConnectionState.CONNECTED; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import java.awt.Frame; +import java.awt.event.ActionEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.ResourceBundle; +import javax.swing.JFrame; +import org.jhotdraw.app.SDIApplication; +import org.jhotdraw.app.View; +import org.opentcs.common.PortalManager; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.guing.common.event.ModelNameChangeEvent; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.operationsdesk.application.OpenTCSView; +import org.opentcs.operationsdesk.application.menus.menubar.ApplicationMenuBar; +import org.opentcs.operationsdesk.util.I18nPlantOverviewOperating; +import org.opentcs.operationsdesk.util.OperationsDeskConfiguration; +import org.opentcs.thirdparty.operationsdesk.jhotdraw.application.action.file.CloseFileAction; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.event.EventSource; +import org.opentcs.util.gui.Icons; + +/** + * The enclosing SDI application. + */ +public class OpenTCSSDIApplication + extends + SDIApplication + implements + EventHandler { + + private static final ResourceBundle BUNDLE + = ResourceBundle.getBundle(I18nPlantOverviewOperating.SYSTEM_PATH); + /** + * The JFrame in which the OpenTCSView is shown. + */ + private final JFrame contentFrame; + /** + * A provider for the menu bar. + */ + private final Provider menuBarProvider; + /** + * Provides the current system model. + */ + private final ModelManager modelManager; + /** + * The application's configuration. + */ + private final OperationsDeskConfiguration appConfig; + /** + * Where we register for application events. + */ + private final EventSource eventSource; + /** + * The portal manager. + */ + private final PortalManager portalManager; + + /** + * Creates a new instance. + * + * @param frame The frame in which the OpenTCSView is to be shown. + * @param menuBarProvider Provides the main application menu bar. + * @param modelManager Provides the current system model. + * @param appConfig The application's configuration. + * @param eventSource Where this instance registers for application events. + * @param portalManager The portal manager. + */ + @Inject + public OpenTCSSDIApplication( + @ApplicationFrame + JFrame frame, + Provider menuBarProvider, + ModelManager modelManager, + OperationsDeskConfiguration appConfig, + @ApplicationEventBus + EventSource eventSource, + PortalManager portalManager + ) { + this.contentFrame = requireNonNull(frame, "frame"); + this.menuBarProvider = requireNonNull(menuBarProvider, "menuBarProvider"); + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.appConfig = requireNonNull(appConfig, "appConfig"); + this.eventSource = requireNonNull(eventSource, "eventSource"); + this.portalManager = requireNonNull(portalManager, "portalManager"); + } + + @Override + public void show(final View view) { + requireNonNull(view, "view"); + + if (view.isShowing()) { + return; + } + view.setShowing(true); + + eventSource.subscribe(this); + + final OpenTCSView opentcsView = (OpenTCSView) view; + + setupContentFrame(opentcsView); + + TitleUpdater titleUpdater = new TitleUpdater(opentcsView); + opentcsView.addPropertyChangeListener(titleUpdater); + eventSource.subscribe(titleUpdater); + + updateViewTitle(view, contentFrame); + + // The frame should be shown only after the view has been initialized. + opentcsView.start(); + contentFrame.setVisible(true); + } + + @Override + protected void updateViewTitle(View view, JFrame frame) { + ((OpenTCSView) view).updateModelName(); + } + + @Override + public void onEvent(Object event) { + if (event instanceof ModelNameChangeEvent) { + ModelNameChangeEvent modelNameChangeEvent = (ModelNameChangeEvent) event; + updateViewTitle((OpenTCSView) modelNameChangeEvent.getSource(), contentFrame); + } + } + + private void updateViewTitle(OpenTCSView view, JFrame frame) { + String modelName = modelManager.getModel().getName(); + if (view.hasUnsavedChanges()) { + modelName += "*"; + } + + if (frame != null) { + frame.setTitle( + OpenTCSView.NAME + " - \"" + + modelName + "\" - " + + BUNDLE.getString("openTcsSdiApplication.frameTitle_connectedTo.text") + + portalManager.getDescription() + + " (" + portalManager.getHost() + ":" + portalManager.getPort() + ")" + ); + } + } + + private void setupContentFrame(OpenTCSView opentcsView) { + contentFrame.setJMenuBar(menuBarProvider.get()); + + contentFrame.setIconImages(Icons.getOpenTCSIcons()); + contentFrame.setSize(1024, 768); + + // Restore the window's dimensions from the configuration. + contentFrame.setExtendedState(appConfig.frameMaximized() ? Frame.MAXIMIZED_BOTH : Frame.NORMAL); + + if (contentFrame.getExtendedState() != Frame.MAXIMIZED_BOTH) { + contentFrame.setBounds( + appConfig.frameBoundsX(), + appConfig.frameBoundsY(), + appConfig.frameBoundsWidth(), + appConfig.frameBoundsHeight() + ); + } + + contentFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + contentFrame.addWindowListener(new WindowStatusUpdater(opentcsView)); + } + + private class TitleUpdater + implements + PropertyChangeListener, + EventHandler { + + private final OpenTCSView opentcsView; + + TitleUpdater(OpenTCSView opentcsView) { + this.opentcsView = requireNonNull(opentcsView, "opentcsView"); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + String name = evt.getPropertyName(); + + if (name.equals(View.HAS_UNSAVED_CHANGES_PROPERTY)) { + updateViewTitle(opentcsView, contentFrame); + } + } + + @Override + public void onEvent(Object event) { + if (event instanceof PortalManager.ConnectionState) { + PortalManager.ConnectionState connectionState = (PortalManager.ConnectionState) event; + switch (connectionState) { + case CONNECTED: + break; + case DISCONNECTED: + break; + default: + } + updateViewTitle(opentcsView, contentFrame); + } + } + } + + private class WindowStatusUpdater + extends + WindowAdapter { + + private final OpenTCSView opentcsView; + + WindowStatusUpdater(OpenTCSView opentcsView) { + this.opentcsView = requireNonNull(opentcsView, "opentcsView"); + } + + @Override + public void windowClosing(WindowEvent e) { + // Check if changes to the model still need to be saved. + getAction(opentcsView, CloseFileAction.ID).actionPerformed( + new ActionEvent( + contentFrame, + ActionEvent.ACTION_PERFORMED, + CloseFileAction.ID_WINDOW_CLOSING + ) + ); + } + + @Override + public void windowClosed(WindowEvent e) { + opentcsView.stop(); + } + + @Override + public void windowGainedFocus(WindowEvent e) { + setActiveView(opentcsView); + } + } +} diff --git a/opentcs-operationsdesk/src/main/java/org/opentcs/thirdparty/operationsdesk/jhotdraw/application/action/file/CloseFileAction.java b/opentcs-operationsdesk/src/main/java/org/opentcs/thirdparty/operationsdesk/jhotdraw/application/action/file/CloseFileAction.java new file mode 100644 index 0000000..b04c7fd --- /dev/null +++ b/opentcs-operationsdesk/src/main/java/org/opentcs/thirdparty/operationsdesk/jhotdraw/application/action/file/CloseFileAction.java @@ -0,0 +1,174 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.operationsdesk.jhotdraw.application.action.file; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.operationsdesk.util.I18nPlantOverviewOperating.MENU_PATH; + +import java.awt.event.ActionEvent; +import java.net.URI; +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.ImageIcon; +import javax.swing.JFrame; +import javax.swing.JOptionPane; +import javax.swing.KeyStroke; +import javax.swing.SwingUtilities; +import org.jhotdraw.app.View; +import org.jhotdraw.app.action.file.NewFileAction; +import org.jhotdraw.app.action.file.NewWindowAction; +import org.jhotdraw.app.action.file.OpenDirectoryAction; +import org.jhotdraw.app.action.file.OpenFileAction; +import org.jhotdraw.net.URIUtil; +import org.opentcs.access.Kernel; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.operationsdesk.application.OpenTCSView; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Closes the active view after letting the user save unsaved changes. + * {@code DefaultSDIApplication} automatically exits when the user closes the + * last view. + *

    + * This action is called when the user selects the Close item in + * the File menu. The menu item is automatically created by the application. + *

    + * If you want this behavior in your application, you have to create it and put + * it in your {@code ApplicationModel} in method + * {@link org.jhotdraw.app.ApplicationModel#initApplication}. + *

    + * You should + * include this action in applications which use at least one of the following + * actions, so that the user can close views that he/she created: + * {@link NewFileAction}, {@link NewWindowAction}, + * {@link OpenFileAction}, {@link OpenDirectoryAction}. + * + * @author Werner Randelshofer + */ +public class CloseFileAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "file.close"; + /** + * This action's ID for closing the window. + */ + public static final String ID_WINDOW_CLOSING = "windowClosing"; + /** + * This action's ID for closing the model. + */ + public static final String ID_MODEL_CLOSING = "modelClosing"; + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + /** + * 0: Save file; 1: Don't save file; 2: Canceled. + */ + private int fileSaved; + private final OpenTCSView view; + + /** + * Creates a new instance. + * + * @param view The openTCS view + */ + @SuppressWarnings("this-escape") + public CloseFileAction(OpenTCSView view) { + this.view = requireNonNull(view, "view"); + + putValue(NAME, BUNDLE.getString("closeFileAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("closeFileAction.shortDescription")); + putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("alt F4")); + putValue(Action.MNEMONIC_KEY, Integer.valueOf('C')); + + ImageIcon icon = ImageDirectory.getImageIcon("/menu/document-close-4.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + } + + public int getFileSavedStatus() { + return fileSaved; + } + + @Override + public void actionPerformed(ActionEvent evt) { + final ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.app.Labels"); + + if (view.hasUnsavedChanges()) { + URI unsavedURI = view.getURI(); + String message + = "" + + labels.getFormatted( + "file.saveBefore.doYouWantToSave.message", + (unsavedURI == null) + ? Kernel.DEFAULT_MODEL_NAME + : URIUtil.getName(unsavedURI) + ) + + "

    " + + labels.getString("file.saveBefore.doYouWantToSave.details") + + "

    "; + + Object[] options = { + labels.getString("file.saveBefore.saveOption.text"), + labels.getString("file.saveBefore.dontSaveOption.text"), + labels.getString("file.saveBefore.cancelOption.text") + }; + + int option = JOptionPane.showOptionDialog( + view.getComponent(), + message, + "", + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE, + null, + options, + options[0] + ); + + fileSaved = JOptionPane.CANCEL_OPTION; + + switch (option) { + case JOptionPane.YES_OPTION: // Save + if (view.saveModel()) { + fileSaved = JOptionPane.YES_OPTION; + doIt(evt.getActionCommand(), view); + } + + break; + + case JOptionPane.NO_OPTION: // Don't save + fileSaved = JOptionPane.NO_OPTION; + doIt(evt.getActionCommand(), view); + break; + + default: + case JOptionPane.CANCEL_OPTION: + break; + } + } + else { + fileSaved = JOptionPane.NO_OPTION; + doIt(evt.getActionCommand(), view); + } + } + + protected void doIt(String actionCommand, View view) { + if (!actionCommand.equals(ID_MODEL_CLOSING) && view != null) { + if (view.isShowing()) { + view.setShowing(false); + JFrame f = (JFrame) SwingUtilities.getWindowAncestor(view.getComponent()); + f.setVisible(false); + f.remove(view.getComponent()); + f.dispose(); + } + + view.dispose(); + } + } + + protected void doIt(View view) { + doIt(ID_WINDOW_CLOSING, view); + } +} diff --git a/opentcs-operationsdesk/src/main/resources/REUSE.toml b/opentcs-operationsdesk/src/main/resources/REUSE.toml new file mode 100644 index 0000000..42aab58 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/REUSE.toml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC0-1.0 + +version = 1 + +[[annotations]] +path = ["**/*.gif", "**/*.jpg", "**/*.png", "**/*.svg"] +precedence = "closest" +SPDX-FileCopyrightText = "The openTCS Authors" +SPDX-License-Identifier = "CC-BY-4.0" diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/createPeripheralJob.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/createPeripheralJob.properties new file mode 100644 index 0000000..fc91a4d --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/createPeripheralJob.properties @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +createPeripheralJobPanel.border.title=Peripheral job +createPeripheralJobPanel.label_location.text=Location: +createPeripheralJobPanel.label_operation.text=Operation: +createPeripheralJobPanel.label_reservationToken.text=Reservation token: +createPeripheralJobPanel.optionPane_invalidOperation.message=Location and operation cannot be empty. +createPeripheralJobPanel.optionPane_invalidOperation.title=Invalid operation +createPeripheralJobPanel.optionPane_reserveTokenEmpty.message=The reservation token cannot be empty. +createPeripheralJobPanel.optionPane_reserveTokenEmpty.title=Invalid reservation token +createPeripheralJobPanel.title=Create peripheral job diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/createPeripheralJob_de.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/createPeripheralJob_de.properties new file mode 100644 index 0000000..6e3b838 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/createPeripheralJob_de.properties @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +createPeripheralJobPanel.border.title=Peripherieauftrag +createPeripheralJobPanel.label_location.text=Station: +createPeripheralJobPanel.label_operation.text=Operation: +createPeripheralJobPanel.label_reservationToken.text=Reservierungstoken: +createPeripheralJobPanel.optionPane_invalidOperation.message=Station und Operation d\u00fcrfen nicht leer sein. +createPeripheralJobPanel.optionPane_invalidOperation.title=Ung\u00fcltige Operation +createPeripheralJobPanel.optionPane_reserveTokenEmpty.message=Der Reservierungstoken darf nicht leer sein. +createPeripheralJobPanel.optionPane_reserveTokenEmpty.title=Ung\u00fcltiger Reservierungstoken +createPeripheralJobPanel.title=Peripherieauftrag erzeugen diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/createTransportOrder.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/createTransportOrder.properties new file mode 100644 index 0000000..f19c2eb --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/createTransportOrder.properties @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +createTransportOrderPanel.button_add.text=Add... +createTransportOrderPanel.button_delete.text=Delete +createTransportOrderPanel.button_edit.text=Edit... +createTransportOrderPanel.button_moveDown.text=Down +createTransportOrderPanel.button_up.text=Up +createTransportOrderPanel.comboBox_automatic.text=Automatic +createTransportOrderPanel.label_date.text=Date +createTransportOrderPanel.label_time.text=Time +createTransportOrderPanel.label_type.text=Type +createTransportOrderPanel.label_vehicle.text=Vehicle +createTransportOrderPanel.optionPane_dateTimeParseError.message=Error parsing the date or the time. +createTransportOrderPanel.optionPane_dateTimeParseError.title=Input error +createTransportOrderPanel.optionPane_noOrderError.message=Please create at least one drive order. +createTransportOrderPanel.optionPane_noOrderError.title=Input error +createTransportOrderPanel.panel_deadline.border.title=Deadline +createTransportOrderPanel.panel_stations.border.title=Drive orders +createTransportOrderPanel.panel_type.border.title=Type +createTransportOrderPanel.panel_vehicle.border.title=Vehicle +createTransportOrderPanel.table_driveOrdersTable.column_action.headerText=Action +createTransportOrderPanel.table_driveOrdersTable.column_location.headerText=Location +editDriverOrderPanel.create.title=Create drive order +editDriverOrderPanel.edit.title=Edit drive order +editDriverOrderPanel.label_action.text=Action: +editDriverOrderPanel.label_location.text=Location: +transportOrdersContainerPanel.dialog.title=New transport order diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/createTransportOrder_de.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/createTransportOrder_de.properties new file mode 100644 index 0000000..db860b8 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/createTransportOrder_de.properties @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +createTransportOrderPanel.button_add.text=Hinzuf\u00fcgen +createTransportOrderPanel.button_delete.text=Entfernen +createTransportOrderPanel.button_edit.text=Bearbeiten +createTransportOrderPanel.button_moveDown.text=Nach unten +createTransportOrderPanel.button_up.text=Nach oben +createTransportOrderPanel.comboBox_automatic.text=automatisch +createTransportOrderPanel.label_date.text=Datum +createTransportOrderPanel.label_time.text=Zeit +createTransportOrderPanel.label_type.text=Typ +createTransportOrderPanel.label_vehicle.text=Fahrzeug +createTransportOrderPanel.optionPane_dateTimeParseError.message=Fehler beim Parsen des Datums oder der Uhrzeit. +createTransportOrderPanel.optionPane_dateTimeParseError.title=Eingabefehler +createTransportOrderPanel.optionPane_noOrderError.message=Bitte erstellen Sie mindestens einen Teilauftrag. +createTransportOrderPanel.optionPane_noOrderError.title=Eingabefehler +createTransportOrderPanel.panel_deadline.border.title=Frist +createTransportOrderPanel.panel_stations.border.title=Fahrauftr\u00e4ge +createTransportOrderPanel.panel_type.border.title=Typ +createTransportOrderPanel.panel_vehicle.border.title=Fahrzeug +createTransportOrderPanel.table_driveOrdersTable.column_action.headerText=Aktion +createTransportOrderPanel.table_driveOrdersTable.column_location.headerText=Station +editDriverOrderPanel.create.title=Fahrauftrag erzeugen +editDriverOrderPanel.edit.title=Fahrauftrag editieren +editDriverOrderPanel.label_action.text=Aktion: +editDriverOrderPanel.label_location.text=Station: +transportOrdersContainerPanel.dialog.title=Neuer Transportauftrag diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/findVehicle.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/findVehicle.properties new file mode 100644 index 0000000..a41517a --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/findVehicle.properties @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +findVehicleAction.dialog_findVehicle.title=Find vehicle +findVehiclePanel.button_find.text=Find +findVehiclePanel.label_vehicles.text=Vehicle: diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/findVehicle_de.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/findVehicle_de.properties new file mode 100644 index 0000000..0ec4592 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/findVehicle_de.properties @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +findVehicleAction.dialog_findVehicle.title=Fahrzeug suchen +findVehiclePanel.button_find.text=Suchen +findVehiclePanel.label_vehicles.text=Fahrzeug: diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/orderSequenceDetail.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/orderSequenceDetail.properties new file mode 100644 index 0000000..0aa8244 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/orderSequenceDetail.properties @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +orderSequenceHistoryEntryFormatter.code_sequenceCompleted.text=Order sequence was marked as complete. +orderSequenceHistoryEntryFormatter.code_sequenceCreated.text=Order sequence was created. +orderSequenceHistoryEntryFormatter.code_sequenceFinished.text=Order sequence was finished. +orderSequenceHistoryEntryFormatter.code_sequenceOrderAppended.text=Transport order was appended: +orderSequenceHistoryEntryFormatter.code_sequenceProcVehicleChanged.text=Processing vehicle is now: +orderSequenceView.optionPane_markSequenceCompleteConfirmation.message=Mark order sequence as complete? +orderSequenceView.optionPane_markSequenceCompleteConfirmation.title=Order sequence +orderSequenceView.panel_general.border.title=General +orderSequenceView.panel_general.checkBox_complete.text=Complete +orderSequenceView.panel_general.checkBox_complete.tooltipText=Whether the order sequence is complete. +orderSequenceView.panel_general.checkBox_failureFatal.text=Failure fatal +orderSequenceView.panel_general.checkBox_failureFatal.tooltipText=Whether an error in a transport order leads to abortion of the whole order sequence. +orderSequenceView.panel_general.checkBox_finished.text=Finished +orderSequenceView.panel_general.checkBox_finished.tooltipText=Whether all transport orders belonging to the order sequence have been finished. +orderSequenceView.panel_general.label_finishedIndex.text=Finished index: +orderSequenceView.panel_general.label_finishedIndex.tooltipText=The index of the last executed transport order. +orderSequenceView.panel_general.label_intendedVehicle.text=Intended vehicle: +orderSequenceView.panel_general.label_name.text=Name: +orderSequenceView.panel_general.label_processingVehicle.text=Executing vehicle: +orderSequenceView.panel_general.label_type.text=Type: +orderSequenceView.panel_history.border.text=History +orderSequenceView.panel_properties.border.text=Properties +orderSequenceView.panel_transportOrders.border.title=Transport orders +orderSequenceView.table_history.column_event.headerText=Event +orderSequenceView.table_history.column_timestamp.headerText=Timestamp +orderSequenceView.table_properties.column_propertiesKey.headerText=Key +orderSequenceView.table_properties.column_propertiesValue.headerText=Value +orderSequenceView.title=Transport order sequence diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/orderSequenceDetail_de.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/orderSequenceDetail_de.properties new file mode 100644 index 0000000..f887b1e --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/orderSequenceDetail_de.properties @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +orderSequenceHistoryEntryFormatter.code_sequenceCompleted.text=Auftragssequenz wurde als vollst\u00c3\u00a4ndig markiert. +orderSequenceHistoryEntryFormatter.code_sequenceCreated.text=Auftragssequenz wurde erstellt. +orderSequenceHistoryEntryFormatter.code_sequenceFinished.text=Auftragssequenz wurde abgeschlossen. +orderSequenceHistoryEntryFormatter.code_sequenceOrderAppended.text=Transportauftrag wurde hinzugef\u00fcgt: +orderSequenceHistoryEntryFormatter.code_sequenceProcVehicleChanged.text=Bearbeitendes Fahrzeug ist nun: +orderSequenceView.optionPane_markSequenceCompleteConfirmation.message=Auftragssequenz als vollst\u00e4ndig markieren? +orderSequenceView.optionPane_markSequenceCompleteConfirmation.title=Auftragssequenz +orderSequenceView.panel_general.border.title=Allgemein +orderSequenceView.panel_general.checkBox_complete.text=Vollst\u00e4ndig +orderSequenceView.panel_general.checkBox_complete.tooltipText=Ob die Auftragssequenz vollst\u00e4ndig ist. +orderSequenceView.panel_general.checkBox_failureFatal.text=Fehlschlag fatal +orderSequenceView.panel_general.checkBox_failureFatal.tooltipText=Ob ein Fehler in einem Transportauftrag zum Abbruch der gesamten Auftragssequenz f\u00fchrt. +orderSequenceView.panel_general.checkBox_finished.text=Abgeschlossen +orderSequenceView.panel_general.checkBox_finished.tooltipText=Ob alle zur Auftragssequenz geh\u00f6rigen Transportauftr\u00e4ge abgeschlossen wurden. +orderSequenceView.panel_general.label_finishedIndex.text=Abgeschlossen-Index: +orderSequenceView.panel_general.label_finishedIndex.tooltipText=Der Index des zuletzt abgeschlossenen Transportauftrags. +orderSequenceView.panel_general.label_intendedVehicle.text=Gew\u00fcnschtes Fahrzeug: +orderSequenceView.panel_general.label_name.text=Name: +orderSequenceView.panel_general.label_processingVehicle.text=Ausf\u00fchrendes Fahrzeug: +orderSequenceView.panel_general.label_type.text=Typ: +orderSequenceView.panel_history.border.text=Historie +orderSequenceView.panel_properties.border.text=Eigenschaften +orderSequenceView.panel_transportOrders.border.title=Transportauftr\u00e4ge +orderSequenceView.table_history.column_event.headerText=Ereignis +orderSequenceView.table_history.column_timestamp.headerText=Zeitstempel +orderSequenceView.table_properties.column_propertiesKey.headerText=Schl\u00fcssel +orderSequenceView.table_properties.column_propertiesValue.headerText=Wert +orderSequenceView.title=Transportauftragssequenz diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/peripheralJobDetail.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/peripheralJobDetail.properties new file mode 100644 index 0000000..c6225c7 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/peripheralJobDetail.properties @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +peripheralJobHistoryEntryFormatter.code_jobCreated.text=Peripheral job was created. +peripheralJobHistoryEntryFormatter.code_jobReachedFinalState.text=Peripheral job reached final state. +peripheralJobView.panel_general.border.title=General +peripheralJobView.panel_general.label_created.text=Created: +peripheralJobView.panel_general.label_finished.text=Finished: +peripheralJobView.panel_general.label_name.text=Name: +peripheralJobView.panel_general.label_reservationToken.text=Reservation token: +peripheralJobView.panel_general.label_state.text=State: +peripheralJobView.panel_general.label_transportOrder.text=Transport order: +peripheralJobView.panel_general.label_vehicle.text=Vehicle: +peripheralJobView.panel_history.border.title=History +peripheralJobView.panel_operation.border.title=Operation +peripheralJobView.panel_operation.lable_location.text=Location: +peripheralJobView.panel_operation.lable_operation.text=Operation: +peripheralJobView.panel_operation.lable_requireCompletion.text=Completion required: +peripheralJobView.panel_operation.lable_trigger.text=Trigger +peripheralJobView.panel_properties.border.title=Properties +peripheralJobView.table_history.column_event.headerText=Event +peripheralJobView.table_history.column_timestamp.headerText=Timestamp +peripheralJobView.table_properties.column_propertiesKey.headerText=Key +peripheralJobView.table_properties.column_propertiesValue.headerText=Value +peripheralJobView.title=Peripheral job detail view diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/peripheralJobDetail_de.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/peripheralJobDetail_de.properties new file mode 100644 index 0000000..da285e8 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/peripheralJobDetail_de.properties @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +peripheralJobHistoryEntryFormatter.code_jobCreated.text=Peripheriejob wurde erzeugt. +peripheralJobHistoryEntryFormatter.code_jobReachedFinalState.text=Peripheriejob erreichte finalen Zustand. +peripheralJobView.panel_general.border.title=Allgemein +peripheralJobView.panel_general.label_created.text=Erzeugt: +peripheralJobView.panel_general.label_finished.text=Beendet: +peripheralJobView.panel_general.label_name.text=Name: +peripheralJobView.panel_general.label_reservationToken.text=Reservierungstoken: +peripheralJobView.panel_general.label_state.text=Status +peripheralJobView.panel_general.label_transportOrder.text=Transportauftrag: +peripheralJobView.panel_general.label_vehicle.text=Fahrzeug: +peripheralJobView.panel_history.border.title=Historie +peripheralJobView.panel_operation.border.title=Operation +peripheralJobView.panel_operation.lable_location.text=Station: +peripheralJobView.panel_operation.lable_operation.text=Operation: +peripheralJobView.panel_operation.lable_requireCompletion.text=Abschluss erforderlich: +peripheralJobView.panel_operation.lable_trigger.text=Ausl\u00f6ser +peripheralJobView.panel_properties.border.title=Eigenschaften +peripheralJobView.table_history.column_event.headerText=Ereignis +peripheralJobView.table_history.column_timestamp.headerText=Zeitstempel +peripheralJobView.table_properties.column_propertiesKey.headerText=Schl\u00fcssel +peripheralJobView.table_properties.column_propertiesValue.headerText=Wert +peripheralJobView.title=Peripheriejob Detailansicht diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/transportOrderDetail.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/transportOrderDetail.properties new file mode 100644 index 0000000..531bef4 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/transportOrderDetail.properties @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +transportOrderHistoryEntryFormatter.code_driveOrderFinished.text=A drive order was finished. +transportOrderHistoryEntryFormatter.code_orderAssignedToVehicle.text=Transport order was assigned to vehicle: +transportOrderHistoryEntryFormatter.code_orderCreated.text=Transport order was created. +transportOrderHistoryEntryFormatter.code_orderDispatchingDeferred.text=Order dispatching was deferred for the following reasons: +transportOrderHistoryEntryFormatter.code_orderDispatchingResumed.text=Order dispatching was resumed. +transportOrderHistoryEntryFormatter.code_orderProcVehicleChanged.text=Processing vehicle is now: +transportOrderHistoryEntryFormatter.code_orderReachedFinalState.text=Transport order reached final state. +transportOrderHistoryEntryFormatter.code_orderReservedForVehicle.text=Transport order was reserved for vehicle: +transportOrderView.label_costs.title=Costs: +transportOrderView.label_created.text=Created: +transportOrderView.label_deadline.text=Deadline: +transportOrderView.label_dispensable.text=Dispensable: +transportOrderView.label_finished.text=Finished: +transportOrderView.label_reservationToken.text=Reservation token: +transportOrderView.label_type.text=Type: +transportOrderView.label_vehicle.text=Vehicle: +transportOrderView.panel_dependencies.border.title=Dependencies +transportOrderView.panel_driveOrderProperties.border.title=Properties +transportOrderView.panel_driveOrders.border.title=Drive orders +transportOrderView.panel_general.border.title=General +transportOrderView.panel_history.border.title=History +transportOrderView.panel_properties.border.title=Properties +transportOrderView.panel_route.border.title=Route +transportOrderView.table_dependencies.column_dependentTransportOrder.headerText=Transport Order +transportOrderView.table_driveOrderProperties.column_driveOrderPropertiesKey.headerText=Key +transportOrderView.table_driveOrderProperties.column_driveOrderPropertiesValue.headerText=Value +transportOrderView.table_driveOrderProperties.column_target.headerText=Target +transportOrderView.table_history.column_event.headerText=Event +transportOrderView.table_history.column_timestamp.headerText=Timestamp +transportOrderView.table_properties.column_propertiesKey.headerText=Key +transportOrderView.table_properties.column_propertiesValue.headerText=Value +transportOrderView.table_route.column_route.headerText=Route +transportOrderView.table_routeTable.column_destination.headerText=Destination point +transportOrderView.title=Transport order detailed view +transportOrdersContainerPanel.dialog_createTransportOrder.title=Transport order diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/transportOrderDetail_de.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/transportOrderDetail_de.properties new file mode 100644 index 0000000..3ae2336 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/transportOrderDetail_de.properties @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +transportOrderHistoryEntryFormatter.code_driveOrderFinished.text=Ein Teilauftrag wurde abgeschlossen. +transportOrderHistoryEntryFormatter.code_orderAssignedToVehicle.text=Transportauftrag wurde folgendem Fahrzeug zugewiesen: +transportOrderHistoryEntryFormatter.code_orderCreated.text=Transportauftrag wurde erzeugt. +transportOrderHistoryEntryFormatter.code_orderDispatchingDeferred.text=Auftragsdisposition wurde aus folgenden Gr\u00fcnden verschoben: +transportOrderHistoryEntryFormatter.code_orderDispatchingResumed.text=Auftragsdisposition wurde wieder aufgenommen. +transportOrderHistoryEntryFormatter.code_orderProcVehicleChanged.text=Bearbeitendes Fahrzeug ist nun: +transportOrderHistoryEntryFormatter.code_orderReachedFinalState.text=Transportauftrag erreichte finalen Zustand. +transportOrderHistoryEntryFormatter.code_orderReservedForVehicle.text=Transportauftrag wurde f\u00fcr folgendes Fahrzeug reserviert: +transportOrderView.label_costs.title=Kosten: +transportOrderView.label_created.text=Erzeugt: +transportOrderView.label_deadline.text=Frist: +transportOrderView.label_dispensable.text=Entbehrlich: +transportOrderView.label_finished.text=Beendet: +transportOrderView.label_reservationToken.text=Reservierungstoken: +transportOrderView.label_type.text=Typ: +transportOrderView.label_vehicle.text=Fahrzeug: +transportOrderView.panel_dependencies.border.title=Abh\u00e4ngigkeiten +transportOrderView.panel_driveOrderProperties.border.title=Eigenschaften +transportOrderView.panel_driveOrders.border.title=Fahrauftr\u00e4ge +transportOrderView.panel_general.border.title=Allgemein +transportOrderView.panel_history.border.title=Historie +transportOrderView.panel_properties.border.title=Eigenschaften +transportOrderView.panel_route.border.title=Weg +transportOrderView.table_dependencies.column_dependentTransportOrder.headerText=Transportauftrag +transportOrderView.table_driveOrderProperties.column_driveOrderPropertiesKey.headerText=Schl\u00fcssel +transportOrderView.table_driveOrderProperties.column_driveOrderPropertiesValue.headerText=Wert +transportOrderView.table_driveOrderProperties.column_target.headerText=Ziel +transportOrderView.table_history.column_event.headerText=Ereignis +transportOrderView.table_history.column_timestamp.headerText=Zeitstempel +transportOrderView.table_properties.column_propertiesKey.headerText=Schl\u00fcssel +transportOrderView.table_properties.column_propertiesValue.headerText=Wert +transportOrderView.table_route.column_route.headerText=Strecke +transportOrderView.table_routeTable.column_destination.headerText=Zielpunkt +transportOrderView.title=Transportauftrag Detailansicht +transportOrdersContainerPanel.dialog_createTransportOrder.title=Transportauftrag diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/userNotificationDetail.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/userNotificationDetail.properties new file mode 100644 index 0000000..8b793d9 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/userNotificationDetail.properties @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +userNotificationView.title=User notification detailed view +userNotificationView.label_created.text=Timestamp: +userNotificationView.label_level.text=Level: +userNotificationView.label_source.text=Source: +userNotificationView.label_text.text=Text: \ No newline at end of file diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/userNotificationDetail_de.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/userNotificationDetail_de.properties new file mode 100644 index 0000000..4579594 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/userNotificationDetail_de.properties @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +userNotificationView.title=Benachrichtigung Detailansicht +userNotificationView.label_created.text=Erstellt: +userNotificationView.label_level.text=Stufe: +userNotificationView.label_source.text=Quelle: +userNotificationView.label_text.text=Inhalt: \ No newline at end of file diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/vehiclePopup.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/vehiclePopup.properties new file mode 100644 index 0000000..bd5a793 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/vehiclePopup.properties @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +followVehicleAction.name=Follow vehicle +integrationLevelChangeAction.ignore.name=...to ignore this vehicle +integrationLevelChangeAction.notice.name=...to notice this vehicle's position +integrationLevelChangeAction.respect.name=...to respect this vehicle's position +integrationLevelChangeAction.utilize.name=...to utilize this vehicle for transport orders +locationActionPanel.label_action.text=Action: +locationActionPanel.label_location.text=Location: +locationActionPanel.title=Select location and action +pauseAction.pause.name=Pause +pauseAction.resume.name=Resume +pointPanel.label_points.text=Points: +rerouteAction.forcedRerouting.name=...forcibly (i.e. accepting their current reported position.) +rerouteAction.optionPane_confirmForcedRerouting.message=Forced rerouting of a vehicle from its current position can disrupt traffic management if used inappropriately.

    Only do this if you are sure it is necessary and safe, and only if the vehicle is not moving! +rerouteAction.optionPane_confirmForcedRerouting.title=Confirm forced rerouting +rerouteAction.regularRerouting.name=...regularly/safely (i.e. only if they are where they should be.) +scrollToVehicleAction.name=Scroll to vehicle +sendVehicleToLocationAction.name=Send to location... +sendVehicleToPointAction.name=Send to point... +vehiclePopupMenu.menuItem_multipleVehicles.text=Multiple vehicles selected +vehiclePopupMenu.menuItem_singleVehicle.text=Vehicle +vehiclePopupMenu.subMenu_integrate.text=Change integration level +vehiclePopupMenu.subMenu_pause.text=Pause +vehiclePopupMenu.subMenu_reroute.text=Reroute vehicle(s) +vehiclePopupMenu.subMenu_withdraw.text=Withdraw transport order +withdrawAction.optionPane_confirmWithdraw.message=Withdrawing a vehicle's transport order immediately should be used carefully:

    • It can lead to collisions or deadlocks if the vehicle is not currently halted on a point.
    • It aborts all peripheral jobs related to this transport order that may still be pending.
    +withdrawAction.optionPane_confirmWithdraw.title=Confirm immediate withdrawal +withdrawAction.withdraw.name=...and let the vehicle finish movements +withdrawAction.withdrawImmediately.name=...and stop the vehicle immediately diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/vehiclePopup_de.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/vehiclePopup_de.properties new file mode 100644 index 0000000..64841e4 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/dialogs/vehiclePopup_de.properties @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +followVehicleAction.name=Fahrzeug folgen +integrationLevelChangeAction.ignore.name=...und ignoriere dieses Fahrzeug +integrationLevelChangeAction.notice.name=...und nimm die Position dieses Fahrzeugs zur Kenntnis +integrationLevelChangeAction.respect.name=...und ber\u00fccksichtige die Position dieses Fahrzeugs +integrationLevelChangeAction.utilize.name=...und nutze dieses Fahrzeug f\u00fcr Transportauftr\u00e4ge +locationActionPanel.label_action.text=Aktion: +locationActionPanel.label_location.text=Station: +locationActionPanel.title=Station und Aktion ausw\u00e4hlen +pauseAction.pause.name=Pausieren +pauseAction.resume.name=Weitermachen +pointPanel.label_points.text=Meldepunkte: +rerouteAction.regularRerouting.name=...auf regul\u00e4re/sichere Weise (d.h. nur, wenn sie dort sind, wo sie sein sollten). +rerouteAction.optionPane_confirmForcedRerouting.message=Bedingungsloses Rerouting eines Fahrzeugs von seiner aktuellen Position aus kann die Verkehrsregelung st\u00f6ren, wenn es falsch angewendet wird.

    Tun Sie dies nur, falls Sie sicher sind, dass es notwendig und sicher ist, und nur bei Stillstand des Fahrzeugs! +rerouteAction.optionPane_confirmForcedRerouting.title=Bedingungsloses Rerouting best\u00e4tigen +rerouteAction.forcedRerouting.name=...bedingungslos (d.h. ihre aktuelle Position akzeptierend). +scrollToVehicleAction.name=Ansicht zu Fahrzeug verschieben +sendVehicleToLocationAction.name=Fahre zu einer Station... +sendVehicleToPointAction.name=Fahre zu einem Punkt... +vehiclePopupMenu.menuItem_multipleVehicles.text=Mehrere Fahrzeuge ausgew\u00e4hlt +vehiclePopupMenu.menuItem_singleVehicle.text=Fahrzeug +vehiclePopupMenu.subMenu_integrate.text=\u00c4ndere Integrationsstufe +vehiclePopupMenu.subMenu_pause.text=Pausieren +vehiclePopupMenu.subMenu_reroute.text=Route Fahrzeug(e) neu +vehiclePopupMenu.subMenu_withdraw.text=Transportauftrag entziehen +withdrawAction.optionPane_confirmWithdraw.message=Das sofortige Zur\u00fcckziehen des Transportauftrages eines Fahrzeugs sollte vorsichtig verwendet werden:

    • Es kann zu Kollisionen oder Deadlocks f\u00fchren, falls das Fahrzeug gegenw\u00e4rtig nicht auf einem Punkt angehalten ist.
    • Es bricht ggfs. alle ausstehenden Peripheriejobs ab, die zu diesem Transportauftrag geh\u00f6ren.
    +withdrawAction.optionPane_confirmWithdraw.title=Sofortiges Zur\u00fcckziehen best\u00e4tigen +withdrawAction.withdraw.name=...und das Fahrzeug ausrollen lassen +withdrawAction.withdrawImmediately.name=...und das Fahrzeug sofort anhalten diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/mainMenu.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/mainMenu.properties new file mode 100644 index 0000000..e3da800 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/mainMenu.properties @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +aboutAction.name=About +aboutAction.optionPane_applicationInformation.message.baselineVersion=openTCS baseline version: {0} +aboutAction.optionPane_applicationInformation.message.copyright=Copyright (c) the openTCS authors and contributors +aboutAction.optionPane_applicationInformation.message.customization=openTCS customization: {0} {1} +aboutAction.optionPane_applicationInformation.message.mode=Mode: {0} +aboutAction.optionPane_applicationInformation.message.runningOn=Running on +aboutAction.optionPane_applicationInformation.title=About +actionsMenu.menuItem_ignorePreciseOrientation.text=Ignore orientation angle +actionsMenu.menuItem_ignorePrecisePosition.text=Ignore precise positions +actionsMenu.text=Actions +actionsMenu.tooltipText=Actions +addDrawingViewAction.name=Add course view +addTransportOrderSequenceViewAction.name=Add order sequence view +addTransportOrderViewAction.name=Add transport order view +addPeripheralJobViewAction.name=Add peripheral job view +connectToKernelAction.name=Connect to kernel +closeFileAction.name=Close +closeFileAction.shortDescription=Close Plant Overview +createTransportOrderAction.name=Create transport order... +createPeripheralJobAction.name=Create peripheral job... +fileMenu.text=File +fileMenu.tooltipText=File +findVehicleAction.name=Find vehicle... +disconnectFromKernelAction.name=Disconnect from kernel +helpMenu.text=? +helpMenu.tooltipText=Help +viewMenu.menuItem_restoreWindowArrangement.text=Reset window arrangement +viewMenu.text=View +viewMenu.tooltipText=View + diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/mainMenu_de.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/mainMenu_de.properties new file mode 100644 index 0000000..66bae08 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/mainMenu_de.properties @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +aboutAction.name=Info +aboutAction.optionPane_applicationInformation.message.baselineVersion=openTCS Basisversion: {0} +aboutAction.optionPane_applicationInformation.message.customization=openTCS Erweiterung: {0} {1} +aboutAction.optionPane_applicationInformation.message.mode=Modus: {0} +aboutAction.optionPane_applicationInformation.message.runningOn=Ausgef\u00fchrt durch +aboutAction.optionPane_applicationInformation.title=\u00dcber +actionsMenu.menuItem_ignorePreciseOrientation.text=Orientierungswinkel ignorieren +actionsMenu.menuItem_ignorePrecisePosition.text=Pr\u00e4zise Positionen ignorieren +actionsMenu.text=Aktionen +actionsMenu.tooltipText=Aktionen +addDrawingViewAction.name=Fahrkursansicht hinzuf\u00fcgen +addTransportOrderSequenceViewAction.name=Auftragskettenansicht hinzuf\u00fcgen +addTransportOrderViewAction.name=Transportauftragsansicht hinzuf\u00fcgen +addPeripheralJobViewAction.name=Peripheriejobansicht hinzuf\u00fcgen +connectToKernelAction.name=Mit Kernel verbinden +closeFileAction.name=Beenden +closeFileAction.shortDescription=Anlagen\u00fcbersicht schlie\u00dfen +createTransportOrderAction.name=Transportauftrag... +createPeripheralJobAction.name=Peripherieauftrag... +fileMenu.text=Datei +fileMenu.tooltipText=Datei +findVehicleAction.name=Fahrzeug suchen... +disconnectFromKernelAction.name=Von Kernel trennen +helpMenu.tooltipText=Hilfe +viewMenu.menuItem_restoreWindowArrangement.text=Fensteranordnung zur\u00fccksetzen +viewMenu.text=Ansicht +viewMenu.tooltipText=Ansicht + diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/miscellaneous.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/miscellaneous.properties new file mode 100644 index 0000000..36d60ac --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/miscellaneous.properties @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +openTcsView.applicationName.text=Operations Desk +openTcsView.dialog_saveModelConfirmation.message=The Kernel is in modelling mode. Please make sure no one else is editing this model right now, because problems may arise. Do you still want so save it? +openTcsView.dialog_saveModelConfirmation.title=Do you really want to save? +openTcsView.dialog_unsavedChanges.message=You can only switch the Plant Overview state if you don't have any unsaved changes. Please save or discard your recent changes. +openTcsView.dialog_unsavedChanges.option_cancel.text=Cancel +openTcsView.dialog_unsavedChanges.option_discard.text=Discard +openTcsView.dialog_unsavedChanges.option_upload.text=Upload to kernel +openTcsView.dialog_unsavedChanges.title=State switch impossible +openTcsView.message_disconnectedFromKernel.text=Disconnected from kernel. +openTcsView.message_modelLoaded.text=Model "{0}" successfully loaded from kernel. +openTcsView.message_modelSaved.text=Model "{0}" successfully persisted. +openTcsView.optionPane_cannotDeleteLocationType.message=The location type cannot be deleted\nbecause at least one location of this type\nexists in this model. +openTcsView.optionPane_cannotDeleteLocationType.title=Delete not allowed +openTcsView.optionPane_saveModelBeforeKernelPersist.message=The model has to be saved locally, first, before you can persist it in the kernel. +openTcsView.panel_modellingDrawingView.title=Modelling view +openTcsView.panel_operatingDrawingView.title=Driving course +openTcsView.panel_operatingOrderSequencesView.title=Transport order sequences +openTcsView.panel_operatingTransportOrdersView.title=Transport orders +openTcsView.panel_operatingUserNotificationsView.title=User notifications +openTcsView.panel_peripheralJobsView.title=Peripheral jobs +openTcsView.state_modelling.text=Modelling mode +openTcsView.state_operating.text=Operating mode +toolTipTextGeneratorOperationsDesk.figureDecorationDetails.resourceAllocatedBy.text=Allocated by: +toolTipTextGeneratorOperationsDesk.vehicleModel.awaitPeripheralJobCompletion.text=Peripheral operations that need to be waited for: diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/miscellaneous_de.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/miscellaneous_de.properties new file mode 100644 index 0000000..19a686f --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/miscellaneous_de.properties @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +openTcsView.applicationName.text=Operations Desk +openTcsView.dialog_saveModelConfirmation.message=Der Kernel ist im Modellierungs-Modus. Bitte stellen Sie sicher, dass sonst niemand an diesem Modell arbeitet, da es sonst zu Problemen kommen kann. M\u00f6chten Sie das Modell dennoch speichern? +openTcsView.dialog_saveModelConfirmation.title=M\u00f6chten Sie wirklich speichern? +openTcsView.dialog_unsavedChanges.message=Sie k\u00f6nnen den Modus der Anlagen\u00fcbersicht nur wechseln, wenn Sie keine lokalen \u00c4nderungen haben. Bitte speichern oder verwerfen Sie Ihre \u00c4nderungen zuerst. +openTcsView.dialog_unsavedChanges.option_cancel.text=Abbrechen +openTcsView.dialog_unsavedChanges.option_discard.text=Verwerfen +openTcsView.dialog_unsavedChanges.option_upload.text=Hochladen zu Kernel +openTcsView.dialog_unsavedChanges.title=\u00c4ndern des Betriebsmodus unm\u00f6glich. +openTcsView.message_disconnectedFromKernel.text=Vom Kernel getrennt. +openTcsView.message_modelLoaded.text=Modell "{0}" erfolgreich vom Kernel geladen. +openTcsView.message_modelSaved.text=Modell "{0}" erfolgreich persistiert. +openTcsView.optionPane_cannotDeleteLocationType.message=Der Stationstyp kann nicht entfernt werden,\nda im Anlagenmodell noch mindestens eine Station\ndieses Typs existiert. +openTcsView.optionPane_cannotDeleteLocationType.title=L\u00f6schen nicht m\u00f6glich +openTcsView.optionPane_saveModelBeforeKernelPersist.message=Das Modell muss zuerst lokal gespeichert werden, bevor Sie es im Kernel persistieren k\u00f6nnen. +openTcsView.panel_modellingDrawingView.title=Modellierungsansicht +openTcsView.panel_operatingDrawingView.title=Fahrkurs +openTcsView.panel_operatingOrderSequencesView.title=Transportauftragsketten +openTcsView.panel_operatingTransportOrdersView.title=Transportauftr\u00e4ge +openTcsView.panel_operatingUserNotificationsView.title=Benachrichtigungen +openTcsView.panel_peripheralJobsView.title=Peripherieauftr\u00e4ge +openTcsView.state_modelling.text=Modellierungsmodus +openTcsView.state_operating.text=Betriebsmodus +toolTipTextGeneratorOperationsDesk.figureDecorationDetails.resourceAllocatedBy.text=Zugewiesen zu: +toolTipTextGeneratorOperationsDesk.vehicleModel.awaitPeripheralJobCompletion.text=Peripherieoperationen, auf die gewartet werden muss: diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/dockable.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/dockable.properties new file mode 100644 index 0000000..5ac7647 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/dockable.properties @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +dockingManagerOperating.panel_blocks.title=Blocks +dockingManagerOperating.panel_components.title=Components +dockingManagerOperating.panel_layers.title=Layers +dockingManagerOperating.panel_layerGroups.title=Layer groups +dockingManagerOperating.panel_properties.title=Properties diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/dockable_de.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/dockable_de.properties new file mode 100644 index 0000000..6827116 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/dockable_de.properties @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +dockingManagerOperating.panel_blocks.title=Blockbereiche +dockingManagerOperating.panel_components.title=Komponenten +dockingManagerOperating.panel_layers.title=Ebenen +dockingManagerOperating.panel_layerGroups.title=Ebenengruppen +dockingManagerOperating.panel_properties.title=Eigenschaften diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/layers.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/layers.properties new file mode 100644 index 0000000..be0f52c --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/layers.properties @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +layersTableModel.column_group.headerText=Group +layersTableModel.column_groupVisible.headerText=Group visible +layersTableModel.column_name.headerText=Name +layersTableModel.column_ordinal.headerText=Ordinal +layersTableModel.column_visible.headerText=Visible diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/layers_de.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/layers_de.properties new file mode 100644 index 0000000..0b51aaf --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/layers_de.properties @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +layersTableModel.column_group.headerText=Gruppe +layersTableModel.column_groupVisible.headerText=Gruppe sichtbar +layersTableModel.column_name.headerText=Name +layersTableModel.column_ordinal.headerText=Ordnungszahl +layersTableModel.column_visible.headerText=Sichtbar diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/orderSequences.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/orderSequences.properties new file mode 100644 index 0000000..4a52d77 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/orderSequences.properties @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +orderSequenceTableModel.column_complete.headerText=Complete +orderSequenceTableModel.column_executingVehicle.headerText=Executing vehicle +orderSequenceTableModel.column_failureFatal.headerText=Failure fatal +orderSequenceTableModel.column_finished.headerText=Finished +orderSequenceTableModel.column_intendedVehicle.determinedAutomatic.text=Determine automatic +orderSequenceTableModel.column_intendedVehicle.headerText=Intended vehicle +orderSequencesContainerPanel.button_filterFinishedOrderSequences.tooltipText=Filter for successful and completed transport order sequences (FINISHED). +orderSequencesContainerPanel.table_sequences.popupMenuItem_showDetails.text=Show details... diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/orderSequences_de.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/orderSequences_de.properties new file mode 100644 index 0000000..47cf21a --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/orderSequences_de.properties @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +orderSequenceTableModel.column_complete.headerText=Komplett +orderSequenceTableModel.column_executingVehicle.headerText=Ausf\u00fchrendes Fzg +orderSequenceTableModel.column_failureFatal.headerText=Fehlschlag fatal +orderSequenceTableModel.column_finished.headerText=Beendet +orderSequenceTableModel.column_intendedVehicle.determinedAutomatic.text=Automatisch bestimmen +orderSequenceTableModel.column_intendedVehicle.headerText=Gew\u00fcnschtes Fzg +orderSequencesContainerPanel.button_filterFinishedOrderSequences.tooltipText=Filter f\u00fcr erfolgreich abgeschlossene Transportauftragsketten (FINISHED). +orderSequencesContainerPanel.table_sequences.popupMenuItem_showDetails.text=Details anzeigen... diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/peripheralJobs.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/peripheralJobs.properties new file mode 100644 index 0000000..1652fb4 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/peripheralJobs.properties @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +peripheralJobTableModel.column_creationTime.headerText=Created +peripheralJobTableModel.column_location.headerText=Location +peripheralJobTableModel.column_name.headerText=Name +peripheralJobTableModel.column_operation.headerText=Operation +peripheralJobTableModel.column_relatedTransportOrder.headerText=Related transport order +peripheralJobTableModel.column_relatedVehicle.headerText=Related vehicle +peripheralJobTableModel.column_state.headerText=State +peripheralJobsContainerPanel.table_peripheralJobs.popupMenuItem_showDetails.text=Show details... +peripheralJobsContainerPanel.button_withdrawSelectedJobs.tooltipText=Withdraws the selected peripheral jobs. diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/peripheralJobs_de.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/peripheralJobs_de.properties new file mode 100644 index 0000000..8c80c61 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/peripheralJobs_de.properties @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +peripheralJobTableModel.column_creationTime.headerText=Erzeugt +peripheralJobTableModel.column_location.headerText=Station +peripheralJobTableModel.column_name.headerText=Name +peripheralJobTableModel.column_operation.headerText=Operation +peripheralJobTableModel.column_relatedTransportOrder.headerText=Zugeh\u00f6riger Transportauftrag +peripheralJobTableModel.column_relatedVehicle.headerText=Zugeh\u00f6riges Fahrzeug +peripheralJobTableModel.column_state.headerText=Status +peripheralJobsContainerPanel.table_peripheralJobs.popupMenuItem_showDetails.text=Details anzeigen... +peripheralJobsContainerPanel.button_withdrawSelectedJobs.tooltipText=Zieht die ausgew\u00e4hlten Peripheriejobs zur\u00fcck. diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/transportOrders.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/transportOrders.properties new file mode 100644 index 0000000..b076f20 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/transportOrders.properties @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +intendedVehiclesPanel.automatic_entry.text=Automatic +intendedVehiclesPanel.items_label.text=Vehicles: +transportOrderTableModel.column_creationTime.headerText=Created +transportOrderTableModel.column_destination.headerText=Target +transportOrderTableModel.column_executingVehicle.headerText=Executing vehicle +transportOrderTableModel.column_intendedVehicle.determinedAutomatic.text=Determine automatic +transportOrderTableModel.column_intendedVehicle.headerText=Intended vehicle +transportOrderTableModel.column_orderSequence.headerText=Sequence +transportOrderTableModel.column_source.headerText=Source +transportOrdersContainerPanel.button_filterDispatchableOrders.tooltipText=Filter for transport orders that are accepted but no vehicle has been assigned yet (Status = DISPATCHABLE). +transportOrdersContainerPanel.button_filterFailedOrders.tooltipText=Filter for failed transport orders (Status = FAILED). +transportOrdersContainerPanel.button_filterFinishedOrders.tooltipText=Filter for successful and completed transport orders (Status = FINISHED). +transportOrdersContainerPanel.button_filterProcessedOrders.tooltipText=Filter for transport orders that are currently being processed (Status = BEING_PROCESSED). +transportOrdersContainerPanel.button_filterRawOrders.tooltipText=Filter for transport orders that are in the initial state (Status = RAW). +transportOrdersContainerPanel.button_withdrawSelectedOrders.tooltipText=Withdraws the selected transport order from a vehicle. +transportOrdersContainerPanel.table_orders.popupMenuItem_setIntendedVehicle.error.message=There was an error setting the intended vehicle on the transport order. +transportOrdersContainerPanel.table_orders.popupMenuItem_setIntendedVehicle.error.title=Error setting intended vehicle +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.error.genericMessage=There was an error assigning the transport order to the intended vehicle. +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.error.transportOrderIntendedVehicleNotSet=The transport order does not have an intended vehicle. +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.error.transportOrderIsPartOfSequence=The transport order is part of an order sequence. +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.error.transportOrderStateInvalid=The transport order is not in state "DISPATCHABLE". +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.error.vehicleIntegrationLevelInvalid=The intended vehicle is not in integration level "TO_BE_UTILIZED". +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.error.vehicleIsProcessingOrderSequence=The intended vehicle is busy processing an order sequence. +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.error.vehicleProcessingStateInvalid=The intended vehicle is already processing a transport order. +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.error.vehiclePositionUnknown=The intended vehicle's position is unknown. +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.error.vehicleStateInvalid=The intended vehicle's reported state is neither "IDLE" nor "CHARGING". +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.error.title=Error assigning transport order +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.text=Assign to intended vehicle immediately +transportOrdersContainerPanel.table_orders.popupMenuItem_copyOrder.text=Copy +transportOrdersContainerPanel.table_orders.popupMenuItem_orderAsTemplate.text=As template +transportOrdersContainerPanel.table_orders.popupMenuItem_setIntendedVehicle.text=Set intended vehicle... +transportOrdersContainerPanel.table_orders.popupMenuItem_showDetails.text=Show details... diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/transportOrders_de.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/transportOrders_de.properties new file mode 100644 index 0000000..3eae949 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/transportOrders_de.properties @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +intendedVehiclesPanel.automatic_entry.text=Automatisch +intendedVehiclesPanel.items_label.text=Fahrzeuge: +transportOrderTableModel.column_creationTime.headerText=Erzeugt +transportOrderTableModel.column_destination.headerText=Ziel +transportOrderTableModel.column_executingVehicle.headerText=Ausf\u00fchrendes Fahrzeug +transportOrderTableModel.column_intendedVehicle.determinedAutomatic.text=Automatisch bestimmen +transportOrderTableModel.column_intendedVehicle.headerText=Gew\u00fcnschtes Fahrzeug +transportOrderTableModel.column_orderSequence.headerText=Sequenz +transportOrderTableModel.column_source.headerText=Quelle +transportOrdersContainerPanel.button_filterDispatchableOrders.tooltipText=Filter f\u00fcr Transportauftr\u00e4ge, die angenommen sind, denen aber noch kein Fahrzeug zugewiesen wurde (Status = DISPATCHABLE). +transportOrdersContainerPanel.button_filterFailedOrders.tooltipText=Filter f\u00fcr fehlgeschlagene Transportauftr\u00e4ge (Status = FAILED). +transportOrdersContainerPanel.button_filterFinishedOrders.tooltipText=Filter f\u00fcr erfolgreich abgeschlossene Transportauftr\u00e4ge (Filter = FINISHED). +transportOrdersContainerPanel.button_filterProcessedOrders.tooltipText=Filter f\u00fcr Transportauftr\u00e4ge, die gerade bearbeitet werden (Status = BEING_PROCESSED). +transportOrdersContainerPanel.button_filterRawOrders.tooltipText=Filter f\u00fcr Transportauftr\u00e4ge, die sich im Initialzustand befinden (Status = RAW). +transportOrdersContainerPanel.button_withdrawSelectedOrders.tooltipText=Entzieht einem Fahrzeug den selektierten Transportauftrag. +transportOrdersContainerPanel.table_orders.popupMenuItem_setIntendedVehicle.error.message=Es ist ein Fehler beim Setzen des gew\u00fcnschten Fahrzeuges aufgetreten. +transportOrdersContainerPanel.table_orders.popupMenuItem_setIntendedVehicle.error.title=Fehler beim Setzen des gew\u00fcnschten Fahrzeuges +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.error.genericMessage=Es ist ein Fehler beim Zuweisen an das gew\u00fcnschte Fahrzeug aufgetreten. +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.error.transportOrderIntendedVehicleNotSet=Der Transportauftrag hat kein gew\u00fcnschtes Fahrzeug. +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.error.transportOrderIsPartOfSequence=Der Transportauftrag ist Teil einer Auftragssequenz. +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.error.transportOrderStateInvalid=Der Transportauftrag ist nicht im Zustand "DISPATCHABLE". +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.error.vehicleIntegrationLevelInvalid=Das gew\u00fcnschte Fahrzeug ist nicht in der Integrationsstufe "TO_BE_UTILIZED". +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.error.vehicleIsProcessingOrderSequence=Das gew\u00fcnschte Fahrzeug bearbeitet gerade eine Auftragssequenz. +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.error.vehicleProcessingStateInvalid=Das gew\u00fcnschte Fahrzeug bearbeitet gerade einen anderen Auftrag. +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.error.vehiclePositionUnknown=Die Position des gew\u00fcnschten Fahrzeugs ist unbekannt. +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.error.vehicleStateInvalid=Der gemeldete Zustand des gew\u00fcnschten Fahrzeugs ist weder Leerlauf (IDLE) noch Laden (CHARGING). +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.error.title=Fehler beim Zuweisen an das gew\u00fcnschte Fahrzeug +transportOrdersContainerPanel.table_orders.popupMenuItem_assignToVehicle.text=Sofort ge\u00fcnschtem Fahrzeug zuweisen +transportOrdersContainerPanel.table_orders.popupMenuItem_copyOrder.text=Kopieren +transportOrdersContainerPanel.table_orders.popupMenuItem_orderAsTemplate.text=Als Vorlage +transportOrdersContainerPanel.table_orders.popupMenuItem_setIntendedVehicle.text=Gew\u00fcnschtes Fahrzeug setzen... +transportOrdersContainerPanel.table_orders.popupMenuItem_showDetails.text=Details anzeigen... diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/userNotifications.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/userNotifications.properties new file mode 100644 index 0000000..2f48c99 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/userNotifications.properties @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +userNotificationsContainerPanel.table_notifications.popupMenuItem_showDetails.text=Show details... +userNotificationTableModel.column_time.headerText=Timestamp +userNotificationTableModel.column_level.headerText=Level +userNotificationTableModel.column_source.headerText=Source +userNotificationTableModel.column_text.headerText=Text \ No newline at end of file diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/userNotifications_de.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/userNotifications_de.properties new file mode 100644 index 0000000..d48f264 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/userNotifications_de.properties @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +userNotificationsContainerPanel.table_notifications.popupMenuItem_showDetails.text=Details anzeigen... +userNotificationTableModel.column_time.headerText=Erzeugt +userNotificationTableModel.column_level.headerText=Stufe +userNotificationTableModel.column_source.headerText=Quelle +userNotificationTableModel.column_text.headerText=Text \ No newline at end of file diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/vehicleView.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/vehicleView.properties new file mode 100644 index 0000000..45f1efb --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/vehicleView.properties @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +singleVehicleView.label_destination.text=Destination: +singleVehicleView.label_integrated.text=Integrated: +singleVehicleView.label_integratedState.fully.text=Fully +singleVehicleView.label_integratedState.no.text=No +singleVehicleView.label_integratedState.partially.text=Partially +singleVehicleView.label_position.text=Position: +singleVehicleView.label_state.text=State: +vehiclesPanel.title=Vehicles diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/vehicleView_de.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/vehicleView_de.properties new file mode 100644 index 0000000..4d1e83c --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/panels/vehicleView_de.properties @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +singleVehicleView.label_destination.text=Ziel: +singleVehicleView.label_integrated.text=Integriert: +singleVehicleView.label_integratedState.fully.text=Vollst\u00e4ndig +singleVehicleView.label_integratedState.no.text=Nein +singleVehicleView.label_integratedState.partially.text=Teilweise +vehiclesPanel.title=Fahrzeuge diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/system.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/system.properties new file mode 100644 index 0000000..4f0c7b1 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/system.properties @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +openTcsSdiApplication.frameTitle_connectedTo.text=Connected to: diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/system_de.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/system_de.properties new file mode 100644 index 0000000..3571670 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/system_de.properties @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +openTcsSdiApplication.frameTitle_connectedTo.text=Verbunden mit: diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/toolbar.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/toolbar.properties new file mode 100644 index 0000000..fc97b28 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/toolbar.properties @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +pauseAllVehiclesAction.name=Pause vehicles +pauseAllVehiclesAction.shortDescription=Pause all vehicles (speed = 0) +toolBarManager.button_dragTool.tooltipText=Moves the model view +toolBarManager.button_selectionTool.tooltipText=Selects a component +toolBarManager.toolbar_drawing.title=Draw +resumeAllVehiclesAction.name=Resume vehicles +resumeAllVehiclesAction.shortDescription=Resume all vehicles (speed = 100) diff --git a/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/toolbar_de.properties b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/toolbar_de.properties new file mode 100644 index 0000000..af10429 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/i18n/org/opentcs/plantoverview/operating/toolbar_de.properties @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +pauseAllVehiclesAction.name=Fahrzeuge pausieren +pauseAllVehiclesAction.shortDescription=Alle Fahrzeuge pausieren (Geschwindigkeit = 0) +toolBarManager.button_dragTool.tooltipText=Verschiebt die Modellansicht +toolBarManager.button_selectionTool.tooltipText=W\u00e4hlt eine Komponente aus +toolBarManager.toolbar_drawing.title=Zeichnen +resumeAllVehiclesAction.name=Fahrzeuge fortsetzen +resumeAllVehiclesAction.shortDescription=Alle Fahrzeuge fortsetzen (Geschwindigkeit = 100) diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/menu/document-close-4.png b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/menu/document-close-4.png new file mode 100644 index 0000000..063f2f6 Binary files /dev/null and b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/menu/document-close-4.png differ diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/menu/help-contents.png b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/menu/help-contents.png new file mode 100644 index 0000000..a6a986e Binary files /dev/null and b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/menu/help-contents.png differ diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/openTCS/openTCS.300x132.gif b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/openTCS/openTCS.300x132.gif new file mode 100644 index 0000000..17a9130 Binary files /dev/null and b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/openTCS/openTCS.300x132.gif differ diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/battery-060-2.png b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/battery-060-2.png new file mode 100644 index 0000000..3357ff0 Binary files /dev/null and b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/battery-060-2.png differ diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/battery-100-2.png b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/battery-100-2.png new file mode 100644 index 0000000..0ac6460 Binary files /dev/null and b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/battery-100-2.png differ diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/battery-caution-3.png b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/battery-caution-3.png new file mode 100644 index 0000000..5923a53 Binary files /dev/null and b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/battery-caution-3.png differ diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/filterActivated.16x16.gif b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/filterActivated.16x16.gif new file mode 100644 index 0000000..7e441fb Binary files /dev/null and b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/filterActivated.16x16.gif differ diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/filterFailed.16x16.gif b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/filterFailed.16x16.gif new file mode 100644 index 0000000..c18b59c Binary files /dev/null and b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/filterFailed.16x16.gif differ diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/filterFinished.16x16.gif b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/filterFinished.16x16.gif new file mode 100644 index 0000000..96e83da Binary files /dev/null and b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/filterFinished.16x16.gif differ diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/filterProcessing.16x16.gif b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/filterProcessing.16x16.gif new file mode 100644 index 0000000..b4ae7db Binary files /dev/null and b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/filterProcessing.16x16.gif differ diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/filterRaw.16x16.gif b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/filterRaw.16x16.gif new file mode 100644 index 0000000..4bb4697 Binary files /dev/null and b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/filterRaw.16x16.gif differ diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/table-row-delete-2.16x16.png b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/table-row-delete-2.16x16.png new file mode 100644 index 0000000..70bbad6 Binary files /dev/null and b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/panel/table-row-delete-2.16x16.png differ diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/create-order.22.png b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/create-order.22.png new file mode 100644 index 0000000..d0fa81e Binary files /dev/null and b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/create-order.22.png differ diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/cursor-opened-hand.png b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/cursor-opened-hand.png new file mode 100644 index 0000000..70fd2ff Binary files /dev/null and b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/cursor-opened-hand.png differ diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/find-vehicle.22.png b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/find-vehicle.22.png new file mode 100644 index 0000000..f5e8e1f Binary files /dev/null and b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/find-vehicle.22.png differ diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/pause-vehicles.16.png b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/pause-vehicles.16.png new file mode 100644 index 0000000..0d4df1f Binary files /dev/null and b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/pause-vehicles.16.png differ diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/pause-vehicles.22.png b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/pause-vehicles.22.png new file mode 100644 index 0000000..dd197ac Binary files /dev/null and b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/pause-vehicles.22.png differ diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/resume-vehicles.16.png b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/resume-vehicles.16.png new file mode 100644 index 0000000..1ca87f2 Binary files /dev/null and b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/resume-vehicles.16.png differ diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/resume-vehicles.22.png b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/resume-vehicles.22.png new file mode 100644 index 0000000..4cff997 Binary files /dev/null and b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/resume-vehicles.22.png differ diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/select-2.png b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/select-2.png new file mode 100644 index 0000000..ae01970 Binary files /dev/null and b/opentcs-operationsdesk/src/main/resources/org/opentcs/guing/res/symbols/toolbar/select-2.png differ diff --git a/opentcs-operationsdesk/src/main/resources/org/opentcs/operationsdesk/distribution/config/opentcs-operationsdesk-defaults-baseline.properties b/opentcs-operationsdesk/src/main/resources/org/opentcs/operationsdesk/distribution/config/opentcs-operationsdesk-defaults-baseline.properties new file mode 100644 index 0000000..3e9ee36 --- /dev/null +++ b/opentcs-operationsdesk/src/main/resources/org/opentcs/operationsdesk/distribution/config/opentcs-operationsdesk-defaults-baseline.properties @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +# This file contains default configuration values and should not be modified. +# To adjust the application configuration, override values in a separate file. + +operationsdesk.locale = en +operationsdesk.connectionBookmarks = Localhost|localhost|1099 +operationsdesk.useBookmarksWhenConnecting = true +operationsdesk.frameMaximized = false +operationsdesk.frameBoundsHeight = 768 +operationsdesk.frameBoundsWidth = 1024 +operationsdesk.frameBoundsX = 0 +operationsdesk.frameBoundsY = 0 +operationsdesk.locationThemeClass = org.opentcs.guing.plugins.themes.DefaultLocationTheme +operationsdesk.ignoreVehicleOrientationAngle = false +operationsdesk.ignoreVehiclePrecisePosition = false +operationsdesk.vehicleThemeClass = org.opentcs.guing.plugins.themes.StatefulImageVehicleTheme +operationsdesk.userNotificationDisplayCount = 50 +operationsdesk.allowForcedWithdrawal = true + +ssl.enable = false +ssl.truststoreFile = ./config/truststore.p12 +ssl.truststorePassword = password + +continuousloadpanel.enable = true +resourceallocationpanel.enable = true +statisticspanel.enable = true diff --git a/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.addAllocatingVehicleForLocations.approved.txt b/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.addAllocatingVehicleForLocations.approved.txt new file mode 100644 index 0000000..d891cf1 --- /dev/null +++ b/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.addAllocatingVehicleForLocations.approved.txt @@ -0,0 +1,2 @@ +Location Location-0001

    Reservation token:
    Peripheral state:
    Processing state:
    Peripheral job:
    +Allocated by: Vehicle-001
    \ No newline at end of file diff --git a/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.addAllocatingVehicleForPaths.approved.txt b/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.addAllocatingVehicleForPaths.approved.txt new file mode 100644 index 0000000..2284d2d --- /dev/null +++ b/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.addAllocatingVehicleForPaths.approved.txt @@ -0,0 +1,2 @@ +Path Path-0001
    +Allocated by: Vehicle-001
    \ No newline at end of file diff --git a/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.addAllocatingVehicleForPoints.approved.txt b/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.addAllocatingVehicleForPoints.approved.txt new file mode 100644 index 0000000..336d645 --- /dev/null +++ b/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.addAllocatingVehicleForPoints.approved.txt @@ -0,0 +1,2 @@ +Point Point-0001
    +Allocated by: Vehicle-001
    \ No newline at end of file diff --git a/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.java b/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.java new file mode 100644 index 0000000..cd934f3 --- /dev/null +++ b/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.java @@ -0,0 +1,270 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.operationsdesk.components.drawing.figures; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; +import org.approvaltests.Approvals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.data.peripherals.PeripheralOperation; +import org.opentcs.guing.base.AllocationState; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.operationsdesk.peripherals.jobs.PeripheralJobsContainer; + +/** + * Unit tests for {@link ToolTipTextGeneratorOperationsDesk}. + */ +class ToolTipTextGeneratorOperationsDeskTest { + + private ToolTipTextGeneratorOperationsDesk toolTipTextGenerator; + + private PeripheralJobsContainer peripheralJobsContainer; + + private Vehicle vehicle; + private VehicleModel vehicleModel; + private Location location; + private LocationType locationType; + + @BeforeEach + void setup() { + Locale.setDefault(Locale.forLanguageTag("en")); + + SystemModel systemModel = mock(SystemModel.class); + when(systemModel.getBlockModels()).thenReturn(new ArrayList<>()); + + ModelManager modelManager = mock(ModelManager.class); + when(modelManager.getModel()).thenReturn(systemModel); + + peripheralJobsContainer = mock(PeripheralJobsContainer.class); + toolTipTextGenerator = new ToolTipTextGeneratorOperationsDesk( + modelManager, + peripheralJobsContainer + ); + + vehicle = new Vehicle("Vehicle-001"); + vehicleModel = new VehicleModel(); + vehicleModel.setVehicle(vehicle); + vehicleModel.setName(vehicle.getName()); + + locationType = new LocationType("Loc-Type-001"); + location = new Location("Location-001", locationType.getReference()); + } + + @Test + void listNoPeripheralJobs() { + Approvals.verify(toolTipTextGenerator.getToolTipText(vehicleModel)); + } + + @Test + void listOnlyPeripheralJobsRelatedToVehicle() { + PeripheralJob relatedJob = new PeripheralJob( + "Job-name-001", + "ReservationToken-01", + new PeripheralOperation( + location.getReference(), + "PeripheralOperation", + PeripheralOperation.ExecutionTrigger.AFTER_MOVEMENT, + true + ) + ).withRelatedVehicle(vehicle.getReference()); + PeripheralJob unrelatedJob = new PeripheralJob( + "Job-name-002", + "ReservationToken-02", + new PeripheralOperation( + location.getReference(), + "PeripheralOperation", + PeripheralOperation.ExecutionTrigger.AFTER_MOVEMENT, + true + ) + ).withRelatedVehicle(new Vehicle("Other Vehicle").getReference()); + PeripheralJob jobWithNoRelatedVehicle = new PeripheralJob( + "Job-name-003", + "ReservationToken-02", + new PeripheralOperation( + location.getReference(), + "PeripheralOperation", + PeripheralOperation.ExecutionTrigger.AFTER_MOVEMENT, + true + ) + ); + + when(peripheralJobsContainer.getPeripheralJobs()) + .thenReturn(Arrays.asList(relatedJob, unrelatedJob, jobWithNoRelatedVehicle)); + + Approvals.verify(toolTipTextGenerator.getToolTipText(vehicleModel)); + } + + @Test + void listOnlyPeripheralJobsWithCompletionRequired() { + PeripheralJob completionRequiredJob = new PeripheralJob( + "Job-name-001", + "ReservationToken-01", + new PeripheralOperation( + location.getReference(), + "PeripheralOperation", + PeripheralOperation.ExecutionTrigger.AFTER_MOVEMENT, + true + ) + ).withRelatedVehicle(vehicle.getReference()); + PeripheralJob completionNotequiredJob = new PeripheralJob( + "Job-name-002", + "ReservationToken-02", + new PeripheralOperation( + location.getReference(), + "PeripheralOperation", + PeripheralOperation.ExecutionTrigger.AFTER_MOVEMENT, + false + ) + ).withRelatedVehicle(vehicle.getReference()); + + when(peripheralJobsContainer.getPeripheralJobs()) + .thenReturn(Arrays.asList(completionRequiredJob, completionNotequiredJob)); + + Approvals.verify(toolTipTextGenerator.getToolTipText(vehicleModel)); + } + + @Test + public void listOnlyPeripheralJobsInNonFinalState() { + PeripheralJob job1 = new PeripheralJob( + "Approved-job-01", + "ReservationToken-01", + new PeripheralOperation( + location.getReference(), + "PeripheralOperation", + PeripheralOperation.ExecutionTrigger.AFTER_MOVEMENT, + true + ) + ).withRelatedVehicle(vehicle.getReference()) + .withState(PeripheralJob.State.TO_BE_PROCESSED); + PeripheralJob job2 = new PeripheralJob( + "Approved-job-02", + "ReservationToken-02", + new PeripheralOperation( + location.getReference(), + "PeripheralOperation", + PeripheralOperation.ExecutionTrigger.AFTER_MOVEMENT, + true + ) + ).withRelatedVehicle(vehicle.getReference()) + .withState(PeripheralJob.State.BEING_PROCESSED); + PeripheralJob job3 = new PeripheralJob( + "Rejected-job-01", + "ReservationToken-02", + new PeripheralOperation( + location.getReference(), + "PeripheralOperation", + PeripheralOperation.ExecutionTrigger.AFTER_MOVEMENT, + true + ) + ).withRelatedVehicle(vehicle.getReference()) + .withState(PeripheralJob.State.FAILED); + PeripheralJob job4 = new PeripheralJob( + "Rejected-job-02", + "ReservationToken-02", + new PeripheralOperation( + location.getReference(), + "PeripheralOperation", + PeripheralOperation.ExecutionTrigger.AFTER_MOVEMENT, + true + ) + ).withRelatedVehicle(vehicle.getReference()) + .withState(PeripheralJob.State.FINISHED); + + when(peripheralJobsContainer.getPeripheralJobs()) + .thenReturn(Arrays.asList(job1, job2, job3, job4)); + + Approvals.verify(toolTipTextGenerator.getToolTipText(vehicleModel)); + } + + @Test + void sortPeripheralJobsByCreationTime() { + PeripheralJob firstJob = new PeripheralJob( + "This one first", + "ReservationToken-01", + new PeripheralOperation( + location.getReference(), + "PeripheralOperation", + PeripheralOperation.ExecutionTrigger.AFTER_MOVEMENT, + true + ) + ).withRelatedVehicle(vehicle.getReference()) + .withCreationTime(Instant.ofEpochSecond(10)); + PeripheralJob secondJob = new PeripheralJob( + "This one second", + "ReservationToken-02", + new PeripheralOperation( + location.getReference(), + "PeripheralOperation", + PeripheralOperation.ExecutionTrigger.AFTER_MOVEMENT, + true + ) + ).withRelatedVehicle(vehicle.getReference()) + .withCreationTime(Instant.ofEpochSecond(999)); + + when(peripheralJobsContainer.getPeripheralJobs()) + .thenReturn(Arrays.asList(secondJob, firstJob)); + + Approvals.verify(toolTipTextGenerator.getToolTipText(vehicleModel)); + } + + @Test + void addAllocatingVehicleForPoints() { + Vehicle vehicle2 = new Vehicle("Vehicle-002"); + VehicleModel vehicleModel2 = new VehicleModel(); + vehicleModel2.setVehicle(vehicle2); + vehicleModel2.setName(vehicle2.getName()); + + PointModel point = new PointModel(); + point.setName("Point-0001"); + point.updateAllocationState(vehicleModel, AllocationState.ALLOCATED); + point.updateAllocationState(vehicleModel2, AllocationState.CLAIMED); + + Approvals.verify(toolTipTextGenerator.getToolTipText(point)); + } + + @Test + void addAllocatingVehicleForPaths() { + Vehicle vehicle2 = new Vehicle("Vehicle-002"); + VehicleModel vehicleModel2 = new VehicleModel(); + vehicleModel2.setVehicle(vehicle2); + vehicleModel2.setName(vehicle2.getName()); + + PathModel path = new PathModel(); + path.setName("Path-0001"); + path.updateAllocationState(vehicleModel, AllocationState.ALLOCATED); + path.updateAllocationState(vehicleModel2, AllocationState.CLAIMED); + + Approvals.verify(toolTipTextGenerator.getToolTipText(path)); + } + + @Test + void addAllocatingVehicleForLocations() { + Vehicle vehicle2 = new Vehicle("Vehicle-002"); + VehicleModel vehicleModel2 = new VehicleModel(); + vehicleModel2.setVehicle(vehicle2); + vehicleModel2.setName(vehicle2.getName()); + + LocationModel location = new LocationModel(); + location.setName("Location-0001"); + location.updateAllocationState(vehicleModel, AllocationState.ALLOCATED); + location.updateAllocationState(vehicleModel2, AllocationState.CLAIMED); + + Approvals.verify(toolTipTextGenerator.getToolTipText(location)); + } + +} diff --git a/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.listNoPeripheralJobs.approved.txt b/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.listNoPeripheralJobs.approved.txt new file mode 100644 index 0000000..262c7a0 --- /dev/null +++ b/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.listNoPeripheralJobs.approved.txt @@ -0,0 +1,19 @@ + +Vehicle Vehicle-001 +
    +Current point: ? +
    +Next point: ? +
    +State: UNKNOWN +
    +Processing state: IDLE +
    +Integration level: TO_BE_RESPECTED +
    +Current energy level: 0% +
    +Peripheral operations that need to be waited for: +
      +
    + diff --git a/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.listOnlyPeripheralJobsInNonFinalState.approved.txt b/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.listOnlyPeripheralJobsInNonFinalState.approved.txt new file mode 100644 index 0000000..92a7e34 --- /dev/null +++ b/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.listOnlyPeripheralJobsInNonFinalState.approved.txt @@ -0,0 +1,21 @@ + +Vehicle Vehicle-001 +
    +Current point: ? +
    +Next point: ? +
    +State: UNKNOWN +
    +Processing state: IDLE +
    +Integration level: TO_BE_RESPECTED +
    +Current energy level: 0% +
    +Peripheral operations that need to be waited for: +
      +
    • ⧖Location-001: PeripheralOperation (Approved-job-01)
    • +
    • ⏵Location-001: PeripheralOperation (Approved-job-02)
    • +
    + diff --git a/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.listOnlyPeripheralJobsRelatedToVehicle.approved.txt b/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.listOnlyPeripheralJobsRelatedToVehicle.approved.txt new file mode 100644 index 0000000..ca61cd2 --- /dev/null +++ b/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.listOnlyPeripheralJobsRelatedToVehicle.approved.txt @@ -0,0 +1,20 @@ + +Vehicle Vehicle-001 +
    +Current point: ? +
    +Next point: ? +
    +State: UNKNOWN +
    +Processing state: IDLE +
    +Integration level: TO_BE_RESPECTED +
    +Current energy level: 0% +
    +Peripheral operations that need to be waited for: +
      +
    • ⧖Location-001: PeripheralOperation (Job-name-001)
    • +
    + diff --git a/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.listOnlyPeripheralJobsWithCompletionRequired.approved.txt b/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.listOnlyPeripheralJobsWithCompletionRequired.approved.txt new file mode 100644 index 0000000..ca61cd2 --- /dev/null +++ b/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.listOnlyPeripheralJobsWithCompletionRequired.approved.txt @@ -0,0 +1,20 @@ + +Vehicle Vehicle-001 +
    +Current point: ? +
    +Next point: ? +
    +State: UNKNOWN +
    +Processing state: IDLE +
    +Integration level: TO_BE_RESPECTED +
    +Current energy level: 0% +
    +Peripheral operations that need to be waited for: +
      +
    • ⧖Location-001: PeripheralOperation (Job-name-001)
    • +
    + diff --git a/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.sortPeripheralJobsByCreationTime.approved.txt b/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.sortPeripheralJobsByCreationTime.approved.txt new file mode 100644 index 0000000..bf50a21 --- /dev/null +++ b/opentcs-operationsdesk/src/test/java/org/opentcs/operationsdesk/components/drawing/figures/ToolTipTextGeneratorOperationsDeskTest.sortPeripheralJobsByCreationTime.approved.txt @@ -0,0 +1,21 @@ + +Vehicle Vehicle-001 +
    +Current point: ? +
    +Next point: ? +
    +State: UNKNOWN +
    +Processing state: IDLE +
    +Integration level: TO_BE_RESPECTED +
    +Current energy level: 0% +
    +Peripheral operations that need to be waited for: +
      +
    • ⧖Location-001: PeripheralOperation (This one first)
    • +
    • ⧖Location-001: PeripheralOperation (This one second)
    • +
    + diff --git a/opentcs-peripheralcommadapter-loopback/build.gradle b/opentcs-peripheralcommadapter-loopback/build.gradle new file mode 100644 index 0000000..f6741e7 --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/build.gradle @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply from: "${rootDir}/gradle/java-project.gradle" +apply from: "${rootDir}/gradle/java-codequality.gradle" +apply from: "${rootDir}/gradle/guice-project.gradle" +apply from: "${rootDir}/gradle/publishing-java.gradle" + +dependencies { + api project(':opentcs-api-injection') + api project(':opentcs-common') +} + +task release { + dependsOn build +} diff --git a/opentcs-peripheralcommadapter-loopback/gradle.properties b/opentcs-peripheralcommadapter-loopback/gradle.properties new file mode 100644 index 0000000..14431ac --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/gradle.properties @@ -0,0 +1,40 @@ +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineMethodParams=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAfterDotInChainedMethodCalls=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineDisjunctiveCatchTypes=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineFor=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineImplements=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.sortMembersByVisibility=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.visibilityOrder=PUBLIC;PROTECTED;DEFAULT;PRIVATE +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.enable-indent=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineArrayInit=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineCallArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.keepGettersAndSettersTogether=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classMembersOrder=STATIC FIELD;FIELD;STATIC_INIT;CONSTRUCTOR;INSTANCE_INIT;STATIC METHOD;METHOD;STATIC CLASS;CLASS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapCommentText=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.importGroupsOrder=* +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize=4 +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineAnnotationArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineTryResources=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.preserveNewLinesInComments=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineParenthesized=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineThrows=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=100 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project diff --git a/opentcs-peripheralcommadapter-loopback/src/guiceConfig/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralControlCenterModule.java b/opentcs-peripheralcommadapter-loopback/src/guiceConfig/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralControlCenterModule.java new file mode 100644 index 0000000..b7ce702 --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/guiceConfig/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralControlCenterModule.java @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.commadapter.peripheral.loopback; + +import com.google.inject.assistedinject.FactoryModuleBuilder; +import org.opentcs.customizations.controlcenter.ControlCenterInjectionModule; + +/** + * Loopback adapter-specific Gucie configuration for the Kernel Control Center. + */ +public class LoopbackPeripheralControlCenterModule + extends + ControlCenterInjectionModule { + + /** + * Creates a new instance. + */ + public LoopbackPeripheralControlCenterModule() { + } + + @Override + protected void configure() { + install( + new FactoryModuleBuilder().build(LoopbackPeripheralAdapterPanelComponentsFactory.class) + ); + + peripheralCommAdapterPanelFactoryBinder() + .addBinding().to(LoopbackPeripheralCommAdapterPanelFactory.class); + } +} diff --git a/opentcs-peripheralcommadapter-loopback/src/guiceConfig/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralKernelModule.java b/opentcs-peripheralcommadapter-loopback/src/guiceConfig/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralKernelModule.java new file mode 100644 index 0000000..fd35f29 --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/guiceConfig/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralKernelModule.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.commadapter.peripheral.loopback; + +import com.google.inject.assistedinject.FactoryModuleBuilder; +import org.opentcs.customizations.kernel.KernelInjectionModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Loopback adapter-specific Gucie configuration for the Kernel. + */ +public class LoopbackPeripheralKernelModule + extends + KernelInjectionModule { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(LoopbackPeripheralKernelModule.class); + + /** + * Creates a new instance. + */ + public LoopbackPeripheralKernelModule() { + } + + @Override + protected void configure() { + VirtualPeripheralConfiguration configuration + = getConfigBindingProvider().get( + VirtualPeripheralConfiguration.PREFIX, + VirtualPeripheralConfiguration.class + ); + + if (!configuration.enable()) { + LOG.info("Peripheral loopback driver disabled by configuration."); + return; + } + + bind(VirtualPeripheralConfiguration.class) + .toInstance(configuration); + + install(new FactoryModuleBuilder().build(LoopbackPeripheralAdapterComponentsFactory.class)); + + // tag::documentation_createCommAdapterModule[] + peripheralCommAdaptersBinder().addBinding().to(LoopbackPeripheralCommAdapterFactory.class); + // end::documentation_createCommAdapterModule[] + } +} diff --git a/opentcs-peripheralcommadapter-loopback/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.controlcenter.ControlCenterInjectionModule b/opentcs-peripheralcommadapter-loopback/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.controlcenter.ControlCenterInjectionModule new file mode 100644 index 0000000..3b51128 --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.controlcenter.ControlCenterInjectionModule @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT + +org.opentcs.commadapter.peripheral.loopback.LoopbackPeripheralControlCenterModule diff --git a/opentcs-peripheralcommadapter-loopback/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.kernel.KernelInjectionModule b/opentcs-peripheralcommadapter-loopback/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.kernel.KernelInjectionModule new file mode 100644 index 0000000..4a16f88 --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.kernel.KernelInjectionModule @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT + +org.opentcs.commadapter.peripheral.loopback.LoopbackPeripheralKernelModule diff --git a/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/I18nLoopbackPeripheralCommAdapter.java b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/I18nLoopbackPeripheralCommAdapter.java new file mode 100644 index 0000000..6fd7b89 --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/I18nLoopbackPeripheralCommAdapter.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.commadapter.peripheral.loopback; + +/** + * Defines constants regarding internationalization. + */ +public interface I18nLoopbackPeripheralCommAdapter { + + /** + * The path to the project's resource bundle. + */ + String BUNDLE_PATH = "i18n/org/opentcs/commadapter/peripheral/loopback/Bundle"; +} diff --git a/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LocationProperties.java b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LocationProperties.java new file mode 100644 index 0000000..fe1714c --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LocationProperties.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.commadapter.peripheral.loopback; + +/** + */ +public interface LocationProperties { + + /** + * The key of the location property indicating that the location represents a peripheral device + * and that it should be attached to the loopback peripheral communication adapter. + */ + String PROPKEY_LOOPBACK_PERIPHERAL = "tcs:loopbackPeripheral"; +} diff --git a/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralAdapterComponentsFactory.java b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralAdapterComponentsFactory.java new file mode 100644 index 0000000..ec08200 --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralAdapterComponentsFactory.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.commadapter.peripheral.loopback; + +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; + +/** + * A factory for various loopback specific instances. + */ +public interface LoopbackPeripheralAdapterComponentsFactory { + + /** + * Creates a new loopback communication adapter for the given location. + * + * @param location The location. + * @return A loopback communication adapter instance. + */ + LoopbackPeripheralCommAdapter createLoopbackCommAdapter(TCSResourceReference location); +} diff --git a/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralAdapterPanelComponentsFactory.java b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralAdapterPanelComponentsFactory.java new file mode 100644 index 0000000..d2032ea --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralAdapterPanelComponentsFactory.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.commadapter.peripheral.loopback; + +/** + * A factory for creating various comm adapter panel-specific instances. + */ +public interface LoopbackPeripheralAdapterPanelComponentsFactory { + + /** + * Creates a {@link LoopbackPeripheralCommAdapterPanel} representing the given process model's + * content. + * + * @param processModel The process model to represent. + * @return The comm adapter panel. + */ + LoopbackPeripheralCommAdapterPanel createPanel(LoopbackPeripheralProcessModel processModel); +} diff --git a/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralCommAdapter.java b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralCommAdapter.java new file mode 100644 index 0000000..71d2bdf --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralCommAdapter.java @@ -0,0 +1,230 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.commadapter.peripheral.loopback; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkState; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.time.Duration; +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.PeripheralInformation; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.drivers.peripherals.BasicPeripheralCommAdapter; +import org.opentcs.drivers.peripherals.PeripheralAdapterCommand; +import org.opentcs.drivers.peripherals.PeripheralCommAdapter; +import org.opentcs.drivers.peripherals.PeripheralJobCallback; +import org.opentcs.drivers.peripherals.PeripheralProcessModel; +import org.opentcs.drivers.peripherals.management.PeripheralProcessModelEvent; +import org.opentcs.util.ExplainedBoolean; +import org.opentcs.util.event.EventHandler; + +/** + * A {@link PeripheralCommAdapter} implementation that is doing nothing. + */ +public class LoopbackPeripheralCommAdapter + extends + BasicPeripheralCommAdapter { + + /** + * The time it takes for loopback peripherals to process a job. + */ + private static final Duration JOB_PROCESSING_DURATION = Duration.ofSeconds(10); + /** + * The kernel's executor. + */ + private final ScheduledExecutorService kernelExecutor; + /** + * The queue of tasks to be executed to simulate the processing of jobs. + * This queue may contain at most one item at any time. + */ + private final Queue jobTaskQueue = new ArrayDeque<>(); + /** + * A future for the current job task's execution. + * Indicates whehter the current job task has been executed or whether it is still to be executed. + */ + private Future currentJobFuture; + /** + * Whether the execution of jobs should fail. + */ + private boolean failJobs; + + /** + * Creates a new instance. + * + * @param location The reference to the location this adapter is attached to. + * @param eventHandler The handler used to send events to. + * @param kernelExecutor The kernel's executor. + */ + @Inject + public LoopbackPeripheralCommAdapter( + @Assisted + TCSResourceReference location, + @ApplicationEventBus + EventHandler eventHandler, + @KernelExecutor + ScheduledExecutorService kernelExecutor + ) { + super(new LoopbackPeripheralProcessModel(location), eventHandler); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + super.initialize(); + + setProcessModel(getProcessModel().withState(PeripheralInformation.State.IDLE)); + sendProcessModelChangedEvent(PeripheralProcessModel.Attribute.STATE); + } + + @Override + public LoopbackPeripheralProcessModel getProcessModel() { + return (LoopbackPeripheralProcessModel) super.getProcessModel(); + } + + @Override + public ExplainedBoolean canProcess(PeripheralJob job) { + if (!isEnabled()) { + return new ExplainedBoolean(false, "Comm adapter not enabled."); + } + else if (hasJobWaitingToBeProcessed()) { + return new ExplainedBoolean(false, "Busy processing another job."); + } + + return new ExplainedBoolean(true, ""); + } + + @Override + public void process(PeripheralJob job, PeripheralJobCallback callback) { + ExplainedBoolean canProcess = canProcess(job); + checkState( + canProcess.getValue(), + "%s: Can't process job: %s", + getProcessModel().getLocation().getName(), + canProcess.getReason() + ); + + jobTaskQueue.add(() -> { + if (failJobs) { + callback.peripheralJobFailed(job.getReference()); + } + else { + callback.peripheralJobFinished(job.getReference()); + } + setProcessModel(getProcessModel().withState(PeripheralInformation.State.IDLE)); + sendProcessModelChangedEvent(PeripheralProcessModel.Attribute.STATE); + }); + + setProcessModel(getProcessModel().withState(PeripheralInformation.State.EXECUTING)); + sendProcessModelChangedEvent(PeripheralProcessModel.Attribute.STATE); + + if (!getProcessModel().isManualModeEnabled()) { + currentJobFuture = kernelExecutor.schedule( + jobTaskQueue.poll(), + JOB_PROCESSING_DURATION.getSeconds(), + TimeUnit.SECONDS + ); + } + } + + @Override + public void abortJob() { + jobTaskQueue.clear(); + if (currentJobFuture != null) { + currentJobFuture.cancel(false); + } + setProcessModel(getProcessModel().withState(PeripheralInformation.State.IDLE)); + sendProcessModelChangedEvent(PeripheralProcessModel.Attribute.STATE); + } + + @Override + public void execute(PeripheralAdapterCommand command) { + command.execute(this); + } + + @Override + protected void connectPeripheral() { + } + + @Override + protected void disconnectPeripheral() { + } + + public void enableManualMode(boolean enabled) { + kernelExecutor.submit(() -> { + LoopbackPeripheralProcessModel oldProcessModel = getProcessModel(); + setProcessModel(getProcessModel().withManualModeEnabled(enabled)); + + if (oldProcessModel.isManualModeEnabled() && !getProcessModel().isManualModeEnabled()) { + // Job processing mode changed to "automatic". If there's a task that has not yet been + // processed while the processing mode was set to "manual", make sure the task is executed + // and schedule it for execution on the kernel executor. + if (!jobTaskQueue.isEmpty()) { + currentJobFuture = kernelExecutor.submit(jobTaskQueue.poll()); + } + } + + sendProcessModelChangedEvent(LoopbackPeripheralProcessModel.Attribute.MANUAL_MODE_ENABLED); + }); + } + + public void updateState(PeripheralInformation.State state) { + kernelExecutor.submit(() -> { + setProcessModel(getProcessModel().withState(state)); + sendProcessModelChangedEvent(PeripheralProcessModel.Attribute.STATE); + }); + } + + public void triggerJobProcessing(boolean failJob) { + if (!getProcessModel().isManualModeEnabled()) { + return; + } + + if (jobTaskQueue.isEmpty() || !isCurrentJobFutureDone()) { + // There's no job to be processed or we are not yet done processing the current one. + return; + } + + currentJobFuture = kernelExecutor.submit(() -> { + failJobs = failJob; + jobTaskQueue.poll().run(); + failJobs = false; + }); + } + + private boolean hasJobWaitingToBeProcessed() { + return !jobTaskQueue.isEmpty() || !isCurrentJobFutureDone(); + } + + private boolean isCurrentJobFutureDone() { + if (currentJobFuture == null) { + return true; + } + + return currentJobFuture.isDone(); + } + + private void sendProcessModelChangedEvent( + LoopbackPeripheralProcessModel.Attribute attributeChanged + ) { + getEventHandler().onEvent( + new PeripheralProcessModelEvent( + getProcessModel().getLocation(), + attributeChanged.name(), + getProcessModel() + ) + ); + } +} diff --git a/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralCommAdapterDescription.java b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralCommAdapterDescription.java new file mode 100644 index 0000000..0155dc0 --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralCommAdapterDescription.java @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.commadapter.peripheral.loopback; + +import static org.opentcs.commadapter.peripheral.loopback.I18nLoopbackPeripheralCommAdapter.BUNDLE_PATH; + +import java.util.ResourceBundle; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; + +/** + * A {@link PeripheralCommAdapterDescription} for no comm adapter. + */ +public class LoopbackPeripheralCommAdapterDescription + extends + PeripheralCommAdapterDescription { + + /** + * Creates a new instance. + */ + public LoopbackPeripheralCommAdapterDescription() { + } + + @Override + public String getDescription() { + return ResourceBundle.getBundle(BUNDLE_PATH) + .getString("loopbackPeripheralCommAdapterDescription.description"); + } +} diff --git a/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralCommAdapterFactory.java b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralCommAdapterFactory.java new file mode 100644 index 0000000..4fe7cbe --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralCommAdapterFactory.java @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.commadapter.peripheral.loopback; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.data.model.Location; +import org.opentcs.drivers.peripherals.PeripheralCommAdapter; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterFactory; + +/** + * A factory for loopback communication adapters (virtual peripherals). + */ +public class LoopbackPeripheralCommAdapterFactory + implements + PeripheralCommAdapterFactory { + + /** + * The adapter components factory. + */ + private final LoopbackPeripheralAdapterComponentsFactory componentsFactory; + /** + * Indicates whether this component is initialized or not. + */ + private boolean initialized; + + @Inject + public LoopbackPeripheralCommAdapterFactory( + LoopbackPeripheralAdapterComponentsFactory componentsFactory + ) { + this.componentsFactory = requireNonNull(componentsFactory, "componentsFactory"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + initialized = false; + } + + @Override + public PeripheralCommAdapterDescription getDescription() { + return new LoopbackPeripheralCommAdapterDescription(); + } + + @Override + public boolean providesAdapterFor(Location location) { + requireNonNull(location, "location"); + return location.getProperties().containsKey(LocationProperties.PROPKEY_LOOPBACK_PERIPHERAL); + } + + @Override + public PeripheralCommAdapter getAdapterFor(Location location) { + requireNonNull(location, "location"); + return componentsFactory.createLoopbackCommAdapter(location.getReference()); + } +} diff --git a/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralCommAdapterPanel.form b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralCommAdapterPanel.form new file mode 100644 index 0000000..9b5edb5 --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralCommAdapterPanel.form @@ -0,0 +1,193 @@ + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralCommAdapterPanel.java b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralCommAdapterPanel.java new file mode 100644 index 0000000..0591d20 --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralCommAdapterPanel.java @@ -0,0 +1,299 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.commadapter.peripheral.loopback; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.event.ItemEvent; +import java.util.Arrays; +import javax.swing.DefaultComboBoxModel; +import javax.swing.SwingUtilities; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.commadapter.peripheral.loopback.commands.EnableManualModeCommand; +import org.opentcs.commadapter.peripheral.loopback.commands.FinishJobProcessingCommand; +import org.opentcs.commadapter.peripheral.loopback.commands.SetStateCommand; +import org.opentcs.customizations.ServiceCallWrapper; +import org.opentcs.data.model.PeripheralInformation; +import org.opentcs.drivers.peripherals.PeripheralAdapterCommand; +import org.opentcs.drivers.peripherals.PeripheralProcessModel; +import org.opentcs.drivers.peripherals.management.PeripheralCommAdapterPanel; +import org.opentcs.util.CallWrapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The panel for the loopback peripheral communication adapter. + */ +public class LoopbackPeripheralCommAdapterPanel + extends + PeripheralCommAdapterPanel { + + /** + * This class's logger. + */ + private static final Logger LOG + = LoggerFactory.getLogger(LoopbackPeripheralCommAdapterPanel.class); + /** + * The service portal to use. + */ + private final KernelServicePortal servicePortal; + /** + * The call wrapper to use for service calls. + */ + private final CallWrapper callWrapper; + /** + * The comm adapter's process model. + */ + private LoopbackPeripheralProcessModel processModel; + + @Inject + @SuppressWarnings("this-escape") + public LoopbackPeripheralCommAdapterPanel( + @Assisted + LoopbackPeripheralProcessModel processModel, + KernelServicePortal servicePortal, + @ServiceCallWrapper + CallWrapper callWrapper + ) { + this.processModel = requireNonNull(processModel, "processModel"); + this.servicePortal = requireNonNull(servicePortal, "servicePortal"); + this.callWrapper = requireNonNull(callWrapper, "callWrapper"); + + initComponents(); + updateComponentsEnabled(); + updateComponentContents(); + } + + @Override + public void processModelChanged(PeripheralProcessModel processModel) { + requireNonNull(processModel, "processModel"); + if (!(processModel instanceof LoopbackPeripheralProcessModel)) { + return; + } + + SwingUtilities.invokeLater(() -> { + this.processModel = (LoopbackPeripheralProcessModel) processModel; + updateComponentsEnabled(); + updateComponentContents(); + }); + } + + private void updateComponentsEnabled() { + boolean enabled = processModel.isCommAdapterEnabled(); + stateComboBox.setEnabled(enabled); + finishCurrentJobButton.setEnabled(processModel.isManualModeEnabled()); + failCurrentJobButton.setEnabled(processModel.isManualModeEnabled()); + } + + private void updateComponentContents() { + stateComboBox.setSelectedItem(processModel.getState()); + manualModeRadioButton.setSelected(processModel.isManualModeEnabled()); + automaticModeRadioButton.setSelected(!processModel.isManualModeEnabled()); + } + + private void sendCommAdapterCommand(PeripheralAdapterCommand command) { + try { + callWrapper.call( + () -> servicePortal.getPeripheralService() + .sendCommAdapterCommand(processModel.getLocation(), command) + ); + } + catch (Exception ex) { + LOG.warn("Error sending comm adapter command '{}'", command, ex); + } + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always regenerated + * by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + statePanel = new javax.swing.JPanel(); + stateLabel = new javax.swing.JLabel(); + stateComboBox = new javax.swing.JComboBox<>(); + jobProcessingPanel = new javax.swing.JPanel(); + automaticModeRadioButton = new javax.swing.JRadioButton(); + manualModeRadioButton = new javax.swing.JRadioButton(); + finishCurrentJobButton = new javax.swing.JButton(); + failCurrentJobButton = new javax.swing.JButton(); + + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/commadapter/peripheral/loopback/Bundle"); // NOI18N + statePanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("loopbackPeripheralCommAdapterPanel.panel_state.border.title"))); // NOI18N + statePanel.setName("statePanel"); // NOI18N + statePanel.setLayout(new java.awt.GridBagLayout()); + + stateLabel.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + stateLabel.setText(bundle.getString("loopbackPeripheralCommAdapterPanel.label_state.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 0); + statePanel.add(stateLabel, gridBagConstraints); + + stateComboBox.setModel(new DefaultComboBoxModel<>(Arrays.asList(PeripheralInformation.State.values()).stream().filter(state -> state != PeripheralInformation.State.NO_PERIPHERAL).toArray(PeripheralInformation.State[]::new))); + stateComboBox.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + stateComboBoxItemStateChanged(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + statePanel.add(stateComboBox, gridBagConstraints); + + jobProcessingPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("loopbackPeripheralCommAdapterPanel.panel_jobProcessing.border.title"))); // NOI18N + jobProcessingPanel.setLayout(new java.awt.GridBagLayout()); + + automaticModeRadioButton.setSelected(true); + automaticModeRadioButton.setText(bundle.getString("loopbackPeripheralCommAdapterPanel.radioButton_jobProcessingAutomatic.text")); // NOI18N + automaticModeRadioButton.setBorder(javax.swing.BorderFactory.createEmptyBorder(0, 0, 0, 0)); + automaticModeRadioButton.setMargin(new java.awt.Insets(0, 0, 0, 0)); + automaticModeRadioButton.setName("automaticModeRadioButton"); // NOI18N + automaticModeRadioButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + automaticModeRadioButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + jobProcessingPanel.add(automaticModeRadioButton, gridBagConstraints); + + manualModeRadioButton.setText(bundle.getString("loopbackPeripheralCommAdapterPanel.radioButton_jobProcessingManual.text")); // NOI18N + manualModeRadioButton.setBorder(javax.swing.BorderFactory.createEmptyBorder(0, 0, 0, 0)); + manualModeRadioButton.setMargin(new java.awt.Insets(0, 0, 0, 0)); + manualModeRadioButton.setName("manualModeRadioButton"); // NOI18N + manualModeRadioButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + manualModeRadioButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 0); + jobProcessingPanel.add(manualModeRadioButton, gridBagConstraints); + + finishCurrentJobButton.setText(bundle.getString("loopbackPeripheralCommAdapterPanel.button_finishCurrentJob.text")); // NOI18N + finishCurrentJobButton.setEnabled(false); + finishCurrentJobButton.setName("finishCurrentJobButton"); // NOI18N + finishCurrentJobButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + finishCurrentJobButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 3); + jobProcessingPanel.add(finishCurrentJobButton, gridBagConstraints); + + failCurrentJobButton.setText(bundle.getString("loopbackPeripheralCommAdapterPanel.button_failCurrentJob.text")); // NOI18N + failCurrentJobButton.setEnabled(false); + failCurrentJobButton.setName("processCurrentJobButton"); // NOI18N + failCurrentJobButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + failCurrentJobButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 3); + jobProcessingPanel.add(failCurrentJobButton, gridBagConstraints); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(statePanel, javax.swing.GroupLayout.DEFAULT_SIZE, 194, Short.MAX_VALUE) + .addComponent(jobProcessingPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addContainerGap(170, Short.MAX_VALUE)) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(statePanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jobProcessingPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap(165, Short.MAX_VALUE)) + ); + + getAccessibleContext().setAccessibleName(bundle.getString("loopbackPeripheralCommAdapterPanel.accessibleName")); // NOI18N + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void manualModeRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_manualModeRadioButtonActionPerformed + sendCommAdapterCommand(new EnableManualModeCommand(manualModeRadioButton.isSelected())); + }//GEN-LAST:event_manualModeRadioButtonActionPerformed + + private void automaticModeRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_automaticModeRadioButtonActionPerformed + sendCommAdapterCommand(new EnableManualModeCommand(!manualModeRadioButton.isSelected())); + }//GEN-LAST:event_automaticModeRadioButtonActionPerformed + + private void finishCurrentJobButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_finishCurrentJobButtonActionPerformed + sendCommAdapterCommand(new FinishJobProcessingCommand(false)); + }//GEN-LAST:event_finishCurrentJobButtonActionPerformed + + private void stateComboBoxItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_stateComboBoxItemStateChanged + if (evt.getStateChange() != ItemEvent.SELECTED) { + return; + } + + PeripheralInformation.State selectedState + = (PeripheralInformation.State) stateComboBox.getSelectedItem(); + if (selectedState == processModel.getState()) { + // If the selection has changed due to an update in the process model (i.e. the user has not + // selected an item in this panel's combo box), we don't want to send a set state command. + return; + } + + sendCommAdapterCommand( + new SetStateCommand((PeripheralInformation.State) stateComboBox.getSelectedItem()) + ); + }//GEN-LAST:event_stateComboBoxItemStateChanged + + private void failCurrentJobButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_failCurrentJobButtonActionPerformed + sendCommAdapterCommand(new FinishJobProcessingCommand(true)); + }//GEN-LAST:event_failCurrentJobButtonActionPerformed + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JRadioButton automaticModeRadioButton; + private javax.swing.JButton failCurrentJobButton; + private javax.swing.JButton finishCurrentJobButton; + private javax.swing.JPanel jobProcessingPanel; + private javax.swing.JRadioButton manualModeRadioButton; + private javax.swing.JComboBox stateComboBox; + private javax.swing.JLabel stateLabel; + private javax.swing.JPanel statePanel; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralCommAdapterPanelFactory.java b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralCommAdapterPanelFactory.java new file mode 100644 index 0000000..8ce0a43 --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralCommAdapterPanelFactory.java @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.commadapter.peripheral.loopback; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription; +import org.opentcs.drivers.peripherals.PeripheralProcessModel; +import org.opentcs.drivers.peripherals.management.PeripheralCommAdapterPanel; +import org.opentcs.drivers.peripherals.management.PeripheralCommAdapterPanelFactory; + +/** + * A factory for creating {@link LoopbackPeripheralCommAdapterPanel} instances. + */ +public class LoopbackPeripheralCommAdapterPanelFactory + implements + PeripheralCommAdapterPanelFactory { + + /** + * The panel components factory to use. + */ + private final LoopbackPeripheralAdapterPanelComponentsFactory panelComponentsFactory; + /** + * Indicates whether this component is initialized or not. + */ + private boolean initialized; + + @Inject + public LoopbackPeripheralCommAdapterPanelFactory( + LoopbackPeripheralAdapterPanelComponentsFactory panelComponentsFactory + ) { + this.panelComponentsFactory = requireNonNull(panelComponentsFactory, "panelComponentsFactory"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + initialized = false; + } + + @Override + public List getPanelsFor( + @Nonnull + PeripheralCommAdapterDescription description, + @Nonnull + TCSResourceReference location, + @Nonnull + PeripheralProcessModel processModel + ) { + requireNonNull(description, "description"); + requireNonNull(location, "location"); + requireNonNull(processModel, "processModel"); + + if (!providesPanelsFor(description, processModel)) { + return new ArrayList<>(); + } + + return Arrays.asList( + panelComponentsFactory + .createPanel((LoopbackPeripheralProcessModel) processModel) + ); + } + + private boolean providesPanelsFor( + PeripheralCommAdapterDescription description, + PeripheralProcessModel processModel + ) { + return (description instanceof LoopbackPeripheralCommAdapterDescription) + && (processModel instanceof LoopbackPeripheralProcessModel); + } +} diff --git a/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralProcessModel.java b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralProcessModel.java new file mode 100644 index 0000000..a6a8aab --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/LoopbackPeripheralProcessModel.java @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.commadapter.peripheral.loopback; + +import java.io.Serializable; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.PeripheralInformation; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.drivers.peripherals.PeripheralProcessModel; + +/** + * The process model for the loopback peripheral communication adapter. + */ +public class LoopbackPeripheralProcessModel + extends + PeripheralProcessModel + implements + Serializable { + + /** + * Whether the peripheral device is manual mode (i.e. the user triggers the job execution). + */ + private final boolean manualModeEnabled; + + public LoopbackPeripheralProcessModel(TCSResourceReference location) { + this(location, false, false, PeripheralInformation.State.UNKNOWN, false); + } + + private LoopbackPeripheralProcessModel( + TCSResourceReference location, + boolean commAdapterEnabled, + boolean commAdapterConnected, + PeripheralInformation.State state, + boolean manualModeEnabled + ) { + super(location, commAdapterEnabled, commAdapterConnected, state); + this.manualModeEnabled = manualModeEnabled; + } + + @Override + public LoopbackPeripheralProcessModel withLocation(TCSResourceReference location) { + return new LoopbackPeripheralProcessModel( + location, + isCommAdapterEnabled(), + isCommAdapterConnected(), + getState(), + manualModeEnabled + ); + } + + @Override + public LoopbackPeripheralProcessModel withCommAdapterEnabled(boolean commAdapterEnabled) { + return new LoopbackPeripheralProcessModel( + getLocation(), + commAdapterEnabled, + isCommAdapterConnected(), + getState(), + manualModeEnabled + ); + } + + @Override + public LoopbackPeripheralProcessModel withCommAdapterConnected(boolean commAdapterConnected) { + return new LoopbackPeripheralProcessModel( + getLocation(), + isCommAdapterEnabled(), + commAdapterConnected, + getState(), + manualModeEnabled + ); + } + + @Override + public LoopbackPeripheralProcessModel withState(PeripheralInformation.State state) { + return new LoopbackPeripheralProcessModel( + getLocation(), + isCommAdapterEnabled(), + isCommAdapterConnected(), + state, + manualModeEnabled + ); + } + + /** + * Returns whether the peripheral device is manual mode (i.e. the user triggers the job + * execution). + * + * @return Whether the peripheral device is manual mode. + */ + public boolean isManualModeEnabled() { + return manualModeEnabled; + } + + /** + * Creates a copy of the object, with the given value. + * + * @param manualModeEnabled The value to be set in the copy. + * @return A copy of this object, differing in the given value. + */ + public LoopbackPeripheralProcessModel withManualModeEnabled(boolean manualModeEnabled) { + return new LoopbackPeripheralProcessModel( + getLocation(), + isCommAdapterEnabled(), + isCommAdapterConnected(), + getState(), + manualModeEnabled + ); + } + + /** + * Used to describe what has changed in a process model. + */ + public enum Attribute { + /** + * Indicates a change of the manual mode enabled property. + */ + MANUAL_MODE_ENABLED, + } +} diff --git a/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/VirtualPeripheralConfiguration.java b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/VirtualPeripheralConfiguration.java new file mode 100644 index 0000000..e5246a4 --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/VirtualPeripheralConfiguration.java @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.commadapter.peripheral.loopback; + +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; + +/** + * Provides methods to configure to {@link LoopbackPeripheralCommAdapter}. + */ +@ConfigurationPrefix(VirtualPeripheralConfiguration.PREFIX) +public interface VirtualPeripheralConfiguration { + + /** + * This configuration's prefix. + */ + String PREFIX = "virtualperipheral"; + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to enable to register/enable the peripheral loopback driver.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_enable" + ) + boolean enable(); +} diff --git a/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/commands/EnableManualModeCommand.java b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/commands/EnableManualModeCommand.java new file mode 100644 index 0000000..bff6590 --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/commands/EnableManualModeCommand.java @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.commadapter.peripheral.loopback.commands; + +import org.opentcs.commadapter.peripheral.loopback.LoopbackPeripheralCommAdapter; +import org.opentcs.drivers.peripherals.PeripheralAdapterCommand; +import org.opentcs.drivers.peripherals.PeripheralCommAdapter; + +/** + * A command to enable/disable the comm adapter's manual mode. + */ +public class EnableManualModeCommand + implements + PeripheralAdapterCommand { + + /** + * Whether to enable/disable manual mode. + */ + private final boolean enabled; + + /** + * Creates a new instance. + * + * @param enabled Whether to enable/disable manual mode. + */ + public EnableManualModeCommand(boolean enabled) { + this.enabled = enabled; + } + + @Override + public void execute(PeripheralCommAdapter adapter) { + if (!(adapter instanceof LoopbackPeripheralCommAdapter)) { + return; + } + + LoopbackPeripheralCommAdapter loopbackAdapter = (LoopbackPeripheralCommAdapter) adapter; + loopbackAdapter.enableManualMode(enabled); + } +} diff --git a/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/commands/FinishJobProcessingCommand.java b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/commands/FinishJobProcessingCommand.java new file mode 100644 index 0000000..238573b --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/commands/FinishJobProcessingCommand.java @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.commadapter.peripheral.loopback.commands; + +import org.opentcs.commadapter.peripheral.loopback.LoopbackPeripheralCommAdapter; +import org.opentcs.drivers.peripherals.PeripheralAdapterCommand; +import org.opentcs.drivers.peripherals.PeripheralCommAdapter; + +/** + * A command to trigger the comm adapter in manual mode and finish the job the simulated peripheral + * device is currently processing. + */ +public class FinishJobProcessingCommand + implements + PeripheralAdapterCommand { + + /** + * Whether to fail the execution of the job the (simulated) peripheral device is currently + * processing. + */ + private final boolean failJob; + + /** + * Creates a new instance. + * + * @param failJob Whether to fail the execution of the job the (simulated) peripheral device is + * currently processing. + */ + public FinishJobProcessingCommand(boolean failJob) { + this.failJob = failJob; + } + + @Override + public void execute(PeripheralCommAdapter adapter) { + if (!(adapter instanceof LoopbackPeripheralCommAdapter)) { + return; + } + + LoopbackPeripheralCommAdapter loopbackAdapter = (LoopbackPeripheralCommAdapter) adapter; + loopbackAdapter.triggerJobProcessing(failJob); + } +} diff --git a/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/commands/SetStateCommand.java b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/commands/SetStateCommand.java new file mode 100644 index 0000000..0508b0a --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/main/java/org/opentcs/commadapter/peripheral/loopback/commands/SetStateCommand.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.commadapter.peripheral.loopback.commands; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import org.opentcs.commadapter.peripheral.loopback.LoopbackPeripheralCommAdapter; +import org.opentcs.data.model.PeripheralInformation; +import org.opentcs.drivers.peripherals.PeripheralAdapterCommand; +import org.opentcs.drivers.peripherals.PeripheralCommAdapter; + +/** + * A command to set the peripheral device's state. + */ +public class SetStateCommand + implements + PeripheralAdapterCommand { + + /** + * The peripheral device state to set. + */ + private final PeripheralInformation.State state; + + /** + * Creates a new instance. + * + * @param state The peripheral device state to set. + */ + public SetStateCommand( + @Nonnull + PeripheralInformation.State state + ) { + this.state = requireNonNull(state, "state"); + } + + @Override + public void execute(PeripheralCommAdapter adapter) { + if (!(adapter instanceof LoopbackPeripheralCommAdapter)) { + return; + } + + LoopbackPeripheralCommAdapter loopbackAdapter = (LoopbackPeripheralCommAdapter) adapter; + loopbackAdapter.updateState(state); + } +} diff --git a/opentcs-peripheralcommadapter-loopback/src/main/resources/i18n/org/opentcs/commadapter/peripheral/loopback/Bundle.properties b/opentcs-peripheralcommadapter-loopback/src/main/resources/i18n/org/opentcs/commadapter/peripheral/loopback/Bundle.properties new file mode 100644 index 0000000..ec0d224 --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/main/resources/i18n/org/opentcs/commadapter/peripheral/loopback/Bundle.properties @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +loopbackPeripheralCommAdapterPanel.radioButton_jobProcessingManual.text=Manual mode +loopbackPeripheralCommAdapterPanel.radioButton_jobProcessingAutomatic.text=Automatic mode +loopbackPeripheralCommAdapterPanel.panel_state.border.title=Current state +loopbackPeripheralCommAdapterPanel.panel_jobProcessing.border.title=Job processing +loopbackPeripheralCommAdapterPanel.label_state.text=State: +loopbackPeripheralCommAdapterPanel.label_pausePeripheral.text=Pause peripheral: +loopbackPeripheralCommAdapterPanel.button_failCurrentJob.text=Fail current job +loopbackPeripheralCommAdapterPanel.button_finishCurrentJob.text=Finish current job +loopbackPeripheralCommAdapterPanel.accessibleName=Loopback options +loopbackPeripheralCommAdapterDescription.description=Loopback adapter (virtual peripheral) diff --git a/opentcs-peripheralcommadapter-loopback/src/main/resources/i18n/org/opentcs/commadapter/peripheral/loopback/Bundle_de.properties b/opentcs-peripheralcommadapter-loopback/src/main/resources/i18n/org/opentcs/commadapter/peripheral/loopback/Bundle_de.properties new file mode 100644 index 0000000..825ace0 --- /dev/null +++ b/opentcs-peripheralcommadapter-loopback/src/main/resources/i18n/org/opentcs/commadapter/peripheral/loopback/Bundle_de.properties @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +loopbackPeripheralCommAdapterPanel.radioButton_jobProcessingManual.text=Manuell +loopbackPeripheralCommAdapterPanel.radioButton_jobProcessingAutomatic.text=Automatik +loopbackPeripheralCommAdapterPanel.panel_state.border.title=Aktueller Zustand +loopbackPeripheralCommAdapterPanel.panel_jobProcessing.border.title=Auftragsbearbeitung +loopbackPeripheralCommAdapterPanel.label_state.text=Zustand: +loopbackPeripheralCommAdapterPanel.label_pausePeripheral.text=Peripherie anhalten: +loopbackPeripheralCommAdapterPanel.button_failCurrentJob.text=Aktuellen Auftrag fehlschlagen lassen +loopbackPeripheralCommAdapterPanel.button_finishCurrentJob.text=Aktuellen Auftrag abschlie\u00dfen +loopbackPeripheralCommAdapterPanel.accessibleName=Loopback-Optionen +loopbackPeripheralCommAdapterDescription.description=Loopback-Treiber (virtuelle Peripherie) diff --git a/opentcs-plantoverview-base/build.gradle b/opentcs-plantoverview-base/build.gradle new file mode 100644 index 0000000..825217f --- /dev/null +++ b/opentcs-plantoverview-base/build.gradle @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply from: "${rootDir}/gradle/java-project.gradle" +apply from: "${rootDir}/gradle/java-codequality.gradle" +apply from: "${rootDir}/gradle/guice-project.gradle" +apply from: "${rootDir}/gradle/publishing-java.gradle" + +dependencies { + api project(':opentcs-api-base') +} + +task release { + dependsOn build +} diff --git a/opentcs-plantoverview-base/gradle.properties b/opentcs-plantoverview-base/gradle.properties new file mode 100644 index 0000000..14431ac --- /dev/null +++ b/opentcs-plantoverview-base/gradle.properties @@ -0,0 +1,40 @@ +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineMethodParams=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAfterDotInChainedMethodCalls=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineDisjunctiveCatchTypes=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineFor=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineImplements=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.sortMembersByVisibility=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.visibilityOrder=PUBLIC;PROTECTED;DEFAULT;PRIVATE +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.enable-indent=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineArrayInit=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineCallArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.keepGettersAndSettersTogether=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classMembersOrder=STATIC FIELD;FIELD;STATIC_INIT;CONSTRUCTOR;INSTANCE_INIT;STATIC METHOD;METHOD;STATIC CLASS;CLASS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapCommentText=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.importGroupsOrder=* +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize=4 +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineAnnotationArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineTryResources=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.preserveNewLinesInComments=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineParenthesized=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineThrows=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=100 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/AllocationState.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/AllocationState.java new file mode 100644 index 0000000..9b281b4 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/AllocationState.java @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base; + +/** + * Defines the allocation states a resource can be in. + */ +public enum AllocationState { + + /** + * The resource is claimed by a vehicle. + */ + CLAIMED, + /** + * The resource is allocated by a vehicle. + */ + ALLOCATED, + /** + * The resource is allocated by a vehicle but its related transport order is withdrawn. + */ + ALLOCATED_WITHDRAWN; +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/I18nPlantOverviewBase.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/I18nPlantOverviewBase.java new file mode 100644 index 0000000..3603755 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/I18nPlantOverviewBase.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base; + +/** + * Defines constants regarding internationalization. + */ +public interface I18nPlantOverviewBase { + + /** + * The path to the project's resource bundle. + */ + String BUNDLE_PATH = "i18n/org/opentcs/plantoverview/base/Bundle"; +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/layer/LayerWrapper.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/layer/LayerWrapper.java new file mode 100644 index 0000000..5a92c80 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/layer/LayerWrapper.java @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.layer; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import org.opentcs.data.model.visualization.Layer; +import org.opentcs.data.model.visualization.LayerGroup; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * Wraps a {@link Layer} instance and the {@link LayerGroup} instance that the layer is assigned to. + * Instances of this class are referenced by {@link ModelComponent}s. This allows multiple model + * components to be updated simultaneously with the update of only one layer wrapper. + */ +public class LayerWrapper { + + /** + * The layer. + */ + private Layer layer; + /** + * The layer group the layer is assigned to. + */ + private LayerGroup layerGroup; + + /** + * Creates a new instance. + * + * @param layer The layer. + * @param layerGroup The layer group the layer is assigned to. + */ + public LayerWrapper( + @Nonnull + Layer layer, + @Nonnull + LayerGroup layerGroup + ) { + this.layer = requireNonNull(layer, "layer"); + this.layerGroup = requireNonNull(layerGroup, "layerGroup"); + } + + /** + * Returns the layer. + * + * @return The layer. + */ + @Nonnull + public Layer getLayer() { + return layer; + } + + /** + * Sets the layer. + * + * @param layer The layer. + */ + public void setLayer( + @Nonnull + Layer layer + ) { + this.layer = requireNonNull(layer, "layer"); + } + + /** + * Returns the layer group the layer is assigned to. + * + * @return The layer group. + */ + public LayerGroup getLayerGroup() { + return layerGroup; + } + + /** + * Sets the layer group the layer is assigned to. + * + * @param layerGroup The layer group. + */ + public void setLayerGroup(LayerGroup layerGroup) { + this.layerGroup = layerGroup; + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/layer/NullLayerWrapper.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/layer/NullLayerWrapper.java new file mode 100644 index 0000000..a4ce05d --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/layer/NullLayerWrapper.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.layer; + +import org.opentcs.data.model.visualization.Layer; +import org.opentcs.data.model.visualization.LayerGroup; + +/** + * A null object for a layer wrapper. + */ +public class NullLayerWrapper + extends + LayerWrapper { + + public NullLayerWrapper() { + super(new Layer(0, 0, true, "null", 0), new LayerGroup(0, "null", true)); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/event/AttributesChangeEvent.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/event/AttributesChangeEvent.java new file mode 100644 index 0000000..2c593b5 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/event/AttributesChangeEvent.java @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.event; + +import java.util.EventObject; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * An event that notifies all {@link AttributesChangeListener}s that a model component has been + * changed. + */ +public class AttributesChangeEvent + extends + EventObject { + + /** + * The model. + */ + protected ModelComponent fModelComponent; + + /** + * Creates a new instance. + * + * @param listener The listener. + * @param model The model component. + */ + public AttributesChangeEvent(AttributesChangeListener listener, ModelComponent model) { + super(listener); + fModelComponent = model; + } + + /** + * @return the model. + */ + public ModelComponent getModel() { + return fModelComponent; + } + + /** + * @return the initiator. + */ + public AttributesChangeListener getInitiator() { + return (AttributesChangeListener) getSource(); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/event/AttributesChangeListener.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/event/AttributesChangeListener.java new file mode 100644 index 0000000..ad7c85a --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/event/AttributesChangeListener.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.event; + +/** + * Interface that controllers/views implement. + * + * @see AttributesChangeEvent + */ +public interface AttributesChangeListener { + + /** + * Event received when the model has been changed. + * + * @param e The event. + */ + void propertiesChanged(AttributesChangeEvent e); +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/event/NullAttributesChangeListener.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/event/NullAttributesChangeListener.java new file mode 100644 index 0000000..0cf8f2c --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/event/NullAttributesChangeListener.java @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.event; + +/** + * A PropertiesModelChangeListener that does nothing. + */ +public class NullAttributesChangeListener + implements + AttributesChangeListener { + + /** + * Creates a new instance of NullPropertiesModelChangeListener + */ + public NullAttributesChangeListener() { + } + + @Override + public void propertiesChanged(AttributesChangeEvent e) { + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/AbstractComplexProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/AbstractComplexProperty.java new file mode 100644 index 0000000..6649c12 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/AbstractComplexProperty.java @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import org.opentcs.guing.base.model.ModelComponent; + +/** + * Abstract super class for properties that should get their own details dialog + * for editing because editing in simple text fields or combo boxes is hard to + * realize or not comfortable for the user. + */ +public abstract class AbstractComplexProperty + extends + AbstractProperty { + + /** + * Creates a new instance. + * + * @param model The model component this property belongs to. + */ + public AbstractComplexProperty(ModelComponent model) { + super(model); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/AbstractModelAttribute.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/AbstractModelAttribute.java new file mode 100644 index 0000000..2806f7a --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/AbstractModelAttribute.java @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import org.opentcs.guing.base.model.ModelComponent; + +/** + * Attribute of a {@link ModelComponent}. + */ +public abstract class AbstractModelAttribute + implements + ModelAttribute { + + /** + * The model this attribute is attached to. + */ + private ModelComponent fModel; + /** + * Indicates that this attribute has changed. + */ + private ChangeState fChangeState = ChangeState.NOT_CHANGED; + /** + * Description of this attribute. + */ + private String fDescription = ""; + /** + * Tooltip text. + */ + private String fHelptext = ""; + /** + * Indicates whether or not this attribute can simultaneously be edited with other + * attributes of the same name of other model components. + */ + private boolean fCollectiveEditable; + /** + * Indicates whether or not this attribute can be changed in modeling mode. + */ + private boolean fModellingEditable = true; + /** + * Indicates whether or not this attribute can be changed in operating mode. + */ + private boolean fOperatingEditable; + + /** + * Creates a new instance. + * + * @param model The model component. + */ + public AbstractModelAttribute(ModelComponent model) { + fModel = model; + } + + @Override // ModelAttribute + public ModelComponent getModel() { + return fModel; + } + + @Override // ModelAttribute + public void setModel(ModelComponent model) { + fModel = model; + } + + @Override // ModelAttribute + public void markChanged() { + fChangeState = ChangeState.CHANGED; + } + + @Override // ModelAttribute + public void unmarkChanged() { + fChangeState = ChangeState.NOT_CHANGED; + } + + @Override // ModelAttribute + public void setChangeState(ChangeState state) { + fChangeState = state; + } + + /** + * Returns the change state of this attribute. + * + * @return The change state. + */ + public ChangeState getChangeState() { + return fChangeState; + } + + @Override // ModelAttribute + public boolean hasChanged() { + return (fChangeState != ChangeState.NOT_CHANGED); + } + + @Override // ModelAttribute + public void setDescription(String description) { + fDescription = description; + } + + @Override // ModelAttribute + public String getDescription() { + return fDescription; + } + + @Override // ModelAttribute + public void setHelptext(String helptext) { + fHelptext = helptext; + } + + @Override // ModelAttribute + public String getHelptext() { + return fHelptext; + } + + @Override // ModelAttribute + public void setCollectiveEditable(boolean collectiveEditable) { + fCollectiveEditable = collectiveEditable; + } + + @Override // ModelAttribute + public boolean isCollectiveEditable() { + return fCollectiveEditable; + } + + @Override // ModelAttribute + public void setModellingEditable(boolean editable) { + fModellingEditable = editable; + } + + @Override // ModelAttribute + public boolean isModellingEditable() { + return fModellingEditable; + } + + @Override // ModelAttribute + public void setOperatingEditable(boolean editable) { + fOperatingEditable = editable; + } + + @Override // ModelAttribute + public boolean isOperatingEditable() { + return fOperatingEditable; + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/AbstractProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/AbstractProperty.java new file mode 100644 index 0000000..7eeb0e2 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/AbstractProperty.java @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import org.opentcs.guing.base.model.ModelComponent; + +/** + * Base implementation for a property. + */ +public abstract class AbstractProperty + extends + AbstractModelAttribute + implements + Property { + + /** + * The value of this property. + */ + protected Object fValue; + + /** + * Creates a new instance. + * + * @param model The model component. + */ + public AbstractProperty(ModelComponent model) { + super(model); + } + + /** + * Sets the value. + * + * @param newValue The new value. + */ + public void setValue(Object newValue) { + fValue = newValue; + } + + /** + * Returns the value of this property. + * + * @return The value. + */ + public Object getValue() { + return fValue; + } + + @Override + public void copyFrom(Property property) { + } + + @Override + public Object clone() { + try { + return super.clone(); + } + catch (CloneNotSupportedException exc) { + throw new RuntimeException("Unexpected exception", exc); + } + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/AbstractQuantity.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/AbstractQuantity.java new file mode 100644 index 0000000..e35fd18 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/AbstractQuantity.java @@ -0,0 +1,436 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import static java.util.Objects.requireNonNull; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import org.opentcs.guing.base.model.ModelComponent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base implementation for properties having a value and a unit. + * (Examples: 1 s, 200 m, 30 m/s.) + * Also specifies conversion relations (see {@link Relation}) between units that allow conversion + * to other units. + * + * @param The enum type. + */ +public abstract class AbstractQuantity> + extends + AbstractProperty { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(AbstractQuantity.class); + /** + * A {@link ValidRangePair} indicating the range of the valid values + * for this quantity. + */ + protected ValidRangePair validRange = new ValidRangePair(); + /** + * The unit's enum class; + */ + private final Class fUnitClass; + /** + * List of possible units. + */ + private final List fPossibleUnits; + /** + * List of relations between different units. + */ + private final List> fRelations; + /** + * Current unit. + */ + private U fUnit; + /** + * Whether or not this property is an integer value. + */ + private boolean fIsInteger; + /** + * Whether or not this property is unsigned. + */ + private boolean fIsUnsigned; + + /** + * Creates a new instance. + * + * @param model The model component. + * @param value The value. + * @param unit The unit. + * @param unitClass The unit class. + * @param relations The relations. + */ + @SuppressWarnings("this-escape") + public AbstractQuantity( + ModelComponent model, double value, U unit, Class unitClass, + List> relations + ) { + super(model); + fUnit = requireNonNull(unit, "unit"); + fUnitClass = requireNonNull(unitClass, "unitClass"); + fPossibleUnits = Arrays.asList(unitClass.getEnumConstants()); + fRelations = requireNonNull(relations, "relations"); + fIsInteger = false; + fIsUnsigned = false; + initValidRange(); + setValue(value); + } + + /** + * Sets the new valid range. + * + * @param newRange The new {@link ValidRangePair}. + */ + public void setValidRangePair(ValidRangePair newRange) { + validRange = Objects.requireNonNull(newRange, "newRange is null"); + } + + /** + * Returns the valid range for this quantity. + * + * @return The {@link ValidRangePair}. + */ + public ValidRangePair getValidRange() { + return validRange; + } + + /** + * Initializes the valid range of values. + */ + protected abstract void initValidRange(); + + /** + * Sets the value of this property to be an integer or decimal number. + * + * @param isInteger Whether the value is an integer. + */ + public void setInteger(boolean isInteger) { + fIsInteger = isInteger; + } + + /** + * Returns true if the value of this property is an integer value. + * + * @return Whether the value is an integer. + */ + public boolean isInteger() { + return fIsInteger; + } + + /** + * Sets the value of this property to be unsigned or not. + * + * @param isUnsigned Whether the value is unsigned. + */ + public void setUnsigned(boolean isUnsigned) { + this.fIsUnsigned = isUnsigned; + } + + /** + * Indicates whether or not the value is unsigned or not. + * + * @return Whether the value is unsigned. + */ + public boolean isUnsigned() { + return fIsUnsigned; + } + + @Override + public Object getValue() { + try { + double value = Double.parseDouble(fValue.toString()); + + if (isInteger()) { + return (int) (value + 0.5); + } + else { + return value; + } + } + catch (NumberFormatException nfe) { + LOG.info("Error parsing value", nfe); + return fValue; + } + } + + /** + * Returns the value of this property converted to the specified unit. + * + * @param unit The unit return. + * @return The value by the given unit. + */ + public double getValueByUnit(U unit) { + try { + @SuppressWarnings("unchecked") + AbstractQuantity property = (AbstractQuantity) clone(); + // PercentProperty threw + // java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.Double + // this is a workaround 12.09.14 + double value; + if (isInteger()) { + value = ((Integer) getValue()).doubleValue(); + } + else { + value = (double) getValue(); + } + property.setValueAndUnit(value, getUnit()); + property.convertTo(unit); + if (isInteger()) { + value = ((Integer) property.getValue()).doubleValue(); + } + else { + value = (double) property.getValue(); + } + + return value; + } + catch (IllegalArgumentException e) { + LOG.error("Exception: ", e); + } + + return Double.NaN; + } + + /** + * Converts the property to the new unit. + * + * @param unit The new unit to use. + */ + public void convertTo(U unit) { + if (!fPossibleUnits.contains(unit)) { + return; + } + + if (fUnit.equals(unit)) { + return; + } + + int indexUnitA = fPossibleUnits.indexOf(fUnit); + int indexUnitB = fPossibleUnits.indexOf(unit); + + int lowerIndex; + int upperIndex; + if (indexUnitA < indexUnitB) { + lowerIndex = indexUnitA; + upperIndex = indexUnitB; + } + else { + lowerIndex = indexUnitB; + upperIndex = indexUnitA; + } + + double relationValue = 1.0; + + for (int i = lowerIndex; i < upperIndex; i++) { + U unitA = fPossibleUnits.get(i); + U unitB = fPossibleUnits.get(i + 1); + + Relation relation = findFittingRelation(unitA, unitB); + relationValue *= relation.relationValue(); + } + + U lowerUnit = fPossibleUnits.get(lowerIndex); + U upperUnit = fPossibleUnits.get(upperIndex); + + Relation relation = new Relation<>(lowerUnit, upperUnit, relationValue); + Relation.Operation operation = relation.getOperation(fUnit, unit); + + fUnit = unit; + + switch (operation) { + case DIVISION: + setValue((double) fValue / relation.relationValue()); + break; + case MULTIPLICATION: + setValue((double) fValue * relation.relationValue()); + break; + default: + throw new IllegalArgumentException("Unhandled operation: " + operation); + } + + } + + /** + * Returns the current unit for this property. + * + * @return The unit. + */ + public U getUnit() { + return fUnit; + } + + /** + * Checks if this property is applicable to the specified unit. + * + * @param unit The unit. + * @return {@code true}, if the given unit is a valid/possible one, otherwise {@code false}. + */ + public boolean isPossibleUnit(U unit) { + return fPossibleUnits.contains(unit); + } + + /** + * Checks if the given string is a valid/possible unit. + * + * @param unitString The unit as a string. + * @return {@code true}, if the given string is a valid/possible unit, otherwise {@code false}. + */ + public boolean isPossibleUnit(String unitString) { + for (U unit : fPossibleUnits) { + if (Objects.equals(unitString, unit.toString())) { + return true; + } + } + return false; + } + + /** + * Set the value and unit for this property. + * {@link IllegalArgumentException} is thrown if the unit is not applicable to this property. + * + * @param value The new value. + * @param unit The new unit. + * @throws IllegalArgumentException If the given unit is not usable. + */ + public void setValueAndUnit(double value, U unit) + throws IllegalArgumentException { + if (!isPossibleUnit(unit)) { + throw new IllegalArgumentException(String.format("'%s' is not a valid unit.", unit)); + } + if (!Double.isNaN(value)) { + if (fValue instanceof Double) { + if ((double) fValue != value) { + markChanged(); + } + } + else { + markChanged(); + } + } + + fUnit = unit; + + setValue(value); + + if (fIsUnsigned) { + setValue(Math.abs(value)); + } + } + + public void setValueAndUnit(double value, String unitString) + throws IllegalArgumentException { + requireNonNull(unitString); + + for (U unit : fUnitClass.getEnumConstants()) { + if (unitString.equals(unit.toString())) { + setValueAndUnit(value, unit); + return; + } + } + throw new IllegalArgumentException("Unknown unit \"" + unitString + "\""); + } + + @Override + public String toString() { + if (fValue instanceof Integer) { + return ((int) fValue) + " " + fUnit; + } + else if (fValue instanceof Double) { + return fValue + " " + fUnit; + } + else { + return fValue.toString(); + } + } + + /** + * Returns a list of possible units for this property. + * + * @return A list of possible units. + */ + public List getPossibleUnits() { + return fPossibleUnits; + } + + @Override + public void copyFrom(Property property) { + @SuppressWarnings("unchecked") + AbstractQuantity quantity = (AbstractQuantity) property; + + try { + if (quantity.getValue() instanceof Double) { + setValueAndUnit((double) quantity.getValue(), quantity.getUnit()); + } + else if (quantity.getValue() instanceof Integer) { + setValueAndUnit(((Integer) quantity.getValue()).doubleValue(), quantity.getUnit()); + } + } + catch (IllegalArgumentException e) { + LOG.error("Exception: ", e); + } + } + + /** + * Finds the conversion relation that is applicable for both specified units. + * + * @return the conversion relation for the units. + */ + private Relation findFittingRelation(U unitFrom, U unitTo) { + for (Relation relation : fRelations) { + if (relation.fits(unitFrom, unitTo)) { + return relation; + } + } + + return null; + } + + public class ValidRangePair { + + private double min = Double.NEGATIVE_INFINITY; + private double max = Double.MAX_VALUE; + + public ValidRangePair() { + } + + public ValidRangePair(double min, double max) { + this.min = min; + this.max = max; + } + + /** + * Returns whether the given value is in the valid range. + * + * @param value The value to test. + * @return true if the value is in range, false + * otherwise. + */ + public boolean isValueValid(double value) { + return value >= min && value <= max; + } + + public double getMin() { + return min; + } + + public ValidRangePair setMin(double min) { + this.min = min; + return this; + } + + public double getMax() { + return max; + } + + public ValidRangePair setMax(double max) { + this.max = max; + return this; + } + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/AcceptableInvalidValue.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/AcceptableInvalidValue.java new file mode 100644 index 0000000..d80829a --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/AcceptableInvalidValue.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +/** + * An invalid but still acceptable value for a property. + */ +public interface AcceptableInvalidValue { + + /** + * Returns a description for the value. + * + * @return A description. + */ + String getDescription(); + + /** + * Returns a helptext for the value. + * + * @return A helptext. + */ + String getHelptext(); +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/AngleProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/AngleProperty.java new file mode 100644 index 0000000..c8837e3 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/AngleProperty.java @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import java.util.ArrayList; +import java.util.List; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property for angles. + */ +public class AngleProperty + extends + AbstractQuantity { + + /** + * Creates a new instance. + * + * @param model The model component. + */ + public AngleProperty(ModelComponent model) { + this(model, Double.NaN, Unit.DEG); + } + + /** + * Creates a new instance. + * + * @param model The model component. + * @param value The property value. + * @param unit The value's unit. + */ + public AngleProperty(ModelComponent model, double value, Unit unit) { + super(model, value, unit, Unit.class, relations()); + } + + @Override + public Object getComparableValue() { + return String.valueOf(fValue) + getUnit(); + } + + @Override + public void setValue(Object newValue) { + if (newValue instanceof Double) { + if (getUnit() == Unit.DEG) { + super.setValue(((double) newValue) % 360); + } + else { + super.setValue(((double) newValue) % (2 * Math.PI)); + } + } + else { + super.setValue(newValue); + } + } + + @Override + protected void initValidRange() { + validRange.setMin(0); + } + + private static List> relations() { + List> relations = new ArrayList<>(); + relations.add(new Relation<>(Unit.DEG, Unit.RAD, 180.0 / Math.PI)); + relations.add(new Relation<>(Unit.RAD, Unit.DEG, Math.PI / 180.0)); + return relations; + } + + /** + * The supported units. + */ + public enum Unit { + /** + * Degrees. + */ + DEG("deg"), + /** + * Radians. + */ + RAD("rad"); + + private final String displayString; + + Unit(String displayString) { + this.displayString = displayString; + } + + @Override + public String toString() { + return displayString; + } + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/BlockTypeProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/BlockTypeProperty.java new file mode 100644 index 0000000..fdd0c4b --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/BlockTypeProperty.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import java.util.List; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.BlockModel.Type; + +/** + * Subclass for a {@link Type} selection property. + */ +public class BlockTypeProperty + extends + SelectionProperty { + + public BlockTypeProperty( + ModelComponent model, + List possibleValues, + Object value + ) { + super(model, possibleValues, value); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/BooleanProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/BooleanProperty.java new file mode 100644 index 0000000..2af7a2c --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/BooleanProperty.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property for a boolean value. + */ +public class BooleanProperty + extends + AbstractProperty { + + /** + * Creates a new instance. + * + * @param model The model component. + */ + public BooleanProperty(ModelComponent model) { + this(model, false); + } + + /** + * Creates a new property with a value. + * + * @param model The model component. + * @param value The value. + */ + @SuppressWarnings("this-escape") + public BooleanProperty(ModelComponent model, boolean value) { + super(model); + setValue(value); + } + + @Override // Property + public Object getComparableValue() { + return String.valueOf(fValue); + } + + @Override + public String toString() { + return getValue().toString(); + } + + @Override + public void copyFrom(Property property) { + BooleanProperty booleanProperty = (BooleanProperty) property; + setValue(booleanProperty.getValue()); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/BoundingBoxProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/BoundingBoxProperty.java new file mode 100644 index 0000000..ade9cf3 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/BoundingBoxProperty.java @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import static org.opentcs.util.Assertions.checkArgument; + +import org.opentcs.guing.base.model.BoundingBoxModel; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property that contains a bounding box. + */ +public class BoundingBoxProperty + extends + AbstractComplexProperty { + + public BoundingBoxProperty(ModelComponent model, BoundingBoxModel boundingBox) { + super(model); + fValue = boundingBox; + } + + @Override + public Object getComparableValue() { + return this.toString(); + } + + @Override + public void copyFrom(Property property) { + BoundingBoxProperty other = (BoundingBoxProperty) property; + setValue(other.getValue()); + } + + @Override + public Object clone() { + BoundingBoxProperty clone = (BoundingBoxProperty) super.clone(); + clone.setValue(getValue()); + return clone; + } + + @Override + public String toString() { + return String.format( + "(%s, %s, %s), offset: (%s, %s)", + getValue().getLength(), + getValue().getWidth(), + getValue().getHeight(), + getValue().getReferenceOffset().getX(), + getValue().getReferenceOffset().getY() + ); + } + + @Override + public BoundingBoxModel getValue() { + return (BoundingBoxModel) super.getValue(); + } + + @Override + public void setValue(Object newValue) { + checkArgument( + newValue instanceof BoundingBoxModel, + "newValue is not an instance of BoundingBoxModel" + ); + + super.setValue(newValue); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/ColorProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/ColorProperty.java new file mode 100644 index 0000000..d948dba --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/ColorProperty.java @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import java.awt.Color; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A color property. + */ +public final class ColorProperty + extends + AbstractProperty { + + /** + * The color. + */ + private Color fColor; + + /** + * Create a new instance with a color. + * + * @param model The model component. + * @param color The color. + */ + public ColorProperty(ModelComponent model, Color color) { + super(model); + setColor(color); + } + + /** + * Set the color. + * + * @param color The color + */ + public void setColor(Color color) { + fColor = color; + } + + /** + * Returns the color. + * + * @return The color. + */ + public Color getColor() { + return fColor; + } + + @Override // Property + public Object getComparableValue() { + return fColor; + } + + @Override // AbstractProperty + public void copyFrom(Property property) { + ColorProperty colorProperty = (ColorProperty) property; + setColor(colorProperty.getColor()); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/CoordinateProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/CoordinateProperty.java new file mode 100644 index 0000000..672c761 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/CoordinateProperty.java @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import org.opentcs.guing.base.model.ModelComponent; + +/** + * An attribute for coordinates. + * Examples: 1 mm, 20 cm, 3.4 m, 17.98 km + */ +public class CoordinateProperty + extends + LengthProperty { + + /** + * Creates a new instance of CoordinateProperty. + * + * @param model Point- or LocationModel. + */ + public CoordinateProperty(ModelComponent model) { + this(model, 0, Unit.MM); + } + + /** + * Creates a new instance of CoordinateProperty. + * + * @param model Point- or LocationModel. + * @param value The initial value. + * @param unit The initial unit. + */ + public CoordinateProperty(ModelComponent model, double value, Unit unit) { + super(model, value, unit); + } + + @Override + protected void initValidRange() { + validRange.setMin(Double.NEGATIVE_INFINITY); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/EnergyLevelThresholdSetModel.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/EnergyLevelThresholdSetModel.java new file mode 100644 index 0000000..4c7f105 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/EnergyLevelThresholdSetModel.java @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import org.opentcs.data.model.Vehicle.EnergyLevelThresholdSet; + +/** + * A representation of an {@link EnergyLevelThresholdSet}. + */ +public class EnergyLevelThresholdSetModel { + + private final int energyLevelCritical; + private final int energyLevelGood; + private final int energyLevelSufficientlyRecharged; + private final int energyLevelFullyRecharged; + + /** + * Creates a new instance. + * + * @param energyLevelCritical The value at/below which the vehicle's energy level is considered + * "critical". + * @param energyLevelGood The value at/above which the vehicle's energy level is considered + * "good". + * @param energyLevelSufficientlyRecharged The value at/above which the vehicle's energy level + * is considered fully recharged. + * @param energyLevelFullyRecharged The value at/above which the vehicle's energy level is + * considered sufficiently recharged. + */ + public EnergyLevelThresholdSetModel( + int energyLevelCritical, + int energyLevelGood, + int energyLevelSufficientlyRecharged, + int energyLevelFullyRecharged + ) { + this.energyLevelCritical = energyLevelCritical; + this.energyLevelGood = energyLevelGood; + this.energyLevelSufficientlyRecharged = energyLevelSufficientlyRecharged; + this.energyLevelFullyRecharged = energyLevelFullyRecharged; + } + + public int getEnergyLevelCritical() { + return energyLevelCritical; + } + + public int getEnergyLevelGood() { + return energyLevelGood; + } + + public int getEnergyLevelSufficientlyRecharged() { + return energyLevelSufficientlyRecharged; + } + + public int getEnergyLevelFullyRecharged() { + return energyLevelFullyRecharged; + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/EnergyLevelThresholdSetProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/EnergyLevelThresholdSetProperty.java new file mode 100644 index 0000000..545f85e --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/EnergyLevelThresholdSetProperty.java @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import static org.opentcs.util.Assertions.checkArgument; + +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property that contains an {@link EnergyLevelThresholdSetModel}. + */ +public class EnergyLevelThresholdSetProperty + extends + AbstractComplexProperty { + + public EnergyLevelThresholdSetProperty( + ModelComponent model, + EnergyLevelThresholdSetModel energyLevelThresholdSet + ) { + super(model); + fValue = energyLevelThresholdSet; + } + + @Override + public Object getComparableValue() { + return this.toString(); + } + + @Override + public void copyFrom(Property property) { + EnergyLevelThresholdSetProperty other = (EnergyLevelThresholdSetProperty) property; + setValue(other.getValue()); + } + + @Override + public Object clone() { + EnergyLevelThresholdSetProperty clone = (EnergyLevelThresholdSetProperty) super.clone(); + clone.setValue(getValue()); + return clone; + } + + @Override + public String toString() { + return String.format( + "(%s%%, %s%%, %s%%, %s%%)", + getValue().getEnergyLevelCritical(), + getValue().getEnergyLevelGood(), + getValue().getEnergyLevelSufficientlyRecharged(), + getValue().getEnergyLevelFullyRecharged() + ); + } + + @Override + @SuppressWarnings("unchecked") + public EnergyLevelThresholdSetModel getValue() { + return (EnergyLevelThresholdSetModel) super.getValue(); + } + + @Override + public void setValue(Object newValue) { + checkArgument( + newValue instanceof EnergyLevelThresholdSetModel, + "newValue is not an instance of EnergyLevelThresholdSetModel" + ); + + super.setValue(newValue); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/EnvelopesProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/EnvelopesProperty.java new file mode 100644 index 0000000..8b01c5c --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/EnvelopesProperty.java @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import org.opentcs.guing.base.model.EnvelopeModel; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property that contains a list of envelopes. + */ +public class EnvelopesProperty + extends + AbstractComplexProperty { + + public EnvelopesProperty( + ModelComponent model, + List envelopes + ) { + super(model); + fValue = envelopes; + } + + @Override + public Object getComparableValue() { + return this.toString(); + } + + @Override + public void copyFrom(Property property) { + EnvelopesProperty other = (EnvelopesProperty) property; + setValue(new ArrayList<>(other.getValue())); + } + + @Override + public Object clone() { + EnvelopesProperty clone = (EnvelopesProperty) super.clone(); + clone.setValue(new ArrayList<>(getValue())); + return clone; + } + + @Override + public String toString() { + return getValue().stream() + .map(envelope -> envelope.getKey() + ": " + envelope.getVertices()) + .collect(Collectors.joining(", ")); + } + + @Override + @SuppressWarnings("unchecked") + public List getValue() { + return (List) super.getValue(); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/IntegerProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/IntegerProperty.java new file mode 100644 index 0000000..2b1f954 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/IntegerProperty.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property for an integer value. + */ +public class IntegerProperty + extends + AbstractProperty { + + /** + * Creates a new instance. + * + * @param model The model component. + */ + public IntegerProperty(ModelComponent model) { + this(model, 0); + } + + /** + * Creates a new instance with a value. + * + * @param model The model component. + * @param value The value. + */ + @SuppressWarnings("this-escape") + public IntegerProperty(ModelComponent model, int value) { + super(model); + setValue(value); + } + + @Override + public Object getComparableValue() { + return String.valueOf(fValue); + } + + @Override + public String toString() { + return fValue instanceof Integer ? Integer.toString((int) fValue) : (String) fValue; + } + + @Override + public void copyFrom(Property property) { + IntegerProperty integerProperty = (IntegerProperty) property; + setValue(integerProperty.getValue()); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/KeyValueProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/KeyValueProperty.java new file mode 100644 index 0000000..4bc0f62 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/KeyValueProperty.java @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property containing a key-value pair. + */ +public class KeyValueProperty + extends + AbstractComplexProperty { + + /** + * The key. + */ + private String fKey; + + /** + * Creates a new instance. + * + * @param model The model component. + */ + public KeyValueProperty(ModelComponent model) { + this(model, "", ""); + } + + /** + * Creates a new instance with a key and value. + * + * @param model The model component. + * @param key The key. + * @param value The value. + */ + public KeyValueProperty(ModelComponent model, String key, String value) { + super(model); + fKey = key; + fValue = value; + } + + @Override + public Object getComparableValue() { + return fKey + fValue; + } + + /** + * Set the key and the value. + * + * @param key The key + * @param value The value + */ + public void setKeyAndValue(String key, String value) { + fKey = key; + fValue = value; + } + + /** + * Returns the key. + * + * @return The key of this property. + */ + public String getKey() { + return fKey; + } + + @Override + public String getValue() { + return (String) fValue; + } + + @Override + public String toString() { + return fKey + "=" + fValue; + } + + @Override + public void copyFrom(Property property) { + KeyValueProperty keyValueProperty = (KeyValueProperty) property; + setKeyAndValue(keyValueProperty.getKey(), keyValueProperty.getValue()); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/KeyValueSetProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/KeyValueSetProperty.java new file mode 100644 index 0000000..15f9ba1 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/KeyValueSetProperty.java @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * An attribute which contains a quantity of key-value pairs. + */ +public class KeyValueSetProperty + extends + AbstractComplexProperty { + + /** + * The quantity of key-value-pairs. + */ + private List fItems = new ArrayList<>(); + + /** + * Creates a new instance. + * + * @param model The model component this property belongs to. + */ + public KeyValueSetProperty(ModelComponent model) { + super(model); + } + + @Override + public Object getComparableValue() { + StringBuilder sb = new StringBuilder(); + + for (KeyValueProperty property : fItems) { + sb.append(property.getKey()).append(property.getValue()); + } + + return sb.toString(); + } + + /** + * Adds a property. + * + * @param item The property to add. + */ + public void addItem(KeyValueProperty item) { + for (KeyValueProperty property : fItems) { + if (item.getKey().equals(property.getKey())) { + property.setKeyAndValue(property.getKey(), item.getValue()); + return; + } + } + + fItems.add(item); + } + + /** + * Removes a property. + * + * @param item The property to remove. + */ + public void removeItem(KeyValueProperty item) { + fItems.remove(item); + } + + /** + * Sets the list with key-value-pairs.. + * + * @param items The values. + */ + public void setItems(List items) { + fItems = items; + } + + /** + * Returns all key-value-pairs. + * + * @return The properties. + */ + public List getItems() { + return fItems; + } + + @Override + public void copyFrom(Property property) { + KeyValueSetProperty other = (KeyValueSetProperty) property; + List items = new ArrayList<>(other.getItems()); + setItems(items); + } + + @Override + public String toString() { + if (fValue != null) { + return fValue.toString(); + } + + return getItems().stream() + .sorted((i1, i2) -> i1.getKey().compareTo(i2.getKey())) + .map(item -> item.toString()) + .collect(Collectors.joining(", ")); + } + + @Override + public Object clone() { + KeyValueSetProperty clone = (KeyValueSetProperty) super.clone(); + List items = new ArrayList<>(getItems()); + clone.setItems(items); + + return clone; + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LayerGroupsProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LayerGroupsProperty.java new file mode 100644 index 0000000..1172ed4 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LayerGroupsProperty.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import org.opentcs.data.model.visualization.LayerGroup; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property that contains a list of layer groups. + */ +public class LayerGroupsProperty + extends + AbstractComplexProperty { + + /** + * Creates a new instance. + * + * @param model The model component this property belongs to. + * @param layerGroups The layer groups. + */ + public LayerGroupsProperty(ModelComponent model, Map layerGroups) { + super(model); + fValue = layerGroups; + } + + @Override + public Object getComparableValue() { + return this.toString(); + } + + @Override + public String toString() { + return getValue().values().stream() + .sorted(Comparator.comparing(group -> group.getId())) + .map(group -> group.getName()) + .collect(Collectors.joining(", ")); + } + + @Override + public void copyFrom(Property property) { + LayerGroupsProperty other = (LayerGroupsProperty) property; + Map items = new HashMap<>(other.getValue()); + setValue(items); + } + + @Override + @SuppressWarnings("unchecked") + public Map getValue() { + return (Map) super.getValue(); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LayerWrapperProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LayerWrapperProperty.java new file mode 100644 index 0000000..2baccb2 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LayerWrapperProperty.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import org.opentcs.guing.base.components.layer.LayerWrapper; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property that contains a {@link LayerWrapper} instance. + */ +public class LayerWrapperProperty + extends + AbstractComplexProperty { + + /** + * Creates a new instance. + * + * @param model The model component. + * @param value The layer wrapper. + */ + public LayerWrapperProperty(ModelComponent model, LayerWrapper value) { + super(model); + fValue = value; + } + + @Override + public Object getComparableValue() { + return getValue().getLayer().getId(); + } + + @Override + public String toString() { + return getValue().getLayer().getName(); + } + + @Override + public void copyFrom(Property property) { + LayerWrapperProperty layerWrapperProperty = (LayerWrapperProperty) property; + setValue(layerWrapperProperty.getValue()); + } + + @Override + public LayerWrapper getValue() { + return (LayerWrapper) super.getValue(); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LayerWrappersProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LayerWrappersProperty.java new file mode 100644 index 0000000..c6b4709 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LayerWrappersProperty.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import org.opentcs.guing.base.components.layer.LayerWrapper; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property that contains a list of layer wrappers. + */ +public class LayerWrappersProperty + extends + AbstractComplexProperty { + + /** + * Creates a new instance. + * + * @param model The model component this property belongs to. + * @param layerWrappers The layer wrappers. + */ + public LayerWrappersProperty(ModelComponent model, Map layerWrappers) { + super(model); + fValue = layerWrappers; + } + + @Override + public Object getComparableValue() { + return this.toString(); + } + + @Override + public String toString() { + return getValue().values().stream() + .sorted(Comparator.comparing(wrapper -> wrapper.getLayer().getId())) + .map(wrapper -> wrapper.getLayer().getName()) + .collect(Collectors.joining(", ")); + } + + @Override + public void copyFrom(Property property) { + LayerWrappersProperty other = (LayerWrappersProperty) property; + Map items = new HashMap<>(other.getValue()); + setValue(items); + } + + @Override + @SuppressWarnings("unchecked") + public Map getValue() { + return (Map) super.getValue(); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LengthProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LengthProperty.java new file mode 100644 index 0000000..93ebb8c --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LengthProperty.java @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import java.util.ArrayList; +import java.util.List; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property for a length. + */ +public class LengthProperty + extends + AbstractQuantity { + + /** + * Creates a new instance. + * + * @param model The model component. + */ + public LengthProperty(ModelComponent model) { + this(model, 0, Unit.MM); + } + + /** + * Creates a new instance with a value and a unit. + * + * @param model The model component. + * @param value The value. + * @param unit The unit. + */ + public LengthProperty(ModelComponent model, double value, Unit unit) { + super(model, value, unit, Unit.class, relations()); + } + + @Override // Property + public Object getComparableValue() { + return String.valueOf(fValue) + getUnit(); + } + + private static List> relations() { + List> relations = new ArrayList<>(); + relations.add(new Relation<>(Unit.MM, Unit.CM, 10)); + relations.add(new Relation<>(Unit.CM, Unit.M, 100)); + relations.add(new Relation<>(Unit.M, Unit.KM, 1000)); + return relations; + } + + @Override + protected void initValidRange() { + validRange.setMin(0); + } + + /** + * Supported length units. + */ + public enum Unit { + + /** + * Millimeters. + */ + MM("mm"), + /** + * Centimeters. + */ + CM("cm"), + /** + * Meters. + */ + M("m"), + /** + * Kilometers. + */ + KM("km"); + + private final String displayString; + + Unit(String displayString) { + this.displayString = displayString; + } + + @Override + public String toString() { + return displayString; + } + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LinerTypeProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LinerTypeProperty.java new file mode 100644 index 0000000..f2d4daf --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LinerTypeProperty.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import java.util.List; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.PathModel.Type; + +/** + * Subclass for a {@link Type} selection property. + */ +public class LinerTypeProperty + extends + SelectionProperty { + + public LinerTypeProperty( + ModelComponent model, + List possibleValues, + Object value + ) { + super(model, possibleValues, value); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LinkActionsProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LinkActionsProperty.java new file mode 100644 index 0000000..f6038b9 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LinkActionsProperty.java @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property for link actions. + */ +public class LinkActionsProperty + extends + StringSetProperty { + + /** + * Creates a new instance. + * + * @param model The model component this property belongs to. + */ + public LinkActionsProperty(ModelComponent model) { + super(model); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LocationTypeActionsProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LocationTypeActionsProperty.java new file mode 100644 index 0000000..0b923a4 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LocationTypeActionsProperty.java @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property for location actions. + */ +public class LocationTypeActionsProperty + extends + StringSetProperty { + + /** + * Creates a new instance. + * + * @param model The model component this property belongs to. + */ + public LocationTypeActionsProperty(ModelComponent model) { + super(model); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LocationTypeProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LocationTypeProperty.java new file mode 100644 index 0000000..57da09c --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/LocationTypeProperty.java @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property that can take a value from a given set of location types. + */ +public class LocationTypeProperty + extends + AbstractProperty + implements + Selectable { + + private List fPossibleValues; + + public LocationTypeProperty(ModelComponent model) { + this(model, new ArrayList<>(), ""); + } + + public LocationTypeProperty(ModelComponent model, List possibleValues, Object value) { + super(model); + this.fPossibleValues = requireNonNull(possibleValues, "possibleValues"); + fValue = value; + } + + @Override + public Object getComparableValue() { + return fValue; + } + + @Override + public void setValue(Object value) { + if (fPossibleValues.contains(value) + || value instanceof AcceptableInvalidValue) { + super.setValue(value); + } + } + + @Override + public List getPossibleValues() { + return fPossibleValues; + } + + @Override + public void setPossibleValues(List possibleValues) { + fPossibleValues = requireNonNull(possibleValues, "possibleValues"); + } + + @Override + public void copyFrom(Property property) { + LocationTypeProperty locTypeProperty = (LocationTypeProperty) property; + setValue(locTypeProperty.getValue()); + } + + @Override + public String toString() { + return getValue().toString(); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/ModelAttribute.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/ModelAttribute.java new file mode 100644 index 0000000..6f2e8b0 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/ModelAttribute.java @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import java.io.Serializable; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * Interface to specify how an attribute of a model component must appear. + */ +public interface ModelAttribute + extends + Serializable { + + /** + * Potential change states of the attribute. + */ + enum ChangeState { + /** + * The attribute has not changed. + */ + NOT_CHANGED, + /** + * The attribute has changed. + */ + CHANGED, + /** + * (Only) A detail of the attribute has changed. + */ + DETAIL_CHANGED, + }; + + /** + * Returns the model component this attribute is attached to. + * + * @return The model component. + */ + ModelComponent getModel(); + + /** + * Sets the model component this attribute is attached to. + * + * @param model The model component. + */ + void setModel(ModelComponent model); + + /** + * Marks the attribute as changed. + */ + void markChanged(); + + /** + * Marks the attribute as not changed. + */ + void unmarkChanged(); + + /** + * Sets the change state for this attribute. + * + * @param changeState The new change state. + */ + void setChangeState(AbstractModelAttribute.ChangeState changeState); + + /** + * Returns whether or not the attribute has changed. + * + * @return {@code true}, if the state of the model attribute has changed, otherwiese + * {@code false}. + */ + boolean hasChanged(); + + /** + * Sets the description of the attribute. + * + * @param description The description. + */ + void setDescription(String description); + + /** + * Returns the description of the attribute. + * + * @return The description. + */ + String getDescription(); + + /** + * Sets the tooltip text for this attribute. + * + * @param helptext The tooltip text. + */ + void setHelptext(String helptext); + + /** + * Returns the tooltip text for this attribute. + * + * @return The helptext. + */ + String getHelptext(); + + /** + * + * Sets whether or not the attribute is collectively editable with attributes + * of the same name of other model components. + * + * @param collectiveEditable Whether the attribute is collectively editable with attributes + * of the same name of other model components. + */ + void setCollectiveEditable(boolean collectiveEditable); + + /** + * Returns whether or not the attribute is collectively editable with attributes of the same name + * of other model components. + * + * @return Whether the attribute is collectively editable with attributes of the same name + * of other model components. + */ + boolean isCollectiveEditable(); + + /** + * Sets whether or not the attribute can be changed in modelling mode. + * + * @param editable True if the attribute can be changed in modelling mode. + */ + void setModellingEditable(boolean editable); + + /** + * Returns whether or not the attribute can be changed in modelling mode. + * + * @return True if the attribute can be changed in modelling mode. + */ + boolean isModellingEditable(); + + /** + * Sets whether or not the attribute can be changed in operating mode. + * + * @param editable True if the attribute can be changed in operating mode. + */ + void setOperatingEditable(boolean editable); + + /** + * Returns whether or not the attribute can be changed in operating mode. + * + * @return True if the attribute can be changed in operating mode. + */ + boolean isOperatingEditable(); + + /** + * Whether this attribute should be included when persisting the model to a file. + * + * @return true, if this attribute should be included when persisting the model to a file. + */ + default boolean isPersistent() { + return true; + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/MultipleDifferentValues.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/MultipleDifferentValues.java new file mode 100644 index 0000000..69bcb63 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/MultipleDifferentValues.java @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import static org.opentcs.guing.base.I18nPlantOverviewBase.BUNDLE_PATH; + +import java.util.ResourceBundle; + +/** + */ +public class MultipleDifferentValues + implements + AcceptableInvalidValue { + + /** + * Creates a new instance. + */ + public MultipleDifferentValues() { + } + + @Override + public String getDescription() { + return ResourceBundle.getBundle(BUNDLE_PATH) + .getString("multipleDifferentValues.description"); + } + + @Override + public String getHelptext() { + return ResourceBundle.getBundle(BUNDLE_PATH) + .getString("multipleDifferentValues.helptext"); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/OrderTypesProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/OrderTypesProperty.java new file mode 100644 index 0000000..c64f2b1 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/OrderTypesProperty.java @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import java.util.Iterator; +import java.util.Set; +import java.util.TreeSet; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property that contains a set of transport order types represented by strings. + */ +public class OrderTypesProperty + extends + AbstractComplexProperty { + + /** + * The set of transport order types. + */ + private Set fItems = new TreeSet<>(); + + /** + * Creates a new instance. + * + * @param model The model component this property belongs to. + */ + public OrderTypesProperty(ModelComponent model) { + super(model); + } + + @Override + public Object getComparableValue() { + StringBuilder sb = new StringBuilder(); + + for (String s : fItems) { + sb.append(s); + } + + return sb.toString(); + } + + /** + * Adds a string. + * + * @param item The string to add. + */ + public void addItem(String item) { + fItems.add(item); + } + + /** + * Sets the list of strings. + * + * @param items The list. + */ + public void setItems(Set items) { + fItems = items; + } + + /** + * Returns the list of string. + * + * @return The list. + */ + public Set getItems() { + return fItems; + } + + @Override + public void copyFrom(Property property) { + OrderTypesProperty other = (OrderTypesProperty) property; + Set items = new TreeSet<>(other.getItems()); + setItems(items); + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + Iterator e = getItems().iterator(); + + while (e.hasNext()) { + b.append(e.next()); + + if (e.hasNext()) { + b.append(", "); + } + } + + return b.toString(); + } + + @Override + public Object clone() { + OrderTypesProperty clone = (OrderTypesProperty) super.clone(); + Set items = new TreeSet<>(getItems()); + clone.setItems(items); + return clone; + } + + @Override + public boolean isPersistent() { + return false; + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/PercentProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/PercentProperty.java new file mode 100644 index 0000000..009204d --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/PercentProperty.java @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import java.util.ArrayList; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property for percentages. + */ +public class PercentProperty + extends + AbstractQuantity { + + /** + * Creates a new instance. + * + * @param model The model component. + */ + public PercentProperty(ModelComponent model) { + this(model, false); + } + + /** + * Creates a new instance. + * + * @param model The model component. + * @param isInteger Whether only the integer part of the value is relevant. + */ + public PercentProperty(ModelComponent model, boolean isInteger) { + this(model, Double.NaN, Unit.PERCENT, isInteger); + } + + /** + * Creates a new instance with value. + * + * @param model The model component. + * @param value The property's value. + * @param unit The unit in which the value is measured. + * @param isInteger Whether only the integer part of the value is relevant. + */ + @SuppressWarnings("this-escape") + public PercentProperty(ModelComponent model, double value, Unit unit, boolean isInteger) { + super(model, value, unit, Unit.class, new ArrayList>()); + setInteger(isInteger); + } + + @Override // Property + public Object getComparableValue() { + return String.valueOf(fValue) + getUnit(); + } + + @Override + protected void initValidRange() { + validRange.setMin(0).setMax(100); + } + + /** + * The supported units. + */ + public enum Unit { + + /** + * Percent. + */ + PERCENT("%"); + + private final String displayString; + + Unit(String displayString) { + this.displayString = displayString; + } + + @Override + public String toString() { + return displayString; + } + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/PeripheralOperationsProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/PeripheralOperationsProperty.java new file mode 100644 index 0000000..7fe43e3 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/PeripheralOperationsProperty.java @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.PeripheralOperationModel; + +/** + * A property that contains a list of Peripheral operations. + */ +public class PeripheralOperationsProperty + extends + AbstractComplexProperty { + + public PeripheralOperationsProperty( + ModelComponent model, + List operations + ) { + super(model); + fValue = operations; + } + + @Override + public Object getComparableValue() { + return this.toString(); + } + + @Override + public String toString() { + return getValue().stream() + .map(op -> op.getLocationName() + ": " + op.getOperation()) + .collect(Collectors.joining(", ")); + } + + @Override + public void copyFrom(Property property) { + PeripheralOperationsProperty other = (PeripheralOperationsProperty) property; + setValue(new ArrayList<>(other.getValue())); + } + + @Override + public Object clone() { + PeripheralOperationsProperty clone = (PeripheralOperationsProperty) super.clone(); + clone.setValue(new ArrayList<>(getValue())); + return clone; + } + + @Override + @SuppressWarnings("unchecked") + public List getValue() { + return (List) super.getValue(); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/PointTypeProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/PointTypeProperty.java new file mode 100644 index 0000000..5ca9095 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/PointTypeProperty.java @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import java.util.List; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.PointModel.Type; + +/** + * Subclass for a {@link Type} selection property. + */ +public class PointTypeProperty + extends + SelectionProperty { + + public PointTypeProperty(ModelComponent model) { + super(model); + } + + public PointTypeProperty( + ModelComponent model, + List possibleValues, + Object value + ) { + super(model, possibleValues, value); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/Property.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/Property.java new file mode 100644 index 0000000..383d9ca --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/Property.java @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +/** + * Interface for properties. + * Wraps a type into a property to be able to change a value without creating a new object. + * The property object stays the same while the value changes. + */ +public interface Property + extends + ModelAttribute, + Cloneable { + + /** + * Copies the value of the property into this property. + * + * @param property The property. + */ + void copyFrom(Property property); + + /** + * Returns a comparable represantation of the value of this property. + * + * @return A represantation to compare this property to other ones. + */ + Object getComparableValue(); + + /** + * Creates a copy of this property. + * + * @return The cloned property. + */ + Object clone(); +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/Relation.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/Relation.java new file mode 100644 index 0000000..31d1780 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/Relation.java @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import java.io.Serializable; + +/** + * A conversion relationship between two units. + * + * @param The type of units the relation is valid for. + */ +public class Relation + implements + Serializable { + + /** + * The source unit. + */ + private final U fUnitFrom; + /** + * The destination unit. + */ + private final U fUnitTo; + /** + * The conversion relationship. + */ + private final double fRelationValue; + + /** + * Creates a new instance. + * + * @param unitFrom The unit from which to convert. + * @param unitTo The unit to which to convert. + * @param relationValue The relation value between the two units. + */ + public Relation(U unitFrom, U unitTo, double relationValue) { + fUnitFrom = unitFrom; + fUnitTo = unitTo; + fRelationValue = relationValue; + } + + /** + * Checks if this relation is applicable to the specified units. + * + * @param unitA The first unit. + * @param unitB The second unit. + * @return {@code true}, if the two given units are covered by this relation, otherwise + * {@code false}. + */ + public boolean fits(U unitA, U unitB) { + if (fUnitFrom.equals(unitA) && fUnitTo.equals(unitB)) { + return true; + } + + if (fUnitFrom.equals(unitB) && fUnitTo.equals(unitA)) { + return true; + } + + return false; + } + + /** + * Returns the conversion relationship as a number. + * + * @return The relation value between the two units. + */ + public double relationValue() { + return fRelationValue; + } + + /** + * Returns the operation used for the conversion of the first unit into the second unit. + * + * @param unitFrom The unit from which to convert. + * @param unitTo The unit to which to convert. + * @return The operation that is to be used for the conversion. + */ + public Operation getOperation(U unitFrom, U unitTo) { + if (unitFrom.equals(fUnitFrom)) { + return Operation.DIVISION; + } + else { + return Operation.MULTIPLICATION; + } + } + + /** + * The supported operations. + */ + public enum Operation { + /** + * A division. + */ + DIVISION, + /** + * A multiplication. + */ + MULTIPLICATION + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/ResourceProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/ResourceProperty.java new file mode 100644 index 0000000..9e4f7a3 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/ResourceProperty.java @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property that holds a list of sets of {@link TCSResourceReference}s. + */ +public class ResourceProperty + extends + AbstractComplexProperty { + + /** + * A list of of sets {@link TCSResourceReference}s. + */ + private List>> items = new ArrayList<>(); + + /** + * Creates a new ResourceProperty instance. + * + * @param model The model component this property belongs to. + */ + public ResourceProperty(ModelComponent model) { + super(model); + } + + @Override + public Object getComparableValue() { + return items.stream() + .flatMap(set -> set.stream()) + .map(r -> r.getName()) + .collect(Collectors.joining()); + } + + /** + * Returns a list of all items of this property. + * + * @return a list of all items of this property. + */ + public List>> getItems() { + return items; + } + + /** + * Sets the list. + * + * @param items the list of items for this property. + */ + public void setItems(List>> items) { + this.items = items; + } + + /** + * Adds one item to the list. + * + * @param item the item to add. + */ + public void addItem(Set> item) { + items.add(item); + } + + /** + * Removes one item from the list. + * + * @param item the item to remove. + */ + public void removeItem(Set> item) { + items.remove(item); + } + + @Override + public void copyFrom(Property property) { + ResourceProperty other = (ResourceProperty) property; + List>> items = new ArrayList<>(other.getItems()); + setItems(items); + } + + @Override + public String toString() { + if (fValue != null) { + return fValue.toString(); + } + + return items.stream() + .flatMap(set -> set.stream()) + .map(r -> r.getName()) + .collect(Collectors.joining(", ")); + } + + @Override + public Object clone() { + ResourceProperty clone = (ResourceProperty) super.clone(); + List>> items = new ArrayList<>(getItems()); + clone.setItems(items); + + return clone; + } + +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/Selectable.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/Selectable.java new file mode 100644 index 0000000..8fbe679 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/Selectable.java @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import java.util.List; + +/** + * Interface for a property indicating this property has different + * possible values to choose from. + * + * @param The type of elements that can be selected from. + */ +public interface Selectable { + + /** + * Sets the possible values. + * + * @param possibleValues An array with the possible values. + */ + void setPossibleValues(List possibleValues); + + /** + * Returns the possible values. + * + * @return The possible values. + */ + List getPossibleValues(); +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/SelectionProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/SelectionProperty.java new file mode 100644 index 0000000..79419ce --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/SelectionProperty.java @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property whose value is one out of a list of possible values. + * + * @param The type of the enum. + */ +public class SelectionProperty> + extends + AbstractProperty + implements + Selectable { + + /** + * The possible values. + */ + private List fPossibleValues; + + /** + * Creates a new instance. + * + * @param model The model component. + */ + public SelectionProperty(ModelComponent model) { + this(model, new ArrayList<>(), ""); + } + + /** + * Creates a new instance. + * + * @param model The model Component. + * @param possibleValues The possible values. + * @param value The value. + */ + @SuppressWarnings("this-escape") + public SelectionProperty( + ModelComponent model, List possibleValues, + Object value + ) { + super(model); + setPossibleValues(possibleValues); + fValue = value; + } + + @Override + public Object getComparableValue() { + return fValue; + } + + /** + * Sets the possible values for this property. + * + * @param possibleValues A list of possible values. + */ + @Override + public void setPossibleValues(List possibleValues) { + fPossibleValues = Objects.requireNonNull(possibleValues, "possibleValues is null"); + } + + @Override + public void setValue(Object value) { + if (fPossibleValues.contains(value) + || value instanceof AcceptableInvalidValue) { + fValue = value; + } + } + + @Override + public String toString() { + return getValue().toString(); + } + + @Override + public List getPossibleValues() { + return fPossibleValues; + } + + @Override + public void copyFrom(Property property) { + AbstractProperty selectionProperty = (AbstractProperty) property; + setValue(selectionProperty.getValue()); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/SpeedProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/SpeedProperty.java new file mode 100644 index 0000000..4acad3d --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/SpeedProperty.java @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import java.util.ArrayList; +import java.util.List; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property for speeds. + */ +public class SpeedProperty + extends + AbstractQuantity { + + /** + * Creates a new instance. + * + * @param model The model component. + */ + public SpeedProperty(ModelComponent model) { + this(model, 0, Unit.M_S); + } + + /** + * Creates a new instance with a value and a unit. + * + * @param model The model component. + * @param value The value. + * @param unit The unit. + */ + @SuppressWarnings("this-escape") + public SpeedProperty(ModelComponent model, double value, Unit unit) { + super(model, value, unit, Unit.class, relations()); + setUnsigned(true); + } + + @Override + public Object getComparableValue() { + return String.valueOf(fValue) + getUnit(); + } + + private static List> relations() { + List> relations = new ArrayList<>(); + relations.add(new Relation<>(Unit.KM_H, Unit.M_S, 3.6)); + relations.add(new Relation<>(Unit.M_S, Unit.MM_S, 0.001)); + return relations; + } + + @Override + protected void initValidRange() { + validRange.setMin(0); + } + + /** + * Supported speed units. + */ + public enum Unit { + /** + * Kilometers per hour. + */ + KM_H("km/h"), + /** + * Meters per second. + */ + M_S("m/s"), + /** + * Millimeters per second. + */ + MM_S("mm/s"); + + private final String displayString; + + Unit(String displayString) { + this.displayString = displayString; + } + + @Override + public String toString() { + return displayString; + } + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/StringProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/StringProperty.java new file mode 100644 index 0000000..eda52e5 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/StringProperty.java @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import static java.util.Objects.requireNonNull; + +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property for a string. + */ +public class StringProperty + extends + AbstractProperty { + + /** + * The string. + */ + private String fText; + + /** + * Creates a new instance. + * + * @param model The model component. + */ + public StringProperty(ModelComponent model) { + this(model, ""); + } + + /** + * Creates a new instance with a value. + * + * @param model The model component. + * @param text The text. + */ + public StringProperty(ModelComponent model, String text) { + super(model); + fText = requireNonNull(text, "text"); + } + + @Override + public Object getComparableValue() { + return fText; + } + + /** + * Set the string. + * + * @param text the new string. + */ + public void setText(String text) { + fText = text; + } + + /** + * Returns the string. + * + * @return The String. + */ + public String getText() { + return fText; + } + + @Override + public String toString() { + return fText; + } + + @Override + public void copyFrom(Property property) { + StringProperty stringProperty = (StringProperty) property; + setText(stringProperty.getText()); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/StringSetProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/StringSetProperty.java new file mode 100644 index 0000000..00e0b88 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/StringSetProperty.java @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property that contains a quantity of strings. + */ +public class StringSetProperty + extends + AbstractComplexProperty { + + /** + * The strings. + */ + private List fItems = new ArrayList<>(); + + /** + * Creates a new instance. + * + * @param model The model component this property belongs to. + */ + public StringSetProperty(ModelComponent model) { + super(model); + } + + @Override + public Object getComparableValue() { + StringBuilder sb = new StringBuilder(); + + for (String s : fItems) { + sb.append(s); + } + + return sb.toString(); + } + + /** + * Adds a string. + * + * @param item The string to add. + */ + public void addItem(String item) { + fItems.add(item); + } + + /** + * Sets the list of strings. + * + * @param items The list. + */ + public void setItems(List items) { + fItems = items; + } + + /** + * Returns the list of string. + * + * @return The list. + */ + public List getItems() { + return fItems; + } + + @Override + public void copyFrom(Property property) { + StringSetProperty other = (StringSetProperty) property; + setItems(new ArrayList<>(other.getItems())); + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + Iterator e = getItems().iterator(); + + while (e.hasNext()) { + b.append(e.next()); + + if (e.hasNext()) { + b.append(", "); + } + } + + return b.toString(); + } + + @Override + public Object clone() { + StringSetProperty clone = (StringSetProperty) super.clone(); + clone.setItems(new ArrayList<>(getItems())); + + return clone; + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/SymbolProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/SymbolProperty.java new file mode 100644 index 0000000..b2c9ed8 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/SymbolProperty.java @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import org.opentcs.data.model.visualization.LocationRepresentation; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A property for a graphical symbol. + */ +public class SymbolProperty + extends + AbstractComplexProperty { + + /** + * The location representation. + */ + private LocationRepresentation locationRepresentation; + + /** + * Creates a new instance. + * + * @param model The model component this property belongs to. + */ + public SymbolProperty(ModelComponent model) { + super(model); + } + + @Override + public Object getComparableValue() { + return locationRepresentation; + } + + /** + * Set the location representation for this property. + * + * @param locationRepresentation The location representation. + */ + public void setLocationRepresentation(LocationRepresentation locationRepresentation) { + this.locationRepresentation = locationRepresentation; + } + + /** + * Returns the location representation for this property. + * + * @return The location representation. + */ + public LocationRepresentation getLocationRepresentation() { + return locationRepresentation; + } + + @Override // java.lang.Object + public String toString() { + if (fValue != null) { + return fValue.toString(); + } + + return locationRepresentation == null ? "" : locationRepresentation.name(); + } + + @Override // AbstractProperty + public void copyFrom(Property property) { + SymbolProperty symbolProperty = (SymbolProperty) property; + symbolProperty.setValue(null); + setLocationRepresentation(symbolProperty.getLocationRepresentation()); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/TripleProperty.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/TripleProperty.java new file mode 100644 index 0000000..73b2d4d --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/components/properties/type/TripleProperty.java @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import org.opentcs.data.model.Triple; +import org.opentcs.guing.base.model.ModelComponent; +import org.slf4j.LoggerFactory; + +/** + * A property for a 3 dimensional point. + */ +public class TripleProperty + extends + AbstractProperty { + + /** + * The point. + */ + private Triple fTriple; + + /** + * Creates a new instance. + * + * @param model The model component. + */ + public TripleProperty(ModelComponent model) { + super(model); + } + + @Override + public Object getComparableValue() { + return fTriple.getX() + "," + fTriple.getY() + "," + fTriple.getZ(); + } + + /** + * Set the value of this property. + * + * @param triple The triple. + */ + public void setValue(Triple triple) { + fTriple = triple; + } + + @Override + public Triple getValue() { + return fTriple; + } + + @Override + public String toString() { + return fTriple == null ? "null" + : String.format("(%d, %d, %d)", fTriple.getX(), fTriple.getY(), fTriple.getZ()); + } + + @Override + public void copyFrom(Property property) { + TripleProperty tripleProperty = (TripleProperty) property; + + try { + Triple foreignTriple = tripleProperty.getValue(); + setValue(foreignTriple); + } + catch (Exception e) { + LoggerFactory.getLogger(TripleProperty.class).error("Exception", e); + } + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/event/BlockChangeEvent.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/event/BlockChangeEvent.java new file mode 100644 index 0000000..775c668 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/event/BlockChangeEvent.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.event; + +import java.util.EventObject; +import org.opentcs.guing.base.model.elements.BlockModel; + +/** + * An event that informs listener about changes in a block area. + */ +public class BlockChangeEvent + extends + EventObject { + + /** + * Creates a new instance of BlockElementChangeEvent. + * + * @param block The BlockModel that has changed. + */ + public BlockChangeEvent(BlockModel block) { + super(block); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/event/BlockChangeListener.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/event/BlockChangeListener.java new file mode 100644 index 0000000..c5534ba --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/event/BlockChangeListener.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.event; + +/** + * Interface for listener that want to be informed wenn a block area has + * changed. + */ +public interface BlockChangeListener { + + /** + * Message that the course elements have changed. + * + * @param e The fire event. + */ + void courseElementsChanged(BlockChangeEvent e); + + /** + * Message that the color of the block area has changed. + * + * @param e The fire event. + */ + void colorChanged(BlockChangeEvent e); + + /** + * Message that a block area was removed. + * + * @param e The fire event. + */ + void blockRemoved(BlockChangeEvent e); +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/event/ConnectionChangeEvent.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/event/ConnectionChangeEvent.java new file mode 100644 index 0000000..177191e --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/event/ConnectionChangeEvent.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.event; + +import java.util.EventObject; +import org.opentcs.guing.base.model.elements.AbstractConnection; + +/** + * An event for changes on connections. + */ +public class ConnectionChangeEvent + extends + EventObject { + + /** + * Creates a new instance of ConnectionChangeEvent. + * + * @param connection The connection that has changed. + */ + public ConnectionChangeEvent(AbstractConnection connection) { + super(connection); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/event/ConnectionChangeListener.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/event/ConnectionChangeListener.java new file mode 100644 index 0000000..46d8664 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/event/ConnectionChangeListener.java @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.event; + +/** + * Interface for listeners that want to be informed on connection changes. + */ +public interface ConnectionChangeListener { + + /** + * Message from a connection that its components have changed. + * + * @param e The fired event. + */ + void connectionChanged(ConnectionChangeEvent e); +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/AbstractConnectableModelComponent.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/AbstractConnectableModelComponent.java new file mode 100644 index 0000000..0cc8947 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/AbstractConnectableModelComponent.java @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model; + +import java.util.ArrayList; +import java.util.List; +import org.opentcs.guing.base.model.elements.AbstractConnection; + +/** + * A {@link ModelComponent} that can be connected with another model component. + */ +public abstract class AbstractConnectableModelComponent + extends + AbstractModelComponent + implements + DrawnModelComponent, + ConnectableModelComponent { + + /** + * The Links and Paths connected with this model component (Location or Point). + */ + private List fConnections = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public AbstractConnectableModelComponent() { + // Do nada. + } + + @Override // ConnectableModelComponent + public void addConnection(AbstractConnection connection) { + fConnections.add(connection); + } + + @Override // ConnectableModelComponent + public void removeConnection(AbstractConnection connection) { + fConnections.remove(connection); + } + + @Override // ConnectableModelComponent + public List getConnections() { + return fConnections; + } + + /** + * Checks whether there is a connection to the given component. + * + * @param component The component. + * @return true if, and only if, this component is connected to + * the given one. + */ + public boolean hasConnectionTo(ConnectableModelComponent component) { + return getConnectionTo(component) != null; + } + + /** + * Returns the connection to the given component. + * + * @param component The component. + * @return The connection to the given component, or null, if + * there is none. + */ + public AbstractConnection getConnectionTo(ConnectableModelComponent component) { + for (AbstractConnection connection : fConnections) { + if (connection.getStartComponent() == this + && connection.getEndComponent() == component) { + return connection; + } + } + + return null; + } + + @Override // AbstractModelComponent + public AbstractModelComponent clone() + throws CloneNotSupportedException { + AbstractConnectableModelComponent clone = (AbstractConnectableModelComponent) super.clone(); + clone.fConnections = new ArrayList<>(); + + return clone; + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/AbstractModelComponent.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/AbstractModelComponent.java new file mode 100644 index 0000000..a6bd473 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/AbstractModelComponent.java @@ -0,0 +1,215 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.event.AttributesChangeListener; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.base.components.properties.type.StringProperty; + +/** + * Abstract implementation of a model component. + */ +public abstract class AbstractModelComponent + implements + ModelComponent { + + /** + * Name of the component in the tree view. + */ + private final String fTreeViewName; + /** + * Whether or not the component is visible in the tree view. + */ + private boolean fTreeViewVisibility = true; + /** + * The parent component. + */ + private transient ModelComponent fParent; + /** + * Attribute change listeners. + */ + private transient List fAttributesChangeListeners + = new CopyOnWriteArrayList<>(); + /** + * The actual parent of this component. PropertiesCollection e.g. + * overwrites it. + */ + private ModelComponent actualParent; + /** + * The component's attributes. + */ + private Map fProperties = new LinkedHashMap<>(); + + /** + * Creates a new instance. + */ + public AbstractModelComponent() { + this(""); + } + + /** + * Creates a new instance with the given name. + * + * @param treeViewName The name. + */ + public AbstractModelComponent(String treeViewName) { + fTreeViewName = requireNonNull(treeViewName, "treeViewName"); + } + + @Override + public void add(ModelComponent component) { + } + + @Override + public void remove(ModelComponent component) { + } + + @Override + public List getChildComponents() { + return new ArrayList<>(); + } + + @Override + public String getTreeViewName() { + return fTreeViewName; + } + + @Override + public boolean contains(ModelComponent component) { + return false; + } + + @Override + public ModelComponent getParent() { + return fParent; + } + + @Override + public ModelComponent getActualParent() { + return actualParent; + } + + @Override + public void setParent(ModelComponent parent) { + if (parent instanceof PropertiesCollection) { + actualParent = fParent; + } + fParent = parent; + } + + @Override + public boolean isTreeViewVisible() { + return fTreeViewVisibility; + } + + @Override + public final void setTreeViewVisibility(boolean visibility) { + fTreeViewVisibility = visibility; + } + + @Override // ModelComponent + public String getDescription() { + return ""; + } + + @Override + public String getName() { + StringProperty property = getPropertyName(); + return property == null ? "" : property.getText(); + } + + @Override + public void setName(String name) { + getPropertyName().setText(name); + } + + @Override + public Property getProperty(String name) { + return fProperties.get(name); + } + + @Override + public Map getProperties() { + return fProperties; + } + + @Override + public void setProperty(String name, Property property) { + fProperties.put(name, property); + } + + @Override + public void addAttributesChangeListener(AttributesChangeListener listener) { + if (fAttributesChangeListeners == null) { + fAttributesChangeListeners = new CopyOnWriteArrayList<>(); + } + + if (!fAttributesChangeListeners.contains(listener)) { + fAttributesChangeListeners.add(listener); + } + } + + @Override + public void removeAttributesChangeListener(AttributesChangeListener listener) { + if (fAttributesChangeListeners != null) { + fAttributesChangeListeners.remove(listener); + } + } + + @Override + public boolean containsAttributesChangeListener(AttributesChangeListener listener) { + if (fAttributesChangeListeners == null) { + return false; + } + + return fAttributesChangeListeners.contains(listener); + } + + @Override + public void propertiesChanged(AttributesChangeListener initiator) { + if (fAttributesChangeListeners != null) { + for (AttributesChangeListener listener : fAttributesChangeListeners) { + if (initiator != listener) { + listener.propertiesChanged(new AttributesChangeEvent(initiator, this)); + } + } + } + unmarkAllPropertyChanges(); + } + + @Override + public AbstractModelComponent clone() + throws CloneNotSupportedException { + AbstractModelComponent clonedModelComponent = (AbstractModelComponent) super.clone(); + clonedModelComponent.fAttributesChangeListeners = new CopyOnWriteArrayList<>(); + // "Shallow" copy of the Map + clonedModelComponent.fProperties = new LinkedHashMap<>(); + // "Deep" copy: clone all properties + for (Map.Entry entry : getProperties().entrySet()) { + Property clonedProperty = (Property) entry.getValue().clone(); + // XXX Don't clone the name but create a new, unique one here! + if (entry.getKey().equals(NAME)) { + ((StringProperty) clonedProperty).setText(""); + } + + clonedProperty.setModel(clonedModelComponent); + clonedModelComponent.setProperty(entry.getKey(), clonedProperty); + } + + return clonedModelComponent; + } + + private void unmarkAllPropertyChanges() { + for (Property property : fProperties.values()) { + property.unmarkChanged(); + } + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/BoundingBoxModel.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/BoundingBoxModel.java new file mode 100644 index 0000000..3fecd8f --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/BoundingBoxModel.java @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import org.opentcs.data.model.BoundingBox; +import org.opentcs.data.model.Couple; + +/** + * A representation of a {@link BoundingBox}. + */ +public class BoundingBoxModel { + + private final long length; + private final long width; + private final long height; + private final Couple referenceOffset; + + /** + * Creates a new instance. + * + * @param length The bounding box's length. + * @param width The bounding box's width. + * @param height The bounding box's height. + * @param referenceOffset The bounding box's reference offset. + */ + public BoundingBoxModel( + long length, + long width, + long height, + @Nonnull + Couple referenceOffset + ) { + this.length = length; + this.width = width; + this.height = height; + this.referenceOffset = requireNonNull(referenceOffset, "referenceOffset"); + } + + public long getLength() { + return length; + } + + public long getWidth() { + return width; + } + + public long getHeight() { + return height; + } + + public Couple getReferenceOffset() { + return referenceOffset; + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/CompositeModelComponent.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/CompositeModelComponent.java new file mode 100644 index 0000000..6f0438c --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/CompositeModelComponent.java @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model; + +import java.util.ArrayList; +import java.util.List; + +/** + * Abstract implementation of a composite model component that holds a set of child components. + */ +public abstract class CompositeModelComponent + extends + AbstractModelComponent { + + /** + * The child elements. + */ + private List fChildComponents = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public CompositeModelComponent() { + this("Composite"); + } + + /** + * Creates a new instance with the given name. + * + * @param treeViewName The name to be used. + */ + public CompositeModelComponent(String treeViewName) { + super(treeViewName); + } + + @Override // AbstractModelComponent + public void add(ModelComponent component) { + getChildComponents().add(component); + component.setParent(this); + } + + @Override // AbstractModelComponent + public List getChildComponents() { + return fChildComponents; + } + + public boolean hasProperties() { + return false; + } + + @Override // AbstractModelComponent + public void remove(ModelComponent component) { + getChildComponents().remove(component); + } + + @Override // AbstractModelComponent + public boolean contains(ModelComponent component) { + return getChildComponents().contains(component); + } + + @Override + public CompositeModelComponent clone() + throws CloneNotSupportedException { + CompositeModelComponent clone = (CompositeModelComponent) super.clone(); + clone.fChildComponents = new ArrayList<>(fChildComponents); + return clone; + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/ConnectableModelComponent.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/ConnectableModelComponent.java new file mode 100644 index 0000000..2db6851 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/ConnectableModelComponent.java @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model; + +import java.util.List; +import org.opentcs.guing.base.model.elements.AbstractConnection; + +/** + * A {@link ModelComponent} that can be connected with another model component. + */ +public interface ConnectableModelComponent + extends + ModelComponent { + + /** + * Adds a connection. + * + * @param connection The connection to be added. + */ + void addConnection(AbstractConnection connection); + + /** + * Returns all connections. + * + * @return All connections. + */ + List getConnections(); + + /** + * Removes a connection. + * + * @param connection The connection to be removed. + */ + void removeConnection(AbstractConnection connection); +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/DrawnModelComponent.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/DrawnModelComponent.java new file mode 100644 index 0000000..938c080 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/DrawnModelComponent.java @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model; + +import org.opentcs.guing.base.components.properties.type.LayerWrapperProperty; + +/** + * A model component that is drawn and represented by a corresponding figure. + */ +public interface DrawnModelComponent + extends + ModelComponent { + + /** + * The property key for the layer wrapper that contains the layer on which a model component + * (respectively its figure) is to be drawn. + */ + String LAYER_WRAPPER = "LAYER_WRAPPER"; + + /** + * Returns this component's layer wrapper property. + * + * @return This component's layer wrapper property. + */ + default LayerWrapperProperty getPropertyLayerWrapper() { + return (LayerWrapperProperty) getProperty(LAYER_WRAPPER); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/EnvelopeModel.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/EnvelopeModel.java new file mode 100644 index 0000000..038f122 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/EnvelopeModel.java @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.List; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Envelope; + +/** + * A representation of an {@link Envelope} with an additional (reference) key. + */ +public class EnvelopeModel { + + private final String key; + private final List vertices; + + /** + * Creates a new instance. + * + * @param key The key to be used for referencing the envelope. + * @param vertices The sequence of vertices the envelope consists of. + */ + public EnvelopeModel( + @Nonnull + String key, + @Nonnull + List vertices + ) { + this.key = requireNonNull(key, "key"); + this.vertices = requireNonNull(vertices, "vertices"); + } + + @Nonnull + public String getKey() { + return key; + } + + @Nonnull + public List getVertices() { + return vertices; + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/FigureDecorationDetails.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/FigureDecorationDetails.java new file mode 100644 index 0000000..1350afa --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/FigureDecorationDetails.java @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model; + +import jakarta.annotation.Nonnull; +import java.util.Map; +import java.util.Set; +import org.opentcs.guing.base.AllocationState; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.VehicleModel; + +/** + * Provides methods to configure the data that is used to decorate a model component's figure. + */ +public interface FigureDecorationDetails { + + /** + * Returns a map of vehicles that claim or allocate the resource (the figure is associated with) + * to the respective allocation state. + *

    + * This information is used to decorate a model component's figure to indicate that it is part of + * the route of the respective vehicles. + * + * @return A map of vehicles to allocation states. + */ + @Nonnull + Map getAllocationStates(); + + /** + * Updates the allocation state for the given vehicle. + * + * @param model The vehicle model. + * @param allocationState The vehicle's new allocation state. + */ + void updateAllocationState( + @Nonnull + VehicleModel model, + @Nonnull + AllocationState allocationState + ); + + /** + * Clears the allocation state for the given vehicle. + * + * @param model The vehicle model. + */ + void clearAllocationState( + @Nonnull + VehicleModel model + ); + + /** + * Adds a block model. + * + * @param model The block model. + */ + void addBlockModel(BlockModel model); + + /** + * Removes a block model. + * + * @param model The block model. + */ + void removeBlockModel(BlockModel model); + + /** + * Returns a set of block models for which a model component's figure is to be decorated to + * indicate that the model component is part of the respective block. + * + * @return A set of block models. + */ + Set getBlockModels(); +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/ModelComponent.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/ModelComponent.java new file mode 100644 index 0000000..dd87d3a --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/ModelComponent.java @@ -0,0 +1,197 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model; + +import java.util.List; +import java.util.Map; +import org.opentcs.guing.base.components.properties.event.AttributesChangeListener; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.base.components.properties.type.StringProperty; + +/** + * Defines a component in the system model. + */ +public interface ModelComponent + extends + Cloneable { + + /** + * Key for the name property. + */ + String NAME = "Name"; + /** + * Key for the miscellaneous properties. + */ + String MISCELLANEOUS = "Miscellaneous"; + + /** + * Adds a child component. + * + * @param component The model component to add. + */ + void add(ModelComponent component); + + /** + * Removes a child component. + * + * @param component The model component to remove. + */ + void remove(ModelComponent component); + + /** + * Returns all child components. + * + * @return A list of all child components. + */ + List getChildComponents(); + + /** + * Retruns this model component's name that is displayed in the tree view. + * + * @return The name that is displayed in the tree view. + */ + String getTreeViewName(); + + /** + * Returns whether the given component is a child of this model component. + * + * @param component The component. + * @return {@code true}, if the given component is a child of this model component, otherwise + * {@code false}. + */ + boolean contains(ModelComponent component); + + /** + * Returns this model component's parent component. + * + * @return The parent component. + */ + ModelComponent getParent(); + + /** + * Returns the actual parent of this model component. + * PropertiesCollection e.g. overwrites it. May be null. + * + * @return The actual parent. + */ + ModelComponent getActualParent(); + + /** + * Set this model component's parent component. + * + * @param parent The new parent component. + */ + void setParent(ModelComponent parent); + + /** + * Returns whether this model component is to be shown in the tree view. + * + * @return {@code true}, if the model component is to be shown in the tree view, otherwise + * {@code false}. + */ + boolean isTreeViewVisible(); + + /** + * Sets this model component's visibility in the tree view. + * + * @param visibility Whether the model component is to be shown in the tree view or not. + */ + void setTreeViewVisibility(boolean visibility); + + /** + * Returns a description for the model component. + * + * @return A description for the model component. + */ + String getDescription(); + + /** + * Returns the name of the component. + * + * @return The name of the component. + */ + String getName(); + + /** + * Sets this model component's name. + * + * @param name The new name. + */ + void setName(String name); + + /** + * Returns the property with the given name. + * + * @param name The name of the property. + * @return The property with the given name. + */ + Property getProperty(String name); + + /** + * Returns all properties. + * + * @return A map containing all properties. + */ + Map getProperties(); + + /** + * Sets the property with the given name. + * + * @param name The name of the property. + * @param property The property. + */ + void setProperty(String name, Property property); + + /** + * Returns this component's name property. + * + * @return This component's name property. + */ + default StringProperty getPropertyName() { + return (StringProperty) getProperty(NAME); + } + + /** + * Adds the given {@link AttributesChangeListener}. + * The {@link AttributesChangeListener} is notified when properties of the model component + * have changed. + * + * @param l The listener. + */ + void addAttributesChangeListener(AttributesChangeListener l); + + /** + * Removes the given {@link AttributesChangeListener}. + * + * @param l The listener. + */ + void removeAttributesChangeListener(AttributesChangeListener l); + + /** + * Returns whether the given {@link AttributesChangeListener} is already registered with the model + * component. + * + * @param l The listener. + * @return {@code true}, if the given {@link AttributesChangeListener} is already registered with + * the model component, otherwise {@code false}. + */ + boolean containsAttributesChangeListener(AttributesChangeListener l); + + /** + * Notifies all registered {@link AttributesChangeListener}s that properties of the model + * component have changed. + * + * @param l The initiator of the change. + */ + void propertiesChanged(AttributesChangeListener l); + + /** + * Clones this ModelComponent. + * + * @return A clone of this ModelComponent. + * @throws java.lang.CloneNotSupportedException If the model component doesn't implement the + * {@link Cloneable} interface. + */ + ModelComponent clone() + throws CloneNotSupportedException; +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/PeripheralOperationModel.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/PeripheralOperationModel.java new file mode 100644 index 0000000..d7b650c --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/PeripheralOperationModel.java @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import org.opentcs.data.peripherals.PeripheralOperation; + +/** + * A Bean to hold the Peripheral operation. + */ +public class PeripheralOperationModel { + + /** + * The operation to be performed by the peripheral device. + */ + private final String operation; + /** + * The name of the location the peripheral device is associated with. + */ + @Nonnull + private final String locationName; + /** + * The moment at which this operation is to be performed. + */ + @Nonnull + private final PeripheralOperation.ExecutionTrigger executionTrigger; + /** + * Whether the completion of this operation is required to allow a vehicle to continue driving. + */ + private final boolean completionRequired; + + public PeripheralOperationModel( + String locationName, + String operation, + PeripheralOperation.ExecutionTrigger executionTrigger, + boolean completionRequired + ) { + this.operation = requireNonNull(operation, "operation"); + this.locationName = requireNonNull(locationName, "locationName"); + this.executionTrigger = requireNonNull(executionTrigger, "executionTrigger"); + this.completionRequired = requireNonNull(completionRequired, "completionRequired"); + } + + public String getOperation() { + return operation; + } + + public String getLocationName() { + return locationName; + } + + public PeripheralOperation.ExecutionTrigger getExecutionTrigger() { + return executionTrigger; + } + + public boolean isCompletionRequired() { + return completionRequired; + } + +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/PositionableModelComponent.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/PositionableModelComponent.java new file mode 100644 index 0000000..b71f6b7 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/PositionableModelComponent.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model; + +/** + * Defines constants for {@link ModelComponent}s that have model coordinates. + */ +public interface PositionableModelComponent + extends + ModelComponent { + + /** + * Key for the X (model) cordinate. + */ + String MODEL_X_POSITION = "modelXPosition"; + /** + * Key for the Y (model) cordinate. + */ + String MODEL_Y_POSITION = "modelYPosition"; +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/PropertiesCollection.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/PropertiesCollection.java new file mode 100644 index 0000000..3e3b95c --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/PropertiesCollection.java @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model; + +import static org.opentcs.guing.base.I18nPlantOverviewBase.BUNDLE_PATH; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.ResourceBundle; +import org.opentcs.guing.base.components.properties.event.AttributesChangeListener; +import org.opentcs.guing.base.components.properties.type.AbstractProperty; +import org.opentcs.guing.base.components.properties.type.MultipleDifferentValues; +import org.opentcs.guing.base.components.properties.type.Property; + +/** + * Allows to change properties with the same name of multiple model components at the same time. + */ +public class PropertiesCollection + extends + CompositeModelComponent { + + /** + * This class's resource bundle. + */ + private final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_PATH); + + /** + * Creates a new instance. + * + * @param models The model components. + */ + @SuppressWarnings("this-escape") + public PropertiesCollection(Collection models) { + Objects.requireNonNull(models, "models is null"); + + for (ModelComponent curModel : models) { + add(curModel); + } + + extractSameProperties(); + } + + /** + * Finds the properties that can be collectively edited. + */ + private void extractSameProperties() { + if (getChildComponents().isEmpty()) { + return; + } + ModelComponent firstModel = getChildComponents().get(0); + Map properties = firstModel.getProperties(); + + for (Map.Entry curEntry : properties.entrySet()) { + String name = curEntry.getKey(); + Property property = curEntry.getValue(); + boolean ok = true; + boolean differentValues = false; + + if (!property.isCollectiveEditable()) { + ok = false; + } + + for (int i = 1; i < getChildComponents().size(); i++) { + ModelComponent followingModel = getChildComponents().get(i); + if (!firstModel.getClass().equals(followingModel.getClass())) { + return; + } + + Property pendant = followingModel.getProperty(name); + + if (pendant == null) { + ok = false; + break; + } + else if ((!pendant.isCollectiveEditable())) { + ok = false; + break; + } + + if (!(property.getComparableValue() == null && pendant.getComparableValue() == null) + && (property.getComparableValue() != null && pendant.getComparableValue() == null + || property.getComparableValue() == null && pendant.getComparableValue() != null + || !property.getComparableValue().equals(pendant.getComparableValue()))) { + differentValues = true; + } + } + + if (ok) { + AbstractProperty clone = (AbstractProperty) property.clone(); + + if (differentValues) { + clone.setValue(new MultipleDifferentValues()); + + clone.unmarkChanged(); + + clone.setDescription(property.getDescription()); + clone.setHelptext(property.getHelptext()); + clone.setModellingEditable(property.isModellingEditable()); + clone.setOperatingEditable(property.isOperatingEditable()); + setProperty(name, clone); + } + else { + // TODO: clone() Methode ? + clone.setDescription(property.getDescription()); + clone.setHelptext(property.getHelptext()); + clone.setModellingEditable(property.isModellingEditable()); + clone.setOperatingEditable(property.isOperatingEditable()); + setProperty(name, clone); + } + } + } + } + + /** + * Notifies the registered listeners about the changed properties. + * + * @param listener The listener that initiated the property change. + */ + @Override // AbstractModelComponent + public void propertiesChanged(AttributesChangeListener listener) { + for (ModelComponent model : getChildComponents()) { + copyPropertiesToModel(model); + model.propertiesChanged(listener); + } + + extractSameProperties(); + } + + /** + * Copies the properties to the model component. + * + * @param model The model component to copy properties to. + */ + protected void copyPropertiesToModel(ModelComponent model) { + for (String name : getProperties().keySet()) { + Property property = getProperty(name); + if (property.hasChanged()) { + Property modelProperty = model.getProperty(name); + modelProperty.copyFrom(property); + modelProperty.markChanged(); + } + } + } + + @Override // AbstractModelComponent + public String getDescription() { + return bundle.getString("propertiesCollection.description"); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/SimpleFolder.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/SimpleFolder.java new file mode 100644 index 0000000..c179a84 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/SimpleFolder.java @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model; + +/** + * The simplest form of a component in the system model that contains child elements. + * SimpleFolder is used for plain folders. + */ +public class SimpleFolder + extends + CompositeModelComponent { + + /** + * Creates a new instance. + * + * @param name The name of the folder. + */ + public SimpleFolder(String name) { + super(name); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/AbstractConnection.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/AbstractConnection.java new file mode 100644 index 0000000..be8b4ad --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/AbstractConnection.java @@ -0,0 +1,284 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model.elements; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.event.AttributesChangeListener; +import org.opentcs.guing.base.components.properties.event.NullAttributesChangeListener; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.base.event.ConnectionChangeEvent; +import org.opentcs.guing.base.event.ConnectionChangeListener; +import org.opentcs.guing.base.model.AbstractConnectableModelComponent; +import org.opentcs.guing.base.model.AbstractModelComponent; +import org.opentcs.guing.base.model.DrawnModelComponent; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * Abstract implementation for connections: + *

      + *
    1. between two points {@link PathModel}
    2. + *
    3. between a point and a location {@link LinkModel}
    4. + *
    + */ +public abstract class AbstractConnection + extends + AbstractModelComponent + implements + DrawnModelComponent, + AttributesChangeListener { + + /** + * Key for the start component. + */ + public static final String START_COMPONENT = "startComponent"; + /** + * Key for the end component. + */ + public static final String END_COMPONENT = "endComponent"; + /** + * The start component (Location or Point). + */ + private transient ModelComponent fStartComponent; + /** + * The end component (Location or Point). + */ + private transient ModelComponent fEndComponent; + /** + * Listeners that are interested in changes of the connected objects. + */ + private transient List fConnectionChangeListeners = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public AbstractConnection() { + // Do nada. + } + + /** + * Returns this connection's start component. + * + * @return The start component. + */ + public ModelComponent getStartComponent() { + return fStartComponent; + } + + /** + * Returns this connection's end component. + * + * @return The end component. + */ + public ModelComponent getEndComponent() { + return fEndComponent; + } + + /** + * Sets this connection's start and end component. + * + * @param startComponent The start component. + * @param endComponent The end component. + */ + public void setConnectedComponents( + ModelComponent startComponent, + ModelComponent endComponent + ) { + updateListenerRegistrations(startComponent, endComponent); + updateComponents(startComponent, endComponent); + + // TODO: Points and locations are still missing an implementation of equals(). + if (!Objects.equals(fStartComponent, startComponent) + || !Objects.equals(fEndComponent, endComponent)) { + fStartComponent = startComponent; + fEndComponent = endComponent; + connectionChanged(); + } + } + + /** + * Notifies this connection that it is being removed. + */ + public void removingConnection() { + if (fStartComponent != null) { + fStartComponent.removeAttributesChangeListener(this); + + if (fStartComponent instanceof AbstractConnectableModelComponent) { + ((AbstractConnectableModelComponent) fStartComponent).removeConnection(this); + } + } + + if (fEndComponent != null) { + fEndComponent.removeAttributesChangeListener(this); + + if (fEndComponent instanceof AbstractConnectableModelComponent) { + ((AbstractConnectableModelComponent) fEndComponent).removeConnection(this); + } + } + } + + /** + * Adds a listener. + * + * @param listener The new listener. + */ + public void addConnectionChangeListener(ConnectionChangeListener listener) { + if (fConnectionChangeListeners == null) { + fConnectionChangeListeners = new ArrayList<>(); + } + + if (!fConnectionChangeListeners.contains(listener)) { + fConnectionChangeListeners.add(listener); + } + } + + /** + * Removes a listener. + * + * @param listener The listener to remove. + */ + public void removeConnectionChangeListener(ConnectionChangeListener listener) { + fConnectionChangeListeners.remove(listener); + } + + public StringProperty getPropertyStartComponent() { + return (StringProperty) getProperty(START_COMPONENT); + } + + public StringProperty getPropertyEndComponent() { + return (StringProperty) getProperty(END_COMPONENT); + } + + @Override // AttributesChangeListener + public void propertiesChanged(AttributesChangeEvent e) { + if (getStartComponent().getPropertyName().hasChanged() + || getEndComponent().getPropertyName().hasChanged()) { + if (nameFulfillsConvention()) { + updateName(); + } + else { + // Don't forget to update these properties. + getPropertyStartComponent().setText(fStartComponent.getName()); + getPropertyEndComponent().setText(fEndComponent.getName()); + } + } + } + + @Override + public AbstractConnection clone() + throws CloneNotSupportedException { + AbstractConnection clone = (AbstractConnection) super.clone(); + clone.fConnectionChangeListeners = new ArrayList<>(); + + return clone; + } + + /** + * Removes the current start and end components and establishes this connection + * between the given components. + * + * @param startComponent The new start component. + * @param endComponent The new end component. + */ + private void updateComponents( + ModelComponent startComponent, + ModelComponent endComponent + ) { + if (fStartComponent instanceof AbstractConnectableModelComponent) { + ((AbstractConnectableModelComponent) fStartComponent).removeConnection(this); + } + + if (fEndComponent instanceof AbstractConnectableModelComponent) { + ((AbstractConnectableModelComponent) fEndComponent).removeConnection(this); + } + + if (startComponent instanceof AbstractConnectableModelComponent) { + ((AbstractConnectableModelComponent) startComponent).addConnection(this); + } + + if (endComponent instanceof AbstractConnectableModelComponent) { + ((AbstractConnectableModelComponent) endComponent).addConnection(this); + } + } + + /** + * Informs all listener that the start and/or end component have changed. + */ + private void connectionChanged() { + if (fConnectionChangeListeners == null) { + return; + } + + for (ConnectionChangeListener listener : fConnectionChangeListeners) { + listener.connectionChanged(new ConnectionChangeEvent(this)); + } + } + + /** + * Deregistrates and reregistrates itself as a listener on the + * connected components. This is important as the name of the connection + * is dependant on the connected components. + * + * @param startComponent The new start component to register with. + * @param endComponent The new end component to register with. + */ + private void updateListenerRegistrations( + ModelComponent startComponent, + ModelComponent endComponent + ) { + if (fStartComponent != null) { + fStartComponent.removeAttributesChangeListener(this); + } + + if (fEndComponent != null) { + fEndComponent.removeAttributesChangeListener(this); + } + + startComponent.addAttributesChangeListener(this); + endComponent.addAttributesChangeListener(this); + } + + /** + * Refreshes the name of this connection. + */ + public void updateName() { + StringProperty property = getPropertyName(); + + if (property != null) { + String oldName = property.getText(); + String newName = getStartComponent().getName() + " --- " + getEndComponent().getName(); + property.setText(newName); + + if (!newName.equals(oldName)) { + property.markChanged(); + } + + propertiesChanged(new NullAttributesChangeListener()); + } + getPropertyStartComponent().setText(fStartComponent.getName()); + getPropertyEndComponent().setText(fEndComponent.getName()); + } + + /** + * Checks if the name of this connection follows a defined naming scheme. + * Definition: {@literal --- } + * + * @return {@code true}, if the name fulfills the convention, otherwise {@code false}. + */ + private boolean nameFulfillsConvention() { + StringProperty property = getPropertyName(); + if (property != null) { + String name = property.getText(); + String nameByConvention = getPropertyStartComponent().getText() + " --- " + + getPropertyEndComponent().getText(); + + if (name.equals(nameByConvention)) { + return true; + } + } + + return false; + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/BlockModel.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/BlockModel.java new file mode 100644 index 0000000..5854a1b --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/BlockModel.java @@ -0,0 +1,259 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model.elements; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.guing.base.I18nPlantOverviewBase.BUNDLE_PATH; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.ResourceBundle; +import org.opentcs.data.model.visualization.ElementPropKeys; +import org.opentcs.guing.base.components.properties.event.AttributesChangeListener; +import org.opentcs.guing.base.components.properties.type.BlockTypeProperty; +import org.opentcs.guing.base.components.properties.type.ColorProperty; +import org.opentcs.guing.base.components.properties.type.KeyValueSetProperty; +import org.opentcs.guing.base.components.properties.type.SelectionProperty; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.base.components.properties.type.StringSetProperty; +import org.opentcs.guing.base.event.BlockChangeEvent; +import org.opentcs.guing.base.event.BlockChangeListener; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.SimpleFolder; + +/** + * A block area. Contains figure components that are part of this block + * area. + */ +public class BlockModel + extends + SimpleFolder { + + /** + * The key/name of the 'type' property. + */ + public static final String TYPE = "Type"; + /** + * The key/name of the 'elements' property. + */ + public static final String ELEMENTS = "blockElements"; + /** + * This class's resource bundle. + */ + private final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_PATH); + /** + * A list of change listeners for this object. + */ + private List fListeners = new ArrayList<>(); + + /** + * Creates a new instance. + */ + @SuppressWarnings("this-escape") + public BlockModel() { + super(""); + createProperties(); + } + + @Override // AbstractModelComponent + public String getTreeViewName() { + String treeViewName = getDescription() + " " + getName(); + + return treeViewName; + } + + @Override // AbstractModelComponent + public String getDescription() { + return bundle.getString("blockModel.description"); + } + + @Override // AbstractModelComponent + public void propertiesChanged(AttributesChangeListener listener) { + if (getPropertyColor().hasChanged()) { + colorChanged(); + } + + super.propertiesChanged(listener); + } + + /** + * Returns the color of this block area. + * + * @return The color. + */ + public Color getColor() { + return getPropertyColor().getColor(); + } + + /** + * Adds an element to this block area. + * + * @param model The component to add. + */ + public void addCourseElement(ModelComponent model) { + if (!contains(model)) { + getChildComponents().add(model); + String addedModelName = model.getName(); + if (!getPropertyElements().getItems().contains(addedModelName)) { + getPropertyElements().addItem(addedModelName); + } + } + } + + /** + * Removes an element from this block area. + * + * @param model The component to remove. + */ + public void removeCourseElement(ModelComponent model) { + if (contains(model)) { + remove(model); + getPropertyElements().getItems().remove(model.getName()); + } + } + + /** + * Removes all elements in this block area. + */ + public void removeAllCourseElements() { + for (Object o : new ArrayList<>(getChildComponents())) { + remove((ModelComponent) o); + } + } + + /** + * Adds a listeners that gets informed when this block members change. + * + * @param listener The new listener. + */ + public void addBlockChangeListener(BlockChangeListener listener) { + if (fListeners == null) { + fListeners = new ArrayList<>(); + } + + if (!fListeners.contains(listener)) { + fListeners.add(listener); + } + } + + /** + * Removes a listener. + * + * @param listener The listener to remove. + */ + public void removeBlockChangeListener(BlockChangeListener listener) { + fListeners.remove(listener); + } + + /** + * Informs all listeners that the block elements have changed. + */ + public void courseElementsChanged() { + for (BlockChangeListener listener : fListeners) { + listener.courseElementsChanged(new BlockChangeEvent(this)); + } + } + + /** + * Informs all listeners that the color of this block has changed. + */ + public void colorChanged() { + for (BlockChangeListener listener : fListeners) { + listener.colorChanged(new BlockChangeEvent(this)); + } + } + + /** + * Informs all listeners that this block was removed. + */ + public void blockRemoved() { + for (BlockChangeListener listener : new ArrayList<>(fListeners)) { + listener.blockRemoved(new BlockChangeEvent(this)); + } + } + + public ColorProperty getPropertyColor() { + return (ColorProperty) getProperty(ElementPropKeys.BLOCK_COLOR); + } + + @SuppressWarnings("unchecked") + public SelectionProperty getPropertyType() { + return (SelectionProperty) getProperty(TYPE); + } + + public StringSetProperty getPropertyElements() { + return (StringSetProperty) getProperty(ELEMENTS); + } + + public KeyValueSetProperty getPropertyMiscellaneous() { + return (KeyValueSetProperty) getProperty(MISCELLANEOUS); + } + + private void createProperties() { + StringProperty pName = new StringProperty(this); + pName.setDescription(bundle.getString("blockModel.property_name.description")); + pName.setHelptext(bundle.getString("blockModel.property_name.helptext")); + setProperty(NAME, pName); + + ColorProperty pColor = new ColorProperty(this, Color.red); + pColor.setDescription(bundle.getString("blockModel.property_color.description")); + pColor.setHelptext(bundle.getString("blockModel.property_color.helptext")); + setProperty(ElementPropKeys.BLOCK_COLOR, pColor); + + BlockTypeProperty pType = new BlockTypeProperty( + this, + Arrays.asList(Type.values()), + Type.values()[0] + ); + pType.setDescription(bundle.getString("blockModel.property_type.description")); + pType.setHelptext(bundle.getString("blockModel.property_type.helptext")); + pType.setCollectiveEditable(true); + setProperty(TYPE, pType); + + StringSetProperty pElements = new StringSetProperty(this); + pElements.setDescription(bundle.getString("blockModel.property_elements.description")); + pElements.setHelptext(bundle.getString("blockModel.property_elements.helptext")); + pElements.setModellingEditable(false); + pElements.setOperatingEditable(false); + setProperty(ELEMENTS, pElements); + + KeyValueSetProperty pMiscellaneous = new KeyValueSetProperty(this); + pMiscellaneous.setDescription( + bundle.getString("blockModel.property_miscellaneous.description") + ); + pMiscellaneous.setHelptext(bundle.getString("blockModel.property_miscellaneous.helptext")); + pMiscellaneous.setOperatingEditable(true); + setProperty(MISCELLANEOUS, pMiscellaneous); + } + + /** + * The supported point types. + */ + public enum Type { + + /** + * Single vehicle only allowed. + */ + SINGLE_VEHICLE_ONLY( + ResourceBundle.getBundle(BUNDLE_PATH) + .getString("blockModel.type.singleVehicleOnly.description")), + /** + * Same direction only allowed. + */ + SAME_DIRECTION_ONLY( + ResourceBundle.getBundle(BUNDLE_PATH) + .getString("blockModel.type.sameDirectionOnly.description")); + + private final String description; + + Type(String description) { + this.description = requireNonNull(description, "description"); + } + + public String getDescription() { + return description; + } + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/LayoutModel.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/LayoutModel.java new file mode 100644 index 0000000..d720e5e --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/LayoutModel.java @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model.elements; + +import static org.opentcs.guing.base.I18nPlantOverviewBase.BUNDLE_PATH; + +import java.util.HashMap; +import java.util.ResourceBundle; +import org.opentcs.guing.base.components.properties.type.KeyValueSetProperty; +import org.opentcs.guing.base.components.properties.type.LayerGroupsProperty; +import org.opentcs.guing.base.components.properties.type.LayerWrappersProperty; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.base.model.CompositeModelComponent; + +/** + * Basic implementation of a layout. + */ +public class LayoutModel + extends + CompositeModelComponent { + + /** + * The key/name of the 'scale X' property. + */ + public static final String SCALE_X = "scaleX"; + /** + * The key/name of the 'scale Y' property. + */ + public static final String SCALE_Y = "scaleY"; + /** + * The key/name of the 'layer wrappers' property. + */ + public static final String LAYERS_WRAPPERS = "layerWrappers"; + /** + * The key/name of the 'layer groups' property. + */ + public static final String LAYER_GROUPS = "layerGroups"; + /** + * This class's resource bundle. + */ + private final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_PATH); + + /** + * Creates a new instance. + */ + @SuppressWarnings("this-escape") + public LayoutModel() { + super(ResourceBundle.getBundle(BUNDLE_PATH).getString("layoutModel.treeViewName")); + createProperties(); + } + + @Override // AbstractModelComponent + public String getDescription() { + return bundle.getString("layoutModel.description"); + } + + public LengthProperty getPropertyScaleX() { + return (LengthProperty) getProperty(SCALE_X); + } + + public LengthProperty getPropertyScaleY() { + return (LengthProperty) getProperty(SCALE_Y); + } + + public KeyValueSetProperty getPropertyMiscellaneous() { + return (KeyValueSetProperty) getProperty(MISCELLANEOUS); + } + + public LayerWrappersProperty getPropertyLayerWrappers() { + return (LayerWrappersProperty) getProperty(LAYERS_WRAPPERS); + } + + public LayerGroupsProperty getPropertyLayerGroups() { + return (LayerGroupsProperty) getProperty(LAYER_GROUPS); + } + + private void createProperties() { + StringProperty pName = new StringProperty(this); + pName.setDescription(bundle.getString("layoutModel.property_name.description")); + pName.setHelptext(bundle.getString("layoutModel.property_name.helptext")); + setProperty(NAME, pName); + + LengthProperty pScaleX = new LengthProperty(this); + pScaleX.setDescription(bundle.getString("layoutModel.property_scaleX.description")); + pScaleX.setHelptext(bundle.getString("layoutModel.property_scaleX.helptext")); + setProperty(SCALE_X, pScaleX); + + LengthProperty pScaleY = new LengthProperty(this); + pScaleY.setDescription(bundle.getString("layoutModel.property_scaleY.description")); + pScaleY.setHelptext(bundle.getString("layoutModel.property_scaleY.helptext")); + setProperty(SCALE_Y, pScaleY); + + KeyValueSetProperty pMiscellaneous = new KeyValueSetProperty(this); + pMiscellaneous.setDescription( + bundle.getString("layoutModel.property_miscellaneous.description") + ); + pMiscellaneous.setHelptext(bundle.getString("layoutModel.property_miscellaneous.helptext")); + pMiscellaneous.setOperatingEditable(true); + setProperty(MISCELLANEOUS, pMiscellaneous); + + LayerWrappersProperty pLayerWrappers = new LayerWrappersProperty(this, new HashMap<>()); + pLayerWrappers.setDescription( + bundle.getString("layoutModel.property_layerWrappers.description") + ); + pLayerWrappers.setHelptext(bundle.getString("layoutModel.property_layerWrappers.helptext")); + pLayerWrappers.setModellingEditable(false); + setProperty(LAYERS_WRAPPERS, pLayerWrappers); + + LayerGroupsProperty pLayerGroups = new LayerGroupsProperty(this, new HashMap<>()); + pLayerGroups.setDescription(bundle.getString("layoutModel.property_layerGroups.description")); + pLayerGroups.setHelptext(bundle.getString("layoutModel.property_layerGroups.helptext")); + pLayerGroups.setModellingEditable(false); + setProperty(LAYER_GROUPS, pLayerGroups); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/LinkModel.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/LinkModel.java new file mode 100644 index 0000000..28cfdc8 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/LinkModel.java @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model.elements; + +import static org.opentcs.guing.base.I18nPlantOverviewBase.BUNDLE_PATH; + +import java.util.ResourceBundle; +import org.opentcs.guing.base.components.layer.NullLayerWrapper; +import org.opentcs.guing.base.components.properties.type.LayerWrapperProperty; +import org.opentcs.guing.base.components.properties.type.LinkActionsProperty; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.base.components.properties.type.StringSetProperty; + +/** + * Basic implementation of link between a point and a location. + */ +public class LinkModel + extends + AbstractConnection { + + /** + * The key for the possible actions on the location. + */ + public static final String ALLOWED_OPERATIONS = "AllowedOperations"; + /** + * This class's resource bundle. + */ + private final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_PATH); + + /** + * Creates a new instance. + */ + @SuppressWarnings("this-escape") + public LinkModel() { + createProperties(); + } + + /** + * Returns the connected point. + * + * @return The model of the connected Point. + */ + public PointModel getPoint() { + if (getStartComponent() instanceof PointModel) { + return (PointModel) getStartComponent(); + } + + if (getEndComponent() instanceof PointModel) { + return (PointModel) getEndComponent(); + } + + return null; + } + + /** + * Returns the connected location. + * + * @return The model of the connected Location. + */ + public LocationModel getLocation() { + if (getStartComponent() instanceof LocationModel) { + return (LocationModel) getStartComponent(); + } + + if (getEndComponent() instanceof LocationModel) { + return (LocationModel) getEndComponent(); + } + + return null; + } + + @Override // AbstractModelComponent + public String getDescription() { + return bundle.getString("linkModel.description"); + } + + public StringSetProperty getPropertyAllowedOperations() { + return (StringSetProperty) getProperty(ALLOWED_OPERATIONS); + } + + private void createProperties() { + StringProperty pName = new StringProperty(this); + pName.setDescription(bundle.getString("linkModel.property_name.description")); + pName.setHelptext(bundle.getString("linkModel.property_name.helptext")); + // The name of a link cannot be changed because it is not stored in the kernel model + pName.setModellingEditable(false); + setProperty(NAME, pName); + + StringSetProperty pOperations = new LinkActionsProperty(this); + pOperations.setDescription(bundle.getString("linkModel.property_operations.description")); + pOperations.setHelptext(bundle.getString("linkModel.property_operations.helptext")); + setProperty(ALLOWED_OPERATIONS, pOperations); + + StringProperty startComponent = new StringProperty(this); + startComponent.setDescription( + bundle.getString("linkModel.property_startComponent.description") + ); + startComponent.setModellingEditable(false); + startComponent.setOperatingEditable(false); + setProperty(START_COMPONENT, startComponent); + + StringProperty endComponent = new StringProperty(this); + endComponent.setDescription(bundle.getString("linkModel.property_endComponent.description")); + endComponent.setModellingEditable(false); + endComponent.setOperatingEditable(false); + setProperty(END_COMPONENT, endComponent); + + LayerWrapperProperty pLayerWrapper = new LayerWrapperProperty(this, new NullLayerWrapper()); + pLayerWrapper.setDescription(bundle.getString("linkModel.property_layerWrapper.description")); + pLayerWrapper.setHelptext(bundle.getString("linkModel.property_layerWrapper.helptext")); + pLayerWrapper.setModellingEditable(false); + setProperty(LAYER_WRAPPER, pLayerWrapper); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/LocationModel.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/LocationModel.java new file mode 100644 index 0000000..2390396 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/LocationModel.java @@ -0,0 +1,444 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model.elements; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.guing.base.I18nPlantOverviewBase.BUNDLE_PATH; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import org.opentcs.data.ObjectPropConstants; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.visualization.ElementPropKeys; +import org.opentcs.data.model.visualization.LocationRepresentation; +import org.opentcs.guing.base.AllocationState; +import org.opentcs.guing.base.components.layer.NullLayerWrapper; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.event.AttributesChangeListener; +import org.opentcs.guing.base.components.properties.type.BooleanProperty; +import org.opentcs.guing.base.components.properties.type.CoordinateProperty; +import org.opentcs.guing.base.components.properties.type.KeyValueSetProperty; +import org.opentcs.guing.base.components.properties.type.LayerWrapperProperty; +import org.opentcs.guing.base.components.properties.type.LocationTypeProperty; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.base.components.properties.type.SymbolProperty; +import org.opentcs.guing.base.model.AbstractConnectableModelComponent; +import org.opentcs.guing.base.model.AbstractModelComponent; +import org.opentcs.guing.base.model.FigureDecorationDetails; +import org.opentcs.guing.base.model.PositionableModelComponent; + +/** + * Basic implementation for every kind of location. + */ +public class LocationModel + extends + AbstractConnectableModelComponent + implements + AttributesChangeListener, + PositionableModelComponent, + FigureDecorationDetails { + + /** + * The property key for the location's type. + */ + public static final String TYPE = "Type"; + /** + * Key for the locked state. + */ + public static final String LOCKED = "locked"; + /** + * Key for the reservation token. + */ + public static final String PERIPHERAL_RESERVATION_TOKEN = "peripheralReservationToken"; + /** + * Key for the peripheral state. + */ + public static final String PERIPHERAL_STATE = "peripheralState"; + /** + * Key for the peripheral processing state. + */ + public static final String PERIPHERAL_PROC_STATE = "peripheralProcState"; + /** + * Key for the peripheral job. + */ + public static final String PERIPHERAL_JOB = "peripheralJob"; + /** + * This class's resource bundle. + */ + private final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_PATH); + /** + * The map of vehicle models to allocations states for which this model component's figure is to + * be decorated to indicate that it is part of the route of the respective vehicles. + */ + private Map allocationStates + = new TreeMap<>(Comparator.comparing(VehicleModel::getName)); + /** + * The set of block models for which this model component's figure is to be decorated to indicate + * that it is part of the respective block. + */ + private Set blocks + = new TreeSet<>(Comparator.comparing(BlockModel::getName)); + /** + * The model of the type. + */ + private transient LocationTypeModel fLocationType; + /** + * The location for this model. + */ + private Location location; + + /** + * Creates a new instance. + */ + @SuppressWarnings("this-escape") + public LocationModel() { + createProperties(); + } + + /** + * Sets the location type. + * + * @param type The model of the type. + */ + public void setLocationType(LocationTypeModel type) { + if (fLocationType != null) { + fLocationType.removeAttributesChangeListener(this); + } + + if (type != null) { + fLocationType = type; + fLocationType.addAttributesChangeListener(this); + } + } + + /** + * Returns the location type. + * + * @return The type. + */ + public LocationTypeModel getLocationType() { + return fLocationType; + } + + /** + * Refreshes the name of this location. + */ + protected void updateName() { + StringProperty property = getPropertyName(); + String oldName = property.getText(); + String newName = getName(); + property.setText(newName); + + if (!newName.equals(oldName)) { + property.markChanged(); + } + + propertiesChanged(this); + } + + @Override // AbstractModelComponent + public String getDescription() { + return bundle.getString("locationModel.description"); + } + + @Override // AttributesChangeListener + public void propertiesChanged(AttributesChangeEvent e) { + if (fLocationType.getPropertyName().hasChanged()) { + updateName(); + } + + if (fLocationType.getPropertyDefaultRepresentation().hasChanged()) { + propertiesChanged(this); + } + } + + public void updateTypeProperty(List types) { + requireNonNull(types, "types"); + + List possibleValues = new ArrayList<>(); + String value = null; + + for (LocationTypeModel type : types) { + possibleValues.add(type.getName()); + + if (type == fLocationType) { + value = type.getName(); + } + } + + getPropertyType().setPossibleValues(possibleValues); + getPropertyType().setValue(value); + getPropertyType().markChanged(); + } + + public CoordinateProperty getPropertyModelPositionX() { + return (CoordinateProperty) getProperty(MODEL_X_POSITION); + } + + public CoordinateProperty getPropertyModelPositionY() { + return (CoordinateProperty) getProperty(MODEL_Y_POSITION); + } + + public LocationTypeProperty getPropertyType() { + return (LocationTypeProperty) getProperty(TYPE); + } + + public BooleanProperty getPropertyLocked() { + return (BooleanProperty) getProperty(LOCKED); + } + + public KeyValueSetProperty getPropertyMiscellaneous() { + return (KeyValueSetProperty) getProperty(MISCELLANEOUS); + } + + public SymbolProperty getPropertyDefaultRepresentation() { + return (SymbolProperty) getProperty(ObjectPropConstants.LOC_DEFAULT_REPRESENTATION); + } + + public StringProperty getPropertyLayoutPositionX() { + return (StringProperty) getProperty(ElementPropKeys.LOC_POS_X); + } + + public StringProperty getPropertyLayoutPositionY() { + return (StringProperty) getProperty(ElementPropKeys.LOC_POS_Y); + } + + public StringProperty getPropertyLabelOffsetX() { + return (StringProperty) getProperty(ElementPropKeys.LOC_LABEL_OFFSET_X); + } + + public StringProperty getPropertyLabelOffsetY() { + return (StringProperty) getProperty(ElementPropKeys.LOC_LABEL_OFFSET_Y); + } + + public StringProperty getPropertyLabelOrientationAngle() { + return (StringProperty) getProperty(ElementPropKeys.LOC_LABEL_ORIENTATION_ANGLE); + } + + public StringProperty getPropertyPeripheralReservationToken() { + return (StringProperty) getProperty(PERIPHERAL_RESERVATION_TOKEN); + } + + public StringProperty getPropertyPeripheralState() { + return (StringProperty) getProperty(PERIPHERAL_STATE); + } + + public StringProperty getPropertyPeripheralProcState() { + return (StringProperty) getProperty(PERIPHERAL_PROC_STATE); + } + + public StringProperty getPropertyPeripheralJob() { + return (StringProperty) getProperty(PERIPHERAL_JOB); + } + + @Override + @Nonnull + public Map getAllocationStates() { + return allocationStates; + } + + @Override + public void updateAllocationState(VehicleModel model, AllocationState allocationState) { + requireNonNull(model, "model"); + requireNonNull(allocationStates, "allocationStates"); + + allocationStates.put(model, allocationState); + } + + @Override + public void clearAllocationState( + @Nonnull + VehicleModel model + ) { + requireNonNull(model, "model"); + + allocationStates.remove(model); + } + + @Override + public void addBlockModel(BlockModel model) { + blocks.add(model); + } + + @Override + public void removeBlockModel(BlockModel model) { + blocks.remove(model); + } + + @Override + public Set getBlockModels() { + return blocks; + } + + @Override + @SuppressWarnings({"unchecked", "checkstyle:LineLength"}) + public AbstractModelComponent clone() + throws CloneNotSupportedException { + LocationModel clone = (LocationModel) super.clone(); + clone.setAllocationStates( + (Map) ((TreeMap) allocationStates) + .clone() + ); + clone.setBlockModels((Set) ((TreeSet) blocks).clone()); + + return clone; + } + + private void createProperties() { + StringProperty pName = new StringProperty(this); + pName.setDescription(bundle.getString("locationModel.property_name.description")); + pName.setHelptext(bundle.getString("locationModel.property_name.helptext")); + setProperty(NAME, pName); + + CoordinateProperty pPosX = new CoordinateProperty(this); + pPosX.setDescription(bundle.getString("locationModel.property_modelPositionX.description")); + pPosX.setHelptext(bundle.getString("locationModel.property_modelPositionX.helptext")); + setProperty(MODEL_X_POSITION, pPosX); + + CoordinateProperty pPosY = new CoordinateProperty(this); + pPosY.setDescription(bundle.getString("locationModel.property_modelPositionY.description")); + pPosY.setHelptext(bundle.getString("locationModel.property_modelPositionY.helptext")); + setProperty(MODEL_Y_POSITION, pPosY); + + LocationTypeProperty pType = new LocationTypeProperty(this); + pType.setDescription(bundle.getString("locationModel.property_type.description")); + pType.setHelptext(bundle.getString("locationModel.property_type.helptext")); + setProperty(TYPE, pType); + + BooleanProperty pLocked = new BooleanProperty(this); + pLocked.setDescription(bundle.getString("locationModel.property_locked.description")); + pLocked.setHelptext(bundle.getString("locationModel.property_locked.helptext")); + pLocked.setCollectiveEditable(true); + pLocked.setOperatingEditable(true); + setProperty(LOCKED, pLocked); + + SymbolProperty pSymbol = new SymbolProperty(this); + pSymbol.setLocationRepresentation(LocationRepresentation.DEFAULT); + pSymbol.setDescription(bundle.getString("locationModel.property_symbol.description")); + pSymbol.setHelptext(bundle.getString("locationModel.property_symbol.helptext")); + pSymbol.setCollectiveEditable(true); + setProperty(ObjectPropConstants.LOC_DEFAULT_REPRESENTATION, pSymbol); + + StringProperty pLocPosX = new StringProperty(this); + pLocPosX.setDescription(bundle.getString("locationModel.property_positionX.description")); + pLocPosX.setHelptext(bundle.getString("locationModel.property_positionX.helptext")); + pLocPosX.setModellingEditable(false); + setProperty(ElementPropKeys.LOC_POS_X, pLocPosX); + + StringProperty pLocPosY = new StringProperty(this); + pLocPosY.setDescription(bundle.getString("locationModel.property_positionY.description")); + pLocPosY.setHelptext(bundle.getString("locationModel.property_positionY.helptext")); + pLocPosY.setModellingEditable(false); + setProperty(ElementPropKeys.LOC_POS_Y, pLocPosY); + + StringProperty pLocLabelOffsetX = new StringProperty(this); + pLocLabelOffsetX.setDescription( + bundle.getString("locationModel.property_labelOffsetX.description") + ); + pLocLabelOffsetX.setHelptext(bundle.getString("locationModel.property_labelOffsetX.helptext")); + pLocLabelOffsetX.setModellingEditable(false); + setProperty(ElementPropKeys.LOC_LABEL_OFFSET_X, pLocLabelOffsetX); + + StringProperty pLocLabelOffsetY = new StringProperty(this); + pLocLabelOffsetY.setDescription( + bundle.getString("locationModel.property_labelOffsetY.description") + ); + pLocLabelOffsetY.setHelptext(bundle.getString("locationModel.property_labelOffsetY.helptext")); + pLocLabelOffsetY.setModellingEditable(false); + setProperty(ElementPropKeys.LOC_LABEL_OFFSET_Y, pLocLabelOffsetY); + + StringProperty pLocLabelOrientationAngle = new StringProperty(this); + pLocLabelOrientationAngle.setDescription( + bundle.getString("locationModel.property_labelOrientationAngle.description") + ); + pLocLabelOrientationAngle.setHelptext( + bundle.getString("locationModel.property_labelOrientationAngle.helptext") + ); + pLocLabelOrientationAngle.setModellingEditable(false); + setProperty(ElementPropKeys.LOC_LABEL_ORIENTATION_ANGLE, pLocLabelOrientationAngle); + + LayerWrapperProperty pLayerWrapper = new LayerWrapperProperty(this, new NullLayerWrapper()); + pLayerWrapper.setDescription( + bundle.getString("locationModel.property_layerWrapper.description") + ); + pLayerWrapper.setHelptext(bundle.getString("locationModel.property_layerWrapper.helptext")); + pLayerWrapper.setModellingEditable(false); + setProperty(LAYER_WRAPPER, pLayerWrapper); + + StringProperty peripheralReservationTokenProperty = new StringProperty(this); + peripheralReservationTokenProperty.setDescription( + bundle.getString("locationModel.property_peripheralReservationToken.description") + ); + peripheralReservationTokenProperty.setHelptext( + bundle.getString("locationModel.property_peripheralReservationToken.helptext") + ); + peripheralReservationTokenProperty.setOperatingEditable(false); + peripheralReservationTokenProperty.setModellingEditable(false); + setProperty(PERIPHERAL_RESERVATION_TOKEN, peripheralReservationTokenProperty); + + StringProperty peripheralStateProperty = new StringProperty(this); + peripheralStateProperty.setDescription( + bundle.getString("locationModel.property_peripheralState.description") + ); + peripheralStateProperty.setHelptext( + bundle.getString("locationModel.property_peripheralState.helptext") + ); + peripheralStateProperty.setOperatingEditable(false); + peripheralStateProperty.setModellingEditable(false); + setProperty(PERIPHERAL_STATE, peripheralStateProperty); + + StringProperty peripheralProcState = new StringProperty(this); + peripheralProcState.setDescription( + bundle.getString("locationModel.property_peripheralProcState.description") + ); + peripheralProcState.setHelptext( + bundle.getString("locationModel.property_peripheralProcState.helptext") + ); + peripheralProcState.setOperatingEditable(false); + peripheralProcState.setModellingEditable(false); + setProperty(PERIPHERAL_PROC_STATE, peripheralProcState); + + StringProperty peripheralJob = new StringProperty(this); + peripheralJob.setDescription( + bundle.getString("locationModel.property_peripheralJob.description") + ); + peripheralJob.setHelptext(bundle.getString("locationModel.property_peripheralJob.helptext")); + peripheralJob.setOperatingEditable(false); + peripheralJob.setModellingEditable(false); + setProperty(PERIPHERAL_JOB, peripheralJob); + + KeyValueSetProperty pMiscellaneous = new KeyValueSetProperty(this); + pMiscellaneous.setDescription( + bundle.getString("locationModel.property_miscellaneous.description") + ); + pMiscellaneous.setHelptext(bundle.getString("locationModel.property_miscellaneous.helptext")); + pMiscellaneous.setOperatingEditable(true); + setProperty(MISCELLANEOUS, pMiscellaneous); + } + + private void setAllocationStates(Map allocationStates) { + this.allocationStates = allocationStates; + } + + private void setBlockModels(Set blocks) { + this.blocks = blocks; + } + + public void setLocation( + @Nonnull + Location location + ) { + this.location = requireNonNull(location, "location"); + } + + public Location getLocation() { + return location; + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/LocationTypeModel.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/LocationTypeModel.java new file mode 100644 index 0000000..8f21341 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/LocationTypeModel.java @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model.elements; + +import static org.opentcs.guing.base.I18nPlantOverviewBase.BUNDLE_PATH; + +import java.util.ResourceBundle; +import org.opentcs.data.ObjectPropConstants; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.visualization.LocationRepresentation; +import org.opentcs.guing.base.components.properties.type.KeyValueSetProperty; +import org.opentcs.guing.base.components.properties.type.LocationTypeActionsProperty; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.base.components.properties.type.StringSetProperty; +import org.opentcs.guing.base.components.properties.type.SymbolProperty; +import org.opentcs.guing.base.model.AbstractModelComponent; + +/** + * Basic implementation of a location type. + */ +public class LocationTypeModel + extends + AbstractModelComponent { + + /** + * The key for the possible actions on this type. + */ + public static final String ALLOWED_OPERATIONS = "AllowedOperations"; + /** + * The key fo the poissible peripheral actions on this type. + */ + public static final String ALLOWED_PERIPHERAL_OPERATIONS = "AllowedPeripheralOperations"; + /** + * This class's resource bundle. + */ + private final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_PATH); + /** + * Reference to the LocationType object. + */ + private LocationType locationType; + + /** + * Creates a new instance. + */ + @SuppressWarnings("this-escape") + public LocationTypeModel() { + createProperties(); + } + + @Override + public String getDescription() { + return bundle.getString("locationTypeModel.description"); + } + + @Override + public String getTreeViewName() { + String treeViewName = getDescription() + " " + getName(); + + return treeViewName; + } + + public StringSetProperty getPropertyAllowedOperations() { + return (StringSetProperty) getProperty(ALLOWED_OPERATIONS); + } + + public StringSetProperty getPropertyAllowedPeripheralOperations() { + return (StringSetProperty) getProperty(ALLOWED_PERIPHERAL_OPERATIONS); + } + + public KeyValueSetProperty getPropertyMiscellaneous() { + return (KeyValueSetProperty) getProperty(MISCELLANEOUS); + } + + public SymbolProperty getPropertyDefaultRepresentation() { + return (SymbolProperty) getProperty(ObjectPropConstants.LOCTYPE_DEFAULT_REPRESENTATION); + } + + private void createProperties() { + StringProperty pName = new StringProperty(this); + pName.setDescription(bundle.getString("locationTypeModel.property_name.description")); + pName.setHelptext(bundle.getString("locationTypeModel.property_name.helptext")); + setProperty(NAME, pName); + + StringSetProperty pOperations = new LocationTypeActionsProperty(this); + pOperations.setDescription( + bundle.getString("locationTypeModel.property_allowedOperations.description") + ); + pOperations.setHelptext( + bundle.getString("locationTypeModel.property_allowedOperations.helptext") + ); + setProperty(ALLOWED_OPERATIONS, pOperations); + + StringSetProperty pPeripheralOperations = new LocationTypeActionsProperty(this); + pPeripheralOperations.setDescription( + bundle.getString("locationTypeModel.property_allowedPeripheralOperations.description") + ); + pPeripheralOperations.setHelptext( + bundle.getString("locationTypeModel.property_allowedPeripheralOperations.helptext") + ); + setProperty(ALLOWED_PERIPHERAL_OPERATIONS, pPeripheralOperations); + + SymbolProperty pSymbol = new SymbolProperty(this); + pSymbol.setLocationRepresentation(LocationRepresentation.NONE); + pSymbol.setDescription(bundle.getString("locationTypeModel.property_symbol.description")); + pSymbol.setHelptext(bundle.getString("locationTypeModel.property_symbol.helptext")); + setProperty(ObjectPropConstants.LOCTYPE_DEFAULT_REPRESENTATION, pSymbol); + + KeyValueSetProperty pMiscellaneous = new KeyValueSetProperty(this); + pMiscellaneous.setDescription( + bundle.getString("locationTypeModel.property_miscellaneous.description") + ); + pMiscellaneous.setHelptext( + bundle.getString("locationTypeModel.property_miscellaneous.helptext") + ); + pMiscellaneous.setOperatingEditable(true); + setProperty(MISCELLANEOUS, pMiscellaneous); + } + + public LocationType getLocationType() { + return locationType; + } + + public void setLocationType(LocationType locationType) { + this.locationType = locationType; + } + +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/OtherGraphicalElement.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/OtherGraphicalElement.java new file mode 100644 index 0000000..8b01877 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/OtherGraphicalElement.java @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model.elements; + +import static org.opentcs.guing.base.I18nPlantOverviewBase.BUNDLE_PATH; + +import java.util.ResourceBundle; +import org.opentcs.guing.base.model.AbstractModelComponent; + +/** + * A graphical component with illustrating effect, but without any impact + * on the driving course. + */ +public class OtherGraphicalElement + extends + AbstractModelComponent { + + /** + * Creates a new instance of OtherGraphicalElement. + */ + public OtherGraphicalElement() { + super(); + } + + @Override + public String getDescription() { + return ResourceBundle.getBundle(BUNDLE_PATH) + .getString("otherGraphicalElement.description"); + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/PathModel.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/PathModel.java new file mode 100644 index 0000000..456f16b --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/PathModel.java @@ -0,0 +1,362 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model.elements; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.guing.base.I18nPlantOverviewBase.BUNDLE_PATH; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import org.opentcs.data.model.visualization.ElementPropKeys; +import org.opentcs.guing.base.AllocationState; +import org.opentcs.guing.base.components.layer.NullLayerWrapper; +import org.opentcs.guing.base.components.properties.type.BooleanProperty; +import org.opentcs.guing.base.components.properties.type.EnvelopesProperty; +import org.opentcs.guing.base.components.properties.type.KeyValueSetProperty; +import org.opentcs.guing.base.components.properties.type.LayerWrapperProperty; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.components.properties.type.LinerTypeProperty; +import org.opentcs.guing.base.components.properties.type.PeripheralOperationsProperty; +import org.opentcs.guing.base.components.properties.type.SelectionProperty; +import org.opentcs.guing.base.components.properties.type.SpeedProperty; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.base.model.FigureDecorationDetails; + +/** + * A connection between two points. + */ +public class PathModel + extends + AbstractConnection + implements + FigureDecorationDetails { + + /** + * Key for the length. + */ + public static final String LENGTH = "length"; + /** + * Key for maximum forward velocity. + */ + public static final String MAX_VELOCITY = "maxVelocity"; + /** + * Key for maximum reverse velocity. + */ + public static final String MAX_REVERSE_VELOCITY = "maxReverseVelocity"; + /** + * Key for the locked state. + */ + public static final String LOCKED = "locked"; + /** + * Key for the peripheral operations on this path. + */ + public static final String PERIPHERAL_OPERATIONS = "peripheralOperations"; + /** + * Key for the vehicle envelopes on this path. + */ + public static final String VEHICLE_ENVELOPES = "vehicleEnvelopes"; + /** + * This class's resource bundle. + */ + private final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_PATH); + /** + * The map of vehicle models to allocations states for which this model component's figure is to + * be decorated to indicate that it is part of the route of the respective vehicles. + */ + private Map allocationStates + = new TreeMap<>(Comparator.comparing(VehicleModel::getName)); + /** + * The set of block models for which this model component's figure is to be decorated to indicate + * that it is part of the respective block. + */ + private Set blocks = new TreeSet<>(Comparator.comparing(BlockModel::getName)); + + /** + * Creates a new instance. + */ + @SuppressWarnings("this-escape") + public PathModel() { + createProperties(); + } + + @Override + public String getDescription() { + return bundle.getString("pathModel.description"); + } + + public LengthProperty getPropertyLength() { + return (LengthProperty) getProperty(LENGTH); + } + + public SpeedProperty getPropertyMaxVelocity() { + return (SpeedProperty) getProperty(MAX_VELOCITY); + } + + public SpeedProperty getPropertyMaxReverseVelocity() { + return (SpeedProperty) getProperty(MAX_REVERSE_VELOCITY); + } + + public BooleanProperty getPropertyLocked() { + return (BooleanProperty) getProperty(LOCKED); + } + + @SuppressWarnings("unchecked") + public SelectionProperty getPropertyPathConnType() { + return (SelectionProperty) getProperty(ElementPropKeys.PATH_CONN_TYPE); + } + + public StringProperty getPropertyPathControlPoints() { + return (StringProperty) getProperty(ElementPropKeys.PATH_CONTROL_POINTS); + } + + public EnvelopesProperty getPropertyVehicleEnvelopes() { + return (EnvelopesProperty) getProperty(VEHICLE_ENVELOPES); + } + + public KeyValueSetProperty getPropertyMiscellaneous() { + return (KeyValueSetProperty) getProperty(MISCELLANEOUS); + } + + public PeripheralOperationsProperty getPropertyPeripheralOperations() { + return (PeripheralOperationsProperty) getProperty(PERIPHERAL_OPERATIONS); + } + + @Override + @Nonnull + public Map getAllocationStates() { + return allocationStates; + } + + @Override + public void updateAllocationState(VehicleModel model, AllocationState allocationState) { + requireNonNull(model, "model"); + requireNonNull(allocationStates, "allocationStates"); + + allocationStates.put(model, allocationState); + } + + @Override + public void clearAllocationState( + @Nonnull + VehicleModel model + ) { + requireNonNull(model, "model"); + + allocationStates.remove(model); + } + + @Override + public void addBlockModel(BlockModel model) { + blocks.add(model); + } + + @Override + public void removeBlockModel(BlockModel model) { + blocks.remove(model); + } + + @Override + public Set getBlockModels() { + return blocks; + } + + @Override + @SuppressWarnings({"unchecked", "checkstyle:LineLength"}) + public AbstractConnection clone() + throws CloneNotSupportedException { + PathModel clone = (PathModel) super.clone(); + clone.setAllocationStates( + (Map) ((TreeMap) allocationStates) + .clone() + ); + clone.setBlockModels((Set) ((TreeSet) blocks).clone()); + + return clone; + } + + private void createProperties() { + StringProperty pName = new StringProperty(this); + pName.setDescription(bundle.getString("pathModel.property_name.description")); + pName.setHelptext(bundle.getString("pathModel.property_name.helptext")); + setProperty(NAME, pName); + + LengthProperty pLength = new LengthProperty(this); + pLength.setDescription(bundle.getString("pathModel.property_length.description")); + pLength.setHelptext(bundle.getString("pathModel.property_length.helptext")); + setProperty(LENGTH, pLength); + + SpeedProperty pMaxVelocity = new SpeedProperty(this, 1.0, SpeedProperty.Unit.M_S); + pMaxVelocity.setDescription(bundle.getString("pathModel.property_maximumVelocity.description")); + pMaxVelocity.setHelptext(bundle.getString("pathModel.property_maximumVelocity.helptext")); + setProperty(MAX_VELOCITY, pMaxVelocity); + + SpeedProperty pMaxReverseVelocity = new SpeedProperty(this, 0.0, SpeedProperty.Unit.M_S); + pMaxReverseVelocity.setDescription( + bundle.getString("pathModel.property_maximumReverseVelocity.description") + ); + pMaxReverseVelocity.setHelptext( + bundle.getString("pathModel.property_maximumReverseVelocity.helptext") + ); + setProperty(MAX_REVERSE_VELOCITY, pMaxReverseVelocity); + + LinerTypeProperty pPathConnType + = new LinerTypeProperty(this, Arrays.asList(Type.values()), Type.values()[0]); + pPathConnType.setDescription( + bundle.getString("pathModel.property_pathConnectionType.description") + ); + pPathConnType.setHelptext(bundle.getString("pathModel.property_pathConnectionType.helptext")); + pPathConnType.setCollectiveEditable(true); + setProperty(ElementPropKeys.PATH_CONN_TYPE, pPathConnType); + + StringProperty pPathControlPoints = new StringProperty(this); + pPathControlPoints.setDescription( + bundle.getString("pathModel.property_pathControlPoints.description") + ); + pPathControlPoints.setHelptext( + bundle.getString("pathModel.property_pathControlPoints.helptext") + ); + // Control points may only be moved in the drawing. + pPathControlPoints.setModellingEditable(false); + setProperty(ElementPropKeys.PATH_CONTROL_POINTS, pPathControlPoints); + + StringProperty startComponent = new StringProperty(this); + startComponent.setDescription( + bundle.getString("pathModel.property_startComponent.description") + ); + startComponent.setModellingEditable(false); + startComponent.setOperatingEditable(false); + setProperty(START_COMPONENT, startComponent); + StringProperty endComponent = new StringProperty(this); + endComponent.setDescription(bundle.getString("pathModel.property_endComponent.description")); + endComponent.setModellingEditable(false); + endComponent.setOperatingEditable(false); + setProperty(END_COMPONENT, endComponent); + + LayerWrapperProperty pLayerWrapper = new LayerWrapperProperty(this, new NullLayerWrapper()); + pLayerWrapper.setDescription(bundle.getString("pathModel.property_layerWrapper.description")); + pLayerWrapper.setHelptext(bundle.getString("pathModel.property_layerWrapper.helptext")); + pLayerWrapper.setModellingEditable(false); + setProperty(LAYER_WRAPPER, pLayerWrapper); + + BooleanProperty pLocked = new BooleanProperty(this); + pLocked.setDescription(bundle.getString("pathModel.property_locked.description")); + pLocked.setHelptext(bundle.getString("pathModel.property_locked.helptext")); + pLocked.setCollectiveEditable(true); + pLocked.setOperatingEditable(true); + setProperty(LOCKED, pLocked); + + PeripheralOperationsProperty pOperations + = new PeripheralOperationsProperty(this, new ArrayList<>()); + pOperations.setDescription( + bundle.getString("pathModel.property_peripheralOperations.description") + ); + pOperations.setHelptext(bundle.getString("pathModel.property_peripheralOperations.helptext")); + setProperty(PERIPHERAL_OPERATIONS, pOperations); + + EnvelopesProperty pEnvelope = new EnvelopesProperty(this, new ArrayList<>()); + pEnvelope.setDescription(bundle.getString("pathModel.property_vehicleEnvelopes.description")); + pEnvelope.setHelptext(bundle.getString("pathModel.property_vehicleEnvelopes.helptext")); + pEnvelope.setModellingEditable(true); + setProperty(VEHICLE_ENVELOPES, pEnvelope); + + KeyValueSetProperty pMiscellaneous = new KeyValueSetProperty(this); + pMiscellaneous.setDescription(bundle.getString("pathModel.property_miscellaneous.description")); + pMiscellaneous.setHelptext(bundle.getString("pathModel.property_miscellaneous.helptext")); + pMiscellaneous.setOperatingEditable(true); + setProperty(MISCELLANEOUS, pMiscellaneous); + } + + private void setAllocationStates(Map allocationStates) { + this.allocationStates = allocationStates; + } + + private void setBlockModels(Set blocks) { + this.blocks = blocks; + } + + /** + * The supported liner types for connections. + */ + public enum Type { + + /** + * A direct connection. + */ + DIRECT(ResourceBundle.getBundle(BUNDLE_PATH).getString("pathModel.type.direct.description"), + ResourceBundle.getBundle(BUNDLE_PATH).getString("pathModel.type.direct.helptext")), + /** + * An elbow connection. + */ + ELBOW(ResourceBundle.getBundle(BUNDLE_PATH).getString("pathModel.type.elbow.description"), + ResourceBundle.getBundle(BUNDLE_PATH).getString("pathModel.type.elbow.helptext")), + /** + * A slanted connection. + */ + SLANTED(ResourceBundle.getBundle(BUNDLE_PATH).getString("pathModel.type.slanted.description"), + ResourceBundle.getBundle(BUNDLE_PATH).getString("pathModel.type.slanted.helptext")), + /** + * A polygon path with any number of vertecies. + */ + POLYPATH(ResourceBundle.getBundle(BUNDLE_PATH).getString("pathModel.type.polypath.description"), + ResourceBundle.getBundle(BUNDLE_PATH).getString("pathModel.type.polypath.helptext")), + + /** + * A bezier curve with 2 control points. + */ + BEZIER(ResourceBundle.getBundle(BUNDLE_PATH).getString("pathModel.type.bezier.description"), + ResourceBundle.getBundle(BUNDLE_PATH).getString("pathModel.type.bezier.helptext")), + /** + * A bezier curve with 3 control points. + */ + BEZIER_3(ResourceBundle.getBundle(BUNDLE_PATH).getString("pathModel.type.bezier3.description"), + ResourceBundle.getBundle(BUNDLE_PATH).getString("pathModel.type.bezier3.helptext")); + + private final String description; + + private final String helptext; + + Type(String description, String helptext) { + this.description = requireNonNull(description, "description"); + this.helptext = requireNonNull(helptext, "helptext"); + } + + public String getDescription() { + return description; + } + + public String getHelptext() { + return helptext; + } + + /** + * Returns the Type constant matching the name in the + * given input. This method permits extraneous whitespace around the name + * and is case insensitive, which makes it a bit more liberal than the + * default valueOf that all enums provide. + * + * @param input The name of the enum constant to return. + * @return The enum constant matching the given name. + * @throws IllegalArgumentException If this enum has no constant with the + * given name. + */ + public static Type valueOfNormalized(String input) + throws IllegalArgumentException { + String normalizedInput = requireNonNull(input, "input is null").trim(); + + for (Type curType : values()) { + if (normalizedInput.equalsIgnoreCase(curType.name())) { + return curType; + } + } + + throw new IllegalArgumentException("No match for '" + input + "'"); + } + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/PointModel.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/PointModel.java new file mode 100644 index 0000000..78db2fd --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/PointModel.java @@ -0,0 +1,350 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model.elements; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.guing.base.I18nPlantOverviewBase.BUNDLE_PATH; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.visualization.ElementPropKeys; +import org.opentcs.guing.base.AllocationState; +import org.opentcs.guing.base.components.layer.NullLayerWrapper; +import org.opentcs.guing.base.components.properties.type.AngleProperty; +import org.opentcs.guing.base.components.properties.type.BoundingBoxProperty; +import org.opentcs.guing.base.components.properties.type.CoordinateProperty; +import org.opentcs.guing.base.components.properties.type.EnvelopesProperty; +import org.opentcs.guing.base.components.properties.type.KeyValueSetProperty; +import org.opentcs.guing.base.components.properties.type.LayerWrapperProperty; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.components.properties.type.PointTypeProperty; +import org.opentcs.guing.base.components.properties.type.SelectionProperty; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.base.model.AbstractConnectableModelComponent; +import org.opentcs.guing.base.model.AbstractModelComponent; +import org.opentcs.guing.base.model.BoundingBoxModel; +import org.opentcs.guing.base.model.FigureDecorationDetails; +import org.opentcs.guing.base.model.PositionableModelComponent; + +/** + * Basic implementation of a point. + */ +public class PointModel + extends + AbstractConnectableModelComponent + implements + PositionableModelComponent, + FigureDecorationDetails { + + /** + * Key for the prefered angle of a vehicle on this point. + */ + public static final String VEHICLE_ORIENTATION_ANGLE = "vehicleOrientationAngle"; + /** + * Key for the type. + */ + public static final String TYPE = "Type"; + /** + * Key for the vehicle envelopes at this point. + */ + public static final String VEHICLE_ENVELOPES = "vehicleEnvelopes"; + /** + * Key for the maximum bounding box that a vehicle at this point is allowed to have. + */ + public static final String MAX_VEHICLE_BOUNDING_BOX = "maxVehicleBoundingBox"; + /** + * The point's default position for both axes. + */ + private static final int DEFAULT_XY_POSITION = 0; + /** + * This class's resource bundle. + */ + private final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_PATH); + /** + * The map of vehicle models to allocations states for which this model component's figure is to + * be decorated to indicate that it is part of the route of the respective vehicles. + */ + private Map allocationStates + = new TreeMap<>(Comparator.comparing(VehicleModel::getName)); + /** + * The set of block models for which this model component's figure is to be decorated to indicate + * that it is part of the respective block. + */ + private Set blocks = new TreeSet<>(Comparator.comparing(BlockModel::getName)); + + /** + * Creates a new instance. + */ + @SuppressWarnings("this-escape") + public PointModel() { + createProperties(); + } + + @Override // AbstractModelComponent + public String getDescription() { + return bundle.getString("pointModel.description"); + } + + public CoordinateProperty getPropertyModelPositionX() { + return (CoordinateProperty) getProperty(MODEL_X_POSITION); + } + + public CoordinateProperty getPropertyModelPositionY() { + return (CoordinateProperty) getProperty(MODEL_Y_POSITION); + } + + public AngleProperty getPropertyVehicleOrientationAngle() { + return (AngleProperty) getProperty(VEHICLE_ORIENTATION_ANGLE); + } + + @SuppressWarnings("unchecked") + public SelectionProperty getPropertyType() { + return (SelectionProperty) getProperty(TYPE); + } + + public EnvelopesProperty getPropertyVehicleEnvelopes() { + return (EnvelopesProperty) getProperty(VEHICLE_ENVELOPES); + } + + public BoundingBoxProperty getPropertyMaxVehicleBoundingBox() { + return (BoundingBoxProperty) getProperty(MAX_VEHICLE_BOUNDING_BOX); + } + + public KeyValueSetProperty getPropertyMiscellaneous() { + return (KeyValueSetProperty) getProperty(MISCELLANEOUS); + } + + public StringProperty getPropertyLayoutPosX() { + return (StringProperty) getProperty(ElementPropKeys.POINT_POS_X); + } + + public StringProperty getPropertyLayoutPosY() { + return (StringProperty) getProperty(ElementPropKeys.POINT_POS_Y); + } + + public StringProperty getPropertyPointLabelOffsetX() { + return (StringProperty) getProperty(ElementPropKeys.POINT_LABEL_OFFSET_X); + } + + public StringProperty getPropertyPointLabelOffsetY() { + return (StringProperty) getProperty(ElementPropKeys.POINT_LABEL_OFFSET_Y); + } + + public StringProperty getPropertyPointLabelOrientationAngle() { + return (StringProperty) getProperty(ElementPropKeys.POINT_LABEL_ORIENTATION_ANGLE); + } + + @Override + @Nonnull + public Map getAllocationStates() { + return allocationStates; + } + + @Override + public void updateAllocationState(VehicleModel model, AllocationState allocationState) { + requireNonNull(model, "model"); + requireNonNull(allocationStates, "allocationStates"); + + allocationStates.put(model, allocationState); + } + + @Override + public void clearAllocationState( + @Nonnull + VehicleModel model + ) { + requireNonNull(model, "model"); + + allocationStates.remove(model); + } + + @Override + public void addBlockModel(BlockModel model) { + blocks.add(model); + } + + @Override + public void removeBlockModel(BlockModel model) { + blocks.remove(model); + } + + @Override + public Set getBlockModels() { + return blocks; + } + + @Override + @SuppressWarnings({"unchecked", "checkstyle:LineLength"}) + public AbstractModelComponent clone() + throws CloneNotSupportedException { + PointModel clone = (PointModel) super.clone(); + clone.setAllocationStates( + (Map) ((TreeMap) allocationStates) + .clone() + ); + clone.setBlockModels((Set) ((TreeSet) blocks).clone()); + + return clone; + } + + private void createProperties() { + StringProperty pName = new StringProperty(this); + pName.setDescription(bundle.getString("pointModel.property_name.description")); + pName.setHelptext(bundle.getString("pointModel.property_name.helptext")); + setProperty(NAME, pName); + + CoordinateProperty pPosX = new CoordinateProperty( + this, + DEFAULT_XY_POSITION, + LengthProperty.Unit.MM + ); + pPosX.setDescription(bundle.getString("pointModel.property_modelPositionX.description")); + pPosX.setHelptext(bundle.getString("pointModel.property_modelPositionX.helptext")); + setProperty(MODEL_X_POSITION, pPosX); + + CoordinateProperty pPosY = new CoordinateProperty( + this, + DEFAULT_XY_POSITION, + LengthProperty.Unit.MM + ); + pPosY.setDescription(bundle.getString("pointModel.property_modelPositionY.description")); + pPosY.setHelptext(bundle.getString("pointModel.property_modelPositionY.helptext")); + setProperty(MODEL_Y_POSITION, pPosY); + + AngleProperty pPhi = new AngleProperty(this); + pPhi.setDescription(bundle.getString("pointModel.property_angle.description")); + pPhi.setHelptext(bundle.getString("pointModel.property_angle.helptext")); + setProperty(VEHICLE_ORIENTATION_ANGLE, pPhi); + + PointTypeProperty pType = new PointTypeProperty( + this, + Arrays.asList(Type.values()), + Type.values()[0] + ); + pType.setDescription(bundle.getString("pointModel.property_type.description")); + pType.setHelptext(bundle.getString("pointModel.property_type.helptext")); + pType.setCollectiveEditable(true); + setProperty(TYPE, pType); + + EnvelopesProperty pEnvelope = new EnvelopesProperty(this, new ArrayList<>()); + pEnvelope.setDescription(bundle.getString("pointModel.property_vehicleEnvelopes.description")); + pEnvelope.setHelptext(bundle.getString("pointModel.property_vehicleEnvelopes.helptext")); + pEnvelope.setModellingEditable(true); + setProperty(VEHICLE_ENVELOPES, pEnvelope); + + BoundingBoxProperty pMaxVehicleBoundingBox = new BoundingBoxProperty( + this, + new BoundingBoxModel(1000, 1000, 1000, new Couple(0, 0)) + ); + pMaxVehicleBoundingBox.setDescription( + bundle.getString("pointModel.property_maxVehicleBoundingBox.description") + ); + pMaxVehicleBoundingBox.setHelptext( + bundle.getString("pointModel.property_maxVehicleBoundingBox.helptext") + ); + setProperty(MAX_VEHICLE_BOUNDING_BOX, pMaxVehicleBoundingBox); + + KeyValueSetProperty pMiscellaneous = new KeyValueSetProperty(this); + pMiscellaneous.setDescription( + bundle.getString("pointModel.property_miscellaneous.description") + ); + pMiscellaneous.setHelptext(bundle.getString("pointModel.property_miscellaneous.helptext")); + pMiscellaneous.setOperatingEditable(true); + setProperty(MISCELLANEOUS, pMiscellaneous); + + StringProperty pPointPosX = new StringProperty(this, String.valueOf(DEFAULT_XY_POSITION)); + pPointPosX.setDescription(bundle.getString("pointModel.property_positionX.description")); + pPointPosX.setHelptext(bundle.getString("pointModel.property_positionX.helptext")); + // The position can only be changed in the drawing. + pPointPosX.setModellingEditable(false); + setProperty(ElementPropKeys.POINT_POS_X, pPointPosX); + + StringProperty pPointPosY = new StringProperty(this, String.valueOf(DEFAULT_XY_POSITION)); + pPointPosY.setDescription(bundle.getString("pointModel.property_positionY.description")); + pPointPosY.setHelptext(bundle.getString("pointModel.property_positionY.helptext")); + // The position can only be changed in the drawing. + pPointPosY.setModellingEditable(false); + setProperty(ElementPropKeys.POINT_POS_Y, pPointPosY); + + StringProperty pPointLabelOffsetX = new StringProperty(this); + pPointLabelOffsetX.setDescription( + bundle.getString("pointModel.property_labelOffsetX.description") + ); + pPointLabelOffsetX.setHelptext(bundle.getString("pointModel.property_labelOffsetX.helptext")); + pPointLabelOffsetX.setModellingEditable(false); + setProperty(ElementPropKeys.POINT_LABEL_OFFSET_X, pPointLabelOffsetX); + + StringProperty pPointLabelOffsetY = new StringProperty(this); + pPointLabelOffsetY.setDescription( + bundle.getString("pointModel.property_labelOffsetY.description") + ); + pPointLabelOffsetY.setHelptext(bundle.getString("pointModel.property_labelOffsetY.helptext")); + pPointLabelOffsetY.setModellingEditable(false); + setProperty(ElementPropKeys.POINT_LABEL_OFFSET_Y, pPointLabelOffsetY); + + StringProperty pPointLabelOrientationAngle = new StringProperty(this); + pPointLabelOrientationAngle.setDescription( + bundle.getString("pointModel.property_labelOrientationAngle.description") + ); + pPointLabelOrientationAngle.setHelptext( + bundle.getString("pointModel.property_labelOrientationAngle.helptext") + ); + pPointLabelOrientationAngle.setModellingEditable(false); + setProperty(ElementPropKeys.POINT_LABEL_ORIENTATION_ANGLE, pPointLabelOrientationAngle); + + LayerWrapperProperty pLayerWrapper = new LayerWrapperProperty(this, new NullLayerWrapper()); + pLayerWrapper.setDescription(bundle.getString("pointModel.property_layerWrapper.description")); + pLayerWrapper.setHelptext(bundle.getString("pointModel.property_layerWrapper.helptext")); + pLayerWrapper.setModellingEditable(false); + setProperty(LAYER_WRAPPER, pLayerWrapper); + } + + private void setAllocationStates(Map allocationStates) { + this.allocationStates = allocationStates; + } + + private void setBlockModels(Set blocks) { + this.blocks = blocks; + } + + /** + * The supported point types. + */ + public enum Type { + + /** + * A halting position. + */ + HALT(ResourceBundle.getBundle(BUNDLE_PATH).getString("pointModel.type.halt.description"), + ResourceBundle.getBundle(BUNDLE_PATH).getString("pointModel.type.halt.helptext")), + /** + * A parking position. + */ + PARK(ResourceBundle.getBundle(BUNDLE_PATH).getString("pointModel.type.park.description"), + ResourceBundle.getBundle(BUNDLE_PATH).getString("pointModel.type.park.helptext")); + + private final String description; + private final String helptext; + + Type(String description, String helptext) { + this.description = requireNonNull(description, "description"); + this.helptext = requireNonNull(helptext, "helptext"); + } + + public String getDescription() { + return description; + } + + public String getHelptext() { + return helptext; + } + } +} diff --git a/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/VehicleModel.java b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/VehicleModel.java new file mode 100644 index 0000000..b364d07 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/java/org/opentcs/guing/base/model/elements/VehicleModel.java @@ -0,0 +1,693 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model.elements; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.guing.base.I18nPlantOverviewBase.BUNDLE_PATH; + +import jakarta.annotation.Nonnull; +import java.awt.Color; +import java.util.Arrays; +import java.util.ResourceBundle; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Triple; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.model.visualization.ElementPropKeys; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.guing.base.components.properties.type.AngleProperty; +import org.opentcs.guing.base.components.properties.type.BooleanProperty; +import org.opentcs.guing.base.components.properties.type.BoundingBoxProperty; +import org.opentcs.guing.base.components.properties.type.ColorProperty; +import org.opentcs.guing.base.components.properties.type.EnergyLevelThresholdSetModel; +import org.opentcs.guing.base.components.properties.type.EnergyLevelThresholdSetProperty; +import org.opentcs.guing.base.components.properties.type.KeyValueSetProperty; +import org.opentcs.guing.base.components.properties.type.OrderTypesProperty; +import org.opentcs.guing.base.components.properties.type.PercentProperty; +import org.opentcs.guing.base.components.properties.type.ResourceProperty; +import org.opentcs.guing.base.components.properties.type.SelectionProperty; +import org.opentcs.guing.base.components.properties.type.SpeedProperty; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.base.components.properties.type.TripleProperty; +import org.opentcs.guing.base.model.AbstractModelComponent; +import org.opentcs.guing.base.model.BoundingBoxModel; +import org.opentcs.guing.base.model.DrawnModelComponent; + +/** + * Basic implementation of a vehicle. A vehicle has an unique number. + */ +public class VehicleModel + extends + AbstractModelComponent + implements + DrawnModelComponent { + + /** + * The name/key of the 'bounding box' property. + */ + public static final String BOUNDING_BOX = "BoundingBox"; + /** + * The name/key of the 'energyLevelThresholdSet' property. + */ + public static final String ENERGY_LEVEL_THRESHOLD_SET = "EnergyLevelThresholdSet"; + /** + * The name/key of the 'loaded' property. + */ + public static final String LOADED = "Loaded"; + /** + * The name/key of the 'state' property. + */ + public static final String STATE = "State"; + /** + * The name/key of the 'proc state' property. + */ + public static final String PROC_STATE = "ProcState"; + /** + * The name/key of the 'integration level' property. + */ + public static final String INTEGRATION_LEVEL = "IntegrationLevel"; + /** + * The name/key of the 'paused' property. + */ + public static final String PAUSED = "Paused"; + /** + * The name/key of the 'point' property. + */ + public static final String POINT = "Point"; + /** + * The name/key of the 'next point' property. + */ + public static final String NEXT_POINT = "NextPoint"; + /** + * The name/key of the 'precise position' property. + */ + public static final String PRECISE_POSITION = "PrecisePosition"; + /** + * The name/key of the 'orientation angle' property. + */ + public static final String ORIENTATION_ANGLE = "OrientationAngle"; + /** + * The name/key of the 'energy level' property. + */ + public static final String ENERGY_LEVEL = "EnergyLevel"; + /** + * The name/key of the 'current transport order name' property. + */ + public static final String CURRENT_TRANSPORT_ORDER_NAME = "currentTransportOrderName"; + /** + * The name/key of the 'current order sequence name' property. + */ + public static final String CURRENT_SEQUENCE_NAME = "currentOrderSequenceName"; + /** + * The name/key of the 'maximum velocity' property. + */ + public static final String MAXIMUM_VELOCITY = "MaximumVelocity"; + /** + * The name/key of the 'maximum reverse velocity' property. + */ + public static final String MAXIMUM_REVERSE_VELOCITY = "MaximumReverseVelocity"; + /** + * The name/key of the 'allowed order types' property. + */ + public static final String ALLOWED_ORDER_TYPES = "AllowedOrderTypes"; + /** + * The name/key of the 'allocated resources' property. + */ + public static final String ALLOCATED_RESOURCES = "AllocatedResources"; + /** + * The name/key of the 'claimed resources' property. + */ + public static final String CLAIMED_RESOURCES = "ClaimedResources"; + /** + * The name/key of the 'envelope key' property. + */ + public static final String ENVELOPE_KEY = "EnvelopeKey"; + /** + * This class's resource bundle. + */ + private final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_PATH); + + /** + * The point the vehicle currently remains on. + */ + private PointModel fPoint; + /** + * The point the vehicle will drive to next. + */ + private PointModel fNextPoint; + /** + * The current position (x,y,z) the vehicle driver reported. + */ + private Triple fPrecisePosition; + /** + * The current vehicle orientation. + */ + private double fOrientationAngle; + /** + * The state of the drive order. + */ + private TransportOrder.State fDriveOrderState; + /** + * Flag whether the drive order will be displayed. + */ + private boolean fDisplayDriveOrders; + /** + * Flag whether the view follows this vehicle as it drives. + */ + private boolean fViewFollows; + /** + * A reference to the vehicle. + */ + private Vehicle vehicle = new Vehicle("Dummy"); + /** + * The current or next path in a drive order. + */ + private PathModel currentDriveOrderPath; + /** + * The destination for the current drive order. + */ + private PointModel driveOrderDestination; + + /** + * Creates a new instance. + */ + @SuppressWarnings("this-escape") + public VehicleModel() { + createProperties(); + } + + /** + * Sets the point the vehicle currently remains on. + * + * @param point The point. + */ + public void placeOnPoint(PointModel point) { + fPoint = point; + } + + /** + * Returns the point the vehicle currently remains on. + * + * @return The current point. + */ + public PointModel getPoint() { + return fPoint; + } + + /** + * Returns the point the vehicle will drive to next. + * + * @return The next point. + */ + public PointModel getNextPoint() { + return fNextPoint; + } + + /** + * Sets the point the vehicle will drive to next. + * + * @param point The next point. + */ + public void setNextPoint(PointModel point) { + fNextPoint = point; + } + + /** + * Returns the current position. + * + * @return The position (x,y,z). + */ + public Triple getPrecisePosition() { + return fPrecisePosition; + } + + /** + * Sets the current position. + * + * @param position A triple containing the position. + */ + public void setPrecisePosition(Triple position) { + fPrecisePosition = position; + } + + /** + * Returns the current orientation angle. + * + * @return The orientation angle. + */ + public double getOrientationAngle() { + return fOrientationAngle; + } + + /** + * Sets the orientation angle. + * + * @param angle The new angle. + */ + public void setOrientationAngle(double angle) { + fOrientationAngle = angle; + } + + /** + * Returns the color the drive order is painted in. + * + * @return The color. + */ + public Color getDriveOrderColor() { + return getPropertyRouteColor().getColor(); + } + + /** + * Returns the state of the drive order. + * + * @return The state. + */ + public TransportOrder.State getDriveOrderState() { + return fDriveOrderState; + } + + /** + * Sets the drive order state. + * + * @param driveOrderState The new state. + */ + public void setDriveOrderState(TransportOrder.State driveOrderState) { + fDriveOrderState = driveOrderState; + } + + /** + * Sets whether the drive order shall be displayed or not. + * + * @param state true to display the drive order. + */ + public void setDisplayDriveOrders(boolean state) { + fDisplayDriveOrders = state; + } + + /** + * Returns whether the drive order is displayed. + * + * @return true, if it displayed. + */ + public boolean getDisplayDriveOrders() { + return fDisplayDriveOrders; + } + + /** + * Returns whether the view follows this vehicle as it drives. + * + * @return true if it follows. + */ + public boolean isViewFollows() { + return fViewFollows; + } + + /** + * Sets whether the view follows this vehicle as it drives. + * + * @param viewFollows true if it follows. + */ + public void setViewFollows(boolean viewFollows) { + this.fViewFollows = viewFollows; + } + + /** + * Returns the kernel object. + * + * @return The kernel object. + */ + @Nonnull + public Vehicle getVehicle() { + return vehicle; + } + + /** + * Sets the kernel object. + * + * @param vehicle The kernel object. + */ + public void setVehicle( + @Nonnull + Vehicle vehicle + ) { + this.vehicle = requireNonNull(vehicle, "vehicle"); + } + + /** + * Returns the current path for the drive order. + * + * @return Path for the drive order. + */ + public PathModel getCurrentDriveOrderPath() { + return currentDriveOrderPath; + } + + /** + * Sets the current drive order path. + * + * @param path the current drive order path. + */ + public void setCurrentDriveOrderPath(PathModel path) { + currentDriveOrderPath = path; + } + + /** + * Returns the destination for the current drive order. + * + * @return the destination for the current drive order. + */ + public PointModel getDriveOrderDestination() { + return driveOrderDestination; + } + + /** + * Sets the destination for the current drive order. + * + * @param driveOrderDestination destination for the current drive order. + */ + public void setDriveOrderDestination(PointModel driveOrderDestination) { + this.driveOrderDestination = driveOrderDestination; + } + + /** + * Checks whether the last reported processing state of the vehicle would allow it to be assigned + * an order. + * + * @return {@code true} if, and only if, the vehicle's integration level is TO_BE_UTILIZED. + */ + public boolean isAvailableForOrder() { + return vehicle != null + && vehicle.getIntegrationLevel() == Vehicle.IntegrationLevel.TO_BE_UTILIZED; + } + + @Override // AbstractModelComponent + public String getTreeViewName() { + String treeViewName = getDescription() + " " + getName(); + + return treeViewName; + } + + @Override // AbstractModelComponent + public String getDescription() { + return bundle.getString("vehicleModel.description"); + } + + public BoundingBoxProperty getPropertyBoundingBox() { + return (BoundingBoxProperty) getProperty(BOUNDING_BOX); + } + + public ColorProperty getPropertyRouteColor() { + return (ColorProperty) getProperty(ElementPropKeys.VEHICLE_ROUTE_COLOR); + } + + public SpeedProperty getPropertyMaxVelocity() { + return (SpeedProperty) getProperty(MAXIMUM_VELOCITY); + } + + public SpeedProperty getPropertyMaxReverseVelocity() { + return (SpeedProperty) getProperty(MAXIMUM_REVERSE_VELOCITY); + } + + public EnergyLevelThresholdSetProperty getPropertyEnergyLevelThresholdSet() { + return (EnergyLevelThresholdSetProperty) getProperty(ENERGY_LEVEL_THRESHOLD_SET); + } + + public PercentProperty getPropertyEnergyLevel() { + return (PercentProperty) getProperty(ENERGY_LEVEL); + } + + @SuppressWarnings("unchecked") + public SelectionProperty getPropertyState() { + return (SelectionProperty) getProperty(STATE); + } + + @SuppressWarnings("unchecked") + public SelectionProperty getPropertyProcState() { + return (SelectionProperty) getProperty(PROC_STATE); + } + + @SuppressWarnings("unchecked") + public SelectionProperty getPropertyIntegrationLevel() { + return (SelectionProperty) getProperty(INTEGRATION_LEVEL); + } + + public BooleanProperty getPropertyPaused() { + return (BooleanProperty) getProperty(PAUSED); + } + + public AngleProperty getPropertyOrientationAngle() { + return (AngleProperty) getProperty(ORIENTATION_ANGLE); + } + + public TripleProperty getPropertyPrecisePosition() { + return (TripleProperty) getProperty(PRECISE_POSITION); + } + + public StringProperty getPropertyPoint() { + return (StringProperty) getProperty(POINT); + } + + public StringProperty getPropertyNextPoint() { + return (StringProperty) getProperty(NEXT_POINT); + } + + public BooleanProperty getPropertyLoaded() { + return (BooleanProperty) getProperty(LOADED); + } + + public StringProperty getPropertyCurrentOrderName() { + return (StringProperty) getProperty(CURRENT_TRANSPORT_ORDER_NAME); + } + + public StringProperty getPropertyCurrentSequenceName() { + return (StringProperty) getProperty(CURRENT_SEQUENCE_NAME); + } + + public OrderTypesProperty getPropertyAllowedOrderTypes() { + return (OrderTypesProperty) getProperty(ALLOWED_ORDER_TYPES); + } + + public StringProperty getPropertyEnvelopeKey() { + return (StringProperty) getProperty(ENVELOPE_KEY); + } + + public KeyValueSetProperty getPropertyMiscellaneous() { + return (KeyValueSetProperty) getProperty(MISCELLANEOUS); + } + + public ResourceProperty getAllocatedResources() { + return (ResourceProperty) getProperty(ALLOCATED_RESOURCES); + } + + public ResourceProperty getClaimedResources() { + return (ResourceProperty) getProperty(CLAIMED_RESOURCES); + } + + private void createProperties() { + StringProperty pName = new StringProperty(this); + pName.setDescription(bundle.getString("vehicleModel.property_name.description")); + pName.setHelptext(bundle.getString("vehicleModel.property_name.helptext")); + setProperty(NAME, pName); + + BoundingBoxProperty pBoundingBox = new BoundingBoxProperty( + this, + new BoundingBoxModel(1000, 1000, 1000, new Couple(0, 0)) + ); + pBoundingBox.setDescription(bundle.getString("vehicleModel.property_boundingBox.description")); + pBoundingBox.setHelptext(bundle.getString("vehicleModel.property_boundingBox.helptext")); + setProperty(BOUNDING_BOX, pBoundingBox); + + ColorProperty pColor = new ColorProperty(this, Color.red); + pColor.setDescription(bundle.getString("vehicleModel.property_routeColor.description")); + pColor.setHelptext(bundle.getString("vehicleModel.property_routeColor.helptext")); + setProperty(ElementPropKeys.VEHICLE_ROUTE_COLOR, pColor); + + SpeedProperty pMaximumVelocity = new SpeedProperty(this, 1000, SpeedProperty.Unit.MM_S); + pMaximumVelocity.setDescription( + bundle.getString("vehicleModel.property_maximumVelocity.description") + ); + pMaximumVelocity.setHelptext( + bundle.getString("vehicleModel.property_maximumVelocity.helptext") + ); + setProperty(MAXIMUM_VELOCITY, pMaximumVelocity); + + SpeedProperty pMaximumReverseVelocity = new SpeedProperty(this, 1000, SpeedProperty.Unit.MM_S); + pMaximumReverseVelocity.setDescription( + bundle.getString("vehicleModel.property_maximumReverseVelocity.description") + ); + pMaximumReverseVelocity.setHelptext( + bundle.getString("vehicleModel.property_maximumReverseVelocity.helptext") + ); + setProperty(MAXIMUM_REVERSE_VELOCITY, pMaximumReverseVelocity); + + EnergyLevelThresholdSetProperty pEnergyLevelThresholdSet = new EnergyLevelThresholdSetProperty( + this, new EnergyLevelThresholdSetModel(30, 90, 40, 95) + ); + pEnergyLevelThresholdSet.setDescription( + bundle.getString("vehicleModel.property_energyLevelThresholdSet.description") + ); + pEnergyLevelThresholdSet.setHelptext( + bundle.getString("vehicleModel.property_energyLevelThresholdSet.helptext") + ); + pEnergyLevelThresholdSet.setModellingEditable(true); + pEnergyLevelThresholdSet.setOperatingEditable(true); + setProperty(ENERGY_LEVEL_THRESHOLD_SET, pEnergyLevelThresholdSet); + + PercentProperty pEnergyLevel = new PercentProperty(this, true); + pEnergyLevel.setDescription(bundle.getString("vehicleModel.property_energyLevel.description")); + pEnergyLevel.setHelptext(bundle.getString("vehicleModel.property_energyLevel.helptext")); + pEnergyLevel.setModellingEditable(false); + setProperty(ENERGY_LEVEL, pEnergyLevel); + + BooleanProperty pLoaded = new BooleanProperty(this); + pLoaded.setDescription(bundle.getString("vehicleModel.property_loaded.description")); + pLoaded.setHelptext(bundle.getString("vehicleModel.property_loaded.helptext")); + pLoaded.setModellingEditable(false); + setProperty(LOADED, pLoaded); + + SelectionProperty pState + = new SelectionProperty<>( + this, + Arrays.asList(Vehicle.State.values()), + Vehicle.State.UNKNOWN + ); + pState.setDescription(bundle.getString("vehicleModel.property_state.description")); + pState.setHelptext(bundle.getString("vehicleModel.property_state.helptext")); + pState.setModellingEditable(false); + setProperty(STATE, pState); + + SelectionProperty pProcState + = new SelectionProperty<>( + this, + Arrays.asList(Vehicle.ProcState.values()), + Vehicle.ProcState.IDLE + ); + pProcState.setDescription( + bundle.getString("vehicleModel.property_processingState.description") + ); + pProcState.setHelptext(bundle.getString("vehicleModel.property_processingState.helptext")); + pProcState.setModellingEditable(false); + setProperty(PROC_STATE, pProcState); + + SelectionProperty pIntegrationLevel + = new SelectionProperty<>( + this, + Arrays.asList(Vehicle.IntegrationLevel.values()), + Vehicle.IntegrationLevel.TO_BE_RESPECTED + ); + pIntegrationLevel.setDescription( + bundle.getString("vehicleModel.property_integrationLevel.description") + ); + pIntegrationLevel.setHelptext( + bundle.getString("vehicleModel.property_integrationLevel.helptext") + ); + pIntegrationLevel.setModellingEditable(false); + setProperty(INTEGRATION_LEVEL, pIntegrationLevel); + + BooleanProperty pPaused = new BooleanProperty(this); + pPaused.setDescription(bundle.getString("vehicleModel.property_paused.description")); + pPaused.setHelptext(bundle.getString("vehicleModel.property_paused.helptext")); + pPaused.setModellingEditable(false); + pPaused.setOperatingEditable(true); + setProperty(PAUSED, pPaused); + + StringProperty pPoint = new StringProperty(this); + pPoint.setDescription(bundle.getString("vehicleModel.property_currentPoint.description")); + pPoint.setHelptext(bundle.getString("vehicleModel.property_currentPoint.helptext")); + pPoint.setModellingEditable(false); + setProperty(POINT, pPoint); + + StringProperty pNextPoint = new StringProperty(this); + pNextPoint.setDescription(bundle.getString("vehicleModel.property_nextPoint.description")); + pNextPoint.setHelptext(bundle.getString("vehicleModel.property_nextPoint.helptext")); + pNextPoint.setModellingEditable(false); + setProperty(NEXT_POINT, pNextPoint); + + TripleProperty pPrecisePosition = new TripleProperty(this); + pPrecisePosition.setDescription( + bundle.getString("vehicleModel.property_precisePosition.description") + ); + pPrecisePosition.setHelptext( + bundle.getString("vehicleModel.property_precisePosition.helptext") + ); + pPrecisePosition.setModellingEditable(false); + setProperty(PRECISE_POSITION, pPrecisePosition); + + AngleProperty pOrientationAngle = new AngleProperty(this); + pOrientationAngle.setDescription( + bundle.getString("vehicleModel.property_orientationAngle.description") + ); + pOrientationAngle.setHelptext( + bundle.getString("vehicleModel.property_orientationAngle.helptext") + ); + pOrientationAngle.setModellingEditable(false); + setProperty(ORIENTATION_ANGLE, pOrientationAngle); + + StringProperty pEnvelopeKey = new StringProperty(this); + pEnvelopeKey.setDescription(bundle.getString("vehicleModel.property_envelopeKey.description")); + pEnvelopeKey.setHelptext(bundle.getString("vehicleModel.property_envelopeKey.helptext")); + pEnvelopeKey.setModellingEditable(true); + pEnvelopeKey.setOperatingEditable(false); + setProperty(ENVELOPE_KEY, pEnvelopeKey); + + KeyValueSetProperty pMiscellaneous = new KeyValueSetProperty(this); + pMiscellaneous.setDescription( + bundle.getString("vehicleModel.property_miscellaneous.description") + ); + pMiscellaneous.setHelptext(bundle.getString("vehicleModel.property_miscellaneous.helptext")); + pMiscellaneous.setOperatingEditable(true); + setProperty(MISCELLANEOUS, pMiscellaneous); + + StringProperty curTransportOrderName = new StringProperty(this); + curTransportOrderName.setDescription( + bundle.getString("vehicleModel.property_currentTransportOrder.description") + ); + curTransportOrderName.setHelptext( + bundle.getString("vehicleModel.property_currentTransportOrder.helptext") + ); + curTransportOrderName.setModellingEditable(false); + setProperty(CURRENT_TRANSPORT_ORDER_NAME, curTransportOrderName); + + StringProperty curOrderSequenceName = new StringProperty(this); + curOrderSequenceName.setDescription( + bundle.getString("vehicleModel.property_currentOrderSequence.description") + ); + curOrderSequenceName.setHelptext( + bundle.getString("vehicleModel.property_currentOrderSequence.helptext") + ); + curOrderSequenceName.setModellingEditable(false); + setProperty(CURRENT_SEQUENCE_NAME, curOrderSequenceName); + + OrderTypesProperty pAllowedOrderTypes = new OrderTypesProperty(this); + pAllowedOrderTypes.setDescription( + bundle.getString("vehicleModel.property_allowedOrderTypes.description") + ); + pAllowedOrderTypes.setHelptext( + bundle.getString("vehicleModel.property_allowedOrderTypes.helptext") + ); + pAllowedOrderTypes.setModellingEditable(false); + pAllowedOrderTypes.setOperatingEditable(true); + setProperty(ALLOWED_ORDER_TYPES, pAllowedOrderTypes); + + ResourceProperty allocatedResources = new ResourceProperty(this); + allocatedResources.setDescription( + bundle.getString("vehicleModel.property_allocatedResources.description") + ); + allocatedResources.setHelptext( + bundle.getString("vehicleModel.property_allocatedResources.helptext") + ); + allocatedResources.setModellingEditable(false); + allocatedResources.setOperatingEditable(true); + setProperty(ALLOCATED_RESOURCES, allocatedResources); + + ResourceProperty claimedResources = new ResourceProperty(this); + claimedResources.setDescription( + bundle.getString("vehicleModel.property_claimedResources.description") + ); + claimedResources.setHelptext( + bundle.getString("vehicleModel.property_claimedResources.helptext") + ); + claimedResources.setModellingEditable(false); + claimedResources.setOperatingEditable(true); + setProperty(CLAIMED_RESOURCES, claimedResources); + } +} diff --git a/opentcs-plantoverview-base/src/main/resources/i18n/org/opentcs/plantoverview/base/Bundle.properties b/opentcs-plantoverview-base/src/main/resources/i18n/org/opentcs/plantoverview/base/Bundle.properties new file mode 100644 index 0000000..ce451d2 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/resources/i18n/org/opentcs/plantoverview/base/Bundle.properties @@ -0,0 +1,212 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +blockModel.description=Block +blockModel.property_color.description=Block color +blockModel.property_color.helptext=Color of the block's elements +blockModel.property_elements.description=Block members +blockModel.property_elements.helptext=The members of this block +blockModel.property_miscellaneous.description=Miscellaneous +blockModel.property_miscellaneous.helptext=Miscellaneous properties of this block +blockModel.property_name.description=Name +blockModel.property_name.helptext=The name of this block +blockModel.property_type.description=Type +blockModel.property_type.helptext=The type of this block +blockModel.type.sameDirectionOnly.description=Same direction only +blockModel.type.singleVehicleOnly.description=Single vehicle only +groupModel.description=Group +groupModel.property_elements.description=Group members +groupModel.property_miscellaneous.description=Miscellaneous +groupModel.property_miscellaneous.helptext=Miscellaneous properties of this group +groupModel.property_name.description=Name +groupModel.property_name.helptext=The name of this group +layoutModel.description=Layout +layoutModel.property_layerGroups.description=Layer groups +layoutModel.property_layerGroups.helptext=The layer groups of the model. +layoutModel.property_layerWrappers.description=Layers +layoutModel.property_layerWrappers.helptext=The layers of the model. +layoutModel.property_miscellaneous.description=Miscellaneous +layoutModel.property_miscellaneous.helptext=All miscellaneous properties can be placed here +layoutModel.property_name.description=Name +layoutModel.property_name.helptext=The name of this layout +layoutModel.property_scaleX.description=Scale of x-axis +layoutModel.property_scaleX.helptext=Length per pixel in x direction +layoutModel.property_scaleY.description=Scale of y-axis +layoutModel.property_scaleY.helptext=Length per pixel in y direction +layoutModel.treeViewName=Layout +linkModel.description=Link +linkModel.property_endComponent.description=End Component +linkModel.property_layerWrapper.description=Layer +linkModel.property_layerWrapper.helptext=The layer on which the point is drawn. +linkModel.property_name.description=Name +linkModel.property_name.helptext=Name of the link +linkModel.property_operations.description=Actions +linkModel.property_operations.helptext=Action ... +linkModel.property_startComponent.description=Start Component +locationModel.description=Location +locationModel.property_labelOffsetX.description=Label x offset +locationModel.property_labelOffsetX.helptext=X offset of the location's Label +locationModel.property_labelOffsetY.description=Label y offset +locationModel.property_labelOffsetY.helptext=Y offset of the location's Label +locationModel.property_labelOrientationAngle.description=Label orientation angle +locationModel.property_labelOrientationAngle.helptext=Angle orientation of the location's Label +locationModel.property_layerWrapper.description=Layer +locationModel.property_layerWrapper.helptext=The layer on which the location is drawn. +locationModel.property_locked.description=Locked +locationModel.property_locked.helptext=Indicates whether the location is locked and therefore cannot be used by vehicles. +locationModel.property_miscellaneous.description=Miscellaneous +locationModel.property_miscellaneous.helptext=Miscelleneous properties of this location +locationModel.property_modelPositionX.description=x-Position (model) +locationModel.property_modelPositionX.helptext=The x coordinate of the location in the kernel model +locationModel.property_modelPositionY.description=y-Position (model) +locationModel.property_modelPositionY.helptext=The y coordinate of the location in the kernel model +locationModel.property_name.description=Name +locationModel.property_name.helptext=The name of this location +locationModel.property_peripheralJob.description=Peripheral job +locationModel.property_peripheralJob.helptext=The current peripheral job +locationModel.property_peripheralReservationToken.description=Reservation token +locationModel.property_peripheralReservationToken.helptext=The reservation token for this location +locationModel.property_peripheralState.description=Peripheral state +locationModel.property_peripheralState.helptext=The current state of the peripheral +locationModel.property_peripheralProcState.description=Processing state +locationModel.property_peripheralProcState.helptext=The current processing state for this peripheral +locationModel.property_positionX.description=Location x position +locationModel.property_positionX.helptext=X coordinate of the location +locationModel.property_positionY.description=Location y position +locationModel.property_positionY.helptext=Y coordinate of the location +locationModel.property_symbol.description=Symbol +locationModel.property_symbol.helptext=The graphical symbol for this location +locationModel.property_type.description=Type +locationModel.property_type.helptext=The type of this location +locationTypeModel.description=Location type +locationTypeModel.property_allowedOperations.description=Supported vehicle operations +locationTypeModel.property_allowedOperations.helptext=The actions that are allowed at locations of this type +locationTypeModel.property_allowedPeripheralOperations.description=Supported peripheral operations +locationTypeModel.property_allowedPeripheralOperations.helptext=The peripheral operations that are allowed at locations of this type +locationTypeModel.property_miscellaneous.description=Miscellaneous +locationTypeModel.property_miscellaneous.helptext=All miscellaneous properties can be placed here +locationTypeModel.property_name.description=Name +locationTypeModel.property_name.helptext=The description of the location type +locationTypeModel.property_symbol.description=Symbol +locationTypeModel.property_symbol.helptext=The graphical symbol for locations of this type +multipleDifferentValues.description= +multipleDifferentValues.helptext=This value is not uniform. Changes will be applied to all selected objects. +otherGraphicalElement.description=Graphical Object +pathModel.description=Path +pathModel.property_endComponent.description=End Component +pathModel.property_layerWrapper.description=Layer +pathModel.property_layerWrapper.helptext=The layer on which the path is drawn. +pathModel.property_length.description=Length +pathModel.property_length.helptext=The length of this path in the kernel model +pathModel.property_locked.description=Locked +pathModel.property_locked.helptext=Shows if an element is locked and therefore cannot be used by vehicles. +pathModel.property_maximumReverseVelocity.description=Maxmimum reverse velocity +pathModel.property_maximumReverseVelocity.helptext=The maximum reverse velocity vehicles are allow to drive on this course segment. +pathModel.property_maximumVelocity.description=Maximum velocity +pathModel.property_maximumVelocity.helptext=The maximum velocity vehicles are allow to drive on this course segment. +pathModel.property_miscellaneous.description=Miscellaneous +pathModel.property_miscellaneous.helptext=All miscellaneous properties can of the course can be placed here +pathModel.property_name.description=Name +pathModel.property_name.helptext=The name of this path segment +pathModel.property_pathConnectionType.description=Path connection type +pathModel.property_pathConnectionType.helptext=Connection type of this path +pathModel.property_pathControlPoints.description=Path control points +pathModel.property_pathControlPoints.helptext=Bezier control points of this path +pathModel.property_peripheralOperations.description=Peripheral operations +pathModel.property_peripheralOperations.helptext=The peripheral operations that are to be executed at this path. +pathModel.property_startComponent.description=Start Component +pathModel.property_vehicleEnvelopes.description=Vehicle envelopes +pathModel.property_vehicleEnvelopes.helptext=The vehicle envelopes for this path +pathModel.type.bezier.description=2-Bezier +pathModel.type.bezier.helptext=Creates a bezier path with two control points +pathModel.type.bezier3.description=3-Bezier +pathModel.type.bezier3.helptext=Creates a bezier path with three control points +pathModel.type.direct.description=Direct +pathModel.type.direct.helptext=Creates a direct path +pathModel.type.elbow.description=Elbow +pathModel.type.elbow.helptext=Creates an elbow path +pathModel.type.polypath.description=Poly-Path +pathModel.type.polypath.helptext=Creates a path with multiple conrol points. ('CTRL+double click' adds a control point, 'CTRL+ALT+double click' removes a control point) +pathModel.type.slanted.description=Slanted +pathModel.type.slanted.helptext=Creates a slanted path +pointModel.description=Point +pointModel.property_angle.description=Angle +pointModel.property_angle.helptext=The angle orientation of a vehicle on this point +pointModel.property_labelOffsetX.description=Label x offset +pointModel.property_labelOffsetX.helptext=X offset of the point's label +pointModel.property_labelOffsetY.description=Label y offset +pointModel.property_labelOffsetY.helptext=Y offset of the point's label +pointModel.property_labelOrientationAngle.description=Label orientation angle +pointModel.property_labelOrientationAngle.helptext=Angle orientation of the point's Label +pointModel.property_layerWrapper.description=Layer +pointModel.property_layerWrapper.helptext=The layer on which the point is drawn. +pointModel.property_maxVehicleBoundingBox.description=Maximum vehicle bounding box +pointModel.property_maxVehicleBoundingBox.helptext=The maximum bounding box that a vehicle at this point is allowed to have +pointModel.property_miscellaneous.description=Miscellaneous +pointModel.property_miscellaneous.helptext=All miscellaneous properties of the point can be placed here +pointModel.property_modelPositionX.description=x-position (model) +pointModel.property_modelPositionX.helptext=The x coordinate of the point in the kernel model +pointModel.property_modelPositionY.description=y-position (model) +pointModel.property_modelPositionY.helptext=The y coordinate of the point in the kernel model +pointModel.property_name.description=Name +pointModel.property_name.helptext=The description of the point +pointModel.property_positionX.description=Point x position +pointModel.property_positionX.helptext=X coordinate of the point +pointModel.property_positionY.description=Point y position +pointModel.property_positionY.helptext=Y coordinate of the point +pointModel.property_type.description=Type +pointModel.property_type.helptext=The type of the point +pointModel.property_vehicleEnvelopes.description=Vehicle envelopes +pointModel.property_vehicleEnvelopes.helptext=The vehicle envelopes for this point +pointModel.type.halt.description=Halt point +pointModel.type.halt.helptext=Creates a point where a vehicle can stop +pointModel.type.park.description=Park point +pointModel.type.park.helptext=Creates a point where a vehicle can park +propertiesCollection.description=Multiple objects selected +vehicleModel.description=Vehicle +vehicleModel.property_allocatedResources.description=Allocated resources +vehicleModel.property_allocatedResources.helptext=Resources allocated by this vehicle +vehicleModel.property_allowedOrderTypes.description=Allowed order types +vehicleModel.property_allowedOrderTypes.helptext=The types of transport orders the vehicle is allowed to process +vehicleModel.property_boundingBox.description=Bounding box +vehicleModel.property_boundingBox.helptext=The vehicle's bounding box +vehicleModel.property_claimedResources.description=Claimed resources +vehicleModel.property_claimedResources.helptext=Resources claimed by this vehicle +vehicleModel.property_currentOrderSequence.description=Current order sequence +vehicleModel.property_currentOrderSequence.helptext=Current order sequence +vehicleModel.property_currentPoint.description=Current point +vehicleModel.property_currentPoint.helptext=Current point reported by the kernel +vehicleModel.property_currentTransportOrder.description=Current transport order +vehicleModel.property_currentTransportOrder.helptext=Current transport order +vehicleModel.property_energyLevel.description=Current energy level +vehicleModel.property_energyLevel.helptext=Current energy level reported by the kernel +vehicleModel.property_energyLevelThresholdSet.description= Energy level threshold set +vehicleModel.property_energyLevelThresholdSet.helptext= Energy level threshold set +vehicleModel.property_envelopeKey.description=Envelope key +vehicleModel.property_envelopeKey.helptext=The vehicle's envelope key +vehicleModel.property_integrationLevel.description=Integration level +vehicleModel.property_integrationLevel.helptext=The vehicle's integration level +vehicleModel.property_loaded.description=Loaded +vehicleModel.property_loaded.helptext=Displays if this vehicle has accepted load. +vehicleModel.property_maximumReverseVelocity.description=Maximum reverse velocity +vehicleModel.property_maximumReverseVelocity.helptext =The maximum reverse velocity of the vehicle +vehicleModel.property_maximumVelocity.description=Maximum velocity +vehicleModel.property_maximumVelocity.helptext =The maximum forward velocity of the vehicle +vehicleModel.property_miscellaneous.description=Miscellaneous +vehicleModel.property_miscellaneous.helptext=Miscellaneous properties of the vehicle. +vehicleModel.property_name.description=Name +vehicleModel.property_name.helptext=The name of the vehicle +vehicleModel.property_nextPoint.description=Next point +vehicleModel.property_nextPoint.helptext=Next point reported by the kernel +vehicleModel.property_orientationAngle.description=Vehicle orientation +vehicleModel.property_orientationAngle.helptext=Vehicle orientation reported by the kernel +vehicleModel.property_paused.description=Paused +vehicleModel.property_paused.helptext=Indicates whether the vehicle is paused. +vehicleModel.property_precisePosition.description=Exact position +vehicleModel.property_precisePosition.helptext=Exact position reported by the kernel +vehicleModel.property_processingState.description=Processing state +vehicleModel.property_processingState.helptext=Processing state reported by the kernel +vehicleModel.property_routeColor.description=Route color +vehicleModel.property_routeColor.helptext=The color the vehicle routes are emphasised +vehicleModel.property_state.description=State +vehicleModel.property_state.helptext=State reported by the kernel diff --git a/opentcs-plantoverview-base/src/main/resources/i18n/org/opentcs/plantoverview/base/Bundle_de.properties b/opentcs-plantoverview-base/src/main/resources/i18n/org/opentcs/plantoverview/base/Bundle_de.properties new file mode 100644 index 0000000..1669bd8 --- /dev/null +++ b/opentcs-plantoverview-base/src/main/resources/i18n/org/opentcs/plantoverview/base/Bundle_de.properties @@ -0,0 +1,211 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +blockModel.description=Block +blockModel.property_color.description=Farbe +blockModel.property_color.helptext=Die Farbe, in der die Elemente des Blockbereichs dargestellt werden +blockModel.property_elements.description=Block Elemente +blockModel.property_elements.helptext=Die Mitglieder dieses Blocks +blockModel.property_miscellaneous.description=Die \u00dcbrigen +blockModel.property_miscellaneous.helptext=Die \u00dcbrigen Eigenschaften dieses Blocks +blockModel.property_name.description=Name +blockModel.property_name.helptext=Der Name dieses Blocks +blockModel.property_type.description=Typ +blockModel.property_type.helptext=Der Typ dieses Blocks +blockModel.type.sameDirectionOnly.description=Nur selbe Richtung +blockModel.type.singleVehicleOnly.description=Nur ein Fahrzeug +groupModel.description=Gruppe +groupModel.property_elements.description=Gruppenmitglieder +groupModel.property_miscellaneous.description=Sonstiges +groupModel.property_miscellaneous.helptext=Hier k\u00f6nnen alle sonstigen Eigenschaften der Gruppe eingetragen werden +groupModel.property_name.description=Gruppe +groupModel.property_name.helptext=Die Bezeichnung der Gruppe +layoutModel.description=Layout +layoutModel.property_layerGroups.description=Ebenengruppen +layoutModel.property_layerGroups.helptext=Die Ebenengruppen des Modells. +layoutModel.property_layerWrappers.description=Ebenen +layoutModel.property_layerWrappers.helptext=Die Ebenen des Modells. +layoutModel.property_miscellaneous.description=Sonstiges +layoutModel.property_miscellaneous.helptext=Hier k\u00f6nnen alle sonstigen Eigenschaften des Layouts eingetragen werden +layoutModel.property_name.description=Name +layoutModel.property_name.helptext=Die Bezeichnung des Layouts +layoutModel.property_scaleX.description=Ma\u00dfstab x-Achse +layoutModel.property_scaleX.helptext=L\u00e4nge pro Pixel in Richtung der x-Achse +layoutModel.property_scaleY.description=Ma\u00dfstab y-Achse +layoutModel.property_scaleY.helptext=L\u00e4nge pro Pixel in Richtung der y-Achse +layoutModel.treeViewName=Layout +linkModel.description=Link +linkModel.property_endComponent.description=Endkomponente +linkModel.property_layerWrapper.description=Ebene +linkModel.property_layerWrapper.helptext=Die Ebene, auf welcher der Link gezeichnet wird. +linkModel.property_name.description=Name +linkModel.property_name.helptext=Die Bezeichnung der Referenz +linkModel.property_operations.description=Aktionen +linkModel.property_operations.helptext=Die Aktion, die in Abh\u00e4ngigkeit vom Meldepunkt an der Station ausgef\u00fchrt werden kann. +linkModel.property_startComponent.description=Startkomponente +locationModel.description=Station +locationModel.property_labelOffsetX.description=X Offset Beschriftung +locationModel.property_labelOffsetX.helptext=X Offset der Beschriftung der Station +locationModel.property_labelOffsetY.description=Y Offset Beschriftung +locationModel.property_labelOffsetY.helptext=Y Offset der Beschriftung der Station +locationModel.property_labelOrientationAngle.description=Winkelsausrichtung der Beschriftung +locationModel.property_labelOrientationAngle.helptext=Winkelausrichtung der Stations-Beschriftung +locationModel.property_layerWrapper.description=Ebene +locationModel.property_layerWrapper.helptext=Die Ebene, auf welcher die Station gezeichnet wird. +locationModel.property_locked.description=Gesperrt +locationModel.property_locked.helptext=Zeigt an, ob die Station gesperrt ist und somit nicht von Fahrzeugen benutzt werden kann. +locationModel.property_miscellaneous.description=Sonstiges +locationModel.property_miscellaneous.helptext=Hier k\u00f6nnen alle sonstigen Eigenschaften der Station eingetragen werden +locationModel.property_modelPositionX.description=x-Position (Modell) +locationModel.property_modelPositionX.helptext=Die x-Koordinate der Station im Kernel-Modell +locationModel.property_modelPositionY.description=y-Position (Modell) +locationModel.property_modelPositionY.helptext=Die y-Koordinate der Station im Kernel-Modell +locationModel.property_name.description=Name +locationModel.property_name.helptext=Die Bezeichnung der Station +locationModel.property_peripheralJob.description=Peripherieauftrag +locationModel.property_peripheralJob.helptext=Der Peripherieauftrag der zurzeit ausgef\u00fchrt wird +locationModel.property_peripheralReservationToken.description=Reservierungstoken +locationModel.property_peripheralReservationToken.helptext=Der Reservierungstoken f\u00fcr diese Station +locationModel.property_peripheralState.description=Peripheriestatus +locationModel.property_peripheralState.helptext=Der Status dieser Peripherie +locationModel.property_peripheralProcState.description=Ausf\u00fchrungsstatus +locationModel.property_peripheralProcState.helptext=Der Ausf\u00fchrungsstatus dieser Peripherie +locationModel.property_positionX.description=x-Position (Layout) +locationModel.property_positionX.helptext=Die x-Koordinate der Station im Layout +locationModel.property_positionY.description=y-Position (Layout) +locationModel.property_positionY.helptext=Die y-Koordinate der Station im Layout +locationModel.property_symbol.description=Symbol +locationModel.property_symbol.helptext=Das grafische Symbol f\u00fcr diese Station +locationModel.property_type.description=Typ +locationModel.property_type.helptext=Der Typ der Station +locationTypeModel.description=Stationstyp +locationTypeModel.property_allowedOperations.description=Unterst\u00fctzte Fahrzeugoperationen +locationTypeModel.property_allowedOperations.helptext=Die Aktionen, die an Stationen dieses Typs ausgef\u00fchrt werden k\u00f6nnen +locationTypeModel.property_allowedPeripheralOperations.description=Unterst\u00fctzte Peripherieoperationen +locationTypeModel.property_allowedPeripheralOperations.helptext=Die Peripherieoperationen, die an Stationen dieses Typs ausgef\u00fchrt werden k\u00f6nnen +locationTypeModel.property_miscellaneous.description=Sonstiges +locationTypeModel.property_miscellaneous.helptext=Hier k\u00f6nnen alle sonstigen Eigenschaften des Stationstyps eingetragen werden +locationTypeModel.property_name.description=Name +locationTypeModel.property_name.helptext=Die Bezeichnung des Stationstyps +locationTypeModel.property_symbol.description=Symbol +locationTypeModel.property_symbol.helptext=Das grafische Symbol f\u00fcr Stationen dieses Typs +multipleDifferentValues.description= +multipleDifferentValues.helptext=Dieser Wert ist nicht einheitlich. \u00c4nderungen werden auf alle selektieren Objekte angewandt. +otherGraphicalElement.description=Grafisches Objekt +pathModel.description=Strecke +pathModel.property_endComponent.description=Endkomponente +pathModel.property_layerWrapper.description=Ebene +pathModel.property_layerWrapper.helptext=Die Ebene, auf welcher der Pfad gezeichnet wird. +pathModel.property_length.description=L\u00e4nge +pathModel.property_length.helptext=Die L\u00e4nge des Streckensegments im Kernel-Modell +pathModel.property_locked.description=Gesperrt +pathModel.property_locked.helptext=Zeigt an, ob das Steckensegment gesperrt ist und somit nicht von Fahrzeugen benutzt werden kann. +pathModel.property_maximumReverseVelocity.description=H\u00f6chstgeschwindigkeit r\u00fcckw\u00e4rts +pathModel.property_maximumReverseVelocity.helptext=Die H\u00f6chstgeschwindigkeit, mit der Fahrzeuge auf diesem Streckensegment r\u00fcckw\u00e4rts unterwegs sein d\u00fcrfen. +pathModel.property_maximumVelocity.description=H\u00f6chstgeschwindigkeit vorw\u00e4rts +pathModel.property_maximumVelocity.helptext=Die H\u00f6chstgeschwindigkeit, mit der Fahrzeuge auf diesem Streckensegment vorw\u00e4rts unterwegs sein d\u00fcrfen. +pathModel.property_miscellaneous.description=Sonstiges +pathModel.property_miscellaneous.helptext=Hier k\u00f6nnen alle sonstigen Eigenschaften der Strecke eingetragen werden +pathModel.property_name.description=Name +pathModel.property_name.helptext=Die Bezeichnung des Streckensegments +pathModel.property_pathConnectionType.description=Verbinder-Typ +pathModel.property_pathConnectionType.helptext=Die Art der Verbinder-Linie +pathModel.property_pathControlPoints.description=Kontrollpunkte +pathModel.property_pathControlPoints.helptext=2 Kontrollpunkte (nur f\u00fcr Bezier-Verbinder) +pathModel.property_peripheralOperations.description=Peripherieoperationen +pathModel.property_peripheralOperations.helptext=Die Peripherieoperationen die auf diesem Pfad ausgef\u00fchrt werden. +pathModel.property_startComponent.description=Startkomponente +pathModel.property_vehicleEnvelopes.description=Fahrzeugh\u00fcllkurven +pathModel.property_vehicleEnvelopes.helptext=Die H\u00fcllkurven von Fahrzeugen auf diesem Pfad +pathModel.type.bezier.description=2-Bezier +pathModel.type.bezier.helptext=Erstellt einen Bezier-Pfad mit zwei Kontrollpunkten +pathModel.type.bezier3.description=3-Bezier +pathModel.type.bezier3.helptext=Erstellt einen Bezier-Pfad mit drei Kontrollpunkten +pathModel.type.direct.description=Direkt +pathModel.type.direct.helptext=Erstellt eine direkte Verbindung +pathModel.type.elbow.description=Abgewinkelt +pathModel.type.elbow.helptext=Erstellt eine abgewinkelte Verbindung +pathModel.type.polypath.helptext=Erstellt eine Pfad mit mehreren Kontrollpunkten.('STRG+Doppelklick' erstellt einen Kontrollpuntk, 'STRG+ALT+Doppelklick' l\u00f6scht einen Kontrollpuntk) +pathModel.type.slanted.description=Abgeschr\u00e4gt +pathModel.type.slanted.helptext=Erstellt eine abgeschr\\u00e4gte Verbindung +pointModel.description=Punkt +pointModel.property_angle.description=Winkel +pointModel.property_angle.helptext=Die Winkelausrichtung eines Fahrzeugs auf diesem Punkt +pointModel.property_labelOffsetX.description=X Offset Beschriftung +pointModel.property_labelOffsetX.helptext=X Offset der Beschriftung des Punktes +pointModel.property_labelOffsetY.description=Y Offset Beschriftung +pointModel.property_labelOffsetY.helptext=Y Offset der Beschriftung des Punktes +pointModel.property_labelOrientationAngle.description=Winkelausrichtung der Beschriftung +pointModel.property_labelOrientationAngle.helptext=Winkelausrichtung der Beschriftung des Punktes +pointModel.property_layerWrapper.description=Ebene +pointModel.property_layerWrapper.helptext=Die Ebene, auf welcher der Punkt gezeichnet wird. +pointModel.property_maxVehicleBoundingBox.description=Maximale Fahrzeug-Bounding-Box +pointModel.property_maxVehicleBoundingBox.helptext=Die maximale Bounding-Box, die ein Fahrzeug an diesem Punkt haben darf +pointModel.property_miscellaneous.description=Sonstiges +pointModel.property_miscellaneous.helptext=Hier k\u00f6nnen alle sonstigen Eigenschaften des Punktes eingetragen werden +pointModel.property_modelPositionX.description=x-Position (Modell) +pointModel.property_modelPositionX.helptext=Die x-Koordinate des Punktes im Kernel-Modell +pointModel.property_modelPositionY.description=y-Position (Modell) +pointModel.property_modelPositionY.helptext=Die y-Koordinate des Punktes im Kernel-Modell +pointModel.property_name.description=Name +pointModel.property_name.helptext=Die Bezeichnung des Punktes +pointModel.property_positionX.description=x-Position (Layout) +pointModel.property_positionX.helptext=Die x-Koordinate des Punktes im Layout +pointModel.property_positionY.description=y-Position (Layout) +pointModel.property_positionY.helptext=Die y-Koordinate des Punktes im Layout +pointModel.property_type.description=Typ +pointModel.property_type.helptext=Der Typ des Punktes +pointModel.property_vehicleEnvelopes.description=Fahrzeugh\u00fcllkurven +pointModel.property_vehicleEnvelopes.helptext=Die H\u00fcllkurven von Fahrzeugen an diesem Punkt +pointModel.type.halt.description=Haltepunkt +pointModel.type.halt.helptext=Erstellt ein Punkt an dem ein Fahrzeug stoppen kann +pointModel.type.park.description=Parkposition +pointModel.type.park.helptext=Erstellt einen Punkt wo ein Fahrzeug parken kann +propertiesCollection.description=Mehrere Objekte ausgew\u00e4hlt +vehicleModel.description=Fahrzeug +vehicleModel.property_allocatedResources.description=Zugewiesene Ressourcen +vehicleModel.property_allocatedResources.helptext=Ressourcen die zu diesem Fahrzeug zugewiesen sind +vehicleModel.property_allowedOrderTypes.description=Erlaubte Auftragstypen +vehicleModel.property_allowedOrderTypes.helptext=Typen von Transportauftr\u00e4gen die das Fahrzeug ausf\u00fchren darf +vehicleModel.property_boundingBox.description=Bounding-Box +vehicleModel.property_boundingBox.helptext=Die Bounding-Box des Fahrzeugs +vehicleModel.property_claimedResources.description=Beanspruchte Ressourcen +vehicleModel.property_claimedResources.helptext=Ressourcen die von diesem Fahrzeug beansprucht sind +vehicleModel.property_currentOrderSequence.description=Aktuelle Auftragssequenz +vehicleModel.property_currentOrderSequence.helptext=Aktuelle Auftragssequenz +vehicleModel.property_currentPoint.description=Aktueller Punkt +vehicleModel.property_currentPoint.helptext=Vom Kernel gemeldeter aktueller Punkt +vehicleModel.property_currentTransportOrder.description=Aktueller Transportauftrag +vehicleModel.property_currentTransportOrder.helptext=Aktueller Transportauftrag +vehicleModel.property_energyLevel.description=Aktueller Ladezustand +vehicleModel.property_energyLevel.helptext=Vom Kernel gemeldeter aktueller Ladezustand +vehicleModel.property_energyLevelThresholdSet.description= Energieschwellwerte +vehicleModel.property_energyLevelThresholdSet.helptext= Energieschwellwerte +vehicleModel.property_envelopeKey.description=H\u00fcllkurvenschl\u00fcssel +vehicleModel.property_envelopeKey.helptext=Der H\u00fcllkurvenschl\u00fcssel des Fahrzeugs +vehicleModel.property_integrationLevel.description=Integrationsstufe +vehicleModel.property_integrationLevel.helptext=Die Integrationsstufe des Fahrzeugs +vehicleModel.property_loaded.description=Beladen +vehicleModel.property_loaded.helptext=Zeigt an, ob das Fahrzeug eine Last aufgenommen hat. +vehicleModel.property_maximumReverseVelocity.description=Maximale r\u00fcckw\u00e4rts Geschwindigkeit +vehicleModel.property_maximumReverseVelocity.helptext =Die maximale Geschwindigkeit mit der das Fahrzeug r\u00fcckw\u00e4rts fahren kann. +vehicleModel.property_maximumVelocity.description=Maximale Geschwindigkeit +vehicleModel.property_maximumVelocity.helptext =Die maximale Geschwindigkeit mit der das Fahrzeug vorw\u00e4rts fahren kann. +vehicleModel.property_miscellaneous.description=Sonstiges +vehicleModel.property_miscellaneous.helptext=Sonstige Attribute des Fahrzeugs +vehicleModel.property_name.description=Name +vehicleModel.property_name.helptext=Die Bezeichnung des Fahrzeugs +vehicleModel.property_nextPoint.description=N\u00e4chster Punkt +vehicleModel.property_nextPoint.helptext=Vom Kernel gemeldeter n\u00e4chster Punkt +vehicleModel.property_orientationAngle.description=Fahrzeugausrichtung +vehicleModel.property_orientationAngle.helptext=Vom Kernel gemeldete Fahrzeugausrichtung +vehicleModel.property_paused.description=Pausiert +vehicleModel.property_paused.helptext=Zeigt, ob das Fahrzeug pausiert ist. +vehicleModel.property_precisePosition.description=Exakte Position +vehicleModel.property_precisePosition.helptext=Vom Kernel gemeldete exakte Position +vehicleModel.property_processingState.description=Bearbeitungszustand +vehicleModel.property_processingState.helptext=Vom Kernel gemeldeter Bearbeitungszustand +vehicleModel.property_routeColor.description=Routenfarbe +vehicleModel.property_routeColor.helptext=Die Farbe in der abzufahrende Routen dargestellt werden +vehicleModel.property_state.description=Zustand +vehicleModel.property_state.helptext=Vom Kernel gemeldeter Zustand diff --git a/opentcs-plantoverview-base/src/test/java/org/opentcs/guing/base/components/properties/type/AnglePropertyTest.java b/opentcs-plantoverview-base/src/test/java/org/opentcs/guing/base/components/properties/type/AnglePropertyTest.java new file mode 100644 index 0000000..d7d76a8 --- /dev/null +++ b/opentcs-plantoverview-base/src/test/java/org/opentcs/guing/base/components/properties/type/AnglePropertyTest.java @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.opentcs.guing.base.components.properties.type.AngleProperty.Unit; +import org.opentcs.guing.base.model.AbstractModelComponent; + +/** + * A test for an angle property. + * For Degrees, min value is 0 and max value is 360. + * For Radians, min value is 0 and max value is 2*PI ~ 6.283185 + */ +class AnglePropertyTest { + + private AngleProperty property; + + @ParameterizedTest + @ValueSource(strings = {"deg", "rad"}) + void testValidUnits(String unit) { + property = new AngleProperty(new DummyComponent()); + assertTrue(property.isPossibleUnit(unit)); + } + + @Test + void testPropertyConversionDegToRad() { + // 180 deg = PI rad + property = new AngleProperty(new DummyComponent(), 180, Unit.DEG); + property.convertTo(Unit.RAD); + assertEquals(Math.PI, (double) property.getValue(), 0); + assertEquals(Unit.RAD, property.getUnit()); + } + + @Test + void testPropertyConversionRadToDeg() { + // 3.7168 rad ~ 212.96 deg + property = new AngleProperty(new DummyComponent(), 3.7168, Unit.RAD); + property.convertTo(Unit.DEG); + assertEquals(212.96, (double) property.getValue(), 0.01); + assertEquals(Unit.DEG, property.getUnit()); + } + + @Test + void testPropertyRange() { + property = new AngleProperty(new DummyComponent()); + assertEquals(0, property.getValidRange().getMin(), 0); + assertEquals(Double.MAX_VALUE, property.getValidRange().getMax(), 0); + } + + @Test + void shouldStayInRangeDeg() { + property = new AngleProperty(new DummyComponent(), 540, Unit.DEG); + assertEquals(180.0, property.getValue()); + assertEquals(Unit.DEG, property.getUnit()); + } + + @Test + void shouldStayInRangeRad() { + property = new AngleProperty(new DummyComponent(), 10, Unit.RAD); + assertEquals(3.716, (double) property.getValue(), 0.001); + assertEquals(AngleProperty.Unit.RAD, property.getUnit()); + } + + private class DummyComponent + extends + AbstractModelComponent { + } +} diff --git a/opentcs-plantoverview-base/src/test/java/org/opentcs/guing/base/components/properties/type/LengthPropertyTest.java b/opentcs-plantoverview-base/src/test/java/org/opentcs/guing/base/components/properties/type/LengthPropertyTest.java new file mode 100644 index 0000000..483424a --- /dev/null +++ b/opentcs-plantoverview-base/src/test/java/org/opentcs/guing/base/components/properties/type/LengthPropertyTest.java @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.opentcs.guing.base.components.properties.type.LengthProperty.Unit; +import org.opentcs.guing.base.model.AbstractModelComponent; + +/** + * A test for a length property. + */ +class LengthPropertyTest { + + private LengthProperty property; + + @ParameterizedTest + @ValueSource(strings = {"mm", "cm", "m", "km"}) + void testValidUnits(String unit) { + property = new LengthProperty(new DummyComponent()); + assertTrue(property.isPossibleUnit(unit)); + } + + @ParameterizedTest + @MethodSource("paramsFactory") + void testPropertyConversion(Unit unit, Object result) { + property = new LengthProperty(new DummyComponent(), 10, Unit.CM); + property.convertTo(unit); + assertEquals(result, property.getValue()); + assertEquals(unit, property.getUnit()); + } + + @Test + void testPropertyRange() { + property = new LengthProperty(new DummyComponent()); + assertEquals(0, property.getValidRange().getMin(), 0); + assertEquals(Double.MAX_VALUE, property.getValidRange().getMax(), 0); + } + + static Object[][] paramsFactory() { + return new Object[][]{{Unit.MM, 100.0}, + {Unit.CM, 10.0}, + {Unit.M, 0.1}, + {Unit.KM, 0.0001}}; + } + + private class DummyComponent + extends + AbstractModelComponent { + } +} diff --git a/opentcs-plantoverview-base/src/test/java/org/opentcs/guing/base/components/properties/type/PercentPropertyTest.java b/opentcs-plantoverview-base/src/test/java/org/opentcs/guing/base/components/properties/type/PercentPropertyTest.java new file mode 100644 index 0000000..5638103 --- /dev/null +++ b/opentcs-plantoverview-base/src/test/java/org/opentcs/guing/base/components/properties/type/PercentPropertyTest.java @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.opentcs.guing.base.model.AbstractModelComponent; + +/** + * A test for a percent property. + */ +class PercentPropertyTest { + + private PercentProperty property; + + @Test + void testPropertyRange() { + property = new PercentProperty(new DummyComponent()); + assertEquals(0, property.getValidRange().getMin(), 0); + assertEquals(100, property.getValidRange().getMax(), 0); + } + + private class DummyComponent + extends + AbstractModelComponent { + } +} diff --git a/opentcs-plantoverview-base/src/test/java/org/opentcs/guing/base/components/properties/type/SpeedPropertyTest.java b/opentcs-plantoverview-base/src/test/java/org/opentcs/guing/base/components/properties/type/SpeedPropertyTest.java new file mode 100644 index 0000000..e403fdf --- /dev/null +++ b/opentcs-plantoverview-base/src/test/java/org/opentcs/guing/base/components/properties/type/SpeedPropertyTest.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.components.properties.type; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.opentcs.guing.base.components.properties.type.SpeedProperty.Unit; +import org.opentcs.guing.base.model.AbstractModelComponent; + +/** + * A test for a speed property. + */ +class SpeedPropertyTest { + + private SpeedProperty property; + + @ParameterizedTest + @ValueSource(strings = {"mm/s", "m/s", "km/h"}) + void testValidUnits(String unit) { + property = new SpeedProperty(new DummyComponent()); + assertTrue(property.isPossibleUnit(unit)); + } + + @ParameterizedTest + @MethodSource("paramsFactory") + void testPropertyConversion(Unit unit, Object result) { + property = new SpeedProperty(new DummyComponent(), 10000.0, Unit.MM_S); + property.convertTo(unit); + assertEquals(result, property.getValue()); + assertEquals(unit, property.getUnit()); + } + + @Test + void testPropertyRange() { + property = new SpeedProperty(new DummyComponent()); + assertEquals(0, property.getValidRange().getMin(), 0); + assertEquals(Double.MAX_VALUE, property.getValidRange().getMax(), 0); + } + + static Object[][] paramsFactory() { + return new Object[][]{{Unit.MM_S, 10000.0}, + {Unit.M_S, 10.0}, + {Unit.KM_H, 36.0}}; + } + + private class DummyComponent + extends + AbstractModelComponent { + } +} diff --git a/opentcs-plantoverview-base/src/test/java/org/opentcs/guing/base/model/elements/PathModelTest.java b/opentcs-plantoverview-base/src/test/java/org/opentcs/guing/base/model/elements/PathModelTest.java new file mode 100644 index 0000000..4e9d70e --- /dev/null +++ b/opentcs-plantoverview-base/src/test/java/org/opentcs/guing/base/model/elements/PathModelTest.java @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model.elements; + +import static java.util.Map.entry; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.anEmptyMap; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.guing.base.AllocationState; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.components.properties.type.SpeedProperty; + +/** + * Unit tests for {@link PathModel}. + */ +class PathModelTest { + + private PathModel pathModel; + + @BeforeEach + void setUp() { + pathModel = new PathModel(); + } + + @Test + void setLength() { + pathModel.getPropertyLength().setValueAndUnit(4321.0, LengthProperty.Unit.MM); + + assertThat( + pathModel.getPropertyLength().getValueByUnit(LengthProperty.Unit.MM), + is(4321.0) + ); + } + + @Test + void setMaxVelocity() { + pathModel.getPropertyMaxVelocity().setValueAndUnit(900.0, SpeedProperty.Unit.MM_S); + + assertThat( + pathModel.getPropertyMaxVelocity().getValueByUnit(SpeedProperty.Unit.MM_S), + is(900.0) + ); + } + + @Test + void setMaxReverseVelocity() { + pathModel.getPropertyMaxReverseVelocity().setValueAndUnit(900.0, SpeedProperty.Unit.MM_S); + + assertThat( + pathModel.getPropertyMaxReverseVelocity().getValueByUnit(SpeedProperty.Unit.MM_S), + is(900.0) + ); + } + + @Test + void manageVehicleModels() { + assertThat(pathModel.getAllocationStates(), is(anEmptyMap())); + + VehicleModel vehicleModel1 = new VehicleModel(); + vehicleModel1.setName("vehicle-1"); + VehicleModel vehicleModel2 = new VehicleModel(); + vehicleModel2.setName("vehicle-2"); + + pathModel.updateAllocationState(vehicleModel1, AllocationState.ALLOCATED); + pathModel.updateAllocationState(vehicleModel2, AllocationState.CLAIMED); + + assertThat(pathModel.getAllocationStates(), is(aMapWithSize(2))); + Assertions.assertThat(pathModel.getAllocationStates()) + .contains(entry(vehicleModel1, AllocationState.ALLOCATED)) + .contains(entry(vehicleModel2, AllocationState.CLAIMED)); + + pathModel.clearAllocationState(vehicleModel1); + + assertThat(pathModel.getAllocationStates(), is(aMapWithSize(1))); + Assertions.assertThat(pathModel.getAllocationStates()) + .contains(entry(vehicleModel2, AllocationState.CLAIMED)); + } + + @Test + void manageBlockModels() { + assertThat(pathModel.getBlockModels(), is(empty())); + + BlockModel blockModel1 = new BlockModel(); + blockModel1.setName("block-1"); + BlockModel blockModel2 = new BlockModel(); + blockModel2.setName("block-2"); + + pathModel.addBlockModel(blockModel1); + pathModel.addBlockModel(blockModel2); + + assertThat(pathModel.getBlockModels(), hasSize(2)); + assertThat(pathModel.getBlockModels(), containsInAnyOrder(blockModel1, blockModel2)); + + pathModel.removeBlockModel(blockModel1); + + assertThat(pathModel.getBlockModels(), hasSize(1)); + assertThat(pathModel.getBlockModels(), containsInAnyOrder(blockModel2)); + } +} diff --git a/opentcs-plantoverview-base/src/test/java/org/opentcs/guing/base/model/elements/PointModelTest.java b/opentcs-plantoverview-base/src/test/java/org/opentcs/guing/base/model/elements/PointModelTest.java new file mode 100644 index 0000000..77a96fd --- /dev/null +++ b/opentcs-plantoverview-base/src/test/java/org/opentcs/guing/base/model/elements/PointModelTest.java @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.base.model.elements; + +import static java.util.Map.entry; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.anEmptyMap; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.guing.base.AllocationState; +import org.opentcs.guing.base.components.properties.type.LengthProperty; + +/** + * Unit tests for {@link PointModel}. + */ +class PointModelTest { + + private PointModel pointModel; + + @BeforeEach + void setUp() { + pointModel = new PointModel(); + } + + @Test + void setModelPositionX() { + pointModel.getPropertyModelPositionX().setValueAndUnit(1234.0, LengthProperty.Unit.MM); + + assertThat( + pointModel.getPropertyModelPositionX().getValueByUnit(LengthProperty.Unit.MM), + is(1234.0) + ); + } + + @Test + void setModelPositionY() { + pointModel.getPropertyModelPositionY().setValueAndUnit(1234.0, LengthProperty.Unit.MM); + + assertThat( + pointModel.getPropertyModelPositionY().getValueByUnit(LengthProperty.Unit.MM), + is(1234.0) + ); + } + + @Test + void manageVehicleModels() { + assertThat(pointModel.getAllocationStates(), is(anEmptyMap())); + + VehicleModel vehicleModel1 = new VehicleModel(); + vehicleModel1.setName("vehicle-1"); + VehicleModel vehicleModel2 = new VehicleModel(); + vehicleModel2.setName("vehicle-2"); + + pointModel.updateAllocationState(vehicleModel1, AllocationState.ALLOCATED); + pointModel.updateAllocationState(vehicleModel2, AllocationState.CLAIMED); + + assertThat(pointModel.getAllocationStates(), is(aMapWithSize(2))); + Assertions.assertThat(pointModel.getAllocationStates()) + .contains(entry(vehicleModel1, AllocationState.ALLOCATED)) + .contains(entry(vehicleModel2, AllocationState.CLAIMED)); + + pointModel.clearAllocationState(vehicleModel1); + + assertThat(pointModel.getAllocationStates(), is(aMapWithSize(1))); + Assertions.assertThat(pointModel.getAllocationStates()) + .contains(entry(vehicleModel2, AllocationState.CLAIMED)); + } + + @Test + void manageBlockModels() { + assertThat(pointModel.getBlockModels(), is(empty())); + + BlockModel blockModel1 = new BlockModel(); + blockModel1.setName("block-1"); + BlockModel blockModel2 = new BlockModel(); + blockModel2.setName("block-2"); + + pointModel.addBlockModel(blockModel1); + pointModel.addBlockModel(blockModel2); + + assertThat(pointModel.getBlockModels(), hasSize(2)); + assertThat(pointModel.getBlockModels(), containsInAnyOrder(blockModel1, blockModel2)); + + pointModel.removeBlockModel(blockModel1); + + assertThat(pointModel.getBlockModels(), hasSize(1)); + assertThat(pointModel.getBlockModels(), containsInAnyOrder(blockModel2)); + } +} diff --git a/opentcs-plantoverview-common/build.gradle b/opentcs-plantoverview-common/build.gradle new file mode 100644 index 0000000..3a07301 --- /dev/null +++ b/opentcs-plantoverview-common/build.gradle @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply from: "${rootDir}/gradle/java-project.gradle" +apply from: "${rootDir}/gradle/java-codequality.gradle" +apply from: "${rootDir}/gradle/guice-project.gradle" +apply from: "${rootDir}/gradle/publishing-java.gradle" + +dependencies { + api project(':opentcs-api-injection') + api project(':opentcs-common') + api project(':opentcs-plantoverview-base') + + // There does not seem to be an official binary release for JHotDraw... + api group: 'org.opentcs.thirdparty.jhotdraw', name: 'jhotdraw', version: '7.6.20190506' + // This preview version of Docking Frames is not in Maven Central, yet. + api group: 'org.opentcs.thirdparty.dockingframes', name: 'docking-frames-common', version: '1.1.2p11' + api group: 'org.opentcs.thirdparty.dockingframes', name: 'docking-frames-core', version: '1.1.2p11' +} + +task release { + dependsOn build +} + +javadoc { + // For now, suppress a bunch of JavaDoc warnings. + options.addStringOption('Xdoclint:none', '-quiet') +} diff --git a/opentcs-plantoverview-common/gradle.properties b/opentcs-plantoverview-common/gradle.properties new file mode 100644 index 0000000..14431ac --- /dev/null +++ b/opentcs-plantoverview-common/gradle.properties @@ -0,0 +1,40 @@ +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineMethodParams=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAfterDotInChainedMethodCalls=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineDisjunctiveCatchTypes=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineFor=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineImplements=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.sortMembersByVisibility=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.visibilityOrder=PUBLIC;PROTECTED;DEFAULT;PRIVATE +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.enable-indent=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineArrayInit=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineCallArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.keepGettersAndSettersTogether=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classMembersOrder=STATIC FIELD;FIELD;STATIC_INIT;CONSTRUCTOR;INSTANCE_INIT;STATIC METHOD;METHOD;STATIC CLASS;CLASS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapCommentText=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.importGroupsOrder=* +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize=4 +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineAnnotationArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineTryResources=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.preserveNewLinesInComments=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineParenthesized=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineThrows=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=100 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/AbstractViewManager.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/AbstractViewManager.java new file mode 100644 index 0000000..a3af9c7 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/AbstractViewManager.java @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application; + +import static java.util.Objects.requireNonNull; + +import bibliothek.gui.dock.common.DefaultSingleCDockable; +import bibliothek.gui.dock.common.intern.DefaultCommonDockable; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.opentcs.guing.common.components.dockable.DockableTitleComparator; +import org.opentcs.guing.common.components.drawing.DrawingViewScrollPane; +import org.opentcs.util.event.EventSource; + +/** + * Manages the mapping of dockables to drawing views, transport order views and + * order sequence views. + */ +public abstract class AbstractViewManager + implements + ViewManager { + + /** + * Where we register event listeners. + */ + private final EventSource eventSource; + /** + * Map for Dockable -> DrawingView + Rulers. + */ + private final Map drawingViewMap; + + /** + * Creates a new instance. + * + * @param eventSource Where this instance registers event listeners. + */ + public AbstractViewManager(EventSource eventSource) { + this.eventSource = requireNonNull(eventSource, "eventSource"); + drawingViewMap = new TreeMap<>(new DockableTitleComparator()); + } + + @Override + public Map getDrawingViewMap() { + return drawingViewMap; + } + + /** + * Returns the title texts of all drawing views. + * + * @return List of strings containing the names. + */ + @Override + public List getDrawingViewNames() { + return drawingViewMap.keySet().stream() + .map(dock -> dock.getTitleText()) + .sorted() + .collect(Collectors.toList()); + } + + /** + * Forgets the given dockable. + * + * @param dockable The dockable. + */ + @Override + public void removeDockable(DefaultSingleCDockable dockable) { + DrawingViewScrollPane scrollPane = drawingViewMap.remove(dockable); + if (scrollPane != null) { + eventSource.unsubscribe(scrollPane.getDrawingView()); + } + } + + /** + * Resets all components. + */ + public void reset() { + drawingViewMap.clear(); + } + + public int getNextDrawingViewIndex() { + return nextAvailableIndex(drawingViewMap.keySet()); + } + + /** + * Puts a scroll pane with a key dockable into the drawing view map. + * The scroll pane has to contain the drawing view and both rulers. + * + * @param dockable The dockable the scrollPane is wrapped into. Used as the key. + * @param scrollPane The scroll pane containing the drawing view and rulers. + */ + public void addDrawingView( + DefaultSingleCDockable dockable, + DrawingViewScrollPane scrollPane + ) { + requireNonNull(dockable, "dockable"); + requireNonNull(scrollPane, "scrollPane"); + + eventSource.subscribe(scrollPane.getDrawingView()); + drawingViewMap.put(dockable, scrollPane); + } + + /** + * Evaluates which dockable should be the front dockable. + * + * @return The dockable that should be the front dockable. null + * if no dockables exist. + */ + public DefaultCommonDockable evaluateFrontDockable() { + if (!drawingViewMap.isEmpty()) { + return drawingViewMap.keySet().iterator().next().intern(); + } + return null; + } + + /** + * Returns the next available index of a set of dockables. + * E.g. if "Dock 0" and "Dock 2" are being used, 1 would be returned. + * + * @param setToIterate The set to iterate. + * @return The next available index. + */ + protected int nextAvailableIndex(Set setToIterate) { + // Name + Pattern p = Pattern.compile("\\d"); + Matcher m; + int biggestIndex = 0; + + for (DefaultSingleCDockable dock : setToIterate) { + m = p.matcher(dock.getTitleText()); + + if (m.find()) { + int index = Integer.parseInt(m.group(0)); + + if (index > biggestIndex) { + biggestIndex = index; + } + } + } + + return biggestIndex + 1; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/ApplicationState.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/ApplicationState.java new file mode 100644 index 0000000..8867eed --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/ApplicationState.java @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; + +/** + * Keeps and provides information about the current state of the application as + * a whole. + */ +public class ApplicationState { + + /** + * The application's current mode of operation. + */ + private OperationMode operationMode = OperationMode.UNDEFINED; + + /** + * Creates a new instance. + */ + @Inject + public ApplicationState() { + } + + /** + * Returns the application's current mode of operation. + * + * @return The application's current mode of operation. + */ + public OperationMode getOperationMode() { + return operationMode; + } + + /** + * Checks whether the application is currently in the given mode of operation. + * + * @param mode The mode to check for. + * @return true if, and only if, the application is currently in + * the given mode. + */ + public boolean hasOperationMode(OperationMode mode) { + return operationMode == mode; + } + + /** + * Sets the application's current mode of operation. + * + * @param operationMode The application's new mode of operation. + */ + public void setOperationMode(OperationMode operationMode) { + this.operationMode = requireNonNull(operationMode, "operationMode"); + } + +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/ComponentsManager.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/ComponentsManager.java new file mode 100644 index 0000000..98295fe --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/ComponentsManager.java @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application; + +import java.util.List; +import org.opentcs.guing.common.components.tree.elements.UserObject; + +/** + */ +public interface ComponentsManager { + + /** + * Adds the given model components to the data model. (e.g. when pasting) + * + * @param userObjects The user objects to restore. + * @return The restored user objects. + */ + List restoreModelComponents(List userObjects); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/GuiManager.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/GuiManager.java new file mode 100644 index 0000000..249ee77 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/GuiManager.java @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application; + +import org.opentcs.components.plantoverview.PlantModelExporter; +import org.opentcs.components.plantoverview.PlantModelImporter; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * Provides some central services for various parts of the plant overview application. + */ +public interface GuiManager { + + /** + * Called when an object was selected in the tree view. + * + * @param modelComponent The selected object. + */ + void selectModelComponent(ModelComponent modelComponent); + + /** + * Called when an additional object was selected in the tree view. + * + * @param modelComponent The selected object. + */ + void addSelectedModelComponent(ModelComponent modelComponent); + + /** + * Called when an object was removed from the tree view (by user interaction). + * + * @param fDataObject The object to be removed. + * @return Indicates whether the object was really removed from the model. + */ + boolean treeComponentRemoved(ModelComponent fDataObject); + + /** + * Notifies about a figure object being selected. + * + * @param modelComponent The selected object. + */ + void figureSelected(ModelComponent modelComponent); + + /** + * Creates a new, empty model and initializes it. + */ + void createEmptyModel(); + + /** + * Loads a plant model. + */ + void loadModel(); + + /** + * Imports a plant model using the given importer. + * + * @param importer The importer. + */ + void importModel(PlantModelImporter importer); + + /** + * @return + */ + boolean saveModel(); + + /** + * + * @return + */ + boolean saveModelAs(); + + /** + * Exports a plant model using the given exporter. + * + * @param exporter The exporter. + */ + void exportModel(PlantModelExporter exporter); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/GuiManagerModeling.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/GuiManagerModeling.java new file mode 100644 index 0000000..c0dda98 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/GuiManagerModeling.java @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application; + +import jakarta.annotation.Nonnull; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.base.model.elements.VehicleModel; + +/** + * Provides services concerning model editing. + */ +public interface GuiManagerModeling + extends + GuiManager { + + /** + * Creates a new vehicle model. + * + * @return The created vehicle model. + */ + VehicleModel createVehicleModel(); + + /** + * Creates a new location type model. + * + * @return The created location type model. + */ + LocationTypeModel createLocationTypeModel(); + + /** + * Creates a new block model. + * + * @return The created block model. + */ + BlockModel createBlockModel(); + + /** + * Removes a block model. + * + *

    + * This method is primarily provided for use in plugin panels. + *

    + * + * @param blockModel The block model to be removed. + */ + void removeBlockModel( + @Nonnull + BlockModel blockModel + ); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/KernelStateHandler.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/KernelStateHandler.java new file mode 100644 index 0000000..148472b --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/KernelStateHandler.java @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application; + +import org.opentcs.access.Kernel; + +/** + * Listener interface implemented by classes interested in changes of a + * connected kernel's state. + */ +public interface KernelStateHandler { + + /** + * Informs the handler that the kernel is now in the given state. + * + * @param state The new state. + */ + void enterKernelState(Kernel.State state); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/ModelRestorationProgressStatus.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/ModelRestorationProgressStatus.java new file mode 100644 index 0000000..b4a4f70 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/ModelRestorationProgressStatus.java @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application; + +import java.util.ResourceBundle; +import org.opentcs.guing.common.util.I18nPlantOverview; + +/** + * Progress status for the process of loading a model. + */ +public enum ModelRestorationProgressStatus + implements + ProgressStatus { + + /** + * Cleanup phase. + */ + CLEANUP(0, "modelRestorationProgressStatus.description.cleanup"), + /** + * Starting to load model. + */ + START_LOADING_MODEL(10, "modelRestorationProgressStatus.description.startLoadingModel"), + /** + * Loading points. + */ + LOADING_POINTS(20, "modelRestorationProgressStatus.description.startLoadingPoints"), + /** + * Loading paths. + */ + LOADING_PATHS(30, "modelRestorationProgressStatus.description.startLoadingPaths"), + /** + * Loading locations. + */ + LOADING_LOCATIONS(40, "modelRestorationProgressStatus.description.startLoadingLocations"), + /** + * Loading vehicles. + */ + LOADING_VEHICLES(50, "modelRestorationProgressStatus.description.startLoadingVehicles"), + /** + * Loading blocks. + */ + LOADING_BLOCKS(60, "modelRestorationProgressStatus.description.startLoadingBlocks"), + /** + * Setting up model view. + */ + SET_UP_MODEL_VIEW(70, "modelRestorationProgressStatus.description.setUpModelView"), + /** + * Setting up directory tree. + */ + SET_UP_DIRECTORY_TREE(80, "modelRestorationProgressStatus.description.setUpDirectoryTree"), + /** + * Setting up working area. + */ + SET_UP_WORKING_AREA(90, "modelRestorationProgressStatus.description.setUpWorkingArea"); + + private final int percentage; + + private final String description; + + ModelRestorationProgressStatus(int percentage, String description) { + this.percentage = percentage; + this.description = ResourceBundle.getBundle(I18nPlantOverview.MISC_PATH).getString(description); + } + + @Override + public int getPercentage() { + return percentage; + } + + @Override + public String getStatusDescription() { + return description; + } + +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/OperationMode.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/OperationMode.java new file mode 100644 index 0000000..7a7eb2e --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/OperationMode.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application; + +import java.util.Objects; +import org.opentcs.access.Kernel; + +/** + * Defines the plant overview's potential modes of operation. + */ +public enum OperationMode { + + /** + * For cases in which the mode of operation has not been defined, yet. + */ + UNDEFINED, + /** + * Used when modelling a driving course. + */ + MODELLING, + /** + * Used when operating a plant/system. + */ + OPERATING; + + /** + * Returns the equivalent operation mode to the given kernel state. + * + * @param state The kernel state. + * @return The equivalent operation mode to the given kernel state. + */ + public static OperationMode equivalent(Kernel.State state) { + if (Objects.equals(state, Kernel.State.MODELLING)) { + return MODELLING; + } + else if (Objects.equals(state, Kernel.State.OPERATING)) { + return OPERATING; + } + else { + return UNDEFINED; + } + } + + public static Kernel.State equivalent(OperationMode mode) { + if (Objects.equals(mode, MODELLING)) { + return Kernel.State.MODELLING; + } + else if (Objects.equals(mode, OPERATING)) { + return Kernel.State.OPERATING; + } + else { + return null; + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/PluginPanelManager.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/PluginPanelManager.java new file mode 100644 index 0000000..7f9c3bd --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/PluginPanelManager.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application; + +import org.opentcs.components.plantoverview.PluggablePanelFactory; + +/** + */ +public interface PluginPanelManager { + + /** + * Shows or hides the specific {@code PanelFactory}. + * + * @param factory The factory resp. panel that shall be shown / hidden. + * @param visible True to set it visible, false otherwise. + */ + void showPluginPanel(PluggablePanelFactory factory, boolean visible); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/ProgressIndicator.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/ProgressIndicator.java new file mode 100644 index 0000000..dd6e5c9 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/ProgressIndicator.java @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application; + +/** + * Makes progress information available in some way. + */ +public interface ProgressIndicator { + + /** + * Initializes the progress indicator, possibly resetting the percentage and + * message. + */ + void initialize(); + + /** + * Sets/publishes the current progress status. + * + * @param progressStatus The progress status. + */ + void setProgress(ProgressStatus progressStatus); + + /** + * Terminates the progress indicator, indicating that no further progress is + * going to be published. + * The progress indicator may be reused after a call to {@code initialize()}. + */ + void terminate(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/ProgressStatus.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/ProgressStatus.java new file mode 100644 index 0000000..835499e --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/ProgressStatus.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application; + +/** + * A state of progress, to be used with {@link ProgressIndicator}. + */ +public interface ProgressStatus { + + /** + * Returns a percentage value. + * + * @return A percentage value, greater than or equal to 0 and less than or equal to 100. + */ + int getPercentage(); + + /** + * Returns a (possibly localized) description of the current status to be displayed. + * + * @return A description of the current status to be displayed. + */ + String getStatusDescription(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/SplashFrame.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/SplashFrame.form new file mode 100644 index 0000000..ebf27ba --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/SplashFrame.form @@ -0,0 +1,100 @@ + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/SplashFrame.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/SplashFrame.java new file mode 100644 index 0000000..df21e3c --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/SplashFrame.java @@ -0,0 +1,178 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application; + +import static java.util.Objects.requireNonNull; + +import java.lang.reflect.InvocationTargetException; +import javax.swing.JFrame; +import javax.swing.SwingUtilities; +import org.opentcs.util.gui.Icons; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A frame for displaying the progress of longer-running processes. + */ +public class SplashFrame + extends + JFrame + implements + ProgressIndicator { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(SplashFrame.class); + + /** + * Creates new form SplashFrame + */ + @SuppressWarnings("this-escape") + public SplashFrame() { + initComponents(); + } + + @Override + public void initialize() { + // Ensure this method is called on the event dispatcher thread. + if (!SwingUtilities.isEventDispatchThread()) { + try { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + initialize(); + } + }); + } + catch (InterruptedException | InvocationTargetException exc) { + LOG.warn("Unexpected exception", exc); + } + return; + } + setVisible(true); + setProgress(0, ""); + } + + @Override + public void setProgress(ProgressStatus progressStatus) { + requireNonNull(progressStatus, "progressStatus"); + + setProgress(progressStatus.getPercentage(), progressStatus.getStatusDescription()); + } + + @Override + public void terminate() { + // Ensure this method is called on the event dispatcher thread. + if (!SwingUtilities.isEventDispatchThread()) { + try { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + terminate(); + } + }); + } + catch (InterruptedException | InvocationTargetException exc) { + LOG.warn("Unexpected exception", exc); + } + return; + } + dispose(); + } + + private void setProgress(final int percent, final String message) { + // Ensure this method is called on the event dispatcher thread. + if (!SwingUtilities.isEventDispatchThread()) { + try { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + setProgress(percent, message); + } + }); + } + catch (InterruptedException | InvocationTargetException exc) { + LOG.warn("Unexpected exception", exc); + } + return; + } + labelMessage.setText(message); + progressBar.setValue(percent); + update(getGraphics()); + toFront(); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + panel = new javax.swing.JPanel(); + labelImage = new javax.swing.JLabel(); + labelMessage = new javax.swing.JLabel(); + progressBar = new javax.swing.JProgressBar(); + + setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/system"); // NOI18N + setTitle(bundle.getString("splashFrame.title.text")); // NOI18N + setBackground(new java.awt.Color(255, 255, 255)); + setIconImages(Icons.getOpenTCSIcons()); + setUndecorated(true); + getContentPane().setLayout(new java.awt.GridBagLayout()); + + panel.setBackground(new java.awt.Color(255, 255, 255)); + panel.setLayout(new java.awt.GridBagLayout()); + + labelImage.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/opentcs/guing/res/symbols/openTCS/splash.320x152.gif"))); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.anchor = java.awt.GridBagConstraints.FIRST_LINE_START; + panel.add(labelImage, gridBagConstraints); + + labelMessage.setBackground(new java.awt.Color(255, 255, 255)); + labelMessage.setFont(new java.awt.Font("Arial", 1, 12)); // NOI18N + labelMessage.setText(bundle.getString("splashFrame.label_message.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.SOUTH; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.weighty = 0.5; + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + panel.add(labelMessage, gridBagConstraints); + + progressBar.setBackground(new java.awt.Color(255, 255, 255)); + progressBar.setForeground(new java.awt.Color(153, 153, 255)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.SOUTH; + gridBagConstraints.weighty = 0.5; + panel.add(progressBar, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + getContentPane().add(panel, gridBagConstraints); + + setSize(new java.awt.Dimension(316, 186)); + setLocationRelativeTo(null); + }// //GEN-END:initComponents + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel labelImage; + private javax.swing.JLabel labelMessage; + private javax.swing.JPanel panel; + private javax.swing.JProgressBar progressBar; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/StartupProgressStatus.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/StartupProgressStatus.java new file mode 100644 index 0000000..b6872c5 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/StartupProgressStatus.java @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application; + +import java.util.ResourceBundle; +import org.opentcs.guing.common.util.I18nPlantOverview; + +/** + * Progress status for the process of starting the application. + */ +public enum StartupProgressStatus + implements + ProgressStatus { + + /** + * Starting the plant overview. + */ + START_PLANT_OVERVIEW(0, "startupProgressStatus.description.startPlantOverview"), + /** + * Showing the plant overview. + */ + SHOW_PLANT_OVERVIEW(5, "startupProgressStatus.description.showPlantOverview"), + /** + * Application initialized. + */ + INITIALIZED(10, "startupProgressStatus.description.initialized"), + /** + * Initializing model. + */ + INITIALIZE_MODEL(15, "startupProgressStatus.description.initializeModel"); + + private final int percentage; + + private final String description; + + StartupProgressStatus(int percentage, String description) { + this.percentage = percentage; + this.description = ResourceBundle.getBundle(I18nPlantOverview.MISC_PATH).getString(description); + } + + @Override + public int getPercentage() { + return percentage; + } + + @Override + public String getStatusDescription() { + return description; + } + +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/StatusPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/StatusPanel.java new file mode 100644 index 0000000..d896bef --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/StatusPanel.java @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application; + +import static java.util.Objects.requireNonNull; + +import java.awt.Color; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.util.logging.Level; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A panel at the bottom of the view, showing the mouse position and status. + */ +public class StatusPanel + extends + JPanel { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(StatusPanel.class); + /** + * A text field for status messages. + */ + private final JTextField textFieldStatus = new JTextField(); + /** + * A text field for cursor positions/coordinates. + */ + private final JTextField textFieldPosition = new JTextField(); + + /** + * Creates a new instance. + */ + @SuppressWarnings("this-escape") + public StatusPanel() { + initComponents(); + } + + private void initComponents() { + textFieldStatus.setText(null); + textFieldPosition.setText(null); + + removeAll(); + setLayout(new GridBagLayout()); + + textFieldPosition.setEditable(false); + GridBagConstraints gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = GridBagConstraints.BOTH; + add(textFieldPosition, gridBagConstraints); + + textFieldStatus.setEditable(false); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 0.8; + add(textFieldStatus, gridBagConstraints); + } + + /** + * Clears the status textfield, removing any logged messages. + */ + public void clear() { + textFieldStatus.setForeground(Color.black); + textFieldStatus.setText(""); + } + + /** + * Text display in the status bar (at the bottom). + * + * @param level Log-Level, determines the text color. + * @param text Text to display. + */ + public void setLogMessage(Level level, String text) { + if (level == Level.SEVERE) { + showOptionPane(text); + textFieldStatus.setForeground(Color.magenta); + LOG.error(text); + } + else if (level == Level.WARNING) { + showOptionPane(text); + textFieldStatus.setForeground(Color.red); + LOG.warn(text); + } + else if (level == Level.INFO) { + textFieldStatus.setForeground(Color.blue); + LOG.info(text); + } + else { + textFieldStatus.setForeground(Color.black); + LOG.info(text); + } + + textFieldStatus.setText(text); + } + + /** + * Sets the given text to the position text field. + * + * @param text The text to set. + */ + public void setPositionText(String text) { + requireNonNull(text, "text"); + + // Add a space in front of the position text to avoid that a part of the lefthand side gets cut + // off with some graphical environments (observed on Windows 10). + textFieldPosition.setText(" " + text); + revalidate(); + } + + private void showOptionPane(String text) { + JOptionPane.showMessageDialog(this.getParent(), text, "", JOptionPane.ERROR_MESSAGE); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/ViewManager.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/ViewManager.java new file mode 100644 index 0000000..d7bec0e --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/ViewManager.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application; + +import bibliothek.gui.dock.common.DefaultSingleCDockable; +import java.util.List; +import java.util.Map; +import org.opentcs.guing.common.components.drawing.DrawingViewScrollPane; + +/** + * Manages the mapping of dockables to drawing views, transport order views and + * order sequence views. + */ +public interface ViewManager { + + Map getDrawingViewMap(); + + /** + * Returns the title texts of all drawing views. + * + * @return List of strings containing the names. + */ + List getDrawingViewNames(); + + /** + * Forgets the given dockable. + * + * @param dockable The dockable. + */ + void removeDockable(DefaultSingleCDockable dockable); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/action/ToolButtonListener.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/action/ToolButtonListener.java new file mode 100644 index 0000000..a943fb2 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/action/ToolButtonListener.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application.action; + +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.util.Objects; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.tool.Tool; + +/** + * A listener if a tool was (de)selected. + */ +public final class ToolButtonListener + implements + ItemListener { + + private final Tool tool; + private final DrawingEditor editor; + + /** + * Creates a new instance. + * + * @param tool The tool + * @param editor The drawing editor + */ + public ToolButtonListener(Tool tool, DrawingEditor editor) { + this.tool = Objects.requireNonNull(tool, "tool is null"); + this.editor = Objects.requireNonNull(editor, "editor is null"); + } + + @Override + public void itemStateChanged(ItemEvent evt) { + if (evt.getStateChange() == ItemEvent.SELECTED) { + editor.setTool(tool); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/action/file/ModelPropertiesAction.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/action/file/ModelPropertiesAction.java new file mode 100644 index 0000000..f51bd5b --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/action/file/ModelPropertiesAction.java @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application.action.file; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.guing.common.util.I18nPlantOverview.MENU_PATH; + +import jakarta.inject.Inject; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.util.Objects; +import javax.swing.AbstractAction; +import javax.swing.JOptionPane; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.data.ObjectPropConstants; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Shows a message window with some of the currently loaded model's properties. + */ +public class ModelPropertiesAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "file.modelProperties"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + /** + * The parent component for dialogs shown by this action. + */ + private final Component dialogParent; + /** + * Provides the current system model. + */ + private final ModelManager modelManager; + + @Inject + @SuppressWarnings("this-escape") + public ModelPropertiesAction( + @ApplicationFrame + Component dialogParent, + ModelManager modelManager + ) { + this.dialogParent = requireNonNull(dialogParent, "dialogParent"); + this.modelManager = requireNonNull(modelManager, "modelManager"); + + putValue(NAME, BUNDLE.getString("modelPropertiesAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("modelPropertiesAction.shortDescription")); + } + + @Override + public void actionPerformed(ActionEvent e) { + ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverview.MODELPROPERTIES_PATH); + + JOptionPane.showMessageDialog( + dialogParent, + "

    " + modelManager.getModel().getName() + "
    " + + bundle.getString("modelPropertiesAction.optionPane_properties.message.numberOfPoints") + + numberOfPoints() + + "
    " + + bundle.getString("modelPropertiesAction.optionPane_properties.message.numberOfPaths") + + numberOfPaths() + + "
    " + + bundle.getString( + "modelPropertiesAction.optionPane_properties.message.numberOfLocations" + ) + + numberOfLocations() + + "
    " + + bundle.getString( + "modelPropertiesAction.optionPane_properties.message.numberOfLocationTypes" + ) + + numberOfLocationTypes() + + "
    " + + bundle.getString("modelPropertiesAction.optionPane_properties.message.numberOfBlocks") + + numberOfBlocks() + + "
    " + + bundle.getString( + "modelPropertiesAction.optionPane_properties.message.numberOfVehicles" + ) + + numberOfVehicles() + + "
    " + + "
    " + + bundle.getString("modelPropertiesAction.optionPane_properties.message.lastModified") + + lastModified() + + "

    " + ); + + } + + private String lastModified() { + return modelManager.getModel().getPropertyMiscellaneous().getItems().stream() + .filter(kvp -> Objects.equals(kvp.getKey(), ObjectPropConstants.MODEL_FILE_LAST_MODIFIED)) + .findAny() + .map(kvp -> kvp.getValue()) + .orElse("?"); + } + + private int numberOfPoints() { + return modelManager.getModel().getPointModels().size(); + } + + private int numberOfPaths() { + return modelManager.getModel().getPathModels().size(); + } + + private int numberOfLocations() { + return modelManager.getModel().getLocationModels().size(); + } + + private int numberOfLocationTypes() { + return modelManager.getModel().getLocationTypeModels().size(); + } + + private int numberOfBlocks() { + return modelManager.getModel().getBlockModels().size(); + } + + private int numberOfVehicles() { + return modelManager.getModel().getVehicleModels().size(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/action/file/SaveModelAction.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/action/file/SaveModelAction.java new file mode 100644 index 0000000..b8f1fca --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/action/file/SaveModelAction.java @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application.action.file; + +import static org.opentcs.guing.common.util.I18nPlantOverview.MENU_PATH; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import javax.swing.ImageIcon; +import javax.swing.KeyStroke; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + */ +public class SaveModelAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "file.saveModel"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + + private final GuiManager view; + + /** + * Creates a new instance. + * + * @param view The gui manager + */ + @SuppressWarnings("this-escape") + public SaveModelAction(GuiManager view) { + this.view = view; + + putValue(NAME, BUNDLE.getString("saveModelAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("saveModelAction.shortDescription")); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke("ctrl S")); + putValue(MNEMONIC_KEY, Integer.valueOf('S')); + + ImageIcon icon = ImageDirectory.getImageIcon("/menu/document-save.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + } + + @Override + public void actionPerformed(ActionEvent evt) { + view.saveModel(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/action/file/SaveModelAsAction.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/action/file/SaveModelAsAction.java new file mode 100644 index 0000000..c6488e8 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/action/file/SaveModelAsAction.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application.action.file; + +import static org.opentcs.guing.common.util.I18nPlantOverview.MENU_PATH; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import javax.swing.ImageIcon; +import javax.swing.KeyStroke; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + */ +public class SaveModelAsAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "file.saveModelAs"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + /** + * The manager this instance is working with. + */ + private final GuiManager guiManager; + + /** + * Creates a new instance. + * + * @param manager The gui manager + */ + @SuppressWarnings("this-escape") + public SaveModelAsAction(final GuiManager manager) { + this.guiManager = manager; + + putValue(NAME, BUNDLE.getString("saveModelAsAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("saveModelAsAction.shortDescription")); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke("shift ctrl S")); + putValue(MNEMONIC_KEY, Integer.valueOf('A')); + + ImageIcon icon = ImageDirectory.getImageIcon("/menu/document-save-as.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + } + + @Override + public void actionPerformed(ActionEvent evt) { + guiManager.saveModelAs(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/action/view/AddPluginPanelAction.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/action/view/AddPluginPanelAction.java new file mode 100644 index 0000000..96b0de7 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/action/view/AddPluginPanelAction.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application.action.view; + +import static java.util.Objects.requireNonNull; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import javax.swing.JCheckBoxMenuItem; +import org.opentcs.components.plantoverview.PluggablePanelFactory; +import org.opentcs.guing.common.application.PluginPanelManager; + +/** + * An action to add a plugin panel. + */ +public class AddPluginPanelAction + extends + AbstractAction { + + /** + * This action's ID. + */ + public static final String ID = "view.addPluginPanel"; + private final PluggablePanelFactory factory; + private final PluginPanelManager pluginPanelManager; + + /** + * Creates a new instance. + * + * @param pluginPanelManager The openTCS view + * @param factory The pluggable panel factory + */ + public AddPluginPanelAction( + PluginPanelManager pluginPanelManager, + PluggablePanelFactory factory + ) { + this.pluginPanelManager = requireNonNull(pluginPanelManager, "pluginPanelManager"); + this.factory = requireNonNull(factory, "factory"); + } + + @Override + public void actionPerformed(ActionEvent e) { + JCheckBoxMenuItem item = (JCheckBoxMenuItem) e.getSource(); + pluginPanelManager.showPluginPanel(factory, item.isSelected()); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/menus/menubar/PluginPanelPropertyHandler.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/menus/menubar/PluginPanelPropertyHandler.java new file mode 100644 index 0000000..fecf123 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/menus/menubar/PluginPanelPropertyHandler.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application.menus.menubar; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Objects; +import javax.swing.JCheckBoxMenuItem; +import org.opentcs.guing.common.components.dockable.AbstractDockingManager; + +/** + * Handles changes of a plugin panel's properties. + */ +class PluginPanelPropertyHandler + implements + PropertyChangeListener { + + /** + * The menu item corresponding to the plugin panel. + */ + private final JCheckBoxMenuItem utilMenuItem; + + /** + * Creates a new instance. + * + * @param utilMenuItem The menu item corresponding to the plugin panel. + */ + PluginPanelPropertyHandler(JCheckBoxMenuItem utilMenuItem) { + this.utilMenuItem = Objects.requireNonNull(utilMenuItem, "utilMenuItem is null"); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getPropertyName().equals(AbstractDockingManager.DOCKABLE_CLOSED)) { + utilMenuItem.setSelected(false); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/menus/menubar/ViewPluginPanelsMenu.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/menus/menubar/ViewPluginPanelsMenu.java new file mode 100644 index 0000000..0c7bbc5 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/menus/menubar/ViewPluginPanelsMenu.java @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application.menus.menubar; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.SortedSet; +import java.util.TreeSet; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import org.opentcs.access.Kernel; +import org.opentcs.components.plantoverview.PluggablePanelFactory; +import org.opentcs.guing.common.application.OperationMode; +import org.opentcs.guing.common.application.PluginPanelManager; +import org.opentcs.guing.common.application.action.view.AddPluginPanelAction; +import org.opentcs.guing.common.components.dockable.DockingManager; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.guing.common.util.PanelRegistry; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + */ +public class ViewPluginPanelsMenu + extends + JMenu { + + private static final ResourceBundleUtil BUNDLE + = ResourceBundleUtil.getBundle(I18nPlantOverview.MENU_PATH); + + /** + * The plugin panel manager. + */ + private final PluginPanelManager pluginPanelManager; + /** + * Provides the registered plugin panel factories. + */ + private final PanelRegistry panelRegistry; + /** + * Manages docking frames. + */ + private final DockingManager dockingManager; + + @Inject + public ViewPluginPanelsMenu( + PluginPanelManager pluginPanelManager, + PanelRegistry panelRegistry, + DockingManager dockingManager + ) { + super(BUNDLE.getString("viewPluginPanelsMenu.text")); + + this.pluginPanelManager = requireNonNull(pluginPanelManager, "pluginPanelManager"); + this.panelRegistry = requireNonNull(panelRegistry, "panelRegistry"); + this.dockingManager = requireNonNull(dockingManager, "dockingManager"); + } + + public void setOperationMode(OperationMode mode) { + requireNonNull(mode, "mode"); + + evaluatePluginPanels(mode); + } + + /** + * Removes/adds plugin panels depending on the OperationMode. + * + * @param operationMode The operation mode. + */ + private void evaluatePluginPanels(OperationMode operationMode) { + Kernel.State kernelState = OperationMode.equivalent(operationMode); + if (kernelState == null) { + return; + } + + removeAll(); + + SortedSet factories = new TreeSet<>((factory1, factory2) -> { + return factory1.getPanelDescription().compareTo(factory2.getPanelDescription()); + }); + factories.addAll(panelRegistry.getFactories()); + + for (final PluggablePanelFactory factory : factories) { + if (factory.providesPanel(kernelState)) { + String title = factory.getPanelDescription(); + final JCheckBoxMenuItem utilMenuItem = new JCheckBoxMenuItem(); + utilMenuItem.setAction(new AddPluginPanelAction(pluginPanelManager, factory)); + utilMenuItem.setText(title); + dockingManager.addPropertyChangeListener(new PluginPanelPropertyHandler(utilMenuItem)); + add(utilMenuItem); + } + } + // If the menu is empty, add a single disabled menu item to it that explains + // to the user that no plugin panels are available. + if (getMenuComponentCount() == 0) { + JMenuItem dummyItem + = new JMenuItem(BUNDLE.getString("viewPluginPanelsMenu.menuItem_none.text")); + dummyItem.setEnabled(false); + add(dummyItem); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/toolbar/DragTool.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/toolbar/DragTool.java new file mode 100644 index 0000000..6f379c8 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/application/toolbar/DragTool.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.application.toolbar; + +import java.awt.event.MouseEvent; +import org.jhotdraw.draw.tool.AbstractTool; + +/** + * The tool to drag the drawing. + */ +public class DragTool + extends + AbstractTool { + + public DragTool() { + super(); + } + + @Override + public void mouseDragged(MouseEvent e) { + // Do nada. + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/EditableComponent.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/EditableComponent.java new file mode 100644 index 0000000..29cdfd2 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/EditableComponent.java @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components; + +/** + */ +public interface EditableComponent + extends + org.jhotdraw.gui.EditableComponent { + + /** + * Delete the components that are currently selected in the tree and save + * them to allow restoring by a Paste operation. + */ + void cutSelectedItems(); + + /** + * Save the components that are currently selected in the tree + * to allow creating a clone by a Paste operation. + */ + void copySelectedItems(); + + /** + * + */ + void pasteBufferedItems(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/CancelButton.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/CancelButton.java new file mode 100644 index 0000000..9524aa1 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/CancelButton.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.dialogs; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import javax.swing.JButton; +import javax.swing.KeyStroke; + +/** + * Cancel Button which closes a dialog by pressing ESC. + */ +public class CancelButton + extends + JButton { + + /** + * Creates a new instance. + */ + public CancelButton() { + this(null); + } + + /** + * Creates a new instance. + * + * @param text Label of this button. + */ + @SuppressWarnings("this-escape") + public CancelButton(String text) { + super(text); + + ActionListener al = new ActionListener() { + + @Override + public void actionPerformed(ActionEvent event) { + String cmd = event.getActionCommand(); + + if (cmd.equals("PressedESCAPE")) { + doClick(); + } + } + }; + + registerKeyboardAction( + al, "PressedESCAPE", + KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), + JButton.WHEN_IN_FOCUSED_WINDOW + ); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/ClosableDialog.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/ClosableDialog.form new file mode 100644 index 0000000..b19b330 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/ClosableDialog.form @@ -0,0 +1,55 @@ + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/ClosableDialog.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/ClosableDialog.java new file mode 100644 index 0000000..f589ffb --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/ClosableDialog.java @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.dialogs; + +import java.awt.Component; +import java.awt.Insets; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JOptionPane; +import javax.swing.border.EmptyBorder; + +/** + * A dialog that has a close button. + */ +public class ClosableDialog + extends + JDialog { + + /** + * Creates new instance. + * + * @param parent The dialog's parent. + * @param modal Whether the dialog is modal or not. + * @param content The dialog's actual content. + * @param title The dialog's title. + */ + @SuppressWarnings("this-escape") + public ClosableDialog(Component parent, boolean modal, JComponent content, String title) { + super(JOptionPane.getFrameForComponent(parent), title, modal); + initComponents(); + getContentPane().add(content, java.awt.BorderLayout.CENTER); + content.setBorder(new EmptyBorder(new Insets(4, 4, 4, 4))); + getRootPane().setDefaultButton(buttonClose); + pack(); + } + + /** + * Closes the dialog. + */ + private void doClose() { + setVisible(false); + dispose(); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + + panelButton = new javax.swing.JPanel(); + buttonClose = new CancelButton(); + + addWindowListener(new java.awt.event.WindowAdapter() { + public void windowClosing(java.awt.event.WindowEvent evt) { + closeDialog(evt); + } + }); + + buttonClose.setFont(buttonClose.getFont().deriveFont(buttonClose.getFont().getStyle() | java.awt.Font.BOLD)); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/system"); // NOI18N + buttonClose.setText(bundle.getString("closableDialog.button_close.text")); // NOI18N + buttonClose.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + buttonCloseActionPerformed(evt); + } + }); + panelButton.add(buttonClose); + + getContentPane().add(panelButton, java.awt.BorderLayout.SOUTH); + + pack(); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void buttonCloseActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_buttonCloseActionPerformed + doClose(); + }//GEN-LAST:event_buttonCloseActionPerformed + + /** + * Closes the dialog + */ + private void closeDialog(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_closeDialog + doClose(); + }//GEN-LAST:event_closeDialog + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton buttonClose; + private javax.swing.JPanel panelButton; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/DetailsDialog.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/DetailsDialog.java new file mode 100644 index 0000000..81c0337 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/DetailsDialog.java @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.dialogs; + +/** + * Defines a dialog that allows for easy editing of a property. + * The actual editing of the properties is implemented in a {@link DetailsDialogContent}. + */ +public interface DetailsDialog { + + /** + * Returns the {@link DetailsDialogContent} that is used to edit the property. + * + * @return + */ + DetailsDialogContent getDialogContent(); + + /** + * Activates the dialog. + */ + void activate(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/DetailsDialogContent.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/DetailsDialogContent.java new file mode 100644 index 0000000..188055a --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/DetailsDialogContent.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.dialogs; + +import org.opentcs.guing.base.components.properties.type.Property; + +/** + * Interface for components to edit properties. + * Classes that implement this interface are generally embedded in a dialog. + * The dialog then calls these methods. + */ +public interface DetailsDialogContent { + + /** + * Writes the values of the dialog back to the attribute object. + * This should happen when the user clicked "OK". + */ + void updateValues(); + + /** + * Returns the title of the dialog. + * + * @return The title. + */ + String getTitle(); + + /** + * Sets the property. + * + * @param property The property. + */ + void setProperty(Property property); + + /** + * Returns the property. + * + * @return The property. + */ + Property getProperty(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/DialogContent.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/DialogContent.java new file mode 100644 index 0000000..ef07b7e --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/DialogContent.java @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.dialogs; + +import javax.swing.JComponent; +import javax.swing.JPanel; + +/** + * Base implementation for a dialog and tab content. + */ +public abstract class DialogContent + extends + JPanel { + + /** + * Title of the component in a dialog. + */ + protected String fDialogTitle; + /** + * Title of the component in a tab. + */ + protected String fTabTitle; + /** + * Indicates that the update of the value has failed. + */ + protected boolean updateFailed; + /** + * Whether or not this dialog is modal. + */ + protected boolean fModal; + /** + * Parent dialog where this content is added to. + */ + protected StandardContentDialog fDialog; + + /** + * Creates a new instance of AbstractDialogContent. + */ + public DialogContent() { + setModal(true); + } + + /** + * Returns the component. + * + * @return The component of this dialog. + */ + public JComponent getComponent() { + return this; + } + + /** + * Sets the dialog to be modal. + * + * @param modal true if the dialog should be modal. + */ + public final void setModal(boolean modal) { + fModal = modal; + } + + /** + * Returns whether or not the component is modal. + * + * @return Whether or not the component is modal. + */ + public boolean getModal() { + return fModal; + } + + /** + * Returns the title for a tab. + * + * @return The title for a tab. + */ + public String getTabTitle() { + return fTabTitle; + } + + /** + * Returns the title for a dialog. + * + * @return The title for a dialog. + */ + public String getDialogTitle() { + return fDialogTitle; + } + + /** + * Set the title for a dialog. + * + * @param title The new title for a dialog. + */ + protected void setDialogTitle(String title) { + fDialogTitle = title; + } + + /** + * Set the title for a tab. + * + * @param title The new title for a tab. + */ + protected void setTabTitle(String title) { + fTabTitle = title; + } + + /** + * Notifies the registered listeners that the dialog would like to close. + */ + protected void notifyRequestClose() { + fDialog.requestClose(); + } + + /** + * Returns whether or not the update of the UI elements failed. + * + * @return true if the update failed. + */ + public boolean updateFailed() { + return updateFailed; + } + + /** + * Initialises the dialog elements. + */ + public abstract void initFields(); + + /** + * Updates the values from the dialog elements. + */ + public abstract void update(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/InputValidationListener.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/InputValidationListener.java new file mode 100644 index 0000000..2c9490a --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/InputValidationListener.java @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.dialogs; + +/** + * A Listener Interface, instances can handle validation of the user input. + */ +public interface InputValidationListener { + + /** + * Notifies about the validity of an input. + * + * @param success true if input is valid, false otherwise. + */ + void inputValidationSuccessful(boolean success); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/ModifiedFlowLayout.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/ModifiedFlowLayout.java new file mode 100644 index 0000000..a125632 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/ModifiedFlowLayout.java @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.dialogs; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Insets; + +/** + * A modified version of FlowLayout that allows containers using this + * Layout to behave in a reasonable manner when placed inside a + * JScrollPane. + */ +public class ModifiedFlowLayout + extends + FlowLayout { + + /** + * Creates a new instance. + */ + public ModifiedFlowLayout() { + super(); + } + + /** + * Creates a new instance. + * + * @param align The alignment value. + */ + public ModifiedFlowLayout(int align) { + super(align); + } + + /** + * Creates a new instance. + * + * @param align the alignment value + * @param hgap the horizontal gap between components and between the + * components and the borders of the container + * @param vgap the vertical gap between components and between the components + * and the borders of the container + */ + public ModifiedFlowLayout(int align, int hgap, int vgap) { + super(align, hgap, vgap); + } + + @Override + public Dimension minimumLayoutSize(Container target) { + // Size of largest component, so we can resize it in + // either direction with something like a split-pane. + return computeMinSize(target); + } + + @Override + public Dimension preferredLayoutSize(Container target) { + return computeSize(target); + } + + private Dimension computeSize(Container target) { + synchronized (target.getTreeLock()) { + int hgap = getHgap(); + int vgap = getVgap(); + int w = target.getWidth(); + + // Let this behave like a regular FlowLayout (single row) + // if the container hasn't been assigned any size yet + if (w == 0) { + w = Integer.MAX_VALUE; + } + + Insets insets = target.getInsets(); + + if (insets == null) { + insets = new Insets(0, 0, 0, 0); + } + + int reqdWidth = 0; + int maxwidth = w - (insets.left + insets.right + hgap * 2); + int n = target.getComponentCount(); + int x = 0; + int y = insets.top + vgap; // FlowLayout starts by adding vgap, so do that here too. + int rowHeight = 0; + + for (int i = 0; i < n; i++) { + Component c = target.getComponent(i); + + if (c.isVisible()) { + Dimension d = c.getPreferredSize(); + + if ((x == 0) || ((x + d.width) <= maxwidth)) { + // fits in current row. + if (x > 0) { + x += hgap; + } + + x += d.width; + rowHeight = Math.max(rowHeight, d.height); + } + else { + // Start of new row + x = d.width; + y += vgap + rowHeight; + rowHeight = d.height; + } + + reqdWidth = Math.max(reqdWidth, x); + } + } + + y += rowHeight; + y += insets.bottom; + + return new Dimension(reqdWidth + insets.left + insets.right, y); + } + } + + private Dimension computeMinSize(Container target) { + synchronized (target.getTreeLock()) { + int minx = Integer.MAX_VALUE; + int miny = Integer.MIN_VALUE; + boolean foundOne = false; + int n = target.getComponentCount(); + + for (int i = 0; i < n; i++) { + Component c = target.getComponent(i); + + if (c.isVisible()) { + foundOne = true; + Dimension d = c.getPreferredSize(); + minx = Math.min(minx, d.width); + miny = Math.min(miny, d.height); + } + } + + if (foundOne) { + return new Dimension(minx, miny); + } + + return new Dimension(0, 0); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/StandardContentDialog.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/StandardContentDialog.form new file mode 100644 index 0000000..b6dcc9a --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/StandardContentDialog.form @@ -0,0 +1,109 @@ + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/StandardContentDialog.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/StandardContentDialog.java new file mode 100644 index 0000000..93cbf94 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/StandardContentDialog.java @@ -0,0 +1,305 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.dialogs; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JOptionPane; +import javax.swing.border.EmptyBorder; + +/** + * A standard dialog with an OK and a cancel button. + */ +public class StandardContentDialog + extends + javax.swing.JDialog + implements + InputValidationListener { + + /** + * A return status code - returned if Cancel button has been pressed. + */ + public static final int RET_CANCEL = 0; + /** + * A return status code - returned if OK button has been pressed. + */ + public static final int RET_OK = 1; + /** + * Button configuration for an OK and a cancel button. + */ + public static final int OK_CANCEL = 10; + /** + * Button configuration for an OK, cancel and apply button. + */ + public static final int OK_CANCEL_APPLY = 11; + /** + * Button configuration for a close button. + */ + public static final int CLOSE = 12; + /** + * Button configuration user-defined. + */ + public static final int USER_DEFINED = 13; + /** + * Content for this dialog. + */ + protected DialogContent fContent; + /** + * The return status. + */ + private int returnStatus = RET_CANCEL; + + /** + * Creates new instance. + */ + public StandardContentDialog(Component parent, DialogContent content) { + this(parent, content, true, OK_CANCEL); + } + + /** + * Creates new form StandardDialog. + * + * @param parent The parent component on which this dialog is centered. + * @param content The content. + * @param modal whether or not this dialog is modal. + * @param options Which user interface options to use. + */ + @SuppressWarnings("this-escape") + public StandardContentDialog( + Component parent, + DialogContent content, + boolean modal, + int options + ) { + super(JOptionPane.getFrameForComponent(parent), modal); + + initComponents(); + initButtons(options); + + JComponent component = content.getComponent(); + + if (component.getBorder() == null) { + component.setBorder(new EmptyBorder(4, 4, 4, 4)); + } + + getContentPane().add(component, BorderLayout.CENTER); + setTitle(content.getDialogTitle()); + content.initFields(); + pack(); + setLocationRelativeTo(parent); + fContent = content; + + getRootPane().setDefaultButton(okButton); + } + + @Override + public void inputValidationSuccessful(boolean success) { + this.okButton.setEnabled(success); + } + + /** + * Returns the returns status code. + * + * @return the return status of this dialog - one of RET_OK or RET_CANCEL + */ + public int getReturnStatus() { + return returnStatus; + } + + /** + * Adds a user-defined button. + * + * @param text The text for the button. + * @param returnStatus The return value when the button is pressed. + */ + public void addUserDefinedButton(String text, final int returnStatus) { + JButton button = new JButton(text); + button.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent evt) { + doClose(returnStatus); + } + }); + + buttonPanel.add(button); + } + + /** + * Event of the dialog content that the dialog can be closed. + */ + public void requestClose() { + doClose(RET_CANCEL); + } + + protected final void initButtons(int options) { + switch (options) { + case OK_CANCEL: + applyButton.setVisible(false); + closeButton.setVisible(false); + break; + + case OK_CANCEL_APPLY: + closeButton.setVisible(false); + break; + + case CLOSE: + okButton.setVisible(false); + cancelButton.setVisible(false); + applyButton.setVisible(false); + break; + + case USER_DEFINED: + okButton.setVisible(false); + cancelButton.setVisible(false); + applyButton.setVisible(false); + closeButton.setVisible(false); + break; + + default: + } + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + + buttonPanel = new javax.swing.JPanel(); + okButton = new javax.swing.JButton(); + cancelButton = new CancelButton(); + applyButton = new javax.swing.JButton(); + closeButton = new javax.swing.JButton(); + + addWindowListener(new java.awt.event.WindowAdapter() { + public void windowClosing(java.awt.event.WindowEvent evt) { + closeDialog(evt); + } + }); + + buttonPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(0, 0, 0, 5)); + buttonPanel.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.CENTER, 10, 5)); + + okButton.setFont(okButton.getFont().deriveFont(okButton.getFont().getStyle() | java.awt.Font.BOLD)); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/system"); // NOI18N + okButton.setText(bundle.getString("standardContentDialog.button_ok.text")); // NOI18N + okButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + okButtonActionPerformed(evt); + } + }); + buttonPanel.add(okButton); + + cancelButton.setFont(cancelButton.getFont()); + cancelButton.setText(bundle.getString("standardContentDialog.button_cancel.text")); // NOI18N + cancelButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cancelButtonActionPerformed(evt); + } + }); + buttonPanel.add(cancelButton); + + applyButton.setFont(applyButton.getFont()); + applyButton.setText(bundle.getString("standardContentDialog.button_apply.text")); // NOI18N + applyButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + applyButtonActionPerformed(evt); + } + }); + buttonPanel.add(applyButton); + + closeButton.setFont(closeButton.getFont()); + closeButton.setText(bundle.getString("standardContentDialog.button_close.text")); // NOI18N + closeButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + closeButtonActionPerformed(evt); + } + }); + buttonPanel.add(closeButton); + + getContentPane().add(buttonPanel, java.awt.BorderLayout.SOUTH); + + pack(); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + /** + * Button "close" pressed. + * + * @param evt The action event. + */ + private void closeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_closeButtonActionPerformed + doClose(RET_CANCEL); + }//GEN-LAST:event_closeButtonActionPerformed + + /** + * Button "apply" pressed. + * + * @param evt The action event. + */ + private void applyButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_applyButtonActionPerformed + fContent.update(); + }//GEN-LAST:event_applyButtonActionPerformed + + /** + * Button "Ok" pressed. + * + * @param evt The action event. + */ + private void okButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_okButtonActionPerformed + fContent.update(); + + if (!fContent.updateFailed()) { + doClose(RET_OK); + } + }//GEN-LAST:event_okButtonActionPerformed + + /** + * Button "cancel" pressed. + * + * @param evt The action event. + */ + private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed + doClose(RET_CANCEL); + }//GEN-LAST:event_cancelButtonActionPerformed + + /** + * Closes the dialog + */ + private void closeDialog(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_closeDialog + doClose(RET_CANCEL); + }//GEN-LAST:event_closeDialog + + /** + * Closes the dialog. + * + * @param retStatus The return status code. + */ + private void doClose(int retStatus) { + returnStatus = retStatus; + setVisible(false); + dispose(); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton applyButton; + private javax.swing.JPanel buttonPanel; + private javax.swing.JButton cancelButton; + private javax.swing.JButton closeButton; + private javax.swing.JButton okButton; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/StandardDetailsDialog.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/StandardDetailsDialog.form new file mode 100644 index 0000000..d936b90 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/StandardDetailsDialog.form @@ -0,0 +1,92 @@ + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/StandardDetailsDialog.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/StandardDetailsDialog.java new file mode 100644 index 0000000..db431b9 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/StandardDetailsDialog.java @@ -0,0 +1,201 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.dialogs; + +import java.awt.Component; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; +import org.opentcs.guing.base.components.properties.type.ModelAttribute; +import org.opentcs.guing.base.components.properties.type.Property; + +/** + * A dialog in which a {@link DialogContent} can be added. + * The dialog has an OK and a cancel button. + */ +public class StandardDetailsDialog + extends + javax.swing.JDialog + implements + DetailsDialog { + + /** + * A return status code - returned if Cancel button has been pressed + */ + public static final int RET_CANCEL = 0; + /** + * A return status code - returned if OK button has been pressed + */ + public static final int RET_OK = 1; + private int returnStatus = RET_CANCEL; + /** + * The details dialog content to change a property. + */ + private final DetailsDialogContent fContent; + private final Component fParentComponent; + + /** + * Creates new form JDialog + * + * @param parent The parent component. + * @param content Details dialog content. + * @param modal Whether or not the dialog is modal. + */ + @SuppressWarnings("this-escape") + public StandardDetailsDialog(JPanel parent, boolean modal, DetailsDialogContent content) { + super(JOptionPane.getFrameForComponent(parent), modal); + fContent = content; + fParentComponent = parent; + initialize(); + } + + /** + * Creates a new dialog. + * + * @param parent The parent dialog. + * @param modal Whether or not the dialog is modal. + * @param content Details dialog content. + */ + @SuppressWarnings("this-escape") + public StandardDetailsDialog(JDialog parent, boolean modal, DetailsDialogContent content) { + super(parent, modal); + fContent = content; + fParentComponent = parent; + initialize(); + } + + /* + * Initialises the dialog. + */ + protected final void initialize() { + JComponent component = (JComponent) fContent; + component.setBorder(new EmptyBorder(new java.awt.Insets(5, 5, 5, 5))); + getContentPane().add(component, java.awt.BorderLayout.CENTER); + initComponents(); + setTitle(fContent.getTitle()); + activate(); + } + + @Override + public void activate() { + getRootPane().setDefaultButton(okButton); + pack(); + setLocationRelativeTo(fParentComponent); + } + + public Component getParentComponent() { + return fParentComponent; + } + + /** + * Returns the return status. + * + * @return the return status of this dialog - one of RET_OK or RET_CANCEL + */ + public int getReturnStatus() { + return returnStatus; + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + + controlPanel = new javax.swing.JPanel(); + buttonPanel = new javax.swing.JPanel(); + okButton = new javax.swing.JButton(); + cancelButton = new CancelButton(); + + setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); + setIconImage(null); + addWindowListener(new java.awt.event.WindowAdapter() { + public void windowClosing(java.awt.event.WindowEvent evt) { + closeDialog(evt); + } + }); + + controlPanel.setLayout(new java.awt.BorderLayout()); + + buttonPanel.setOpaque(false); + + okButton.setFont(okButton.getFont().deriveFont(okButton.getFont().getStyle() | java.awt.Font.BOLD)); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/system"); // NOI18N + okButton.setText(bundle.getString("standardDetailsDialog.button_ok.text")); // NOI18N + okButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + okButtonActionPerformed(evt); + } + }); + buttonPanel.add(okButton); + + cancelButton.setFont(cancelButton.getFont()); + cancelButton.setText(bundle.getString("standardDetailsDialog.button_cancel.text")); // NOI18N + cancelButton.setOpaque(false); + cancelButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cancelButtonActionPerformed(evt); + } + }); + buttonPanel.add(cancelButton); + + controlPanel.add(buttonPanel, java.awt.BorderLayout.SOUTH); + + getContentPane().add(controlPanel, java.awt.BorderLayout.SOUTH); + + pack(); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed + doClose(RET_CANCEL); + }//GEN-LAST:event_cancelButtonActionPerformed + + private void okButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_okButtonActionPerformed + fContent.updateValues(); + + Property property = fContent.getProperty(); + + if (property != null) { + property.setChangeState(ModelAttribute.ChangeState.DETAIL_CHANGED); + } + + doClose(RET_OK); + }//GEN-LAST:event_okButtonActionPerformed + + /** + * Closes the dialog. + */ + private void doClose(int retStatus) { + returnStatus = retStatus; + + setVisible(false); + } + + private void closeDialog(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_closeDialog + doClose(RET_CANCEL); + }//GEN-LAST:event_closeDialog + + @Override + public DetailsDialogContent getDialogContent() { + return fContent; + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel buttonPanel; + private javax.swing.JButton cancelButton; + private javax.swing.JPanel controlPanel; + private javax.swing.JButton okButton; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/StandardDialog.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/StandardDialog.form new file mode 100644 index 0000000..df3de69 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/StandardDialog.form @@ -0,0 +1,72 @@ + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/StandardDialog.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/StandardDialog.java new file mode 100644 index 0000000..bab0b19 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dialogs/StandardDialog.java @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.dialogs; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Insets; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JOptionPane; +import javax.swing.border.EmptyBorder; +import org.opentcs.util.gui.Icons; + +/** + * A dialog with an ok and a cancel button. + */ +public class StandardDialog + extends + JDialog { + + /** + * A return status code - returned if Cancel button has been pressed + */ + public static final int RET_CANCEL = 0; + /** + * A return status code - returned if OK button has been pressed + */ + public static final int RET_OK = 1; + /** + * The content of this dialog. + */ + protected JComponent fContent; + private int returnStatus = RET_CANCEL; + + /** + * Creates a new instance. + * + * @param parent The parent component. + * @param modal Whether or not the dialog is modal. + * @param content The content component. + * @param title The title of the dialog. + */ + @SuppressWarnings("this-escape") + public StandardDialog(Component parent, boolean modal, JComponent content, String title) { + super(JOptionPane.getFrameForComponent(parent), title, modal); + initComponents(); + initSize(content); + setTitle(title); + setIconImages(Icons.getOpenTCSIcons()); + } + + /** + * Initialises the size of the dialog based on the content. + * + * @param content the dialog to base the size on. + */ + protected final void initSize(JComponent content) { + fContent = content; + getContentPane().add(content, BorderLayout.CENTER); + content.setBorder(new EmptyBorder(new Insets(3, 3, 3, 3))); + getRootPane().setDefaultButton(okButton); + pack(); + } + + /** + * Returns the content of this dialog. + * + * @return the content of this dialog. + */ + public JComponent getContent() { + return fContent; + } + + /** + * Return the return status of this dialog - one of RET_OK or RET_CANCEL. + * + * @return the return status of this dialog - one of RET_OK or RET_CANCEL. + */ + public int getReturnStatus() { + return returnStatus; + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + + buttonPanel = new javax.swing.JPanel(); + okButton = new javax.swing.JButton(); + cancelButton = new CancelButton(); + + addWindowListener(new java.awt.event.WindowAdapter() { + public void windowClosing(java.awt.event.WindowEvent evt) { + closeDialog(evt); + } + }); + + okButton.setFont(okButton.getFont().deriveFont(okButton.getFont().getStyle() | java.awt.Font.BOLD)); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/system"); // NOI18N + okButton.setText(bundle.getString("standardDialog.button_ok.text")); // NOI18N + okButton.setOpaque(false); + okButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + okButtonActionPerformed(evt); + } + }); + buttonPanel.add(okButton); + + cancelButton.setFont(cancelButton.getFont()); + cancelButton.setText(bundle.getString("standardDialog.button_cancel.text")); // NOI18N + cancelButton.setOpaque(false); + cancelButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cancelButtonActionPerformed(evt); + } + }); + buttonPanel.add(cancelButton); + + getContentPane().add(buttonPanel, java.awt.BorderLayout.SOUTH); + + pack(); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void okButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_okButtonActionPerformed + doClose(RET_OK); + }//GEN-LAST:event_okButtonActionPerformed + + private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed + doClose(RET_CANCEL); + }//GEN-LAST:event_cancelButtonActionPerformed + + private void closeDialog(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_closeDialog + doClose(RET_CANCEL); + }//GEN-LAST:event_closeDialog + + /** + * Closes the dialog. + */ + private void doClose(int retStatus) { + returnStatus = retStatus; + setVisible(false); + dispose(); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel buttonPanel; + private javax.swing.JButton cancelButton; + private javax.swing.JButton okButton; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/AbstractDockingManager.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/AbstractDockingManager.java new file mode 100644 index 0000000..d58fccb --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/AbstractDockingManager.java @@ -0,0 +1,289 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.dockable; + +import static java.util.Objects.requireNonNull; + +import bibliothek.gui.dock.common.CContentArea; +import bibliothek.gui.dock.common.CControl; +import bibliothek.gui.dock.common.CLocation; +import bibliothek.gui.dock.common.DefaultSingleCDockable; +import bibliothek.gui.dock.common.SingleCDockable; +import bibliothek.gui.dock.common.event.CVetoClosingEvent; +import bibliothek.gui.dock.common.event.CVetoClosingListener; +import bibliothek.gui.dock.common.mode.ExtendedMode; +import java.awt.Rectangle; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import javax.swing.JComponent; + +/** + * Utility class for working with dockables. + */ +public abstract class AbstractDockingManager + implements + DockingManager { + + /** + * PropertyChangeEvent when a floating dockable closes. + */ + public static final String DOCKABLE_CLOSED = "DOCKABLE_CLOSED"; + /** + * Map that contains all tab panes. They are stored by their id. + */ + private final Map tabPanes = new HashMap<>(); + /** + * Control for the dockable panels. + */ + private final CControl control; + /** + * The listeners for closing events. + */ + private final List listeners = new ArrayList<>(); + + public AbstractDockingManager(CControl control) { + this.control = requireNonNull(control, "control"); + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + if (!listeners.contains(listener)) { + listeners.add(listener); + } + } + + /** + * Creates a new dockable. + * + * @param id The unique id for this dockable. + * @param title The title text of this new dockable. + * @param comp The JComponent wrapped by the new dockable. + * @param closeable If the dockable can be closeable or not. + * @return The newly created dockable. + */ + public DefaultSingleCDockable createDockable( + String id, + String title, + JComponent comp, + boolean closeable + ) { + Objects.requireNonNull(id, "id is null"); + Objects.requireNonNull(title, "title is null"); + Objects.requireNonNull(comp, "comp is null"); + if (control == null) { + return null; + } + DefaultSingleCDockable dockable = new DefaultSingleCDockable(id, title); + dockable.setCloseable(closeable); + dockable.add(comp); + return dockable; + } + + /** + * Creates a new floating dockable. + * + * @param id The unique id for this dockable. + * @param title The title text of this new dockable. + * @param comp The JComponent wrapped by the new dockable. + * @return The newly created dockable. + */ + public DefaultSingleCDockable createFloatingDockable( + String id, + String title, + JComponent comp + ) { + if (control == null) { + return null; + } + final DefaultSingleCDockable dockable = new DefaultSingleCDockable(id, title); + dockable.setCloseable(true); + dockable.setFocusComponent(comp); + dockable.add(comp); + dockable.addVetoClosingListener(new CVetoClosingListener() { + + @Override + public void closing(CVetoClosingEvent event) { + } + + @Override + public void closed(CVetoClosingEvent event) { + fireFloatingDockableClosed(dockable); + } + }); + control.addDockable(dockable); + dockable.setExtendedMode(ExtendedMode.EXTERNALIZED); + Rectangle centerRectangle = control.getContentArea().getCenter().getBounds(); + dockable.setLocation( + CLocation.external( + (centerRectangle.width - comp.getWidth()) / 2, + (centerRectangle.height - comp.getHeight()) / 2, + comp.getWidth(), + comp.getHeight() + ) + ); + return dockable; + } + + /** + * Adds a dockable as tab to the tab pane identified by the given id. + * + * @param newTab The new dockable that shall be added. + * @param id The ID of the tab pane. + * @param index Index where to insert the dockable in the tab pane. + */ + public void addTabTo(DefaultSingleCDockable newTab, String id, int index) { + Objects.requireNonNull(newTab, "newTab is null."); + Objects.requireNonNull(id, "id is null"); + CStack tabPane = tabPanes.get(id); + if (tabPane != null) { + control.addDockable(newTab); + newTab.setWorkingArea(tabPane); + tabPane.getStation().add(newTab.intern(), index); + tabPane.getStation().setFrontDockable(newTab.intern()); + } + } + + @Override + public void removeDockable(SingleCDockable dockable) { + Objects.requireNonNull(dockable, "dockable is null"); + if (control != null) { + control.removeDockable(dockable); + } + } + + @Override + public void removeDockable(String id) { + Objects.requireNonNull(id); + SingleCDockable dock = control.getSingleDockable(id); + if (dock != null) { + removeDockable(dock); + } + } + + /** + * Returns the CControl. + * + * @return The CControl. + */ + public CControl getCControl() { + return control; + } + + /** + * Returns the whole component with all dockables, tab panes etc. + * + * @return The CContentArea of the CControl. + */ + public CContentArea getContentArea() { + if (control != null) { + return control.getContentArea(); + } + else { + return null; + } + } + + /** + * Returns the tab pane with the given id. + * + * @param id ID of the tab pane. + * @return The tab pane or null if there is no tab pane with this id. + */ + public CStack getTabPane(String id) { + if (control != null) { + return tabPanes.get(id); + } + else { + return null; + } + } + + public abstract void reset(); + + /** + * Wraps all given JComponents into a dockable and deploys them on the CControl. + */ + public abstract void initializeDockables(); + + protected void addTabPane(String id, CStack tabPane) { + tabPanes.put(id, tabPane); + } + + /** + * Hides a dockable (by actually removing it from its station). + * + * @param station The CStackDockStation the dockable belongs to. + * @param dockable The dockable to hide. + */ + public void hideDockable(CStackDockStation station, DefaultSingleCDockable dockable) { + int index = station.indexOf(dockable.intern()); + + if (index <= -1) { + station.add(dockable.intern(), station.getDockableCount()); + index = station.indexOf(dockable.intern()); + } + station.remove(index); + } + + /** + * Shows a dockable (by actually adding it to its station). + * + * @param station The CStackDockStation the dockable belongs to. + * @param dockable The dockable to show. + * @param index Where to add the dockable. + */ + public void showDockable( + CStackDockStation station, + DefaultSingleCDockable dockable, + int index + ) { + if (station.indexOf(dockable.intern()) <= -1) { + station.add(dockable.intern(), index); + } + } + + /** + * Sets the visibility status of a dockable with the given id. + * + * @param id The id of the dockable. + * @param visible If it shall be visible or not. + */ + public void setDockableVisibility(String id, boolean visible) { + if (control != null) { + SingleCDockable dockable = control.getSingleDockable(id); + if (dockable != null) { + dockable.setVisible(visible); + } + } + } + + /** + * Checks if the given dockable is docked to its CStackDockStation. + * + * @param station The station the dockable should be docked in. + * @param dockable The dockable to check. + * @return True if it is docked, false otherwise. + */ + public boolean isDockableDocked(CStackDockStation station, DefaultSingleCDockable dockable) { + return station.indexOf(dockable.intern()) <= -1; + } + + /** + * Fires a PropertyChangeEvent when a floatable dockable is closed + * (eg a plugin panel). + * + * @param dockable The dockable that was closed. + */ + private void fireFloatingDockableClosed(DefaultSingleCDockable dockable) { + for (PropertyChangeListener listener : listeners) { + listener.propertyChange( + new PropertyChangeEvent(this, DOCKABLE_CLOSED, dockable, dockable) + ); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/CStack.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/CStack.java new file mode 100644 index 0000000..d4e0f70 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/CStack.java @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.dockable; + +import bibliothek.gui.DockController; +import bibliothek.gui.Dockable; +import bibliothek.gui.dock.action.DockActionSource; +import bibliothek.gui.dock.common.CLocation; +import bibliothek.gui.dock.common.intern.AbstractDockableCStation; +import bibliothek.gui.dock.common.intern.CControlAccess; +import bibliothek.gui.dock.common.mode.CNormalModeArea; +import bibliothek.gui.dock.common.mode.ExtendedMode; +import bibliothek.gui.dock.common.perspective.CStationPerspective; +import bibliothek.gui.dock.facile.mode.Location; +import bibliothek.gui.dock.facile.mode.LocationMode; +import bibliothek.gui.dock.facile.mode.ModeAreaListener; +import bibliothek.gui.dock.layout.DockableProperty; +import bibliothek.gui.dock.support.mode.AffectedSet; +import bibliothek.gui.dock.util.DockUtilities; +import bibliothek.util.Path; + +/** + * A tab dockable copied from the dockingframes examples. + */ +public class CStack + extends + AbstractDockableCStation + implements + CNormalModeArea { + + @SuppressWarnings("this-escape") + public CStack(String id) { + CStackDockStation delegate = new CStackDockStation(this); + + @SuppressWarnings("deprecation") + CLocation stationLocation = new CLocation() { + + @Override + public CLocation getParent() { + return null; + } + + @Override + public String findRoot() { + return getUniqueId(); + } + + @Override + public DockableProperty findProperty(DockableProperty successor) { + return successor; + } + + @Override + public ExtendedMode findMode() { + return ExtendedMode.NORMALIZED; + } + + @Override + public CLocation aside() { + return this; + } + }; + + init(delegate, id, stationLocation, delegate); + } + + @Override + protected void install(CControlAccess access) { + access.getLocationManager().getNormalMode().add(this); + } + + @Override + protected void uninstall(CControlAccess access) { + access.getLocationManager().getNormalMode().remove(getUniqueId()); + } + + @Override + public CStationPerspective createPerspective() { + throw new IllegalStateException("not implemented"); + } + + @Override + public boolean isNormalModeChild(Dockable dockable) { + return isChild(dockable); + } + + @Override + public DockableProperty getLocation(Dockable child) { + return DockUtilities.getPropertyChain(getStation(), child); + } + + @Override + public boolean setLocation(Dockable dockable, DockableProperty location, AffectedSet set) { + set.add(dockable); + + if (isChild(dockable)) { + getStation().move(dockable, location); + } + else { + boolean acceptable = DockUtilities.acceptable(getStation(), dockable); + if (!acceptable) { + return false; + } + + if (!getStation().drop(dockable, location)) { + getStation().drop(dockable); + } + } + + return true; + } + + @Override + public void addModeAreaListener(ModeAreaListener listener) { + + } + + @Override + public boolean autoDefaultArea() { + return true; + } + + @Override + public boolean isLocationRoot() { + return true; + } + + @Override + public boolean isChild(Dockable dockable) { + return dockable.getDockParent() == getStation(); + } + + @Override + public void removeModeAreaListener(ModeAreaListener listener) { + + } + + @Override + public void setController(DockController controller) { + + } + + @Override + public void setMode(LocationMode mode) { + + } + + @Override + public CLocation getCLocation(Dockable dockable) { + DockableProperty property = DockUtilities.getPropertyChain(getStation(), dockable); + return getStationLocation().expandProperty(getStation().getController(), property); + } + + @Override + public CLocation getCLocation(Dockable dockable, Location location) { + DockableProperty property = location.getLocation(); + if (property == null) { + return getStationLocation(); + } + + return getStationLocation().expandProperty(getStation().getController(), property); + } + + @Override + public boolean respectWorkingAreas() { + return true; + } + + @Override + public boolean isCloseable() { + return false; + } + + @Override + public boolean isExternalizable() { + return false; + } + + @Override + public boolean isMinimizable() { + return false; + } + + @Override + public boolean isStackable() { + return false; + } + + @Override + public boolean isWorkingArea() { + return true; + } + + public DockActionSource[] getSources() { + return new DockActionSource[]{getClose()}; + } + + @Override + public boolean isMaximizable() { + return false; + } + + @Override + public Path getTypeId() { + return null; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/CStackDockStation.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/CStackDockStation.java new file mode 100644 index 0000000..408ec8f --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/CStackDockStation.java @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.dockable; + +import bibliothek.gui.dock.StackDockStation; +import bibliothek.gui.dock.action.DockActionSource; +import bibliothek.gui.dock.common.CStation; +import bibliothek.gui.dock.common.intern.CDockable; +import bibliothek.gui.dock.common.intern.CommonDockable; +import bibliothek.gui.dock.common.intern.station.CommonDockStation; +import bibliothek.gui.dock.common.intern.station.CommonDockStationFactory; + +/** + */ +public class CStackDockStation + extends + StackDockStation + implements + CommonDockStation, + CommonDockable { + + private final CStack delegate; + + public CStackDockStation(CStack stack) { + this.delegate = stack; + } + + @Override + public String getFactoryID() { + return CommonDockStationFactory.FACTORY_ID; + } + + @Override + public String getConverterID() { + return super.getFactoryID(); + } + + @Override + public CDockable getDockable() { + return delegate; + } + + @Override + public DockActionSource[] getSources() { + return delegate.getSources(); + } + + @Override + public CStation getStation() { + return delegate; + } + + @Override + public StackDockStation getDockStation() { + return this; + } + + @Override + public CStackDockStation asDockStation() { + return this; + } + + @Override + public CommonDockable asDockable() { + return this; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/DockableClosingHandler.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/DockableClosingHandler.java new file mode 100644 index 0000000..357e093 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/DockableClosingHandler.java @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.dockable; + +import static java.util.Objects.requireNonNull; + +import bibliothek.gui.dock.common.DefaultSingleCDockable; +import bibliothek.gui.dock.common.event.CVetoClosingEvent; +import bibliothek.gui.dock.common.event.CVetoClosingListener; +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import org.opentcs.guing.common.application.ViewManager; + +/** + * Handles closing of a dockable. + */ +public class DockableClosingHandler + implements + CVetoClosingListener { + + /** + * The dockable. + */ + private final DefaultSingleCDockable dockable; + /** + * Manages the application's dockables. + */ + private final DockingManager dockingManager; + /** + * Manages the application's views. + */ + private final ViewManager viewManager; + + /** + * Creates a new instance. + * + * @param dockable The dockable. + * @param viewManager Manages the application's views. + * @param dockingManager Manages the application's dockables. + */ + @Inject + public DockableClosingHandler( + @Assisted + DefaultSingleCDockable dockable, + ViewManager viewManager, + DockingManager dockingManager + ) { + this.dockable = requireNonNull(dockable, "dockable"); + this.viewManager = requireNonNull(viewManager, "viewManager"); + this.dockingManager = requireNonNull(dockingManager, "dockingManager"); + } + + @Override + public void closing(CVetoClosingEvent event) { + } + + @Override + public void closed(CVetoClosingEvent event) { + if (event.isExpected()) { + dockingManager.removeDockable(dockable); + viewManager.removeDockable(dockable); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/DockableHandlerFactory.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/DockableHandlerFactory.java new file mode 100644 index 0000000..4be216f --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/DockableHandlerFactory.java @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.dockable; + +import bibliothek.gui.dock.common.DefaultSingleCDockable; + +/** + * A factory for handlers related to dockables. + */ +public interface DockableHandlerFactory { + + /** + * Creates a new handler for closing the given dockable. + * + * @param dockable The dockable. + * @return A new handler for closing the given dockable. + */ + DockableClosingHandler createDockableClosingHandler( + DefaultSingleCDockable dockable + ); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/DockableTitleComparator.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/DockableTitleComparator.java new file mode 100644 index 0000000..0d10ad1 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/DockableTitleComparator.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.dockable; + +import bibliothek.gui.dock.common.DefaultSingleCDockable; +import java.util.Comparator; + +/** + * Compares two DefaultSingleCDockable instances by their titles. + */ +public class DockableTitleComparator + implements + Comparator { + + /** + * Creates a new instance. + */ + public DockableTitleComparator() { + } + + @Override + public int compare(DefaultSingleCDockable o1, DefaultSingleCDockable o2) { + return o1.getTitleText().compareTo(o2.getTitleText()); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/DockingManager.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/DockingManager.java new file mode 100644 index 0000000..6645fd0 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/DockingManager.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.dockable; + +import bibliothek.gui.dock.common.SingleCDockable; +import java.beans.PropertyChangeListener; + +/** + * Utility class for working with dockables. + */ +public interface DockingManager { + + /** + * PropertyChangeEvent when a floating dockable closes. + */ + String DOCKABLE_CLOSED = "DOCKABLE_CLOSED"; + + /** + * Adds a PropertyChangeListener. + * + * @param listener The new listener. + */ + void addPropertyChangeListener(PropertyChangeListener listener); + + /** + * Removes a dockable from the CControl. + * + * @param dockable The dockable that shall be removed. + */ + void removeDockable(SingleCDockable dockable); + + /** + * Removes a dockable with the given id. + * + * @param id The id of the dockable to remove. + */ + void removeDockable(String id); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/DrawingViewFocusHandler.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/DrawingViewFocusHandler.java new file mode 100644 index 0000000..6b09c85 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/dockable/DrawingViewFocusHandler.java @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.dockable; + +import static java.util.Objects.requireNonNull; + +import bibliothek.gui.dock.common.event.CFocusListener; +import bibliothek.gui.dock.common.intern.CDockable; +import jakarta.inject.Inject; +import java.awt.event.FocusEvent; +import org.jhotdraw.draw.DrawingEditor; +import org.opentcs.guing.common.application.ViewManager; +import org.opentcs.guing.common.components.drawing.DrawingViewScrollPane; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingView; + +/** + * Handles focussing of dockable drawing views. + */ +public class DrawingViewFocusHandler + implements + CFocusListener { + + /** + * Manages the application's views. + */ + private final ViewManager viewManager; + /** + * The drawing editor. + */ + private final DrawingEditor drawingEditor; + + /** + * Creates a new instance. + * + * @param viewManager Manages the application's views. + * @param drawingEditor The drawing editor. + */ + @Inject + public DrawingViewFocusHandler( + ViewManager viewManager, + DrawingEditor drawingEditor + ) { + this.viewManager = requireNonNull(viewManager, "viewManager"); + this.drawingEditor = requireNonNull(drawingEditor, "drawingEditor"); + } + + @Override + public void focusGained(CDockable dockable) { + DrawingViewScrollPane scrollPane = viewManager.getDrawingViewMap().get(dockable); + if (scrollPane == null) { + return; + } + OpenTCSDrawingView drawView = scrollPane.getDrawingView(); + drawingEditor.setActiveView(drawView); + // XXX Looks suspicious: Why are the same values set again here? + drawView.setConstrainerVisible(drawView.isConstrainerVisible()); + drawView.setLabelsVisible(drawView.isLabelsVisible()); + scrollPane.setRulersVisible(scrollPane.isRulersVisible()); + drawView.getComponent().dispatchEvent(new FocusEvent(scrollPane, FocusEvent.FOCUS_GAINED)); + } + + @Override + public void focusLost(CDockable dockable) { + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/BezierLinerEditHandler.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/BezierLinerEditHandler.java new file mode 100644 index 0000000..12ef643 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/BezierLinerEditHandler.java @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing; + +import javax.swing.event.UndoableEditEvent; +import javax.swing.event.UndoableEditListener; +import org.jhotdraw.draw.BezierFigure; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.common.components.drawing.figures.PathConnection; +import org.opentcs.guing.common.components.drawing.figures.liner.BezierLinerEdit; + +/** + * Updates a bezier-style PathConnection's control points on edits. + */ +public class BezierLinerEditHandler + implements + UndoableEditListener { + + /** + * Creates a new instance. + */ + public BezierLinerEditHandler() { + } + + @Override + public void undoableEditHappened(UndoableEditEvent evt) { + if (!(evt.getEdit() instanceof BezierLinerEdit)) { + return; + } + BezierFigure owner = ((BezierLinerEdit) evt.getEdit()).getOwner(); + if (!(owner instanceof PathConnection)) { + return; + } + + PathConnection path = (PathConnection) owner; + path.updateControlPoints(); + PathModel pathModel = path.getModel(); + pathModel.getPropertyPathControlPoints().markChanged(); + pathModel.propertiesChanged(path); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/DrawingOptions.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/DrawingOptions.java new file mode 100644 index 0000000..f36833b --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/DrawingOptions.java @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing; + +/** + * Allows the configuration of drawing options. + */ +public class DrawingOptions { + + /** + * Indicates whether blocks should be drawn or not. + */ + private boolean blocksVisible = true; + + public DrawingOptions() { + } + + /** + * Returns whether blocks should be drawn or not. + * + * @return {@code true}, if blocks should be drawn, otherwise {@code false}. + */ + public boolean isBlocksVisible() { + return blocksVisible; + } + + /** + * Sets whether blocks should be drawn or not. + * + * @param blocksVisible The new value. + */ + public void setBlocksVisible(boolean blocksVisible) { + this.blocksVisible = blocksVisible; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/DrawingViewPlacardPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/DrawingViewPlacardPanel.java new file mode 100644 index 0000000..242290e --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/DrawingViewPlacardPanel.java @@ -0,0 +1,306 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing; + +import static java.util.Objects.requireNonNull; + +import java.awt.FlowLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ItemEvent; +import java.beans.PropertyChangeEvent; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JPanel; +import javax.swing.JToggleButton; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * A placard panel for drawing views. + */ +public class DrawingViewPlacardPanel + extends + JPanel { + + /** + * This instance's resource bundle. + */ + private final ResourceBundleUtil labels + = ResourceBundleUtil.getBundle(I18nPlantOverview.MODELVIEW_PATH); + /** + * A combo box for selecting the zoom level. + */ + private final JComboBox zoomComboBox; + /** + * A toggle button for turning rulers on/off. + */ + private final JToggleButton toggleRulersButton; + /** + * The drawing options. + */ + private final DrawingOptions drawingOptions; + + /** + * Creates a new instance. + * + * @param drawingView The drawing view. + * @param drawingOptions The drawing options. + */ + @SuppressWarnings("this-escape") + public DrawingViewPlacardPanel( + OpenTCSDrawingView drawingView, + DrawingOptions drawingOptions + ) { + requireNonNull(drawingView, "drawingView"); + this.drawingOptions = requireNonNull(drawingOptions, "drawingOptions"); + + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + + // Create an extra panel so that the contents can be centered vertically. + JPanel vCenteringPanel = new JPanel(); + vCenteringPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 0, 0)); + this.add(Box.createVerticalGlue()); + this.add(vCenteringPanel); + + this.zoomComboBox = zoomComboBox(drawingView); + vCenteringPanel.add(zoomComboBox); + + vCenteringPanel.add(zoomViewToWindowButton(drawingView)); + + // Show/hide grid + JToggleButton toggleConstrainerButton = toggleConstrainerButton(drawingView); + toggleConstrainerButton.setSelected(drawingView.isConstrainerVisible()); + vCenteringPanel.add(toggleConstrainerButton); + + // Show/hide rulers + toggleRulersButton = toggleRulersButton(); + vCenteringPanel.add(toggleRulersButton); + + // Show/hide leabels + JToggleButton toggleLabelsButton = toggleLabelsButton(drawingView); + toggleLabelsButton.setSelected(drawingView.isLabelsVisible()); + vCenteringPanel.add(toggleLabelsButton); + + // Show/hide blocks + JToggleButton toggleBlocksButton = toggleBlocksButton(drawingView); + toggleBlocksButton.setSelected(drawingOptions.isBlocksVisible()); + vCenteringPanel.add(toggleBlocksButton); + } + + public JComboBox getZoomComboBox() { + return zoomComboBox; + } + + public JToggleButton getToggleRulersButton() { + return toggleRulersButton; + } + + /** + * Creates the combo box with different zoom factors. + * + * @param drawingView The DrawingView this combo box will belong to. + * @return The created combo box. + */ + private JComboBox zoomComboBox(final OpenTCSDrawingView drawingView) { + final JComboBox comboBox = new JComboBox<>(); + comboBox.setEditable(true); + comboBox.setFocusable(true); + + final double[] scaleFactors = { + 5.00, 4.00, 3.00, 2.00, 1.50, 1.25, 1.00, 0.75, 0.50, 0.25, 0.10 + }; + for (int i = 0; i < scaleFactors.length; i++) { + comboBox.addItem(new ZoomItem(scaleFactors[i])); + + if (scaleFactors[i] == 1.0) { + comboBox.setSelectedIndex(i); + } + } + + comboBox.addActionListener((ActionEvent e) -> { + final double scaleFactor; + + if (comboBox.getSelectedItem() instanceof ZoomItem) { + // A zoom step of the array scaleFactors[] + ZoomItem item = (ZoomItem) comboBox.getSelectedItem(); + scaleFactor = item.getScaleFactor(); + } + else { + try { + // Text input in the combo box + String text = (String) comboBox.getSelectedItem(); + double factor = Double.parseDouble(text.split(" ")[0]); + scaleFactor = factor * 0.01; // Eingabe in % + comboBox.setSelectedItem((int) (factor + 0.5) + " %"); + } + catch (NumberFormatException ex) { + comboBox.setSelectedIndex(0); + return; + } + } + + drawingView.setScaleFactor(scaleFactor); + }); + + drawingView.addPropertyChangeListener((PropertyChangeEvent evt) -> { + // String constants are interned + if ("scaleFactor".equals(evt.getPropertyName())) { + double scaleFactor = (double) evt.getNewValue(); + + for (int i = 0; i < comboBox.getItemCount(); i++) { + // One of the predefined scale factors was selected + if (scaleFactor == comboBox.getItemAt(i).getScaleFactor()) { + comboBox.setSelectedIndex(i); + break; + } + + if (i + 1 < comboBox.getItemCount() + && scaleFactor < comboBox.getItemAt(i).getScaleFactor() + && scaleFactor > comboBox.getItemAt(i + 1).getScaleFactor()) { + // Insert the new scale factor between the next smaller / larger entries + ZoomItem newItem = new ZoomItem(scaleFactor); + comboBox.insertItemAt(newItem, i + 1); + comboBox.setSelectedItem(newItem); + } + else if (scaleFactor > comboBox.getItemAt(0).getScaleFactor()) { + // Insert new item for scale factor larger than the largest predefined factor + ZoomItem newItem = new ZoomItem(scaleFactor); + comboBox.insertItemAt(newItem, 0); + comboBox.setSelectedItem(newItem); + } + else if (scaleFactor < comboBox.getItemAt(comboBox.getItemCount() - 1).getScaleFactor()) { + // Insert new item for scale factor larger than the largest predefined factor + ZoomItem newItem = new ZoomItem(scaleFactor); + comboBox.insertItemAt(newItem, comboBox.getItemCount()); + comboBox.setSelectedItem(newItem); + } + } + } + }); + + return comboBox; + } + + /** + * Creates a button that zooms the drawing to a scale factor so that + * it fits the window size. + * + * @return The created button. + */ + private JButton zoomViewToWindowButton(final OpenTCSDrawingView drawingView) { + final JButton button = new JButton(); + + button.setToolTipText( + labels.getString("drawingViewPlacardPanel.button_zoomViewToWindow.tooltipText") + ); + + button.setIcon(ImageDirectory.getImageIcon("/menu/zoom-fit-best-4.png")); + + button.setMargin(new Insets(0, 0, 0, 0)); + button.setFocusable(false); + + button.addActionListener((ActionEvent e) -> drawingView.zoomViewToWindow()); + + return button; + } + + /** + * Creates a button to toggle the grid in the drawing. + * + * @param view The DrawingView the button will belong to. + * @return The created button. + */ + private JToggleButton toggleConstrainerButton(final OpenTCSDrawingView drawingView) { + final JToggleButton toggleButton = new JToggleButton(); + + toggleButton.setToolTipText( + labels.getString("drawingViewPlacardPanel.button_toggleGrid.tooltipText") + ); + + toggleButton.setIcon(ImageDirectory.getImageIcon("/menu/view-split.png")); + + toggleButton.setMargin(new Insets(0, 0, 0, 0)); + toggleButton.setFocusable(false); + + toggleButton.addItemListener( + (ItemEvent event) -> drawingView.setConstrainerVisible(toggleButton.isSelected()) + ); + + return toggleButton; + } + + /** + * Creates a button to toggle the rulers in the drawing. + * + * @return The created button. + */ + private JToggleButton toggleRulersButton() { + final JToggleButton toggleButton = new JToggleButton(); + + toggleButton.setToolTipText( + labels.getString("drawingViewPlacardPanel.button_toggleRulers.tooltipText") + ); + + toggleButton.setIcon(ImageDirectory.getImageIcon("/toolbar/document-page-setup.16x16.png")); + + toggleButton.setMargin(new Insets(0, 0, 0, 0)); + toggleButton.setFocusable(false); + + return toggleButton; + } + + /** + * Creates a button to toglle the labels. + * + * @param view The DrawingView the button will belong to. + * @return The created button. + */ + private JToggleButton toggleLabelsButton(final OpenTCSDrawingView drawingView) { + final JToggleButton toggleButton = new JToggleButton(); + + toggleButton.setToolTipText( + labels.getString("drawingViewPlacardPanel.button_toggleLabels.tooltipText") + ); + + toggleButton.setIcon(ImageDirectory.getImageIcon("/menu/comment-add.16.png")); + + toggleButton.setMargin(new Insets(0, 0, 0, 0)); + toggleButton.setFocusable(false); + + toggleButton.addItemListener( + (ItemEvent event) -> drawingView.setLabelsVisible(toggleButton.isSelected()) + ); + + return toggleButton; + } + + /** + * Creates a button to toggle the blocks in the drawing. + * + * @param view The DrawingView the button will belong to. + * @return The created button. + */ + private JToggleButton toggleBlocksButton(final OpenTCSDrawingView drawingView) { + final JToggleButton toggleButton = new JToggleButton(); + + toggleButton.setToolTipText( + labels.getString("drawingViewPlacardPanel.button_toggleBlocks.tooltipText") + ); + + toggleButton.setIcon(ImageDirectory.getImageIcon("/tree/block.18x18.png")); + + toggleButton.setMargin(new Insets(0, 0, 0, 0)); + toggleButton.setFocusable(false); + + toggleButton.addItemListener(itemEvent -> { + drawingOptions.setBlocksVisible(toggleButton.isSelected()); + drawingView.drawingOptionsChanged(); + }); + + return toggleButton; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/DrawingViewScrollPane.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/DrawingViewScrollPane.java new file mode 100644 index 0000000..9fbd374 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/DrawingViewScrollPane.java @@ -0,0 +1,181 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.util.EventObject; +import javax.swing.BorderFactory; +import javax.swing.JScrollPane; +import javax.swing.JToggleButton; +import javax.swing.JViewport; +import javax.swing.ScrollPaneConstants; +import javax.swing.border.EmptyBorder; +import org.jhotdraw.gui.PlacardScrollPaneLayout; +import org.opentcs.guing.common.components.drawing.course.Origin; +import org.opentcs.guing.common.components.drawing.course.OriginChangeListener; + +/** + * A custom scroll pane to wrap an OpenTCSDrawingView. + */ +public class DrawingViewScrollPane + extends + JScrollPane + implements + OriginChangeListener { + + /** + * The drawing view. + */ + private final OpenTCSDrawingView drawingView; + /** + * The view's placard panel. + */ + private final DrawingViewPlacardPanel placardPanel; + /** + * Whether the rulers are currently visible or not. + */ + private boolean rulersVisible = true; + private Origin origin = new Origin(); + + /** + * Creates a new instance. + * + * @param drawingView The drawing view. + * @param placardPanel The view's placard panel. + */ + @SuppressWarnings("this-escape") + public DrawingViewScrollPane( + OpenTCSDrawingView drawingView, + DrawingViewPlacardPanel placardPanel + ) { + this.drawingView = requireNonNull(drawingView, "drawingView"); + this.placardPanel = requireNonNull(placardPanel, "placardPanel"); + + setViewport(new JViewport()); + getViewport().setView(drawingView.getComponent()); + setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS); + setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); + setViewportBorder(BorderFactory.createLineBorder(new Color(0, 0, 0))); + setLayout(new PlacardScrollPaneLayout()); + setBorder(new EmptyBorder(0, 0, 0, 0)); + + // Horizontal and vertical rulers + Ruler.Horizontal newHorizontalRuler = new Ruler.Horizontal(drawingView); + drawingView.addPropertyChangeListener(newHorizontalRuler); + newHorizontalRuler.setPreferredWidth(drawingView.getComponent().getWidth()); + Ruler.Vertical newVerticalRuler = new Ruler.Vertical(drawingView); + drawingView.addPropertyChangeListener(newVerticalRuler); + newVerticalRuler.setPreferredHeight(drawingView.getComponent().getHeight()); + setColumnHeaderView(newHorizontalRuler); + setRowHeaderView(newVerticalRuler); + + this.add(placardPanel, JScrollPane.LOWER_LEFT_CORNER); + + // Increase the preferred height of the horizontal scroll bar, which also affects the height of + // the corner in which the DrawingViewPlacardPanel is located. This ensures there is enough + // vertical space for all components in all graphical environments. (Without this, the vertical + // space is not sufficient for some components e.g. on Ubuntu 20.04.) + getHorizontalScrollBar().setPreferredSize(new Dimension(100, 34)); + + // Register handler for rulers toggle button. + placardPanel.getToggleRulersButton().addItemListener( + new RulersToggleListener(placardPanel.getToggleRulersButton()) + ); + placardPanel.getToggleRulersButton().setSelected(rulersVisible); + } + + public OpenTCSDrawingView getDrawingView() { + return drawingView; + } + + public DrawingViewPlacardPanel getPlacardPanel() { + return placardPanel; + } + + public Ruler.Horizontal getHorizontalRuler() { + return (Ruler.Horizontal) getColumnHeader().getView(); + } + + public Ruler.Vertical getVerticalRuler() { + return (Ruler.Vertical) getRowHeader().getView(); + } + + public boolean isRulersVisible() { + return rulersVisible; + } + + public void setRulersVisible(boolean visible) { + this.rulersVisible = visible; + if (visible) { + getHorizontalRuler().setVisible(true); + getHorizontalRuler().setPreferredWidth(getWidth()); + getVerticalRuler().setVisible(true); + getVerticalRuler().setPreferredHeight(getHeight()); + getPlacardPanel().getToggleRulersButton().setSelected(true); + } + else { + getHorizontalRuler().setVisible(false); + getHorizontalRuler().setPreferredSize(new Dimension(0, 0)); + getVerticalRuler().setVisible(false); + getVerticalRuler().setPreferredSize(new Dimension(0, 0)); + getPlacardPanel().getToggleRulersButton().setSelected(false); + } + } + + public void originChanged( + @Nonnull + Origin origin + ) { + requireNonNull(origin, "origin"); + if (origin == this.origin) { + return; + } + + this.origin.removeListener(getHorizontalRuler()); + this.origin.removeListener(getVerticalRuler()); + this.origin.removeListener(this); + this.origin = origin; + + origin.addListener(getHorizontalRuler()); + origin.addListener(getVerticalRuler()); + origin.addListener(this); + + // Notify the rulers directly. This is necessary to initialize/update the rulers scale when a + // model is created or loaded. + // Calling origin.notifyScaleChanged() would lead to all model elements being notified (loading + // times for bigger models would suffer). + getHorizontalRuler().originScaleChanged(new EventObject(origin)); + getVerticalRuler().originScaleChanged(new EventObject(origin)); + } + + @Override + public void originLocationChanged(EventObject evt) { + } + + @Override + public void originScaleChanged(EventObject evt) { + drawingView.getComponent().revalidate(); + } + + private class RulersToggleListener + implements + ItemListener { + + private final JToggleButton rulersButton; + + RulersToggleListener(JToggleButton rulersButton) { + this.rulersButton = requireNonNull(rulersButton, "rulersButton"); + } + + @Override + public void itemStateChanged(ItemEvent e) { + setRulersVisible(rulersButton.isSelected()); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/OffsetListener.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/OffsetListener.java new file mode 100644 index 0000000..059c20e --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/OffsetListener.java @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing; + +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; + +/** + * Triggers a (re-)initialization of the view's offset figures when notified + * about resize events. + */ +public class OffsetListener + implements + ComponentListener { + + /** + * The drawing view we're working with. + */ + private final OpenTCSDrawingEditor drawingEditor; + /** + * Initiales the offset figures once the view is resized (normally + * done when the window becomes visible). But the listener shouldn't + * listen to further resizing events. + * XXX get rid of this listener? + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param drawingEditor The drawing editor to call for (re-)initialization of its + * offset figures. + */ + public OffsetListener(OpenTCSDrawingEditor drawingEditor) { + this.drawingEditor = drawingEditor; + } + + @Override + public void componentResized(ComponentEvent e) { + if (!initialized) { + drawingEditor.initializeViewport(); + initialized = true; + } + } + + @Override + public void componentMoved(ComponentEvent e) { + } + + @Override + public void componentShown(ComponentEvent e) { + } + + @Override + public void componentHidden(ComponentEvent e) { + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/OpenTCSDrawingEditor.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/OpenTCSDrawingEditor.java new file mode 100644 index 0000000..d4f4992 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/OpenTCSDrawingEditor.java @@ -0,0 +1,380 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.awt.Rectangle; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javax.swing.ActionMap; +import javax.swing.InputMap; +import javax.swing.KeyStroke; +import org.jhotdraw.draw.DefaultDrawingEditor; +import org.jhotdraw.draw.Drawing; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.action.IncreaseHandleDetailLevelAction; +import org.jhotdraw.draw.event.CompositeFigureEvent; +import org.jhotdraw.draw.event.CompositeFigureListener; +import org.opentcs.guing.common.components.drawing.figures.LabeledFigure; +import org.opentcs.guing.common.components.drawing.figures.OffsetFigure; +import org.opentcs.guing.common.components.drawing.figures.TCSLabelFigure; +import org.opentcs.guing.common.event.DrawingEditorEvent; +import org.opentcs.guing.common.event.DrawingEditorListener; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.guing.common.util.CourseObjectFactory; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.draw.MoveAction; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.DeleteAction; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.SelectAllAction; +import org.opentcs.util.event.EventHandler; + +/** + * The DrawingEditor coordinates DrawingViews + * and the Drawing. + * It also offers methods to add specific unique figures to the + * Drawing. + */ +public class OpenTCSDrawingEditor + extends + DefaultDrawingEditor + implements + EventHandler { + + /** + * Width on the screen edge. + */ + private static final int MARGIN = 20; + /** + * A factory for course objects. + */ + private final CourseObjectFactory crsObjectFactory; + /** + * + */ + private final CompositeFigureEventHandler cmpFigureEvtHandler = new CompositeFigureEventHandler(); + /** + * Listens for figure selection, addition or removal events. + */ + private final List fDrawingEditorListeners = new ArrayList<>(); + /** + * The drawing that contains all figures. + */ + private Drawing fDrawing; + // These invisible figures are moved automatically to enlarge the drawing. + private OffsetFigure topOffsetFigure; + private OffsetFigure bottomOffsetFigure; + private OffsetFigure rightOffsetFigure; + private OffsetFigure leftOffsetFigure; + + /** + * Creates a new instance. + * + * @param crsObjFactory A factory for course objects. + */ + @Inject + public OpenTCSDrawingEditor(CourseObjectFactory crsObjFactory) { + this.crsObjectFactory = requireNonNull(crsObjFactory, "crsObjectFactory"); + } + + @Override + public void onEvent(Object event) { + } + + /** + * Creates the offset figures, sets their position to the current bounds of the + * view and repaints the ruler to fit the current grid. + */ + public void initializeViewport() { + initializeRuler(); + removeOffsetFigures(); + topOffsetFigure = crsObjectFactory.createOffsetFigure(); + bottomOffsetFigure = crsObjectFactory.createOffsetFigure(); + leftOffsetFigure = crsObjectFactory.createOffsetFigure(); + rightOffsetFigure = crsObjectFactory.createOffsetFigure(); + + OpenTCSDrawingView activeView = getActiveView(); + if (activeView == null) { + return; + } + + // Rectangle that contains all figures + Rectangle2D.Double drawingArea = getDrawing().getDrawingArea(); + // The visible rectangle + Rectangle visibleRect = activeView.getComponent().getVisibleRect(); + // The size of the invisible offset figures + double wFigure = topOffsetFigure.getBounds().width; + double hFigure = topOffsetFigure.getBounds().height; + + // When the drawing already contains figures + double xLeft = drawingArea.x; + double xRight = drawingArea.x + drawingArea.width; + double yTop = drawingArea.y; + double yBottom = drawingArea.y + drawingArea.height; + + // An empty drawing only contains the origin figure, which shall be on the bottom left. + if (visibleRect.width > drawingArea.width && visibleRect.height > drawingArea.height) { + xLeft = -drawingArea.width / 2 - MARGIN; + xRight = visibleRect.width + xLeft - (MARGIN + wFigure / 2); + yBottom = -(-drawingArea.height / 2 - MARGIN); + yTop = -(visibleRect.height - yBottom - (MARGIN + hFigure / 2)); + } + + double xCenter = (xLeft + xRight) / 2; + double yCenter = (yBottom + yTop) / 2; + + topOffsetFigure.setBounds(new Point2D.Double(xCenter, yTop), null); + bottomOffsetFigure.setBounds(new Point2D.Double(xCenter, yBottom), null); + leftOffsetFigure.setBounds(new Point2D.Double(xLeft, yCenter), null); + rightOffsetFigure.setBounds(new Point2D.Double(xRight, yCenter), null); + + getDrawing().add(topOffsetFigure); + getDrawing().add(bottomOffsetFigure); + getDrawing().add(leftOffsetFigure); + getDrawing().add(rightOffsetFigure); + +// validateViewTranslation(); + } + + protected CourseObjectFactory getCourseObjectFactory() { + return crsObjectFactory; + } + + private void initializeRuler() { + OpenTCSDrawingView activeView = getActiveView(); + DrawingViewScrollPane scrollPane + = (DrawingViewScrollPane) activeView.getComponent().getParent().getParent(); + Rectangle2D.Double drawingArea + = activeView.getDrawing().getDrawingArea(); + scrollPane.getHorizontalRuler().setPreferredWidth((int) drawingArea.width); + scrollPane.getVerticalRuler().setPreferredHeight((int) drawingArea.height); + } + + /** + * Removes the OffsetFigures off the drawing. + */ + private void removeOffsetFigures() { + if (getDrawing() == null) { + return; + } + + getDrawing().remove(topOffsetFigure); + getDrawing().remove(bottomOffsetFigure); + getDrawing().remove(leftOffsetFigure); + getDrawing().remove(rightOffsetFigure); + } + + /** + * Adds a listener. + * + * @param listener The listener. + */ + public void addDrawingEditorListener(DrawingEditorListener listener) { + requireNonNull(listener, "listener"); + fDrawingEditorListeners.add(listener); + } + + /** + * Removes a listener. + * + * @param listener The listener. + */ + public void removeDrawingEditorListener(DrawingEditorListener listener) { + requireNonNull(listener, "listener"); + fDrawingEditorListeners.remove(listener); + } + + /** + * Sets the system model. + * + * @param systemModel The model of the course. + */ + public void setSystemModel(SystemModel systemModel) { + setDrawing(systemModel.getDrawing()); + for (DrawingView drawView : getDrawingViews()) { + ((OpenTCSDrawingView) drawView).setBlocks( + systemModel.getMainFolder(SystemModel.FolderKey.BLOCKS) + ); + } + } + + public Drawing getDrawing() { + return fDrawing; + } + + public void setDrawing(Drawing drawing) { + requireNonNull(drawing, "drawing"); + + if (fDrawing != null) { + fDrawing.removeCompositeFigureListener(cmpFigureEvtHandler); + } + fDrawing = drawing; + fDrawing.addCompositeFigureListener(cmpFigureEvtHandler); + + // Also let the drawing views know about the new drawing. + for (DrawingView view : getDrawingViews()) { + view.setDrawing(drawing); + } + } + + @Override + public void add(DrawingView view) { + super.add(view); + view.setDrawing(fDrawing); + } + + @Override + public OpenTCSDrawingView getActiveView() { + return (OpenTCSDrawingView) super.getActiveView(); + } + + public Collection getAllViews() { + Collection result = new ArrayList<>(); + for (DrawingView view : getDrawingViews()) { + result.add((OpenTCSDrawingView) view); + } + return result; + } + + /** + * Notification of the DrawingView that a figure was added. + * + * @param figure The added figure. + */ + public void figureAdded(Figure figure) { + // Create the data model to a new point or location figure and show + // the name in the label + if (figure instanceof LabeledFigure) { + LabeledFigure labeledFigure = (LabeledFigure) figure; + + if (labeledFigure.getLabel() == null) { + // Create the label and add the figure to the data model + TCSLabelFigure label = new TCSLabelFigure(); + Point2D.Double pos = labeledFigure.getStartPoint(); + pos.x += label.getOffset().x; + pos.y += label.getOffset().y; + label.setBounds(pos, pos); + labeledFigure.setLabel(label); + } + } + + for (DrawingEditorListener listener : fDrawingEditorListeners) { + listener.figureAdded(new DrawingEditorEvent(this, figure)); + } + } + + /** + * Notification of the DrawingView that a figure was removed. + * + * @param figure The figure that was removed. + */ + public void figureRemoved(Figure figure) { + for (DrawingEditorListener listener : fDrawingEditorListeners) { + listener.figureRemoved(new DrawingEditorEvent(this, figure)); + } + } + + /** + * Notification of the DrawingView that figures were selected. + * + * @param figures The selected figures. + */ + public void figuresSelected(List
    figures) { + for (DrawingEditorListener listener : fDrawingEditorListeners) { + listener.figureSelected(new DrawingEditorEvent(this, figures)); + } + } + + /** + * Overrides the method from DefaultDrawingEditor to create a tool-specific + * input map. + * The implementation of this class creates an input map for the following + * action ID's: + * - DeleteAction + * - MoveAction.West, .East, .North, .South + * + * SelectAll, Cut, Copy, Paste are handled by SelectAllAction etc. + * + * @return The input map. + */ + @Override // DefaultDrawingEditor + protected InputMap createInputMap() { + InputMap m = new InputMap(); + + m.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), DeleteAction.ID); + m.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), DeleteAction.ID); + m.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), MoveAction.West.ID); + m.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), MoveAction.East.ID); + m.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), MoveAction.North.ID); + m.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), MoveAction.South.ID); + + m.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.ALT_DOWN_MASK), MoveAction.West.ID); + m.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.ALT_DOWN_MASK), MoveAction.East.ID); + m.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.ALT_DOWN_MASK), MoveAction.North.ID); + m.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.ALT_DOWN_MASK), MoveAction.South.ID); + + m.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.SHIFT_DOWN_MASK), MoveAction.West.ID); + m.put( + KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.SHIFT_DOWN_MASK), + MoveAction.East.ID + ); + m.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.SHIFT_DOWN_MASK), MoveAction.North.ID); + m.put( + KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.SHIFT_DOWN_MASK), + MoveAction.South.ID + ); + + m.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.CTRL_DOWN_MASK), MoveAction.West.ID); + m.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.CTRL_DOWN_MASK), MoveAction.East.ID); + m.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.CTRL_DOWN_MASK), MoveAction.North.ID); + m.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.CTRL_DOWN_MASK), MoveAction.South.ID); + + return m; + } + + @Override // DefaultDrawingEditor + protected ActionMap createActionMap() { + ActionMap m = new ActionMap(); + + m.put(DeleteAction.ID, new DeleteAction()); + m.put(SelectAllAction.ID, new SelectAllAction()); + m.put(IncreaseHandleDetailLevelAction.ID, new IncreaseHandleDetailLevelAction(this)); + + m.put(MoveAction.East.ID, new MoveAction.East(this)); + m.put(MoveAction.West.ID, new MoveAction.West(this)); + m.put(MoveAction.North.ID, new MoveAction.North(this)); + m.put(MoveAction.South.ID, new MoveAction.South(this)); + +// m.put(CutAction.ID, new CutAction()); +// m.put(CopyAction.ID, new CopyAction()); +// m.put(PasteAction.ID, new PasteAction()); + return m; + } + + private class CompositeFigureEventHandler + implements + CompositeFigureListener { + + /** + * Creates a new instance. + */ + CompositeFigureEventHandler() { + } + + @Override + public void figureAdded(CompositeFigureEvent e) { + OpenTCSDrawingEditor.this.figureAdded(e.getChildFigure()); + } + + @Override + public void figureRemoved(CompositeFigureEvent e) { + OpenTCSDrawingEditor.this.figureRemoved(e.getChildFigure()); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/OpenTCSDrawingView.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/OpenTCSDrawingView.java new file mode 100644 index 0000000..ea4a129 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/OpenTCSDrawingView.java @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing; + +import jakarta.annotation.Nonnull; +import java.awt.Point; +import java.io.File; +import java.util.Set; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.Figure; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.drawing.figures.BitmapFigure; +import org.opentcs.util.event.EventHandler; + +/** + */ +public interface OpenTCSDrawingView + extends + DrawingView, + EventHandler { + + boolean isLabelsVisible(); + + void setLabelsVisible(boolean newValue); + + /** + * Called when the drawing options have changed. + */ + void drawingOptionsChanged(); + + /** + * Returns if a given point on the screen is contained in this drawing view. + * + * @param p The reference point on the screen. + * @return Boolean if this point is contained. + */ + boolean containsPointOnScreen(Point p); + + /** + * Adds a background image to this drawing view. + * + * @param file The file with the image. + */ + void addBackgroundBitmap(File file); + + /** + * Adds a background image to this drawing view. + * + * @param bitmapFigure The figure containing the image. + */ + void addBackgroundBitmap(BitmapFigure bitmapFigure); + + /** + * Scales the view to a value so the whole model fits. + */ + void zoomViewToWindow(); + + /** + * Sets the elements of the blocks. + * + * @param blocks A ModelComponent which childs must be BlockModels. + */ + void setBlocks(ModelComponent blocks); + + /** + * Shows or hides the current route of a vehicle. + * + * @param vehicle The vehicle + * @param visible true to set it to visible, false otherwise. + */ + void displayDriveOrders(VehicleModel vehicle, boolean visible); + + /** + * Updates the figures of a block. + * + * @param block The block. + */ + void updateBlock(BlockModel block); + + /** + * Scrolls to the given figure. Normally called when the user clicks on a model component in the + * TreeView and wants to see the corresponding figure. + * + * @param figure The figure to be scrolled to. + */ + void scrollTo(Figure figure); + + /** + * Fixes the view on the vehicle and marks it and its destination with a colored circle. + * + * @param model The vehicle model. + */ + void followVehicle( + @Nonnull + VehicleModel model + ); + + /** + * Releases the view and stops following the current vehicle. + */ + void stopFollowVehicle(); + + /** + * Deletes the given model components from the drawing view. + * + * @param components The components to delete. + */ + void delete(Set components); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/Ruler.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/Ruler.java new file mode 100644 index 0000000..5c4448e --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/Ruler.java @@ -0,0 +1,397 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing; + +import static java.util.Objects.requireNonNull; + +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.geom.AffineTransform; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.EventObject; +import javax.swing.JComponent; +import javax.swing.SwingUtilities; +import org.jhotdraw.draw.DrawingView; +import org.opentcs.guing.common.components.drawing.course.Origin; +import org.opentcs.guing.common.components.drawing.course.OriginChangeListener; + +/** + * A ruler. + */ +public abstract class Ruler + extends + JComponent + implements + PropertyChangeListener, + OriginChangeListener { + + /** + * Size of the rulers (height of the horizontal ruler, width of the vertical). + */ + private static final int SIZE = 25; + /** + * The standard translation of the drawing view. Not sure though why + * it is -12. + */ + private static final int STANDARD_TRANSLATION = -12; + /** + * The DrawingView. + */ + protected final DrawingView drawingView; + /** + * The current scale factor. + */ + protected double scaleFactor = 1.0; + /** + * The scale factor for the horizontal ruler. + */ + protected double horizontalRulerScale = Origin.DEFAULT_SCALE; + /** + * The scale factor for the vertical ruler. + */ + protected double verticalRulerScale = Origin.DEFAULT_SCALE; + + /** + * Creates a new instance. + * + * @param drawingView The drawing view. + */ + private Ruler(DrawingView drawingView) { + this.drawingView = requireNonNull(drawingView, "drawingView"); + + } + + /** + * A horizontal ruler. + */ + public static class Horizontal + extends + Ruler { + + /** + * Creates a new instance. + * + * @param drawingView The drawing view. + */ + public Horizontal(DrawingView drawingView) { + super(drawingView); + } + + /** + * Sets a new width of the ruler and repaints it. + * + * @param preferredWidth The new width. + */ + public void setPreferredWidth(int preferredWidth) { + setPreferredSize(new Dimension(preferredWidth, SIZE)); + repaint(); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + + Rectangle drawHere = g.getClipBounds(); + Point translation = new Point( + (int) -drawingView.getDrawingToViewTransform().getTranslateX(), + (int) -drawingView.getDrawingToViewTransform().getTranslateY() + ); + // If we scroll right, the translation isn't incremented by default. + // We use the translation of the visible rectangle instead. + int visibleRectX = drawingView.getComponent().getVisibleRect().x + STANDARD_TRANSLATION; + if (STANDARD_TRANSLATION == translation.x) { + translation.x = visibleRectX; + } + + Graphics2D g2d = (Graphics2D) g; + g2d.setFont(new Font("Arial", Font.PLAIN, 10)); + // i translated + int translated; + // i normalized to decimal + int draw; + int drawOld = 0; + // draw translated + int drawTranslated; + String translatedAsString; + String lastIndex; + + // base line + g2d.drawLine( + 0, SIZE - 1, + getWidth(), SIZE - 1 + ); + + for (int i = drawHere.x; i < getWidth(); i += 10) { + translated = translateValue(i, translation); + translatedAsString = Integer.toString(translated); + lastIndex = translatedAsString.substring(translatedAsString.length() - 1); + + int decimal = Integer.parseInt(lastIndex); + { + // These steps are neccessary to guarantee lines are drawn + // at every pixel. It always rounds i to a decimal, so the modulo + // operators work + draw = i; + + if (translated < 0) { + draw += decimal; + } + else { + draw -= decimal; + } + + drawTranslated = translateValue(draw, translation); + // draw has to be incremented by 1, otherwise the drawn lines + // are wrong by 1 pixel + draw++; + } + + if (drawTranslated % (10 * scaleFactor) == 0) { + g2d.drawLine(draw, SIZE - 1, draw, SIZE - 4); + } + + if (drawTranslated % (50 * scaleFactor) == 0) { + g2d.drawLine(draw, SIZE - 1, draw, SIZE - 7); + } + + if (drawTranslated % (100 * scaleFactor) == 0) { + g2d.drawLine(draw, SIZE - 1, draw, SIZE - 11); + int value = (int) (drawTranslated / scaleFactor) * (int) horizontalRulerScale; + String textValue = Integer.toString(value); + if (scaleFactor < 0.06) { + if (value % 5000 == 0) { + g2d.drawString(textValue, value == 0 ? draw - 2 : draw - 8, 9); + } + } + else if ((draw - drawOld) < 31) { + if (value % 500 == 0) { + g2d.drawString(textValue, value == 0 ? draw - 2 : draw - 8, 9); + } + } + else { + g2d.drawString(textValue, value == 0 ? draw - 2 : draw - 8, 9); + } + + drawOld = draw; + } + } + + } + + /** + * Returns a translated value, considering current translation of the view. + * + * @param i The value. + * @return The translated value. + */ + private int translateValue(int i, Point translation) { + if (translation.x < 0) { + return i + translation.x; + } + else { + return i; + } + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getPropertyName().equals("scaleFactor")) { + scaleFactor = (double) evt.getNewValue(); + SwingUtilities.invokeLater(new Runnable() { + + @Override + public void run() { + setPreferredWidth(drawingView.getComponent().getWidth()); + } + }); + + } + } + + @Override + public void originLocationChanged(EventObject evt) { + } + + @Override + public void originScaleChanged(EventObject evt) { + if (evt.getSource() instanceof Origin) { + Origin origin = (Origin) evt.getSource(); + SwingUtilities.invokeLater(() -> { + horizontalRulerScale = origin.getScaleX(); + repaint(); + }); + } + } + } + + /** + * A vertical ruler. + */ + public static class Vertical + extends + Ruler { + + /** + * Creates a new instance. + * + * @param drawingView The drawing view. + */ + public Vertical(DrawingView drawingView) { + super(drawingView); + } + + /** + * Sets a new height of the ruler and repaints it. + * + * @param preferredHeight The new height. + */ + public void setPreferredHeight(int preferredHeight) { + setPreferredSize(new Dimension(SIZE, preferredHeight)); + repaint(); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + + Rectangle drawHere = g.getClipBounds(); + Point translation = new Point( + (int) -drawingView.getDrawingToViewTransform().getTranslateX(), + (int) -drawingView.getDrawingToViewTransform().getTranslateY() + ); + // If we scroll downwards, the translation isn't incremented by default. + // We use the translation of the visible rectangle instead. + if (translation.y == STANDARD_TRANSLATION) { + translation.y = drawingView.getComponent().getVisibleRect().y + STANDARD_TRANSLATION; + } + + Graphics2D g2d = (Graphics2D) g; + g2d.setFont(new Font("Arial", Font.PLAIN, 10)); + // i translated + int translated; + // i normalized to decimal + int draw; + int drawOld = 0; + // draw translated + int drawTranslated; + String translatedAsString; + String lastIndex; + + // base line + g2d.drawLine( + SIZE - 1, 0, + SIZE - 1, getHeight() + ); + + // Rotate the font for vertical axis + AffineTransform fontAT = new AffineTransform(); + fontAT.rotate(270 * java.lang.Math.PI / 180); + Font font = g2d.getFont().deriveFont(fontAT); + g2d.setFont(font); + + for (int i = drawHere.y; i < getHeight(); i += 10) { + translated = translateValue(i, translation); + translatedAsString = Integer.toString(translated); + lastIndex = translatedAsString.substring(translatedAsString.length() - 1); + int decimal = Integer.parseInt(lastIndex); + + { + // These steps are neccessary to guarantee lines are drawn + // at every pixel. It always rounds i to a decimal, so the modulo + // operators work + draw = i; + + if (translated < 0) { + draw += decimal; + } + else { + draw -= decimal; + } + + drawTranslated = translateValue(draw, translation); + draw++; + } + + if (drawTranslated % (10 * scaleFactor) == 0) { + g2d.drawLine(SIZE - 1, draw, SIZE - 4, draw); + } + + if (drawTranslated % (50 * scaleFactor) == 0) { + g2d.drawLine(SIZE - 1, draw, SIZE - 7, draw); + } + + if (drawTranslated % (100 * scaleFactor) == 0) { + g2d.drawLine(SIZE - 1, draw, SIZE - 11, draw); + int value = -(int) (drawTranslated / scaleFactor) * (int) verticalRulerScale; + String textValue = Integer.toString(value); + + if (scaleFactor < 0.06) { + if (value % 5000 == 0) { + g2d.drawString(textValue, 9, value == 0 ? draw + 2 : draw + 8); + } + } + else if ((draw - drawOld) < 31) { + if (value % 500 == 0) { + g2d.drawString(textValue, 9, value == 0 ? draw + 2 : draw + 8); + } + } + else { + g2d.drawString(textValue, 9, value == 0 ? draw + 2 : draw + 8); + } + + drawOld = draw; + } + } + } + + /** + * Returns a translated value, considering current translation of the view. + * + * @param i The value. + * @return The translated value. + */ + private int translateValue(int i, Point translation) { + if (translation.y < 0) { + return i + translation.y; + } + else { + return i; + } + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getPropertyName().equals("scaleFactor")) { + scaleFactor = (double) evt.getNewValue(); + SwingUtilities.invokeLater(new Runnable() { + + @Override + public void run() { + setPreferredHeight(drawingView.getComponent().getHeight()); + } + }); + + } + } + + @Override + public void originLocationChanged(EventObject evt) { + } + + @Override + public void originScaleChanged(EventObject evt) { + if (evt.getSource() instanceof Origin) { + Origin origin = (Origin) evt.getSource(); + SwingUtilities.invokeLater(() -> { + verticalRulerScale = origin.getScaleY(); + repaint(); + }); + } + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/Strokes.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/Strokes.java new file mode 100644 index 0000000..d2599aa --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/Strokes.java @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing; + +import java.awt.BasicStroke; +import java.awt.Stroke; + +/** + * Strokes used in the drawing. + */ +public class Strokes { + + /** + * Decoration of paths, points and locations that are part of a block. + */ + public static final Stroke BLOCK_ELEMENT = new BasicStroke(4.0f); + /** + * Decoration of paths that are part of a transport order. + */ + public static final Stroke PATH_ON_ROUTE + = new BasicStroke( + 6.0f, + BasicStroke.CAP_BUTT, + BasicStroke.JOIN_MITER, + 10.0f, + new float[]{10.0f, 5.0f}, + 0.0f + ); + /** + * Decoration of paths that are part of a withdrawn transport order. + */ + public static final Stroke PATH_ON_WITHDRAWN_ROUTE + = new BasicStroke( + 6.0f, + BasicStroke.CAP_BUTT, + BasicStroke.JOIN_MITER, + 10.0f, + new float[]{8.0f, 4.0f, 2.0f, 4.0f}, + 0.0f + ); + + /** + * Prevents instantiation of this utility class. + */ + private Strokes() { + } + +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/ZoomItem.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/ZoomItem.java new file mode 100644 index 0000000..de27c2a --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/ZoomItem.java @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing; + +/** + * An item to show in a combo box. + */ +public class ZoomItem { + + private final double scaleFactor; + + public ZoomItem(double scaleFactor) { + this.scaleFactor = scaleFactor; + } + + public double getScaleFactor() { + return scaleFactor; + } + + @Override + public String toString() { + return String.format("%d %%", (int) (scaleFactor * 100)); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/ZoomPoint.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/ZoomPoint.java new file mode 100644 index 0000000..4285b6c --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/ZoomPoint.java @@ -0,0 +1,145 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing; + +import java.awt.Point; +import java.awt.geom.Point2D; +import java.io.Serializable; + +/** + * Represents an exact point that won't change when zooming the model. + */ +public class ZoomPoint + implements + Serializable { + + /** + * The x position with a scale of 1. + */ + protected double fX; + /** + * The y position with a scale of 1. + */ + protected double fY; + /** + * The current scale. + */ + protected double fScale; + + /** + * Creates a new instance of ZoomPoint + */ + public ZoomPoint() { + this(0, 0); + } + + /** + * Creates a new instance. + * + * @param x The x position for this point. + * @param y The y position for this point. + */ + public ZoomPoint(double x, double y) { + this(x, y, 1.0); + } + + /** + * Creates a new instance. + * + * @param x The x position for this point. + * @param y The y position for this point. + * @param scale The current scale. + */ + public ZoomPoint(double x, double y, double scale) { + fX = x / scale; + fY = y / scale; + fScale = scale; + } + + /** + * Returns the current scale. + */ + public double scale() { + return fScale; + } + + /** + * Sets the x position. + * + * @param x the x position. + */ + public void setX(double x) { + fX = x; + } + + /** + * Sets the y position. + * + * @param y the y position. + */ + public void setY(double y) { + fY = y; + } + + /** + * Returns the x position. + * + * @return the x position. + */ + public double getX() { + return fX; + } + + /** + * Returns the y position. + * + * @return the y position. + */ + public double getY() { + return fY; + } + + /** + * Returns a point with the position. + * + * @return a point with the position. + */ + public Point getPixelLocation() { + int x = (int) (getX() * scale()); + int y = (int) (getY() * scale()); + + return new Point(x, y); + } + + /** + * Returns the exact position of the point in pixels with the current zoom level. + * + * @return the exact position. + */ + public Point2D getPixelLocationExactly() { + double x = getX() * scale(); + double y = getY() * scale(); + + return new Point2D.Double(x, y); + } + + /** + * Event that the scale has changed. + * + * @param scale The new scale factor. + */ + public void scaleChanged(double scale) { + fScale = scale; + } + + /** + * Event that the point has been moved by the user. + * + * @param x The x-coordinate in Pixel. + * @param y The y-coordinate in Pixel. + */ + public void movedByMouse(int x, int y) { + fX = x / scale(); + fY = y / scale(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/course/CoordinateBasedDrawingMethod.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/course/CoordinateBasedDrawingMethod.java new file mode 100644 index 0000000..6635152 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/course/CoordinateBasedDrawingMethod.java @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.course; + +/** + * A drawing method where the position of a figure and the real position are in relation + * to each other. + */ +public class CoordinateBasedDrawingMethod + implements + DrawingMethod { + + /** + * The origin point. + */ + protected Origin fOrigin; + + /** + * Creates a new instance. + */ + public CoordinateBasedDrawingMethod() { + fOrigin = new Origin(); + } + + @Override + public Origin getOrigin() { + return fOrigin; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/course/CoordinateSystem.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/course/CoordinateSystem.java new file mode 100644 index 0000000..b67effa --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/course/CoordinateSystem.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.course; + +import java.awt.Point; +import java.awt.geom.Point2D; +import java.io.Serializable; + +/** + * A strategy that can translate pixel coordinates to real coordinates. + */ +public interface CoordinateSystem + extends + Serializable { + + /** + * Translates the real coordinate into a pixel coordinate. + * + * @param refPointLocation The current position of the reference point. + * @param realValue The real position to translate. + * @param relationX The amount of mm for one pixel in the x axis. + * @param relationY The amount of mm for one pixel in the y axis. + * @return A point with the pixel coordinates. + */ + Point2D toPixel(Point refPointLocation, Point2D realValue, double relationX, double relationY); + + /** + * Translates the pixel coordinate into a real coordinate. + * + * @param refPointLocation The current position of the reference point. + * @param pixelValue The pixel coordinate position to translate. + * @param relationX The amount of mm for one pixel in the x axis. + * @param relationY The amount of mm for one pixel in the y axis. + * @return A point with the real position. + */ + Point2D toReal(Point refPointLocation, Point pixelValue, double relationX, double relationY); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/course/DrawingMethod.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/course/DrawingMethod.java new file mode 100644 index 0000000..00b905f --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/course/DrawingMethod.java @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.course; + +/** + * An interface for drawing methods. Possible drawing methods are: + *

    + *

    • symbolic: No relation between the real position and the position of the figure. + *
    • coordinate based: The position of the figure is the exact real position.
    + */ +public interface DrawingMethod { + + /** + * Returns the origin point. + * + * @return the origin point. + */ + Origin getOrigin(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/course/NormalCoordinateSystem.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/course/NormalCoordinateSystem.java new file mode 100644 index 0000000..9751f98 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/course/NormalCoordinateSystem.java @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.course; + +import java.awt.Point; +import java.awt.geom.Point2D; + +/** + * A coordinate system strategy that converts pixel coordinates into real coordinates. + */ +public class NormalCoordinateSystem + implements + CoordinateSystem { + + /** + * Creates a new instance. + */ + public NormalCoordinateSystem() { + } + + @Override + public Point2D toPixel(Point refPointLocation, Point2D realValue, double scaleX, double scaleY) { + double xPixel = realValue.getX() / scaleX; + double yPixel = realValue.getY() / scaleY; + return new Point2D.Double(refPointLocation.x + xPixel, -(refPointLocation.y + yPixel)); + } + + @Override + public Point2D toReal(Point refPointLocation, Point pixelValue, double scaleX, double scaleY) { + int xDiff = pixelValue.x - refPointLocation.x; + int yDiff = pixelValue.y - refPointLocation.y; + return new Point2D.Double(scaleX * xDiff, -scaleY * yDiff); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/course/Origin.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/course/Origin.java new file mode 100644 index 0000000..7c1b592 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/course/Origin.java @@ -0,0 +1,266 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.course; + +import java.awt.Point; +import java.awt.geom.Point2D; +import java.util.EventObject; +import java.util.HashSet; +import java.util.Set; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.common.components.drawing.figures.OriginFigure; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The origin of the coordinate system. Represents the current scale, coordinate system and + * position of the origin on the screen. + */ +public final class Origin { + + /** + * Scale (in mm per pixel) of the layout. + */ + public static final double DEFAULT_SCALE = 50.0; + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(Origin.class); + /** + * Amount of mm to equal one pixel on screen in horizontal direction. + */ + private double fScaleX = DEFAULT_SCALE; + /** + * Amount of mm to equal one pixel on screen in horizontal direction. + */ + private double fScaleY = DEFAULT_SCALE; + /** + * Current position in pixels. + */ + private Point fPosition; + /** + * The coordinate system. + */ + private CoordinateSystem fCoordinateSystem; + /** + * List of {@link OriginChangeListener}. + */ + private final Set fListeners = new HashSet<>(); + /** + * Graphical figure to represent the origin. + */ + private final OriginFigure fFigure = new OriginFigure(); + + /** + * Creates a new instance. + */ + public Origin() { + setCoordinateSystem(new NormalCoordinateSystem()); + fFigure.setModel(this); + } + + /** + * Set the scale in millimeter per pixel. + */ + public void setScale(double scaleX, double scaleY) { + if (fScaleX == scaleX && fScaleY == scaleY) { + return; + } + fScaleX = scaleX; + fScaleY = scaleY; + notifyScaleChanged(); + } + + /** + * Return the millimeter per pixel in horizontal direction. + * + * @return the millimeter per pixel in horizontal direction. + */ + public double getScaleX() { + return fScaleX; + } + + /** + * Return the millimeter per pixel in vertical direction. + * + * @return the millimeter per pixel in vertical direction. + */ + public double getScaleY() { + return fScaleY; + } + + /** + * Set the coordinate system. + */ + public void setCoordinateSystem(CoordinateSystem coordinateSystem) { + fCoordinateSystem = coordinateSystem; + notifyLocationChanged(); + } + + /** + * Set the position of the origin. + * + * @param position the position of the origin. + */ + public void setPosition(Point position) { + fPosition = position; + } + + /** + * Return the current position of the origin. + * + * @return the current position of the origin. + */ + public Point getPosition() { + return fPosition; + } + + /** + * Translates the real coordinate into pixel coordinates. + * + * @param xReal The real x position. + * @param yReal The real y position. + * @return A point with the pixel position. + */ + public Point calculatePixelPosition(LengthProperty xReal, LengthProperty yReal) { + Point2D pixelExact = calculatePixelPositionExactly(xReal, yReal); + + return new Point((int) pixelExact.getX(), (int) pixelExact.getY()); + } + + /** + * Translates the real coordinate into pixel coordinates with double precision. + * + * @param xReal The real x position. + * @param yReal The real y position. + * @return A point with the pixel position with double precision. + */ + public Point2D calculatePixelPositionExactly(LengthProperty xReal, LengthProperty yReal) { + Point2D realPosition = new Point2D.Double( + xReal.getValueByUnit(LengthProperty.Unit.MM), + yReal.getValueByUnit(LengthProperty.Unit.MM) + ); + + Point2D pixelPosition = fCoordinateSystem.toPixel(fPosition, realPosition, fScaleX, fScaleY); + + return pixelPosition; + } + + /** + * Translates the real coordinate into pixel coordinates with double precision from + * string properties. + * + * @param xReal The real x position. + * @param yReal The real y position. + * @return A point with the pixel position with double precision. + */ + public Point2D calculatePixelPositionExactly(StringProperty xReal, StringProperty yReal) { + try { + double xPos = Double.parseDouble(xReal.getText()); + double yPos = Double.parseDouble(yReal.getText()); + Point2D realPosition = new Point2D.Double(xPos, yPos); + Point2D pixelPosition = fCoordinateSystem.toPixel(fPosition, realPosition, fScaleX, fScaleY); + + return pixelPosition; + } + catch (NumberFormatException e) { + LOG.info("Couldn't parse layout coordinates", e); + return new Point2D.Double(); + } + } + + /** + * Translates a pixel position into a real position and write to the length properties. + * + * + * @param pixelPosition The pixel position to convert. + * @param xReal The length property to write the x position to. + * @param yReal The length property to write the y position to. + * @return A point with the pixel position with double precision. + */ + public Point2D calculateRealPosition( + Point pixelPosition, LengthProperty xReal, + LengthProperty yReal + ) { + Point2D realPosition = fCoordinateSystem.toReal(fPosition, pixelPosition, fScaleX, fScaleY); + + LengthProperty.Unit unitX = xReal.getUnit(); + LengthProperty.Unit unitY = yReal.getUnit(); + + xReal.setValueAndUnit((int) realPosition.getX(), LengthProperty.Unit.MM); + yReal.setValueAndUnit((int) realPosition.getY(), LengthProperty.Unit.MM); + xReal.convertTo(unitX); + yReal.convertTo(unitY); + + return realPosition; + } + + /** + * Translates a pixel position onto a real position. + * + * @param pixelPosition The pixel position to convert. + * @return A point with the pixel position with double precision. + */ + public Point2D calculateRealPosition(Point pixelPosition) { + Point2D realPosition = fCoordinateSystem.toReal(fPosition, pixelPosition, fScaleX, fScaleY); + + return realPosition; + } + + /** + * Add an origin change listener. + * + * @param l The origin change listener to add. + */ + public void addListener(OriginChangeListener l) { + fListeners.add(l); + } + + /** + * Remove an origin change listener. + * + * @param l The origin change listener to remove. + */ + public void removeListener(OriginChangeListener l) { + fListeners.remove(l); + } + + /** + * + * Tests whether a specific origin change listener is registerd. + * + * @param l The origin change listener to test for. + * @return true , if the listener is registerd. + */ + public boolean containsListener(OriginChangeListener l) { + return fListeners.contains(l); + } + + /** + * Notifies all registered listeners that the position of the origin has changed. + */ + public void notifyLocationChanged() { + for (OriginChangeListener l : fListeners) { + l.originLocationChanged(new EventObject(this)); + } + } + + /** + * Notifies all registered listeners that the scale has changed. + */ + public void notifyScaleChanged() { + for (OriginChangeListener l : fListeners) { + l.originScaleChanged(new EventObject(this)); + } + } + + /** + * Return the graphical representation of the origin. + * + * @return The graphical representation of the origin. + */ + public OriginFigure getFigure() { + return fFigure; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/course/OriginChangeListener.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/course/OriginChangeListener.java new file mode 100644 index 0000000..7ff47eb --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/course/OriginChangeListener.java @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.course; + +import java.util.EventObject; + +/** + * Interface for classes that want to be notified about origin position changes + * and origin scale changes. + */ +public interface OriginChangeListener { + + /** + * Event that the position of the origin has changed. + * + * @param evt event that the position has changed. + */ + void originLocationChanged(EventObject evt); + + /** + * Event that the scale of the origin has changed. + * + * @param evt event that the scale of the origin has changed. + */ + void originScaleChanged(EventObject evt); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/BitmapFigure.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/BitmapFigure.java new file mode 100644 index 0000000..591ec92 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/BitmapFigure.java @@ -0,0 +1,183 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures; + +import static java.awt.image.ImageObserver.ABORT; +import static java.awt.image.ImageObserver.ALLBITS; +import static java.awt.image.ImageObserver.FRAMEBITS; + +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.ImageObserver; +import java.io.File; +import java.io.IOException; +import javax.imageio.ImageIO; +import org.jhotdraw.draw.AbstractAttributedDecoratedFigure; +import org.opentcs.guing.common.components.drawing.ZoomPoint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A Figure displaying a bitmap. + */ +public class BitmapFigure + extends + AbstractAttributedDecoratedFigure + implements + ImageObserver { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(BitmapFigure.class); + /** + * The image to be displayed. + */ + private BufferedImage image; + /** + * The enclosing rectangle. + */ + private Rectangle fDisplayBox; + /** + * The exact position of the figure's center. + */ + private ZoomPoint fZoomPoint; + /** + * Flag, if this figures has been removed from the drawing due to + * an other view is active or if it visible. + */ + private boolean temporarilyRemoved = false; + /** + * Path of the image. + */ + private String imagePath; + + @SuppressWarnings("this-escape") + public BitmapFigure(File file) { + try { + image = ImageIO.read(file); + imagePath = file.getPath(); + if (image == null) { + LOG.error("Couldn't open image file at" + file.getPath()); + fDisplayBox = new Rectangle(0, 0, 0, 0); + fZoomPoint = new ZoomPoint(0, 0); + requestRemove(); + return; + } + fDisplayBox = new Rectangle(image.getWidth(), image.getHeight()); + fZoomPoint = new ZoomPoint(0.5 * image.getWidth(), 0.5 * image.getHeight()); + } + catch (IOException ex) { + LOG.error("", ex); + requestRemove(); + } + } + + public String getImagePath() { + return imagePath; + } + + public boolean isTemporarilyRemoved() { + return temporarilyRemoved; + } + + public void setTemporarilyRemoved(boolean temporarilyRemoved) { + this.temporarilyRemoved = temporarilyRemoved; + } + + public Rectangle displayBox() { + return new Rectangle( + fDisplayBox.x, fDisplayBox.y, + fDisplayBox.width, fDisplayBox.height + ); + } + + public void setDisplayBox(Rectangle displayBox) { + fDisplayBox = displayBox; + } + + @Override // AbstractFigure + public void setBounds(Point2D.Double anchor, Point2D.Double lead) { + //resize + if (lead != null) { + //anchor is upper left, lead lower right + fDisplayBox.width = (int) (lead.x - anchor.x); + fDisplayBox.height = (int) (lead.y - anchor.y); + } + else { + fZoomPoint.setX(anchor.x); + fZoomPoint.setY(anchor.y); + fDisplayBox.x = (int) (anchor.x - 0.5 * fDisplayBox.width); + fDisplayBox.y = (int) (anchor.y - 0.5 * fDisplayBox.height); + } + } + + @Override // ImageObserver + public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { + if ((infoflags & (FRAMEBITS | ALLBITS)) != 0) { + invalidate(); + } + + return (infoflags & (ALLBITS | ABORT)) == 0; + } + + @Override + protected boolean figureContains(Point2D.Double p) { + return fDisplayBox.contains(p); + } + + @Override + protected void drawFill(Graphics2D g) { + if (image != null) { + Rectangle r = displayBox(); + g.drawImage(image, r.x, r.y, r.width, r.height, this); + } + } + + @Override + protected void drawStroke(Graphics2D g) { + Rectangle r = displayBox(); + g.drawRect(r.x, r.y, r.width - 1, r.height - 1); + } + + @Override + public Rectangle2D.Double getBounds() { + Rectangle2D r2 = fDisplayBox.getBounds2D(); + Rectangle2D.Double r2d = new Rectangle2D.Double(); + r2d.setRect(r2); + + return r2d; + } + + @Override + public Object getTransformRestoreData() { + return fDisplayBox.clone(); + } + + @Override + public void restoreTransformTo(Object restoreData) { + Rectangle r = (Rectangle) restoreData; + fDisplayBox.x = r.x; + fDisplayBox.y = r.y; + fDisplayBox.width = r.width; + fDisplayBox.height = r.height; + fZoomPoint.setX(r.x + 0.5 * r.width); + fZoomPoint.setY(r.y + 0.5 * r.height); + } + + @Override + public void transform(AffineTransform tx) { + Point2D center = fZoomPoint.getPixelLocationExactly(); + setBounds((Point2D.Double) tx.transform(center, center), null); + } + + public void setScaleFactor(double oldValue, double newValue) { + fDisplayBox.width = (int) (fDisplayBox.width / oldValue * newValue); + fDisplayBox.height = (int) (fDisplayBox.height / oldValue * newValue); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/FigureConstants.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/FigureConstants.java new file mode 100644 index 0000000..4078e74 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/FigureConstants.java @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures; + +import org.jhotdraw.draw.AttributeKey; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.common.components.drawing.course.Origin; + +/** + * Constants that are relevant to figures. + */ +public interface FigureConstants { + + /** + * Key for figures to access their models. + */ + AttributeKey MODEL = new AttributeKey<>("Model", ModelComponent.class); + /** + * Key for figures to access the origin. + */ + AttributeKey ORIGIN = new AttributeKey<>("Origin", Origin.class); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/FigureFactory.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/FigureFactory.java new file mode 100644 index 0000000..f5f25d0 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/FigureFactory.java @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures; + +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; + +/** + */ +public interface FigureFactory { + + PointFigure createPointFigure(PointModel model); + + LabeledPointFigure createLabeledPointFigure(PointFigure figure); + + LocationFigure createLocationFigure(LocationModel model); + + LabeledLocationFigure createLabeledLocationFigure(LocationFigure figure); + + PathConnection createPathConnection(PathModel model); + + LinkConnection createLinkConnection(LinkModel model); + + OffsetFigure createOffsetFigure(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/FigureOrdinals.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/FigureOrdinals.java new file mode 100644 index 0000000..7292c9c --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/FigureOrdinals.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures; + +/** + * Defines fixed ordinals for some figures. + */ +public interface FigureOrdinals { + + /** + * The layer ordinal to be used for the origin figure. + * Note: Be cautious with this value. The ordinal for the default layer is 0 . So a value + * of -1 for the origin figure should be enough. We can't really use e.g. Integer.MIN_VALUE as + * this leads to unexpected behavior and exceptions when moving layers in the Model Editor + * application (probably caused by FigureLayerComparator). + */ + int ORIGIN_FIGURE_ORDINAL = -1; + /** + * The layer ordinal to be used for vehicle figures. + * Note: Be cautious with this value. We want vehicles to be on the uppermost layer. We can't + * really use e.g. Integer.MAX_VALUE as this leads to unexpected behavior when showing/hiding + * layers in the Operations Desk application (probably caused by FigureLayerComparator). + */ + int VEHICLE_FIGURE_ORDINAL = 1000000; +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/LabeledFigure.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/LabeledFigure.java new file mode 100644 index 0000000..08b2d7e --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/LabeledFigure.java @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures; + +import java.awt.Shape; +import java.awt.event.MouseEvent; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EventObject; +import org.jhotdraw.draw.AttributeKey; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.GraphicalCompositeFigure; +import org.jhotdraw.draw.handle.BoundsOutlineHandle; +import org.jhotdraw.draw.handle.DragHandle; +import org.jhotdraw.draw.handle.Handle; +import org.jhotdraw.draw.handle.MoveHandle; +import org.jhotdraw.draw.handle.ResizeHandleKit; +import org.opentcs.guing.base.components.properties.event.AttributesChangeListener; +import org.opentcs.guing.common.components.drawing.course.OriginChangeListener; + +/** + * A figure that is labeled by another figure. + */ +public abstract class LabeledFigure + extends + GraphicalCompositeFigure + implements + AttributesChangeListener, + OriginChangeListener { + + /** + * The figure of the label of this labeled figure. + */ + private TCSLabelFigure fLabel; + + /** + * Creates a new instance. + */ + public LabeledFigure() { + } + + public void setLabel(TCSLabelFigure label) { + add(0, label); // Allow only one label for each figure + addFigureListener(label); + label.setParent(this); + fLabel = label; + } + + public TCSLabelFigure getLabel() { + return fLabel; + } + + /** + * Sets the visibility of the label. + * + * @param visible Indicates whether the label should be visible or not. + */ + public void setLabelVisible(boolean visible) { + fLabel.setLabelVisible(visible); + } + + public abstract Shape getShape(); + + @Override + public TCSFigure getPresentationFigure() { + return (TCSFigure) super.getPresentationFigure(); + } + + @Override + public boolean handleMouseClick(Point2D.Double p, MouseEvent evt, DrawingView view) { + boolean ret = getPresentationFigure().handleMouseClick(p, evt, view); + + return ret; + } + + @Override + public void changed() { + super.changed(); + updateModel(); + } + + @Override + public Collection createHandles(int detailLevel) { + Collection handles = new ArrayList<>(); + + switch (detailLevel) { + case -1: // Mouse Moved + handles.add(new BoundsOutlineHandle(getPresentationFigure(), false, true)); + break; + + case 0: // Mouse clicked + MoveHandle.addMoveHandles(this, handles); + for (Figure child : getChildren()) { + MoveHandle.addMoveHandles(child, handles); + handles.add(new DragHandle(child)); + } + + break; + + case 1: // Double-Click + ResizeHandleKit.addResizeHandles(this, handles); + break; + + default: + break; + } + + return handles; + } + + @Override + public void set(AttributeKey key, T newValue) { + super.set(key, newValue); + + if (fLabel != null) { + fLabel.set(key, newValue); + } + } + + @Override + public void setBounds(Point2D.Double anchor, Point2D.Double lead) { + basicSetPresentationFigureBounds(anchor, anchor); + + if (fLabel != null) { + Point2D.Double p = getStartPoint(); + p.x += fLabel.getOffset().x; + p.y += fLabel.getOffset().y; + fLabel.setBounds(p, p); + } + } + + @Override + public void originLocationChanged(EventObject event) { + updateModel(); + } + + @Override + public void originScaleChanged(EventObject event) { + scaleModel(event); + } + + public abstract void updateModel(); + + /** + * Scales the model coodinates accodring to changes to the layout scale. + * + * @param event The event containing the layout scale change. + */ + public abstract void scaleModel(EventObject event); + + @Override + public LabeledFigure clone() { + LabeledFigure clone = (LabeledFigure) super.clone(); + clone.fLabel = null; + return clone; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/LabeledLocationFigure.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/LabeledLocationFigure.java new file mode 100644 index 0000000..2240b11 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/LabeledLocationFigure.java @@ -0,0 +1,230 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures; + +import static java.util.Objects.requireNonNull; + +import com.google.common.base.Strings; +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.Shape; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EventObject; +import javax.swing.Action; +import org.jhotdraw.draw.handle.Handle; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.type.CoordinateProperty; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.common.components.drawing.ZoomPoint; +import org.opentcs.guing.common.components.drawing.course.Origin; + +/** + * {@link LocationFigure} with a label. + */ +public class LabeledLocationFigure + extends + LabeledFigure { + + /** + * The tool tip text generator. + */ + private final ToolTipTextGenerator textGenerator; + + /** + * Creates a new instance. + * + * @param figure The presentation figure. + * @param textGenerator The tool tip text generator. + */ + @Inject + @SuppressWarnings("this-escape") + public LabeledLocationFigure( + @Assisted + LocationFigure figure, + ToolTipTextGenerator textGenerator + ) { + requireNonNull(figure, "figure"); + this.textGenerator = requireNonNull(textGenerator, "textGenerator"); + + setPresentationFigure(figure); + } + + @Override + public LocationFigure getPresentationFigure() { + return (LocationFigure) super.getPresentationFigure(); + } + + @Override + public Shape getShape() { + return getPresentationFigure().getDrawingArea(); + } + + @Override + public String getToolTipText(Point2D.Double p) { + return textGenerator.getToolTipText(getPresentationFigure().getModel()); + } + + @Override + public LabeledLocationFigure clone() { + // Do NOT clone the label here. + LabeledLocationFigure that = (LabeledLocationFigure) super.clone(); + + if (that.getChildCount() > 0) { + that.removeChild(0); + } + + LocationFigure thatPresentationFigure = that.getPresentationFigure(); + thatPresentationFigure.addFigureListener(that.eventHandler); + // Force loading of the symbol bitmap + thatPresentationFigure.propertiesChanged(null); + + return that; + } + + @Override + public Collection getActions(Point2D.Double p) { + return new ArrayList<>(); + } + + @Override + public Collection createHandles(int detailLevel) { + if (!isVisible()) { + return new ArrayList<>(); + } + + return super.createHandles(detailLevel); + } + + @Override + public int getLayer() { + return getPresentationFigure().getLayer(); + } + + @Override + public boolean isVisible() { + return getPresentationFigure().isVisible(); + } + + @Override + public void propertiesChanged(AttributesChangeEvent event) { + if (event.getInitiator().equals(this)) { + return; + } + + // Move the figure if the model coordinates have been changed in the + // Properties panel + Origin origin = get(FigureConstants.ORIGIN); + + if (origin != null) { + LocationFigure lf = getPresentationFigure(); + + if (lf.getModel().getPropertyLayoutPositionX().hasChanged() + || lf.getModel().getPropertyLayoutPositionY().hasChanged()) { + getLabel().willChange(); + Point2D exact + = origin.calculatePixelPositionExactly( + lf.getModel().getPropertyLayoutPositionX(), + lf.getModel().getPropertyLayoutPositionY() + ); + double scale = lf.getZoomPoint().scale(); + double xNew = exact.getX() / scale; + double yNew = exact.getY() / scale; + Point2D.Double anchor = new Point2D.Double(xNew, yNew); + setBounds(anchor, anchor); + getLabel().changed(); + } + } + + // Update the image of the actual Location type + getPresentationFigure().propertiesChanged(event); + + invalidate(); + // also update the label. + fireFigureChanged(); + } + + @Override + public void scaleModel(EventObject event) { + Origin origin = get(FigureConstants.ORIGIN); + + if (origin != null) { + LocationFigure lf = getPresentationFigure(); + + Point2D exact + = origin.calculatePixelPositionExactly( + lf.getModel().getPropertyLayoutPositionX(), + lf.getModel().getPropertyLayoutPositionY() + ); + Point2D.Double anchor = new Point2D.Double(exact.getX(), exact.getY()); + setBounds(anchor, anchor); + } + + invalidate(); + // also update the label. + fireFigureChanged(); + } + + @Override + public void updateModel() { + Origin origin = get(FigureConstants.ORIGIN); + LocationFigure lf = getPresentationFigure(); + LocationModel model = lf.getModel(); + CoordinateProperty cpx = model.getPropertyModelPositionX(); + CoordinateProperty cpy = model.getPropertyModelPositionY(); + if ((double) cpx.getValue() == 0.0 && (double) cpy.getValue() == 0.0) { + origin.calculateRealPosition(lf.center(), cpx, cpy); + cpx.markChanged(); + cpy.markChanged(); + } + ZoomPoint zoomPoint = lf.getZoomPoint(); + if (zoomPoint != null && origin != null) { + StringProperty lpx = model.getPropertyLayoutPositionX(); + int oldX = 0; + + if (!Strings.isNullOrEmpty(lpx.getText())) { + oldX = (int) Double.parseDouble(lpx.getText()); + } + int newX = (int) (zoomPoint.getX() * origin.getScaleX()); + + if (newX != oldX || newX == 0) { + lpx.setText(String.format("%d", newX)); + lpx.markChanged(); + } + + StringProperty lpy = model.getPropertyLayoutPositionY(); + + int oldY = 0; + if (!Strings.isNullOrEmpty(lpy.getText())) { + oldY = (int) Double.parseDouble(lpy.getText()); + } + + int newY = (int) (-zoomPoint.getY() * origin.getScaleY()); // Vorzeichen! + + if (newY != oldY || newY == 0) { + lpy.setText(String.format("%d", newY)); + lpy.markChanged(); + } + + // Offset of the labels of the location will be updated here. + StringProperty propOffsetX = model.getPropertyLabelOffsetX(); + if (Strings.isNullOrEmpty(propOffsetX.getText())) { + propOffsetX.setText(String.format("%d", TCSLabelFigure.DEFAULT_LABEL_OFFSET_X)); + } + + StringProperty propOffsetY = model.getPropertyLabelOffsetY(); + if (Strings.isNullOrEmpty(propOffsetY.getText())) { + propOffsetY.setText(String.format("%d", TCSLabelFigure.DEFAULT_LABEL_OFFSET_Y)); + } + + } + // update the type. + model.getPropertyType().markChanged(); + + model.propertiesChanged(this); + // also update the label. + fireFigureChanged(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/LabeledPointFigure.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/LabeledPointFigure.java new file mode 100644 index 0000000..e3ab34d --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/LabeledPointFigure.java @@ -0,0 +1,252 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures; + +import static java.util.Objects.requireNonNull; + +import com.google.common.base.Strings; +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.Shape; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EventObject; +import javax.swing.Action; +import org.jhotdraw.draw.ConnectionFigure; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.connector.ChopEllipseConnector; +import org.jhotdraw.draw.connector.Connector; +import org.jhotdraw.draw.handle.DragHandle; +import org.jhotdraw.draw.handle.Handle; +import org.jhotdraw.draw.handle.MoveHandle; +import org.jhotdraw.draw.handle.ResizeHandleKit; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.type.CoordinateProperty; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.common.components.drawing.ZoomPoint; +import org.opentcs.guing.common.components.drawing.course.Origin; +import org.opentcs.guing.common.components.drawing.figures.decoration.PointOutlineHandle; + +/** + * {@link PointFigure} with a label. + */ +public class LabeledPointFigure + extends + LabeledFigure { + + /** + * The tool tip text generator. + */ + private final ToolTipTextGenerator textGenerator; + + /** + * Creates a new instance. + * + * @param figure The presentation figure. + * @param textGenerator The tool tip text generator. + */ + @Inject + @SuppressWarnings("this-escape") + public LabeledPointFigure( + @Assisted + PointFigure figure, + ToolTipTextGenerator textGenerator + ) { + requireNonNull(figure, "figure"); + this.textGenerator = requireNonNull(textGenerator, "textGenerator"); + + setPresentationFigure(figure); + } + + @Override + public PointFigure getPresentationFigure() { + return (PointFigure) super.getPresentationFigure(); + } + + @Override + public Shape getShape() { + return getPresentationFigure().getShape(); + } + + @Override + public Connector findConnector(Point2D.Double p, ConnectionFigure prototype) { + return new ChopEllipseConnector(this); + } + + @Override + public String getToolTipText(Point2D.Double p) { + return textGenerator.getToolTipText(getPresentationFigure().getModel()); + } + + @Override + public LabeledPointFigure clone() { + // Do NOT clone the label here. + LabeledPointFigure that = (LabeledPointFigure) super.clone(); + + if (that.getChildCount() > 0) { + that.basicRemoveAllChildren(); + } + + return that; + } + + @Override + public Collection getActions(Point2D.Double p) { + return new ArrayList<>(); + } + + @Override + public int getLayer() { + return getPresentationFigure().getLayer(); + } + + @Override + public boolean isVisible() { + return getPresentationFigure().isVisible(); + } + + @Override + public void propertiesChanged(AttributesChangeEvent event) { + if (event.getInitiator().equals(this)) { + return; + } + + // Move the figure if the model coordinates have been changed in the + // Properties panel + Origin origin = get(FigureConstants.ORIGIN); + + if (origin != null) { + PointFigure pf = getPresentationFigure(); + + StringProperty xLayout = pf.getModel().getPropertyLayoutPosX(); + StringProperty yLayout = pf.getModel().getPropertyLayoutPosY(); + + if (xLayout.hasChanged() || yLayout.hasChanged()) { + getLabel().willChange(); + Point2D exact = origin.calculatePixelPositionExactly(xLayout, yLayout); + double scale = pf.getZoomPoint().scale(); + double xNew = exact.getX() / scale; + double yNew = exact.getY() / scale; + Point2D.Double anchor = new Point2D.Double(xNew, yNew); + setBounds(anchor, anchor); + getLabel().changed(); + } + } + + invalidate(); + fireFigureChanged(); + } + + @Override + public void scaleModel(EventObject event) { + Origin origin = get(FigureConstants.ORIGIN); + + if (origin != null) { + PointFigure pf = getPresentationFigure(); + + Point2D exact = origin.calculatePixelPositionExactly( + pf.getModel().getPropertyLayoutPosX(), + pf.getModel().getPropertyLayoutPosY() + ); + Point2D.Double anchor = new Point2D.Double(exact.getX(), exact.getY()); + setBounds(anchor, anchor); + } + + invalidate(); + fireFigureChanged(); + } + + @Override + public void updateModel() { + Origin origin = get(FigureConstants.ORIGIN); + PointFigure pf = getPresentationFigure(); + PointModel model = pf.getModel(); + CoordinateProperty cpx = model.getPropertyModelPositionX(); + CoordinateProperty cpy = model.getPropertyModelPositionY(); + // Write current model position to properties once when creating the layout. + if ((double) cpx.getValue() == 0.0 && (double) cpy.getValue() == 0.0) { + origin.calculateRealPosition(pf.center(), cpx, cpy); + cpx.markChanged(); + cpy.markChanged(); + } + ZoomPoint zoomPoint = pf.getZoomPoint(); + if (zoomPoint != null && origin != null) { + StringProperty lpx = model.getPropertyLayoutPosX(); + + int oldX = 0; + if (!Strings.isNullOrEmpty(lpx.getText())) { + oldX = (int) Double.parseDouble(lpx.getText()); + } + + int newX = (int) (zoomPoint.getX() * origin.getScaleX()); + if (newX != oldX) { + lpx.setText(String.format("%d", newX)); + lpx.markChanged(); + } + + StringProperty lpy = model.getPropertyLayoutPosY(); + + int oldY = 0; + if (!Strings.isNullOrEmpty(lpy.getText())) { + oldY = (int) Double.parseDouble(lpy.getText()); + } + + int newY = (int) (-zoomPoint.getY() * origin.getScaleY()); // Vorzeichen! + if (newY != oldY) { + lpy.setText(String.format("%d", newY)); + lpy.markChanged(); + } + + // Offset of the labels of the location will be updated here. + StringProperty propOffsetX = model.getPropertyPointLabelOffsetX(); + if (Strings.isNullOrEmpty(propOffsetX.getText())) { + propOffsetX.setText(String.format("%d", TCSLabelFigure.DEFAULT_LABEL_OFFSET_X)); + } + + StringProperty propOffsetY = model.getPropertyPointLabelOffsetY(); + if (Strings.isNullOrEmpty(propOffsetY.getText())) { + propOffsetY.setText(String.format("%d", TCSLabelFigure.DEFAULT_LABEL_OFFSET_Y)); + } + + } + model.getPropertyType().markChanged(); + + model.propertiesChanged(this); + fireFigureChanged(); + } + + @Override + public Collection createHandles(int detailLevel) { + Collection handles = new ArrayList<>(); + + if (!isVisible()) { + return handles; + } + + switch (detailLevel) { + case -1: // Mouse Moved + handles.add(new PointOutlineHandle(getPresentationFigure())); + break; + + case 0: // Mouse clicked + MoveHandle.addMoveHandles(this, handles); + for (Figure child : getChildren()) { + MoveHandle.addMoveHandles(child, handles); + handles.add(new DragHandle(child)); + } + + break; + + case 1: // Double-Click + ResizeHandleKit.addResizeHandles(this, handles); + break; + + default: + break; + } + + return handles; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/LinkConnection.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/LinkConnection.java new file mode 100644 index 0000000..77f6f5a --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/LinkConnection.java @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.BasicStroke; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EventObject; +import org.jhotdraw.draw.AttributeKeys; +import org.jhotdraw.draw.connector.ChopEllipseConnector; +import org.jhotdraw.draw.connector.Connector; +import org.jhotdraw.draw.handle.Handle; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.PointModel; + +/** + * A dashed line that connects a decision point with a location. + */ +public class LinkConnection + extends + SimpleLineConnection { + + /** + * The tool tip text generator. + */ + private final ToolTipTextGenerator textGenerator; + + /** + * Creates a new instance. + * + * @param model The model corresponding to this graphical object. + * @param textGenerator The tool tip text generator. + */ + @Inject + @SuppressWarnings("this-escape") + public LinkConnection( + @Assisted + LinkModel model, + ToolTipTextGenerator textGenerator + ) { + super(model); + this.textGenerator = requireNonNull(textGenerator, "textGenerator"); + + double[] dash = {5.0, 5.0}; + set(AttributeKeys.START_DECORATION, null); + set(AttributeKeys.END_DECORATION, null); + set(AttributeKeys.STROKE_WIDTH, 1.0); + set(AttributeKeys.STROKE_CAP, BasicStroke.CAP_BUTT); + set(AttributeKeys.STROKE_JOIN, BasicStroke.JOIN_MITER); + set(AttributeKeys.STROKE_MITER_LIMIT, 1.0); + set(AttributeKeys.STROKE_DASHES, dash); + set(AttributeKeys.STROKE_DASH_PHASE, 0.0); + } + + @Override + public LinkModel getModel() { + return (LinkModel) get(FigureConstants.MODEL); + } + + /** + * Connects two figures. + * + * @param point The point figure to connect. + * @param location The location figure to connect. + */ + public void connect(LabeledPointFigure point, LabeledLocationFigure location) { + Connector compConnector = new ChopEllipseConnector(); + Connector startConnector = point.findCompatibleConnector(compConnector, true); + Connector endConnector = location.findCompatibleConnector(compConnector, true); + + if (!canConnect(startConnector, endConnector)) { + return; + } + + setStartConnector(startConnector); + setEndConnector(endConnector); + } + + @Override // AbstractFigure + public String getToolTipText(Point2D.Double p) { + return textGenerator.getToolTipText(getModel()); + } + + @Override + public boolean canConnect(Connector start) { + return start.getOwner().get(FigureConstants.MODEL) instanceof LocationModel; + } + + @Override // SimpleLineConnection + public boolean canConnect(Connector start, Connector end) { + ModelComponent modelStart = start.getOwner().get(FigureConstants.MODEL); + ModelComponent modelEnd = end.getOwner().get(FigureConstants.MODEL); + + if (modelStart == null || modelEnd == null) { + return false; + } + + // Even though new links can now only be created starting from a location, we need this to + // ensure backward campatibility for older models that may still have links with a point + // as the start component. Otherwise those links would not be connected/drawn properly. + if ((modelStart instanceof PointModel) && modelEnd instanceof LocationModel) { + LocationModel location = (LocationModel) modelEnd; + PointModel point = (PointModel) modelStart; + + return !location.hasConnectionTo(point); + } + + if (modelStart instanceof LocationModel && (modelEnd instanceof PointModel)) { + LocationModel location = (LocationModel) modelStart; + PointModel point = (PointModel) modelEnd; + + return !point.hasConnectionTo(location); + } + + return false; + } + + @Override + public Collection createHandles(int detailLevel) { + if (!isVisible()) { + return new ArrayList<>(); + } + + return super.createHandles(detailLevel); + } + + @Override + public int getLayer() { + return getModel().getPropertyLayerWrapper().getValue().getLayer().getOrdinal(); + } + + @Override + public boolean isVisible() { + return super.isVisible() + && getModel().getPropertyLayerWrapper().getValue().getLayer().isVisible() + && getModel().getPropertyLayerWrapper().getValue().getLayerGroup().isVisible(); + } + + @Override + public void updateModel() { + } + + @Override + public void scaleModel(EventObject event) { + } + + @Override + public LinkConnection clone() { + LinkConnection clone = (LinkConnection) super.clone(); + clone.initConnectionFigure(); + + return clone; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/LocationFigure.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/LocationFigure.java new file mode 100644 index 0000000..b3bb81c --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/LocationFigure.java @@ -0,0 +1,326 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.guing.base.AllocationState.ALLOCATED; +import static org.opentcs.guing.base.AllocationState.CLAIMED; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Stroke; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.ImageObserver; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import org.jhotdraw.draw.AttributeKeys; +import org.jhotdraw.draw.ConnectionFigure; +import org.jhotdraw.draw.connector.ChopEllipseConnector; +import org.jhotdraw.draw.connector.Connector; +import org.jhotdraw.geom.Geom; +import org.opentcs.components.plantoverview.LocationTheme; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.visualization.LocationRepresentation; +import org.opentcs.guing.base.AllocationState; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.type.SymbolProperty; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.drawing.DrawingOptions; +import org.opentcs.guing.common.components.drawing.Strokes; +import org.opentcs.guing.common.components.drawing.ZoomPoint; + +/** + * A figure for locations. + */ +public class LocationFigure + extends + TCSFigure + implements + ImageObserver { + + /** + * The fill color for locked locations. + */ + private static final Color LOCKED_COLOR = new Color(255, 50, 50); + /** + * The image representing the location. + */ + private transient Image fImage; + private int fWidth; + private int fHeight; + private final LocationTheme locationTheme; + private final DrawingOptions drawingOptions; + + /** + * Creates a new instance. + * + * @param locationTheme The location theme to be used. + * @param model The model corresponding to this graphical object. + * @param drawingOptions The drawing options. + */ + @Inject + public LocationFigure( + LocationTheme locationTheme, + @Assisted + LocationModel model, + DrawingOptions drawingOptions + ) { + super(model); + this.locationTheme = requireNonNull(locationTheme, "locationTheme"); + this.drawingOptions = requireNonNull(drawingOptions, "drawingOptions"); + + fWidth = 30; + fHeight = 30; + fDisplayBox = new Rectangle(fWidth, fHeight); + fZoomPoint = new ZoomPoint(0.5 * fWidth, 0.5 * fHeight); + } + + @Override + public LocationModel getModel() { + return (LocationModel) get(FigureConstants.MODEL); + } + + public Point center() { + return Geom.center(fDisplayBox); + } + + @Override // Figure + public Rectangle2D.Double getBounds() { + Rectangle2D r2 = fDisplayBox.getBounds2D(); + Rectangle2D.Double r2d = new Rectangle2D.Double(); + r2d.setRect(r2); + + return r2d; + } + + @Override // Figure + public Object getTransformRestoreData() { + return fDisplayBox.clone(); + } + + @Override // Figure + public void restoreTransformTo(Object restoreData) { + Rectangle r = (Rectangle) restoreData; + fDisplayBox.x = r.x; + fDisplayBox.y = r.y; + fDisplayBox.width = r.width; + fDisplayBox.height = r.height; + fZoomPoint.setX(r.x + 0.5 * r.width); + fZoomPoint.setY(r.y + 0.5 * r.height); + } + + @Override // Figure + public void transform(AffineTransform tx) { + Point2D center = getZoomPoint().getPixelLocationExactly(); + setBounds((Point2D.Double) tx.transform(center, center), null); + } + + @Override // AbstractFigure + public void setBounds(Point2D.Double anchor, Point2D.Double lead) { + fZoomPoint.setX(anchor.x); + fZoomPoint.setY(anchor.y); + fDisplayBox.x = (int) (anchor.x - 0.5 * fDisplayBox.width); + fDisplayBox.y = (int) (anchor.y - 0.5 * fDisplayBox.height); + } + + @Override // AbstractFigure + public Connector findConnector(Point2D.Double p, ConnectionFigure prototype) { + return new ChopEllipseConnector(this); + } + + @Override // AbstractFigure + public Connector findCompatibleConnector(Connector c, boolean isStartConnector) { + return new ChopEllipseConnector(this); + } + + @Override + public int getLayer() { + return getModel().getPropertyLayerWrapper().getValue().getLayer().getOrdinal(); + } + + @Override + public boolean isVisible() { + return super.isVisible() + && getModel().getPropertyLayerWrapper().getValue().getLayer().isVisible() + && getModel().getPropertyLayerWrapper().getValue().getLayerGroup().isVisible(); + } + + @Override + protected void drawFigure(Graphics2D g) { + if (drawingOptions.isBlocksVisible()) { + drawBlockDecoration(g); + } + drawRouteDecoration(g); + + super.drawFigure(g); + } + + private void drawRouteDecoration(Graphics2D g) { + for (Map.Entry entry : getModel().getAllocationStates() + .entrySet()) { + VehicleModel vehicleModel = entry.getKey(); + switch (entry.getValue()) { + case CLAIMED: + drawDecoration( + g, + Strokes.PATH_ON_ROUTE, + transparentColor(vehicleModel.getDriveOrderColor(), 70) + ); + break; + case ALLOCATED: + drawDecoration(g, Strokes.PATH_ON_ROUTE, vehicleModel.getDriveOrderColor()); + break; + case ALLOCATED_WITHDRAWN: + drawDecoration(g, Strokes.PATH_ON_WITHDRAWN_ROUTE, Color.GRAY); + break; + default: + // Don't draw any decoration. + } + } + } + + private void drawBlockDecoration(Graphics2D g) { + for (BlockModel blockModel : getModel().getBlockModels()) { + drawDecoration(g, Strokes.BLOCK_ELEMENT, transparentColor(blockModel.getColor(), 192)); + } + } + + private Color transparentColor(Color color, int alpha) { + return new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha); + } + + private void drawDecoration(Graphics2D g, Stroke stroke, Color color) { + g.setStroke(stroke); + g.setColor(color); + g.draw(this.getDrawingArea()); + } + + @Override // AbstractAttributedFigure + protected void drawFill(Graphics2D g) { + int dx; + int dy; + Rectangle r = displayBox(); + g.fillRect(r.x, r.y, r.width, r.height); + + if (fImage != null) { + dx = (r.width - fImage.getWidth(this)) / 2; + dy = (r.height - fImage.getHeight(this)) / 2; + g.drawImage(fImage, r.x + dx, r.y + dy, this); + } + } + + @Override // AbstractAttributedFigure + protected void drawStroke(Graphics2D g) { + Rectangle r = displayBox(); + g.drawRect(r.x, r.y, r.width - 1, r.height - 1); + } + + @Override + public LocationFigure clone() { + LocationFigure thatFigure = (LocationFigure) super.clone(); + thatFigure.setZoomPoint(new ZoomPoint(fZoomPoint.getX(), fZoomPoint.getY())); + + return thatFigure; + } + + @Override // ImageObserver + public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { + if ((infoflags & (FRAMEBITS | ALLBITS)) != 0) { + invalidate(); + } + + return (infoflags & (ALLBITS | ABORT)) == 0; + } + + public void propertiesChanged(AttributesChangeEvent e) { + handleLocationTypeChanged(); + handleLocationLockChanged(); + } + + private void handleLocationTypeChanged() { + LocationTypeModel locationType = getModel().getLocationType(); + + if (locationType == null) { + return; + } + if (getModel().getLocation() != null && locationType.getLocationType() != null) { + fImage = locationTheme.getImageFor(getModel().getLocation(), locationType.getLocationType()); + } + else { + SymbolProperty pSymbol = getModel().getPropertyDefaultRepresentation(); + LocationRepresentation locationRepresentation = pSymbol.getLocationRepresentation(); + if (locationRepresentation == null + || locationRepresentation == LocationRepresentation.DEFAULT) { + pSymbol = locationType.getPropertyDefaultRepresentation(); + locationRepresentation = pSymbol.getLocationRepresentation(); + fImage = locationTheme.getImageFor(locationRepresentation); + } + else { + fImage = locationTheme.getImageFor(locationRepresentation); + } + } + fWidth = Math.max(fImage.getWidth(this) + 10, 30); + fHeight = Math.max(fImage.getHeight(this) + 10, 30); + fDisplayBox.setSize(fWidth, fHeight); + } + + private void handleLocationLockChanged() { + set( + AttributeKeys.FILL_COLOR, + (Boolean) getModel().getPropertyLocked().getValue() ? LOCKED_COLOR : Color.WHITE + ); + } + + private List>> getCurrentDriveOrderClaim(VehicleModel vehicle) { + List>> result = new ArrayList<>(); + + boolean driveOrderEndFound = false; + for (Set> res : vehicle.getClaimedResources().getItems()) { + result.add(res); + + if (containsDriveOrderDestination(res, vehicle)) { + driveOrderEndFound = true; + break; + } + } + + if (driveOrderEndFound) { + return result; + } + else { + // With the end of the drive order not found, there is nothing from the current drive order in + // the claimed resources. + return List.of(); + } + } + + private boolean containsDriveOrderDestination( + Set> resources, + VehicleModel vehicle + ) { + if (vehicle.getDriveOrderDestination() == null) { + return false; + } + + return resources.stream() + .anyMatch( + resource -> Objects.equals( + resource.getName(), + vehicle.getDriveOrderDestination().getName() + ) + ); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/ModelBasedFigure.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/ModelBasedFigure.java new file mode 100644 index 0000000..6d71bba --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/ModelBasedFigure.java @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures; + +import org.jhotdraw.draw.Figure; +import org.opentcs.guing.base.model.DrawnModelComponent; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A figure that is based on/is a graphical representation for a {@link ModelComponent}. + */ +public interface ModelBasedFigure + extends + Figure { + + /** + * Returns the model component for this figure. + * + * @return The model component for this figure. + */ + DrawnModelComponent getModel(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/OffsetFigure.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/OffsetFigure.java new file mode 100644 index 0000000..2b7815f --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/OffsetFigure.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import org.jhotdraw.draw.AttributeKeys; +import org.opentcs.guing.common.components.drawing.course.Origin; + +/** + * An OffsetFigure is an (invisible) figure that moves as the user drags the view + * beyond its current bounds, so the view becomes larger resp is repainted + * larger. + */ +public class OffsetFigure + extends + OriginFigure { + + @SuppressWarnings("this-escape") + public OffsetFigure() { + super(); + setModel(new Origin()); // The figure needs a model to work + set(AttributeKeys.STROKE_COLOR, Color.darkGray); + setVisible(false); // only visible for test + } + + @Override + protected void drawStroke(Graphics2D g) { + // Shape: "Crosshair" with square + Rectangle r = (Rectangle) fDisplayBox.clone(); + + if (r.width > 0 && r.height > 0) { + g.drawLine(r.x + r.width / 2, r.y, r.x + r.width / 2, r.y + r.height); + g.drawLine(r.x, r.y + r.height / 2, r.x + r.width, r.y + r.height / 2); + r.grow(-4, -4); + g.drawRect(r.x, r.y, r.width, r.height); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/OriginFigure.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/OriginFigure.java new file mode 100644 index 0000000..7e1d4ce --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/OriginFigure.java @@ -0,0 +1,179 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.Collection; +import org.jhotdraw.draw.AbstractAttributedFigure; +import org.jhotdraw.draw.AttributeKeys; +import org.jhotdraw.draw.handle.Handle; +import org.jhotdraw.geom.Geom; +import org.opentcs.guing.common.components.drawing.ZoomPoint; +import org.opentcs.guing.common.components.drawing.course.Origin; + +/** + * A Figure for the coordinate system's origin. + */ +public class OriginFigure + extends + AbstractAttributedFigure { + + /** + * The enclosing rectangle. + */ + protected final Rectangle fDisplayBox; + /** + * The width and height. + */ + private final int fSideLength; + /** + * The origin's model. + */ + private Origin fModel; + /** + * The exact position of the figure. + */ + private final ZoomPoint fZoomPoint; + + /** + * Creates a new instance. + */ + @SuppressWarnings("this-escape") + public OriginFigure() { + super(); + fSideLength = 20; + fZoomPoint = new ZoomPoint(0.0, 0.0); + fDisplayBox = new Rectangle( + -fSideLength / 2, -fSideLength / 2, + fSideLength, fSideLength + ); + set(AttributeKeys.STROKE_COLOR, Color.blue); + setSelectable(false); + } + + /** + * Set's the origin's model. + * + * @param model The model. + */ + public void setModel(Origin model) { + fModel = model; + getModel().setPosition(Geom.center(fDisplayBox)); + } + + /** + * Returns the origin's model. + * + * @return The model. + */ + public Origin getModel() { + return fModel; + } + + /** + * Returns the exact position of the origin. + * + * @return The exact position of the origin. + */ + public ZoomPoint getZoomPoint() { + return fZoomPoint; + } + + @Override + public Rectangle2D.Double getBounds() { + Rectangle2D.Double r2d = new Rectangle2D.Double(); + r2d.setRect(fDisplayBox.getBounds2D()); + + return r2d; + } + + @Override + public boolean contains(Point2D.Double p) { + Rectangle r = (Rectangle) fDisplayBox.clone(); + double grow = AttributeKeys.getPerpendicularHitGrowth(this); + r.x = (int) (r.x - grow); + r.y = (int) (r.y - grow); + r.width = (int) (r.width + grow * 2); + r.height = (int) (r.height + grow * 2); + + return r.contains(p); + } + + @Override + public Object getTransformRestoreData() { + return fDisplayBox.clone(); + } + + @Override + public void restoreTransformTo(Object restoreData) { + Rectangle r = (Rectangle) restoreData; + fDisplayBox.x = r.x; + fDisplayBox.y = r.y; + fDisplayBox.width = r.width; + fDisplayBox.height = r.height; + fZoomPoint.setX(r.x + 0.5 * r.width); + fZoomPoint.setY(r.y + 0.5 * r.height); + } + + @Override + public void transform(AffineTransform tx) { + Point2D center = getZoomPoint().getPixelLocationExactly(); + Point2D lead = new Point2D.Double(); // not used + setBounds( + (Point2D.Double) tx.transform(center, center), + (Point2D.Double) tx.transform(lead, lead) + ); + } + + @Override + public void changed() { + super.changed(); + getModel().setPosition(Geom.center(fDisplayBox)); + getModel().notifyLocationChanged(); + } + + @Override + public void setBounds(Point2D.Double anchor, Point2D.Double lead) { + // Only change the position here, NOT the size! + // Draw the center of the figure at the mouse cursor's position. + fZoomPoint.setX(anchor.x); + fZoomPoint.setY(anchor.y); + fDisplayBox.x = (int) (anchor.x - 0.5 * fSideLength); + fDisplayBox.y = (int) (anchor.y - 0.5 * fSideLength); + } + + @Override + public Collection createHandles(int detailLevel) { + // No handles for the origin figure. + return new ArrayList<>(); + } + + @Override + protected void drawFill(Graphics2D g) { + // No filling for the origin's figure. + } + + @Override + protected void drawStroke(Graphics2D g) { + // Outline: "Crosshair" with circle + Rectangle r = (Rectangle) fDisplayBox.clone(); + + if (r.width > 0 && r.height > 0) { + g.drawLine(r.x + r.width / 2, r.y, r.x + r.width / 2, r.y + r.height); + g.drawLine(r.x, r.y + r.height / 2, r.x + r.width, r.y + r.height / 2); + r.grow(-4, -4); + g.drawOval(r.x, r.y, r.width, r.height); + } + } + + @Override + public int getLayer() { + return FigureOrdinals.ORIGIN_FIGURE_ORDINAL; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/PathConnection.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/PathConnection.java new file mode 100644 index 0000000..e381358 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/PathConnection.java @@ -0,0 +1,945 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Stroke; +import java.awt.event.MouseEvent; +import java.awt.geom.Point2D; +import java.awt.geom.Point2D.Double; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EventObject; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.StringJoiner; +import org.jhotdraw.draw.AttributeKey; +import org.jhotdraw.draw.AttributeKeys; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.connector.ChopEllipseConnector; +import org.jhotdraw.draw.connector.Connector; +import org.jhotdraw.draw.handle.BezierOutlineHandle; +import org.jhotdraw.draw.handle.Handle; +import org.jhotdraw.draw.liner.ElbowLiner; +import org.jhotdraw.draw.liner.Liner; +import org.jhotdraw.draw.liner.SlantedLiner; +import org.jhotdraw.geom.BezierPath; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.guing.base.AllocationState; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.type.AbstractProperty; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.components.properties.type.SpeedProperty; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.drawing.DrawingOptions; +import org.opentcs.guing.common.components.drawing.Strokes; +import org.opentcs.guing.common.components.drawing.course.Origin; +import org.opentcs.guing.common.components.drawing.figures.liner.BezierLinerControlPointHandle; +import org.opentcs.guing.common.components.drawing.figures.liner.PolyPathLiner; +import org.opentcs.guing.common.components.drawing.figures.liner.TripleBezierLiner; +import org.opentcs.guing.common.components.drawing.figures.liner.TupelBezierLiner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A connection between two points. + */ +public class PathConnection + extends + SimpleLineConnection { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PathConnection.class); + /** + * The dash pattern for locked paths. + */ + private static final double[] LOCKED_DASH = {6.0, 4.0}; + /** + * The dash pattern for unlocked paths. + */ + private static final double[] UNLOCKED_DASH = {10.0, 0.0}; + /** + * The tool tip text generator. + */ + private final ToolTipTextGenerator textGenerator; + /** + * The drawing options. + */ + private final DrawingOptions drawingOptions; + /** + * Control point 1. + */ + private Point2D.Double cp1; + /** + * Control point 2. + */ + private Point2D.Double cp2; + /** + * Control point 3. + */ + private Point2D.Double cp3; + /** + * Control point 4. + */ + private Point2D.Double cp4; + /** + * Control point 5. + */ + private Point2D.Double cp5; + + private Origin previousOrigin; + + /** + * Creates a new instance. + * + * @param model The model corresponding to this graphical object. + * @param textGenerator The tool tip text generator. + * @param drawingOptions The drawing options. + */ + @Inject + @SuppressWarnings("this-escape") + public PathConnection( + @Assisted + PathModel model, + ToolTipTextGenerator textGenerator, + DrawingOptions drawingOptions + ) { + super(model); + this.textGenerator = requireNonNull(textGenerator, "textGenerator"); + this.drawingOptions = requireNonNull(drawingOptions, "drawingOptions"); + resetPath(); + } + + @Override + public PathModel getModel() { + return (PathModel) get(FigureConstants.MODEL); + } + + @Override + public void updateConnection() { + super.updateConnection(); + initializePreviousOrigin(); + updateControlPoints(); + } + + /** + * Resets control points and connects start and end point with a straight line. + */ + private void resetPath() { + Point2D.Double sp = path.get(0, BezierPath.C0_MASK); + Point2D.Double ep = path.get(path.size() - 1, BezierPath.C0_MASK); + + path.clear(); + path.add(new BezierPath.Node(sp)); + path.add(new BezierPath.Node(ep)); + cp1 = null; + cp2 = null; + cp3 = null; + cp4 = null; + cp5 = null; + getModel().getPropertyPathControlPoints().markChanged(); + } + + /** + * Initialise the control points when converting into BEZIER curve. + * + * @param type the type of the curve + */ + private void initControlPoints(PathModel.Type type) { + Point2D.Double sp = path.get(0, BezierPath.C0_MASK); + Point2D.Double ep = path.get(path.size() - 1, BezierPath.C0_MASK); + + if (sp.x != ep.x || sp.y != ep.y) { + path.clear(); + if (type == PathModel.Type.BEZIER_3) { //BEZIER curve with 3 control points); + //Add the scaled vector between start and endpoint to the startpoint + cp1 = new Point2D.Double(sp.x + (ep.x - sp.x) * 1 / 6, sp.y + (ep.y - sp.y) * 1 / 6); + cp2 = new Point2D.Double(sp.x + (ep.x - sp.x) * 2 / 6, sp.y + (ep.y - sp.y) * 2 / 6); + cp3 = new Point2D.Double(sp.x + (ep.x - sp.x) * 3 / 6, sp.y + (ep.y - sp.y) * 3 / 6); + cp4 = new Point2D.Double(sp.x + (ep.x - sp.x) * 4 / 6, sp.y + (ep.y - sp.y) * 4 / 6); + cp5 = new Point2D.Double(sp.x + (ep.x - sp.x) * 5 / 6, sp.y + (ep.y - sp.y) * 5 / 6); + path.add( + new BezierPath.Node( + BezierPath.C2_MASK, + sp.x, sp.y, //Current point + sp.x, sp.y, //Previous point - not in use because of C2_MASK + cp1.x, cp1.y + ) + ); //Next point + //Use cp1 and cp2 to draw between sp and cp3 + path.add( + new BezierPath.Node( + BezierPath.C1C2_MASK, + cp3.x, cp3.y, //Current point + cp2.x, cp2.y, //Previous point + cp4.x, cp4.y + ) + ); //Next point + //Use cp4 and cp5 to draw between cp3 and ep + path.add( + new BezierPath.Node( + BezierPath.C1_MASK, + ep.x, ep.y, //Current point + cp5.x, cp5.y, //Previous point + ep.x, ep.y + ) + ); //Next point - not in use because of C1_MASK + } + else { + cp1 = new Point2D.Double(sp.x + (ep.x - sp.x) / 3, sp.y + (ep.y - sp.y) / 3); //point at 1/3 + cp2 = new Point2D.Double(ep.x - (ep.x - sp.x) / 3, ep.y - (ep.y - sp.y) / 3); //point at 2/3 + cp3 = null; + cp4 = null; + cp5 = null; + path.add( + new BezierPath.Node( + BezierPath.C2_MASK, + sp.x, sp.y, //Current point + sp.x, sp.y, //Previous point - not in use because of C2_MASK + cp1.x, cp1.y + ) + ); //Next point + path.add( + new BezierPath.Node( + BezierPath.C1_MASK, + ep.x, ep.y, //Current point + cp2.x, cp2.y, //Previous point + ep.x, ep.y + ) + ); //Next point - not in use because of C1_MASK + } + + getModel().getPropertyPathControlPoints().markChanged(); + path.invalidatePath(); + } + } + + /** + * Add control points. + * + * @param cp1 First control point. + * @param cp2 Identical with cp1 for quadratic curves. + */ + public void addControlPoints(Point2D.Double cp1, Point2D.Double cp2) { + this.cp1 = cp1; + this.cp2 = cp2; + this.cp3 = null; + Point2D.Double sp = path.get(0, BezierPath.C0_MASK); + Point2D.Double ep = path.get(1, BezierPath.C0_MASK); + path.clear(); + path.add( + new BezierPath.Node( + BezierPath.C2_MASK, + sp.x, sp.y, //Current point + sp.x, sp.y, //Previous point + cp1.x, cp1.y + ) + ); //Next point + path.add( + new BezierPath.Node( + BezierPath.C1_MASK, + ep.x, ep.y, //Current point + cp2.x, cp2.y, //Previous point + ep.x, ep.y + ) + ); //Next point + } + + /** + * A bezier curve with three control points. + * + * @param cp1 Control point 1 + * @param cp2 Control point 2 + * @param cp3 Control point 3 + * @param cp4 Control point 4 + * @param cp5 Control point 5 + */ + public void addControlPoints( + Point2D.Double cp1, + Point2D.Double cp2, + Point2D.Double cp3, + Point2D.Double cp4, + Point2D.Double cp5 + ) { + this.cp1 = cp1; + this.cp2 = cp2; + this.cp3 = cp3; + this.cp4 = cp4; + this.cp5 = cp5; + Point2D.Double sp = path.get(0, BezierPath.C0_MASK); + Point2D.Double ep = path.get(path.size() - 1, BezierPath.C0_MASK); + path.clear(); + path.add( + new BezierPath.Node( + BezierPath.C2_MASK, + sp.x, sp.y, //Current point + sp.x, sp.y, //Previous point + cp1.x, cp1.y + ) + ); //Next point + //Use cp1 and cp2 to draw between sp and cp3 + path.add( + new BezierPath.Node( + BezierPath.C1C2_MASK, + cp3.x, cp3.y, //Current point + cp2.x, cp2.y, //Previous point + cp4.x, cp4.y + ) + ); //Next point + //Use cp4 and cp5 to draw between cp3 and ep + path.add( + new BezierPath.Node( + BezierPath.C1_MASK, + ep.x, ep.y, //Current point + cp5.x, cp5.y, //Previous point + cp4.x, cp4.y + ) + ); //Next point + StringProperty sProp = getModel().getPropertyPathControlPoints(); + sProp.setText( + String.format( + "%d,%d;%d,%d;%d,%d;%d,%d;%d,%d;", + (int) cp1.x, (int) cp1.y, + (int) cp2.x, (int) cp2.y, + (int) cp3.x, (int) cp3.y, + (int) cp4.x, (int) cp4.y, + (int) cp5.x, (int) cp5.y + ) + ); + sProp.markChanged(); + getModel().propertiesChanged(this); + } + + public Point2D.Double getCp1() { + return cp1; + } + + public Point2D.Double getCp2() { + return cp2; + } + + public Point2D.Double getCp3() { + return cp3; + } + + public Point2D.Double getCp4() { + return cp4; + } + + public Point2D.Double getCp5() { + return cp5; + } + + @Override + public Point2D.Double getCenter() { + // Computes the center of the curve. + // Approximation: Center of the control points. + Point2D.Double p1; + Point2D.Double p2; + Point2D.Double pc; + + p1 = (cp1 == null ? path.get(0, BezierPath.C0_MASK) : cp1); + p2 = (cp2 == null ? path.get(1, BezierPath.C0_MASK) : cp2); + if (cp3 == null) { + pc = new Point2D.Double((p1.x + p2.x) / 2, (p1.y + p2.y) / 2); + } + else { + //Use cp3 for 3-bezier as center because the curve goes through it at 50% + pc = (cp3 == null ? path.get(3, BezierPath.C0_MASK) : cp3); + } + + return pc; + } + + @Override + public int getLayer() { + return getModel().getPropertyLayerWrapper().getValue().getLayer().getOrdinal(); + } + + @Override + public boolean isVisible() { + return super.isVisible() + && getModel().getPropertyLayerWrapper().getValue().getLayer().isVisible() + && getModel().getPropertyLayerWrapper().getValue().getLayerGroup().isVisible(); + } + + /** + * Initializes the previous origin which is used to scale the control points of this path. + */ + private void initializePreviousOrigin() { + if (previousOrigin == null) { + Origin origin = get(FigureConstants.ORIGIN); + previousOrigin = new Origin(); + previousOrigin.setScale(origin.getScaleX(), origin.getScaleY()); + } + } + + /** + * Update bezier and polypath control points. + */ + public void updateControlPoints() { + String sControlPoints = ""; + if (getLinerType() == PathModel.Type.POLYPATH) { + sControlPoints = updatePolyPathControlPoints(); + } + else if (getLinerType() == PathModel.Type.BEZIER || getLinerType() == PathModel.Type.BEZIER_3) { + sControlPoints = updateBezierControlPoints(); + } + + StringProperty sProp = getModel().getPropertyPathControlPoints(); + sProp.setText(sControlPoints); + invalidate(); + sProp.markChanged(); + getModel().propertiesChanged(this); + } + + private String updatePolyPathControlPoints() { + StringJoiner rtn = new StringJoiner(";"); + for (int i = 1; i < path.size() - 1; i++) { + Point2D.Double p = path.get(i, BezierPath.C0_MASK); + rtn.add(String.format("%d,%d", (int) p.x, (int) p.y)); + } + return rtn.toString(); + } + + private String updateBezierControlPoints() { + if (cp1 != null && cp2 != null) { + if (cp3 != null) { + cp1 = path.get(0, BezierPath.C2_MASK); + cp2 = path.get(1, BezierPath.C1_MASK); + cp3 = path.get(1, BezierPath.C0_MASK); + cp4 = path.get(1, BezierPath.C2_MASK); + cp5 = path.get(2, BezierPath.C1_MASK); + } + else { + cp1 = path.get(0, BezierPath.C2_MASK); + cp2 = path.get(1, BezierPath.C1_MASK); + } + } + String sControlPoints = ""; + if (cp1 != null) { + if (cp2 != null) { + if (cp3 != null) { + // Format: x1,y1;x2,y2;x3,y3;x4,y4;x5,y5 + sControlPoints = String.format( + "%d,%d;%d,%d;%d,%d;%d,%d;%d,%d", + (int) (cp1.x), + (int) (cp1.y), + (int) (cp2.x), + (int) (cp2.y), + (int) (cp3.x), + (int) (cp3.y), + (int) (cp4.x), + (int) (cp4.y), + (int) (cp5.x), + (int) (cp5.y) + ); + } + else { + // Format: x1,y1;x2,y2 + sControlPoints = String.format( + "%d,%d;%d,%d", (int) (cp1.x), + (int) (cp1.y), (int) (cp2.x), + (int) (cp2.y) + ); + } + } + else { + // Format: x1,y1 + sControlPoints = String.format("%d,%d", (int) (cp1.x), (int) (cp1.y)); + } + } + return sControlPoints; + } + + /** + * Connects two figures with this connection. + * + * @param start The first figure. + * @param end The second figure. + */ + public void connect(LabeledPointFigure start, LabeledPointFigure end) { + Connector compConnector = new ChopEllipseConnector(); + Connector startConnector = start.findCompatibleConnector(compConnector, true); + Connector endConnector = end.findCompatibleConnector(compConnector, true); + + if (!canConnect(startConnector, endConnector)) { + return; + } + + setStartConnector(startConnector); + setEndConnector(endConnector); + + getModel().setConnectedComponents( + start.get(FigureConstants.MODEL), + end.get(FigureConstants.MODEL) + ); + } + + /** + * Returns the type of this path. + * + * @return The type of this path + */ + public PathModel.Type getLinerType() { + return (PathModel.Type) getModel().getPropertyPathConnType().getValue(); + } + + public void setLinerByType(PathModel.Type type) { + switch (type) { + case DIRECT: + resetPath(); + updateLiner(null); + break; + + case ELBOW: + if (!(getLiner() instanceof ElbowLiner)) { + resetPath(); + updateLiner(new ElbowLiner()); + } + + break; + + case SLANTED: + if (!(getLiner() instanceof SlantedLiner)) { + resetPath(); + updateLiner(new SlantedLiner()); + } + + break; + + case BEZIER: + if (!(getLiner() instanceof TupelBezierLiner)) { + initControlPoints(type); + updateLiner(new TupelBezierLiner()); + } + break; + case BEZIER_3: + if (!(getLiner() instanceof TripleBezierLiner)) { + initControlPoints(type); + updateLiner(new TripleBezierLiner()); + } + break; + case POLYPATH: + if (!(getLiner() instanceof PolyPathLiner)) { + initPolyPath(); + updateLiner(new PolyPathLiner()); + } + break; + default: + setLiner(null); + } + } + + private void updateLiner(Liner newLiner) { + setLiner(newLiner); + fireFigureHandlesChanged(); + fireAreaInvalidated(); + updateControlPoints(); + invalidate(); + getModel().propertiesChanged(this); + } + + private LengthProperty calculateLength() { + try { + LengthProperty property = getModel().getPropertyLength(); + + if (property != null) { + double length = (double) property.getValue(); + if (length <= 0.0) { + PointFigure start = ((LabeledPointFigure) getStartFigure()).getPresentationFigure(); + PointFigure end = ((LabeledPointFigure) getEndFigure()).getPresentationFigure(); + double startPosX + = start.getModel().getPropertyModelPositionX().getValueByUnit(LengthProperty.Unit.MM); + double startPosY + = start.getModel().getPropertyModelPositionY().getValueByUnit(LengthProperty.Unit.MM); + double endPosX + = end.getModel().getPropertyModelPositionX().getValueByUnit(LengthProperty.Unit.MM); + double endPosY + = end.getModel().getPropertyModelPositionY().getValueByUnit(LengthProperty.Unit.MM); + length = distance(startPosX, startPosY, endPosX, endPosY); + property.setValueAndUnit(length, LengthProperty.Unit.MM); + property.markChanged(); + } + } + + return property; + } + catch (IllegalArgumentException ex) { + LOG.error("calculateLength()", ex); + return null; + } + } + + @Override + public String getToolTipText(Point2D.Double p) { + return textGenerator.getToolTipText(getModel()); + } + + /** + * Checks whether two points can be connected with each other. + * + * @param start The start connector. + * @param end The end connector. + * @return {@code true}, if the two points can be connected, otherwise {@code false}. + */ + @Override + public boolean canConnect(Connector start, Connector end) { + ModelComponent modelStart = start.getOwner().get(FigureConstants.MODEL); + ModelComponent modelEnd = end.getOwner().get(FigureConstants.MODEL); + + return modelStart instanceof PointModel + && modelEnd instanceof PointModel + && modelStart != modelEnd; + } + + @Override + public Collection createHandles(int detailLevel) { + Collection handles = new ArrayList<>(); + + if (!isVisible()) { + return handles; + } + + // see BezierFigure + switch (detailLevel % 2) { + case -1: // Mouse hover handles + handles.add(new BezierOutlineHandle(this, true)); + break; + + case 0: // Mouse clicked + if (getLinerType() == PathModel.Type.POLYPATH) { + for (int i = 1; i < path.size() - 1; i++) { + handles.add(new BezierLinerControlPointHandle(this, i, BezierPath.C0_MASK)); + } + } + else { + if (cp1 != null) { + // Start point: handle to CP2 + handles.add(new BezierLinerControlPointHandle(this, 0, BezierPath.C2_MASK)); + if (cp2 != null) { + // End point: handle to CP3 + handles.add(new BezierLinerControlPointHandle(this, 1, BezierPath.C1_MASK)); + if (cp3 != null) { + // End point: handle to EP + handles.add(new BezierLinerControlPointHandle(this, 2, BezierPath.C1_MASK)); + } + } + } + } + + break; + + case 1: // double click + handles.add(new BezierOutlineHandle(this)); + break; + + default: + } + + return handles; + } + + @Override + public void lineout() { + if (getLiner() == null) { + path.invalidatePath(); + } + else { + getLiner().lineout(this); + } + } + + @Override + public void propertiesChanged(AttributesChangeEvent e) { + if (!e.getInitiator().equals(this)) { + setLinerByType((PathModel.Type) getModel().getPropertyPathConnType().getValue()); + calculateLength(); + lineout(); + } + + super.propertiesChanged(e); + } + + @Override + public void draw(Graphics2D g) { + if (drawingOptions.isBlocksVisible()) { + drawBlockDecoration(g); + } + drawRouteDecoration(g); + + super.draw(g); + } + + private void drawRouteDecoration(Graphics2D g) { + for (Map.Entry entry : getModel().getAllocationStates() + .entrySet()) { + VehicleModel vehicleModel = entry.getKey(); + switch (entry.getValue()) { + case CLAIMED: + drawDecoration( + g, + Strokes.PATH_ON_ROUTE, + transparentColor(vehicleModel.getDriveOrderColor(), 70) + ); + break; + case ALLOCATED: + drawDecoration(g, Strokes.PATH_ON_ROUTE, vehicleModel.getDriveOrderColor()); + break; + case ALLOCATED_WITHDRAWN: + drawDecoration(g, Strokes.PATH_ON_WITHDRAWN_ROUTE, Color.GRAY); + break; + default: + // Don't draw any decoration. + } + } + } + + private void drawBlockDecoration(Graphics2D g) { + for (BlockModel blockModel : getModel().getBlockModels()) { + drawDecoration(g, Strokes.BLOCK_ELEMENT, transparentColor(blockModel.getColor(), 192)); + } + } + + private Color transparentColor(Color color, int alpha) { + return new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha); + } + + private void drawDecoration(Graphics2D g, Stroke stroke, Color color) { + g.setStroke(stroke); + g.setColor(color); + g.draw(this.getShape()); + } + + @Override + public void updateDecorations() { + if (getModel() == null) { + return; + } + + set(AttributeKeys.START_DECORATION, navigableBackward() ? ARROW_BACKWARD : null); + set(AttributeKeys.END_DECORATION, navigableForward() ? ARROW_FORWARD : null); + + // Mark locked path. + if (Boolean.TRUE.equals(getModel().getPropertyLocked().getValue())) { + set(AttributeKeys.STROKE_COLOR, Color.red); + set(AttributeKeys.STROKE_DASHES, LOCKED_DASH); + } + else { + set(AttributeKeys.STROKE_COLOR, Color.black); + set(AttributeKeys.STROKE_DASHES, UNLOCKED_DASH); + } + } + + @Override + public void set(AttributeKey key, T newValue) { + super.set(key, newValue); + // if the ModelComponent is set we update the decorations, because + // properties like maxReverseVelocity could have changed + if (key.equals(FigureConstants.MODEL)) { + updateDecorations(); + } + } + + @Override + public void updateModel() { + if (calculateLength() == null) { + return; + } + + getModel().getPropertyMaxVelocity().markChanged(); + getModel().getPropertyMaxReverseVelocity().markChanged(); + + getModel().propertiesChanged(this); + } + + @Override + public void scaleModel(EventObject event) { + if (!(event.getSource() instanceof Origin)) { + return; + } + + Origin origin = (Origin) event.getSource(); + if (previousOrigin.getScaleX() == origin.getScaleX() + && previousOrigin.getScaleY() == origin.getScaleY()) { + return; + } + + if (isTupelBezier()) { // BEZIER + Point2D.Double scaledControlPoint = scaleControlPoint(cp1, origin); + path.set(0, BezierPath.C2_MASK, scaledControlPoint); + scaledControlPoint = scaleControlPoint(cp2, origin); + path.set(1, BezierPath.C1_MASK, scaledControlPoint); + } + else if (isTripleBezier()) { // BEZIER_3 + Point2D.Double scaledControlPoint = scaleControlPoint(cp1, origin); + path.set(0, BezierPath.C2_MASK, scaledControlPoint); + scaledControlPoint = scaleControlPoint(cp2, origin); + path.set(1, BezierPath.C1_MASK, scaledControlPoint); + scaledControlPoint = scaleControlPoint(cp3, origin); + path.set(1, BezierPath.C0_MASK, scaledControlPoint); + scaledControlPoint = scaleControlPoint(cp4, origin); + path.set(1, BezierPath.C2_MASK, scaledControlPoint); + path.set(2, BezierPath.C2_MASK, scaledControlPoint); + scaledControlPoint = scaleControlPoint(cp5, origin); + path.set(2, BezierPath.C1_MASK, scaledControlPoint); + } + + // Remember the new scale + previousOrigin.setScale(origin.getScaleX(), origin.getScaleY()); + updateControlPoints(); + } + + private boolean navigableForward() { + return getModel().getPropertyMaxVelocity().getValueByUnit(SpeedProperty.Unit.MM_S) > 0.0; + } + + private boolean navigableBackward() { + return getModel().getPropertyMaxReverseVelocity().getValueByUnit(SpeedProperty.Unit.MM_S) > 0.0; + } + + private Point2D.Double scaleControlPoint(Point2D.Double p, Origin newScale) { + return new Double( + (p.x * previousOrigin.getScaleX()) / newScale.getScaleX(), + (p.y * previousOrigin.getScaleY()) / newScale.getScaleY() + ); + } + + private boolean isTupelBezier() { + return cp1 != null && cp2 != null && cp3 == null && cp4 == null && cp5 == null; + } + + private boolean isTripleBezier() { + return cp1 != null && cp2 != null && cp3 != null && cp4 != null && cp5 != null; + } + + @Override // LineConnectionFigure + public PathConnection clone() { + PathConnection clone = (PathConnection) super.clone(); + + AbstractProperty pConnType = (AbstractProperty) clone.getModel().getPropertyPathConnType(); + if (getLiner() instanceof TupelBezierLiner) { + pConnType.setValue(PathModel.Type.BEZIER); + } + else if (getLiner() instanceof TripleBezierLiner) { + pConnType.setValue(PathModel.Type.BEZIER_3); + } + else if (getLiner() instanceof ElbowLiner) { + pConnType.setValue(PathModel.Type.ELBOW); + } + else if (getLiner() instanceof SlantedLiner) { + pConnType.setValue(PathModel.Type.SLANTED); + } + + return clone; + } + + private void initPolyPath() { + Point2D.Double sp = path.get(0, BezierPath.C0_MASK); + Point2D.Double ep = path.get(path.size() - 1, BezierPath.C0_MASK); + + if (sp.x == ep.x && sp.y == ep.y) { + path.clear(); + path.add(sp); + String[] coords = getModel().getPropertyPathControlPoints().getText().split("[;]"); + for (String coordinates : coords) { + String[] c = coordinates.split("[,]"); + int x = (int) java.lang.Double.parseDouble(c[0]); + int y = (int) java.lang.Double.parseDouble(c[1]); + path.add(x, y); + } + + path.add(ep); + } + else { + + path.clear(); + path.add(new BezierPath.Node(sp)); + path.add(new BezierPath.Node((sp.x + ep.x) / 2.0, (sp.y + ep.y) / 2.0)); + path.add(new BezierPath.Node(ep)); + } + } + + @Override // SimpleLineConnection + public boolean handleMouseClick(Point2D.Double p, MouseEvent evt, DrawingView drawingView) { + if (getLinerType() == PathModel.Type.POLYPATH) { + int addPointMask = MouseEvent.CTRL_DOWN_MASK; + int deletePointMask = MouseEvent.ALT_DOWN_MASK | MouseEvent.CTRL_DOWN_MASK; + if ((evt.getModifiersEx() & (addPointMask | deletePointMask)) == addPointMask) { + int index = path.findSegment(p, 10); + if (index != -1) { + path.add(index + 1, new BezierPath.Node(p)); + updateConnection(); + } + } + else if ((evt.getModifiersEx() & (deletePointMask | addPointMask)) == deletePointMask) { + int index = path.findSegment(p, 10); + if (index != -1) { + Point2D.Double[] points = path.toPolygonArray(); + path.clear(); + for (int i = 0; i < points.length; i++) { + if (i != index) { + path.add(new BezierPath.Node(points[i])); + } + } + updateConnection(); + } + } + } + return false; + } + + private List>> getCurrentDriveOrderClaim(VehicleModel vehicle) { + List>> result = new ArrayList<>(); + + boolean driveOrderEndFound = false; + for (Set> res : vehicle.getClaimedResources().getItems()) { + result.add(res); + + if (containsDriveOrderDestination(res, vehicle)) { + driveOrderEndFound = true; + break; + } + } + + if (driveOrderEndFound) { + return result; + } + else { + // With the end of the drive order not found, there is nothing from the current drive order in + // the claimed resources. + return List.of(); + } + } + + private boolean containsDriveOrderDestination( + Set> resources, + VehicleModel vehicle + ) { + if (vehicle.getDriveOrderDestination() == null) { + return false; + } + + return resources.stream() + .anyMatch( + resource -> Objects.equals( + resource.getName(), + vehicle.getDriveOrderDestination().getName() + ) + ); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/PointFigure.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/PointFigure.java new file mode 100644 index 0000000..41e5822 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/PointFigure.java @@ -0,0 +1,282 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Stroke; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import org.jhotdraw.geom.Geom; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.guing.base.AllocationState; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.drawing.DrawingOptions; +import org.opentcs.guing.common.components.drawing.Strokes; +import org.opentcs.guing.common.components.drawing.ZoomPoint; + +/** + * A figure that represents a decision point. + */ +public class PointFigure + extends + TCSFigure { + + /** + * A color for parking positions. + */ + private static final Color C_PARK = Color.BLUE; + /** + * A color for halt positions. + */ + private static final Color C_HALT = Color.LIGHT_GRAY; + /** + * The figure's diameter in drawing units (pixels at 100% zoom). + */ + private final int fDiameter; + /** + * The drawing options. + */ + private final DrawingOptions drawingOptions; + + /** + * Creates a new instance. + * + * @param model The model corresponding to this graphical object. + * @param drawingOptions The drawing options. + */ + @Inject + public PointFigure( + @Assisted + PointModel model, + DrawingOptions drawingOptions + ) { + super(model); + this.drawingOptions = requireNonNull(drawingOptions, "drawingOptions"); + + fDiameter = 10; + fDisplayBox = new Rectangle(fDiameter, fDiameter); + fZoomPoint = new ZoomPoint(0.5 * fDiameter, 0.5 * fDiameter); + } + + @Override + public PointModel getModel() { + return (PointModel) get(FigureConstants.MODEL); + } + + public Point center() { + return Geom.center(fDisplayBox); + } + + public Ellipse2D.Double getShape() { + Rectangle2D r2 = fDisplayBox.getBounds2D(); + Ellipse2D.Double shape + = new Ellipse2D.Double(r2.getX(), r2.getY(), fDiameter - 1, fDiameter - 1); + return shape; + } + + @Override // Figure + public Rectangle2D.Double getBounds() { + Rectangle2D r2 = fDisplayBox.getBounds2D(); + Rectangle2D.Double r2d = new Rectangle2D.Double(); + r2d.setRect(r2); + + return r2d; + } + + @Override // Figure + public Object getTransformRestoreData() { + // Never used? + return fDisplayBox.clone(); + } + + @Override // Figure + public void restoreTransformTo(Object restoreData) { + // Never used? + Rectangle r = (Rectangle) restoreData; + fDisplayBox.x = r.x; + fDisplayBox.y = r.y; + fDisplayBox.width = r.width; + fDisplayBox.height = r.height; + fZoomPoint.setX(r.x + 0.5 * r.width); + fZoomPoint.setY(r.y + 0.5 * r.height); + } + + @Override // Figure + public void transform(AffineTransform tx) { + Point2D center = getZoomPoint().getPixelLocationExactly(); + Point2D lead = new Point2D.Double(); // not used + setBounds( + (Point2D.Double) tx.transform(center, center), + (Point2D.Double) tx.transform(lead, lead) + ); + } + + @Override // AbstractFigure + public void setBounds(Point2D.Double anchor, Point2D.Double lead) { + fZoomPoint.setX(anchor.x); + fZoomPoint.setY(anchor.y); + fDisplayBox.x = (int) (anchor.x - 0.5 * fDiameter); + fDisplayBox.y = (int) (anchor.y - 0.5 * fDiameter); + } + + @Override + protected void drawFigure(Graphics2D g) { + if (drawingOptions.isBlocksVisible()) { + drawBlockDecoration(g); + } + drawRouteDecoration(g); + + super.drawFigure(g); + } + + private void drawRouteDecoration(Graphics2D g) { + for (Map.Entry entry : getModel().getAllocationStates() + .entrySet()) { + VehicleModel vehicleModel = entry.getKey(); + switch (entry.getValue()) { + case CLAIMED: + drawDecoration( + g, + Strokes.PATH_ON_ROUTE, + transparentColor(vehicleModel.getDriveOrderColor(), 70) + ); + break; + case ALLOCATED: + drawDecoration(g, Strokes.PATH_ON_ROUTE, vehicleModel.getDriveOrderColor()); + break; + case ALLOCATED_WITHDRAWN: + drawDecoration(g, Strokes.PATH_ON_WITHDRAWN_ROUTE, Color.GRAY); + break; + default: + // Don't draw any decoration. + } + } + } + + private void drawBlockDecoration(Graphics2D g) { + for (BlockModel blockModel : getModel().getBlockModels()) { + drawDecoration(g, Strokes.BLOCK_ELEMENT, transparentColor(blockModel.getColor(), 192)); + } + } + + private Color transparentColor(Color color, int alpha) { + return new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha); + } + + private void drawDecoration(Graphics2D g, Stroke stroke, Color color) { + g.setStroke(stroke); + g.setColor(color); + g.draw(this.getShape()); + } + + @Override + protected void drawFill(Graphics2D g) { + Rectangle rect = fDisplayBox; + + if (getModel().getPropertyType().getValue() == PointModel.Type.PARK) { + g.setColor(C_PARK); + } + else { + g.setColor(C_HALT); + } + + if (rect.width > 0 && rect.height > 0) { + g.fillOval(rect.x, rect.y, rect.width, rect.height); + } + + if (getModel().getPropertyType().getValue() == PointModel.Type.PARK) { + g.setColor(Color.white); + Font oldFont = g.getFont(); + Font newFont = new Font(Font.DIALOG, Font.BOLD, 7); + g.setFont(newFont); + g.drawString("P", rect.x + 3, rect.y + rect.height - 3); + g.setFont(oldFont); + } + } + + @Override // AbstractAttributedFigure + protected void drawStroke(Graphics2D g) { + Rectangle r = fDisplayBox; + + if (r.width > 0 && r.height > 0) { + g.drawOval(r.x, r.y, r.width - 1, r.height - 1); + } + } + + @Override // AbstractAttributedDecoratedFigure + public PointFigure clone() { + PointFigure thatFigure = (PointFigure) super.clone(); + thatFigure.setZoomPoint(new ZoomPoint(fZoomPoint.getX(), fZoomPoint.getY())); + + return thatFigure; + } + + @Override // AbstractFigure + public int getLayer() { + return getModel().getPropertyLayerWrapper().getValue().getLayer().getOrdinal(); + } + + @Override + public boolean isVisible() { + return super.isVisible() + && getModel().getPropertyLayerWrapper().getValue().getLayer().isVisible() + && getModel().getPropertyLayerWrapper().getValue().getLayerGroup().isVisible(); + } + + private List>> getCurrentDriveOrderClaim(VehicleModel vehicle) { + List>> result = new ArrayList<>(); + + boolean driveOrderEndFound = false; + for (Set> res : vehicle.getClaimedResources().getItems()) { + result.add(res); + + if (containsDriveOrderDestination(res, vehicle)) { + driveOrderEndFound = true; + break; + } + } + + if (driveOrderEndFound) { + return result; + } + else { + // With the end of the drive order not found, there is nothing from the current drive order in + // the claimed resources. + return List.of(); + } + } + + private boolean containsDriveOrderDestination( + Set> resources, + VehicleModel vehicle + ) { + if (vehicle.getDriveOrderDestination() == null) { + return false; + } + + return resources.stream() + .anyMatch( + resource -> Objects.equals( + resource.getName(), + vehicle.getDriveOrderDestination().getName() + ) + ); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/SimpleLineConnection.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/SimpleLineConnection.java new file mode 100644 index 0000000..1309632 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/SimpleLineConnection.java @@ -0,0 +1,172 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures; + +import java.awt.Color; +import java.awt.Shape; +import java.awt.event.MouseEvent; +import java.awt.geom.Point2D; +import java.util.EventObject; +import org.jhotdraw.draw.AttributeKey; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.LineConnectionFigure; +import org.jhotdraw.draw.connector.Connector; +import org.jhotdraw.draw.decoration.ArrowTip; +import org.jhotdraw.geom.BezierPath; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.event.AttributesChangeListener; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.AbstractConnection; +import org.opentcs.guing.common.components.drawing.course.OriginChangeListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + */ +public abstract class SimpleLineConnection + extends + LineConnectionFigure + implements + ModelBasedFigure, + AttributesChangeListener, + OriginChangeListener { + + protected static final AttributeKey FILL_COLOR + = new AttributeKey<>("FillColor", Color.class); + protected static final AttributeKey STROKE_COLOR + = new AttributeKey<>("StrokeColor", Color.class); + protected static final ArrowTip ARROW_FORWARD + = new ArrowTip(0.35, 12.0, 11.3, true, true, true); + protected static final ArrowTip ARROW_BACKWARD + = new ArrowTip(0.35, 12.0, 11.3, true, true, false); + private static final Logger LOG = LoggerFactory.getLogger(SimpleLineConnection.class); + + /** + * Creates a new instance. + * + * @param model The model corresponding to this graphical object. + */ + @SuppressWarnings("this-escape") + public SimpleLineConnection(AbstractConnection model) { + set(FigureConstants.MODEL, model); + initConnectionFigure(); + } + + /** + * Initialise this figure. + */ + protected final void initConnectionFigure() { + updateDecorations(); + } + + @Override + public AbstractConnection getModel() { + return (AbstractConnection) get(FigureConstants.MODEL); + } + + /** + * Return the shape. + * + * @return the shape. + */ + public Shape getShape() { + return path; + } + + @Override // BezierFigure + protected BezierPath getCappedPath() { + // Workaround for NullPointerException in BezierFigure.getCappedPath() + try { + return super.getCappedPath(); + } + catch (NullPointerException ex) { + LOG.warn("", ex); + return path.clone(); + } + } + + /** + * Update the properties of the model. + */ + public abstract void updateModel(); + + /** + * Scales the model coodinates accodring to changes to the layout scale. + * + * @param event The event containing the layout scale change. + */ + public abstract void scaleModel(EventObject event); + + /** + * Calculates the euclid distance between the start position and the end position. + * + * @param startPosX The x coordiante of the start position. + * @param startPosY The y coordiante of the start position. + * @param endPosX The x coordinate of the end position. + * @param endPosY The y coordinate of the end position. + * @return the euclid distance between start and end point rounded to the next integer. + */ + protected double distance(double startPosX, double startPosY, double endPosX, double endPosY) { + double dX = startPosX - endPosX; + double dY = startPosY - endPosY; + double dist = Math.sqrt(dX * dX + dY * dY); + dist = Math.floor(dist + 0.5); // round to an integer value. + + return dist; + } + + public void updateDecorations() { + } + + @Override // LineConnectionFigure + protected void handleConnect(Connector start, Connector end) { + if (start != null && end != null) { + ModelComponent startModel = start.getOwner().get(FigureConstants.MODEL); + ModelComponent endModel = end.getOwner().get(FigureConstants.MODEL); + getModel().setConnectedComponents(startModel, endModel); + updateModel(); + } + } + + @Override // LineConnectionFigure + protected void handleDisconnect(Connector start, Connector end) { + super.handleDisconnect(start, end); + + getModel().removingConnection(); + } + + @Override // LineConnectionFigure + public boolean handleMouseClick(Point2D.Double p, MouseEvent evt, DrawingView drawingView) { + return false; + } + + @Override // AttributesChangeListener + public void propertiesChanged(AttributesChangeEvent e) { + if (!e.getInitiator().equals(this)) { + updateDecorations(); + fireFigureChanged(getDrawingArea()); + } + } + + @Override // OriginChangeListener + public void originLocationChanged(EventObject event) { + } + + @Override // OriginChangeListener + public void originScaleChanged(EventObject event) { + scaleModel(event); + } + + @Override + public SimpleLineConnection clone() { + try { + SimpleLineConnection clone = (SimpleLineConnection) super.clone(); + clone.set(FigureConstants.MODEL, getModel().clone()); + + return clone; + } + catch (CloneNotSupportedException exc) { + throw new IllegalStateException("Unexpected exception encountered", exc); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/TCSFigure.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/TCSFigure.java new file mode 100644 index 0000000..266021b --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/TCSFigure.java @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures; + +import java.awt.Rectangle; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import org.jhotdraw.draw.AbstractAttributedDecoratedFigure; +import org.jhotdraw.geom.Geom; +import org.opentcs.guing.base.model.DrawnModelComponent; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.common.components.drawing.ZoomPoint; + +/** + * Base implementation for figures. + */ +public abstract class TCSFigure + extends + AbstractAttributedDecoratedFigure + implements + ModelBasedFigure { + + /** + * The enclosing rectangle. + */ + protected Rectangle fDisplayBox; + /** + * The exact position for the middle of the figure. + */ + protected ZoomPoint fZoomPoint; + + /** + * Creates a new instance. + * + * @param modelComponent The model corresponding to this graphical object. + */ + @SuppressWarnings("this-escape") + public TCSFigure(ModelComponent modelComponent) { + super(); + set(FigureConstants.MODEL, modelComponent); + } + + /** + * Returns the exact point at the middle of the figure. + * + * @return the exact point at the middle of the figure. + */ + public ZoomPoint getZoomPoint() { + return fZoomPoint; + } + + /** + * Sets the zoom point. + * + * @param zoomPoint The point at the middle of the figure. + */ + public void setZoomPoint(ZoomPoint zoomPoint) { + fZoomPoint = zoomPoint; + } + + /** + * Clones this figure, also clones the associated model component. + * + * @return + */ + @Override // AbstractAttributedDecoratedFigure + public TCSFigure clone() { + try { + TCSFigure that = (TCSFigure) super.clone(); + that.fDisplayBox = new Rectangle(fDisplayBox); + that.setModel(getModel().clone()); + + return that; + } + catch (CloneNotSupportedException ex) { + throw new Error("Cannot clone() unexpectedly", ex); + } + } + + @Override + protected Rectangle2D.Double getFigureDrawingArea() { + // Add some margin to the drawing area of the figure, so the + // drawing area scrolls a little earlier + Rectangle2D.Double drawingArea = super.getFigureDrawingArea(); + // if we add these two lines the Drawing becomes grey, if we start + // the application in operating mode.. +// drawingArea.height += 50; +// drawingArea.width += 100; + + return drawingArea; + } + + @Override + public DrawnModelComponent getModel() { + return (DrawnModelComponent) get(FigureConstants.MODEL); + } + + public void setModel(ModelComponent model) { + set(FigureConstants.MODEL, model); + } + + /** + * Returns the enclosing rectangle. + * + * @return The enclosing rectangle. + */ + public Rectangle displayBox() { + return new Rectangle(fDisplayBox); + } + + @Override + public boolean figureContains(Point2D.Double p) { + Rectangle2D.Double r2d = getBounds(); + // Grow for connectors + Geom.grow(r2d, 10d, 10d); + + return (r2d.contains(p)); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/TCSLabelFigure.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/TCSLabelFigure.java new file mode 100644 index 0000000..6f7a52a --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/TCSLabelFigure.java @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures; + +import java.awt.geom.Point2D; +import java.awt.geom.Point2D.Double; +import org.jhotdraw.draw.LabelFigure; +import org.jhotdraw.draw.event.FigureEvent; +import org.opentcs.data.model.visualization.ElementPropKeys; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A label belonging to another {@code Figure} that shows the name of the affiliated object in the + * kernel model. + */ +public class TCSLabelFigure + extends + LabelFigure { + + /** + * The default x label offset for label figures. + */ + public static final int DEFAULT_LABEL_OFFSET_X = -10; + + /** + * The default y label offset for label figures. + */ + public static final int DEFAULT_LABEL_OFFSET_Y = -20; + + private Point2D.Double fOffset; + private LabeledFigure fParent; + private boolean isLabelVisible = true; + + /** + * Creates a new instance. + */ + public TCSLabelFigure() { + this("?"); + } + + /** + * Creates a new instance. + * + * @param text The text of the label + */ + public TCSLabelFigure(String text) { + super(text); + fOffset = new Point2D.Double(DEFAULT_LABEL_OFFSET_X, DEFAULT_LABEL_OFFSET_Y); + } + + /** + * Sets the visibility flag of the label. + * + * @param visible The visibility flag. + */ + public void setLabelVisible(boolean visible) { + isLabelVisible = visible; + + if (visible) { + setText(fParent.getPresentationFigure().getModel().getName()); + } + else { + setText(""); + } + + invalidate(); + validate(); + } + + /** + * Sets the position relative to the {@code Figure}. + * + * @param posX The X-Offset of the label. + * @param posY The Y-Offset of the label. + */ + public void setOffset(int posX, int posY) { + fOffset = new Point2D.Double(posX, posY); + } + + public Double getOffset() { + return fOffset; + } + + void setParent(LabeledFigure parent) { + fParent = parent; + } + + @Override // AbstractFigure + public void changed() { + // Called when the figure has changed - movement with MouseDragger. + super.changed(); + + if (fParent != null) { + TCSFigure figure = fParent.getPresentationFigure(); + ModelComponent model = figure.getModel(); + + Point2D.Double newOffset = new Point2D.Double( + getBounds().getX() - figure.getBounds().x, + getBounds().getY() - figure.getBounds().y + ); + + if (newOffset.x != fOffset.x || newOffset.y != fOffset.y) { + fOffset = newOffset; + StringProperty sp + = (StringProperty) model.getProperty(ElementPropKeys.POINT_LABEL_OFFSET_X); + + if (sp != null) { + sp.setText(String.format("%d", (long) newOffset.x)); + sp.markChanged(); + } + + sp = (StringProperty) model.getProperty(ElementPropKeys.POINT_LABEL_OFFSET_Y); + + if (sp != null) { + sp.setText(String.format("%d", (long) newOffset.y)); + sp.markChanged(); + } + model.propertiesChanged(fParent); + } + } + } + + @Override // AbstractFigure + public int getLayer() { + return 1; // stay above other figures ? + } + + @Override // LabelFigure + public void figureChanged(FigureEvent event) { + if (event.getFigure() instanceof LabeledFigure) { + LabeledFigure lf = (LabeledFigure) event.getFigure(); + TCSFigure figure = lf.getPresentationFigure(); + ModelComponent model = figure.getModel(); + String name = model.getName(); + setText(name); + + if (isLabelVisible) { + invalidate(); + validate(); + } + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/ToolTipTextGenerator.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/ToolTipTextGenerator.java new file mode 100644 index 0000000..8b8f240 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/ToolTipTextGenerator.java @@ -0,0 +1,216 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.guing.base.model.ModelComponent.MISCELLANEOUS; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import org.opentcs.guing.base.components.properties.type.KeyValueProperty; +import org.opentcs.guing.base.components.properties.type.KeyValueSetProperty; +import org.opentcs.guing.base.model.FigureDecorationDetails; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * Generates tooltip texts for various model elements. + */ +public class ToolTipTextGenerator { + + /** + * The model manager. + */ + private final ModelManager modelManager; + + /** + * Create a new instance. + * + * @param modelManager The model manager to use. + */ + @Inject + public ToolTipTextGenerator(ModelManager modelManager) { + this.modelManager = requireNonNull(modelManager, "modelManager"); + } + + /** + * Generate a tooltip text for a vehicle model. + * + * @param model The vehicle model. + * @return A tooltip text for the model element. + */ + public String getToolTipText(VehicleModel model) { + return ""; + } + + /** + * Generate a tooltip text for a point model. + * + * @param model The point model. + * @return A tooltip text for the model element. + */ + public String getToolTipText(PointModel model) { + String pointDesc = model.getDescription(); + StringBuilder sb = new StringBuilder(""); + sb.append(pointDesc).append(" ").append("").append(model.getName()).append(""); + + appendBlockInfo(sb, model); + appendMiscProps(sb, model); + appendAllocatingVehicle(sb, model); + + sb.append(""); + + return sb.toString(); + } + + /** + * Generate a tooltip text for a location model. + * + * @param model The location model. + * @return A tooltip text for the model element. + */ + public String getToolTipText(LocationModel model) { + String locationDesc = model.getDescription(); + StringBuilder sb = new StringBuilder(""); + sb.append(locationDesc).append(" ").append("").append(model.getName()).append(""); + + appendBlockInfo(sb, model); + appendPeripheralInformation(sb, model); + appendMiscProps(sb, model); + appendAllocatingVehicle(sb, model); + + sb.append(""); + + return sb.toString(); + } + + /** + * Generate a tooltip text for a path model. + * + * @param model The path model. + * @return A tooltip text for the model element. + */ + public String getToolTipText(PathModel model) { + String pathDesc = model.getDescription(); + StringBuilder sb = new StringBuilder(""); + sb.append(pathDesc).append(" ").append("").append(model.getName()).append(""); + + appendBlockInfo(sb, model); + appendMiscProps(sb, model); + appendAllocatingVehicle(sb, model); + + sb.append(""); + + return sb.toString(); + } + + /** + * Generate a tooltip text for a link model. + * + * @param model The link model. + * @return A tooltip text for the model element. + */ + public String getToolTipText(LinkModel model) { + return new StringBuilder("") + .append(model.getDescription()).append(" ") + .append("").append(model.getName()).append("") + .append("").toString(); + } + + private void appendBlockInfo(StringBuilder sb, ModelComponent component) { + sb.append(blocksToToolTipContent(getBlocksWith(component))); + } + + private void appendBlockInfo(StringBuilder sb, LocationModel location) { + List links = modelManager.getModel().getLinkModels(); + links = links.stream() + .filter(link -> link.getLocation().getName().equals(location.getName())) + .collect(Collectors.toList()); + + List partOfBlocks = new ArrayList<>(); + for (LinkModel link : links) { + partOfBlocks.addAll(getBlocksWith(link.getPoint())); + } + + sb.append(blocksToToolTipContent(partOfBlocks)); + } + + protected void appendMiscProps(StringBuilder sb, ModelComponent component) { + KeyValueSetProperty miscProps = (KeyValueSetProperty) component.getProperty(MISCELLANEOUS); + + if (miscProps.getItems().isEmpty()) { + return; + } + + sb.append("
    \n"); + sb.append(miscProps.getDescription()).append(": \n"); + sb.append("
      \n"); + miscProps.getItems().stream() + .sorted(Comparator.comparing(KeyValueProperty::getKey)) + .forEach(kvp -> { + sb.append("
    • ") + .append(kvp.getKey()).append(": ").append(kvp.getValue()) + .append("
    • \n"); + }); + sb.append("
    \n"); + } + + protected void appendAllocatingVehicle(StringBuilder sb, FigureDecorationDetails figure) { + // Displaying information about allocating vehicles is only relevant in the Operations Desk + // application, which is why this method is empty here. + } + + private void appendPeripheralInformation(StringBuilder sb, LocationModel model) { + sb.append("
    "); + sb.append("
    ").append(model.getPropertyPeripheralReservationToken().getDescription()) + .append(": ") + .append(model.getPropertyPeripheralReservationToken().getText()); + sb.append("
    ").append(model.getPropertyPeripheralState().getDescription()) + .append(": ") + .append(model.getPropertyPeripheralState().getText()); + sb.append("
    ").append(model.getPropertyPeripheralProcState().getDescription()) + .append(": ") + .append(model.getPropertyPeripheralProcState().getText()); + sb.append("
    ").append(model.getPropertyPeripheralJob().getDescription()) + .append(": ") + .append(model.getPropertyPeripheralJob().getText()); + } + + private List getBlocksWith(ModelComponent component) { + List result = new ArrayList<>(); + List blocks = modelManager.getModel().getBlockModels(); + for (BlockModel block : blocks) { + if (block.contains(component)) { + result.add(block); + } + } + return result; + } + + private String blocksToToolTipContent(List blocks) { + if (blocks.isEmpty()) { + return ""; + } + + blocks.sort((b1, b2) -> b1.getName().compareTo(b2.getName())); + + String desc = blocks.get(0).getDescription(); + StringBuilder sb = new StringBuilder("
    ") + .append(desc).append(": "); + for (BlockModel block : blocks) { + sb.append(block.getName()).append(", "); + } + sb.delete(sb.lastIndexOf(", "), sb.length()); + + return sb.toString(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/decoration/PointOutlineHandle.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/decoration/PointOutlineHandle.java new file mode 100644 index 0000000..0d9b11b --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/decoration/PointOutlineHandle.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures.decoration; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Paint; +import java.awt.RadialGradientPaint; +import java.awt.Shape; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import org.jhotdraw.draw.AttributeKeys; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.handle.BoundsOutlineHandle; +import org.opentcs.guing.common.components.drawing.figures.PointFigure; + +/** + */ +public class PointOutlineHandle + extends + BoundsOutlineHandle { + + public PointOutlineHandle(Figure owner) { + super(owner); + } + + @Override + public void draw(Graphics2D g) { + PointFigure pf = (PointFigure) getOwner(); + Shape bounds = pf.getShape(); + + if (getOwner().get(AttributeKeys.TRANSFORM) != null) { + bounds = getOwner().get(AttributeKeys.TRANSFORM).createTransformedShape(bounds); + } + + if (view != null) { + bounds = view.getDrawingToViewTransform().createTransformedShape(bounds); + Rectangle2D bounds2D = bounds.getBounds2D(); + float centerX = (float) bounds2D.getCenterX(); + float centerY = (float) bounds2D.getCenterY(); + Point2D center = new Point2D.Float(centerX, centerY); + float radius = 10.0f; + float[] dist = {0.1f, 0.9f}; + Color[] colors = {Color.CYAN, Color.BLUE}; + + RadialGradientPaint radialGradientPaint + = new RadialGradientPaint(center, radius, dist, colors); + Paint oldPaint = g.getPaint(); + g.setPaint(radialGradientPaint); + + g.fill(bounds); + g.setPaint(oldPaint); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/liner/BezierLinerControlPointHandle.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/liner/BezierLinerControlPointHandle.java new file mode 100644 index 0000000..6253267 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/liner/BezierLinerControlPointHandle.java @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures.liner; + +import java.awt.Point; +import java.util.Objects; +import org.jhotdraw.draw.BezierFigure; +import org.jhotdraw.draw.Drawing; + +/** + * A Handle which allows to interactively change a control point + */ +public class BezierLinerControlPointHandle + extends + org.jhotdraw.draw.handle.BezierControlPointHandle { + + public BezierLinerControlPointHandle(BezierFigure owner, int index, int coord) { + super(owner, index, coord); + } + + @Override // BezierControlPointHandle + public void trackEnd(Point anchor, Point lead, int modifiersEx) { + super.trackEnd(anchor, lead, modifiersEx); + // Fire edit event to update the control points of the Path figure + Drawing drawing = Objects.requireNonNull(view.getDrawing()); + drawing.fireUndoableEditHappened(new BezierLinerEdit(getBezierFigure())); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/liner/BezierLinerEdit.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/liner/BezierLinerEdit.java new file mode 100644 index 0000000..8a170dd --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/liner/BezierLinerEdit.java @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures.liner; + +import javax.swing.SwingUtilities; +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; +import org.jhotdraw.draw.BezierFigure; +import org.jhotdraw.draw.event.BezierNodeEdit; +import org.jhotdraw.geom.BezierPath; +import org.opentcs.guing.common.components.drawing.figures.PathConnection; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + */ +public class BezierLinerEdit + extends + javax.swing.undo.AbstractUndoableEdit { + + private final BezierFigure fOwner; + private final BezierNodeEdit fNodeEdit; + + /** + * @param owner A path + */ + public BezierLinerEdit(BezierFigure owner) { + fOwner = owner; + BezierPath.Node node = owner.getNode(0); + fNodeEdit = new BezierNodeEdit(owner, 0, node, node); + } + + /** + * + * @return The associated PathConnection + */ + public BezierFigure getOwner() { + return fOwner; + } + + @Override // AbstractUndoableEdit + public boolean isSignificant() { + return false; + } + + @Override // AbstractUndoableEdit + public String getPresentationName() { + return ResourceBundleUtil.getBundle(I18nPlantOverview.MISC_PATH) + .getString("bezierLinerEdit.presentationName"); + } + + @Override // AbstractUndoableEdit + public void redo() + throws CannotRedoException { + fNodeEdit.redo(); + updateProperties(); + } + + @Override // AbstractUndoableEdit + public void undo() + throws CannotUndoException { + fNodeEdit.undo(); + updateProperties(); + } + + private void updateProperties() { + SwingUtilities.invokeLater(() -> { + PathConnection path = (PathConnection) fOwner; + path.updateControlPoints(); + path.getModel().getPropertyPathControlPoints().markChanged(); + path.getModel().propertiesChanged(path); + }); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/liner/PolyPathLiner.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/liner/PolyPathLiner.java new file mode 100644 index 0000000..0a3c8ee --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/liner/PolyPathLiner.java @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures.liner; + +import java.util.Collection; +import java.util.Collections; +import org.jhotdraw.draw.ConnectionFigure; +import org.jhotdraw.draw.LineConnectionFigure; +import org.jhotdraw.draw.handle.Handle; +import org.jhotdraw.draw.liner.Liner; +import org.jhotdraw.geom.BezierPath; + +/** + */ +public class PolyPathLiner + implements + org.jhotdraw.draw.liner.Liner { + + public PolyPathLiner() { + } + + @Override + public void lineout(ConnectionFigure figure) { + BezierPath path = ((LineConnectionFigure) figure).getBezierPath(); + + if (path != null) { + path.invalidatePath(); + } + + } + + @Override + public Collection createHandles(BezierPath path) { + return Collections.emptyList(); + } + + @Override // Object + public Liner clone() { + try { + return (Liner) super.clone(); + } + catch (CloneNotSupportedException ex) { + InternalError error = new InternalError(ex.getMessage()); + error.initCause(ex); + throw error; + } + } + +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/liner/TripleBezierLiner.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/liner/TripleBezierLiner.java new file mode 100644 index 0000000..ef3b269 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/liner/TripleBezierLiner.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures.liner; + +import java.util.Collection; +import java.util.Collections; +import org.jhotdraw.draw.ConnectionFigure; +import org.jhotdraw.draw.LineConnectionFigure; +import org.jhotdraw.draw.handle.Handle; +import org.jhotdraw.draw.liner.Liner; +import org.jhotdraw.geom.BezierPath; + +/** + * A {@link Liner} that constrains a connection to a fourth-order curved + * line. + */ +public class TripleBezierLiner + implements + org.jhotdraw.draw.liner.Liner { + + /** + * Creates a new instance. + */ + public TripleBezierLiner() { + } + + @Override // Liner + public Collection createHandles(BezierPath path) { + return Collections.emptyList(); + } + + @Override // Liner + public void lineout(ConnectionFigure figure) { + BezierPath path = ((LineConnectionFigure) figure).getBezierPath(); + + if (path != null) { + path.invalidatePath(); + } + } + + @Override // Object + public Liner clone() { + try { + return (Liner) super.clone(); + } + catch (CloneNotSupportedException ex) { + InternalError error = new InternalError(ex.getMessage()); + error.initCause(ex); + throw error; + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/liner/TupelBezierLiner.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/liner/TupelBezierLiner.java new file mode 100644 index 0000000..b5400f4 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/drawing/figures/liner/TupelBezierLiner.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures.liner; + +import java.util.Collection; +import java.util.Collections; +import org.jhotdraw.draw.ConnectionFigure; +import org.jhotdraw.draw.LineConnectionFigure; +import org.jhotdraw.draw.handle.Handle; +import org.jhotdraw.draw.liner.Liner; +import org.jhotdraw.geom.BezierPath; + +/** + * A {@link Liner} that constrains a connection to a quadratic or cubic curved + * line. + */ +public class TupelBezierLiner + implements + org.jhotdraw.draw.liner.Liner { + + /** + * Creates a new instance. + */ + public TupelBezierLiner() { + } + + @Override // Liner + public Collection createHandles(BezierPath path) { + return Collections.emptyList(); + } + + @Override // Liner + public void lineout(ConnectionFigure figure) { + BezierPath path = ((LineConnectionFigure) figure).getBezierPath(); + + if (path != null) { + path.invalidatePath(); + } + } + + @Override // Object + public Liner clone() { + try { + return (Liner) super.clone(); + } + catch (CloneNotSupportedException ex) { + InternalError error = new InternalError(ex.getMessage()); + error.initCause(ex); + throw error; + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/AbstractLayerGroupsTableModel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/AbstractLayerGroupsTableModel.java new file mode 100644 index 0000000..f9af1c2 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/AbstractLayerGroupsTableModel.java @@ -0,0 +1,177 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.layer; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.stream.Collectors; +import javax.swing.table.AbstractTableModel; +import org.opentcs.data.model.visualization.LayerGroup; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.I18nPlantOverview; + +/** + * A table model for layer groups. + */ +public abstract class AbstractLayerGroupsTableModel + extends + AbstractTableModel + implements + LayerGroupChangeListener { + + /** + * The number of the "ID" column. + */ + public static final int COLUMN_ID = 0; + /** + * The number of the "Name" column. + */ + public static final int COLUMN_NAME = 1; + /** + * The number of the "Visible" column. + */ + public static final int COLUMN_VISIBLE = 2; + /** + * The resource bundle to use. + */ + private static final ResourceBundle BUNDLE + = ResourceBundle.getBundle(I18nPlantOverview.LAYERS_PATH); + /** + * The column names. + */ + private static final String[] COLUMN_NAMES + = new String[]{ + BUNDLE.getString("abstractLayerGroupsTableModel.column_id.headerText"), + BUNDLE.getString("abstractLayerGroupsTableModel.column_name.headerText"), + BUNDLE.getString("abstractLayerGroupsTableModel.column_visible.headerText") + }; + /** + * The column classes. + */ + private static final Class[] COLUMN_CLASSES + = new Class[]{ + Integer.class, + String.class, + Boolean.class + }; + /** + * The model manager. + */ + private final ModelManager modelManager; + /** + * The layer group editor. + */ + private final LayerGroupEditor layerGroupEditor; + + /** + * Creates a new instance. + * + * @param modelManager The model manager. + * @param layerGroupEditor The layer group editor. + */ + public AbstractLayerGroupsTableModel( + ModelManager modelManager, + LayerGroupEditor layerGroupEditor + ) { + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.layerGroupEditor = requireNonNull(layerGroupEditor, "layerGroupEditor"); + } + + @Override + public int getRowCount() { + return layerGroups().size(); + } + + @Override + public int getColumnCount() { + return COLUMN_NAMES.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= getRowCount()) { + return null; + } + + LayerGroup entry = layerGroups().get(rowIndex); + switch (columnIndex) { + case COLUMN_ID: + return entry.getId(); + case COLUMN_NAME: + return entry.getName(); + case COLUMN_VISIBLE: + return entry.isVisible(); + default: + throw new IllegalArgumentException("Invalid column index: " + columnIndex); + } + } + + @Override + public String getColumnName(int columnIndex) { + return COLUMN_NAMES[columnIndex]; + } + + @Override + public Class getColumnClass(int columnIndex) { + return COLUMN_CLASSES[columnIndex]; + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + switch (columnIndex) { + case COLUMN_ID: + return false; + case COLUMN_NAME: + return isNameColumnEditable(); + case COLUMN_VISIBLE: + return isVisibleColumnEditable(); + default: + throw new IllegalArgumentException("Invalid column index: " + columnIndex); + } + } + + @Override + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= getRowCount()) { + return; + } + + if (aValue == null) { + return; + } + + LayerGroup entry = layerGroups().get(rowIndex); + switch (columnIndex) { + case COLUMN_ID: + // Do nothing. + break; + case COLUMN_NAME: + layerGroupEditor.setGroupName(entry.getId(), aValue.toString()); + break; + case COLUMN_VISIBLE: + layerGroupEditor.setGroupVisible(entry.getId(), (boolean) aValue); + break; + default: + throw new IllegalArgumentException("Invalid column index: " + columnIndex); + } + } + + public LayerGroup getDataAt(int index) { + return layerGroups().get(index); + } + + protected abstract boolean isNameColumnEditable(); + + protected abstract boolean isVisibleColumnEditable(); + + private List layerGroups() { + return getLayerGroups().values().stream().collect(Collectors.toList()); + } + + private Map getLayerGroups() { + return modelManager.getModel().getLayoutModel().getPropertyLayerGroups().getValue(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/DefaultLayerManager.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/DefaultLayerManager.java new file mode 100644 index 0000000..ed3e69c --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/DefaultLayerManager.java @@ -0,0 +1,406 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.layer; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import javax.swing.SwingUtilities; +import org.jhotdraw.draw.Drawing; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.model.visualization.Layer; +import org.opentcs.data.model.visualization.LayerGroup; +import org.opentcs.guing.base.components.layer.LayerWrapper; +import org.opentcs.guing.base.model.DrawnModelComponent; +import org.opentcs.guing.common.application.ViewManager; +import org.opentcs.guing.common.event.SystemModelTransitionEvent; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.util.event.EventBus; + +/** + * The default implementation of {@link LayerManager}. + */ +public class DefaultLayerManager + implements + LayerManager, + LayerGroupManager { + + /** + * The sets of model components mapped to the IDs of the layers the model components are drawn on. + */ + private final Map> components = new HashMap<>(); + /** + * The view manager. + */ + private final ViewManager viewManager; + /** + * The application's event bus. + */ + private final EventBus eventBus; + /** + * The listeners for layer group data changes. + */ + private final Set layerGroupChangeListeners = new HashSet<>(); + /** + * The listener for layer data changes. + */ + private LayerChangeListener layerChangeListener; + /** + * The system model we're working with. + */ + private SystemModel systemModel; + /** + * Whether this instance is initialized or not. + */ + private boolean initialized; + + @Inject + public DefaultLayerManager( + ViewManager viewManager, + @ApplicationEventBus + EventBus eventBus + ) { + this.viewManager = requireNonNull(viewManager, "viewManager"); + this.eventBus = requireNonNull(eventBus, "eventBus"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + eventBus.subscribe(this); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + eventBus.unsubscribe(this); + + initialized = false; + } + + @Override + public void setLayerChangeListener(LayerChangeListener listener) { + layerChangeListener = listener; + } + + @Override + public void setLayerVisible(int layerId, boolean visible) { + checkArgument( + getLayerWrapper(layerId) != null, + "A layer with layer ID '%d' doesn't exist.", + layerId + ); + + LayerWrapper wrapper = getLayerWrapper(layerId); + // We want to manipulate the drawing (by adding or removing figures) only if the visible state + // of the layer has actually changed. This prevents figures from being added multiple times and + // figures from being removed although they are no longer present in the drawing. + if (wrapper.getLayer().isVisible() == visible) { + return; + } + + boolean visibleBefore = wrapper.getLayer().isVisible() && wrapper.getLayerGroup().isVisible(); + Layer oldLayer = wrapper.getLayer(); + Layer newLayer = oldLayer.withVisible(visible); + wrapper.setLayer(newLayer); + boolean visibleAfter = wrapper.getLayer().isVisible() && wrapper.getLayerGroup().isVisible(); + + if (visibleBefore != visibleAfter) { + layerVisibilityChanged(newLayer, visibleAfter); + } + + layerChangeListener.layersChanged(); + } + + @Override + public void setLayerName(int layerId, String name) { + checkArgument( + getLayerWrapper(layerId) != null, + "A layer with layer ID '%d' doesn't exist.", + layerId + ); + + LayerWrapper wrapper = getLayerWrapper(layerId); + Layer oldLayer = wrapper.getLayer(); + Layer newLayer = oldLayer.withName(name); + wrapper.setLayer(newLayer); + + layerChangeListener.layersChanged(); + } + + @Override + public void onEvent(Object event) { + if (!(event instanceof SystemModelTransitionEvent)) { + return; + } + + SystemModelTransitionEvent evt = (SystemModelTransitionEvent) event; + switch (evt.getStage()) { + case UNLOADED: + reset(); + break; + case LOADED: + systemModel = evt.getModel(); + restoreLayers(); + break; + default: // Do nothing. + } + } + + @Override + public void addLayerGroupChangeListener(LayerGroupChangeListener listener) { + layerGroupChangeListeners.add(listener); + } + + @Override + public boolean containsComponents(int layerId) { + return !components.getOrDefault(layerId, new HashSet<>()).isEmpty(); + } + + @Override + public void setGroupVisible(int groupId, boolean visible) { + checkArgument( + getLayerGroup(groupId) != null, + "A layer group with layer group ID '%d' doesn't exist.", + groupId + ); + + LayerGroup oldGroup = getLayerGroup(groupId); + if (oldGroup.isVisible() == visible) { + return; + } + + Map> wrappersByLayerVisibility + = getLayerWrappers().values().stream() + .filter(wrapper -> wrapper.getLayer().getGroupId() == groupId) + .collect(Collectors.partitioningBy(wrapper -> wrapper.getLayer().isVisible())); + + // We only need to take care of "visible" layers. Non-visible layers are not affected by + // any changes to a group's visibility. + for (LayerWrapper wrapper : wrappersByLayerVisibility.get(Boolean.TRUE)) { + if (wrapper.getLayerGroup().isVisible() != visible) { + layerVisibilityChanged(wrapper.getLayer(), visible); + } + } + + // Update the group for all layer wrappers that are assigned to it. + LayerGroup newGroup = oldGroup.withVisible(visible); + getLayerWrappers().values().stream() + .filter(wrapper -> wrapper.getLayer().getGroupId() == groupId) + .forEach(wrapper -> wrapper.setLayerGroup(newGroup)); + + // Update the group in the system model's layout. + getLayerGroups().put(groupId, newGroup); + + notifyGroupsChanged(); + } + + @Override + public void setGroupName(int groupId, String name) { + checkArgument( + getLayerGroup(groupId) != null, + "A layer group with layer group ID '%d' doesn't exist.", + groupId + ); + + LayerGroup oldGroup = getLayerGroup(groupId); + LayerGroup newGroup = oldGroup.withName(name); + + // Update the group for all layer wrappers that are assigned to it. + getLayerWrappers().values().stream() + .filter(wrapper -> wrapper.getLayer().getGroupId() == groupId) + .forEach(wrapper -> wrapper.setLayerGroup(newGroup)); + + // Update the group in the system model's layout. + getLayerGroups().put(groupId, newGroup); + + notifyGroupsChanged(); + layerChangeListener.layersChanged(); + } + + protected void reset() { + components.clear(); + } + + protected void restoreLayers() { + restoreComponentsMap(); + + notifyGroupsInitialized(); + layerChangeListener.layersInitialized(); + } + + protected LayerChangeListener getLayerChangeListener() { + return layerChangeListener; + } + + /** + * Returns the system model the layer manager is working with. + * + * @return The system model. + */ + protected SystemModel getSystemModel() { + return systemModel; + } + + /** + * Returns the model components mapped to the IDs of the layers they are drawn on. + * + * @return The model components. + */ + protected Map> getComponents() { + return components; + } + + /** + * Returns the view manager the layer manager is working with. + * + * @return The view manager. + */ + protected ViewManager getViewManager() { + return viewManager; + } + + /** + * Returns the set of drawings the layer manager is working with. + * + * @return The set of drawings the layer manager is working with. + */ + protected Set getDrawings() { + return viewManager.getDrawingViewMap().values().stream() + .map(scrollPane -> scrollPane.getDrawingView().getDrawing()) + .collect(Collectors.toSet()); + } + + /** + * Maps the given model component to the given layer ID. + * + * @param modelComponent The model component to add. + * @param layerId The ID of the layer to map the model component to. + * @see #getComponents() + */ + protected void addComponent(DrawnModelComponent modelComponent, int layerId) { + components.get(layerId).add(modelComponent); + } + + /** + * Removes the mapping for the given model component. + * + * @param modelComponent The model component to remove. + * @see #getComponents() + */ + protected void removeComponent(DrawnModelComponent modelComponent) { + LayerWrapper layerWrapper = modelComponent.getPropertyLayerWrapper().getValue(); + components.get(layerWrapper.getLayer().getId()).remove(modelComponent); + } + + /** + * Returns the layer wrappers in the system model's layout. + * + * @return The layer wrappers in the system model's layout. + */ + protected Map getLayerWrappers() { + return systemModel.getLayoutModel().getPropertyLayerWrappers().getValue(); + } + + /** + * Returns the layer wrapper for the given layer ID. + * + * @param layerId The layer ID. + * @return The layer wrapper. + */ + protected LayerWrapper getLayerWrapper(int layerId) { + return getLayerWrappers().get(layerId); + } + + /** + * Returns the layer groups in the system model's layout. + * + * @return The layer groups in the system model's layout. + */ + protected Map getLayerGroups() { + return systemModel.getLayoutModel().getPropertyLayerGroups().getValue(); + } + + /** + * Returns the layer group for the given layer ID. + * + * @param groupId The layer group ID. + * @return The layer group. + */ + protected LayerGroup getLayerGroup(int groupId) { + return getLayerGroups().get(groupId); + } + + protected void notifyGroupsInitialized() { + for (LayerGroupChangeListener listener : layerGroupChangeListeners) { + listener.groupsInitialized(); + } + } + + protected void notifyGroupsChanged() { + for (LayerGroupChangeListener listener : layerGroupChangeListeners) { + listener.groupsChanged(); + } + } + + protected void notifyGroupAdded() { + for (LayerGroupChangeListener listener : layerGroupChangeListeners) { + listener.groupAdded(); + } + } + + protected void notifyGroupRemoved() { + for (LayerGroupChangeListener listener : layerGroupChangeListeners) { + listener.groupRemoved(); + } + } + + private void restoreComponentsMap() { + // Prepare an entry in the components map for every registered layer. + for (LayerWrapper wrapper : getLayerWrappers().values()) { + components.put(wrapper.getLayer().getId(), new HashSet<>()); + } + + List drawnModelComponents = new ArrayList<>(); + drawnModelComponents.addAll(systemModel.getPointModels()); + drawnModelComponents.addAll(systemModel.getPathModels()); + drawnModelComponents.addAll(systemModel.getLocationModels()); + drawnModelComponents.addAll(systemModel.getLinkModels()); + + // Add all model components to their respective layer. + for (DrawnModelComponent modelComponent : drawnModelComponents) { + addComponent( + modelComponent, + modelComponent.getPropertyLayerWrapper().getValue().getLayer().getId() + ); + } + + } + + protected void layerVisibilityChanged(Layer layer, boolean visible) { + Set drawings = getDrawings(); + SwingUtilities.invokeLater(() -> drawings.forEach(drawing -> drawing.willChange())); + SwingUtilities.invokeLater(() -> drawings.forEach(drawing -> drawing.changed())); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/DisabledCheckBoxCellRenderer.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/DisabledCheckBoxCellRenderer.java new file mode 100644 index 0000000..74913a3 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/DisabledCheckBoxCellRenderer.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.layer; + +import java.awt.Color; +import java.awt.Component; +import javax.swing.JCheckBox; +import javax.swing.JTable; +import javax.swing.SwingConstants; +import javax.swing.table.TableCellRenderer; + +/** + * A table cell renderer for a disabled check box. + */ +public class DisabledCheckBoxCellRenderer + implements + TableCellRenderer { + + private final JCheckBox checkBox; + + public DisabledCheckBoxCellRenderer() { + checkBox = new JCheckBox(); + checkBox.setHorizontalAlignment(SwingConstants.CENTER); + } + + @Override + public Component getTableCellRendererComponent( + JTable table, Object value, boolean isSelected, + boolean hasFocus, int row, int column + ) { + Color bg = isSelected ? table.getSelectionBackground() : table.getBackground(); + checkBox.setBackground(bg); + checkBox.setEnabled(false); + checkBox.setSelected(value == Boolean.TRUE); + return checkBox; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerChangeListener.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerChangeListener.java new file mode 100644 index 0000000..4ccb4ae --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerChangeListener.java @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.layer; + +/** + * Listens for changes to/updates on layer data. + */ +public interface LayerChangeListener { + + /** + * Notifies the listener that the layer data has been initialized. + */ + void layersInitialized(); + + /** + * Notifies the listener that some layer data has changed. + */ + void layersChanged(); + + /** + * Notifies the listener that a layer has been added. + */ + void layerAdded(); + + /** + * Notifies the listener that a layer has been removed. + */ + void layerRemoved(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerEditor.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerEditor.java new file mode 100644 index 0000000..0559f60 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerEditor.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.layer; + +/** + * Provides methods to edit layers. + */ +public interface LayerEditor { + + /** + * Sets a layer's visible state. + * + * @param layerId The ID of the layer. + * @param visible The layer's new visible state. + */ + void setLayerVisible(int layerId, boolean visible); + + /** + * Sets a layer's name. + * + * @param layerId The ID of the layer. + * @param name The layer's new name. + */ + void setLayerName(int layerId, String name); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerGroupCellRenderer.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerGroupCellRenderer.java new file mode 100644 index 0000000..674e526 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerGroupCellRenderer.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.layer; + +import javax.swing.table.DefaultTableCellRenderer; +import org.opentcs.data.model.visualization.LayerGroup; + +/** + * A table cell renderer for {@link LayerGroup}s. + */ +public class LayerGroupCellRenderer + extends + DefaultTableCellRenderer { + + /** + * Creates a new instance. + */ + public LayerGroupCellRenderer() { + } + + @Override + protected void setValue(Object value) { + setText(((LayerGroup) value).getName()); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerGroupChangeListener.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerGroupChangeListener.java new file mode 100644 index 0000000..f7dbd75 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerGroupChangeListener.java @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.layer; + +/** + * Listens for changes to/updates on layer group data. + */ +public interface LayerGroupChangeListener { + + /** + * Notifies the listener that the layer group data has been initialized. + */ + void groupsInitialized(); + + /** + * Notifies the listener that some layer group data has changed. + */ + void groupsChanged(); + + /** + * Notifies the listener that a layer group has been added. + */ + void groupAdded(); + + /** + * Notifies the listener that a layer group has been removed. + */ + void groupRemoved(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerGroupEditor.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerGroupEditor.java new file mode 100644 index 0000000..66e73ea --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerGroupEditor.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.layer; + +/** + * Provides methods to edit layer groups. + */ +public interface LayerGroupEditor { + + /** + * Sets a layer group's visible state. + * + * @param groupId The ID of the layer group. + * @param visible The layer group's new visible state. + */ + void setGroupVisible(int groupId, boolean visible); + + /** + * Sets a layer group's name. + * + * @param groupId The ID of the layer group. + * @param name The layer group's new name. + */ + void setGroupName(int groupId, String name); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerGroupManager.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerGroupManager.java new file mode 100644 index 0000000..296f5fe --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerGroupManager.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.layer; + +/** + * Organizes the layer groups in a plant model. + */ +public interface LayerGroupManager + extends + LayerGroupEditor { + + /** + * Add a listener to the set that's notified each time a change to group data occurs. + * + * @param listener The listener to add. + */ + void addLayerGroupChangeListener(LayerGroupChangeListener listener); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerManager.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerManager.java new file mode 100644 index 0000000..34e1a7c --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/layer/LayerManager.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.layer; + +import org.opentcs.components.Lifecycle; +import org.opentcs.util.event.EventHandler; + +/** + * Organizes the layers in a plant model. + */ +public interface LayerManager + extends + Lifecycle, + LayerEditor, + EventHandler { + + /** + * Sets the listener for layer data changes. + * + * @param listener The listener for layer data changes. + */ + void setLayerChangeListener(LayerChangeListener listener); + + /** + * Returns whether the layer with the given layer ID contains any components. + * + * @param layerId The ID of the layer. + * @return Whether the layer contains any components. + */ + boolean containsComponents(int layerId); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/AbstractAttributesContent.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/AbstractAttributesContent.java new file mode 100644 index 0000000..70532b1 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/AbstractAttributesContent.java @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties; + +import javax.swing.JComponent; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.UndoRedoManager; + +/** + * Base implementation for visualisation of model component properties. + */ +public abstract class AbstractAttributesContent + implements + AttributesContent { + + /** + * The model component to show the properties of. + */ + protected ModelComponent fModel; + /** + * The undo manager. + */ + protected UndoRedoManager fUndoRedoManager; + /** + * The swing component. + */ + protected JComponent fComponent; + + /** + * Creates a new instance of AbstractAttributesContent + */ + public AbstractAttributesContent() { + } + + @Override // AttributesContent + public void setModel(ModelComponent model) { + fModel = model; + } + + @Override // AttributesContent + public abstract void reset(); + + @Override // AttributesContent + public JComponent getComponent() { + return fComponent; + } + + @Override // AttributesContent + public String getDescription() { + return fModel.getDescription(); + } + + @Override // AttributesContent + public void setup(UndoRedoManager undoManager) { + fUndoRedoManager = undoManager; + fComponent = createComponent(); + } + + /** + * Creates the component. + * + * @return The created component. + */ + protected abstract JComponent createComponent(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/AbstractTableContent.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/AbstractTableContent.java new file mode 100644 index 0000000..99271dd --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/AbstractTableContent.java @@ -0,0 +1,133 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Provider; +import java.awt.BorderLayout; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.table.TableCellEditor; +import javax.swing.table.TableColumnModel; +import javax.swing.table.TableModel; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.common.components.properties.event.TableChangeListener; +import org.opentcs.guing.common.components.properties.event.TableSelectionChangeEvent; +import org.opentcs.guing.common.components.properties.table.AttributesTable; + +/** + * Base implementation for content that displays model properties in a table. + */ +public abstract class AbstractTableContent + extends + AbstractAttributesContent + implements + TableChangeListener { + + /** + * The table that shows the properties. + */ + protected AttributesTable fTable; + /** + * The cell editors. + */ + protected List fCellEditors = new ArrayList<>(); + /** + * Indicates that changes to the tabel model should also update the model component. + */ + protected boolean fEvaluateTableChanges = true; + /** + * Provides attribute tables. + */ + private final Provider tableProvider; + + /** + * Creates a new instance. + * + * @param tableProvider Provides attribute tables. + */ + public AbstractTableContent(Provider tableProvider) { + this.tableProvider = requireNonNull(tableProvider, "tableProvider"); + } + + @Override // AbstractAttributesContent + protected JComponent createComponent() { + JPanel component = new JPanel(); + + initTable(); + JScrollPane scrollPane = new JScrollPane(fTable); + + component.setLayout(new BorderLayout()); + component.add(scrollPane, BorderLayout.CENTER); + + return component; + } + + @Override // TableChangeListener + public void tableSelectionChanged(TableSelectionChangeEvent e) { + } + + @Override // TableChangeListener + public void tableModelChanged() { + } + + /** + * Initialises the table. + */ + protected void initTable() { + fTable = tableProvider.get(); + setTableCellRenderers(); + setTableCellEditors(); + fTable.addTableChangeListener(this); + } + + /** + * Set the table cell renderers. + */ + protected void setTableCellRenderers() { + } + + /** + * Set the table cell editors. + */ + protected void setTableCellEditors() { + } + + /** + * Set new table content. + * + * @param content A map from property name to property. + */ + protected void setTableContent(Map content) { + fEvaluateTableChanges = false; + + TableColumnModel columnModel = fTable.getColumnModel(); + int[] widths = new int[columnModel.getColumnCount()]; + + for (int i = 0; i < widths.length; i++) { + widths[i] = columnModel.getColumn(i).getWidth(); + } + + fTable.setModel(createTableModel(content)); + + for (int i = 0; i < widths.length; i++) { + columnModel.getColumn(i).setPreferredWidth(widths[i]); + } + + fEvaluateTableChanges = true; + } + + /** + * Creates a new table model from the content. + * + * @param content Map from property name to property. + * @return A table model that represents the content. + */ + protected abstract TableModel createTableModel(Map content); + +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/AttributesComponent.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/AttributesComponent.form new file mode 100644 index 0000000..efcf3c4 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/AttributesComponent.form @@ -0,0 +1,39 @@ + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/AttributesComponent.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/AttributesComponent.java new file mode 100644 index 0000000..5b5d56e --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/AttributesComponent.java @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.awt.Component; +import javax.swing.JPanel; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.common.model.ComponentSelectionListener; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.UndoRedoManager; + +/** + * A component for viewing and editing of properties. + * Shows a table with two columns. The left column contains the name of the property. + * The right column contains the value of the property and can be clicked to edit the value. + */ +public class AttributesComponent + extends + JPanel + implements + ComponentSelectionListener { + + /** + * The table with the properties. + */ + private AttributesContent fPropertiesContent; + private Component fPropertiesComponent; + /** + * The undo manager. + */ + private final UndoRedoManager fUndoRedoManager; + + /** + * Creates a new instance. + * + * @param undoManager The undo manager to use. + */ + @Inject + @SuppressWarnings("this-escape") + public AttributesComponent(UndoRedoManager undoManager) { + fUndoRedoManager = requireNonNull(undoManager, "undoManager"); + initComponents(); + } + + @Override + public void componentSelected(ModelComponent model) { + setModel(model); + } + + /** + * Set the model component. + * + * @param model the model component to show properties for. + */ + public void setModel(ModelComponent model) { + fPropertiesContent.setModel(model); + fPropertiesComponent.setVisible(!model.getProperties().isEmpty()); + + setDescription(); + } + + /** + * Resets the display when no model component should be shown. + */ + public void reset() { + if (fPropertiesContent != null) { + fPropertiesContent.reset(); + } + + descriptionLabel.setText(""); + } + + /** + * Set the text that describes the current model component. + */ + protected void setDescription() { + descriptionLabel.setText(fPropertiesContent.getDescription()); + } + + /** + * Set the properties content. + * + * @param content the properties content. + */ + public void setPropertiesContent(AttributesContent content) { + fPropertiesContent = content; + fPropertiesContent.setup(fUndoRedoManager); + fPropertiesComponent = add(content.getComponent()); + fPropertiesComponent.setVisible(false); + } + + public AttributesContent getPropertiesContent() { + return fPropertiesContent; + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + + descriptionLabel = new javax.swing.JLabel(); + + setLayout(new javax.swing.BoxLayout(this, javax.swing.BoxLayout.Y_AXIS)); + + descriptionLabel.setFont(descriptionLabel.getFont().deriveFont(descriptionLabel.getFont().getSize()+1f)); + descriptionLabel.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + descriptionLabel.setBorder(javax.swing.BorderFactory.createEmptyBorder(5, 5, 5, 5)); + add(descriptionLabel); + }// //GEN-END:initComponents + // Variables declaration - do not modify//GEN-BEGIN:variables + protected javax.swing.JLabel descriptionLabel; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/AttributesContent.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/AttributesContent.java new file mode 100644 index 0000000..15e727d --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/AttributesContent.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties; + +import javax.swing.JComponent; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.UndoRedoManager; + +/** + * Interface for Swing components that allow editing and viewing a model component attribute. + */ +public interface AttributesContent { + + /** + * Sets the model component whose properties are to be displayed. + * + * @param model The model component whose properties are to be displayed. + */ + void setModel(ModelComponent model); + + /** + * Resets this content to no longer display the model component properties. + */ + void reset(); + + /** + * Returns the content as a Swing component. + * + * @return The content as a Swing component. + */ + JComponent getComponent(); + + /** + * Return a description of the content. + * + * @return A description of the content. + */ + String getDescription(); + + /** + * Initialises the content with the undo manager. + * + * @param undoRedoManager The content with the undo manager. + */ + void setup(UndoRedoManager undoRedoManager); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/CoordinateUndoActivity.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/CoordinateUndoActivity.java new file mode 100644 index 0000000..5c4e700 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/CoordinateUndoActivity.java @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import java.awt.geom.AffineTransform; +import javax.swing.undo.AbstractUndoableEdit; +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; +import org.opentcs.guing.base.components.properties.event.NullAttributesChangeListener; +import org.opentcs.guing.base.components.properties.type.CoordinateProperty; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.PositionableModelComponent; +import org.opentcs.guing.common.components.drawing.figures.LabeledFigure; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * An undo for the modification of a coordinate property. + */ +public abstract class CoordinateUndoActivity + extends + AbstractUndoableEdit { + + protected final CoordinateProperty property; + protected final CoordinateProperty pxModel; + protected final CoordinateProperty pyModel; + protected final LabeledFigure bufferedFigure; + protected final AffineTransform bufferedTransform = new AffineTransform(); + private CoordinateProperty pxBeforeModification; + private CoordinateProperty pyBeforeModification; + private CoordinateProperty pxAfterModification; + private CoordinateProperty pyAfterModification; + + /** + * Creates a new instance. + * + * @param property The affected property. + * @param modelManager The model manager to be used. + */ + public CoordinateUndoActivity( + @Assisted + CoordinateProperty property, + ModelManager modelManager + ) { + this.property = requireNonNull(property, "property"); + + ModelComponent model = property.getModel(); + pxModel = (CoordinateProperty) model.getProperty(PositionableModelComponent.MODEL_X_POSITION); + pyModel = (CoordinateProperty) model.getProperty(PositionableModelComponent.MODEL_Y_POSITION); + bufferedFigure = (LabeledFigure) modelManager.getModel().getFigure(model); + } + + /** + * Creates a snapshot before the modification of the properties. + */ + public void snapShotBeforeModification() { + pxBeforeModification = (CoordinateProperty) pxModel.clone(); + pyBeforeModification = (CoordinateProperty) pyModel.clone(); + + saveTransformBeforeModification(); + } + + /** + * Creates a snapshot after the modification of the properties. + */ + public void snapShotAfterModification() { + pxAfterModification = (CoordinateProperty) pxModel.clone(); + pyAfterModification = (CoordinateProperty) pyModel.clone(); + } + + @Override + public void undo() + throws CannotUndoException { + super.undo(); + + pxModel.copyFrom(pxBeforeModification); + pyModel.copyFrom(pyBeforeModification); + pxModel.markChanged(); + pyModel.markChanged(); + + saveTransformForUndo(); + + pxModel.getModel().propertiesChanged(new NullAttributesChangeListener()); + } + + @Override + public void redo() + throws CannotRedoException { + super.redo(); + + pxModel.copyFrom(pxAfterModification); + pyModel.copyFrom(pyAfterModification); + pxModel.markChanged(); + pyModel.markChanged(); + + saveTransformForRedo(); + + pxModel.getModel().propertiesChanged(new NullAttributesChangeListener()); + } + + protected abstract void saveTransformBeforeModification(); + + protected abstract void saveTransformForUndo(); + + protected abstract void saveTransformForRedo(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/PropertiesComponentsFactory.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/PropertiesComponentsFactory.java new file mode 100644 index 0000000..ce6eac1 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/PropertiesComponentsFactory.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties; + +import javax.swing.JTextField; +import org.opentcs.guing.common.components.properties.table.CoordinateCellEditor; +import org.opentcs.guing.common.util.UserMessageHelper; + +/** + * A factory for creating instances in relation to properties. + */ +public interface PropertiesComponentsFactory { + + /** + * Creates a {@link CoordinateCellEditor}. + * + * @param textField The text field for the cell editor. + * @param userMessageHelper The user message helper. + * @return The {@link CoordinateCellEditor}. + */ + CoordinateCellEditor createCoordinateCellEditor( + JTextField textField, + UserMessageHelper userMessageHelper + ); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/PropertyUndoActivity.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/PropertyUndoActivity.java new file mode 100644 index 0000000..34f4a47 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/PropertyUndoActivity.java @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties; + +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; +import org.opentcs.guing.base.components.properties.event.NullAttributesChangeListener; +import org.opentcs.guing.base.components.properties.type.Property; + +/** + * An undo action for a property change. + */ +public class PropertyUndoActivity + extends + javax.swing.undo.AbstractUndoableEdit { + + /** + * The property that changes. + */ + protected Property fProperty; + /** + * The property before the change. + */ + protected Property fBeforeModification; + /** + * The property after the change. + */ + protected Property fAfterModification; + /** + * Indicates whether this change has been performed. + * Defaults to true; becomes false if this edit is undone, true + * again if it is redone. + */ + private boolean hasBeenDone = true; + /** + * True if this edit has not received die; defaults + * to true. + */ + private boolean alive = true; + + /** + * Creates a new instance of PropertiesUndoActivity + * + * @param property + */ + public PropertyUndoActivity(Property property) { + fProperty = property; + } + + /** + * Creates a snapshot of the property before the change. + */ + public void snapShotBeforeModification() { + fBeforeModification = createMemento(); + } + + /** + * Creates a snapshot of the property after the change. + */ + public void snapShotAfterModification() { + fAfterModification = createMemento(); + } + + /** + * Creates a copy of the current state of the property. + * + * @return A copy of the current state of the property. + */ + protected Property createMemento() { + return (Property) fProperty.clone(); + } + + @Override + public String getPresentationName() { + return fProperty.getDescription(); + } + + @Override + public void die() { + alive = false; + } + + @Override + public void undo() + throws CannotUndoException { + fProperty.copyFrom(fBeforeModification); + fProperty.markChanged(); + fProperty.getModel().propertiesChanged(new NullAttributesChangeListener()); + hasBeenDone = false; + } + + @Override + public void redo() + throws CannotRedoException { + fProperty.copyFrom(fAfterModification); + fProperty.markChanged(); + fProperty.getModel().propertiesChanged(new NullAttributesChangeListener()); + hasBeenDone = true; + } + + @Override + public boolean canUndo() { + return alive && hasBeenDone; + } + + @Override + public boolean canRedo() { + return alive && !hasBeenDone; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/SelectionPropertiesComponent.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/SelectionPropertiesComponent.java new file mode 100644 index 0000000..d579245 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/SelectionPropertiesComponent.java @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties; + +import jakarta.inject.Inject; +import org.opentcs.guing.common.event.OperationModeChangeEvent; +import org.opentcs.guing.common.event.SystemModelTransitionEvent; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.UndoRedoManager; +import org.opentcs.util.event.EventHandler; + +/** + * An AttributesComponent intended to be shown permanently to display the + * properties of the currently selected driving course components. + */ +public class SelectionPropertiesComponent + extends + AttributesComponent + implements + EventHandler { + + /** + * Creates a new instance. + * + * @param undoManager Manages undo/redo actions. + */ + @Inject + public SelectionPropertiesComponent(UndoRedoManager undoManager) { + super(undoManager); + } + + @Override + public void onEvent(Object event) { + if (event instanceof SystemModelTransitionEvent) { + handleSystemModelTransition((SystemModelTransitionEvent) event); + } + if (event instanceof OperationModeChangeEvent) { + handleOperationModeChange((OperationModeChangeEvent) event); + } + } + + private void handleSystemModelTransition(SystemModelTransitionEvent evt) { + switch (evt.getStage()) { + case UNLOADING: + reset(); + break; + default: + // Do nada. + } + } + + private void handleOperationModeChange(OperationModeChangeEvent evt) { + reset(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/event/TableChangeListener.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/event/TableChangeListener.java new file mode 100644 index 0000000..06fb7b4 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/event/TableChangeListener.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.event; + +/** + * A listener that listens for changes on a table. + */ +public interface TableChangeListener + extends + java.util.EventListener { + + /** + * Indicates that a line in the table has been selected. + * + * @param event The event. + */ + void tableSelectionChanged(TableSelectionChangeEvent event); + + /** + * Indicates that changes in the table have occured. + */ + void tableModelChanged(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/event/TableSelectionChangeEvent.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/event/TableSelectionChangeEvent.java new file mode 100644 index 0000000..61f17fd --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/event/TableSelectionChangeEvent.java @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.event; + +import java.util.EventObject; +import org.opentcs.guing.common.components.properties.table.AttributesTable; + +/** + * An Event emitted when a line in a table is selected. + */ +public class TableSelectionChangeEvent + extends + EventObject { + + /** + * The attribute. + */ + protected Object fSelectedValue; + + /** + * Creates a new instance of TableSelectionChangeEvent + */ + public TableSelectionChangeEvent(AttributesTable table, Object selectedValue) { + super(table); + fSelectedValue = selectedValue; + } + + /** + * Returns the attribute contained in the selected line. + */ + public Object getSelectedValue() { + return fSelectedValue; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/BoundingBoxPropertyEditorPanel.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/BoundingBoxPropertyEditorPanel.form new file mode 100644 index 0000000..2b3461d --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/BoundingBoxPropertyEditorPanel.form @@ -0,0 +1,170 @@ + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/BoundingBoxPropertyEditorPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/BoundingBoxPropertyEditorPanel.java new file mode 100644 index 0000000..6dbf426 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/BoundingBoxPropertyEditorPanel.java @@ -0,0 +1,255 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.Inject; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import org.opentcs.data.model.Couple; +import org.opentcs.guing.base.components.properties.type.BoundingBoxProperty; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.base.model.BoundingBoxModel; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * A panel for editing a {@link BoundingBoxProperty}. + */ +public class BoundingBoxPropertyEditorPanel + extends + JPanel + implements + DetailsDialogContent { + + /** + * The bundle to be used. + */ + private static final ResourceBundleUtil BUNDLE + = ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH); + /** + * The property to be edited. + */ + private BoundingBoxProperty boundingBoxProperty; + + /** + * Creates a new instance. + */ + @Inject + @SuppressWarnings("this-escape") + public BoundingBoxPropertyEditorPanel() { + initComponents(); + } + + @Override + public void updateValues() { + boundingBoxProperty.setValue(createBoundingBoxFromInput()); + } + + @Override + public String getTitle() { + return BUNDLE.getString("boundingBoxPropertyEditorPanel.title"); + } + + @Override + public void setProperty(Property property) { + requireNonNull(property, "property"); + this.boundingBoxProperty = (BoundingBoxProperty) property; + + lengthTextField.setText(String.valueOf(boundingBoxProperty.getValue().getLength())); + widthTextField.setText(String.valueOf(boundingBoxProperty.getValue().getWidth())); + heightTextField.setText(String.valueOf(boundingBoxProperty.getValue().getHeight())); + referenceOffsetXTextField.setText( + String.valueOf(boundingBoxProperty.getValue().getReferenceOffset().getX()) + ); + referenceOffsetYTextField.setText( + String.valueOf(boundingBoxProperty.getValue().getReferenceOffset().getY()) + ); + } + + @Override + public Property getProperty() { + return boundingBoxProperty; + } + + private BoundingBoxModel createBoundingBoxFromInput() { + try { + return new BoundingBoxModel( + Long.parseLong(lengthTextField.getText()), + Long.parseLong(widthTextField.getText()), + Long.parseLong(heightTextField.getText()), + new Couple( + Long.parseLong(referenceOffsetXTextField.getText()), + Long.parseLong(referenceOffsetYTextField.getText()) + ) + ); + } + catch (NumberFormatException e) { + JOptionPane.showMessageDialog( + this, + BUNDLE.getString("boundingBoxPropertyEditorPanel.optionPane_numberFormatError.title"), + BUNDLE.getString("boundingBoxPropertyEditorPanel.optionPane_numberFormatError.message"), + JOptionPane.ERROR_MESSAGE + ); + // Re-throw the exception to prevent the editor panel from closing. + throw e; + } + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + dimensionsPanel = new javax.swing.JPanel(); + lengthLabel = new javax.swing.JLabel(); + widthLabel = new javax.swing.JLabel(); + heightLabel = new javax.swing.JLabel(); + lengthTextField = new javax.swing.JTextField(); + widthTextField = new javax.swing.JTextField(); + heightTextField = new javax.swing.JTextField(); + referenceOffsetPanel = new javax.swing.JPanel(); + referenceOffsetXLabel = new javax.swing.JLabel(); + referenceOffsetYLabel = new javax.swing.JLabel(); + referenceOffsetXTextField = new javax.swing.JTextField(); + referenceOffsetYTextField = new javax.swing.JTextField(); + + setLayout(new java.awt.GridBagLayout()); + + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/panels/propertyEditing"); // NOI18N + dimensionsPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("boundingBoxPropertyEditorPanel.panel_dimensions.border.title"))); // NOI18N + dimensionsPanel.setLayout(new java.awt.GridBagLayout()); + + lengthLabel.setText(bundle.getString("boundingBoxPropertyEditorPanel.label_length")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + dimensionsPanel.add(lengthLabel, gridBagConstraints); + + widthLabel.setText(bundle.getString("boundingBoxPropertyEditorPanel.label_width")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + dimensionsPanel.add(widthLabel, gridBagConstraints); + + heightLabel.setText(bundle.getString("boundingBoxPropertyEditorPanel.label_height")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + dimensionsPanel.add(heightLabel, gridBagConstraints); + + lengthTextField.setColumns(12); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 3); + dimensionsPanel.add(lengthTextField, gridBagConstraints); + + widthTextField.setColumns(12); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 3); + dimensionsPanel.add(widthTextField, gridBagConstraints); + + heightTextField.setColumns(12); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + dimensionsPanel.add(heightTextField, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + add(dimensionsPanel, gridBagConstraints); + + referenceOffsetPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("boundingBoxPropertyEditorPanel.panel_referenceOffset.border.title"))); // NOI18N + referenceOffsetPanel.setLayout(new java.awt.GridBagLayout()); + + referenceOffsetXLabel.setText(bundle.getString("boundingBoxPropertyEditorPanel.label_referenceOffsetX")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + referenceOffsetPanel.add(referenceOffsetXLabel, gridBagConstraints); + + referenceOffsetYLabel.setText(bundle.getString("boundingBoxPropertyEditorPanel.label_referenceOffsetY")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + referenceOffsetPanel.add(referenceOffsetYLabel, gridBagConstraints); + + referenceOffsetXTextField.setColumns(12); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 3); + referenceOffsetPanel.add(referenceOffsetXTextField, gridBagConstraints); + + referenceOffsetYTextField.setColumns(12); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + referenceOffsetPanel.add(referenceOffsetYTextField, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(3, 0, 0, 0); + add(referenceOffsetPanel, gridBagConstraints); + }// //GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel dimensionsPanel; + private javax.swing.JLabel heightLabel; + private javax.swing.JTextField heightTextField; + private javax.swing.JLabel lengthLabel; + private javax.swing.JTextField lengthTextField; + private javax.swing.JPanel referenceOffsetPanel; + private javax.swing.JLabel referenceOffsetXLabel; + private javax.swing.JTextField referenceOffsetXTextField; + private javax.swing.JLabel referenceOffsetYLabel; + private javax.swing.JTextField referenceOffsetYTextField; + private javax.swing.JLabel widthLabel; + private javax.swing.JTextField widthTextField; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/EnergyLevelThresholdSetPropertyEditorPanel.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/EnergyLevelThresholdSetPropertyEditorPanel.form new file mode 100644 index 0000000..ac13c85 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/EnergyLevelThresholdSetPropertyEditorPanel.form @@ -0,0 +1,128 @@ + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/EnergyLevelThresholdSetPropertyEditorPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/EnergyLevelThresholdSetPropertyEditorPanel.java new file mode 100644 index 0000000..d259536 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/EnergyLevelThresholdSetPropertyEditorPanel.java @@ -0,0 +1,285 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkInRange; + +import javax.swing.JOptionPane; +import org.opentcs.guing.base.components.properties.type.EnergyLevelThresholdSetModel; +import org.opentcs.guing.base.components.properties.type.EnergyLevelThresholdSetProperty; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * An editor panel for the energy level threshold set of a vehicle. + */ +public class EnergyLevelThresholdSetPropertyEditorPanel + extends + javax.swing.JPanel + implements + DetailsDialogContent { + + /** + * The bundle to be used. + */ + private static final ResourceBundleUtil BUNDLE + = ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH); + /** + * The property to edit. + */ + private EnergyLevelThresholdSetProperty fProperty; + + /** + * Creates new instance. + */ + @SuppressWarnings("this-escape") + public EnergyLevelThresholdSetPropertyEditorPanel() { + initComponents(); + } + + @Override + public void updateValues() { + fProperty.setValue(createEnergyLevelThresholdSetFromInput()); + } + + @Override + public String getTitle() { + return BUNDLE.getString("energyLevelThresholdSetPropertyEditorPanel.title"); + } + + @Override + public void setProperty(Property property) { + requireNonNull(property, "property"); + fProperty = (EnergyLevelThresholdSetProperty) property; + + energyLevelCriticalTextField.setText( + String.valueOf(fProperty.getValue().getEnergyLevelCritical()) + ); + energyLevelGoodTextField.setText(String.valueOf(fProperty.getValue().getEnergyLevelGood())); + energyLevelSufficientlyRechargedTextField.setText( + String.valueOf(fProperty.getValue().getEnergyLevelSufficientlyRecharged()) + ); + energyLevelFullyRechargedTextField.setText( + String.valueOf(fProperty.getValue().getEnergyLevelFullyRecharged()) + ); + } + + @Override + public Property getProperty() { + return fProperty; + } + + @SuppressWarnings("checkstyle:LineLength") + private EnergyLevelThresholdSetModel createEnergyLevelThresholdSetFromInput() { + int energyLevelCritical; + int energyLevelGood; + int energyLevelSufficientlyRecharged; + int energyLevelFullyRecharged; + + try { + energyLevelCritical = checkInRange( + Integer.parseInt(energyLevelCriticalTextField.getText()), + 0, + 100, + "energyLevelCritical" + ); + energyLevelGood = checkInRange( + Integer.parseInt(energyLevelGoodTextField.getText()), + 0, + 100, + "energyLevelGood" + ); + energyLevelSufficientlyRecharged = checkInRange( + Integer.parseInt(energyLevelSufficientlyRechargedTextField.getText()), + 0, + 100, + "energyLevelSufficientlyRecharged" + ); + energyLevelFullyRecharged = checkInRange( + Integer.parseInt(energyLevelFullyRechargedTextField.getText()), + 0, + 100, + "energyLevelFullyRecharged" + ); + } + catch (NumberFormatException e) { + JOptionPane.showMessageDialog( + this, + BUNDLE.getString( + "energyLevelThresholdSetPropertyEditorPanel.optionPane_numberFormatError.message" + ), + BUNDLE.getString( + "energyLevelThresholdSetPropertyEditorPanel.optionPane_numberFormatError.title" + ), + JOptionPane.ERROR_MESSAGE + ); + // Re-throw the exception to prevent the editor panel from closing. + throw e; + } + catch (IllegalArgumentException e) { + JOptionPane.showMessageDialog( + this, + BUNDLE.getString( + "energyLevelThresholdSetPropertyEditorPanel.optionPane_thresholdsNotInRange.message" + ), + BUNDLE.getString( + "energyLevelThresholdSetPropertyEditorPanel.optionPane_thresholdsNotInRange.title" + ), + JOptionPane.ERROR_MESSAGE + ); + // Re-throw the exception to prevent the editor panel from closing. + throw e; + } + + if (energyLevelGood < energyLevelCritical) { + JOptionPane.showMessageDialog( + this, + BUNDLE.getString( + "energyLevelThresholdSetPropertyEditorPanel.optionPane_goodSmallerCritical.message" + ), + BUNDLE.getString( + "energyLevelThresholdSetPropertyEditorPanel.optionPane_goodSmallerCritical.title" + ), + JOptionPane.ERROR_MESSAGE + ); + // Throw the exception to prevent the editor panel from closing. + throw new IllegalArgumentException("Energy level good has to be >= energy level critical."); + } + + if (energyLevelFullyRecharged < energyLevelSufficientlyRecharged) { + JOptionPane.showMessageDialog( + this, + BUNDLE.getString( + "energyLevelThresholdSetPropertyEditorPanel.optionPane_fullyRechargedSmallerSufficientlyRecharged.message" + ), + BUNDLE.getString( + "energyLevelThresholdSetPropertyEditorPanel.optionPane_fullyRechargedSmallerSufficientlyRecharged.title" + ), + JOptionPane.ERROR_MESSAGE + ); + // Throw the exception to prevent the editor panel from closing. + throw new IllegalArgumentException( + "Energy level fully recharged has to be >= energy level sufficiently recharged." + ); + } + + return new EnergyLevelThresholdSetModel( + energyLevelCritical, + energyLevelGood, + energyLevelSufficientlyRecharged, + energyLevelFullyRecharged + ); + } + + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the FormEditor. + */ + // FORMATTER:OFF + // CHECKSTYLE:OFF + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + thresholdsPanel = new javax.swing.JPanel(); + energyLevelGoodLabel = new javax.swing.JLabel(); + energyLevelCriticalLabel = new javax.swing.JLabel(); + energyLevelSufficientlyRechargedLabel = new javax.swing.JLabel(); + energyLevelFullyRechargedLabel = new javax.swing.JLabel(); + energyLevelGoodTextField = new javax.swing.JTextField(); + energyLevelFullyRechargedTextField = new javax.swing.JTextField(); + energyLevelSufficientlyRechargedTextField = new javax.swing.JTextField(); + energyLevelCriticalTextField = new javax.swing.JTextField(); + + setLayout(new java.awt.GridBagLayout()); + + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/panels/propertyEditing"); // NOI18N + thresholdsPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("energyLevelThresholdSetPropertyEditorPanel.panel_thresholds.border.title"))); // NOI18N + thresholdsPanel.setLayout(new java.awt.GridBagLayout()); + + energyLevelGoodLabel.setText(bundle.getString("energyLevelThresholdSetPropertyEditorPanel.label_energyLevelGood.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + thresholdsPanel.add(energyLevelGoodLabel, gridBagConstraints); + + energyLevelCriticalLabel.setText(bundle.getString("energyLevelThresholdSetPropertyEditorPanel.label_energyLevelCritical.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + thresholdsPanel.add(energyLevelCriticalLabel, gridBagConstraints); + + energyLevelSufficientlyRechargedLabel.setText(bundle.getString("energyLevelThresholdSetPropertyEditorPanel.label_energyLevelSufficientlyRecharged.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + thresholdsPanel.add(energyLevelSufficientlyRechargedLabel, gridBagConstraints); + + energyLevelFullyRechargedLabel.setText(bundle.getString("energyLevelThresholdSetPropertyEditorPanel.label_energyLevelFullyRecharged.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + thresholdsPanel.add(energyLevelFullyRechargedLabel, gridBagConstraints); + + energyLevelGoodTextField.setColumns(3); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 3); + thresholdsPanel.add(energyLevelGoodTextField, gridBagConstraints); + + energyLevelFullyRechargedTextField.setColumns(3); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + thresholdsPanel.add(energyLevelFullyRechargedTextField, gridBagConstraints); + + energyLevelSufficientlyRechargedTextField.setColumns(3); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 3); + thresholdsPanel.add(energyLevelSufficientlyRechargedTextField, gridBagConstraints); + + energyLevelCriticalTextField.setColumns(3); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 0, 3); + thresholdsPanel.add(energyLevelCriticalTextField, gridBagConstraints); + + add(thresholdsPanel, new java.awt.GridBagConstraints()); + }// //GEN-END:initComponents + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel energyLevelCriticalLabel; + private javax.swing.JTextField energyLevelCriticalTextField; + private javax.swing.JLabel energyLevelFullyRechargedLabel; + private javax.swing.JTextField energyLevelFullyRechargedTextField; + private javax.swing.JLabel energyLevelGoodLabel; + private javax.swing.JTextField energyLevelGoodTextField; + private javax.swing.JLabel energyLevelSufficientlyRechargedLabel; + private javax.swing.JTextField energyLevelSufficientlyRechargedTextField; + private javax.swing.JPanel thresholdsPanel; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/EnvelopePanel.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/EnvelopePanel.form new file mode 100644 index 0000000..a89c2d6 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/EnvelopePanel.form @@ -0,0 +1,219 @@ + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <Editor/> + <Renderer/> + </Column> + <Column maxWidth="-1" minWidth="-1" prefWidth="-1" resizable="true"> + <Title/> + <Editor/> + <Renderer/> + </Column> + </TableColumnModel> + </Property> + <Property name="tableHeader" type="javax.swing.table.JTableHeader" editor="org.netbeans.modules.form.editors2.JTableHeaderEditor"> + <TableHeader reorderingAllowed="true" resizingAllowed="true"/> + </Property> + </Properties> + </Component> + </SubComponents> + </Container> + <Component class="javax.swing.JLabel" name="validationLabel"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="envelopePanel.label_envelopeValidation.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="4" gridWidth="2" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="12" insetsLeft="0" insetsBottom="3" insetsRight="0" anchor="17" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Container class="javax.swing.JScrollPane" name="validationScrollPane"> + <AuxValues> + <AuxValue name="autoScrollPane" type="java.lang.Boolean" value="true"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="5" gridWidth="1" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JTextArea" name="validationTextArea"> + <Properties> + <Property name="columns" type="int" value="20"/> + <Property name="lineWrap" type="boolean" value="true"/> + <Property name="rows" type="int" value="2"/> + </Properties> + </Component> + </SubComponents> + </Container> + </SubComponents> +</Form> diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/EnvelopePanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/EnvelopePanel.java new file mode 100644 index 0000000..28b0b9e --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/EnvelopePanel.java @@ -0,0 +1,651 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import static java.util.Objects.requireNonNull; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.swing.JOptionPane; +import javax.swing.JTextField; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.table.AbstractTableModel; +import org.opentcs.data.model.Couple; +import org.opentcs.guing.base.components.properties.type.EnvelopesProperty; +import org.opentcs.guing.base.model.EnvelopeModel; +import org.opentcs.guing.common.components.dialogs.DialogContent; +import org.opentcs.guing.common.components.dialogs.InputValidationListener; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.util.ExplainedBoolean; + +/** + * User interface to edit a single envelope. + */ +public class EnvelopePanel + extends + DialogContent { + + /** + * The default key to use for envelopes. + */ + public static final String DEFAULT_ENVELOPE_KEY = ""; + /** + * The color in which the validation text is displayed when an envelope is valid. + */ + private static final Color ENVELOPE_VALID = Color.decode("#2fb348"); + /** + * The color in which the validation text is displayed when an envelope is invalid. + */ + private static final Color ENVELOPE_INVALID = Color.decode("#d60b28"); + /** + * The bundle to be used. + */ + private final ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH); + /** + * The list of listeners to be notified about the validity of user input. + */ + private final List<InputValidationListener> validationListeners = new ArrayList<>(); + private final EnvelopeModel envelopeTemplate; + private final Mode mode; + private final Set<String> propertyEnvelopeKeys; + private final SystemModel systemModel; + + /** + * Creates a new instance. + * + * @param envelopeTemplate The envelope that should be used as a tempalte (i.e. to fill this + * panel's components). + * @param mode The mode to use this panel in. + * this panel belongs. + * @param propertyEnvelopeKeys The envelope keys that are (already) defined in the + * {@link EnvelopesProperty} to which the envelope being edited in this panel belongs. + * @param systemModel The current system model. + */ + @SuppressWarnings("this-escape") + public EnvelopePanel( + EnvelopeModel envelopeTemplate, + Mode mode, + Set<String> propertyEnvelopeKeys, + SystemModel systemModel + ) { + this.envelopeTemplate = requireNonNull(envelopeTemplate, "envelopeTemplate"); + this.mode = requireNonNull(mode, "mode"); + this.propertyEnvelopeKeys = requireNonNull(propertyEnvelopeKeys, "propertyEnvelopeKeys"); + this.systemModel = requireNonNull(systemModel, "systemModel"); + + initComponents(); + initEnvelopeKeyCombobox(); + initTable(); + + fillComponents(); + } + + @Override + public String getDialogTitle() { + return bundle.getString("envelopePanel.title"); + } + + @Override + public void initFields() { + } + + @Override + public void update() { + } + + /** + * Registers the given {@link InputValidationListener} with this panel. + * + * @param listener The listener to register. + */ + public void addInputValidationListener(InputValidationListener listener) { + requireNonNull(listener, "listener"); + + this.validationListeners.add(listener); + validateEnvelope(); + } + + /** + * Returns the {@link EnvelopeModel} that has been edited in this panel. + * + * @return The {@link EnvelopeModel} that has been edited in this panel or {@link Optional#EMPTY} + * if the envelope model was invalid. + */ + public Optional<EnvelopeModel> getEnvelopeModel() { + if (!isEnvelopeValid().getValue()) { + return Optional.empty(); + } + + return Optional.of( + new EnvelopeModel( + envelopeKeyComboBox.getSelectedItem().toString(), + getTableModel().getValues() + ) + ); + } + + private void initEnvelopeKeyCombobox() { + envelopeKeys().stream() + .sorted() + .forEach(envelopeKeyComboBox::addItem); + + JTextField textField = (JTextField) (envelopeKeyComboBox.getEditor().getEditorComponent()); + textField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + validateEnvelope(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + validateEnvelope(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + validateEnvelope(); + } + }); + } + + private void initTable() { + couplesTable.getModel().addTableModelListener(e -> validateEnvelope()); + } + + private void fillComponents() { + envelopeKeyComboBox.setSelectedItem(envelopeTemplate.getKey()); + getTableModel().setValues(envelopeTemplate.getVertices()); + } + + private Set<String> envelopeKeys() { + Set<String> result = new HashSet<>(); + + // Ensure there is at least the default envelope key. + result.add(DEFAULT_ENVELOPE_KEY); + + // Extract all keys of envelopes defined at points and paths in the plant model. + result.addAll( + Stream.concat( + systemModel.getPointModels().stream() + .map(pointModel -> pointModel.getPropertyVehicleEnvelopes()), + systemModel.getPathModels().stream() + .map(pathModel -> pathModel.getPropertyVehicleEnvelopes()) + ) + .flatMap(property -> property.getValue().stream()) + .map(envelopeModel -> envelopeModel.getKey()) + .collect(Collectors.toSet()) + ); + + // Extract all envelope keys defined for vehicles in the plant model. + result.addAll( + systemModel.getVehicleModels().stream() + .map(vehicleModel -> vehicleModel.getPropertyEnvelopeKey().getText()) + .filter(envelopeKey -> envelopeKey != null) + .collect(Collectors.toSet()) + ); + + return result; + } + + private void validateEnvelope() { + ExplainedBoolean envelopeValid = isEnvelopeValid(); + validationTextArea.setForeground(envelopeValid.getValue() ? ENVELOPE_VALID : ENVELOPE_INVALID); + validationTextArea.setText(envelopeValid.getReason()); + + for (InputValidationListener validationListener : validationListeners) { + validationListener.inputValidationSuccessful(envelopeValid.getValue()); + } + } + + private ExplainedBoolean isEnvelopeValid() { + JTextField envelopeKeyTextField + = (JTextField) (envelopeKeyComboBox.getEditor().getEditorComponent()); + String envelopeKeyText = envelopeKeyTextField.getText(); + + CoupleTableModel model = getTableModel(); + if (!(model.getRowCount() >= 4)) { + return new ExplainedBoolean( + false, + bundle.getString("envelopePanel.textArea_validation.text.lessThanFourCoordinatesError") + ); + } + if (!(model.firstAndLastEquals())) { + return new ExplainedBoolean( + false, + bundle.getString( + "envelopePanel.textArea_validation.text.firstAndLastCoordianteNotEqualError" + ) + ); + } + if (isKeyAlreadyDefinedInProperty(envelopeKeyText)) { + return new ExplainedBoolean( + false, + bundle.getString("envelopePanel.textArea_validation.text.envelopeKeyAlreadyDefinedError") + ); + } + + return new ExplainedBoolean( + true, + bundle.getString("envelopePanel.textArea_validation.text.envelopeValid") + ); + } + + private boolean isKeyAlreadyDefinedInProperty(String key) { + switch (mode) { + case CREATE: + return propertyEnvelopeKeys.contains(key); + case EDIT: + return propertyEnvelopeKeys.stream() + // When editing an envelope, ignore the key of the envelope being edited. + .filter(k -> !Objects.equals(k, envelopeTemplate.getKey())) + .anyMatch(k -> Objects.equals(k, key)); + default: + throw new IllegalArgumentException("Unhandled edit mode: " + mode); + } + } + + private CoupleTableModel getTableModel() { + return (CoupleTableModel) couplesTable.getModel(); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + envelopeKeyLabel = new javax.swing.JLabel(); + envelopeKeyComboBox = new javax.swing.JComboBox<>(); + coordintesLabel = new javax.swing.JLabel(); + controlPanel = new javax.swing.JPanel(); + addButton = new javax.swing.JButton(); + removeButton = new javax.swing.JButton(); + moveUpButton = new javax.swing.JButton(); + moveDownButton = new javax.swing.JButton(); + controlFiller = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 32767)); + couplesScrollPane = new javax.swing.JScrollPane(); + couplesTable = new javax.swing.JTable(); + validationLabel = new javax.swing.JLabel(); + validationScrollPane = new javax.swing.JScrollPane(); + validationTextArea = new javax.swing.JTextArea(); + + setLayout(new java.awt.GridBagLayout()); + + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/panels/propertyEditing"); // NOI18N + envelopeKeyLabel.setText(bundle.getString("envelopePanel.label_envelopeKey.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 3, 0); + add(envelopeKeyLabel, gridBagConstraints); + + envelopeKeyComboBox.setEditable(true); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + add(envelopeKeyComboBox, gridBagConstraints); + + coordintesLabel.setText(bundle.getString("envelopePanel.label_envelopeCoordinates.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(12, 0, 3, 0); + add(coordintesLabel, gridBagConstraints); + + controlPanel.setLayout(new java.awt.GridBagLayout()); + + addButton.setText(bundle.getString("envelopePanel.button_add.text")); // NOI18N + addButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 15, 10, 0); + controlPanel.add(addButton, gridBagConstraints); + + removeButton.setText(bundle.getString("envelopePanel.button_remove.text")); // NOI18N + removeButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + removeButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 15, 10, 0); + controlPanel.add(removeButton, gridBagConstraints); + + moveUpButton.setText(bundle.getString("envelopePanel.button_up.text")); // NOI18N + moveUpButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + moveUpButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 15, 10, 0); + controlPanel.add(moveUpButton, gridBagConstraints); + + moveDownButton.setText(bundle.getString("envelopePanel.button_down.text")); // NOI18N + moveDownButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + moveDownButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTH; + gridBagConstraints.insets = new java.awt.Insets(0, 15, 0, 0); + controlPanel.add(moveDownButton, gridBagConstraints); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weighty = 1.0; + controlPanel.add(controlFiller, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + add(controlPanel, gridBagConstraints); + + couplesScrollPane.setPreferredSize(new java.awt.Dimension(300, 200)); + + couplesTable.setModel(new CoupleTableModel()); + couplesScrollPane.setViewportView(couplesTable); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + add(couplesScrollPane, gridBagConstraints); + + validationLabel.setText(bundle.getString("envelopePanel.label_envelopeValidation.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(12, 0, 3, 0); + add(validationLabel, gridBagConstraints); + + validationTextArea.setColumns(20); + validationTextArea.setLineWrap(true); + validationTextArea.setRows(2); + validationScrollPane.setViewportView(validationTextArea); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 5; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + add(validationScrollPane, gridBagConstraints); + }// </editor-fold>//GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void addButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addButtonActionPerformed + getTableModel().add(new Couple(0, 0)); + }//GEN-LAST:event_addButtonActionPerformed + + private void removeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removeButtonActionPerformed + getTableModel().remove(couplesTable.getSelectedRow()); + }//GEN-LAST:event_removeButtonActionPerformed + + private void moveDownButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_moveDownButtonActionPerformed + int selectedRow = couplesTable.getSelectedRow(); + if (getTableModel().moveDown(selectedRow)) { + couplesTable.setRowSelectionInterval(selectedRow + 1, selectedRow + 1); + } + }//GEN-LAST:event_moveDownButtonActionPerformed + + private void moveUpButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_moveUpButtonActionPerformed + int selectedRow = couplesTable.getSelectedRow(); + if (getTableModel().moveUp(selectedRow)) { + couplesTable.setRowSelectionInterval(selectedRow - 1, selectedRow - 1); + } + }//GEN-LAST:event_moveUpButtonActionPerformed + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton addButton; + private javax.swing.Box.Filler controlFiller; + private javax.swing.JPanel controlPanel; + private javax.swing.JLabel coordintesLabel; + private javax.swing.JScrollPane couplesScrollPane; + private javax.swing.JTable couplesTable; + private javax.swing.JComboBox<String> envelopeKeyComboBox; + private javax.swing.JLabel envelopeKeyLabel; + private javax.swing.JButton moveDownButton; + private javax.swing.JButton moveUpButton; + private javax.swing.JButton removeButton; + private javax.swing.JLabel validationLabel; + private javax.swing.JScrollPane validationScrollPane; + private javax.swing.JTextArea validationTextArea; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON + + /** + * Defines the modes this panel can be used in. + */ + public enum Mode { + /** + * The mode for creating envelopes. + */ + CREATE, + /** + * The mode for editing envelopes. + */ + EDIT; + } + + private class CoupleTableModel + extends + AbstractTableModel { + + /** + * The number of the "X" column. + */ + private final int columnX = 0; + /** + * The number of the "Y" column. + */ + private final int columnY = 1; + /** + * The column names. + */ + private final String[] columnNames + = new String[]{ + bundle.getString("envelopePanel.table_couples.column_x.headerText"), + bundle.getString("envelopePanel.table_couples.column_y.headerText") + }; + /** + * Column classes. + */ + private final Class<?>[] columnClasses + = new Class<?>[]{ + String.class, + String.class + }; + /** + * The values in this model. + */ + private final List<Couple> values = new ArrayList<>(); + + /** + * Creates a new instance. + */ + CoupleTableModel() { + } + + @Override + public Class<?> getColumnClass(int columnIndex) { + return columnClasses[columnIndex]; + } + + @Override + public String getColumnName(int columnIndex) { + return columnNames[columnIndex]; + } + + @Override + public boolean isCellEditable(int row, int column) { + return true; + } + + @Override + public int getRowCount() { + return values.size(); + } + + @Override + public int getColumnCount() { + return columnNames.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= getRowCount()) { + return null; + } + + Couple entry = values.get(rowIndex); + switch (columnIndex) { + case columnX: + return entry.getX(); + case columnY: + return entry.getY(); + default: + throw new IllegalArgumentException("Invalid column index: " + columnIndex); + } + } + + @Override + public void setValueAt(Object value, int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= getRowCount()) { + throw new IllegalArgumentException("Invalid row index: " + rowIndex); + } + + long newCoordinate; + try { + newCoordinate = Long.parseLong((String) value); + } + catch (NumberFormatException e) { + JOptionPane.showMessageDialog( + couplesTable, + bundle.getString("envelopePanel.optionPane_invalidNumberError.message") + ); + return; + } + + Couple prevEntry = values.get(rowIndex); + switch (columnIndex) { + case columnX: + values.set(rowIndex, new Couple(newCoordinate, prevEntry.getY())); + break; + case columnY: + values.set(rowIndex, new Couple(prevEntry.getX(), newCoordinate)); + break; + default: + throw new IllegalArgumentException("Invalid column index: " + columnIndex); + } + + fireTableCellUpdated(rowIndex, columnIndex); + } + + public boolean firstAndLastEquals() { + return Objects.equals(values.get(0), values.get(values.size() - 1)); + } + + public void setValues(List<Couple> values) { + requireNonNull(values, "values"); + + this.values.clear(); + this.values.addAll(values); + fireTableDataChanged(); + } + + public List<Couple> getValues() { + return Collections.unmodifiableList(values); + } + + public boolean add(Couple couple) { + values.add(couple); + fireTableRowsInserted(values.size() - 1, values.size() - 1); + return true; + } + + public boolean remove(int row) { + if (!rowInBounds(row)) { + return false; + } + + values.remove(row); + fireTableRowsDeleted(row, row); + return true; + } + + public boolean moveDown(int row) { + if (!rowInBounds(row) || row == values.size() - 1) { + return false; + } + + Couple value = values.remove(row); + values.add(row + 1, value); + fireTableRowsUpdated(row, row + 1); + return true; + } + + public boolean moveUp(int row) { + if (!rowInBounds(row) || row == 0) { + return false; + } + + Couple value = values.remove(row); + values.add(row - 1, value); + fireTableRowsUpdated(row - 1, row); + return true; + } + + private boolean rowInBounds(int row) { + if (values.isEmpty()) { + return false; + } + + return row >= 0 && row <= values.size() - 1; + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/EnvelopesPropertyEditorPanel.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/EnvelopesPropertyEditorPanel.form new file mode 100644 index 0000000..01c7503 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/EnvelopesPropertyEditorPanel.form @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.3" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,44,0,0,1,-112"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/> + <SubComponents> + <Container class="javax.swing.JScrollPane" name="envelopesScrollPane"> + <AuxValues> + <AuxValue name="autoScrollPane" type="java.lang.Boolean" value="true"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription"> + <BorderConstraints direction="Center"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JTable" name="envelopesTable"> + <Properties> + <Property name="model" type="javax.swing.table.TableModel" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> + <Connection code="new EnvelopeTableModel()" type="code"/> + </Property> + </Properties> + </Component> + </SubComponents> + </Container> + <Container class="javax.swing.JPanel" name="controlPanel"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription"> + <BorderConstraints direction="East"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Component class="javax.swing.JButton" name="addButton"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="envelopesPropertyEditorPanel.button_add.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="addButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="-1" gridY="-1" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="15" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JButton" name="editButton"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="envelopesPropertyEditorPanel.button_edit.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="editButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="1" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="10" insetsLeft="15" insetsBottom="10" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JButton" name="removeButton"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="envelopesPropertyEditorPanel.button_remove.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="removeButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="2" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="15" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.Box$Filler" name="controlFiller"> + <Properties> + <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[32767, 32767]"/> + </Property> + </Properties> + <AuxValues> + <AuxValue name="classDetails" type="java.lang.String" value="Box.Filler.Glue"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="3" gridWidth="1" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="1.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> + </Container> + </SubComponents> +</Form> diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/EnvelopesPropertyEditorPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/EnvelopesPropertyEditorPanel.java new file mode 100644 index 0000000..26b03a8 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/EnvelopesPropertyEditorPanel.java @@ -0,0 +1,434 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.awt.Dimension; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.swing.JDialog; +import javax.swing.JPanel; +import javax.swing.RowSorter; +import javax.swing.SortOrder; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableColumn; +import javax.swing.table.TableRowSorter; +import org.opentcs.data.model.Couple; +import org.opentcs.guing.base.components.properties.type.EnvelopesProperty; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.base.model.EnvelopeModel; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; +import org.opentcs.guing.common.components.dialogs.StandardContentDialog; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.util.gui.StringTableCellRenderer; + +/** + * User interface to edit the envelope property of points and paths. + */ +public class EnvelopesPropertyEditorPanel + extends + JPanel + implements + DetailsDialogContent { + + private static final EnvelopeModel DEFAULT_ENVELOPE + = new EnvelopeModel( + EnvelopePanel.DEFAULT_ENVELOPE_KEY, + List.of( + new Couple(0, 0), + new Couple(0, 0), + new Couple(0, 0), + new Couple(0, 0) + ) + ); + /** + * The bundle to be used. + */ + private final ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH); + /** + * Manager of the system model. + */ + private final ModelManager modelManager; + /** + * The property to edit. + */ + private EnvelopesProperty fProperty; + /** + * The sorter for the table. + */ + private TableRowSorter<EnvelopeTableModel> sorter; + + /** + * Creates a new instance. + * + * @param modelManager Manages the system model. + */ + @Inject + @SuppressWarnings("this-escape") + public EnvelopesPropertyEditorPanel(ModelManager modelManager) { + this.modelManager = requireNonNull(modelManager, "modelManager"); + + initComponents(); + initTable(); + + setPreferredSize(new Dimension(500, 250)); + } + + @Override + public void setProperty(Property property) { + fProperty = (EnvelopesProperty) property; + getTableModel().setValues(fProperty.getValue()); + } + + @Override + public void updateValues() { + fProperty.setValue(getTableModel().getValues()); + } + + @Override + public Property getProperty() { + return fProperty; + } + + @Override + public String getTitle() { + return bundle.getString("envelopesPropertyEditorPanel.title"); + } + + private void initTable() { + TableColumn columnKey = envelopesTable.getColumnModel() + .getColumn(envelopesTable.convertColumnIndexToView(EnvelopeTableModel.COLUMN_COORDINATES)); + columnKey.setCellRenderer(new CoupleListCellRenderer()); + + sorter = new TableRowSorter<>(getTableModel()); + // Sort the table by envelope keys. + sorter.setSortKeys( + Arrays.asList( + new RowSorter.SortKey(EnvelopeTableModel.COLUMN_KEY, SortOrder.ASCENDING) + ) + ); + // ...but prevent manual sorting. + for (int i = 0; i < envelopesTable.getColumnCount(); i++) { + sorter.setSortable(i, false); + } + sorter.setSortsOnUpdates(true); + envelopesTable.setRowSorter(sorter); + } + + private EnvelopeTableModel getTableModel() { + return (EnvelopeTableModel) envelopesTable.getModel(); + } + + private Set<String> definedEnvelopeKeys() { + return getTableModel().getValues().stream() + .map(EnvelopeModel::getKey) + .collect(Collectors.toSet()); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + envelopesScrollPane = new javax.swing.JScrollPane(); + envelopesTable = new javax.swing.JTable(); + controlPanel = new javax.swing.JPanel(); + addButton = new javax.swing.JButton(); + editButton = new javax.swing.JButton(); + removeButton = new javax.swing.JButton(); + controlFiller = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 32767)); + + setLayout(new java.awt.BorderLayout()); + + envelopesTable.setModel(new EnvelopeTableModel()); + envelopesScrollPane.setViewportView(envelopesTable); + + add(envelopesScrollPane, java.awt.BorderLayout.CENTER); + + controlPanel.setLayout(new java.awt.GridBagLayout()); + + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/panels/propertyEditing"); // NOI18N + addButton.setText(bundle.getString("envelopesPropertyEditorPanel.button_add.text")); // NOI18N + addButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 15, 0, 0); + controlPanel.add(addButton, gridBagConstraints); + + editButton.setText(bundle.getString("envelopesPropertyEditorPanel.button_edit.text")); // NOI18N + editButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + editButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(10, 15, 10, 0); + controlPanel.add(editButton, gridBagConstraints); + + removeButton.setText(bundle.getString("envelopesPropertyEditorPanel.button_remove.text")); // NOI18N + removeButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + removeButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 15, 0, 0); + controlPanel.add(removeButton, gridBagConstraints); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weighty = 1.0; + controlPanel.add(controlFiller, gridBagConstraints); + + add(controlPanel, java.awt.BorderLayout.EAST); + }// </editor-fold>//GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void addButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addButtonActionPerformed + JDialog parent = (JDialog) getTopLevelAncestor(); + EnvelopePanel content = new EnvelopePanel( + DEFAULT_ENVELOPE, + EnvelopePanel.Mode.CREATE, + definedEnvelopeKeys(), + modelManager.getModel() + ); + StandardContentDialog dialog = new StandardContentDialog(parent, content); + content.addInputValidationListener(dialog); + dialog.setLocationRelativeTo(parent); + dialog.setVisible(true); + if (dialog.getReturnStatus() == StandardContentDialog.RET_OK + && content.getEnvelopeModel().isPresent()) { + getTableModel().add(content.getEnvelopeModel().get()); + } + }//GEN-LAST:event_addButtonActionPerformed + + private void editButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_editButtonActionPerformed + int selectedRow = envelopesTable.getSelectedRow(); + if (selectedRow == -1) { + return; + } + EnvelopeModel selectedModel = getTableModel().getValueAt( + envelopesTable.convertRowIndexToModel(selectedRow) + ); + + JDialog parent = (JDialog) getTopLevelAncestor(); + EnvelopePanel content = new EnvelopePanel( + selectedModel, + EnvelopePanel.Mode.EDIT, + definedEnvelopeKeys(), + modelManager.getModel() + ); + StandardContentDialog dialog = new StandardContentDialog(parent, content); + content.addInputValidationListener(dialog); + dialog.setLocationRelativeTo(parent); + dialog.setVisible(true); + + if (dialog.getReturnStatus() == StandardContentDialog.RET_OK + && content.getEnvelopeModel().isPresent()) { + getTableModel().update( + envelopesTable.convertRowIndexToModel(selectedRow), + content.getEnvelopeModel().get() + ); + } + }//GEN-LAST:event_editButtonActionPerformed + + private void removeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removeButtonActionPerformed + int selectedRow = envelopesTable.getSelectedRow(); + if (selectedRow == -1) { + return; + } + + getTableModel().remove(envelopesTable.convertRowIndexToModel(selectedRow)); + }//GEN-LAST:event_removeButtonActionPerformed + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton addButton; + private javax.swing.Box.Filler controlFiller; + private javax.swing.JPanel controlPanel; + private javax.swing.JButton editButton; + private javax.swing.JScrollPane envelopesScrollPane; + private javax.swing.JTable envelopesTable; + private javax.swing.JButton removeButton; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON + + private class EnvelopeTableModel + extends + AbstractTableModel { + + /** + * The number of the "Key" column. + */ + public static final int COLUMN_KEY = 0; + /** + * The number of the "Coordinates" column. + */ + public static final int COLUMN_COORDINATES = 1; + /** + * The column names. + */ + private final String[] columnNames + = new String[]{ + bundle.getString( + "envelopesPropertyEditorPanel.table_envelopes.column_key.headerText" + ), + bundle.getString( + "envelopesPropertyEditorPanel.table_envelopes.column_coordinates.headerText" + ) + }; + /** + * Column classes. + */ + private final Class<?>[] columnClasses + = new Class<?>[]{ + String.class, + EnvelopeModel.class + }; + /** + * The values in this model. + */ + private final List<EnvelopeModel> values = new ArrayList<>(); + + /** + * Creates a new instance. + */ + EnvelopeTableModel() { + } + + @Override + public Class<?> getColumnClass(int columnIndex) { + return columnClasses[columnIndex]; + } + + @Override + public String getColumnName(int columnIndex) { + return columnNames[columnIndex]; + } + + @Override + public boolean isCellEditable(int row, int column) { + return false; + } + + @Override + public int getRowCount() { + return values.size(); + } + + @Override + public int getColumnCount() { + return columnNames.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= getRowCount()) { + return null; + } + + EnvelopeModel entry = values.get(rowIndex); + switch (columnIndex) { + case COLUMN_KEY: + return entry.getKey(); + case COLUMN_COORDINATES: + return entry.getVertices(); + default: + throw new IllegalArgumentException("Invalid column index: " + columnIndex); + } + } + + public void setValues(List<EnvelopeModel> values) { + requireNonNull(values, "values"); + + this.values.clear(); + this.values.addAll(values); + fireTableDataChanged(); + } + + public List<EnvelopeModel> getValues() { + return Collections.unmodifiableList(values); + } + + public EnvelopeModel getValueAt(int row) { + return values.get(row); + } + + public boolean add(EnvelopeModel envelopeModel) { + values.add(envelopeModel); + fireTableRowsInserted(values.size() - 1, values.size() - 1); + return true; + } + + public boolean update(int row, EnvelopeModel envelopeModel) { + if (!rowInBounds(row)) { + return false; + } + + values.set(row, envelopeModel); + fireTableRowsUpdated(row, row); + return true; + } + + public boolean remove(int row) { + if (!rowInBounds(row)) { + return false; + } + + values.remove(row); + fireTableRowsDeleted(row, row); + return true; + } + + private boolean rowInBounds(int row) { + if (values.isEmpty()) { + return false; + } + + return row >= 0 && row <= values.size() - 1; + } + } + + private class CoupleListCellRenderer + extends + StringTableCellRenderer<List<Couple>> { + + CoupleListCellRenderer() { + super( + couples -> couples.stream() + .map(couple -> "(" + couple.getX() + "," + couple.getY() + ")") + .collect(Collectors.joining(";")) + ); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/KeyValuePropertyEditorPanel.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/KeyValuePropertyEditorPanel.form new file mode 100644 index 0000000..09cc811 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/KeyValuePropertyEditorPanel.form @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,0,102,0,0,0,-31"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Component class="javax.swing.JLabel" name="keyLabel"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="keyLabel" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="keyValuePropertyEditorPanel.label_key.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="-1" gridY="-1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="0" insetsRight="0" anchor="17" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JLabel" name="valueLabel"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="valueLabel" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="keyValuePropertyEditorPanel.label_value.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="0" insetsRight="0" anchor="17" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JComboBox" name="keyComboBox"> + <Properties> + <Property name="editable" type="boolean" value="true"/> + <Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor"> + <StringArray count="0"/> + </Property> + <Property name="prototypeDisplayValue" type="java.lang.Object" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> + <Connection code=""tenletters"" type="code"/> + </Property> + </Properties> + <Events> + <EventHandler event="itemStateChanged" listener="java.awt.event.ItemListener" parameters="java.awt.event.ItemEvent" handler="keyValueChangedListener"/> + </Events> + <AuxValues> + <AuxValue name="JavaCodeGenerator_CreateCodeCustom" type="java.lang.String" value="new JComboBox<>(propertySuggestions.getKeySuggestions().toArray(new String[propertySuggestions.getKeySuggestions().size()]))"/> + <AuxValue name="JavaCodeGenerator_SerializeTo" type="java.lang.String" value="KeyValuePropertyEditorPanel_keyComboBox"/> + <AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="<String>"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="-1" gridY="-1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JComboBox" name="valueComboBox"> + <Properties> + <Property name="editable" type="boolean" value="true"/> + <Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor"> + <StringArray count="0"/> + </Property> + <Property name="prototypeDisplayValue" type="java.lang.Object" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> + <Connection code=""tenletters"" type="code"/> + </Property> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_CreateCodeCustom" type="java.lang.String" value="new JComboBox<>(propertySuggestions.getValueSuggestions().toArray(new String[propertySuggestions.getValueSuggestions().size()]))"/> + <AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="<String>"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="1" gridY="1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> +</Form> diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/KeyValuePropertyEditorPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/KeyValuePropertyEditorPanel.java new file mode 100644 index 0000000..5e1804b --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/KeyValuePropertyEditorPanel.java @@ -0,0 +1,170 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.Inject; +import java.util.Set; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.JPanel; +import javax.swing.JTextField; +import org.opentcs.components.plantoverview.PropertySuggestions; +import org.opentcs.guing.base.components.properties.type.KeyValueProperty; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; +import org.opentcs.guing.common.components.properties.type.MergedPropertySuggestions; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.util.gui.BoundsPopupMenuListener; + +/** + * A panel for editing a property (key-value pair). + * + * @see KeyValueProperty + */ +public class KeyValuePropertyEditorPanel + extends + JPanel + implements + DetailsDialogContent { + + /** + * The property to be edited. + */ + private KeyValueProperty fProperty; + /** + * The combo box's key textfield. + */ + private final JTextField keyTextField; + /** + * The combo box's value textfield. + */ + private final JTextField valueTextField; + /** + * Suggestions for property keys and values. + */ + private final PropertySuggestions propertySuggestions; + + /** + * Creates new instance. + * + * @param propertySuggestions The properties that are suggested. + */ + @Inject + @SuppressWarnings("this-escape") + public KeyValuePropertyEditorPanel(MergedPropertySuggestions propertySuggestions) { + this.propertySuggestions = requireNonNull(propertySuggestions, "propertySuggestions"); + initComponents(); + valueComboBox.addPopupMenuListener(new BoundsPopupMenuListener()); + keyComboBox.addPopupMenuListener(new BoundsPopupMenuListener()); + fProperty = new KeyValueProperty(null, "", ""); + keyTextField = (JTextField) (keyComboBox.getEditor().getEditorComponent()); + valueTextField = (JTextField) (valueComboBox.getEditor().getEditorComponent()); + } + + @Override + public void setProperty(Property property) { + fProperty = (KeyValueProperty) property; + keyTextField.setText(fProperty.getKey()); + valueTextField.setText(fProperty.getValue()); + } + + @Override + public void updateValues() { + fProperty.setKeyAndValue(keyTextField.getText(), valueTextField.getText()); + } + + @Override + public String getTitle() { + return ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH) + .getString("keyValuePropertyEditorPanel.title"); + } + + @Override + public Property getProperty() { + return fProperty; + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + keyLabel = new javax.swing.JLabel(); + valueLabel = new javax.swing.JLabel(); + keyComboBox = new JComboBox<>(propertySuggestions.getKeySuggestions().toArray(new String[propertySuggestions.getKeySuggestions().size()])); + valueComboBox = new JComboBox<>(propertySuggestions.getValueSuggestions().toArray(new String[propertySuggestions.getValueSuggestions().size()])); + + setLayout(new java.awt.GridBagLayout()); + + keyLabel.setFont(keyLabel.getFont()); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/panels/propertyEditing"); // NOI18N + keyLabel.setText(bundle.getString("keyValuePropertyEditorPanel.label_key.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + add(keyLabel, gridBagConstraints); + + valueLabel.setFont(valueLabel.getFont()); + valueLabel.setText(bundle.getString("keyValuePropertyEditorPanel.label_value.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + add(valueLabel, gridBagConstraints); + + keyComboBox.setEditable(true); + keyComboBox.setPrototypeDisplayValue("tenletters"); + keyComboBox.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + keyValueChangedListener(evt); + } + }); + add(keyComboBox, new java.awt.GridBagConstraints()); + + valueComboBox.setEditable(true); + valueComboBox.setPrototypeDisplayValue("tenletters"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + add(valueComboBox, gridBagConstraints); + }// </editor-fold>//GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void keyValueChangedListener(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_keyValueChangedListener + + Set<String> specSuggestions = propertySuggestions.getValueSuggestionsFor( + String.valueOf(keyComboBox.getSelectedItem()) + ); + if (specSuggestions.isEmpty()) { + specSuggestions = propertySuggestions.getValueSuggestions(); + } + String[] valueSuggestions = specSuggestions + .toArray(new String[specSuggestions.size()]); + Object currentSuggestion = valueComboBox.getEditor().getItem(); + valueComboBox.setModel(new DefaultComboBoxModel<>(valueSuggestions)); + valueComboBox.validate(); + valueComboBox.getEditor().setItem(currentSuggestion); + }//GEN-LAST:event_keyValueChangedListener + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JComboBox<String> keyComboBox; + private javax.swing.JLabel keyLabel; + private javax.swing.JComboBox<String> valueComboBox; + private javax.swing.JLabel valueLabel; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/KeyValueSetPropertyEditorPanel.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/KeyValueSetPropertyEditorPanel.form new file mode 100644 index 0000000..7d5da04 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/KeyValueSetPropertyEditorPanel.form @@ -0,0 +1,111 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,44,0,0,1,-112"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/> + <SubComponents> + <Container class="javax.swing.JScrollPane" name="itemsScrollPane"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription"> + <BorderConstraints direction="Center"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JTable" name="itemsTable"> + <Properties> + <Property name="model" type="javax.swing.table.TableModel" editor="org.netbeans.modules.form.editors2.TableModelEditor"> + <Table columnCount="0" rowCount="0"/> + </Property> + </Properties> + </Component> + </SubComponents> + </Container> + <Container class="javax.swing.JPanel" name="controlPanel"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription"> + <BorderConstraints direction="East"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Component class="javax.swing.JButton" name="addButton"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="addButton" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="keyValueSetPropertyEditorPanel.button_add.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + <Property name="opaque" type="boolean" value="false"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="addButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="-1" gridY="-1" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="3" insetsLeft="3" insetsBottom="3" insetsRight="3" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JButton" name="editButton"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="editButton" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="keyValueSetPropertyEditorPanel.button_edit.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + <Property name="opaque" type="boolean" value="false"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="editButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="1" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="3" insetsLeft="3" insetsBottom="3" insetsRight="3" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JButton" name="removeButton"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="removeButton" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="keyValueSetPropertyEditorPanel.button_remove.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + <Property name="opaque" type="boolean" value="false"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="removeButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="2" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="3" insetsLeft="3" insetsBottom="3" insetsRight="3" anchor="19" weightX="0.0" weightY="0.5"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> + </Container> + </SubComponents> +</Form> diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/KeyValueSetPropertyEditorPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/KeyValueSetPropertyEditorPanel.java new file mode 100644 index 0000000..63916d4 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/KeyValueSetPropertyEditorPanel.java @@ -0,0 +1,401 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import java.awt.Dimension; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import javax.swing.JDialog; +import javax.swing.JOptionPane; +import javax.swing.event.ListSelectionEvent; +import javax.swing.table.TableModel; +import org.opentcs.data.ObjectPropConstants; +import org.opentcs.guing.base.components.properties.type.KeyValueProperty; +import org.opentcs.guing.base.components.properties.type.KeyValueSetProperty; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; +import org.opentcs.guing.common.components.dialogs.StandardDetailsDialog; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * UI for editing a set of key value pairs. + * + * @see KeyValueSetProperty + */ +public class KeyValueSetPropertyEditorPanel + extends + KeyValueSetPropertyViewerEditorPanel + implements + DetailsDialogContent { + + /** + * A resource bundle. + */ + private final ResourceBundleUtil resBundle + = ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH); + /** + * A provider that provides new instances of KeyValuePropertyEditorPanels + */ + private final Provider<KeyValuePropertyEditorPanel> editorProvider; + + /** + * Creates a new instance. + * + * @param editorProvider a guice injected provider of KeyValuePropertyEditorPanel Instances + */ + @Inject + @SuppressWarnings("this-escape") + public KeyValueSetPropertyEditorPanel(Provider<KeyValuePropertyEditorPanel> editorProvider) { + this.editorProvider = requireNonNull(editorProvider, "editorProvider"); + + initComponents(); + + itemsTable.setModel(new ItemsTableModel()); + + setPreferredSize(new Dimension(350, 200)); + + itemsTable.getSelectionModel().addListSelectionListener((ListSelectionEvent evt) -> { + if (evt.getValueIsAdjusting()) { + return; + } + + handleSelectionChanged(); + }); + } + + @Override + public void setProperty(Property property) { + super.setProperty(property); + ItemsTableModel model = (ItemsTableModel) itemsTable.getModel(); + + model.setRowCount(0); + + getProperty().getItems().stream() + .sorted((p1, p2) -> p1.getKey().compareTo(p2.getKey())) + .forEach( + keyValueProperty -> model.addRow( + new String[]{keyValueProperty.getKey(), keyValueProperty.getValue()} + ) + ); + + sortItems(); + updateView(); + } + + @Override + public void updateValues() { + List<KeyValueProperty> items = new ArrayList<>(); + TableModel model = itemsTable.getModel(); + int size = model.getRowCount(); + + for (int i = 0; i < size; i++) { + String key = (String) model.getValueAt(i, 0); + String value = (String) model.getValueAt(i, 1); + items.add(new KeyValueProperty(null, key, value)); + } + + getProperty().setItems(items); + } + + @Override + public String getTitle() { + return resBundle.getString("keyValueSetPropertyEditorPanel.title"); + } + + /** + * Returns the selected key/value property. + * + * @return the selected key/value property. + */ + private KeyValueProperty getSelectedKeyValueProperty() { + int i = itemsTable.getSelectedRow(); + + if (i == -1) { + return null; + } + + String key = (String) itemsTable.getValueAt(i, 0); + String value = (String) itemsTable.getValueAt(i, 1); + + return new KeyValueProperty(null, key, value); + } + + /** + * Selects a row/an item based on a key. + * + * @param key The key for the row/item to select. + */ + private void selectItem(String key) { + for (int i = 0; i < itemsTable.getRowCount(); i++) { + if (itemsTable.getValueAt(i, 0).equals(key)) { + itemsTable.getSelectionModel().setSelectionInterval(i, i); + break; + } + } + } + + /** + * Add a new key/value pair. + * + * @param key the key. + * @param value the value. + */ + private void addItem(String key, String value) { + for (int i = 0; i < itemsTable.getRowCount(); i++) { + if (itemsTable.getValueAt(i, 0).equals(key)) { + JOptionPane.showMessageDialog( + this, + resBundle.getString( + "keyValueSetPropertyEditorPanel.optionPane_keyAlreadyExists.message" + ) + ": " + key + ); + return; + } + } + + ItemsTableModel model = (ItemsTableModel) itemsTable.getModel(); + model.addRow(new Object[]{key, value}); + } + + /** + * Searches the key-value list using the old key and updates the key-value pair with the new key + * and the (new) value. + * If the old key equals the new key, only the value will be updated. + * + * @param oldKey The old key of the key-value pair to be updated. + * @param newKey The new key of the key-value pair to be updated. + * @param value The new value. + */ + private void updateItem(String oldKey, String newKey, String value) { + // Searching for the edited key-value pair... + for (int oldKeyRow = 0; oldKeyRow < itemsTable.getRowCount(); oldKeyRow++) { + if (Objects.equals(itemsTable.getValueAt(oldKeyRow, 0), oldKey)) { + // Searching for another key-value pair with the same key as newKey... + for (int newKeyRow = 0; newKeyRow < itemsTable.getRowCount(); newKeyRow++) { + // If there is already a different row with the new key, notify the user and abort. + if (oldKeyRow != newKeyRow + && Objects.equals(itemsTable.getValueAt(newKeyRow, 0), newKey)) { + JOptionPane.showMessageDialog( + this, + resBundle.getString( + "keyValueSetPropertyEditorPanel.optionPane_keyAlreadyExists.message" + ) + ": " + newKey + ); + return; + } + } + // If its a legit edit, update the key-value pair. + itemsTable.setValueAt(value, oldKeyRow, 1); + itemsTable.setValueAt(newKey, oldKeyRow, 0); + } + } + } + + private void edit() { + KeyValueProperty p = getSelectedKeyValueProperty(); + + if (p == null) { + return; + } + KeyValueProperty pOld = new KeyValueProperty(p.getModel(), p.getKey(), p.getValue()); + JDialog parent = (JDialog) getTopLevelAncestor(); + KeyValuePropertyEditorPanel content = editorProvider.get(); + content.setProperty(p); + + StandardDetailsDialog dialog = new StandardDetailsDialog(parent, true, content); + + dialog.setLocationRelativeTo(parent); + dialog.setVisible(true); + + if (dialog.getReturnStatus() == StandardDetailsDialog.RET_OK) { + updateItem(pOld.getKey(), p.getKey(), p.getValue()); + sortItems(); + selectItem(p.getKey()); + + updateView(); + } + } + + /** + * Adds a new entry. + */ + private void add() { + JDialog parent = (JDialog) getTopLevelAncestor(); + + KeyValueProperty p = new KeyValueProperty(null); + KeyValuePropertyEditorPanel content = editorProvider.get(); + content.setProperty(p); + StandardDetailsDialog dialog = new StandardDetailsDialog(parent, true, content); + dialog.setLocationRelativeTo(parent); + dialog.setVisible(true); + + if (dialog.getReturnStatus() == StandardDetailsDialog.RET_OK) { + addItem(p.getKey(), p.getValue()); + sortItems(); + selectItem(p.getKey()); + + updateView(); + } + } + + private void handleSelectionChanged() { + updateView(); + } + + /** + * Updates the UI based on whether or not an entry is selected. + */ + private void updateView() { + final TableModel model = itemsTable.getModel(); + boolean selectedAreEditable = true; + + for (int selRowIndex : itemsTable.getSelectedRows()) { + String key = (String) model.getValueAt(selRowIndex, 0); + if (key.equals(ObjectPropConstants.LOC_DEFAULT_REPRESENTATION) + || key.equals(ObjectPropConstants.LOCTYPE_DEFAULT_REPRESENTATION)) { + selectedAreEditable = false; + break; + } + } + + boolean enableEditing = false; + boolean enableRemoval = false; + // Only allow removal of properties if at least one is selected and all of + // them are editable. + if (itemsTable.getSelectedRowCount() > 0 && selectedAreEditable) { + enableRemoval = true; + } + // Only allow editing for the selection if exactly one property is selected + // and it is editable. + if (itemsTable.getSelectedRowCount() == 1 && selectedAreEditable) { + enableEditing = true; + } + editButton.setEnabled(enableEditing); + removeButton.setEnabled(enableRemoval); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + itemsScrollPane = new javax.swing.JScrollPane(); + itemsTable = new javax.swing.JTable(); + controlPanel = new javax.swing.JPanel(); + addButton = new javax.swing.JButton(); + editButton = new javax.swing.JButton(); + removeButton = new javax.swing.JButton(); + + setLayout(new java.awt.BorderLayout()); + + itemsTable.setModel(new javax.swing.table.DefaultTableModel( + new Object [][] { + + }, + new String [] { + + } + )); + itemsScrollPane.setViewportView(itemsTable); + + add(itemsScrollPane, java.awt.BorderLayout.CENTER); + + controlPanel.setLayout(new java.awt.GridBagLayout()); + + addButton.setFont(addButton.getFont()); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/panels/propertyEditing"); // NOI18N + addButton.setText(bundle.getString("keyValueSetPropertyEditorPanel.button_add.text")); // NOI18N + addButton.setOpaque(false); + addButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + controlPanel.add(addButton, gridBagConstraints); + + editButton.setFont(editButton.getFont()); + editButton.setText(bundle.getString("keyValueSetPropertyEditorPanel.button_edit.text")); // NOI18N + editButton.setOpaque(false); + editButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + editButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + controlPanel.add(editButton, gridBagConstraints); + + removeButton.setFont(removeButton.getFont()); + removeButton.setText(bundle.getString("keyValueSetPropertyEditorPanel.button_remove.text")); // NOI18N + removeButton.setOpaque(false); + removeButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + removeButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.PAGE_START; + gridBagConstraints.weighty = 0.5; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + controlPanel.add(removeButton, gridBagConstraints); + + add(controlPanel, java.awt.BorderLayout.EAST); + }// </editor-fold>//GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void removeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removeButtonActionPerformed + int selectedRowIndex = itemsTable.getSelectedRow(); + + if (selectedRowIndex == -1) { + return; + } + + ((ItemsTableModel) itemsTable.getModel()).removeRow(selectedRowIndex); + + updateView(); + }//GEN-LAST:event_removeButtonActionPerformed + + private void editButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_editButtonActionPerformed + edit(); + }//GEN-LAST:event_editButtonActionPerformed + + private void addButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addButtonActionPerformed + add(); + }//GEN-LAST:event_addButtonActionPerformed + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton addButton; + private javax.swing.JPanel controlPanel; + private javax.swing.JButton editButton; + private javax.swing.JScrollPane itemsScrollPane; + private javax.swing.JTable itemsTable; + private javax.swing.JButton removeButton; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON + +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/KeyValueSetPropertyViewerEditorPanel.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/KeyValueSetPropertyViewerEditorPanel.form new file mode 100644 index 0000000..3e4e81a --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/KeyValueSetPropertyViewerEditorPanel.form @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,44,0,0,1,-112"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/> + <SubComponents> + <Container class="javax.swing.JScrollPane" name="itemsScrollPane"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription"> + <BorderConstraints direction="Center"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JTable" name="itemsTable"> + <Properties> + <Property name="model" type="javax.swing.table.TableModel" editor="org.netbeans.modules.form.editors2.TableModelEditor"> + <Table columnCount="0" rowCount="0"/> + </Property> + </Properties> + </Component> + </SubComponents> + </Container> + <Container class="javax.swing.JPanel" name="controlPanel"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription"> + <BorderConstraints direction="East"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + </Container> + </SubComponents> +</Form> diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/KeyValueSetPropertyViewerEditorPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/KeyValueSetPropertyViewerEditorPanel.java new file mode 100644 index 0000000..656b3af --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/KeyValueSetPropertyViewerEditorPanel.java @@ -0,0 +1,184 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import com.google.inject.Inject; +import java.awt.Dimension; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeSet; +import javax.swing.JPanel; +import javax.swing.table.DefaultTableModel; +import org.opentcs.guing.base.components.properties.type.KeyValueSetProperty; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * UI for viewing a key value property without being able to edit the property. + * + * @see KeyValueSetProperty + */ +public class KeyValueSetPropertyViewerEditorPanel + extends + JPanel + implements + DetailsDialogContent { + + /** + * A resource bundle. + */ + private final ResourceBundleUtil resBundle + = ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH); + /** + * The property edited. + */ + private KeyValueSetProperty fProperty; + + /** + * Creates a new instance. + */ + @Inject + @SuppressWarnings("this-escape") + public KeyValueSetPropertyViewerEditorPanel() { + initComponents(); + + itemsTable.setModel(new ItemsTableModel()); + + setPreferredSize(new Dimension(350, 200)); + } + + @Override + public void setProperty(Property property) { + fProperty = (KeyValueSetProperty) property; + + ItemsTableModel model = (ItemsTableModel) itemsTable.getModel(); + + model.setRowCount(0); + + getProperty().getItems().stream() + .sorted((p1, p2) -> p1.getKey().compareTo(p2.getKey())) + .forEach( + keyValueProperty -> model.addRow( + new String[]{keyValueProperty.getKey(), keyValueProperty.getValue()} + ) + ); + + sortItems(); + } + + @Override + public void updateValues() { + } + + @Override + public String getTitle() { + return resBundle.getString("keyValueSetPropertyViewerEditorPanel.title"); + } + + @Override + public KeyValueSetProperty getProperty() { + return fProperty; + } + + /** + * Sorts the entries based on their keys. + */ + protected void sortItems() { + Map<String, String> items = new HashMap<>(); + + for (int i = 0; i < itemsTable.getRowCount(); i++) { + items.put((String) itemsTable.getValueAt(i, 0), (String) itemsTable.getValueAt(i, 1)); + } + + int index = 0; + + for (String key : new TreeSet<>(items.keySet())) { + String value = items.get(key); + + itemsTable.setValueAt(key, index, 0); + itemsTable.setValueAt(value, index, 1); + + index++; + } + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + itemsScrollPane = new javax.swing.JScrollPane(); + itemsTable = new javax.swing.JTable(); + controlPanel = new javax.swing.JPanel(); + + setLayout(new java.awt.BorderLayout()); + + itemsTable.setModel(new javax.swing.table.DefaultTableModel( + new Object [][] { + + }, + new String [] { + + } + )); + itemsScrollPane.setViewportView(itemsTable); + + add(itemsScrollPane, java.awt.BorderLayout.CENTER); + + controlPanel.setLayout(new java.awt.GridBagLayout()); + add(controlPanel, java.awt.BorderLayout.EAST); + }// </editor-fold>//GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel controlPanel; + private javax.swing.JScrollPane itemsScrollPane; + private javax.swing.JTable itemsTable; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON + + protected class ItemsTableModel + extends + DefaultTableModel { + + private final Class<?>[] types = new Class<?>[]{ + String.class, String.class + }; + + public ItemsTableModel() { + super( + new Object[][]{}, + new String[]{ + resBundle.getString( + "keyValueSetPropertyViewerEditorPanel.table_properties.column_key.headerText" + ), + resBundle.getString( + "keyValueSetPropertyViewerEditorPanel.table_properties.column_value.headerText" + ) + } + ); + } + + @Override + public Class<?> getColumnClass(int columnIndex) { + return types[columnIndex]; + } + + @Override + public boolean isCellEditable(int row, int column) { + return false; + } + } + +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/LinkActionsEditorPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/LinkActionsEditorPanel.java new file mode 100644 index 0000000..f526d09 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/LinkActionsEditorPanel.java @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import java.util.ArrayList; +import java.util.List; +import javax.swing.DefaultListModel; +import javax.swing.JDialog; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.common.components.dialogs.StandardDetailsDialog; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Allows editing of actions that a vehicle can execute at a station. + * Which actions are possible is determined by the station type. + */ +public class LinkActionsEditorPanel + extends + StringSetPropertyEditorPanel { + + /** + * The bundle to be used. + */ + private final ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH); + + /** + * Creates a new instance. + */ + public LinkActionsEditorPanel() { + } + + @Override + public String getTitle() { + return bundle.getString("linkActionsEditorPanel.title"); + } + + @Override + protected void edit() { + String value = getItemsList().getSelectedValue(); + + if (value == null) { + return; + } + + int index = getItemsList().getSelectedIndex(); + JDialog parent = (JDialog) getTopLevelAncestor(); + SelectionPanel content = new SelectionPanel( + bundle.getString("linkActionsEditorPanel.dialog_actionSelectionEdit.title"), + bundle.getString("linkActionsEditorPanel.dialog_actionSelection.label_action.text"), + getPossibleItems(), + value + ); + StandardDetailsDialog dialog = new StandardDetailsDialog(parent, true, content); + dialog.setLocationRelativeTo(parent); + dialog.setVisible(true); + + if (dialog.getReturnStatus() == StandardDetailsDialog.RET_OK) { + DefaultListModel<String> model = (DefaultListModel<String>) getItemsList().getModel(); + model.setElementAt(content.getValue().toString(), index); + } + } + + @Override + protected void add() { + JDialog parent = (JDialog) getTopLevelAncestor(); + SelectionPanel content = new SelectionPanel( + bundle.getString("linkActionsEditorPanel.dialog_actionSelectionAdd.title"), + bundle.getString("linkActionsEditorPanel.dialog_actionSelection.label_action.text"), + getPossibleItems() + ); + StandardDetailsDialog dialog = new StandardDetailsDialog(parent, true, content); + dialog.setLocationRelativeTo(parent); + dialog.setVisible(true); + + if (dialog.getReturnStatus() == StandardDetailsDialog.RET_OK) { + DefaultListModel<String> model = (DefaultListModel<String>) getItemsList().getModel(); + Object value = content.getValue(); + if (value != null) { + model.addElement(value.toString()); + } + } + } + + /** + * Returns the possible actions that can be executed at a station. + * The actions are determined by the station type. + * + * @return The possible actions. + */ + private List<String> getPossibleItems() { + LinkModel ref = (LinkModel) getProperty().getModel(); + LocationTypeModel type = ref.getLocation().getLocationType(); + + return new ArrayList<>(type.getPropertyAllowedOperations().getItems()); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/LocationTypeActionsEditorPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/LocationTypeActionsEditorPanel.java new file mode 100644 index 0000000..2fae54f --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/LocationTypeActionsEditorPanel.java @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import javax.swing.DefaultListModel; +import javax.swing.JDialog; +import org.opentcs.guing.common.components.dialogs.StandardDetailsDialog; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Allows editing of actions that a vehicle can execute at stations with the location type. + */ +public class LocationTypeActionsEditorPanel + extends + StringSetPropertyEditorPanel { + + /** + * The bundle to be used. + */ + private final ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH); + + /** + * Creates a new instance. + */ + public LocationTypeActionsEditorPanel() { + } + + @Override + public String getTitle() { + return bundle.getString("locationTypeActionsEditorPanel.title"); + } + + @Override + protected void edit() { + String value = getItemsList().getSelectedValue(); + + if (value == null) { + return; + } + + int index = getItemsList().getSelectedIndex(); + JDialog parent = (JDialog) getTopLevelAncestor(); + StringPanel content = new StringPanel( + bundle.getString("locationTypeActionsEditorPanel.dialog_actionDefinitionEdit.title"), + bundle.getString( + "locationTypeActionsEditorPanel.dialog_actionDefinition.label_action.text" + ), + value + ); + StandardDetailsDialog dialog = new StandardDetailsDialog(parent, true, content); + dialog.setLocationRelativeTo(parent); + dialog.setVisible(true); + + if (dialog.getReturnStatus() == StandardDetailsDialog.RET_OK) { + DefaultListModel<String> model = (DefaultListModel<String>) getItemsList().getModel(); + model.setElementAt(content.getText(), index); + } + } + + @Override + protected void add() { + JDialog parent = (JDialog) getTopLevelAncestor(); + StringPanel content = new StringPanel( + bundle.getString("locationTypeActionsEditorPanel.dialog_actionDefinitionAdd.title"), + bundle.getString( + "locationTypeActionsEditorPanel.dialog_actionDefinition.label_action.text" + ), + "" + ); + StandardDetailsDialog dialog = new StandardDetailsDialog(parent, true, content); + dialog.setLocationRelativeTo(parent); + dialog.setVisible(true); + + if (dialog.getReturnStatus() == StandardDetailsDialog.RET_OK) { + DefaultListModel<String> model = (DefaultListModel<String>) getItemsList().getModel(); + model.addElement(content.getText()); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/OrderTypesPropertyEditorPanel.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/OrderTypesPropertyEditorPanel.form new file mode 100644 index 0000000..1e68125 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/OrderTypesPropertyEditorPanel.form @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <Properties> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[300, 250]"/> + </Property> + </Properties> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,0,-87,0,0,1,85"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Container class="javax.swing.JScrollPane" name="itemsScrollPane"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="1" gridWidth="1" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="1.0" weightY="1.0"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JList" name="itemsList"> + <Properties> + <Property name="selectionMode" type="int" value="0"/> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="<String>"/> + </AuxValues> + </Component> + </SubComponents> + </Container> + <Component class="javax.swing.JComboBox" name="typeComboBox"> + <Properties> + <Property name="editable" type="boolean" value="true"/> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="<String>"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="0" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="3" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JButton" name="addButton"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="addButton" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="orderTypesPropertyEditorPanel.button_add.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="addButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="1" gridY="0" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="3" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JButton" name="removeButton"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="removeButton" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="orderTypesPropertyEditorPanel.button_remove.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="removeButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="1" gridY="1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="0" insetsRight="0" anchor="11" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> +</Form> diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/OrderTypesPropertyEditorPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/OrderTypesPropertyEditorPanel.java new file mode 100644 index 0000000..2c37cf1 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/OrderTypesPropertyEditorPanel.java @@ -0,0 +1,236 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Enumeration; +import java.util.Set; +import java.util.TreeSet; +import javax.swing.DefaultListModel; +import javax.swing.JList; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.ListModel; +import org.opentcs.guing.base.components.properties.type.OrderTypesProperty; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; +import org.opentcs.guing.common.transport.OrderTypeSuggestionsPool; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * User interface to edit a set of order type strings. + */ +public class OrderTypesPropertyEditorPanel + extends + JPanel + implements + DetailsDialogContent { + + /** + * The bundle to be used. + */ + private final ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH); + /** + * The pool of types to suggest. + */ + private final OrderTypeSuggestionsPool typeSuggestionsPool; + /** + * The property to edit. + */ + private OrderTypesProperty fProperty; + + /** + * Creates a new instance. + * + * @param typeSuggestionsPool The pool of types to suggest. + */ + @Inject + @SuppressWarnings("this-escape") + public OrderTypesPropertyEditorPanel(OrderTypeSuggestionsPool typeSuggestionsPool) { + this.typeSuggestionsPool = requireNonNull(typeSuggestionsPool, "typeSuggestionsPool"); + initComponents(); + initCategoryCombobox(); + } + + @Override + public void setProperty(Property property) { + fProperty = (OrderTypesProperty) property; + DefaultListModel<String> model = new DefaultListModel<>(); + + for (String item : fProperty.getItems()) { + model.addElement(item); + } + + itemsList.setModel(model); + } + + @Override + public void updateValues() { + Set<String> items = new TreeSet<>(); + ListModel<String> model = itemsList.getModel(); + int size = model.getSize(); + + for (int i = 0; i < size; i++) { + items.add(model.getElementAt(i)); + } + + fProperty.setItems(items); + } + + @Override + public String getTitle() { + return bundle.getString("orderTypesPropertyEditorPanel.title"); + } + + @Override + public Property getProperty() { + return fProperty; + } + + /** + * Adds a new entry. Also adds the category to the pool. + */ + protected void add() { + DefaultListModel<String> model = (DefaultListModel<String>) itemsList.getModel(); + String category = typeComboBox.getSelectedItem().toString(); + + // Check for already added categories + Enumeration<String> entries = model.elements(); + while (entries.hasMoreElements()) { + String entry = entries.nextElement(); + if (entry.equals(category)) { + JOptionPane.showMessageDialog( + this, + bundle.getString( + "orderTypesPropertyEditorPanel.optionPane_typeAlreadyPresentError.message" + ) + ); + return; + } + } + + model.addElement(category); + + typeSuggestionsPool.addTypeSuggestion(category); + // Re-initialize the combo box since there may be a new entry + initCategoryCombobox(); + } + + /** + * Returns the list with the values. + * + * @return The list with the values. + */ + protected JList<String> getItemsList() { + return itemsList; + } + + private void initCategoryCombobox() { + typeComboBox.removeAllItems(); + for (String suggestion : typeSuggestionsPool.getTypeSuggestions()) { + typeComboBox.addItem(suggestion); + } + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + itemsScrollPane = new javax.swing.JScrollPane(); + itemsList = new javax.swing.JList<>(); + typeComboBox = new javax.swing.JComboBox<>(); + addButton = new javax.swing.JButton(); + removeButton = new javax.swing.JButton(); + + setPreferredSize(new java.awt.Dimension(300, 250)); + setLayout(new java.awt.GridBagLayout()); + + itemsList.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + itemsScrollPane.setViewportView(itemsList); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + add(itemsScrollPane, gridBagConstraints); + + typeComboBox.setEditable(true); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 3, 0); + add(typeComboBox, gridBagConstraints); + + addButton.setFont(addButton.getFont()); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/panels/propertyEditing"); // NOI18N + addButton.setText(bundle.getString("orderTypesPropertyEditorPanel.button_add.text")); // NOI18N + addButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 3, 0); + add(addButton, gridBagConstraints); + + removeButton.setFont(removeButton.getFont()); + removeButton.setText(bundle.getString("orderTypesPropertyEditorPanel.button_remove.text")); // NOI18N + removeButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + removeButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTH; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + add(removeButton, gridBagConstraints); + }// </editor-fold>//GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void removeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removeButtonActionPerformed + String value = itemsList.getSelectedValue(); + + if (value == null) { + return; + } + + DefaultListModel<String> model = (DefaultListModel<String>) itemsList.getModel(); + model.removeElement(value); + }//GEN-LAST:event_removeButtonActionPerformed + + private void addButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addButtonActionPerformed + add(); + }//GEN-LAST:event_addButtonActionPerformed + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton addButton; + private javax.swing.JList<String> itemsList; + private javax.swing.JScrollPane itemsScrollPane; + private javax.swing.JButton removeButton; + private javax.swing.JComboBox<String> typeComboBox; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/PeripheralOperationPanel.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/PeripheralOperationPanel.form new file mode 100644 index 0000000..c3bd660 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/PeripheralOperationPanel.form @@ -0,0 +1,154 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,0,113,0,0,1,50"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Component class="javax.swing.JLabel" name="locationLabel"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="locationLabel" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="peripheralOperationPanel.label_location.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="0" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="0" insetsRight="0" anchor="13" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JLabel" name="operationLabel"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="operationLabel" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="peripheralOperationPanel.label_operation.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="0" insetsRight="0" anchor="13" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JLabel" name="triggerLabel"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="triggerLabel" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="peripheralOperationPanel.label_trigger.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="2" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="0" insetsRight="0" anchor="13" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JLabel" name="requireCompleteLabel"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="requireCompleteLabel" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="peripheralOperationPanel.label_requireComplete.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="3" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="0" insetsRight="0" anchor="13" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JCheckBox" name="requireCompleteCheckBox"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="1" gridY="3" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="3" insetsLeft="3" insetsBottom="3" insetsRight="3" anchor="17" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JComboBox" name="locationComboBox"> + <Properties> + <Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor"> + <StringArray count="0"/> + </Property> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[200, 22]"/> + </Property> + <Property name="renderer" type="javax.swing.ListCellRenderer" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> + <Connection code="new StringListCellRenderer<LocationModel>(locationModel -> locationModel.getName())" type="code"/> + </Property> + </Properties> + <Events> + <EventHandler event="itemStateChanged" listener="java.awt.event.ItemListener" parameters="java.awt.event.ItemEvent" handler="locationComboBoxItemStateChanged"/> + </Events> + <AuxValues> + <AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="<LocationModel>"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="1" gridY="0" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="3" insetsLeft="3" insetsBottom="3" insetsRight="3" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JComboBox" name="operationComboBox"> + <Properties> + <Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor"> + <StringArray count="0"/> + </Property> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[200, 22]"/> + </Property> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="<String>"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="1" gridY="1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="3" insetsLeft="3" insetsBottom="3" insetsRight="3" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JComboBox" name="triggerComboBox"> + <Properties> + <Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor"> + <StringArray count="0"/> + </Property> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[200, 22]"/> + </Property> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="<PeripheralOperation.ExecutionTrigger>"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="1" gridY="2" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="3" insetsLeft="3" insetsBottom="3" insetsRight="3" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> +</Form> diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/PeripheralOperationPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/PeripheralOperationPanel.java new file mode 100644 index 0000000..de9142c --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/PeripheralOperationPanel.java @@ -0,0 +1,234 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import java.awt.event.ItemEvent; +import java.util.Comparator; +import java.util.Optional; +import javax.swing.JPanel; +import org.opentcs.data.peripherals.PeripheralOperation; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.base.model.PeripheralOperationModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.util.gui.StringListCellRenderer; + +/** + * User interface for a single line text. + */ +public class PeripheralOperationPanel + extends + JPanel + implements + DetailsDialogContent { + + private static final Comparator<LocationModel> BY_NAME + = (o1, o2) -> o1.getName().toLowerCase().compareTo(o2.getName().toLowerCase()); + /** + * The bundle to be used. + */ + private final ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH); + + /** + * Creates new form StringPanel. + * + * @param model The current system model. + */ + @SuppressWarnings("this-escape") + public PeripheralOperationPanel(SystemModel model) { + initComponents(); + + model.getLocationModels().stream() + .sorted(BY_NAME) + .forEach(locationComboBox::addItem); + loadOperations(); + + triggerComboBox.addItem(PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION); + triggerComboBox.addItem(PeripheralOperation.ExecutionTrigger.AFTER_MOVEMENT); + } + + private void loadOperations() { + LocationModel location = (LocationModel) locationComboBox.getSelectedItem(); + if (location == null) { + return; + } + + operationComboBox.removeAllItems(); + for (String op : location.getLocationType().getPropertyAllowedPeripheralOperations() + .getItems()) { + operationComboBox.addItem(op); + } + } + + @Override + public void updateValues() { + } + + @Override + public String getTitle() { + return bundle.getString("peripheralOperationPanel.title"); + } + + @Override + public void setProperty(Property property) { + } + + @Override + public Property getProperty() { + return null; + } + + public Optional<PeripheralOperationModel> getPeripheralOperationModel() { + if (!isSelectionValid()) { + return Optional.empty(); + } + + return Optional.of( + new PeripheralOperationModel( + ((LocationModel) locationComboBox.getSelectedItem()).getName(), + (String) operationComboBox.getSelectedItem(), + (PeripheralOperation.ExecutionTrigger) triggerComboBox.getSelectedItem(), + requireCompleteCheckBox.isSelected() + ) + ); + } + + public void setPeripheralOpartionModel(PeripheralOperationModel model) { + //Set location + for (int i = 0; i < locationComboBox.getItemCount(); i++) { + LocationModel location = locationComboBox.getItemAt(i); + if (location.getName().equals(model.getLocationName())) { + locationComboBox.setSelectedIndex(i); + } + } + operationComboBox.setSelectedItem(model.getOperation()); + triggerComboBox.setSelectedItem(model.getExecutionTrigger()); + requireCompleteCheckBox.setSelected(model.isCompletionRequired()); + } + + private boolean isSelectionValid() { + return locationComboBox.getSelectedItem() != null + && operationComboBox.getSelectedItem() != null + && triggerComboBox.getSelectedItem() != null; + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + locationLabel = new javax.swing.JLabel(); + operationLabel = new javax.swing.JLabel(); + triggerLabel = new javax.swing.JLabel(); + requireCompleteLabel = new javax.swing.JLabel(); + requireCompleteCheckBox = new javax.swing.JCheckBox(); + locationComboBox = new javax.swing.JComboBox<>(); + operationComboBox = new javax.swing.JComboBox<>(); + triggerComboBox = new javax.swing.JComboBox<>(); + + setLayout(new java.awt.GridBagLayout()); + + locationLabel.setFont(locationLabel.getFont()); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/panels/propertyEditing"); // NOI18N + locationLabel.setText(bundle.getString("peripheralOperationPanel.label_location.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + add(locationLabel, gridBagConstraints); + + operationLabel.setFont(operationLabel.getFont()); + operationLabel.setText(bundle.getString("peripheralOperationPanel.label_operation.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + add(operationLabel, gridBagConstraints); + + triggerLabel.setFont(triggerLabel.getFont()); + triggerLabel.setText(bundle.getString("peripheralOperationPanel.label_trigger.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + add(triggerLabel, gridBagConstraints); + + requireCompleteLabel.setFont(requireCompleteLabel.getFont()); + requireCompleteLabel.setText(bundle.getString("peripheralOperationPanel.label_requireComplete.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + add(requireCompleteLabel, gridBagConstraints); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(requireCompleteCheckBox, gridBagConstraints); + + locationComboBox.setPreferredSize(new java.awt.Dimension(200, 22)); + locationComboBox.setRenderer(new StringListCellRenderer<LocationModel>(locationModel -> locationModel.getName())); + locationComboBox.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + locationComboBoxItemStateChanged(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(locationComboBox, gridBagConstraints); + + operationComboBox.setPreferredSize(new java.awt.Dimension(200, 22)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(operationComboBox, gridBagConstraints); + + triggerComboBox.setPreferredSize(new java.awt.Dimension(200, 22)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(triggerComboBox, gridBagConstraints); + }// </editor-fold>//GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void locationComboBoxItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_locationComboBoxItemStateChanged + if (evt.getStateChange() == ItemEvent.SELECTED) { + loadOperations(); + } + }//GEN-LAST:event_locationComboBoxItemStateChanged + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JComboBox<LocationModel> locationComboBox; + private javax.swing.JLabel locationLabel; + private javax.swing.JComboBox<String> operationComboBox; + private javax.swing.JLabel operationLabel; + private javax.swing.JCheckBox requireCompleteCheckBox; + private javax.swing.JLabel requireCompleteLabel; + private javax.swing.JComboBox<PeripheralOperation.ExecutionTrigger> triggerComboBox; + private javax.swing.JLabel triggerLabel; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/PeripheralOperationsPropertyEditorPanel.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/PeripheralOperationsPropertyEditorPanel.form new file mode 100644 index 0000000..44ad88a --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/PeripheralOperationsPropertyEditorPanel.form @@ -0,0 +1,157 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,44,0,0,1,-112"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/> + <SubComponents> + <Container class="javax.swing.JScrollPane" name="itemsScrollPane"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription"> + <BorderConstraints direction="Center"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JTable" name="itemsTable"> + <Properties> + <Property name="model" type="javax.swing.table.TableModel" editor="org.netbeans.modules.form.editors2.TableModelEditor"> + <Table columnCount="0" rowCount="4"/> + </Property> + </Properties> + </Component> + </SubComponents> + </Container> + <Container class="javax.swing.JPanel" name="controlPanel"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription"> + <BorderConstraints direction="East"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Component class="javax.swing.JButton" name="addButton"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="addButton" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="peripheralOperationsPropertyEditorPanel.button_add.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="addButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="-1" gridY="-1" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="15" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JButton" name="editButton"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="editButton" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="peripheralOperationsPropertyEditorPanel.button_edit.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="editButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="1" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="10" insetsLeft="15" insetsBottom="10" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JButton" name="removeButton"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="removeButton" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="peripheralOperationsPropertyEditorPanel.button_remove.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="removeButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="2" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="15" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Container class="javax.swing.JPanel" name="rigidArea"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="5" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="1.0"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignFlowLayout"/> + </Container> + <Component class="javax.swing.JButton" name="moveUpButton"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="moveUpButton" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="peripheralOperationsPropertyEditorPanel.button_up.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="moveUpButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="3" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="10" insetsLeft="15" insetsBottom="10" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JButton" name="moveDownButton"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="moveDownButton" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="peripheralOperationsPropertyEditorPanell.button_down.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="moveDownButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="4" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="15" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> + </Container> + </SubComponents> +</Form> diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/PeripheralOperationsPropertyEditorPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/PeripheralOperationsPropertyEditorPanel.java new file mode 100644 index 0000000..869b975 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/PeripheralOperationsPropertyEditorPanel.java @@ -0,0 +1,422 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.awt.Dimension; +import java.util.ArrayList; +import java.util.List; +import javax.swing.JDialog; +import javax.swing.JPanel; +import javax.swing.table.AbstractTableModel; +import org.opentcs.guing.base.components.properties.type.PeripheralOperationsProperty; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.base.model.PeripheralOperationModel; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; +import org.opentcs.guing.common.components.dialogs.StandardDetailsDialog; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * User interface to edit a peripheral operations property. + */ +public class PeripheralOperationsPropertyEditorPanel + extends + JPanel + implements + DetailsDialogContent { + + /** + * The bundle to be used. + */ + private final ResourceBundleUtil bundle + = ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH); + /** + * Manager of the system model. + */ + private final ModelManager modelManager; + /** + * The property to edit. + */ + private PeripheralOperationsProperty fProperty; + + /** + * Creates a new instance. + * + * @param modelManager Manages the system model. + */ + @Inject + @SuppressWarnings("this-escape") + public PeripheralOperationsPropertyEditorPanel(ModelManager modelManager) { + this.modelManager = requireNonNull(modelManager, "modelManager"); + + initComponents(); + + setPreferredSize(new Dimension(600, 250)); + + itemsTable.setModel(new ItemsTableModel()); + } + + @Override + public void setProperty(Property property) { + fProperty = (PeripheralOperationsProperty) property; + ((ItemsTableModel) itemsTable.getModel()).setValues(fProperty.getValue()); + } + + @Override + public void updateValues() { + fProperty.setValue(((ItemsTableModel) itemsTable.getModel()).getValues()); + } + + @Override + public Property getProperty() { + return fProperty; + } + + @Override + public String getTitle() { + return bundle.getString("peripheralOperationsPropertyEditorPanel.title"); + } + + /** + * Edits the selected value. + */ + protected void edit() { + int selectedRow = itemsTable.getSelectedRow(); + if (selectedRow == -1) { + return; + } + PeripheralOperationModel selectedModel = ((ItemsTableModel) itemsTable.getModel()) + .getValues().get(selectedRow); + + JDialog parent = (JDialog) getTopLevelAncestor(); + PeripheralOperationPanel content = new PeripheralOperationPanel(modelManager.getModel()); + content.setPeripheralOpartionModel(selectedModel); + StandardDetailsDialog dialog = new StandardDetailsDialog(parent, true, content); + dialog.setLocationRelativeTo(parent); + dialog.setVisible(true); + + if (dialog.getReturnStatus() == StandardDetailsDialog.RET_OK + && content.getPeripheralOperationModel().isPresent()) { + ((ItemsTableModel) itemsTable.getModel()) + .getValues().set(selectedRow, content.getPeripheralOperationModel().get()); + ((ItemsTableModel) itemsTable.getModel()).fireTableRowsUpdated(selectedRow, selectedRow); + } + } + + /** + * Adds a new entry. + */ + protected void add() { + JDialog parent = (JDialog) getTopLevelAncestor(); + PeripheralOperationPanel content = new PeripheralOperationPanel(modelManager.getModel()); + StandardDetailsDialog dialog = new StandardDetailsDialog(parent, true, content); + dialog.setLocationRelativeTo(parent); + dialog.setVisible(true); + + if (dialog.getReturnStatus() == StandardDetailsDialog.RET_OK + && content.getPeripheralOperationModel().isPresent()) { + ItemsTableModel model = (ItemsTableModel) itemsTable.getModel(); + List<PeripheralOperationModel> values = model.getValues(); + values.add(content.getPeripheralOperationModel().get()); + model.setValues(values); + model.fireTableRowsInserted(values.size() - 1, values.size() - 1); + } + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + itemsScrollPane = new javax.swing.JScrollPane(); + itemsTable = new javax.swing.JTable(); + controlPanel = new javax.swing.JPanel(); + addButton = new javax.swing.JButton(); + editButton = new javax.swing.JButton(); + removeButton = new javax.swing.JButton(); + rigidArea = new javax.swing.JPanel(); + moveUpButton = new javax.swing.JButton(); + moveDownButton = new javax.swing.JButton(); + + setLayout(new java.awt.BorderLayout()); + + itemsTable.setModel(new javax.swing.table.DefaultTableModel( + new Object [][] { + {}, + {}, + {}, + {} + }, + new String [] { + + } + )); + itemsScrollPane.setViewportView(itemsTable); + + add(itemsScrollPane, java.awt.BorderLayout.CENTER); + + controlPanel.setLayout(new java.awt.GridBagLayout()); + + addButton.setFont(addButton.getFont()); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/panels/propertyEditing"); // NOI18N + addButton.setText(bundle.getString("peripheralOperationsPropertyEditorPanel.button_add.text")); // NOI18N + addButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 15, 0, 0); + controlPanel.add(addButton, gridBagConstraints); + + editButton.setFont(editButton.getFont()); + editButton.setText(bundle.getString("peripheralOperationsPropertyEditorPanel.button_edit.text")); // NOI18N + editButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + editButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(10, 15, 10, 0); + controlPanel.add(editButton, gridBagConstraints); + + removeButton.setFont(removeButton.getFont()); + removeButton.setText(bundle.getString("peripheralOperationsPropertyEditorPanel.button_remove.text")); // NOI18N + removeButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + removeButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 15, 0, 0); + controlPanel.add(removeButton, gridBagConstraints); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 5; + gridBagConstraints.weighty = 1.0; + controlPanel.add(rigidArea, gridBagConstraints); + + moveUpButton.setFont(moveUpButton.getFont()); + moveUpButton.setText(bundle.getString("peripheralOperationsPropertyEditorPanel.button_up.text")); // NOI18N + moveUpButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + moveUpButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(10, 15, 10, 0); + controlPanel.add(moveUpButton, gridBagConstraints); + + moveDownButton.setFont(moveDownButton.getFont()); + moveDownButton.setText(bundle.getString("peripheralOperationsPropertyEditorPanell.button_down.text")); // NOI18N + moveDownButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + moveDownButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 15, 0, 0); + controlPanel.add(moveDownButton, gridBagConstraints); + + add(controlPanel, java.awt.BorderLayout.EAST); + }// </editor-fold>//GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void moveDownButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_moveDownButtonActionPerformed + int selectedRow = itemsTable.getSelectedRow(); + if (selectedRow == -1 || selectedRow == itemsTable.getModel().getRowCount() - 1) { + return; + } + + ItemsTableModel model = (ItemsTableModel) itemsTable.getModel(); + List<PeripheralOperationModel> values = model.getValues(); + PeripheralOperationModel value = values.get(selectedRow); + values.remove(selectedRow); + values.add(selectedRow + 1, value); + model.setValues(values); + model.fireTableRowsUpdated(selectedRow, selectedRow + 1); + itemsTable.setRowSelectionInterval(selectedRow + 1, selectedRow + 1); + }//GEN-LAST:event_moveDownButtonActionPerformed + + private void moveUpButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_moveUpButtonActionPerformed + int selectedRow = itemsTable.getSelectedRow(); + if (selectedRow == -1 || selectedRow == 0) { + return; + } + + ItemsTableModel model = (ItemsTableModel) itemsTable.getModel(); + List<PeripheralOperationModel> values = model.getValues(); + PeripheralOperationModel value = values.get(selectedRow); + values.remove(selectedRow); + values.add(selectedRow - 1, value); + model.setValues(values); + model.fireTableRowsUpdated(selectedRow - 1, selectedRow); + itemsTable.setRowSelectionInterval(selectedRow - 1, selectedRow - 1); + }//GEN-LAST:event_moveUpButtonActionPerformed + + private void removeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removeButtonActionPerformed + int selectedRow = itemsTable.getSelectedRow(); + if (selectedRow == -1) { + return; + } + + ItemsTableModel model = (ItemsTableModel) itemsTable.getModel(); + List<PeripheralOperationModel> values = model.getValues(); + values.remove(selectedRow); + model.setValues(values); + model.fireTableRowsDeleted(selectedRow, selectedRow); + }//GEN-LAST:event_removeButtonActionPerformed + + private void editButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_editButtonActionPerformed + edit(); + }//GEN-LAST:event_editButtonActionPerformed + + private void addButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addButtonActionPerformed + add(); + }//GEN-LAST:event_addButtonActionPerformed + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton addButton; + private javax.swing.JPanel controlPanel; + private javax.swing.JButton editButton; + private javax.swing.JScrollPane itemsScrollPane; + private javax.swing.JTable itemsTable; + private javax.swing.JButton moveDownButton; + private javax.swing.JButton moveUpButton; + private javax.swing.JButton removeButton; + private javax.swing.JPanel rigidArea; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON + + private class ItemsTableModel + extends + AbstractTableModel { + + /** + * Column classes. + */ + private final Class<?>[] columnClasses + = new Class<?>[]{ + String.class, + String.class, + String.class, + Boolean.class + }; + /** + * The column names. + */ + @SuppressWarnings("checkstyle:LineLength") + private final String[] columnNames + = new String[]{ + bundle.getString( + "peripheralOperationsPropertyEditorPanel.table_resources.column_location.headerText" + ), + bundle.getString( + "peripheralOperationsPropertyEditorPanel.table_resources.column_operation.headerText" + ), + bundle.getString( + "peripheralOperationsPropertyEditorPanel.table_resources.column_trigger.headerText" + ), + bundle.getString( + "peripheralOperationsPropertyEditorPanel.table_resources.column_completion.headerText" + ) + }; + + private final int columnLocation = 0; + private final int columnOperation = 1; + private final int columnTrigger = 2; + private final int columnCompletionRequired = 3; + + /** + * Values in this model. + */ + private List<PeripheralOperationModel> values = new ArrayList<>(); + + /** + * Creates a new instance. + */ + ItemsTableModel() { + } + + @Override + public Class<?> getColumnClass(int columnIndex) { + return columnClasses[columnIndex]; + } + + @Override + public String getColumnName(int columnIndex) { + return columnNames[columnIndex]; + } + + @Override + public boolean isCellEditable(int row, int column) { + return false; + } + + @Override + public int getRowCount() { + return values.size(); + } + + @Override + public int getColumnCount() { + return columnNames.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= getRowCount()) { + return null; + } + PeripheralOperationModel entry = values.get(rowIndex); + switch (columnIndex) { + case columnLocation: + return entry.getLocationName(); + case columnOperation: + return entry.getOperation(); + case columnTrigger: + return entry.getExecutionTrigger().name(); + case columnCompletionRequired: + return entry.isCompletionRequired(); + default: + throw new IllegalArgumentException("Invalid column index: " + columnIndex); + } + } + + public void setValues(List<PeripheralOperationModel> values) { + this.values = values; + } + + public List<PeripheralOperationModel> getValues() { + return this.values; + } + + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/PropertiesPanelFactory.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/PropertiesPanelFactory.java new file mode 100644 index 0000000..2efb6ee --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/PropertiesPanelFactory.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import javax.swing.JPanel; + +/** + * A factory for properties-related panels. + */ +public interface PropertiesPanelFactory { + + /** + * Creates a new <code>PropertiesTableContent</code>. + * + * @param dialogParent The component to be used as the parent for dialogs + * created by the new instance. + * @return The newly created instance. + */ + PropertiesTableContent createPropertiesTableContent(JPanel dialogParent); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/PropertiesTableContent.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/PropertiesTableContent.java new file mode 100644 index 0000000..fc6e9a3 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/PropertiesTableContent.java @@ -0,0 +1,423 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import java.util.HashMap; +import java.util.Map; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JFormattedTextField; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.ListCellRenderer; +import javax.swing.event.CellEditorListener; +import javax.swing.event.ChangeEvent; +import javax.swing.table.DefaultTableModel; +import javax.swing.table.TableModel; +import javax.swing.undo.CannotUndoException; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.guing.base.components.properties.event.AttributesChangeEvent; +import org.opentcs.guing.base.components.properties.event.AttributesChangeListener; +import org.opentcs.guing.base.components.properties.type.AbstractComplexProperty; +import org.opentcs.guing.base.components.properties.type.AbstractQuantity; +import org.opentcs.guing.base.components.properties.type.BlockTypeProperty; +import org.opentcs.guing.base.components.properties.type.BooleanProperty; +import org.opentcs.guing.base.components.properties.type.ColorProperty; +import org.opentcs.guing.base.components.properties.type.CoordinateProperty; +import org.opentcs.guing.base.components.properties.type.IntegerProperty; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.components.properties.type.LinerTypeProperty; +import org.opentcs.guing.base.components.properties.type.LocationTypeProperty; +import org.opentcs.guing.base.components.properties.type.PointTypeProperty; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.base.components.properties.type.SelectionProperty; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.base.event.ConnectionChangeEvent; +import org.opentcs.guing.base.event.ConnectionChangeListener; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.PropertiesCollection; +import org.opentcs.guing.base.model.elements.AbstractConnection; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.common.components.dialogs.DetailsDialog; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; +import org.opentcs.guing.common.components.dialogs.StandardDetailsDialog; +import org.opentcs.guing.common.components.properties.AbstractTableContent; +import org.opentcs.guing.common.components.properties.PropertiesComponentsFactory; +import org.opentcs.guing.common.components.properties.table.AttributesTable; +import org.opentcs.guing.common.components.properties.table.AttributesTableModel; +import org.opentcs.guing.common.components.properties.table.BlockTypePropertyCellRenderer; +import org.opentcs.guing.common.components.properties.table.BooleanPropertyCellEditor; +import org.opentcs.guing.common.components.properties.table.BooleanPropertyCellRenderer; +import org.opentcs.guing.common.components.properties.table.CellEditorFactory; +import org.opentcs.guing.common.components.properties.table.ColorPropertyCellEditor; +import org.opentcs.guing.common.components.properties.table.ColorPropertyCellRenderer; +import org.opentcs.guing.common.components.properties.table.CoordinateCellEditor; +import org.opentcs.guing.common.components.properties.table.IntegerPropertyCellEditor; +import org.opentcs.guing.common.components.properties.table.LinerTypePropertyCellRenderer; +import org.opentcs.guing.common.components.properties.table.PointTypePropertyCellRenderer; +import org.opentcs.guing.common.components.properties.table.QuantityCellEditor; +import org.opentcs.guing.common.components.properties.table.SelectionPropertyCellEditor; +import org.opentcs.guing.common.components.properties.table.StandardPropertyCellRenderer; +import org.opentcs.guing.common.components.properties.table.StringPropertyCellEditor; +import org.opentcs.guing.common.components.properties.table.UndoableCellEditor; +import org.opentcs.guing.common.event.ResetInteractionToolCommand; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.guing.common.util.UserMessageHelper; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.gui.StringListCellRenderer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Shows the attributes of a model component in a table. + */ +public class PropertiesTableContent + extends + AbstractTableContent + implements + AttributesChangeListener, + ConnectionChangeListener, + CellEditorListener { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PropertiesTableContent.class); + /** + * A factory for cell editors. + */ + private final CellEditorFactory cellEditorFactory; + /** + * Provides attribute table models. + */ + private final Provider<AttributesTableModel> tableModelProvider; + /** + * The application's event bus. + */ + private final EventBus eventBus; + /** + * A parent for dialogs created by this instance. + */ + private final JPanel dialogParent; + /** + * The components factory. + */ + private final PropertiesComponentsFactory componentsFactory; + + /** + * Creates a new instance. + * + * @param cellEditorFactory A factory for cell editors. + * @param tableProvider Provides attribute tables. + * @param tableModelProvider Provides attribute table models. + * @param eventBus The application's event bus. + * @param dialogParent A parent for dialogs created by this instance. + * @param componentsFactory The components factory. + */ + @Inject + public PropertiesTableContent( + CellEditorFactory cellEditorFactory, + Provider<AttributesTable> tableProvider, + Provider<AttributesTableModel> tableModelProvider, + @ApplicationEventBus + EventBus eventBus, + @Assisted + JPanel dialogParent, + PropertiesComponentsFactory componentsFactory + ) { + super(tableProvider); + this.cellEditorFactory = requireNonNull( + cellEditorFactory, + "cellEditorFactory" + ); + this.tableModelProvider = requireNonNull( + tableModelProvider, + "tableModelProvider" + ); + this.eventBus = requireNonNull(eventBus, "eventBus"); + this.dialogParent = requireNonNull(dialogParent, "dialogParent"); + this.componentsFactory = requireNonNull(componentsFactory, "componentsFactory"); + } + + @Override // AbstractTableContent + public void tableModelChanged() { + if (!fEvaluateTableChanges) { + return; + } + + if (fModel == null) { + return; + } + + if (fModel instanceof PropertiesCollection) { + fModel.propertiesChanged(this); + eventBus.onEvent(new ResetInteractionToolCommand(this)); + // updates some values required by PropertiesCollection + setModel(fModel); + } + else { + if (fModel.getName().isEmpty()) { + try { + if (fUndoRedoManager.canUndo()) { + fUndoRedoManager.undo(); + } + } + catch (CannotUndoException ex) { + LOG.warn("Exception trying to undo", ex); + } + } + fModel.propertiesChanged(this); + eventBus.onEvent(new ResetInteractionToolCommand(this)); + } + } + + @Override // AbstractAttributesContent + public void setModel(ModelComponent model) { + if (fModel != null) { + fModel.removeAttributesChangeListener(this); + + if (fModel instanceof AbstractConnection) { + ((AbstractConnection) fModel).removeConnectionChangeListener(this); + } + } + + fModel = model; + fModel.addAttributesChangeListener(this); + + if (fModel instanceof AbstractConnection) { + ((AbstractConnection) fModel).addConnectionChangeListener(this); + } + + setTableContent(fModel.getProperties()); + } + + @Override // AbstractAttributesContent + public String getDescription() { + if (fModel != null) { + return fModel.getDescription(); + } + + return ""; + } + + @Override // AbstractAttributesContent + public void reset() { + if (fModel != null) { + fModel.removeAttributesChangeListener(this); + + if (fModel instanceof AbstractConnection) { + ((AbstractConnection) fModel).removeConnectionChangeListener(this); + } + } + + fModel = null; + setTableContent(new HashMap<String, Property>()); + } + + @Override // AttributesChangeListener + public void propertiesChanged(AttributesChangeEvent e) { + fEvaluateTableChanges = false; + ((DefaultTableModel) fTable.getModel()).fireTableDataChanged(); + fEvaluateTableChanges = true; + } + + @Override // ConnectionChangeListener + public void connectionChanged(ConnectionChangeEvent e) { + setTableContent(fModel.getProperties()); + } + + @Override + public void editingStopped(ChangeEvent e) { + + } + + @Override + public void editingCanceled(ChangeEvent e) { + + } + + @Override // AbstractTableContent + protected void setTableCellRenderers() { + fTable.setDefaultRenderer(Object.class, new StandardPropertyCellRenderer()); + fTable.setDefaultRenderer(BooleanProperty.class, new BooleanPropertyCellRenderer()); + fTable.setDefaultRenderer(ColorProperty.class, new ColorPropertyCellRenderer()); + fTable.setDefaultRenderer(BlockTypeProperty.class, new BlockTypePropertyCellRenderer()); + fTable.setDefaultRenderer(LinerTypeProperty.class, new LinerTypePropertyCellRenderer()); + fTable.setDefaultRenderer(PointTypeProperty.class, new PointTypePropertyCellRenderer()); + } + + @Override // AbstractTableContent + protected void setTableCellEditors() { + // A dialog for entering values of complex properties + DetailsDialog dialog; + // The content panel of this dialog + DetailsDialogContent content; + // An editor allowing undo/redo + UndoableCellEditor undoableEditor; + UserMessageHelper umh = new UserMessageHelper(); + + ListCellRenderer<Object> defaultRenderer = new StringListCellRenderer<>(this::objectToString); + + // String properties: Name etc. + content = new StringPropertyEditorPanel(); + dialog = new StandardDetailsDialog(dialogParent, true, content); + undoableEditor = new UndoableCellEditor(new StringPropertyCellEditor(new JTextField(), umh)); + undoableEditor.setDetailsDialog(dialog); + undoableEditor.setUndoManager(fUndoRedoManager); + fCellEditors.add(undoableEditor); + fTable.setDefaultEditor(StringProperty.class, undoableEditor); + + // Abstract Quantity: Angle, Percent, Speed + content = new QuantityEditorPanel(); + dialog = new StandardDetailsDialog(dialogParent, true, content); + QuantityCellEditor wrappedQuantityCellEditor = new QuantityCellEditor(new JTextField(), umh); + wrappedQuantityCellEditor.addCellEditorListener(this); + undoableEditor = new UndoableCellEditor(wrappedQuantityCellEditor); + undoableEditor.setDetailsDialog(dialog); + undoableEditor.setUndoManager(fUndoRedoManager); + fCellEditors.add(undoableEditor); + fTable.setDefaultEditor(AbstractQuantity.class, undoableEditor); + + // Length properties: Path length, Vehicle length, Layout scale x/y + content = new QuantityEditorPanel(); + dialog = new StandardDetailsDialog(dialogParent, true, content); + wrappedQuantityCellEditor = new QuantityCellEditor(new JTextField(), umh); + wrappedQuantityCellEditor.addCellEditorListener(this); + undoableEditor = new UndoableCellEditor(wrappedQuantityCellEditor); + undoableEditor.setDetailsDialog(dialog); + undoableEditor.setUndoManager(fUndoRedoManager); + fCellEditors.add(undoableEditor); + fTable.setDefaultEditor(LengthProperty.class, undoableEditor); + + // Coordinate properties: x/y-coordinates of Points, Locations + content = new QuantityEditorPanel(); + dialog = new StandardDetailsDialog(dialogParent, true, content); + // CoordinateCellEditor with buttons "copy model <-> layout" + CoordinateCellEditor wrappedCoordinateCellEditor + = componentsFactory.createCoordinateCellEditor(new JTextField(), umh); + wrappedCoordinateCellEditor.addCellEditorListener(this); + undoableEditor = new UndoableCellEditor(wrappedCoordinateCellEditor); + undoableEditor.setDetailsDialog(dialog); + undoableEditor.setUndoManager(fUndoRedoManager); + fCellEditors.add(undoableEditor); + fTable.setDefaultEditor(CoordinateProperty.class, undoableEditor); + + // Selection property + content = new SelectionPropertyEditorPanel(defaultRenderer); + dialog = new StandardDetailsDialog(dialogParent, true, content); + undoableEditor + = new UndoableCellEditor(new SelectionPropertyCellEditor(new JComboBox<>(), umh)); + undoableEditor.setDetailsDialog(dialog); + undoableEditor.setUndoManager(fUndoRedoManager); + fCellEditors.add(undoableEditor); + fTable.setDefaultEditor(SelectionProperty.class, undoableEditor); + + // Point type property + ListCellRenderer<PointModel.Type> pointTypeRenderer + = new StringListCellRenderer<>(type -> type.getDescription()); + content = new SelectionPropertyEditorPanel(pointTypeRenderer); + dialog = new StandardDetailsDialog(dialogParent, true, content); + JComboBox<PointModel.Type> pointTypeComboBox = new JComboBox<>(); + pointTypeComboBox.setRenderer(pointTypeRenderer); + undoableEditor + = new UndoableCellEditor(new SelectionPropertyCellEditor(pointTypeComboBox, umh)); + undoableEditor.setDetailsDialog(dialog); + undoableEditor.setUndoManager(fUndoRedoManager); + fCellEditors.add(undoableEditor); + fTable.setDefaultEditor(PointTypeProperty.class, undoableEditor); + + // Liner type property + ListCellRenderer<PathModel.Type> linerTypeRenderer + = new StringListCellRenderer<>(type -> type.getDescription()); + content = new SelectionPropertyEditorPanel(linerTypeRenderer); + dialog = new StandardDetailsDialog(dialogParent, true, content); + JComboBox<PathModel.Type> linerTypeComboBox = new JComboBox<>(); + linerTypeComboBox.setRenderer(linerTypeRenderer); + undoableEditor + = new UndoableCellEditor(new SelectionPropertyCellEditor(linerTypeComboBox, umh)); + undoableEditor.setDetailsDialog(dialog); + undoableEditor.setUndoManager(fUndoRedoManager); + fCellEditors.add(undoableEditor); + fTable.setDefaultEditor(LinerTypeProperty.class, undoableEditor); + + // Block type property + ListCellRenderer<BlockModel.Type> blockTypeRenderer + = new StringListCellRenderer<>(type -> type.getDescription()); + content = new SelectionPropertyEditorPanel(blockTypeRenderer); + dialog = new StandardDetailsDialog(dialogParent, true, content); + JComboBox<BlockModel.Type> blockTypeComboBox = new JComboBox<>(); + blockTypeComboBox.setRenderer(blockTypeRenderer); + undoableEditor + = new UndoableCellEditor(new SelectionPropertyCellEditor(blockTypeComboBox, umh)); + undoableEditor.setDetailsDialog(dialog); + undoableEditor.setUndoManager(fUndoRedoManager); + fCellEditors.add(undoableEditor); + fTable.setDefaultEditor(BlockTypeProperty.class, undoableEditor); + + // Location type property + content = new SelectionPropertyEditorPanel(defaultRenderer); + dialog = new StandardDetailsDialog(dialogParent, true, content); + undoableEditor + = new UndoableCellEditor(new SelectionPropertyCellEditor(new JComboBox<>(), umh)); + undoableEditor.setDetailsDialog(dialog); + undoableEditor.setUndoManager(fUndoRedoManager); + fCellEditors.add(undoableEditor); + fTable.setDefaultEditor(LocationTypeProperty.class, undoableEditor); + + // Boolean property: Path locked etc. + undoableEditor + = new UndoableCellEditor(new BooleanPropertyCellEditor(new JCheckBox(), umh)); + undoableEditor.setUndoManager(fUndoRedoManager); + fCellEditors.add(undoableEditor); + fTable.setDefaultEditor(BooleanProperty.class, undoableEditor); + + // Abstract complex property: + undoableEditor = new UndoableCellEditor( + cellEditorFactory.createComplexPropertyCellEditor(dialogParent) + ); + undoableEditor.setUndoManager(fUndoRedoManager); + fCellEditors.add(undoableEditor); + fTable.setDefaultEditor(AbstractComplexProperty.class, undoableEditor); + + // Integer property: + IntegerPropertyCellEditor integerPropertyCellEditor + = new IntegerPropertyCellEditor(new JFormattedTextField(), umh); + undoableEditor = new UndoableCellEditor(integerPropertyCellEditor); + undoableEditor.setUndoManager(fUndoRedoManager); + fCellEditors.add(undoableEditor); + fTable.setDefaultEditor(IntegerProperty.class, undoableEditor); + + // Color property: + undoableEditor = new UndoableCellEditor(new ColorPropertyCellEditor()); + undoableEditor.setUndoManager(fUndoRedoManager); + fCellEditors.add(undoableEditor); + fTable.setDefaultEditor(ColorProperty.class, undoableEditor); + } + + @Override // AbstractTableContent + protected TableModel createTableModel(Map<String, Property> content) { + AttributesTableModel model = tableModelProvider.get(); + ResourceBundleUtil r = ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH); + String attributeColumn = r.getString("propertiesTableContent.column_attribute.headerText"); + String valueColumn = r.getString("propertiesTableContent.column_value.headerText"); + model.setColumnIdentifiers(new Object[]{attributeColumn, valueColumn}); + + for (Property property : content.values()) { + model.addRow(new Object[]{property.getDescription(), property}); + } + + return model; + } + + private String objectToString(Object o) { + return o == null ? "" : o.toString(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/QuantityEditorPanel.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/QuantityEditorPanel.form new file mode 100644 index 0000000..7bf06f3 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/QuantityEditorPanel.form @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.8" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,0,33,0,0,0,-63"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Component class="javax.swing.JTextField" name="numberTextField"> + <Properties> + <Property name="columns" type="int" value="10"/> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="numberTextField" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="0" gridWidth="1" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="3" insetsLeft="3" insetsBottom="3" insetsRight="3" anchor="23" weightX="0.5" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JComboBox" name="unitComboBox"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="unitComboBox" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="<Object>"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="1" gridY="0" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="3" insetsLeft="3" insetsBottom="3" insetsRight="3" anchor="23" weightX="0.5" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> +</Form> diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/QuantityEditorPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/QuantityEditorPanel.java new file mode 100644 index 0000000..b947656 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/QuantityEditorPanel.java @@ -0,0 +1,143 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import javax.swing.DefaultComboBoxModel; +import javax.swing.JPanel; +import org.opentcs.guing.base.components.properties.type.AbstractQuantity; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A panel that can edit a quantity property. + */ +public class QuantityEditorPanel + extends + JPanel + implements + DetailsDialogContent { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(QuantityEditorPanel.class); + /** + * The property that is being edited. + */ + private AbstractQuantity<?> fProperty; + + /** + * Creates new form QuantityEditorPanel + */ + @SuppressWarnings("this-escape") + public QuantityEditorPanel() { + initComponents(); + } + + @Override + public String getTitle() { + return ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH) + .getString("quantityEditorPanel.title"); + } + + /** + * Initialises the dialog elements. + */ + public void initFields() { + unitComboBox.setSelectedItem(fProperty.getUnit()); + + String value; + + if (fProperty.isInteger()) { + value = Integer.toString((int) fProperty.getValue()); + } + else if (fProperty.getValue() instanceof Double) { + value = Double.toString((double) fProperty.getValue()); + } + else { + value = fProperty.getValue().toString(); + } + + numberTextField.setText(value); + } + + @Override + public void updateValues() { + try { + double value = Double.parseDouble(numberTextField.getText()); + String unit = unitComboBox.getSelectedItem().toString(); + fProperty.setValueAndUnit(value, unit); + } + catch (NumberFormatException nfe) { + // Don't parse String "<different values>" + } + catch (IllegalArgumentException e) { + LOG.error("Exception", e); + } + } + + @Override + public void setProperty(Property property) { + fProperty = (AbstractQuantity<?>) property; + unitComboBox.setModel(new DefaultComboBoxModel<>(fProperty.getPossibleUnits().toArray())); + initFields(); + } + + @Override + public AbstractQuantity<?> getProperty() { + return fProperty; + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + numberTextField = new javax.swing.JTextField(); + unitComboBox = new javax.swing.JComboBox<>(); + + setLayout(new java.awt.GridBagLayout()); + + numberTextField.setColumns(10); + numberTextField.setFont(numberTextField.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.FIRST_LINE_START; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(numberTextField, gridBagConstraints); + + unitComboBox.setFont(unitComboBox.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.FIRST_LINE_START; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(unitComboBox, gridBagConstraints); + }// </editor-fold>//GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JTextField numberTextField; + private javax.swing.JComboBox<Object> unitComboBox; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/ResourcePropertyViewerEditorPanel.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/ResourcePropertyViewerEditorPanel.form new file mode 100644 index 0000000..3e4e81a --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/ResourcePropertyViewerEditorPanel.form @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,44,0,0,1,-112"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/> + <SubComponents> + <Container class="javax.swing.JScrollPane" name="itemsScrollPane"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription"> + <BorderConstraints direction="Center"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JTable" name="itemsTable"> + <Properties> + <Property name="model" type="javax.swing.table.TableModel" editor="org.netbeans.modules.form.editors2.TableModelEditor"> + <Table columnCount="0" rowCount="0"/> + </Property> + </Properties> + </Component> + </SubComponents> + </Container> + <Container class="javax.swing.JPanel" name="controlPanel"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription"> + <BorderConstraints direction="East"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + </Container> + </SubComponents> +</Form> diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/ResourcePropertyViewerEditorPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/ResourcePropertyViewerEditorPanel.java new file mode 100644 index 0000000..c21caf9 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/ResourcePropertyViewerEditorPanel.java @@ -0,0 +1,230 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import com.google.inject.Inject; +import java.awt.Dimension; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import javax.swing.JPanel; +import javax.swing.table.AbstractTableModel; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.base.components.properties.type.ResourceProperty; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.util.gui.StringTableCellRenderer; + +/** + * UI for viewing a resource property without being able to edit the property. + */ +public class ResourcePropertyViewerEditorPanel + extends + JPanel + implements + DetailsDialogContent { + + /** + * A resource bundle. + */ + private static final ResourceBundleUtil BUNDLE + = ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH); + /** + * The property edited. + */ + private ResourceProperty fProperty; + + /** + * Creates a new instance. + */ + @Inject + @SuppressWarnings("this-escape") + public ResourcePropertyViewerEditorPanel() { + initComponents(); + + itemsTable.setModel(new ItemsTableModel()); + itemsTable.setDefaultRenderer( + TCSResourceReference.class, + new StringTableCellRenderer<TCSResourceReference<?>>( + ref -> ref == null ? "" : ref.getName() + ) + ); + + setPreferredSize(new Dimension(400, 200)); + } + + @Override + public void setProperty(Property property) { + fProperty = (ResourceProperty) property; + + ItemsTableModel model = (ItemsTableModel) itemsTable.getModel(); + + List<List<TCSResourceReference<?>>> values = new ArrayList<>(); + for (Set<TCSResourceReference<?>> set : fProperty.getItems()) { + List<TCSResourceReference<?>> entry = Arrays.asList(null, null, null); + set.forEach(resource -> { + if (resource.getReferentClass() == Path.class) { + entry.set(0, resource); + } + else if (resource.getReferentClass() == Point.class) { + entry.set(1, resource); + } + else if (resource.getReferentClass() == Location.class) { + entry.set(2, resource); + } + }); + values.add(entry); + } + model.setValues(values); + + } + + @Override + public void updateValues() { + } + + @Override + public String getTitle() { + return BUNDLE.getString("resourcePropertyViewerEditorPanel.title"); + } + + @Override + public ResourceProperty getProperty() { + return fProperty; + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + itemsScrollPane = new javax.swing.JScrollPane(); + itemsTable = new javax.swing.JTable(); + controlPanel = new javax.swing.JPanel(); + + setLayout(new java.awt.BorderLayout()); + + itemsTable.setModel(new javax.swing.table.DefaultTableModel( + new Object [][] { + + }, + new String [] { + + } + )); + itemsScrollPane.setViewportView(itemsTable); + + add(itemsScrollPane, java.awt.BorderLayout.CENTER); + + controlPanel.setLayout(new java.awt.GridBagLayout()); + add(controlPanel, java.awt.BorderLayout.EAST); + }// </editor-fold>//GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel controlPanel; + private javax.swing.JScrollPane itemsScrollPane; + private javax.swing.JTable itemsTable; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON + + private class ItemsTableModel + extends + AbstractTableModel { + + private final Class<?>[] columnClasses + = new Class<?>[]{ + TCSResourceReference.class, + TCSResourceReference.class, + TCSResourceReference.class + }; + + /** + * The column names. + */ + private final String[] columnNames + = new String[]{ + BUNDLE.getString( + "resourcePropertyViewerEditorPanel.table_resources.column_path.headerText" + ), + BUNDLE.getString( + "resourcePropertyViewerEditorPanel.table_resources.column_point.headerText" + ), + BUNDLE.getString( + "resourcePropertyViewerEditorPanel.table_resources.column_location.headerText" + ) + }; + + private final int columnPath = 0; + private final int columnPoint = 1; + private final int columnLocation = 2; + + private List<List<TCSResourceReference<?>>> values = new ArrayList<>(); + + ItemsTableModel() { + } + + @Override + public Class<?> getColumnClass(int columnIndex) { + return columnClasses[columnIndex]; + } + + @Override + public String getColumnName(int columnIndex) { + return columnNames[columnIndex]; + } + + @Override + public boolean isCellEditable(int row, int column) { + return false; + } + + @Override + public int getRowCount() { + return values.size(); + } + + @Override + public int getColumnCount() { + return columnNames.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= getRowCount()) { + return null; + } + List<TCSResourceReference<?>> entry = values.get(rowIndex); + switch (columnIndex) { + case columnPath: + return entry.get(columnPath); + case columnPoint: + return entry.get(columnPoint); + case columnLocation: + return entry.get(columnLocation); + default: + throw new IllegalArgumentException("Invalid column index: " + columnIndex); + } + } + + public void setValues(List<List<TCSResourceReference<?>>> values) { + this.values = values; + } + } + +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/SelectionPanel.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/SelectionPanel.form new file mode 100644 index 0000000..cb49bce --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/SelectionPanel.form @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,0,71,0,0,0,-70"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Component class="javax.swing.JLabel" name="label"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="label" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" value="Text:"/> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="-1" gridY="-1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="0" insetsRight="3" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JComboBox" name="comboBox"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="comboBox" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor"> + <StringArray count="4"> + <StringItem index="0" value="Item 1"/> + <StringItem index="1" value="Item 2"/> + <StringItem index="2" value="Item 3"/> + <StringItem index="3" value="Item 4"/> + </StringArray> + </Property> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="<Object>"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="-1" gridY="-1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> +</Form> diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/SelectionPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/SelectionPanel.java new file mode 100644 index 0000000..8ed4316 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/SelectionPanel.java @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import java.util.ArrayList; +import java.util.List; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JPanel; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; + +/** + * An user interface to select a value in a combobox. + */ +public class SelectionPanel + extends + JPanel + implements + DetailsDialogContent { + + /** + * The title of the dialog. + */ + private String fTitle; + + /** + * Creates new SelectionPanel. + */ + public SelectionPanel() { + this("Titel", "Label", new ArrayList<>()); + } + + /** + * Creates new form StringPanel. + * + * @param title The title. + * @param text The text of the label. + * @param items The selectable values. + */ + public SelectionPanel(String title, String text, List<?> items) { + this(title, text, items, null); + } + + /** + * Creates new form StringPanel. + * + * @param title The title. + * @param text The text of the label. + * @param items The selectable values. + * @param item The value that is initially selected. + */ + @SuppressWarnings("this-escape") + public SelectionPanel(String title, String text, List<?> items, Object item) { + initComponents(); + label.setText(text + ":"); + fTitle = title; + comboBox.setModel(new DefaultComboBoxModel<>(items.toArray())); + + if (item != null) { + comboBox.setSelectedItem(item); + } + } + + /** + * Returns the selected value. + * + * @return The selected value. + */ + public Object getValue() { + return comboBox.getSelectedItem(); + } + + /** + * Returns the selected index. + * + * @return The selected index. + */ + public int getIndex() { + return comboBox.getSelectedIndex(); + } + + + @Override + public String getTitle() { + return fTitle; + } + + @Override + public void setProperty(Property property) { + } + + @Override + public Property getProperty() { + return null; + } + + @Override + public void updateValues() { + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + label = new javax.swing.JLabel(); + comboBox = new javax.swing.JComboBox<>(); + + setLayout(new java.awt.GridBagLayout()); + + label.setFont(label.getFont()); + label.setText("Text:"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 3); + add(label, gridBagConstraints); + + comboBox.setFont(comboBox.getFont()); + comboBox.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "Item 1", "Item 2", "Item 3", "Item 4" })); + add(comboBox, new java.awt.GridBagConstraints()); + }// </editor-fold>//GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JComboBox<Object> comboBox; + private javax.swing.JLabel label; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/SelectionPropertyEditorPanel.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/SelectionPropertyEditorPanel.form new file mode 100644 index 0000000..8da2358 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/SelectionPropertyEditorPanel.form @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,0,46,0,0,0,-88"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Component class="javax.swing.JLabel" name="valueLabel"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="valueLabel" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="selectionPropertyEditorPanel.label_value.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="-1" gridY="-1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="4" insetsBottom="0" insetsRight="4" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JComboBox" name="valueComboBox"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="valueComboBox" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="<Object>"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="-1" gridY="-1" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="4" insetsLeft="0" insetsBottom="4" insetsRight="4" anchor="10" weightX="0.5" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> +</Form> diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/SelectionPropertyEditorPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/SelectionPropertyEditorPanel.java new file mode 100644 index 0000000..3627371 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/SelectionPropertyEditorPanel.java @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import static java.util.Objects.requireNonNull; + +import javax.swing.ComboBoxModel; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JPanel; +import javax.swing.ListCellRenderer; +import org.opentcs.guing.base.components.properties.type.AbstractProperty; +import org.opentcs.guing.base.components.properties.type.ModelAttribute; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.base.components.properties.type.Selectable; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Panel for selecting a property from a combo box. + */ +public class SelectionPropertyEditorPanel + extends + JPanel + implements + DetailsDialogContent { + + private final ListCellRenderer<Object> listCellRenderer; + /** + * Das Attribut. + */ + private AbstractProperty fProperty; + + /** + * Creates new form SelectionPropertyEditorPanel + */ + @SuppressWarnings({"unchecked", "this-escape"}) + public SelectionPropertyEditorPanel(ListCellRenderer<?> listCellRenderer) { + requireNonNull(listCellRenderer, "listCellRenderer"); + + this.listCellRenderer = (ListCellRenderer<Object>) listCellRenderer; + initComponents(); + } + + @Override // DetailsDialogContent + public void setProperty(Property property) { + fProperty = (AbstractProperty) property; + + @SuppressWarnings("unchecked") + ComboBoxModel<Object> model = new DefaultComboBoxModel<>( + ((Selectable<Object>) fProperty).getPossibleValues().toArray() + ); + valueComboBox.setModel(model); + + Object value = fProperty.getValue(); + valueComboBox.setSelectedItem(value); + valueComboBox.setRenderer(listCellRenderer); + } + + @Override // DetailsDialogContent + public void updateValues() { + Object selectedItem = valueComboBox.getSelectedItem(); + fProperty.setValue(selectedItem); + fProperty.setChangeState(ModelAttribute.ChangeState.DETAIL_CHANGED); + } + + @Override // DetailsDialogContent + public String getTitle() { + return ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH) + .getString("selectionPropertyEditorPanel.title"); + } + + @Override // DetailsDialogContent + public Property getProperty() { + return fProperty; + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + valueLabel = new javax.swing.JLabel(); + valueComboBox = new javax.swing.JComboBox<>(); + + setLayout(new java.awt.GridBagLayout()); + + valueLabel.setFont(valueLabel.getFont()); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/panels/propertyEditing"); // NOI18N + valueLabel.setText(bundle.getString("selectionPropertyEditorPanel.label_value.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 4); + add(valueLabel, gridBagConstraints); + + valueComboBox.setFont(valueComboBox.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.insets = new java.awt.Insets(4, 0, 4, 4); + add(valueComboBox, gridBagConstraints); + }// </editor-fold>//GEN-END:initComponents + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JComboBox<Object> valueComboBox; + private javax.swing.JLabel valueLabel; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/StringPanel.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/StringPanel.form new file mode 100644 index 0000000..8b75d38 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/StringPanel.form @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,0,50,0,0,0,-86"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Component class="javax.swing.JLabel" name="label"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="label" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" value="Text:"/> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="-1" gridY="-1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JTextField" name="textField"> + <Properties> + <Property name="columns" type="int" value="10"/> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="textField" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="-1" gridY="-1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="3" insetsLeft="3" insetsBottom="3" insetsRight="3" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> +</Form> diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/StringPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/StringPanel.java new file mode 100644 index 0000000..7c34197 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/StringPanel.java @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import javax.swing.JPanel; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; + +/** + * User interface for a single line text. + */ +public class StringPanel + extends + JPanel + implements + DetailsDialogContent { + + /** + * The title of the dialog. + */ + private String fTitle; + + /** + * Creates new form StringPanel. + */ + public StringPanel() { + this("String-Editor", "Text", ""); + } + + /** + * Creates new form StringPanel. + * + * @param title The title. + * @param labelText The text of the label. + * @param text The text. + */ + @SuppressWarnings("this-escape") + public StringPanel(String title, String labelText, String text) { + initComponents(); + label.setText(labelText + ":"); + textField.setText(text); + fTitle = title; + } + + /** + * Returns the text. + * + * @return The text. + */ + public String getText() { + return textField.getText(); + } + + @Override + public void updateValues() { + } + + @Override + public String getTitle() { + return fTitle; + } + + @Override + public void setProperty(Property property) { + } + + @Override + public Property getProperty() { + return null; + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + label = new javax.swing.JLabel(); + textField = new javax.swing.JTextField(); + + setLayout(new java.awt.GridBagLayout()); + + label.setFont(label.getFont()); + label.setText("Text:"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + add(label, gridBagConstraints); + + textField.setColumns(10); + textField.setFont(textField.getFont()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3); + add(textField, gridBagConstraints); + }// </editor-fold>//GEN-END:initComponents + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel label; + private javax.swing.JTextField textField; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/StringPropertyEditorPanel.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/StringPropertyEditorPanel.form new file mode 100644 index 0000000..0d75ca3 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/StringPropertyEditorPanel.form @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.2" maxVersion="1.2" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,44,0,0,1,-112"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/> + <SubComponents> + <Component class="javax.swing.JTextArea" name="textArea"> + <Properties> + <Property name="columns" type="int" value="20"/> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Monospaced" size="12" style="0"/> + </Property> + <Property name="lineWrap" type="boolean" value="true"/> + <Property name="rows" type="int" value="15"/> + <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> + <Border info="org.netbeans.modules.form.compat2.border.LineBorderInfo"> + <LineBorder/> + </Border> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription"> + <BorderConstraints direction="Center"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> +</Form> diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/StringPropertyEditorPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/StringPropertyEditorPanel.java new file mode 100644 index 0000000..8f8eb6d --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/StringPropertyEditorPanel.java @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import javax.swing.JPanel; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * A panel that can edit a string property. + */ +public class StringPropertyEditorPanel + extends + JPanel + implements + DetailsDialogContent { + + /** + * The property that is being edited. + */ + private StringProperty fProperty; + + /** + * Creates new form StringPropertyPanel + */ + @SuppressWarnings("this-escape") + public StringPropertyEditorPanel() { + initComponents(); + } + + @Override // DetailsDialogContent + public String getTitle() { + return ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH) + .getString("stringPropertyEditorPanel.title"); + } + + /** + * Initialises the dialog elements. + */ + public void initFields() { + textArea.setText(fProperty.getText()); + textArea.setCaretPosition(fProperty.getText().length()); + } + + @Override // DetailsDialogContent + public void updateValues() { + fProperty.setText(textArea.getText()); + } + + @Override // DetailsDialogContent + public void setProperty(Property property) { + fProperty = (StringProperty) property; + initFields(); + } + + @Override // DetailsDialogContent + public Property getProperty() { + return fProperty; + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + textArea = new javax.swing.JTextArea(); + + setLayout(new java.awt.BorderLayout()); + + textArea.setColumns(20); + textArea.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N + textArea.setLineWrap(true); + textArea.setRows(15); + textArea.setBorder(javax.swing.BorderFactory.createLineBorder(new java.awt.Color(0, 0, 0))); + add(textArea, java.awt.BorderLayout.CENTER); + }// </editor-fold>//GEN-END:initComponents + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JTextArea textArea; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/StringSetPropertyEditorPanel.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/StringSetPropertyEditorPanel.form new file mode 100644 index 0000000..649208d --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/StringSetPropertyEditorPanel.form @@ -0,0 +1,158 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,44,0,0,1,-112"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/> + <SubComponents> + <Container class="javax.swing.JScrollPane" name="itemsScrollPane"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription"> + <BorderConstraints direction="Center"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JList" name="itemsList"> + <Properties> + <Property name="selectionMode" type="int" value="0"/> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="<String>"/> + </AuxValues> + </Component> + </SubComponents> + </Container> + <Container class="javax.swing.JPanel" name="controlPanel"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription"> + <BorderConstraints direction="East"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Component class="javax.swing.JButton" name="addButton"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="addButton" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="stringSetPropertyEditorPanel.button_add.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="addButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="-1" gridY="-1" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="15" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JButton" name="editButton"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="editButton" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="stringSetPropertyEditorPanel.button_edit.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="editButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="1" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="10" insetsLeft="15" insetsBottom="10" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JButton" name="removeButton"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="removeButton" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="stringSetPropertyEditorPanel.button_remove.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="removeButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="2" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="15" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Container class="javax.swing.JPanel" name="rigidArea"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="5" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="1.0"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignFlowLayout"/> + </Container> + <Component class="javax.swing.JButton" name="moveUpButton"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="moveUpButton" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="stringSetPropertyEditorPanel.button_up.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="moveUpButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="3" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="10" insetsLeft="15" insetsBottom="10" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JButton" name="moveDownButton"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="moveDownButton" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="stringSetPropertyEditorPanel.button_down.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="moveDownButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="4" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="15" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> + </Container> + </SubComponents> +</Form> diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/StringSetPropertyEditorPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/StringSetPropertyEditorPanel.java new file mode 100644 index 0000000..c8a1703 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/StringSetPropertyEditorPanel.java @@ -0,0 +1,268 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import java.awt.Dimension; +import java.util.ArrayList; +import java.util.List; +import javax.swing.DefaultListModel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.ListModel; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.base.components.properties.type.StringSetProperty; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; + +/** + * User interface to edit a string set. + */ +public abstract class StringSetPropertyEditorPanel + extends + JPanel + implements + DetailsDialogContent { + + /** + * The property to edit. + */ + private StringSetProperty fProperty; + + /** + * Creates new form StringSetPropertyEditorPanel. + */ + @SuppressWarnings("this-escape") + public StringSetPropertyEditorPanel() { + initComponents(); + setPreferredSize(new Dimension(350, 250)); + } + + @Override + public void setProperty(Property property) { + fProperty = (StringSetProperty) property; + DefaultListModel<String> model = new DefaultListModel<>(); + + for (String item : fProperty.getItems()) { + model.addElement(item); + } + + itemsList.setModel(model); + } + + @Override + public void updateValues() { + List<String> items = new ArrayList<>(); + ListModel<String> model = itemsList.getModel(); + int size = model.getSize(); + + for (int i = 0; i < size; i++) { + items.add(model.getElementAt(i)); + } + + fProperty.setItems(items); + } + + @Override + public Property getProperty() { + return fProperty; + } + + /** + * Edits the selected value. + */ + protected abstract void edit(); + + /** + * Adds a new entry. + */ + protected abstract void add(); + + /** + * Returns the list with the values. + * + * @return The list with the values. + */ + protected JList<String> getItemsList() { + return itemsList; + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + itemsScrollPane = new javax.swing.JScrollPane(); + itemsList = new javax.swing.JList<>(); + controlPanel = new javax.swing.JPanel(); + addButton = new javax.swing.JButton(); + editButton = new javax.swing.JButton(); + removeButton = new javax.swing.JButton(); + rigidArea = new javax.swing.JPanel(); + moveUpButton = new javax.swing.JButton(); + moveDownButton = new javax.swing.JButton(); + + setLayout(new java.awt.BorderLayout()); + + itemsList.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + itemsScrollPane.setViewportView(itemsList); + + add(itemsScrollPane, java.awt.BorderLayout.CENTER); + + controlPanel.setLayout(new java.awt.GridBagLayout()); + + addButton.setFont(addButton.getFont()); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/panels/propertyEditing"); // NOI18N + addButton.setText(bundle.getString("stringSetPropertyEditorPanel.button_add.text")); // NOI18N + addButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 15, 0, 0); + controlPanel.add(addButton, gridBagConstraints); + + editButton.setFont(editButton.getFont()); + editButton.setText(bundle.getString("stringSetPropertyEditorPanel.button_edit.text")); // NOI18N + editButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + editButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(10, 15, 10, 0); + controlPanel.add(editButton, gridBagConstraints); + + removeButton.setFont(removeButton.getFont()); + removeButton.setText(bundle.getString("stringSetPropertyEditorPanel.button_remove.text")); // NOI18N + removeButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + removeButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 15, 0, 0); + controlPanel.add(removeButton, gridBagConstraints); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 5; + gridBagConstraints.weighty = 1.0; + controlPanel.add(rigidArea, gridBagConstraints); + + moveUpButton.setFont(moveUpButton.getFont()); + moveUpButton.setText(bundle.getString("stringSetPropertyEditorPanel.button_up.text")); // NOI18N + moveUpButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + moveUpButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(10, 15, 10, 0); + controlPanel.add(moveUpButton, gridBagConstraints); + + moveDownButton.setFont(moveDownButton.getFont()); + moveDownButton.setText(bundle.getString("stringSetPropertyEditorPanel.button_down.text")); // NOI18N + moveDownButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + moveDownButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 15, 0, 0); + controlPanel.add(moveDownButton, gridBagConstraints); + + add(controlPanel, java.awt.BorderLayout.EAST); + }// </editor-fold>//GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void moveDownButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_moveDownButtonActionPerformed + int index = itemsList.getSelectedIndex(); + + if (index == -1) { + return; + } + + DefaultListModel<String> model = (DefaultListModel<String>) itemsList.getModel(); + + if (index == model.size() - 1) { + return; + } + + String value = model.getElementAt(index); + model.removeElementAt(index); + model.insertElementAt(value, index + 1); + itemsList.setSelectedIndex(index + 1); + }//GEN-LAST:event_moveDownButtonActionPerformed + + private void moveUpButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_moveUpButtonActionPerformed + int index = itemsList.getSelectedIndex(); + + if (index == -1) { + return; + } + + if (index == 0) { + return; + } + + DefaultListModel<String> model = (DefaultListModel<String>) itemsList.getModel(); + String value = model.getElementAt(index); + model.removeElementAt(index); + model.insertElementAt(value, index - 1); + itemsList.setSelectedIndex(index - 1); + }//GEN-LAST:event_moveUpButtonActionPerformed + + private void removeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removeButtonActionPerformed + String value = itemsList.getSelectedValue(); + + if (value == null) { + return; + } + + DefaultListModel<String> model = (DefaultListModel<String>) itemsList.getModel(); + model.removeElement(value); + }//GEN-LAST:event_removeButtonActionPerformed + + private void editButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_editButtonActionPerformed + edit(); + }//GEN-LAST:event_editButtonActionPerformed + + private void addButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addButtonActionPerformed + add(); + }//GEN-LAST:event_addButtonActionPerformed + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton addButton; + private javax.swing.JPanel controlPanel; + private javax.swing.JButton editButton; + private javax.swing.JList<String> itemsList; + private javax.swing.JScrollPane itemsScrollPane; + private javax.swing.JButton moveDownButton; + private javax.swing.JButton moveUpButton; + private javax.swing.JButton removeButton; + private javax.swing.JPanel rigidArea; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/SymbolPropertyEditorPanel.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/SymbolPropertyEditorPanel.form new file mode 100644 index 0000000..f623566 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/SymbolPropertyEditorPanel.form @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.2" maxVersion="1.2" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,0,99,0,0,0,-37"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Component class="javax.swing.JButton" name="previousSymbolButton"> + <Properties> + <Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor"> + <Image iconType="3" name="/org/opentcs/guing/res/symbols/panel/back.24x24.gif"/> + </Property> + <Property name="margin" type="java.awt.Insets" editor="org.netbeans.beaninfo.editors.InsetsEditor"> + <Insets value="[2, 2, 2, 2]"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="previousSymbolButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="1" gridWidth="1" gridHeight="1" fill="3" ipadX="0" ipadY="0" insetsTop="4" insetsLeft="4" insetsBottom="4" insetsRight="4" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JButton" name="nextSymbolButton"> + <Properties> + <Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor"> + <Image iconType="3" name="/org/opentcs/guing/res/symbols/panel/forward.24x24.gif"/> + </Property> + <Property name="margin" type="java.awt.Insets" editor="org.netbeans.beaninfo.editors.InsetsEditor"> + <Insets value="[2, 2, 2, 2]"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="nextSymbolButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="2" gridY="1" gridWidth="1" gridHeight="1" fill="3" ipadX="0" ipadY="0" insetsTop="4" insetsLeft="4" insetsBottom="4" insetsRight="4" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JLabel" name="labelSymbol"> + <Properties> + <Property name="horizontalAlignment" type="int" value="0"/> + <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> + <Border info="org.netbeans.modules.form.compat2.border.CompoundBorderInfo"> + <CompoundBorder> + <Border PropertyName="outside" info="org.netbeans.modules.form.compat2.border.LineBorderInfo"> + <LineBorder/> + </Border> + <Border PropertyName="inside" info="org.netbeans.modules.form.compat2.border.EmptyBorderInfo"> + <EmptyBorder bottom="10" left="10" right="10" top="10"/> + </Border> + </CompoundBorder> + </Border> + </Property> + <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[200, 100]"/> + </Property> + <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[100, 60]"/> + </Property> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[100, 60]"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="1" gridY="1" gridWidth="1" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="4" insetsLeft="4" insetsBottom="4" insetsRight="4" anchor="10" weightX="0.5" weightY="0.5"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JLabel" name="labelSymbolName"> + <Properties> + <Property name="horizontalAlignment" type="int" value="0"/> + <Property name="text" type="java.lang.String" value="Name"/> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="2" gridWidth="3" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="6" insetsBottom="4" insetsRight="6" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JButton" name="removeButton"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/panels/propertyEditing.properties" key="symbolPropertyEditorPanel.button_removeSymbol.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="removeButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="1" gridY="3" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="4" insetsLeft="4" insetsBottom="4" insetsRight="4" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> +</Form> diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/SymbolPropertyEditorPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/SymbolPropertyEditorPanel.java new file mode 100644 index 0000000..d076979 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/panel/SymbolPropertyEditorPanel.java @@ -0,0 +1,246 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.panel; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.List; +import javax.swing.ImageIcon; +import javax.swing.JPanel; +import org.opentcs.components.plantoverview.LocationTheme; +import org.opentcs.data.model.visualization.LocationRepresentation; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.base.components.properties.type.SymbolProperty; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * User interface to edit a symbol property. + */ +public class SymbolPropertyEditorPanel + extends + JPanel + implements + DetailsDialogContent { + + /** + * The possible symbols. + */ + private final List<LocationRepresentation> fRepresentations = new ArrayList<>(); + /** + * The symbol. + */ + private final List<ImageIcon> fSymbols = new ArrayList<>(); + /** + * The location theme to be used. + */ + private final LocationTheme locationTheme; + /** + * The index of the selected symbols. + */ + private int fIndex; + /** + * The property. + */ + private SymbolProperty fProperty; + + /** + * Creates new instance. + * + * @param locationTheme The location theme to be used. + */ + @Inject + @SuppressWarnings("this-escape") + public SymbolPropertyEditorPanel( + @Nonnull + LocationTheme locationTheme + ) { + this.locationTheme = requireNonNull(locationTheme, "locationTheme"); + + initComponents(); + init(); + } + + @Override // DetailsDialogContent + public void setProperty(Property property) { + fProperty = (SymbolProperty) property; + fIndex = fRepresentations.indexOf(fProperty.getLocationRepresentation()); + + if (fIndex == -1) { + fIndex = 0; + } + + updateView(); + } + + @Override // DetailsDialogContent + public void updateValues() { + if (fIndex < 0) { + fProperty.setLocationRepresentation(LocationRepresentation.DEFAULT); + } + else { + fProperty.setLocationRepresentation(fRepresentations.get(fIndex)); + } + } + + @Override // DetailsDialogContent + public String getTitle() { + return ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH) + .getString("symbolPropertyEditorPanel.title"); + } + + @Override // DetailsDialogContent + public Property getProperty() { + return fProperty; + } + + private void init() { + for (LocationRepresentation cur : LocationRepresentation.values()) { + fRepresentations.add(cur); + fSymbols.add(new ImageIcon(locationTheme.getImageFor(cur))); + } + } + + /** + * Updates the view. + */ + private void updateView() { + fSymbols.clear(); + fRepresentations.clear(); + init(); + labelSymbol.setIcon(fSymbols.get(fIndex)); + labelSymbolName.setText(fRepresentations.get(fIndex).name()); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + previousSymbolButton = new javax.swing.JButton(); + nextSymbolButton = new javax.swing.JButton(); + labelSymbol = new javax.swing.JLabel(); + labelSymbolName = new javax.swing.JLabel(); + removeButton = new javax.swing.JButton(); + + setLayout(new java.awt.GridBagLayout()); + + previousSymbolButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/opentcs/guing/res/symbols/panel/back.24x24.gif"))); // NOI18N + previousSymbolButton.setMargin(new java.awt.Insets(2, 2, 2, 2)); + previousSymbolButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + previousSymbolButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.VERTICAL; + gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 4); + add(previousSymbolButton, gridBagConstraints); + + nextSymbolButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/opentcs/guing/res/symbols/panel/forward.24x24.gif"))); // NOI18N + nextSymbolButton.setMargin(new java.awt.Insets(2, 2, 2, 2)); + nextSymbolButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + nextSymbolButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.VERTICAL; + gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 4); + add(nextSymbolButton, gridBagConstraints); + + labelSymbol.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + labelSymbol.setBorder(javax.swing.BorderFactory.createCompoundBorder(javax.swing.BorderFactory.createLineBorder(new java.awt.Color(0, 0, 0)), javax.swing.BorderFactory.createEmptyBorder(10, 10, 10, 10))); + labelSymbol.setMaximumSize(new java.awt.Dimension(200, 100)); + labelSymbol.setMinimumSize(new java.awt.Dimension(100, 60)); + labelSymbol.setPreferredSize(new java.awt.Dimension(100, 60)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.weighty = 0.5; + gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 4); + add(labelSymbol, gridBagConstraints); + + labelSymbolName.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + labelSymbolName.setText("Name"); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 6, 4, 6); + add(labelSymbolName, gridBagConstraints); + + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/panels/propertyEditing"); // NOI18N + removeButton.setText(bundle.getString("symbolPropertyEditorPanel.button_removeSymbol.text")); // NOI18N + removeButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + removeButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 4); + add(removeButton, gridBagConstraints); + }// </editor-fold>//GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void nextSymbolButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_nextSymbolButtonActionPerformed + if (fIndex >= fSymbols.size() - 1 || fIndex < 0) { + fIndex = 0; + } + else { + fIndex++; + } + + updateView(); + }//GEN-LAST:event_nextSymbolButtonActionPerformed + + private void previousSymbolButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_previousSymbolButtonActionPerformed + if (fIndex <= 0 || fIndex >= fSymbols.size()) { + fIndex = fSymbols.size() - 1; + } + else { + fIndex--; + } + + updateView(); + }//GEN-LAST:event_previousSymbolButtonActionPerformed + + private void removeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removeButtonActionPerformed + labelSymbol.setIcon(null); + labelSymbolName.setText(LocationRepresentation.DEFAULT.name()); + fIndex = -2; // Invalid index, so in updateValues() no Icon will be loaded + }//GEN-LAST:event_removeButtonActionPerformed + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel labelSymbol; + private javax.swing.JLabel labelSymbolName; + private javax.swing.JButton nextSymbolButton; + private javax.swing.JButton previousSymbolButton; + private javax.swing.JButton removeButton; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/AbstractPropertyCellEditor.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/AbstractPropertyCellEditor.java new file mode 100644 index 0000000..db05723 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/AbstractPropertyCellEditor.java @@ -0,0 +1,177 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.table; + +import java.awt.Color; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Objects; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.JTextField; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.common.components.dialogs.DetailsDialog; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; +import org.opentcs.guing.common.components.dialogs.StandardDetailsDialog; +import org.opentcs.guing.common.util.UserMessageHelper; + +/** + * Base implementation for a cell editor to edit a property. + */ +public abstract class AbstractPropertyCellEditor + extends + javax.swing.DefaultCellEditor { + + /** + * The color a cell is painted in when a property informs about + * different values. + */ + public static final Color DIFFERENT_VALUE_COLOR = new Color(202, 225, 255); + /** + * The property. + */ + protected Property fProperty; + /** + * The details dialog that allows editing of the property. + */ + protected DetailsDialog fDetailsDialog; + /** + * The UI component. + */ + protected final JComponent fComponent; + /** + * Utility class to show messages to the user, eg error messages. + */ + protected final UserMessageHelper userMessageHelper; + + /** + * Creates a new instance. + * + * @param textField + * @param umh + */ + @SuppressWarnings("this-escape") + public AbstractPropertyCellEditor(JTextField textField, UserMessageHelper umh) { + super(textField); + fComponent = createComponent(); + userMessageHelper = Objects.requireNonNull(umh, "umh is null"); + } + + /** + * Creates a new instance. + * + * @param checkBox + * @param umh + */ + @SuppressWarnings("this-escape") + public AbstractPropertyCellEditor(JCheckBox checkBox, UserMessageHelper umh) { + super(checkBox); + fComponent = createComponent(); + userMessageHelper = Objects.requireNonNull(umh, "umh is null"); + } + + /** + * Creates a new instance. + * + * @param comboBox + * @param umh + */ + @SuppressWarnings("this-escape") + public AbstractPropertyCellEditor(JComboBox<?> comboBox, UserMessageHelper umh) { + super(comboBox); + fComponent = createComponent(); + userMessageHelper = Objects.requireNonNull(umh, "umh is null"); + } + + /** + * Creates the component that edits the property. + * + * @return The component that edits the property. + */ + protected JComponent createComponent() { + JPanel panel = new JPanel(); + panel.setLayout(new java.awt.BorderLayout()); + panel.add(getComponent(), java.awt.BorderLayout.CENTER); + + JComponent button = createButtonDetailsDialog(); + + if (button != null) { + panel.add(button, java.awt.BorderLayout.EAST); + } + + return panel; + } + + /** + * Creates a button with three dots. + * + * @return A button with three dots or null. + */ + protected JComponent createButtonDetailsDialog() { + JButton button = new JButton("..."); + button.setMargin(new Insets(0, 2, 0, 2)); + button.addActionListener(new ActionListener() { + + @Override // ActionListener + public void actionPerformed(ActionEvent e) { + showDialog(); + fireEditingStopped(); + } + }); + + return button; + } + + /** + * Set the dialog that alows for easy edit of the property. + * + * @param detailsDialog The dialog that allows editing of the property. + */ + public void setDetailsDialog(DetailsDialog detailsDialog) { + fDetailsDialog = detailsDialog; + } + + /** + * Set the property. + * + * @param value The property. + */ + protected void setValue(Object value) { + fProperty = (Property) value; + } + + /** + * Mark the property as changed. + */ + protected void markProperty() { + fProperty.markChanged(); + } + + /** + * Opens the dialog. + */ + protected void showDialog() { + if (fDetailsDialog != null) { + DetailsDialogContent content = fDetailsDialog.getDialogContent(); + content.setProperty(fProperty); + + StandardDetailsDialog dialog = (StandardDetailsDialog) fDetailsDialog; + dialog.setLocationRelativeTo(dialog.getParentComponent()); + dialog.setVisible(true); + + delegate.setValue(fProperty); + stopCellEditing(); + } + } + + /** + * Sets the focus to the actual component (JTextField etc.). + */ + public void setFocusToComponent() { + getComponent().requestFocusInWindow(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/AttributesTable.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/AttributesTable.java new file mode 100644 index 0000000..84231d9 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/AttributesTable.java @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.table; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.List; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.event.TableModelEvent; +import javax.swing.table.TableCellEditor; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableModel; +import org.opentcs.guing.base.components.properties.type.ModelAttribute; +import org.opentcs.guing.common.application.ApplicationState; +import org.opentcs.guing.common.application.OperationMode; +import org.opentcs.guing.common.components.properties.event.TableChangeListener; +import org.opentcs.guing.common.components.properties.event.TableSelectionChangeEvent; + +/** + * A table in which properties are displayed and can be edited. + * The table has two columns, the first with the name of the property and the second with the + * value of the property. + */ +public class AttributesTable + extends + JTable { + + /** + * Stores the application's current state. + */ + private final ApplicationState appState; + /** + * List of table change listeners. + */ + private final List<TableChangeListener> fTableChangeListeners = new ArrayList<>(); + + /** + * Creates a new instance. + * + * @param appState Stores the application's current state. + */ + @Inject + @SuppressWarnings("this-escape") + public AttributesTable(ApplicationState appState) { + this.appState = requireNonNull(appState, "appState"); + setStyle(); + putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); + } + + /** + * Initialises the style of the table. + */ + protected final void setStyle() { + setRowHeight(20); + setCellSelectionEnabled(false); + getTableHeader().setReorderingAllowed(false); + setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + ListSelectionModel model = getSelectionModel(); + model.addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + if (e.getValueIsAdjusting()) { + return; + } + + ListSelectionModel l = (ListSelectionModel) e.getSource(); + + if (l.isSelectionEmpty()) { + fireSelectionChanged(null); + } + else { + int selectedRow = l.getMinSelectionIndex(); + fireSelectionChanged(getModel().getValueAt(selectedRow, 1)); + } + } + }); + } + + /** + * Adds a TableSelectionChangeListener. + * + * @param l The listener to add. + */ + public void addTableChangeListener(TableChangeListener l) { + fTableChangeListeners.add(l); + } + + /** + * Removes a TableSelectionChangeListener. + * + * @param l the listener to remove. + */ + public void removeTableChangeListener(TableChangeListener l) { + fTableChangeListeners.remove(l); + } + + /** + * Notify all registered listeners that a table row has been selected. + * + * @param selectedValue + */ + protected void fireSelectionChanged(Object selectedValue) { + for (TableChangeListener l : fTableChangeListeners) { + l.tableSelectionChanged(new TableSelectionChangeEvent(this, selectedValue)); + } + } + + @Override + public TableCellEditor getCellEditor(int row, int column) { + TableModel tableModel = getModel(); + Object value = tableModel.getValueAt(row, column); + TableCellEditor editor = getDefaultEditor(value.getClass()); + + return editor; + } + + @Override + public TableCellRenderer getCellRenderer(int row, int column) { + TableModel tableModel = getModel(); + Object value = tableModel.getValueAt(row, column); + TableCellRenderer renderer = getDefaultRenderer(value.getClass()); + + return renderer; + } + + /** + * Indicates whether the specified row is editable. + * + * @param row The index of the row to be checked. + * @return True if the row is editable. + */ + public boolean isEditable(int row) { + AttributesTableModel tableModel = (AttributesTableModel) getModel(); + ModelAttribute attribute = (ModelAttribute) tableModel.getValueAt(row, 1); + + if (appState.hasOperationMode(OperationMode.MODELLING)) { + return attribute.isModellingEditable(); + } + + return attribute.isOperatingEditable(); + } + + @Override + public void tableChanged(TableModelEvent event) { + super.tableChanged(event); + + // Explicitly check for null here, because JTable fires an event before this class's constructor + // is run. + if (fTableChangeListeners != null) { + for (TableChangeListener listener : fTableChangeListeners) { + listener.tableModelChanged(); + } + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/AttributesTableModel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/AttributesTableModel.java new file mode 100644 index 0000000..df85f96 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/AttributesTableModel.java @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.table; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.guing.base.components.properties.type.ModelAttribute; +import org.opentcs.guing.common.application.ApplicationState; +import org.opentcs.guing.common.application.OperationMode; + +/** + * A table model for the PropertiesTable. + */ +public class AttributesTableModel + extends + javax.swing.table.DefaultTableModel { + + /** + * Stores the application's current state. + */ + private final ApplicationState appState; + + /** + * Creates a new instance of AttributesTableModel + * + * @param appState Stores the application's current state. + */ + @Inject + public AttributesTableModel(ApplicationState appState) { + this.appState = requireNonNull(appState, "appState"); + } + + /** + * Indicates whether a cell is editable. + * + * @param row The row of the cell to test. + * @param col The column of the cell to test. + * @return True if the specified cell is editable. + */ + @Override // AbstractTableModel + public boolean isCellEditable(int row, int col) { + if (col == 0) { + return false; + } + else { // col == 1 + ModelAttribute attribute = (ModelAttribute) getValueAt(row, col); + + if (appState.hasOperationMode(OperationMode.MODELLING)) { + return attribute.isModellingEditable(); + } + + return attribute.isOperatingEditable(); + } + } + + @Override // AbstractTableModel + public void fireTableDataChanged() { + super.fireTableDataChanged(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/BlockTypePropertyCellRenderer.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/BlockTypePropertyCellRenderer.java new file mode 100644 index 0000000..023991c --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/BlockTypePropertyCellRenderer.java @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.table; + +import java.awt.Component; +import javax.swing.JLabel; +import javax.swing.JTable; +import org.opentcs.guing.base.components.properties.type.BlockTypeProperty; +import org.opentcs.guing.base.model.elements.BlockModel.Type; + +/** + * A cell renderer for a {@link BlockTypeProperty}. + */ +public class BlockTypePropertyCellRenderer + extends + StandardPropertyCellRenderer { + + public BlockTypePropertyCellRenderer() { + super(); + } + + @Override + public Component getTableCellRendererComponent( + JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column + ) { + + JLabel label = (JLabel) super.getTableCellRendererComponent( + table, value, isSelected, + hasFocus, row, column + ); + + if (value instanceof BlockTypeProperty + && ((BlockTypeProperty) value).getValue() instanceof Type) { + BlockTypeProperty property = (BlockTypeProperty) value; + Type type = (Type) property.getValue(); + label.setText(type.getDescription()); + } + + decorate(table, row, column, label, value); + + return this; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/BooleanPropertyCellEditor.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/BooleanPropertyCellEditor.java new file mode 100644 index 0000000..4fe8e9b --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/BooleanPropertyCellEditor.java @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.table; + +import java.awt.Component; +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.JTable; +import org.opentcs.guing.base.components.properties.type.BooleanProperty; +import org.opentcs.guing.common.util.UserMessageHelper; + +/** + * A cell editor for boolean properties. + */ +public class BooleanPropertyCellEditor + extends + AbstractPropertyCellEditor { + + /** + * Creates a new instance. + * + * @param checkBox + * @param umh + */ + public BooleanPropertyCellEditor(JCheckBox checkBox, UserMessageHelper umh) { + super(checkBox, umh); + checkBox.setHorizontalAlignment(JCheckBox.LEFT); + } + + @Override + public Component getTableCellEditorComponent( + JTable table, Object value, boolean isSelected, int row, int column + ) { + + setValue(value); + JCheckBox checkBox = (JCheckBox) getComponent(); + checkBox.setBackground(table.getBackground()); + + if (property().getValue() instanceof Boolean) { + checkBox.setSelected((boolean) property().getValue()); + } + + return fComponent; + } + + @Override + public Object getCellEditorValue() { + JCheckBox checkBox = (JCheckBox) getComponent(); + boolean newValue = checkBox.isSelected(); + property().setValue(newValue); + + if (property().getValue() instanceof Boolean) { + markProperty(); + } + + return property(); + } + + /** + * Return the property of this editor. + * + * @return The property of this editor. + */ + protected BooleanProperty property() { + return (BooleanProperty) fProperty; + } + + /** + * Creates the details dialog. + * Always returns null, does not create a details dialog. + * + * @return always returns null, does not create a details dialog. + */ + @Override + protected JComponent createButtonDetailsDialog() { + return null; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/BooleanPropertyCellRenderer.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/BooleanPropertyCellRenderer.java new file mode 100644 index 0000000..8c6b147 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/BooleanPropertyCellRenderer.java @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.table; + +import java.awt.Component; +import javax.swing.JCheckBox; +import javax.swing.JTable; +import org.opentcs.guing.base.components.properties.type.BooleanProperty; +import org.opentcs.guing.base.components.properties.type.MultipleDifferentValues; + +/** + * A cell renderer for a boolean property. + */ +public class BooleanPropertyCellRenderer + extends + JCheckBox + implements + javax.swing.table.TableCellRenderer { + + /** + * Creates a new instance of BooleanCellRenderer. + */ + @SuppressWarnings("this-escape") + public BooleanPropertyCellRenderer() { + super(); + setHorizontalAlignment(JCheckBox.LEFT); + } + + @Override + public Component getTableCellRendererComponent( + JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column + ) { + + AttributesTable attributesTable = (AttributesTable) table; + boolean editable = attributesTable.isEditable(row); + + if (isSelected) { + setForeground(table.getSelectionForeground()); + super.setBackground(table.getSelectionBackground()); + } + else { + setForeground(table.getForeground()); + setBackground(editable ? table.getBackground() : StandardPropertyCellRenderer.BG_UNEDITABLE); + } + + if (value instanceof BooleanProperty) { + BooleanProperty property = (BooleanProperty) value; + + if (property.getValue() instanceof MultipleDifferentValues) { + setBackground(AbstractPropertyCellEditor.DIFFERENT_VALUE_COLOR); + } + else if (property.getValue() instanceof Boolean) { + setToolTipText(property.getHelptext()); + setSelected((boolean) property.getValue()); + } + else { + setEnabled(false); + } + } + + return this; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/CellEditorFactory.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/CellEditorFactory.java new file mode 100644 index 0000000..7598e20 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/CellEditorFactory.java @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.table; + +import javax.swing.JPanel; + +/** + * A factory for cell editors. + */ +public interface CellEditorFactory { + + /** + * Creates a new <code>ComplexPropertyCellEditor</code>. + * + * @param dialogParent The component to be used as the parent for dialogs + * created by the new instance. + * @return A new cell editor instance. + */ + ComplexPropertyCellEditor createComplexPropertyCellEditor(JPanel dialogParent); + +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/ColorPropertyCellEditor.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/ColorPropertyCellEditor.java new file mode 100644 index 0000000..c4968c6 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/ColorPropertyCellEditor.java @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.table; + +import static org.opentcs.guing.common.util.I18nPlantOverview.PROPERTIES_PATH; + +import java.awt.Color; +import java.awt.Frame; +import javax.swing.JButton; +import javax.swing.JColorChooser; +import javax.swing.JOptionPane; +import javax.swing.JTable; +import org.opentcs.guing.base.components.properties.type.ColorProperty; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * A cell editor for a color property. + */ +public class ColorPropertyCellEditor + extends + javax.swing.AbstractCellEditor + implements + javax.swing.table.TableCellEditor, + java.awt.event.ActionListener { + + /** + * The button to use for the editor. + */ + protected JButton fButton; + /** + * The color property. + */ + protected ColorProperty fColorProperty; + /** + * The parent table. + */ + protected JTable fTable; + + /** + * Creates a new instance of ColorPropertyCellEditor. + */ + @SuppressWarnings("this-escape") + public ColorPropertyCellEditor() { + super(); + fButton = new JButton(); + fButton.setBorderPainted(false); + fButton.addActionListener(this); + } + + @Override + public java.awt.Component getTableCellEditorComponent( + JTable table, Object value, boolean isSelected, int row, int column + ) { + fTable = table; + fColorProperty = (ColorProperty) value; + fButton.setBackground(fColorProperty.getColor()); + return fButton; + } + + @Override + public Object getCellEditorValue() { + return fColorProperty; + } + + @Override + public void actionPerformed(java.awt.event.ActionEvent actionEvent) { + ResourceBundleUtil bundle = ResourceBundleUtil.getBundle(PROPERTIES_PATH); + + Frame parent = JOptionPane.getFrameForComponent(fTable); + Color newColor = JColorChooser.showDialog( + parent, + bundle.getString("colorPropertyCellEditor.dialog_colorSelection.title"), + fColorProperty.getColor() + ); + + if (newColor != null) { + Color oldColor = fColorProperty.getColor(); + fColorProperty.setColor(newColor); + + if (newColor != oldColor) { + fColorProperty.markChanged(); + } + } + + stopCellEditing(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/ColorPropertyCellRenderer.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/ColorPropertyCellRenderer.java new file mode 100644 index 0000000..4f96b2a --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/ColorPropertyCellRenderer.java @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.table; + +import java.awt.Color; +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.border.Border; +import org.opentcs.guing.base.components.properties.type.ColorProperty; + +/** + * A cell renderer for a color property. + */ +public class ColorPropertyCellRenderer + extends + JLabel + implements + javax.swing.table.TableCellRenderer { + + /** + * Creates a new instance of ColorPropertyCellRenderer. + */ + @SuppressWarnings("this-escape") + public ColorPropertyCellRenderer() { + super(); + setOpaque(true); + Border insideBorder = BorderFactory.createLineBorder(Color.black); + Border outsideBorder = BorderFactory.createMatteBorder(5, 10, 5, 10, Color.white); + setBorder(BorderFactory.createCompoundBorder(outsideBorder, insideBorder)); + } + + @Override + public java.awt.Component getTableCellRendererComponent( + JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column + ) { + + ColorProperty property = (ColorProperty) value; + setBackground(property.getColor()); + + return this; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/ComplexPropertyCellEditor.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/ComplexPropertyCellEditor.java new file mode 100644 index 0000000..459926c --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/ComplexPropertyCellEditor.java @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.table; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.Provider; +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.awt.Component; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Map; +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.JTable; +import org.opentcs.guing.base.components.properties.type.AbstractComplexProperty; +import org.opentcs.guing.common.components.dialogs.DetailsDialogContent; +import org.opentcs.guing.common.components.dialogs.StandardDetailsDialog; + +/** + * A cell editor for a complex property. + */ +public class ComplexPropertyCellEditor + extends + javax.swing.AbstractCellEditor + implements + javax.swing.table.TableCellEditor { + + /** + * The button for showing the details dialog. + */ + private final JButton fButton = new JButton(); + // CHECKSTYLE:OFF (Getting this declaration shorter is difficult with automatic formatting.) + /** + * Provides the appropriate dialog content for a given property. + */ + private final Map<Class<? extends AbstractComplexProperty>, Provider<DetailsDialogContent>> contentMap; + // CHECKSTYLE:ON + /** + * A parent for dialogs created by this instance. + */ + private final JPanel dialogParent; + /** + * The property being edited. + */ + private AbstractComplexProperty fProperty; + + /** + * Creates a new instance. + * + * @param contentMap Provides the appropriate content for a given property. + * @param dialogParent A parent for dialogs created by this instance. + */ + @Inject + public ComplexPropertyCellEditor( + Map<Class<? extends AbstractComplexProperty>, Provider<DetailsDialogContent>> contentMap, + @Assisted + JPanel dialogParent + ) { + this.contentMap = requireNonNull(contentMap, "contentMap"); + this.dialogParent = requireNonNull(dialogParent, "dialogParent"); + + fButton.setFont(new Font("Dialog", Font.PLAIN, 12)); + fButton.setBorder(null); + fButton.setHorizontalAlignment(JButton.LEFT); + fButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + showDialog(); + } + }); + } + + @Override + public Object getCellEditorValue() { + return fProperty; + } + + @Override + public Component getTableCellEditorComponent( + JTable table, Object value, boolean isSelected, int row, int column + ) { + + fProperty = (AbstractComplexProperty) value; + fButton.setText(fProperty.toString()); + fButton.setBackground(table.getBackground()); + + return fButton; + } + + /** + * Shows the dialog for editing the property. + */ + private void showDialog() { + DetailsDialogContent content = contentMap.get(fProperty.getClass()).get(); + + StandardDetailsDialog detailsDialog + = new StandardDetailsDialog(dialogParent, true, content); + detailsDialog.setLocationRelativeTo(dialogParent); + + detailsDialog.getDialogContent().setProperty(fProperty); + detailsDialog.activate(); + detailsDialog.setVisible(true); + + if (detailsDialog.getReturnStatus() == StandardDetailsDialog.RET_OK) { + stopCellEditing(); + } + else { + cancelCellEditing(); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/CoordinateCellEditor.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/CoordinateCellEditor.java new file mode 100644 index 0000000..687c7a2 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/CoordinateCellEditor.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.table; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import javax.swing.BoxLayout; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.JTextField; +import org.opentcs.guing.common.util.UserMessageHelper; + +/** + * A cell editor for a coordinate property. + */ +public class CoordinateCellEditor + extends + QuantityCellEditor { + + /** + * Creates a new instance. + * + * @param textField + */ + @Inject + public CoordinateCellEditor( + @Assisted + JTextField textField, + @Assisted + UserMessageHelper umh + ) { + super(textField, umh); + } + + /** + * The table cell contains (from left to right): + * 1. An editable textfield + * 2. A button with "arrow down" symbol to copy values from model to layout + * 3. A button with "arrow up" symbol to copy values from layout to model + * 4. A button with "..." symbol to open a dialog with extended editor + * functionality. + * + * @return + */ + @Override // AbstractPropertyCellEditor + protected JComponent createComponent() { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS)); + panel.add(getComponent()); + JComponent button = createButtonDetailsDialog(); + panel.add(button); + + return panel; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/IntegerPropertyCellEditor.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/IntegerPropertyCellEditor.java new file mode 100644 index 0000000..8cb2793 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/IntegerPropertyCellEditor.java @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.table; + +import java.awt.Component; +import java.text.ParseException; +import javax.swing.JComponent; +import javax.swing.JFormattedTextField; +import javax.swing.JTable; +import javax.swing.JTextField; +import org.opentcs.guing.base.components.properties.type.IntegerProperty; +import org.opentcs.guing.common.util.UserMessageHelper; +import org.slf4j.LoggerFactory; + +/** + * A cell editor for an integer property. + */ +public class IntegerPropertyCellEditor + extends + AbstractPropertyCellEditor { + + /** + * Creates a new instance of IntegerPropertyCellEditor + * + * @param textField + * @param umh + */ + @SuppressWarnings("this-escape") + public IntegerPropertyCellEditor(JFormattedTextField textField, UserMessageHelper umh) { + super(textField, umh); + setStyle(textField); + } + + /** + * Initialises the style for the text field. + * + * @param textField + */ + protected final void setStyle(JTextField textField) { + setClickCountToStart(1); + textField.setHorizontalAlignment(JTextField.LEFT); + } + + /** + * Create the component for this editor. + * + * @return the component for this editor. + */ + @Override + protected JComponent createComponent() { + return (JComponent) getComponent(); + } + + @Override + public Component getTableCellEditorComponent( + JTable table, Object value, boolean isSelected, int row, int column + ) { + + JFormattedTextField textField = (JFormattedTextField) getComponent(); + setValue(value); + + if (property().getValue() instanceof Integer) { + textField.setValue(property().getValue()); + } + + return fComponent; + } + + @Override + public Object getCellEditorValue() { + JFormattedTextField textField = (JFormattedTextField) getComponent(); + + try { + textField.commitEdit(); + } + catch (ParseException ex) { + LoggerFactory.getLogger(IntegerPropertyCellEditor.class).error( + "ParseException: {0}", + textField.getText() + ); + } + + try { + int newValue = Integer.parseInt(textField.getText()); + int oldValue = (int) property().getValue(); + property().setValue(newValue); + + if (newValue != oldValue) { + markProperty(); + } + + return property(); + } + catch (NumberFormatException e) { + return property(); + } + catch (ClassCastException ex) { + markProperty(); + return property(); + } + } + + /** + * Returns the property for this editor. + * + * @return the property for this editor. + */ + protected IntegerProperty property() { + return (IntegerProperty) fProperty; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/LinerTypePropertyCellRenderer.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/LinerTypePropertyCellRenderer.java new file mode 100644 index 0000000..89d13bf --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/LinerTypePropertyCellRenderer.java @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.table; + +import java.awt.Component; +import javax.swing.JLabel; +import javax.swing.JTable; +import org.opentcs.guing.base.components.properties.type.LinerTypeProperty; +import org.opentcs.guing.base.model.elements.PathModel.Type; + +/** + * A cell renderer for a {@link LinerTypeProperty}. + */ +public class LinerTypePropertyCellRenderer + extends + StandardPropertyCellRenderer { + + public LinerTypePropertyCellRenderer() { + super(); + } + + @Override + public Component getTableCellRendererComponent( + JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column + ) { + + JLabel label = (JLabel) super.getTableCellRendererComponent( + table, value, isSelected, + hasFocus, row, column + ); + + if (value instanceof LinerTypeProperty + && ((LinerTypeProperty) value).getValue() instanceof Type) { + LinerTypeProperty property = (LinerTypeProperty) value; + Type type = (Type) property.getValue(); + label.setText(type.getDescription()); + } + + decorate(table, row, column, label, value); + + return this; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/PointTypePropertyCellRenderer.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/PointTypePropertyCellRenderer.java new file mode 100644 index 0000000..e6e78b4 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/PointTypePropertyCellRenderer.java @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.table; + +import java.awt.Component; +import javax.swing.JLabel; +import javax.swing.JTable; +import org.opentcs.guing.base.components.properties.type.PointTypeProperty; +import org.opentcs.guing.base.model.elements.PointModel.Type; + +/** + * A cell renderer for a {@link PointTypeProperty}. + */ +public class PointTypePropertyCellRenderer + extends + StandardPropertyCellRenderer { + + public PointTypePropertyCellRenderer() { + super(); + } + + @Override + public Component getTableCellRendererComponent( + JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column + ) { + + JLabel label = (JLabel) super.getTableCellRendererComponent( + table, value, isSelected, + hasFocus, row, column + ); + + if (value instanceof PointTypeProperty + && ((PointTypeProperty) value).getValue() instanceof Type) { + PointTypeProperty property = (PointTypeProperty) value; + Type type = (Type) property.getValue(); + label.setText(type.getDescription()); + } + + decorate(table, row, column, label, value); + + return this; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/QuantityCellEditor.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/QuantityCellEditor.java new file mode 100644 index 0000000..bbaf4b0 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/QuantityCellEditor.java @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.table; + +import java.awt.Component; +import javax.swing.JTable; +import javax.swing.JTextField; +import org.opentcs.guing.base.components.properties.type.AbstractQuantity; +import org.opentcs.guing.base.components.properties.type.ModelAttribute; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.guing.common.util.UserMessageHelper; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * A cell editor for a quantity property. + */ +public class QuantityCellEditor + extends + AbstractPropertyCellEditor { + + /** + * Creates a new instance of QuantityCellEditor + * + * @param textField + * @param umh + */ + @SuppressWarnings("this-escape") + public QuantityCellEditor(JTextField textField, UserMessageHelper umh) { + super(textField, umh); + setStyle(textField); + } + + /** + * Initialises the style of the text field. + * + * @param textField + */ + private void setStyle(JTextField textField) { + setClickCountToStart(1); + textField.setHorizontalAlignment(JTextField.LEFT); + } + + /** + * Returns the property for this editor. + * + * @return The property for this editor. + */ + protected AbstractQuantity<?> property() { + return (AbstractQuantity<?>) fProperty; + } + + /** + * Extracts the value and the unit from the text in the text field. + * If the text cannot be parsed, the property is not changed. + * + * @param text The text to extract the value and unit from. + */ + protected void extractQuantity(String text) { + + int blankIndex = text.indexOf(' '); + // No space means wrong format ( Number[SPACE]Unit ) + if (blankIndex == -1) { + showCellEditingErrorMsg("quantityCellEditor.dialog_errorFormat.message"); + return; + } + + String valueString = text.substring(0, blankIndex); + String unitString = text.substring(blankIndex + 1); + + // Check if unitString is a valid unit + if (!property().isPossibleUnit(unitString)) { + showCellEditingErrorMsg( + "quantityCellEditor.dialog_errorUnit.message", + property().getPossibleUnits() + ); + return; + } + + double newValue; + try { + newValue = Double.parseDouble(valueString); + } + catch (NumberFormatException e) { + showCellEditingErrorMsg("quantityCellEditor.dialog_errorNumber.message"); + return; + } + + // Check if value is inside the valid range + if (!property().getValidRange().isValueValid(newValue)) { + showCellEditingErrorMsg( + "quantityCellEditor.dialog_errorRange.message", + property().getValidRange().getMin(), + property().getValidRange().getMax() + ); + return; + } + + // For the layoutModel the Scaling cannot be 0 + if (property().getModel() instanceof LayoutModel && newValue == 0) { + showCellEditingErrorMsg("quantityCellEditor.dialog_errorScale.message"); + return; + } + + // Check if property will change + try { + double oldValue = (double) property().getValue(); + String oldUnit = property().getUnit().toString(); + + if (newValue != oldValue || !unitString.equals(oldUnit)) { + markProperty(); + } + } + catch (ClassCastException e) { + markProperty(); + } + + // Change property + property().setValueAndUnit(newValue, unitString); + + } + + @Override + public boolean stopCellEditing() { + // ChangeState.DETAIL_CHANGED is unwanted at this point and is set in + // StandardDetailsDialog. If we wouldn't change it here the model value + // would be copied to the layout. By changing the ChangeState to CHANGED + // it will only be saved in the model. + if (property().getChangeState() == ModelAttribute.ChangeState.DETAIL_CHANGED) { + property().setChangeState(ModelAttribute.ChangeState.CHANGED); + } + + return super.stopCellEditing(); + } + + @Override // DefaultCellEditor + public Component getTableCellEditorComponent( + JTable table, Object value, boolean isSelected, int row, int column + ) { + + setValue(value); + ((JTextField) getComponent()).setText(property().toString()); + + return fComponent; + } + + @Override // DefaultCellEditor + public Object getCellEditorValue() { + JTextField textField = (JTextField) getComponent(); + String text = textField.getText(); + + extractQuantity(text); + + return property(); + } + + private void showCellEditingErrorMsg(String resourceName, Object... arguments) { + ResourceBundleUtil bundle = ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH); + userMessageHelper.showMessageDialog( + bundle.getString("quantityCellEditor.dialog_error.title"), + bundle.getFormatted(resourceName, arguments), + UserMessageHelper.Type.ERROR + ); + } + + private void showCellEditingErrorMsg(String resourceName) { + showCellEditingErrorMsg(resourceName, ""); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/SelectionPropertyCellEditor.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/SelectionPropertyCellEditor.java new file mode 100644 index 0000000..9243f3a --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/SelectionPropertyCellEditor.java @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.table; + +import java.awt.Component; +import java.awt.Font; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.JTable; +import org.opentcs.guing.base.components.properties.type.AbstractProperty; +import org.opentcs.guing.base.components.properties.type.ModelAttribute; +import org.opentcs.guing.base.components.properties.type.Selectable; +import org.opentcs.guing.common.util.UserMessageHelper; + +/** + * A cell editor for a selection property. + */ +public class SelectionPropertyCellEditor + extends + AbstractPropertyCellEditor { + + /** + * Creates a new instance. + * + * @param comboBox + * @param umh + */ + public SelectionPropertyCellEditor(JComboBox<?> comboBox, UserMessageHelper umh) { + super(comboBox, umh); + comboBox.setFont(new Font("Dialog", Font.PLAIN, 12)); + } + + @Override + @SuppressWarnings("unchecked") + public JComboBox<Object> getComponent() { + return (JComboBox<Object>) super.getComponent(); + } + + @Override + public Component getTableCellEditorComponent( + JTable table, Object value, boolean isSelected, int row, int column + ) { + + setValue(value); + JComboBox<Object> comboBox = getComponent(); + comboBox.setModel( + new DefaultComboBoxModel<>(((Selectable) property()).getPossibleValues().toArray()) + ); + comboBox.setSelectedItem(property().getValue()); + + return fComponent; + } + + @Override + public Object getCellEditorValue() { + if (property().getChangeState() == ModelAttribute.ChangeState.DETAIL_CHANGED) { + Object value = property().getValue(); // DEBUG + } + else { + Object selectedItem = getComponent().getSelectedItem(); + Object oldValue = property().getValue(); + property().setValue(selectedItem); + + if (!selectedItem.equals(oldValue)) { + markProperty(); + } + } + + return property(); + } + + /** + * Return the property for this editor. + * + * @return the property for this editor. + */ + protected AbstractProperty property() { + return (AbstractProperty) fProperty; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/StandardPropertyCellRenderer.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/StandardPropertyCellRenderer.java new file mode 100644 index 0000000..090aa10 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/StandardPropertyCellRenderer.java @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.table; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Font; +import javax.swing.JLabel; +import javax.swing.JTable; +import org.opentcs.guing.base.components.properties.type.AbstractProperty; +import org.opentcs.guing.base.components.properties.type.AcceptableInvalidValue; +import org.opentcs.guing.base.components.properties.type.MultipleDifferentValues; +import org.opentcs.guing.base.components.properties.type.Property; + +/** + * A standard cell renderer for for properties in general. + */ +public class StandardPropertyCellRenderer + extends + javax.swing.table.DefaultTableCellRenderer { + + /** + * The background color for uneditable properties. + */ + public static final Color BG_UNEDITABLE = new Color(0xE0E0E0); + + /** + * Creates a new instance. + */ + @SuppressWarnings("this-escape") + public StandardPropertyCellRenderer() { + super(); + setStyle(); + } + + /** + * Initialises the style fo the labels. + */ + protected final void setStyle() { + setFont(new Font("Dialog", Font.PLAIN, 12)); + setHorizontalAlignment(JLabel.LEFT); + setBorder(null); + } + + /** + * Returns a component with the visualisation for this property. + * + * @return a component with the visualisation for this property. + */ + @Override + public Component getTableCellRendererComponent( + JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column + ) { + + JLabel label = (JLabel) super.getTableCellRendererComponent( + table, value, isSelected, + hasFocus, row, column + ); + label.setText(value.toString()); + + if (value instanceof AbstractProperty + && ((AbstractProperty) value).getValue() instanceof AcceptableInvalidValue) { + AbstractProperty property = (AbstractProperty) value; + AcceptableInvalidValue invalidValue = (AcceptableInvalidValue) property.getValue(); + label.setText(invalidValue.getDescription()); + label.setToolTipText(invalidValue.getHelptext()); + } + else if (value instanceof Property) { + label.setToolTipText(((Property) value).getHelptext()); + } + + decorate(table, row, column, label, value); + + return this; + } + + protected void decorate(JTable table, int row, int column, JLabel label, Object value) { + AttributesTable attributesTable = (AttributesTable) table; + boolean editable = attributesTable.isEditable(row); + + switch (column) { + case 0: + label.setBackground(BG_UNEDITABLE); + label.setForeground(Color.darkGray); + break; + + case 1: + if (value instanceof MultipleDifferentValues) { + label.setBackground(AbstractPropertyCellEditor.DIFFERENT_VALUE_COLOR); + } + else { + label.setBackground(editable ? Color.white : BG_UNEDITABLE); + } + label.setForeground(editable ? Color.blue : Color.darkGray); + break; + + default: + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/StringPropertyCellEditor.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/StringPropertyCellEditor.java new file mode 100644 index 0000000..61b8e6d --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/StringPropertyCellEditor.java @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.table; + +import java.awt.Component; +import javax.swing.JTable; +import javax.swing.JTextField; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.PlainDocument; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.common.util.UserMessageHelper; + +/** + * A cell editor for a string property. + */ +public class StringPropertyCellEditor + extends + AbstractPropertyCellEditor { + + /** + * Creates a new instance of StringPropertyCellEditor + * + * @param textField + * @param umh + */ + @SuppressWarnings("this-escape") + public StringPropertyCellEditor(JTextField textField, UserMessageHelper umh) { + super(textField, umh); + setStyle(textField); + } + + /** + * Initialises the style of the text field. + * + * @param textField the text field to style. + */ + protected final void setStyle(JTextField textField) { + setClickCountToStart(1); + textField.setHorizontalAlignment(JTextField.LEFT); + } + + @Override + public Component getTableCellEditorComponent( + JTable table, Object value, boolean isSelected, int row, int column + ) { + + setValue(value); + JTextField textField = (JTextField) getComponent(); + if (value instanceof StringProperty) { + textField.setDocument(new PlainDocument()); + } + textField.setText(property().getText()); + + return fComponent; + } + + @Override + public Object getCellEditorValue() { + JTextField textField = (JTextField) getComponent(); + String newText = textField.getText(); + String oldText = property().getText(); + property().setText(newText); + + if (!newText.equals(oldText)) { + markProperty(); + } + + return property(); + } + + /** + * Returns the property for this editor. + * + * @return the property for this editor. + */ + protected StringProperty property() { + return (StringProperty) fProperty; + } + + private class JTextFieldLimit + extends + PlainDocument { + + private final int limit; + + JTextFieldLimit(int limit) { + super(); + this.limit = limit; + } + + @Override + public void insertString(int offset, String str, AttributeSet attr) + throws BadLocationException { + if (str == null) { + return; + } + + if ((getLength() + str.length()) <= limit || str.equals(getText(0, getLength()))) { + super.insertString(offset, str, attr); + } + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/UndoableCellEditor.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/UndoableCellEditor.java new file mode 100644 index 0000000..fed274f --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/table/UndoableCellEditor.java @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.table; + +import java.awt.Component; +import java.util.EventObject; +import javax.swing.JTable; +import javax.swing.event.CellEditorListener; +import javax.swing.table.TableCellEditor; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.common.components.dialogs.DetailsDialog; +import org.opentcs.guing.common.components.properties.PropertyUndoActivity; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.UndoRedoManager; + +/** + * A cell editor wrapped in an undo manager. + */ +public class UndoableCellEditor + extends + javax.swing.AbstractCellEditor + implements + javax.swing.table.TableCellEditor { + + /** + * The undo manager. + */ + protected UndoRedoManager fUndoRedoManager; + /** + * The actual cell editor. + */ + protected TableCellEditor fWrappedCellEditor; + /** + * The undo activity. + */ + protected PropertyUndoActivity fUndoActivity; + + /** + * Creates a new instance of UndoableCellEditor + * + * @param cellEditor + */ + public UndoableCellEditor(TableCellEditor cellEditor) { + super(); + fWrappedCellEditor = cellEditor; + } + + /** + * Set the undo manager. + * + * @param undoManager the undo manager. + */ + public void setUndoManager(UndoRedoManager undoManager) { + fUndoRedoManager = undoManager; + } + + /** + * Sets the details dialog that is used to edit the property. + * + * @param detailsDialog the details dialog that is used to edit the property. + */ + public void setDetailsDialog(DetailsDialog detailsDialog) { + if (fWrappedCellEditor instanceof AbstractPropertyCellEditor) { + ((AbstractPropertyCellEditor) fWrappedCellEditor).setDetailsDialog(detailsDialog); + } + } + + /** + * Sets the focus to the actual component (JTextField etc.). + */ + public void setFocusToComponent() { + if (fWrappedCellEditor instanceof AbstractPropertyCellEditor) { + ((AbstractPropertyCellEditor) fWrappedCellEditor).setFocusToComponent(); + } + } + + /** + * Returns the actual cell editor. + * + * @return the actual cell editor. + */ + public TableCellEditor getWrappedCellEditor() { + return fWrappedCellEditor; + } + + @Override + public Object getCellEditorValue() { + Property property = (Property) fWrappedCellEditor.getCellEditorValue(); + fUndoActivity.snapShotAfterModification(); + fUndoRedoManager.addEdit(fUndoActivity); + + return property; + } + + @Override + public Component getTableCellEditorComponent( + JTable table, Object value, boolean isSelected, int row, int column + ) { + + Property property = (Property) value; + fUndoActivity = new PropertyUndoActivity(property); + fUndoActivity.snapShotBeforeModification(); + + return fWrappedCellEditor.getTableCellEditorComponent(table, value, isSelected, row, column); + } + + @Override + public void addCellEditorListener(CellEditorListener l) { + fWrappedCellEditor.addCellEditorListener(l); + } + + @Override + public void removeCellEditorListener(CellEditorListener l) { + fWrappedCellEditor.removeCellEditorListener(l); + } + + @Override + public boolean isCellEditable(EventObject anEvent) { + return fWrappedCellEditor.isCellEditable(anEvent); + } + + @Override + public boolean shouldSelectCell(EventObject anEvent) { + return fWrappedCellEditor.shouldSelectCell(anEvent); + } + + @Override + public boolean stopCellEditing() { + return fWrappedCellEditor.stopCellEditing(); + } + + @Override + public void cancelCellEditing() { + fWrappedCellEditor.cancelCellEditing(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/type/MergedPropertySuggestions.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/type/MergedPropertySuggestions.java new file mode 100644 index 0000000..46d8bd4 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/properties/type/MergedPropertySuggestions.java @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.type; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.Inject; +import java.util.HashSet; +import java.util.Set; +import java.util.TreeSet; +import org.opentcs.components.plantoverview.PropertySuggestions; + +/** + * Merges {@link PropertySuggestions} instances to a single one. + */ +public class MergedPropertySuggestions + implements + PropertySuggestions { + + private final Set<String> keySuggestions = new TreeSet<>(); + private final Set<String> valueSuggestions = new TreeSet<>(); + private final Set<PropertySuggestions> propertySuggestions; + + /** + * Creates a new instance, merging the keys/values of the given suggestions sets. + * + * @param propertySuggestions The suggestions to be merged. + */ + @Inject + public MergedPropertySuggestions(Set<PropertySuggestions> propertySuggestions) { + this.propertySuggestions = requireNonNull(propertySuggestions, "propertySuggestors"); + for (PropertySuggestions suggestor : propertySuggestions) { + keySuggestions.addAll(suggestor.getKeySuggestions()); + valueSuggestions.addAll(suggestor.getValueSuggestions()); + } + } + + @Override + public Set<String> getKeySuggestions() { + return keySuggestions; + } + + @Override + public Set<String> getValueSuggestions() { + return valueSuggestions; + } + + @Override + public Set<String> getValueSuggestionsFor(String key) { + Set<String> mergedCustomSuggestions = new HashSet<>(); + for (PropertySuggestions suggestor : propertySuggestions) { + Set<String> currentSuggestion = suggestor.getValueSuggestionsFor(key); + if (currentSuggestion != null) { + mergedCustomSuggestions.addAll(currentSuggestion); + } + } + return mergedCustomSuggestions; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/AbstractTreeViewPanel.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/AbstractTreeViewPanel.form new file mode 100644 index 0000000..dba58e6 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/AbstractTreeViewPanel.form @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.5" maxVersion="1.5" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,44,0,0,1,-112"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/> + <SubComponents> + <Container class="javax.swing.JScrollPane" name="scrollPaneTree"> + <Properties> + <Property name="horizontalScrollBarPolicy" type="int" value="32"/> + <Property name="verticalScrollBarPolicy" type="int" value="22"/> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription"> + <BorderConstraints direction="Center"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JTree" name="objectTree"> + <Properties> + <Property name="rootVisible" type="boolean" value="false"/> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_CreateCodeCustom" type="java.lang.String" value="new StandardActionTree(this);"/> + </AuxValues> + </Component> + </SubComponents> + </Container> + <Component class="javax.swing.JTextField" name="textFieldModelName"> + <Properties> + <Property name="editable" type="boolean" value="false"/> + <Property name="background" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="ff" green="99" red="99" type="rgb"/> + </Property> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font bold="true" component="textFieldModelName" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="ff" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="Model"/> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription"> + <BorderConstraints direction="First"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> +</Form> diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/AbstractTreeViewPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/AbstractTreeViewPanel.java new file mode 100644 index 0000000..2e5c0e6 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/AbstractTreeViewPanel.java @@ -0,0 +1,683 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.guing.common.model.SystemModel.FolderKey.LINKS; +import static org.opentcs.guing.common.model.SystemModel.FolderKey.LOCATIONS; +import static org.opentcs.guing.common.model.SystemModel.FolderKey.LOCATION_TYPES; +import static org.opentcs.guing.common.model.SystemModel.FolderKey.OTHER_GRAPHICAL_ELEMENTS; +import static org.opentcs.guing.common.model.SystemModel.FolderKey.PATHS; +import static org.opentcs.guing.common.model.SystemModel.FolderKey.POINTS; + +import jakarta.inject.Inject; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import javax.swing.ActionMap; +import javax.swing.InputMap; +import javax.swing.JPanel; +import javax.swing.JTree; +import javax.swing.KeyStroke; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; +import javax.swing.undo.AbstractUndoableEdit; +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; +import org.jhotdraw.draw.Figure; +import org.opentcs.guing.base.model.AbstractConnectableModelComponent; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.SimpleFolder; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.common.application.ComponentsManager; +import org.opentcs.guing.common.components.EditableComponent; +import org.opentcs.guing.common.components.tree.elements.LayoutUserObject; +import org.opentcs.guing.common.components.tree.elements.SimpleFolderUserObject; +import org.opentcs.guing.common.components.tree.elements.UserObject; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.DeleteAction; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.UndoRedoManager; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Abstract implementation of a tree view to display model components in a tree. + */ +public abstract class AbstractTreeViewPanel + extends + JPanel + implements + TreeView, + EditableComponent { + + /** + * The undo manger. + */ + protected final UndoRedoManager fUndoRedoManager; + /** + * + */ + protected final List<Figure> bufferedFigures = new ArrayList<>(); + /** + * + */ + protected List<UserObject> bufferedUserObjects = new ArrayList<>(); + /** + * The root node. + */ + private SortableTreeNode fRootNode; + /** + * The model for the JTree. + */ + private DefaultTreeModel fTreeModel; + /** + * The model manager. + */ + private final ModelManager modelManager; + /** + * The components manager. + */ + private final ComponentsManager componentsManager; + + /** + * Creates a new instance. + * + * @param undoRedoManager The undo redo manager + * @param modelManager The model manager. + * @param componentsManager The components manager. + */ + @Inject + @SuppressWarnings("this-escape") + public AbstractTreeViewPanel( + UndoRedoManager undoRedoManager, + ModelManager modelManager, + ComponentsManager componentsManager + ) { + this.fUndoRedoManager = requireNonNull(undoRedoManager, "undoRedoManager"); + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.componentsManager = requireNonNull(componentsManager, "componentsManager"); + + initComponents(); + objectTree.setCellRenderer(new TreeViewCellRenderer()); + + // Remove JTree's standard keyboard actions to enable the actions defined + // in ActionManager + ActionMap treeActionMap = objectTree.getActionMap(); + treeActionMap.getParent().remove("cut"); // <Ctrl> + X + treeActionMap.getParent().remove("copy"); // <Ctrl> + C + treeActionMap.getParent().remove("paste"); // <Ctrl> + V + treeActionMap.getParent().remove("duplicate"); // <Ctrl> + D + treeActionMap.getParent().remove("selectAll"); // <Ctrl> + A + // Add a keyboard handler for the "Delete" action + InputMap inputMap = objectTree.getInputMap(); + inputMap.getParent().put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), DeleteAction.ID); + inputMap.getParent().put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), DeleteAction.ID); + treeActionMap.getParent().put(DeleteAction.ID, new DeleteAction()); + } + + @Override + public JTree getTree() { + return objectTree; + } + + /** + * Copy the selected tree components and the associated figures in the drawing + * to abuffer. + * + * @param doDelete true if the original object will be deleted after + * copying it to the buffer + */ + protected void bufferSelectedItems(boolean doDelete) { + if (objectTree.getSelectionPaths() != null) { + bufferedUserObjects.clear(); + bufferedFigures.clear(); + + for (TreePath treePath : objectTree.getSelectionPaths()) { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) treePath.getLastPathComponent(); + UserObject userObject = (UserObject) node.getUserObject(); + boolean removed = false; + // Let the user object decide how to remove the item from the tree. + if (doDelete) { + removed = userObject.removed(); + } + + if (removed || !doDelete) { + bufferedUserObjects.add(userObject); + } + // Save deleted figures to allow undo-ing the delete operation + if (doDelete) { + ModelComponent modelComponent = userObject.getModelComponent(); + + if (modelComponent instanceof AbstractConnectableModelComponent) { + bufferedFigures.add(modelManager.getModel().getFigure(modelComponent)); + } + } + } + } + } + + @Override + public boolean hasBufferedObjects() { + return !bufferedFigures.isEmpty() || !bufferedUserObjects.isEmpty(); + } + + protected void restoreItems(List<UserObject> userObjects, List<Figure> figures) { + // Restore deleted model components + bufferedUserObjects = componentsManager.restoreModelComponents(userObjects); + // Restore the figures associated with these model components + bufferedFigures.clear(); + + for (Figure figure : figures) { + bufferedFigures.add(figure); + } + } + + @Override + public synchronized void sortItems(TreeNode treeNode) { + SortableTreeNode sortable = (SortableTreeNode) treeNode; + // Sort children recursively. + + @SuppressWarnings("unchecked") + Enumeration<TreeNode> en = sortable.children(); + + while (en.hasMoreElements()) { + TreeNode node = en.nextElement(); + + if (node.getChildCount() > 0) { + sortItems(node); + } + } + + if (sortable.isSorting()) { + if (sortable.getChildCount() > 0) { + sortable.sort(createSortComparator()); + } + } + + int size = treeNode.getChildCount(); + boolean[] expanded = new boolean[size]; + + for (int i = 0; i < size; i++) { + DefaultMutableTreeNode child = (DefaultMutableTreeNode) treeNode.getChildAt(i); + expanded[i] = objectTree.isExpanded(new TreePath(child.getPath())); + } + + fTreeModel.reload(sortable); + + for (int i = 0; i < expanded.length; i++) { + DefaultMutableTreeNode child = (DefaultMutableTreeNode) treeNode.getChildAt(i); + objectTree.expandPath(new TreePath(child.getPath())); + } + } + + /** + * Searches the tree to find the node that is associated with the specified model component. + * + * @param dataObject the object to search for. + * @return the node that holds the dataObject. + */ + public DefaultMutableTreeNode findFirst(Object dataObject) { + DefaultMutableTreeNode searchNode = null; + + List<DefaultMutableTreeNode> children + = Collections.list(fRootNode.preorderEnumeration()).stream() + .map(treeNode -> (DefaultMutableTreeNode) treeNode) + .collect(Collectors.toList()); + + for (DefaultMutableTreeNode child : children) { + UserObject userObject = (UserObject) child.getUserObject(); + + // Select point and path "directly", not the entries in a block area. + if (dataObject != null && dataObject.equals(userObject.getModelComponent())) { + searchNode = child; + break; + } + } + + return searchNode; + } + + /** + * Returns the user object the user clicked on. + * + * @param e The mouse event. + * @return The clicked user object or null, if none was found. + */ + @Override + public UserObject getDraggedUserObject(MouseEvent e) { + return getUserObject(); + } + + @Override + public void sortRoot() { + sortItems((TreeNode) objectTree.getModel().getRoot()); + } + + @Override + public void sortChildren() { + List<DefaultMutableTreeNode> children + = Collections.list(fRootNode.preorderEnumeration()).stream() + .map(treeNode -> (DefaultMutableTreeNode) treeNode) + .collect(Collectors.toList()); + + for (DefaultMutableTreeNode child : children) { + sortItems(child); + } + } + + @Override // TreeView + public void addItem(Object parent, UserObject item) { + if (parent == null) { + setRoot(item); + } + else { + SimpleFolder folder; + boolean sorting = true; + + if (item instanceof LayoutUserObject) { + sorting = false; + } + + SortableTreeNode treeItem = createTreeNode(item, sorting); + DefaultMutableTreeNode parentItem = findFirst(parent); + + if (parent instanceof LayoutModel) { + + folder = ((SimpleFolder) ((SimpleFolderUserObject) item).getModelComponent()); + + if (Objects.equals( + folder, + modelManager.getModel().getMainFolder(POINTS) + )) { + fTreeModel.insertNodeInto(treeItem, parentItem, 0); + } + else if (Objects.equals( + folder, + modelManager.getModel().getMainFolder(PATHS) + )) { + insertElementAt(treeItem, parentItem, 1); + } + else if (Objects.equals( + folder, + modelManager.getModel().getMainFolder(LOCATIONS) + )) { + insertElementAt(treeItem, parentItem, 2); + } + else if (Objects.equals( + folder, + modelManager.getModel().getMainFolder(LOCATION_TYPES) + )) { + insertElementAt(treeItem, parentItem, 3); + } + else if (Objects.equals( + folder, + modelManager.getModel().getMainFolder(LINKS) + )) { + insertElementAt(treeItem, parentItem, 4); + } + else if (Objects.equals( + folder, + modelManager.getModel().getMainFolder(OTHER_GRAPHICAL_ELEMENTS) + )) { + insertElementAt(treeItem, parentItem, 7); + } + } + else { + if (parentItem == null) { + return; + } + fTreeModel.insertNodeInto(treeItem, parentItem, parentItem.getChildCount()); + if (parent instanceof ModelComponent) { + item.setParent((ModelComponent) parent); + } + } + + objectTree.scrollPathToVisible(new TreePath(treeItem.getPath())); + } + } + + @Override // TreeView + public void removeItem(Object item) { + List<DefaultMutableTreeNode> myList = (item instanceof UserObject) + ? findAll(((UserObject) item)) + : findAll(item); + + for (DefaultMutableTreeNode node : myList) { + fTreeModel.removeNodeFromParent(node); + } + } + + @Override // TreeView + public void removeChildren(Object item) { + DefaultMutableTreeNode node = findFirst(item); + int size = node.getChildCount(); + + for (int i = size - 1; i > -1; i--) { + fTreeModel.removeNodeFromParent((DefaultMutableTreeNode) node.getChildAt(i)); + } + } + + @Override // TreeView + public void selectItem(Object item) { + DefaultMutableTreeNode itemToSelect = findFirst(item); + + if (itemToSelect == null) { + return; + } + + TreePath treePath = new TreePath(itemToSelect.getPath()); + objectTree.setSelectionPath(treePath); + objectTree.scrollPathToVisible(treePath); + } + + @Override + public void selectItems(Set<?> items) { + objectTree.removeSelectionPaths(objectTree.getSelectionPaths()); + + if (items == null) { + return; + } + for (Object item : items) { + DefaultMutableTreeNode itemToSelect = findFirst(item); + + if (itemToSelect == null) { + break; + } + + TreePath treePath = new TreePath(itemToSelect.getPath()); + objectTree.addSelectionPath(treePath); + } + } + + @Override // TreeView + public void itemChanged(Object item) { + for (DefaultMutableTreeNode node : findAll(item)) { + sortItems(node.getParent()); + } + } + + @Override // TreeView + public UserObject getSelectedItem() { + TreePath treePath = objectTree.getSelectionPath(); + + if (treePath != null) { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) treePath.getLastPathComponent(); + return (UserObject) node.getUserObject(); + } + else { + return null; + } + } + + @Override + public Set<UserObject> getSelectedItems() { + Set<UserObject> objects = new HashSet<>(); + TreePath[] selectionPaths = objectTree.getSelectionPaths(); + + if (selectionPaths != null) { + for (TreePath path : selectionPaths) { + if (path != null) { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); + objects.add((UserObject) node.getUserObject()); + } + } + } + + return objects; + } + + /** + * Updates the model name. + * + * @param text The new name. + */ + @Override + public void updateText(String text) { + textFieldModelName.setText(text); + } + + /** + * Adds an element at the specified index or the end if the parent item has fewer elements. + * + * @param treeItem The item to add. + * @param parentItem the parent item. + * @param index The index at which to add the item. + */ + private void insertElementAt( + SortableTreeNode treeItem, + DefaultMutableTreeNode parentItem, + int index + ) { + if (parentItem.getChildCount() < index) { + fTreeModel.insertNodeInto(treeItem, parentItem, parentItem.getChildCount()); + } + else { + fTreeModel.insertNodeInto(treeItem, parentItem, index); + } + } + + private void setRoot(UserObject root) { + fRootNode = new SortableTreeNode(root); + // The root node should not be re-sorted. + fRootNode.setSorting(false); + fTreeModel = new DefaultTreeModel(fRootNode); + objectTree.setModel(fTreeModel); + } + + /** + * Creates a new tree node. + * + * @param item The item the node should represent. + * @param sorting Whether or not the child components should be sorted. + * @return The new tree node. + */ + private SortableTreeNode createTreeNode(UserObject item, boolean sorting) { + SortableTreeNode treeNode = new SortableTreeNode(item); + treeNode.setSorting(sorting); + + return treeNode; + } + + /** + * Creates a new comparator to sort the tree. + * + * @return a new comparator to sort the tree. + */ + private Comparator<Object> createSortComparator() { + return new AscendingTreeViewNameComparator(); + } + + /** + * Searches the tree to find all the nodes that are associated with the specified data object. + * + * @param dataObject The object to search for. + * @return A list of all tree nodes that are associated with the data object. + */ + private List<DefaultMutableTreeNode> findAll(Object dataObject) { + List<DefaultMutableTreeNode> searchNodes = new ArrayList<>(); + + List<DefaultMutableTreeNode> children + = Collections.list(fRootNode.preorderEnumeration()).stream() + .map(treeNode -> (DefaultMutableTreeNode) treeNode) + .collect(Collectors.toList()); + + for (DefaultMutableTreeNode child : children) { + UserObject userObject = (UserObject) child.getUserObject(); + + if (dataObject.equals(userObject.getModelComponent())) { + searchNodes.add(child); + } + } + + return searchNodes; + } + + /** + * Looks for nodes that contain the given user object. + * + * @param o The user object to look for. + * @return All nodes that contain this user object. + */ + private List<DefaultMutableTreeNode> findAll(UserObject o) { + List<DefaultMutableTreeNode> searchNodes = new ArrayList<>(); + + List<DefaultMutableTreeNode> children + = Collections.list(fRootNode.preorderEnumeration()).stream() + .map(treeNode -> (DefaultMutableTreeNode) treeNode) + .collect(Collectors.toList()); + + for (DefaultMutableTreeNode child : children) { + UserObject userObject = (UserObject) child.getUserObject(); + + if (userObject == o) { + searchNodes.add(child); + } + } + + return searchNodes; + } + + /** + * Return the user object for the selected path. + * + * @return the user object for the selected path. + */ + private UserObject getUserObject() { + DefaultMutableTreeNode treeNode + = (DefaultMutableTreeNode) objectTree.getLastSelectedPathComponent(); + + return treeNode != null ? (UserObject) treeNode.getUserObject() : null; + } + + /** + * Called by delete(): Undo/Redo the "Delete" action. + */ + protected class DeleteEdit + extends + AbstractUndoableEdit { + + private final List<UserObject> userObjects = new ArrayList<>(); + private final List<Figure> figures = new ArrayList<>(); + + public DeleteEdit(List<UserObject> userObjects, List<Figure> figures) { + this.userObjects.addAll(userObjects); + this.figures.addAll(figures); + } + + @Override + public String getPresentationName() { + return ResourceBundleUtil.getBundle(I18nPlantOverview.MENU_PATH) + .getString("abstractTreeViewPanel.deleteEdit.presentationName"); + } + + @Override + public void undo() + throws CannotUndoException { + super.undo(); + restoreItems(userObjects, figures); + } + + @Override + public void redo() + throws CannotRedoException { + super.redo(); + // TODO: Delete again ... + for (UserObject userObject : userObjects) { + userObject.removed(); + } + } + } + + /** + * Called by paste(): Undo/Redo the "Paste" action + */ + protected class PasteEdit + extends + AbstractUndoableEdit { + + private final List<UserObject> userObjects = new ArrayList<>(); + private final List<Figure> figures = new ArrayList<>(); + + public PasteEdit(List<UserObject> userObjects, List<Figure> figures) { + this.userObjects.addAll(userObjects); + this.figures.addAll(figures); + } + + @Override + public String getPresentationName() { + return ResourceBundleUtil.getBundle(I18nPlantOverview.MENU_PATH) + .getString("abstractTreeViewPanel.pasteEdit.presentationName"); + } + + @Override + public void undo() + throws CannotUndoException { + super.undo(); + + for (UserObject userObject : userObjects) { + userObject.removed(); + + Figure figure = modelManager.getModel().getFigure(userObject.getModelComponent()); + if (figure != null) { + figures.add(figure); + } + } + } + + @Override + public void redo() + throws CannotRedoException { + super.redo(); + restoreItems(userObjects, figures); + } + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + scrollPaneTree = new javax.swing.JScrollPane(); + objectTree = new StandardActionTree(this); + textFieldModelName = new javax.swing.JTextField(); + + setLayout(new java.awt.BorderLayout()); + + scrollPaneTree.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS); + scrollPaneTree.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); + + objectTree.setRootVisible(false); + scrollPaneTree.setViewportView(objectTree); + + add(scrollPaneTree, java.awt.BorderLayout.CENTER); + + textFieldModelName.setEditable(false); + textFieldModelName.setBackground(new java.awt.Color(153, 153, 255)); + textFieldModelName.setFont(textFieldModelName.getFont().deriveFont(textFieldModelName.getFont().getStyle() | java.awt.Font.BOLD)); + textFieldModelName.setForeground(new java.awt.Color(255, 255, 255)); + textFieldModelName.setText("Model"); + add(textFieldModelName, java.awt.BorderLayout.PAGE_START); + }// </editor-fold>//GEN-END:initComponents + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JTree objectTree; + private javax.swing.JScrollPane scrollPaneTree; + private javax.swing.JTextField textFieldModelName; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/AscendingTreeViewNameComparator.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/AscendingTreeViewNameComparator.java new file mode 100644 index 0000000..1a83166 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/AscendingTreeViewNameComparator.java @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree; + +import java.util.Comparator; + +/** + * Compares two elements of a tree view for sorting. + * Sorts based on their name in descending order. + */ +public class AscendingTreeViewNameComparator + implements + Comparator<Object> { + + /** + * Creates a new instance. + */ + public AscendingTreeViewNameComparator() { + } + + @Override + public int compare(Object o1, Object o2) { + String s1 = o1.toString(); + String s2 = o2.toString(); + s1 = s1.toLowerCase(); + s2 = s2.toLowerCase(); + + return s1.compareTo(s2); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/BlockMouseListener.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/BlockMouseListener.java new file mode 100644 index 0000000..80171a6 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/BlockMouseListener.java @@ -0,0 +1,192 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree; + +import static java.util.Objects.requireNonNull; + +import com.google.common.collect.Lists; +import jakarta.inject.Inject; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.Figure; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.common.application.ApplicationState; +import org.opentcs.guing.common.application.OperationMode; +import org.opentcs.guing.common.components.drawing.figures.FigureConstants; +import org.opentcs.guing.common.components.tree.elements.UserObject; +import org.opentcs.guing.common.util.BlockSelector; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + */ +public class BlockMouseListener + extends + TreeMouseAdapter { + + /** + * Stores the application's current state. + */ + private final ApplicationState appState; + /** + * The application's drawing editor. + */ + private final DrawingEditor drawingEditor; + /** + * A helper for selecting blocks/block elements. + */ + private final BlockSelector blockSelector; + /** + * The affected models. + */ + private List<ModelComponent> fAffectedModels; + + /** + * Creates a new instance. + * + * @param appState The application state + * @param drawingEditor The drawing editor + * @param treeView The tree view + * @param blockSelector A helper for selecting blocks/block elements. + */ + @Inject + public BlockMouseListener( + ApplicationState appState, + DrawingEditor drawingEditor, + TreeView treeView, + BlockSelector blockSelector + ) { + super(treeView); + this.appState = requireNonNull(appState, "appState"); + this.drawingEditor = requireNonNull(drawingEditor, "drawingEditor"); + this.blockSelector = requireNonNull(blockSelector, "blockSelector"); + } + + @Override + protected void evaluateRightClick( + MouseEvent e, + UserObject userObject, + Set<UserObject> oldSelection + ) { + JPopupMenu menu = new JPopupMenu(); + ResourceBundleUtil labels = ResourceBundleUtil.getBundle(I18nPlantOverview.TREEVIEW_PATH); + + ModelComponent modelComponent = userObject.getModelComponent(); + if (modelComponent instanceof BlockModel) { + final BlockModel blockModel = (BlockModel) modelComponent; + JMenuItem item = new JMenuItem( + labels.getString("blockMouseListener.popupMenuItem_addToBlock.text") + ); + item.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent event) { + execute(); + addToBlock(blockModel); + } + }); + + item.setEnabled(appState.hasOperationMode(OperationMode.MODELLING)); + menu.add(item); + + item = new JMenuItem( + labels.getString("blockMouseListener.popupMenuItem_removeFromBlock.text") + ); + item.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent event) { + execute(); + removeFromBlock(blockModel); + } + }); + + item.setEnabled(appState.hasOperationMode(OperationMode.MODELLING)); + menu.add(item); + + menu.addSeparator(); + + item = new JMenuItem( + labels.getString("blockMouseListener.popupMenuItem_selectAllElements.text") + ); + item.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent event) { + blockSelector.blockSelected(blockModel); + } + }); + + menu.add(item); + + menu.show(e.getComponent(), e.getX(), e.getY()); + } + } + + private void execute() { + fAffectedModels = new ArrayList<>(); + + for (Figure figure : drawingEditor.getActiveView().getSelectedFigures()) { + fAffectedModels.add(figure.get(FigureConstants.MODEL)); + } + + List<ModelComponent> suitableModels = new ArrayList<>(); + for (ModelComponent model : fAffectedModels) { + if (isModelOk(model)) { + suitableModels.add(model); + } + } + fAffectedModels = suitableModels; + } + + /** + * Adds all affected models to the block. + */ + private void addToBlock(BlockModel modelComponent) { + for (ModelComponent model : fAffectedModels) { + if (!modelComponent.contains(model) && !(model instanceof LinkModel)) { + modelComponent.addCourseElement(model); + } + } + + modelComponent.courseElementsChanged(); + } + + /** + * Removes all affected models from the block. + */ + private void removeFromBlock(BlockModel modelComponent) { + for (ModelComponent cmp : new ArrayList<>(Lists.reverse(fAffectedModels))) { + modelComponent.removeCourseElement(cmp); + } + + modelComponent.courseElementsChanged(); + } + + /** + * Checks whether the given model can be added to a block. + * + * @param model The model to be checked. + * @return <code>true</code> if, and only if, the given model can be added to + * a block. + */ + private static boolean isModelOk(ModelComponent model) { + return (model != null + && (model instanceof PointModel + || model instanceof LocationModel + || model instanceof PathModel + || model instanceof LinkModel)); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/BlocksTreeViewManager.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/BlocksTreeViewManager.java new file mode 100644 index 0000000..9d8fa75 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/BlocksTreeViewManager.java @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree; + +import jakarta.inject.Inject; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import javax.swing.JTree; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.common.components.tree.elements.UserObjectContext; +import org.opentcs.guing.common.components.tree.elements.UserObjectUtil; + +/** + * A tree view manager for blocks. + */ +public class BlocksTreeViewManager + extends + TreeViewManager { + + @Inject + @SuppressWarnings("this-escape") + public BlocksTreeViewManager( + TreeView treeView, + UserObjectUtil userObjectUtil, + MouseListener mouseListener + ) { + super(treeView, userObjectUtil, mouseListener); + + // If the user clicks on an element in the tree view that is contained in several blocks, + // then we don't want to select the first element but instead the element in the block that + // the user clicked on. + initSpecializedSelector(); + } + + @Override + public void addItem(Object parent, ModelComponent item) { + if (item.isTreeViewVisible()) { + UserObjectContext context = userObjectUtil.createContext(UserObjectContext.ContextType.BLOCK); + getTreeView().addItem(parent, userObjectUtil.createUserObject(item, context)); + } + } + + /** + * Registers a listener for mouse events that helps to select the element in the tree view that + * the user has clicked on. + * This is necessary as long as {@link AbstractTreeViewPanel#selectItem(java.lang.Object)} simply + * selects the first occurrence of the given object, not expecting the object to occur in the tree + * more than once. + */ + private void initSpecializedSelector() { + JTree tree = getTreeView().getTree(); + tree.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + tree.setSelectionPath(tree.getClosestPathForLocation(e.getX(), e.getY())); + } + }); + } + +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/BlocksTreeViewPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/BlocksTreeViewPanel.java new file mode 100644 index 0000000..3f088f9 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/BlocksTreeViewPanel.java @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree; + +import jakarta.inject.Inject; +import java.util.HashSet; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.common.application.ComponentsManager; +import org.opentcs.guing.common.components.tree.elements.UserObject; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.UndoRedoManager; + +/** + * A TreeViewPanel for blocks. + */ +public class BlocksTreeViewPanel + extends + AbstractTreeViewPanel { + + /** + * Creates a new instance. + * + * @param undoRedoManager The undo redo manager + */ + @Inject + public BlocksTreeViewPanel( + UndoRedoManager undoRedoManager, + ModelManager modelManager, + ComponentsManager componentsManager + ) { + super(undoRedoManager, modelManager, componentsManager); + } + + @Override // EditableComponent + public void cutSelectedItems() { + bufferSelectedItems(true); + } + + @Override // EditableComponent + public void copySelectedItems() { + bufferSelectedItems(false); + } + + @Override // EditableComponent + public void pasteBufferedItems() { + restoreItems(bufferedUserObjects, bufferedFigures); + // Also make "Paste" undoable + fUndoRedoManager.addEdit( + new AbstractTreeViewPanel.PasteEdit(bufferedUserObjects, bufferedFigures) + ); + } + + @Override // EditableComponent + public void duplicate() { + bufferSelectedItems(false); + restoreItems(bufferedUserObjects, bufferedFigures); + fUndoRedoManager.addEdit( + new AbstractTreeViewPanel.PasteEdit(bufferedUserObjects, bufferedFigures) + ); + } + + @Override // EditableComponent + public void delete() { + bufferSelectedItems(true); + + if (bufferedUserObjects.isEmpty() && bufferedFigures.isEmpty()) { + return; // nothing to undo/redo + } + + fUndoRedoManager.addEdit( + new AbstractTreeViewPanel.DeleteEdit(bufferedUserObjects, bufferedFigures) + ); + } + + @Override // EditableComponent + public void selectAll() { + // Sample implementation (HH 2014-04-08): + // Select all components in the currently focused tree folder + // TODO: select all components except folders + UserObject selectedItem = getSelectedItem(); + + if (selectedItem != null) { + ModelComponent parent = selectedItem.getParent(); + + if (parent != null) { + selectItems(new HashSet<>(parent.getChildComponents())); + } + } + } + + @Override // EditableComponent + public void clearSelection() { + // Not used for our tree: + // JTree's default action for <Ctrl> + <Shift> + A already does the job. + } + + @Override // EditableComponent + public boolean isSelectionEmpty() { + // Not used for tree ? + return true; + } + +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/ComponentsTreeViewManager.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/ComponentsTreeViewManager.java new file mode 100644 index 0000000..dfaba96 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/ComponentsTreeViewManager.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree; + +import jakarta.inject.Inject; +import java.awt.event.MouseListener; +import javax.swing.tree.TreeSelectionModel; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.common.components.tree.elements.UserObjectContext; +import org.opentcs.guing.common.components.tree.elements.UserObjectUtil; + +/** + * The tree view manager for components. + */ +public class ComponentsTreeViewManager + extends + TreeViewManager { + + /** + * Creates a new instance. + * + * @param treeView The tree view + * @param userObjectUtil The user object util + * @param mouseListener The mouse listener + */ + @Inject + public ComponentsTreeViewManager( + TreeView treeView, + UserObjectUtil userObjectUtil, + MouseListener mouseListener + ) { + super(treeView, userObjectUtil, mouseListener); + treeView.getTree().getSelectionModel().setSelectionMode( + TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION + ); + } + + @Override + public void addItem(Object parent, ModelComponent item) { + if (item.isTreeViewVisible()) { + UserObjectContext context + = userObjectUtil.createContext(UserObjectContext.ContextType.COMPONENT); + getTreeView().addItem(parent, userObjectUtil.createUserObject(item, context)); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/ComponentsTreeViewPanel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/ComponentsTreeViewPanel.java new file mode 100644 index 0000000..c24d54d --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/ComponentsTreeViewPanel.java @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree; + +import jakarta.inject.Inject; +import java.util.HashSet; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.common.application.ComponentsManager; +import org.opentcs.guing.common.components.tree.elements.UserObject; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit.UndoRedoManager; + +/** + * The TreeViewPanel for components. + */ +public class ComponentsTreeViewPanel + extends + AbstractTreeViewPanel { + + /** + * Creates a new instance. + * + * @param undoRedoManager The undo redo manager + */ + @Inject + public ComponentsTreeViewPanel( + UndoRedoManager undoRedoManager, + ModelManager modelManager, + ComponentsManager componentsManager + ) { + super(undoRedoManager, modelManager, componentsManager); + } + + @Override // EditableComponent + public void cutSelectedItems() { + bufferSelectedItems(true); + } + + @Override // EditableComponent + public void copySelectedItems() { + bufferSelectedItems(false); + } + + @Override // EditableComponent + public void pasteBufferedItems() { + restoreItems(bufferedUserObjects, bufferedFigures); + // Also make "Paste" undoable + fUndoRedoManager.addEdit( + new AbstractTreeViewPanel.PasteEdit(bufferedUserObjects, bufferedFigures) + ); + } + + @Override // EditableComponent + public void duplicate() { + bufferSelectedItems(false); + restoreItems(bufferedUserObjects, bufferedFigures); + fUndoRedoManager.addEdit( + new AbstractTreeViewPanel.PasteEdit(bufferedUserObjects, bufferedFigures) + ); + } + + @Override // EditableComponent + public void delete() { + bufferSelectedItems(true); + + if (bufferedUserObjects.isEmpty() && bufferedFigures.isEmpty()) { + return; // nothing to undo/redo + } + + fUndoRedoManager.addEdit( + new AbstractTreeViewPanel.DeleteEdit(bufferedUserObjects, bufferedFigures) + ); + } + + @Override // EditableComponent + public void selectAll() { + // Sample implementation (HH 2014-04-08): + // Select all components in the currently focused tree folder + // TODO: select all components except folders + UserObject selectedItem = getSelectedItem(); + + if (selectedItem != null) { + ModelComponent parent = selectedItem.getParent(); + + if (parent != null) { + selectItems(new HashSet<>(parent.getChildComponents())); + } + } + } + + @Override // EditableComponent + public void clearSelection() { + // Not used for our tree: + // JTree's default action for <Ctrl> + <Shift> + A already does the job. + } + + @Override // EditableComponent + public boolean isSelectionEmpty() { + // Not used for tree ? + return true; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/SortableTreeNode.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/SortableTreeNode.java new file mode 100644 index 0000000..3490ee5 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/SortableTreeNode.java @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree; + +import java.util.Collections; +import java.util.Comparator; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreeNode; +import org.slf4j.LoggerFactory; + +/** + * A tree node that can be sorted. + */ +public class SortableTreeNode + extends + DefaultMutableTreeNode { + + /** + * True if the child elements of this node can be sorted. + */ + private boolean fSorting; + + /** + * Creates a new instance. + * + * @param userObject The UserObject. + */ + public SortableTreeNode(Object userObject) { + super(userObject); + setSorting(true); + } + + /** + * Sets whether or not the child elements of this node can be sorted. + * + * @param sorting True if the child elements of this node can be sorted. + */ + public final void setSorting(boolean sorting) { + fSorting = sorting; + } + + /** + * Returns true if the child elements of this node can be sorted. + * + * @return True if the child elements of this node can be sorted. + */ + public boolean isSorting() { + return fSorting; + } + + /** + * Sort the child elements. + * + * @param comparator The comparator to be used for sorting. + */ + @SuppressWarnings("unchecked") + public void sort(Comparator<Object> comparator) { + Collections.sort(children, comparator); + } + + @Override + public TreeNode getChildAt(int index) { + try { + return super.getChildAt(index); + } + catch (ArrayIndexOutOfBoundsException e) { + // XXX remove if never observed + LoggerFactory.getLogger(SortableTreeNode.class) + .error("Exception while calling SortableTreeNode.getChildAt()", e); + return null; + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/StandardActionTree.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/StandardActionTree.java new file mode 100644 index 0000000..933523f --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/StandardActionTree.java @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree; + +import org.opentcs.guing.common.components.EditableComponent; + +/** + * A JTree which delegates actions to it's parent. + */ +public class StandardActionTree + extends + javax.swing.JTree + implements + EditableComponent { + + private final EditableComponent parent; + + /** + * Creates a new instance. + * + * @param parent The parent editable component + */ + public StandardActionTree(EditableComponent parent) { + this.parent = parent; + } + + @Override + public void cutSelectedItems() { + parent.cutSelectedItems(); + } + + @Override + public void copySelectedItems() { + parent.copySelectedItems(); + } + + @Override + public void pasteBufferedItems() { + parent.pasteBufferedItems(); + } + + @Override + public void delete() { + parent.delete(); + } + + @Override + public void duplicate() { + parent.duplicate(); + } + + @Override + public void selectAll() { + parent.selectAll(); + } + + /** + * Note: EditableComponent.clearSelection() must _not_ be overridden + * since the method JTree.clearSelection() is called in JTree's constructor. + */ +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/TreeMouseAdapter.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/TreeMouseAdapter.java new file mode 100644 index 0000000..b37e3c9 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/TreeMouseAdapter.java @@ -0,0 +1,178 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree; + +import jakarta.inject.Inject; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.Enumeration; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.common.components.tree.elements.AbstractUserObject; +import org.opentcs.guing.common.components.tree.elements.UserObject; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * A mouse adapter for the <code>TreeView</code> for components and blocks. + */ +public class TreeMouseAdapter + extends + MouseAdapter { + + /** + * The TreeView this mouse adapter belongs to. + */ + protected final TreeView treeView; + + /** + * Creates a new instance. + * + * @param treeView The tree view + */ + @Inject + public TreeMouseAdapter(TreeView treeView) { + this.treeView = Objects.requireNonNull(treeView); + } + + @Override + public void mousePressed(MouseEvent e) { + JTree objectTree = treeView.getTree(); + TreePath selPath = objectTree.getPathForLocation(e.getX(), e.getY()); + if (selPath == null) { + if (e.getButton() == MouseEvent.BUTTON3) { + showPopupMenu(e.getX(), e.getY()); + } + } + else { + UserObject userObject = getUserObject(selPath); + + if (e.getButton() == MouseEvent.BUTTON3) { + evaluateRightClick(e, userObject, treeView.getSelectedItems()); + } + else if (e.getButton() == MouseEvent.BUTTON1) { + + //This Method tells the OpenTCSView what elements are currently selected + ((AbstractUserObject) userObject).selectMultipleObjects(); + + if (e.getClickCount() == 2) { + userObject.doubleClicked(); + } + + } + + } + + } + + /** + * Evaluates a right click the user made on an user object. + * + * @param e The MouseEvent. + * @param userObject The user object that was right clicked. + * @param currentSelection The user objects that were selected. + */ + protected void evaluateRightClick( + MouseEvent e, + UserObject userObject, + Set<UserObject> currentSelection + ) { + + if (!currentSelection.contains(userObject)) { + currentSelection.clear(); + currentSelection.add(userObject); + } + + Set<ModelComponent> dataObjects = currentSelection.stream() + .map(userObj -> userObj.getModelComponent()) + .collect(Collectors.toSet()); + + treeView.selectItems(dataObjects); + + userObject.rightClicked(treeView.getTree(), e.getX(), e.getY()); + } + + private UserObject getUserObject(TreePath path) { + return (UserObject) ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject(); + } + + /** + * Shows a popup menu with options for the JTree. + * + * @param x x coordinate. + * @param y y coordinate. + */ + private void showPopupMenu(int x, int y) { + JPopupMenu menu = new JPopupMenu(); + final JTree objectTree = treeView.getTree(); + ResourceBundleUtil labels = ResourceBundleUtil.getBundle(I18nPlantOverview.TREEVIEW_PATH); + + JMenuItem item = new JMenuItem( + labels.getString("treeMouseAdapter.popupMenuItem_expandAllFolders.text") + ); + item.setToolTipText( + labels.getString("treeMouseAdapter.popupMenuItem_expandAllFolders.tooltipText") + ); + + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + for (int i = 0; i < objectTree.getRowCount(); i++) { + objectTree.expandRow(i); + } + } + }); + + menu.add(item); + + item = new JMenuItem(labels.getString("treeMouseAdapter.popupMenuItem_closeAllFolders.text")); + item.setToolTipText( + labels.getString("treeMouseAdapter.popupMenuItem_closeAllFolders.tooltipText") + ); + + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + for (int i = 0; i < objectTree.getRowCount(); i++) { + objectTree.collapseRow(i); + } + } + }); + + menu.add(item); + + menu.addSeparator(); + + item = new JMenuItem(labels.getString("treeMouseAdapter.popupMenuItem_sortAllItems.text")); + item.setToolTipText( + labels.getString("treeMouseAdapter.popupMenuItem_sortAllItems.tooltipText") + ); + + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + Enumeration<? extends TreeNode> eTreeNodes + = ((TreeNode) objectTree.getModel().getRoot()).children(); + + while (eTreeNodes.hasMoreElements()) { + TreeNode node = eTreeNodes.nextElement(); + treeView.sortItems(node); + } + } + }); + + menu.add(item); + + menu.show(objectTree, x, y); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/TreeView.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/TreeView.java new file mode 100644 index 0000000..88d6db1 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/TreeView.java @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree; + +import java.awt.Cursor; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.util.Set; +import javax.swing.JTree; +import javax.swing.tree.TreeNode; +import org.opentcs.guing.common.components.tree.elements.UserObject; + +/** + * A TreeView manages a model which has a set of TreeNode objects. + * Each TreeNode has a UserObject. Each UserObject wraps a real object + * (e.g. Article, Figure, ...). It knows which methods to call to when an object is selected + * in the tree or is deleted or is double-clicked. + * + * @see UserObject + */ +public interface TreeView { + + /** + * Adds an item to the tree. + * + * @param parent A real object that is seached in the tree. + * @param item Is a UserObject. + */ + void addItem(Object parent, UserObject item); + + /** + * Sorts the children of the given node. + * + * @param treeNode The node whose children shall be sorted. + */ + void sortItems(TreeNode treeNode); + + /** + * Returns the <code>JTree</code> that actually holds the objects. + * + * @return The tree. + */ + JTree getTree(); + + /** + * Adds the given listener to the <code>JTree</code>. + * + * @param mouseListener The listener. + */ + void addMouseListener(MouseListener mouseListener); + + /** + * Adds the given motion listener to the <code>JTree</code>. + * + * @param mouseMotionListener The motion listener. + */ + void addMouseMotionListener(MouseMotionListener mouseMotionListener); + + /** + * Updates the text at the top of the <code>JTree</code>. + * + * @param text The new text. + */ + void updateText(String text); + + /** + * Returns whether the tree has buffered objects. + * + * @return <code>true</code> if it has some. + */ + boolean hasBufferedObjects(); + + /** + * Returns the dragged user object. + * + * @param e The event where the mouse click happened. + * @return The user object that was dragged. + */ + UserObject getDraggedUserObject(MouseEvent e); + + /** + * Sets the cursor of the <code>JTree</code>. + * + * @param cursor The new cursor. + */ + void setCursor(Cursor cursor); + + /** + * Removes an item from the tree. + * + * @param item A real object. + */ + void removeItem(Object item); + + /** + * Removes all child elements from an item. + * + * @param item The item to remove child elements from. + */ + void removeChildren(Object item); + + /** + * Selects an item in the tree. + * + * @param item The item to select. + */ + void selectItem(Object item); + + /** + * Selects a set of items in the tree. + * + * @param items The set of items to select. + */ + void selectItems(Set<?> items); + + /** + * Notifies the tree that a property of the specified item has changed. + * + * @param item + */ + void itemChanged(Object item); + + /** + * Return the selected item. + * + * @return the selected item. + */ + UserObject getSelectedItem(); + + /** + * Return the selected items. + * + * @return the selected items. + */ + Set<UserObject> getSelectedItems(); + + /** + * Sorts the root element of the tree. + */ + void sortRoot(); + + /** + * Sorts all children. + */ + void sortChildren(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/TreeViewCellRenderer.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/TreeViewCellRenderer.java new file mode 100644 index 0000000..bb77e55 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/TreeViewCellRenderer.java @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree; + +import java.awt.Component; +import javax.swing.ImageIcon; +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import org.opentcs.guing.common.components.tree.elements.UserObject; + +/** + * A cell renderer for a node in the tree view. + */ +public class TreeViewCellRenderer + extends + DefaultTreeCellRenderer { + + /** + * Creates a new instance. + */ + public TreeViewCellRenderer() { + super(); + } + + @Override + public Component getTreeCellRendererComponent( + JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, + int row, boolean hasFocus + ) { + + super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); + + DefaultMutableTreeNode node = (DefaultMutableTreeNode) value; + Object userObject = node.getUserObject(); + + if (userObject instanceof UserObject) { + ImageIcon icon = ((UserObject) userObject).getIcon(); + + if (icon != null) { + setIcon(icon); + } + } + + return this; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/TreeViewManager.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/TreeViewManager.java new file mode 100644 index 0000000..8e6f056 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/TreeViewManager.java @@ -0,0 +1,275 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.awt.Cursor; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; +import javax.swing.tree.TreeNode; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.common.components.tree.elements.UserObject; +import org.opentcs.guing.common.components.tree.elements.UserObjectUtil; +import org.opentcs.guing.common.event.ModelNameChangeEvent; +import org.opentcs.util.event.EventHandler; + +/** + * The tree view manager is the interface between the application and the TreeView. + * It passes actions on to the TreeView. + * + * @see TreeView + */ +public abstract class TreeViewManager + implements + EventHandler { + + /** + * A factory for UserObjects. + */ + protected final UserObjectUtil userObjectUtil; + /** + * The tree view. + */ + private final TreeView fTreeView; + /** + * This manager's component filter. + */ + private Predicate<ModelComponent> componentFilter = (component) -> true; + + /** + * Creates a new instance. + * + * @param treeView The actual tree view. + * @param userObjectUtil A factory for UserObjects. + * @param mouseListener The MouseListener for the TreeView. + */ + @Inject + public TreeViewManager( + TreeView treeView, + UserObjectUtil userObjectUtil, + MouseListener mouseListener + ) { + this.fTreeView = requireNonNull(treeView, "treeView is null"); + this.userObjectUtil = requireNonNull(userObjectUtil, "userObjectUtil"); + requireNonNull(mouseListener, "mouseListener"); + fTreeView.getTree().addMouseListener(mouseListener); + } + + public TreeView getTreeView() { + return fTreeView; + } + + /** + * Delegates the call to the <code>TreeView</code>. + * + * @param mouseListener The Listener to add. + */ + public void addMouseListener(MouseListener mouseListener) { + getTreeView().addMouseListener(mouseListener); + } + + /** + * Delegates the call to the <code>TreeView</code>. + * + * @param mouseListener The Listener to add. + */ + public void addMouseMotionListener(MouseMotionListener mouseListener) { + getTreeView().addMouseMotionListener(mouseListener); + } + + /** + * Sorts the items of the <code>TreeView</code>. + */ + public void sortItems() { + Enumeration<? extends TreeNode> eTreeNodes + = ((TreeNode) getTreeView().getTree().getModel().getRoot()).children(); + + while (eTreeNodes.hasMoreElements()) { + getTreeView().sortItems(eTreeNodes.nextElement()); + } + } + + @Override + public void onEvent(Object event) { + if (event instanceof ModelNameChangeEvent) { + updateModelName((ModelNameChangeEvent) event); + } + } + + /** + * Updates the text at the top of the <code>TreeView</code>. + * + * @param event The <code>ModelNameChangeEvent</code>. + */ + private void updateModelName(ModelNameChangeEvent event) { + // Updates the text at the top of the tree view. + String newName = event.getNewName(); + getTreeView().updateText(newName); + } + + /** + * Returns whether the <code>TreeView</code> has buffered objects. + * + * @return <code>true</code> if it has some. + */ + public boolean hasBufferedObjects() { + return getTreeView().hasBufferedObjects(); + } + + /** + * Returns the dragged user object of the <code>TreeView</code>. + * + * @param e The event where the mouse click happened. + * @return The user object that was dragged. + */ + public UserObject getDraggedUserObject(MouseEvent e) { + return getTreeView().getDraggedUserObject(e); + } + + /** + * Sets the cursor in the <code>TreeView</code>. + * + * @param cursor The new cursor. + */ + public void setCursor(Cursor cursor) { + getTreeView().setCursor(cursor); + } + + /** + * Returns the currently selected item. + * + * @return The currently selected item. + */ + public ModelComponent getSelectedItem() { + UserObject userObject = fTreeView.getSelectedItem(); + + if (userObject != null) { + return userObject.getModelComponent(); + } + else { + return null; + } + } + + /** + * Returns the currently selected items. + * + * @return The currently selected items. + */ + public Set<ModelComponent> getSelectedItems() { + Set<ModelComponent> components = new HashSet<>(); + + for (UserObject object : fTreeView.getSelectedItems()) { + components.add(object.getModelComponent()); + } + + return components; + } + + /** + * Creates the tree view from the specified model component. + * + * @param component The component to restore. + */ + public void restoreTreeView(ModelComponent component) { + restoreTreeViewRecursively(null, component); + } + + public void setComponentFilter(Predicate<ModelComponent> componentFilter) { + this.componentFilter = requireNonNull(componentFilter, "componentFilter"); + } + + private boolean accepts(ModelComponent component) { + return componentFilter.test(component); + } + + /** + * Add an item to the TreeView. + * + * @param parent The parent item to add to. + * @param item The item to add. + */ + public abstract void addItem(Object parent, ModelComponent item); + + /** + * Notifies the TreeView that the item has changed. + * + * @param item the item that was changed. + */ + public void itemChanged(Object item) { + fTreeView.itemChanged(item); + } + + /** + * Notifies the TreeView that an item has been deleted. + * + * @param item The item that has been deleted. + */ + public void removeItem(Object item) { + fTreeView.removeItem(item); + } + + /** + * Notifies the TreeView that all child elements of the specified element should be deleted. + * + * @param item The item of which to delete its children. + */ + public void removeChildren(Object item) { + fTreeView.removeChildren(item); + } + + /** + * Select an item in the TreeView. + * + * @param component The component to select. + */ + public void selectItem(ModelComponent component) { + if (component != null && component.isTreeViewVisible()) { + fTreeView.selectItem(component); + Set<ModelComponent> comps = new HashSet<>(1); + comps.add(component); + } + } + + /** + * Selects multiple items in the tree view. + * + * @param components The components to select. Pass <code>null</code> + * to deselect everything currently selected. + */ + public void selectItems(Set<ModelComponent> components) { + Set<ModelComponent> visibleComponents = new HashSet<>(); + + if (components != null) { + for (ModelComponent comp : components) { + if (comp.isTreeViewVisible()) { + visibleComponents.add(comp); + } + } + } + + fTreeView.selectItems(visibleComponents); + } + + private void restoreTreeViewRecursively(ModelComponent parent, ModelComponent item) { + if (accepts(item)) { + addItem(parent, item); + } + + for (ModelComponent child : item.getChildComponents()) { + if (!child.getChildComponents().isEmpty()) { + restoreTreeViewRecursively(item, child); + } + else if (accepts(child)) { + addItem(item, child); + } + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/AbstractUserObject.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/AbstractUserObject.java new file mode 100644 index 0000000..8097d1c --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/AbstractUserObject.java @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree.elements; + +import static java.util.Objects.requireNonNull; + +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.JPopupMenu; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * Abstract implementation of a UserObject. + */ +public abstract class AbstractUserObject + implements + UserObject { + + /** + * The model component this user object represents. + */ + private final ModelComponent fModelComponent; + /** + * The gui manager. + */ + private final GuiManager guiManager; + /** + * The model manager. + */ + private final ModelManager modelManager; + /** + * The parent object. + */ + private ModelComponent parent; + + /** + * Creates a new instance. + * + * @param modelComponent The corresponding model component. + * @param guiManager The gui manager. + * @param modelManager Provides access to the currently loaded system model. + */ + public AbstractUserObject( + ModelComponent modelComponent, + GuiManager guiManager, + ModelManager modelManager + ) { + this.fModelComponent = requireNonNull(modelComponent, "modelComponent"); + this.guiManager = requireNonNull(guiManager, "guiManager"); + this.modelManager = requireNonNull(modelManager, "modelManager"); + } + + @Override // Object + public String toString() { + return fModelComponent.getTreeViewName(); + } + + @Override // UserObject + public ModelComponent getModelComponent() { + return fModelComponent; + } + + @Override // UserObject + public void selected() { + getGuiManager().selectModelComponent(getModelComponent()); + } + + @Override // UserObject + public boolean removed() { + return getGuiManager().treeComponentRemoved(fModelComponent); + } + + @Override // UserObject + public void rightClicked(JComponent component, int x, int y) { + JPopupMenu popupMenu = getPopupMenu(); + if (popupMenu != null) { + popupMenu.show(component, x, y); + } + } + + @Override // UserObject + public void doubleClicked() { + } + + @Override // UserObject + public JPopupMenu getPopupMenu() { + return new JPopupMenu(); + } + + @Override // UserObject + public ImageIcon getIcon() { + return null; + } + + /** + * Is called when multiple objects in a tree are selected. + */ + public void selectMultipleObjects() { + getGuiManager().addSelectedModelComponent(getModelComponent()); + } + + @Override + public ModelComponent getParent() { + return parent; + } + + @Override + public void setParent(ModelComponent parent) { + this.parent = parent; + } + + protected GuiManager getGuiManager() { + return guiManager; + } + + protected ModelManager getModelManager() { + return modelManager; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/BlockContext.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/BlockContext.java new file mode 100644 index 0000000..2d8326a --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/BlockContext.java @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree.elements; + +import jakarta.inject.Inject; +import java.util.Set; +import javax.swing.JPopupMenu; +import org.opentcs.guing.base.model.elements.BlockModel; + +/** + * Context for the block tree view. + */ +public class BlockContext + implements + UserObjectContext { + + /** + * Creates a new instance. + */ + @Inject + public BlockContext() { + } + + @Override + public JPopupMenu getPopupMenu(Set<UserObject> selectedUserObjects) { + JPopupMenu menu = new JPopupMenu(); + + return menu; + } + + @Override + public boolean removed(UserObject userObject) { + if (userObject.getParent() instanceof BlockModel) { + BlockModel blockModel = (BlockModel) userObject.getParent(); + blockModel.removeCourseElement(userObject.getModelComponent()); + + blockModel.courseElementsChanged(); + + return true; + } + return false; + } + + @Override + public ContextType getType() { + return ContextType.BLOCK; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/BlockUserObject.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/BlockUserObject.java new file mode 100644 index 0000000..0813a81 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/BlockUserObject.java @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree.elements; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import javax.swing.ImageIcon; +import javax.swing.JPopupMenu; +import org.opentcs.data.model.Block; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.BlockSelector; +import org.opentcs.guing.common.util.IconToolkit; + +/** + * A Block object in the tree view. + * + * @see Block + */ +public class BlockUserObject + extends + AbstractUserObject + implements + ContextObject { + + private final UserObjectContext context; + /** + * A helper for selecting blocks/block elements. + */ + private final BlockSelector blockSelector; + + /** + * Creates a new instance. + * + * @param dataObject The corresponding model component + * @param context The user object context + * @param guiManager The gui manager. + * @param modelManager The model manager + * @param blockSelector A helper for selecting blocks/block elements. + */ + @Inject + public BlockUserObject( + @Assisted + BlockModel dataObject, + @Assisted + UserObjectContext context, + GuiManager guiManager, + ModelManager modelManager, + BlockSelector blockSelector + ) { + super(dataObject, guiManager, modelManager); + this.context = requireNonNull(context, "context"); + this.blockSelector = requireNonNull(blockSelector, "blockSelector"); + } + + @Override + public JPopupMenu getPopupMenu() { + JPopupMenu menu = context.getPopupMenu(null); + + return menu; + } + + @Override + public BlockModel getModelComponent() { + return (BlockModel) super.getModelComponent(); + } + + @Override + public void doubleClicked() { + blockSelector.blockSelected(getModelComponent()); + } + + @Override + public ImageIcon getIcon() { + return IconToolkit.instance().createImageIcon("tree/block.18x18.png"); + } + + @Override + public UserObjectContext.ContextType getContextType() { + return context.getType(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/ComponentContext.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/ComponentContext.java new file mode 100644 index 0000000..917ec9d --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/ComponentContext.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree.elements; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Set; +import javax.swing.JPopupMenu; +import org.opentcs.guing.common.application.GuiManager; + +/** + * Context for the component tree view. + */ +public class ComponentContext + implements + UserObjectContext { + + private final GuiManager guiManager; + + /** + * Creates a new instance. + * + * @param guiManager The gui manager. + */ + @Inject + public ComponentContext(GuiManager guiManager) { + this.guiManager = requireNonNull(guiManager, "guiManager"); + } + + @Override + public JPopupMenu getPopupMenu(final Set<UserObject> selectedUserObjects) { + JPopupMenu menu = new JPopupMenu(); + return menu; + } + + @Override + public boolean removed(UserObject userObject) { + return guiManager.treeComponentRemoved(userObject.getModelComponent()); + } + + @Override + public ContextType getType() { + return ContextType.COMPONENT; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/ContextObject.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/ContextObject.java new file mode 100644 index 0000000..9a8c9fb --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/ContextObject.java @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree.elements; + +/** + */ +public interface ContextObject { + + UserObjectContext.ContextType getContextType(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/FigureUserObject.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/FigureUserObject.java new file mode 100644 index 0000000..7aa255b --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/FigureUserObject.java @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree.elements; + +import java.util.HashSet; +import java.util.Set; +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreePath; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.IconToolkit; + +/** + * Represents a Figure component in the TreeView. + */ +public class FigureUserObject + extends + AbstractUserObject { + + /** + * All selected user objects. + */ + protected Set<UserObject> userObjectItems; + + /** + * Creates a new instance. + * + * @param modelComponent The corresponding data object + * @param guiManager The gui manager. + * @param modelManager The model manager + */ + public FigureUserObject( + ModelComponent modelComponent, + GuiManager guiManager, + ModelManager modelManager + ) { + super(modelComponent, guiManager, modelManager); + } + + @Override // AbstractUserObject + public String toString() { + return getModelComponent().getDescription() + " " + + getModelComponent().getName(); + } + + @Override // AbstractUserObject + public void doubleClicked() { + getGuiManager().figureSelected(getModelComponent()); + } + + @Override // AbstractUserObject + public ImageIcon getIcon() { + return IconToolkit.instance().createImageIcon("tree/figure.18x18.png"); + } + + @Override // UserObject + public void rightClicked(JComponent component, int x, int y) { + userObjectItems = getSelectedUserObjects(((JTree) component)); + super.rightClicked(component, x, y); + } + + /** + * Returns the selected user objects in the tree. + * + * @param objectTree The tree to find the selected items. + * @return All selected user objects. + */ + private Set<UserObject> getSelectedUserObjects(JTree objectTree) { + Set<UserObject> objects = new HashSet<>(); + TreePath[] selectionPaths = objectTree.getSelectionPaths(); + + if (selectionPaths != null) { + for (TreePath path : selectionPaths) { + if (path != null) { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); + if (node.getUserObject() instanceof FigureUserObject) { + objects.add((UserObject) node.getUserObject()); + } + } + } + } + + return objects; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/LayoutUserObject.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/LayoutUserObject.java new file mode 100644 index 0000000..f807714 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/LayoutUserObject.java @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree.elements; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * Represents a point object in the TreeView. + */ +public class LayoutUserObject + extends + FigureUserObject { + + /** + * Creates a new instance. + * + * @param model The corresponding data object + * @param guiManager The gui manager. + * @param modelManager The model manager + */ + @Inject + public LayoutUserObject( + @Assisted + LayoutModel model, + GuiManager guiManager, + ModelManager modelManager + ) { + super(model, guiManager, modelManager); + } + + @Override + public LayoutModel getModelComponent() { + return (LayoutModel) super.getModelComponent(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/LinkUserObject.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/LinkUserObject.java new file mode 100644 index 0000000..a0cfe59 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/LinkUserObject.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree.elements; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import javax.swing.ImageIcon; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.IconToolkit; + +/** + * Represents a link in the TreeView. + */ +public class LinkUserObject + extends + FigureUserObject { + + /** + * Creates a new instance of LinkUserObject + * + * @param modelComponent The corresponding data object + * @param guiManager The gui manager. + * @param modelManager The model manager + */ + @Inject + public LinkUserObject( + @Assisted + LinkModel modelComponent, + GuiManager guiManager, + ModelManager modelManager + ) { + super(modelComponent, guiManager, modelManager); + } + + @Override + public LinkModel getModelComponent() { + return (LinkModel) super.getModelComponent(); + } + + @Override + public ImageIcon getIcon() { + return IconToolkit.instance().createImageIcon("tree/link.18x18.png"); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/LocationTypeUserObject.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/LocationTypeUserObject.java new file mode 100644 index 0000000..d4f8cae --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/LocationTypeUserObject.java @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree.elements; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import javax.swing.ImageIcon; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.IconToolkit; + +/** + * Represents a location type in the TreeView. + */ +public class LocationTypeUserObject + extends + AbstractUserObject { + + /** + * Creates a new instance of StationUserObject + * + * @param modelComponent + * @param guiManager The gui manager. + * @param modelManager The model manager + */ + @Inject + public LocationTypeUserObject( + @Assisted + LocationTypeModel modelComponent, + GuiManager guiManager, + ModelManager modelManager + ) { + super(modelComponent, guiManager, modelManager); + } + + @Override + public LocationTypeModel getModelComponent() { + return (LocationTypeModel) super.getModelComponent(); + } + + @Override + public ImageIcon getIcon() { + return IconToolkit.instance().createImageIcon("tree/locationType.18x18.png"); + } + + @Override + public void doubleClicked() { + getGuiManager().figureSelected(getModelComponent()); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/LocationUserObject.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/LocationUserObject.java new file mode 100644 index 0000000..f596bdf --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/LocationUserObject.java @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree.elements; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.util.Objects; +import javax.swing.ImageIcon; +import javax.swing.JPopupMenu; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.IconToolkit; + +/** + * Represents a location in the TreeView. + */ +public class LocationUserObject + extends + FigureUserObject + implements + ContextObject { + + private final UserObjectContext context; + + /** + * Creates a new instance. + * + * @param model The corresponding model object + * @param context The user object context + * @param guiManager The gui manager. + * @param modelManager The model manager + */ + @Inject + public LocationUserObject( + @Assisted + LocationModel model, + @Assisted + UserObjectContext context, + GuiManager guiManager, + ModelManager modelManager + ) { + super(model, guiManager, modelManager); + this.context = Objects.requireNonNull(context, "context"); + } + + @Override + public LocationModel getModelComponent() { + return (LocationModel) super.getModelComponent(); + } + + @Override // AbstractUserObject + public JPopupMenu getPopupMenu() { + JPopupMenu menu = context.getPopupMenu(userObjectItems); + + return menu; + } + + @Override // FigureUserObject + public ImageIcon getIcon() { + return IconToolkit.instance().createImageIcon("tree/location.18x18.png"); + } + + @Override + public boolean removed() { + return context.removed(this); + } + + @Override + public UserObjectContext.ContextType getContextType() { + return context.getType(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/NullContext.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/NullContext.java new file mode 100644 index 0000000..3dc5624 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/NullContext.java @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree.elements; + +import java.util.Set; +import javax.swing.JPopupMenu; + +/** + * A null context. Use this when no special context is required. + */ +public class NullContext + implements + UserObjectContext { + + /** + * Creates a new instance. + */ + public NullContext() { + } + + @Override + public JPopupMenu getPopupMenu(Set<UserObject> selectedUserObjects) { + return new JPopupMenu(); + } + + @Override + public boolean removed(UserObject userObject) { + return true; + } + + @Override + public ContextType getType() { + return ContextType.NULL; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/PathUserObject.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/PathUserObject.java new file mode 100644 index 0000000..181e254 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/PathUserObject.java @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree.elements; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import javax.swing.ImageIcon; +import javax.swing.JPopupMenu; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.IconToolkit; + +/** + * Represents a path in the TreeView. + */ +public class PathUserObject + extends + FigureUserObject + implements + ContextObject { + + private final UserObjectContext context; + + /** + * Creates a new instance. + * + * @param model The corresponding model object + * @param context The user object context + * @param guiManager The gui manager. + * @param modelManager The model manager + */ + @Inject + public PathUserObject( + @Assisted + PathModel model, + @Assisted + UserObjectContext context, + GuiManager guiManager, + ModelManager modelManager + ) { + super(model, guiManager, modelManager); + this.context = context; + } + + @Override // AbstractUserObject + public JPopupMenu getPopupMenu() { + JPopupMenu menu = context.getPopupMenu(userObjectItems); + + return menu; + } + + @Override // FigureUserObject + public ImageIcon getIcon() { + return IconToolkit.instance().createImageIcon("tree/path.18x18.png"); + } + + @Override + public boolean removed() { + return context.removed(this); + } + + @Override + public UserObjectContext.ContextType getContextType() { + return context.getType(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/PointUserObject.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/PointUserObject.java new file mode 100644 index 0000000..6922ee2 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/PointUserObject.java @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree.elements; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.util.Objects; +import javax.swing.ImageIcon; +import javax.swing.JPopupMenu; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.IconToolkit; + +/** + * Represents a point in the TreeView. + */ +public class PointUserObject + extends + FigureUserObject + implements + ContextObject { + + private final UserObjectContext context; + + /** + * Creates a new instance. + * + * @param model The corresponding model object + * @param context The user object context + * @param guiManager The gui manager. + * @param modelManager The model manager + */ + @Inject + public PointUserObject( + @Assisted + PointModel model, + @Assisted + UserObjectContext context, + GuiManager guiManager, + ModelManager modelManager + ) { + super(model, guiManager, modelManager); + this.context = Objects.requireNonNull(context, "context"); + } + + @Override + public PointModel getModelComponent() { + return (PointModel) super.getModelComponent(); + } + + @Override // AbstractUserObject + public JPopupMenu getPopupMenu() { + JPopupMenu menu = context.getPopupMenu(userObjectItems); + + return menu; + } + + @Override + public ImageIcon getIcon() { + return IconToolkit.instance().createImageIcon("tree/point.18x18.png"); + } + + @Override + public boolean removed() { + return context.removed(this); + } + + @Override + public UserObjectContext.ContextType getContextType() { + return context.getType(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/SimpleFolderUserObject.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/SimpleFolderUserObject.java new file mode 100644 index 0000000..d6235d2 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/SimpleFolderUserObject.java @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree.elements; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import javax.swing.JComponent; +import org.opentcs.guing.base.model.CompositeModelComponent; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * A folder in the TreeView with no added functionality. + */ +public class SimpleFolderUserObject + extends + AbstractUserObject { + + /** + * Creates a new instance. + * + * @param dataObject The associated model component. + * @param guiManager The gui manager. + * @param modelManager Provides access to the currently loaded system model. + */ + @Inject + public SimpleFolderUserObject( + @Assisted + CompositeModelComponent dataObject, + GuiManager guiManager, + ModelManager modelManager + ) { + super(dataObject, guiManager, modelManager); + } + + @Override // AbstractUserObject + public boolean removed() { + return false; + } + + @Override // AbstractUserObject + public void rightClicked(JComponent component, int x, int y) { + // Empty - no popup menu to be displayed. + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/UserObject.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/UserObject.java new file mode 100644 index 0000000..c2901bb --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/UserObject.java @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree.elements; + +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.JPopupMenu; +import org.opentcs.guing.base.model.ModelComponent; + +/** + * A UserObject has the purpose of representing a model component in the TreeView. + * It manages its model component and is responsible for executing user interactions e.g. selecting, + * deleting, double clicking. + * + * @see ModelComponent + */ +public interface UserObject { + + /** + * Returns the wrapped model component. + * + * @return the wrapped model component. + */ + ModelComponent getModelComponent(); + + /** + * Return the popup menu for the model component. + * + * @return the popup menu for the model component. + */ + JPopupMenu getPopupMenu(); + + /** + * Return the icon for this user object. + * + * @return the icon for this user object. + */ + ImageIcon getIcon(); + + /** + * Is called when the object is selected in the tree view. + */ + void selected(); + + /** + * Is called when this object is removed from the tree view. + * + * @return + */ + boolean removed(); + + /** + * Is called when the object was right clicked in the tree view. + * + * @param component + * @param x X position of the mouse click. + * @param y Y position of the mouse click. + */ + void rightClicked(JComponent component, int x, int y); + + /** + * Is called when the object is double clicked in the tree view. + */ + void doubleClicked(); + + /** + * Returns the parent component that contains this user object. + * (Typically a <code>SimpleFolder</code> + * + * @return The parent <code>ModelComponent</code>. + */ + ModelComponent getParent(); + + /** + * Sets the parent component. + * + * @param parent The parent. + */ + void setParent(ModelComponent parent); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/UserObjectContext.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/UserObjectContext.java new file mode 100644 index 0000000..c85d272 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/UserObjectContext.java @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree.elements; + +import jakarta.annotation.Nullable; +import java.util.Set; +import javax.swing.JPopupMenu; + +/** + * A context indicating if an user object is contained in + * the components, blocks or groups tree view. Currently it only + * offers a tree dependant popup menu. + */ +public interface UserObjectContext { + + /** + * Returns a popup menu with actions for this context. + * + * @param selectedUserObjects The user objects that are currently selected + * in the tree view. + * @return A popup menu. + */ + JPopupMenu getPopupMenu( + @Nullable + Set<UserObject> selectedUserObjects + ); + + /** + * Called after a specific item was removed from the tree (via the <code> + * DeleteAction</code>. + * + * @param userObject The UserObject affected. + * @return <code>true</code>, if it was successfully removed. + */ + boolean removed(UserObject userObject); + + /** + * Returns the type of this context. + * + * @return One of CONTEXT_TYPE. + */ + ContextType getType(); + + /** + * Supported context types. + */ + enum ContextType { + + /** + * Component. + */ + COMPONENT, + /** + * Block. + */ + BLOCK, + /** + * Null context. + */ + NULL; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/UserObjectFactory.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/UserObjectFactory.java new file mode 100644 index 0000000..d46ecc1 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/UserObjectFactory.java @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree.elements; + +import org.opentcs.guing.base.model.CompositeModelComponent; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; + +/** + */ +public interface UserObjectFactory { + + BlockUserObject createBlockUserObject(BlockModel model, UserObjectContext context); + + LayoutUserObject createLayoutUserObject(LayoutModel model); + + LinkUserObject createLinkUserObject(LinkModel model); + + LocationTypeUserObject createLocationTypeUserObject(LocationTypeModel model); + + LocationUserObject createLocationUserObject(LocationModel model, UserObjectContext context); + + PathUserObject createPathUserObject(PathModel model, UserObjectContext context); + + PointUserObject createPointUserObject(PointModel model, UserObjectContext context); + + SimpleFolderUserObject createSimpleFolderUserObject(CompositeModelComponent model); + + VehicleUserObject createVehicleUserObject(VehicleModel model); + + ComponentContext createComponentContext(); + + BlockContext createBlockContext(); + + NullContext createNullContext(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/UserObjectUtil.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/UserObjectUtil.java new file mode 100644 index 0000000..85248dd --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/UserObjectUtil.java @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree.elements; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.guing.base.model.CompositeModelComponent; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.tree.elements.UserObjectContext.ContextType; + +/** + */ +public class UserObjectUtil { + + private final UserObjectFactory factory; + + /** + * Creates a new instance. + * + * @param factory A factory for user objects. + */ + @Inject + public UserObjectUtil(UserObjectFactory factory) { + this.factory = requireNonNull(factory, "factory"); + } + + public UserObject createUserObject(ModelComponent model, UserObjectContext context) { + requireNonNull(model, "model"); + + if (model instanceof BlockModel) { + return factory.createBlockUserObject((BlockModel) model, context); + } + else if (model instanceof LayoutModel) { + return factory.createLayoutUserObject((LayoutModel) model); + } + else if (model instanceof LinkModel) { + return factory.createLinkUserObject((LinkModel) model); + } + else if (model instanceof LocationTypeModel) { + return factory.createLocationTypeUserObject((LocationTypeModel) model); + } + else if (model instanceof LocationModel) { + return factory.createLocationUserObject((LocationModel) model, context); + } + else if (model instanceof PathModel) { + return factory.createPathUserObject((PathModel) model, context); + } + else if (model instanceof PointModel) { + return factory.createPointUserObject((PointModel) model, context); + } + else if (model instanceof VehicleModel) { + return factory.createVehicleUserObject((VehicleModel) model); + } + else if (model instanceof CompositeModelComponent) { + return factory.createSimpleFolderUserObject((CompositeModelComponent) model); + } + + throw new IllegalArgumentException( + "Unhandled component class " + + model.getClass() + ); + } + + public UserObjectContext createContext(ContextType type) { + switch (type) { + case COMPONENT: + return factory.createComponentContext(); + case BLOCK: + return factory.createBlockContext(); + default: + return factory.createNullContext(); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/VehicleUserObject.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/VehicleUserObject.java new file mode 100644 index 0000000..bfd2ffb --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/components/tree/elements/VehicleUserObject.java @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.tree.elements; + +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import java.util.HashSet; +import java.util.Set; +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreePath; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.application.GuiManager; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.IconToolkit; + +/** + * Represents a vehicle object in the TreeView. + */ +public class VehicleUserObject + extends + AbstractUserObject { + + /** + * All selected vehicles. + */ + protected Set<VehicleModel> selectedVehicles; + + /** + * Creates a new instance. + * + * @param model The corresponding vehicle object. + * @param guiManager The gui manager. + * @param modelManager Provides the current system model. + */ + @Inject + public VehicleUserObject( + @Assisted + VehicleModel model, + GuiManager guiManager, + ModelManager modelManager + ) { + super(model, guiManager, modelManager); + } + + @Override + public VehicleModel getModelComponent() { + return (VehicleModel) super.getModelComponent(); + } + + @Override // AbstractUserObject + public void doubleClicked() { + getGuiManager().figureSelected(getModelComponent()); + } + + @Override // UserObject + public void rightClicked(JComponent component, int x, int y) { + selectedVehicles = getSelectedVehicles(((JTree) component)); + super.rightClicked(component, x, y); + } + + @Override // AbstractUserObject + public ImageIcon getIcon() { + return IconToolkit.instance().createImageIcon("tree/vehicle.18x18.png"); + } + + /** + * Returns the selected vehicle models in the tree. + * + * @param objectTree The tree to find the selected items. + * @return All selected vehicle models. + */ + private Set<VehicleModel> getSelectedVehicles(JTree objectTree) { + Set<VehicleModel> objects = new HashSet<>(); + TreePath[] selectionPaths = objectTree.getSelectionPaths(); + + if (selectionPaths != null) { + for (TreePath path : selectionPaths) { + if (path != null) { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); + //vehicles can only be selected with other vehicles + if (node.getUserObject() instanceof VehicleUserObject) { + objects.add((VehicleModel) ((UserObject) node.getUserObject()).getModelComponent()); + } + } + } + } + + return objects; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/DrawingEditorEvent.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/DrawingEditorEvent.java new file mode 100644 index 0000000..069e335 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/DrawingEditorEvent.java @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.event; + +import java.util.EventObject; +import java.util.List; +import java.util.Objects; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.Figure; + +/** + * Event object that a DrawingEditor fires when a Figure object was selected, + * added or removed. + * + * @see DrawingEditorListener + */ +public class DrawingEditorEvent + extends + EventObject { + + /** + * The affected Figure objects. + */ + private final List<Figure> fFigures; + + /** + * Creates a new instance. + * + * @param editor The event source. + * @param figures The affected figures. + */ + public DrawingEditorEvent(DrawingEditor editor, List<Figure> figures) { + super(editor); + fFigures = Objects.requireNonNull(figures); + } + + /** + * Creates a new instance for a single figure. + * + * @param editor The event source. + * @param figure The affected figure. + */ + public DrawingEditorEvent(DrawingEditor editor, Figure figure) { + super(editor); + fFigures = List.of(figure); + } + + /** + * Checks whether this event references at least one figure. + * + * @return <code>true</code> if, and only if, this event references at least + * one figure. + */ + public boolean hasFigure() { + return !fFigures.isEmpty(); + } + + /** + * Returns the originating DrawingEditor. + * + * @return The originating DrawingEditor. + */ + public DrawingEditor getDrawingEditor() { + return (DrawingEditor) getSource(); + } + + /** + * Returns the affected Figure objects. + * + * @return The affected Figure objects. + */ + public List<Figure> getFigures() { + return fFigures; + } + + /** + * Returns the first affected figure. + * + * @return The first affected figure. + */ + public Figure getFigure() { + if (!fFigures.isEmpty()) { + return fFigures.get(0); + } + else { + return null; + } + } + + /** + * Returns the number of affected figures. + * + * @return The number of affected figures. + */ + public int getCount() { + return fFigures.size(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/DrawingEditorListener.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/DrawingEditorListener.java new file mode 100644 index 0000000..8229ba9 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/DrawingEditorListener.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.event; + +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; + +/** + * An interface that has to be implemented by the application to receive + * events from the <code>DrawingEditor</code>. Events are: figure added, + * figure removed and figure selected. + * + * @see DrawingEditorEvent + */ +public interface DrawingEditorListener { + + /** + * Gets called when a figure is added in the {@link OpenTCSDrawingEditor} + * by an action of the user. + * + * @param e Event for when a figure is added to the OpenTCSDrawingEditor. + */ + void figureAdded(DrawingEditorEvent e); + + /** + * Gets called when a figure is removed by the user in the + * <code>OpenTCSDrawingEditor</code>. + * + * @param e The fired event. + */ + void figureRemoved(DrawingEditorEvent e); + + /** + * Gets called when a figure was selected in the {@link OpenTCSDrawingEditor}. + * + * @param e The fired event. + */ + void figureSelected(DrawingEditorEvent e); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/EventLogger.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/EventLogger.java new file mode 100644 index 0000000..c7812c6 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/EventLogger.java @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.event; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.Lifecycle; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.event.EventSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + */ +public class EventLogger + implements + EventHandler, + Lifecycle { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(EventLogger.class); + /** + * Where we register for events. + */ + private final EventSource eventSource; + /** + * Whether this component is initialized. + */ + private boolean initialized; + + @Inject + public EventLogger( + @ApplicationEventBus + EventSource eventSource + ) { + this.eventSource = requireNonNull(eventSource, "eventSource"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + eventSource.subscribe(this); + + initialized = true; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + eventSource.unsubscribe(this); + + initialized = false; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void onEvent(Object event) { + LOG.debug("Received event: {}", event); + } + +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/ModelNameChangeEvent.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/ModelNameChangeEvent.java new file mode 100644 index 0000000..d0bfaf8 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/ModelNameChangeEvent.java @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.event; + +import java.util.EventObject; + +/** + * An event that indicates that the model name has changed. + */ +public class ModelNameChangeEvent + extends + EventObject { + + private final String newName; + + /** + * Creates a new instance of PathLockedEvent. + * + * @param source The <code>PathConnection</code> that has been locked. + * @param newName The new name of the model. + */ + public ModelNameChangeEvent(Object source, String newName) { + super(source); + this.newName = newName; + } + + public String getNewName() { + return newName; + } + + @Override + public String toString() { + return "ModelNameChangeEvent{" + + "newName=" + newName + + ", source=" + getSource() + + '}'; + } + +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/OperationModeChangeEvent.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/OperationModeChangeEvent.java new file mode 100644 index 0000000..476803d --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/OperationModeChangeEvent.java @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.event; + +import static java.util.Objects.requireNonNull; + +import java.util.EventObject; +import org.opentcs.guing.common.application.OperationMode; + +/** + * Informs listeners about a change of the application's mode of operation. + */ +public class OperationModeChangeEvent + extends + EventObject { + + /** + * The old operation mode. + */ + private final OperationMode oldMode; + /** + * The new/current operation mode. + */ + private final OperationMode newMode; + + /** + * Creates a new instance. + * + * @param source The source of this event. + * @param oldMode The old operation mode. + * @param newMode The new/current operation mode. + */ + public OperationModeChangeEvent( + Object source, + OperationMode oldMode, + OperationMode newMode + ) { + super(source); + this.oldMode = requireNonNull(oldMode, "oldMode"); + this.newMode = requireNonNull(newMode, "newMode"); + } + + /** + * Returns the old operation mode. + * + * @return The old operation mode. + */ + public OperationMode getOldMode() { + return oldMode; + } + + /** + * Returns the new/current operation mode. + * + * @return The new/current operation mode. + */ + public OperationMode getNewMode() { + return newMode; + } + + @Override + public String toString() { + return "OperationModeChangeEvent{" + + "oldMode=" + oldMode + + ", newMode=" + newMode + + ", source=" + getSource() + + '}'; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/ResetInteractionToolCommand.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/ResetInteractionToolCommand.java new file mode 100644 index 0000000..28e196b --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/ResetInteractionToolCommand.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.event; + +import java.util.EventObject; + +/** + * This event instructs the receiver(s) to reset the currently selected + * interaction tool for the user. + */ +public class ResetInteractionToolCommand + extends + EventObject { + + /** + * Creates a new instance. + * + * @param source The event's originator. + */ + public ResetInteractionToolCommand(Object source) { + super(source); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/SystemModelTransitionEvent.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/SystemModelTransitionEvent.java new file mode 100644 index 0000000..b4fd6e9 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/event/SystemModelTransitionEvent.java @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.event; + +import static java.util.Objects.requireNonNull; + +import java.util.EventObject; +import org.opentcs.guing.common.model.SystemModel; + +/** + * Informs listeners about the current system model being replaced with a + * different one. + * For every stage of the transition from the current/old system model to the + * new one, a separate event is emitted. + */ +public class SystemModelTransitionEvent + extends + EventObject { + + /** + * The current stage of the transition. + */ + private final Stage stage; + /** + * The system model to which this event refers. + */ + private final SystemModel model; + + /** + * Creates a new instance. + * + * @param source The originator of this event. + * @param stage The current state of the transition. + * @param model The system model to which this event refers. + */ + public SystemModelTransitionEvent(Object source, Stage stage, SystemModel model) { + super(source); + this.stage = stage; + this.model = requireNonNull(model, "model"); + } + + /** + * Returns the current stage of the transition. + * + * @return The current stage of the transition. + */ + public Stage getStage() { + return stage; + } + + /** + * Returns the system model to which this event refers. + * + * @return The system model to which this event refers. + */ + public SystemModel getModel() { + return model; + } + + @Override + public String toString() { + return "SystemModelTransitionEvent{" + + "source=" + getSource() + ", " + + "stage=" + stage + ", " + + "model=" + model + + '}'; + } + + /** + * The possible stages of a transition. + */ + public enum Stage { + + /** + * Indicates the current system model is currently being unloaded. + */ + UNLOADING, + /** + * Indicates the current system model has been unloaded. + */ + UNLOADED, + /** + * Indicates the new system model is being loaded. + */ + LOADING, + /** + * Indicates the new system model has been loaded. + */ + LOADED; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/AllocationHistory.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/AllocationHistory.java new file mode 100644 index 0000000..ff0c850 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/AllocationHistory.java @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.exchange; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Vehicle; + +/** + * Keeps track of the resources claimed and allocated by vehicles. + */ +public class AllocationHistory { + + /** + * The resources currently and previously claimed and allocated by vehicles mapped to the + * respective vehicle's name. + */ + private final Map<String, Entry> history = new HashMap<>(); + + /** + * Creates a new instance. + */ + public AllocationHistory() { + } + + /** + * Updates the allocation history for the given vehicle and returns the updated history entry + * containing the vehicle's currently and previously claimed and allocated resources. + * <p> + * The currently allocated resources are divided into two lists, one for resources that still lie + * ahead for the vehicle's current drive order and one for resources that lie behind (including + * the resources for the route step last travelled by the vehicle). If the resources that lie + * ahead of the vehicle cannot be determined, all currently allocated resources are added to the + * 'behind' list. + * + * @param vehicle The vehicle. + * @return The updated history entry containing the vehicle's currently and previously claimed and + * allocated resources. + */ + @Nonnull + public Entry updateHistory( + @Nonnull + Vehicle vehicle + ) { + requireNonNull(vehicle, "vehicle"); + + Set<TCSResourceReference<?>> newClaimedResources = vehicle.getClaimedResources().stream() + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + SplitResources allocatedResources = SplitResources.from( + vehicle.getAllocatedResources(), + vehicle.getCurrentPosition() + ); + Set<TCSResourceReference<?>> newAllocatedResourcesBehind + = allocatedResources.getAllocatedResourcesBehind().stream() + .flatMap(Set::stream) + .collect(Collectors.toSet()); + Set<TCSResourceReference<?>> newAllocatedResourcesAhead + = allocatedResources.getAllocatedResourcesAhead().stream() + .flatMap(Set::stream) + .collect(Collectors.toSet()); + Set<TCSResourceReference<?>> noLongerClaimedOrAllocatedResources + = determineNoLongerClaimedOrAllocatedResources( + vehicle.getName(), + newClaimedResources, + newAllocatedResourcesAhead, + newAllocatedResourcesBehind + ); + + Entry newEntry = new Entry( + newClaimedResources, + newAllocatedResourcesAhead, + newAllocatedResourcesBehind, + noLongerClaimedOrAllocatedResources + ); + + history.put(vehicle.getName(), newEntry); + + return newEntry; + } + + private Set<TCSResourceReference<?>> determineNoLongerClaimedOrAllocatedResources( + String vehicleName, + Set<TCSResourceReference<?>> newCurrentClaimedResources, + Set<TCSResourceReference<?>> newCurrentAllocatedResourcesAhead, + Set<TCSResourceReference<?>> newCurrentAllocatedResourcesBehind + ) { + Set<TCSResourceReference<?>> result = new HashSet<>(); + result.addAll(getEntryFor(vehicleName).getCurrentClaimedResources()); + result.addAll(getEntryFor(vehicleName).getCurrentAllocatedResourcesAhead()); + result.addAll(getEntryFor(vehicleName).getCurrentAllocatedResourcesBehind()); + result.removeAll(newCurrentClaimedResources); + result.removeAll(newCurrentAllocatedResourcesAhead); + result.removeAll(newCurrentAllocatedResourcesBehind); + return result; + } + + private Entry getEntryFor(String vehicleName) { + return history.computeIfAbsent( + vehicleName, v -> new Entry(Set.of(), Set.of(), Set.of(), Set.of()) + ); + } + + /** + * An entry in the allocation history holding the resources currently and previously claimed and + * allocated by a vehicle. + */ + public static class Entry { + + private final Set<TCSResourceReference<?>> currentClaimedResources; + private final Set<TCSResourceReference<?>> currentAllocatedResourcesAhead; + private final Set<TCSResourceReference<?>> currentAllocatedResourcesBehind; + private final Set<TCSResourceReference<?>> previouslyClaimedOrAllocatedResources; + + Entry( + Set<TCSResourceReference<?>> currentClaimedResources, + Set<TCSResourceReference<?>> currentAllocatedResourcesAhead, + Set<TCSResourceReference<?>> currentAllocatedResourcesBehind, + Set<TCSResourceReference<?>> previouslyClaimedOrAllocatedResources + ) { + this.currentClaimedResources = requireNonNull( + currentClaimedResources, + "currentClaimedResources" + ); + this.currentAllocatedResourcesAhead = requireNonNull( + currentAllocatedResourcesAhead, + "currentAllocatedResourcesAhead" + ); + this.currentAllocatedResourcesBehind = requireNonNull( + currentAllocatedResourcesBehind, + "currentAllocatedResourcesBehind" + ); + this.previouslyClaimedOrAllocatedResources + = requireNonNull( + previouslyClaimedOrAllocatedResources, + "previouslyClaimedOrAllocatedResources" + ); + } + + public Set<TCSResourceReference<?>> getCurrentClaimedResources() { + return currentClaimedResources; + } + + public Set<TCSResourceReference<?>> getCurrentAllocatedResourcesAhead() { + return currentAllocatedResourcesAhead; + } + + public Set<TCSResourceReference<?>> getCurrentAllocatedResourcesBehind() { + return currentAllocatedResourcesBehind; + } + + public Set<TCSResourceReference<?>> getPreviouslyClaimedOrAllocatedResources() { + return previouslyClaimedOrAllocatedResources; + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/ApplicationPortal.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/ApplicationPortal.java new file mode 100644 index 0000000..337cfd0 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/ApplicationPortal.java @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.exchange; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkState; + +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.SharedKernelServicePortal; + +/** + * Implementation of the {@link SharedKernelServicePortal} interface to give access to a shared + * portal object. + */ +public class ApplicationPortal + implements + SharedKernelServicePortal { + + /** + * The portal. + */ + private final KernelServicePortal portal; + /** + * The portal provider instance that this client is registered at. + */ + private final ApplicationPortalProvider sharedPortalProvider; + /** + * The object registered with the provider. + */ + private final Object registeredToken; + /** + * Indicates whether this instance is closed or not. + */ + private boolean closed; + + /** + * Creates a new instance. + * + * @param portal The shared portal instance. + * @param portalProvider The provider this client is registered with. + * @param registeredToken The token that is actually registered with the provider. + */ + public ApplicationPortal( + KernelServicePortal portal, + ApplicationPortalProvider portalProvider, + Object registeredToken + ) { + this.portal = requireNonNull(portal, "portal"); + this.sharedPortalProvider = requireNonNull(portalProvider, "portalProvider"); + this.registeredToken = requireNonNull(registeredToken, "registeredToken"); + } + + @Override + public void close() { + if (isClosed()) { + return; + } + + sharedPortalProvider.unregister(registeredToken); + closed = true; + } + + @Override + public boolean isClosed() { + return closed; + } + + @Override + public KernelServicePortal getPortal() + throws IllegalStateException { + checkState(!isClosed(), "Closed already."); + + return portal; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/ApplicationPortalProvider.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/ApplicationPortalProvider.java new file mode 100644 index 0000000..c396938 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/ApplicationPortalProvider.java @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.exchange; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.HashSet; +import java.util.Set; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.common.PortalManager; +import org.opentcs.components.kernel.services.ServiceUnavailableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides an {@link ApplicationPortal} for clients in the kernel control center application. + */ +public class ApplicationPortalProvider + implements + SharedKernelServicePortalProvider { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ApplicationPortalProvider.class); + /** + * The registered clients. + */ + private final Set<Object> clients = new HashSet<>(); + /** + * The portal manager taking care of the portal connection. + */ + private final PortalManager portalManager; + /** + * The application's configuration. + */ + private final ApplicationPortalProviderConfiguration configuration; + + /** + * Creates a new instance. + * + * @param portalManager The portal manager taking care of the portal connection. + * @param configuration The application's configuration. + */ + @Inject + public ApplicationPortalProvider( + PortalManager portalManager, + ApplicationPortalProviderConfiguration configuration + ) { + this.portalManager = requireNonNull(portalManager, "ortalManager"); + this.configuration = requireNonNull(configuration, "configuration"); + } + + @Override + public SharedKernelServicePortal register() + throws ServiceUnavailableException { + Object token = new Object(); + if (!portalShared()) { + LOG.debug("Initiating portal connection for new client..."); + portalManager.connect(toConnectionMode(configuration.useBookmarksWhenConnecting())); + } + clients.add(token); + + if (!portalShared()) { + unregister(token); + throw new ServiceUnavailableException("Could not connect to portal"); + } + return new ApplicationPortal( + portalManager.getPortal(), + this, + token + ); + } + + public synchronized boolean unregister(Object client) { + requireNonNull(client, "client"); + + if (clients.remove(client)) { + if (clients.isEmpty()) { + LOG.debug("Last client left. Terminating portal connection..."); + portalManager.disconnect(); + } + return true; + } + return false; + } + + @Override + public synchronized boolean portalShared() { + return portalManager.isConnected(); + } + + @Override + public synchronized String getPortalDescription() { + return portalShared() + ? portalManager.getHost() + ":" + portalManager.getPort() : "-"; + } + + private PortalManager.ConnectionMode toConnectionMode(boolean autoConnect) { + return autoConnect ? PortalManager.ConnectionMode.AUTO : PortalManager.ConnectionMode.MANUAL; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/ApplicationPortalProviderConfiguration.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/ApplicationPortalProviderConfiguration.java new file mode 100644 index 0000000..14eaf15 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/ApplicationPortalProviderConfiguration.java @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.exchange; + +import java.util.List; +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.util.gui.dialog.ConnectionParamSet; + +/** + * Provides methods to configure the Model Editor and Operations Desk applications. + */ +public interface ApplicationPortalProviderConfiguration { + + @ConfigurationEntry( + type = "Comma-separated list of <description>\\|<hostname>\\|<port>", + description = "Kernel connection bookmarks to be used.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "1_connection_0" + ) + List<ConnectionParamSet> connectionBookmarks(); + + @ConfigurationEntry( + type = "Boolean", + description = { + "Whether to use the configured bookmarks when connecting to the kernel.", + "If 'true', the first connection bookmark will be used for the connection attempt.", + "If 'false', a dialog will be shown to enter connection parameters."}, + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "1_connection_1" + ) + boolean useBookmarksWhenConnecting(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/SplitResources.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/SplitResources.java new file mode 100644 index 0000000..fb30aa6 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/SplitResources.java @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.exchange; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.TCSResourceReference; + +/** + * An AllocationHistory entry's allocated resources, split into resources that lie behind the + * vehicle (including the resources for the vehicle's current position) and resources that still lie + * ahead of it. + */ +public class SplitResources { + + private final List<Set<TCSResourceReference<?>>> allocatedResourcesBehind; + private final List<Set<TCSResourceReference<?>>> allocatedResourcesAhead; + + /** + * Creates a new instance. + * + * @param allocatedResourcesBehind The resources behind the vehicle, including the ones + * for the vehicle's current position. + * @param allocatedResourcesAhead The resources ahead of the vehicle. + */ + public SplitResources( + @Nonnull + List<Set<TCSResourceReference<?>>> allocatedResourcesBehind, + @Nonnull + List<Set<TCSResourceReference<?>>> allocatedResourcesAhead + ) { + this.allocatedResourcesBehind = requireNonNull( + allocatedResourcesBehind, + "allocatedResourcesBehind" + ); + this.allocatedResourcesAhead = requireNonNull( + allocatedResourcesAhead, + "allocatedResourcesAhead" + ); + } + + /** + * Returns the resources behind the vehicle, including the ones for the vehicle's + * current step. + * + * @return The resources behind the vehicle, including the ones for the vehicle's + * current step. + */ + public List<Set<TCSResourceReference<?>>> getAllocatedResourcesBehind() { + return allocatedResourcesBehind; + } + + /** + * Returns the resources ahead of the vehicle. + * + * @return The resources ahead of the vehicle. + */ + public List<Set<TCSResourceReference<?>>> getAllocatedResourcesAhead() { + return allocatedResourcesAhead; + } + + /** + * Returns a new instance created from the given list of resource sets, split at the given + * delimiter. + * + * @param resourceSets The list of resource sets to be split in order of appearance in the route. + * @param delimiter The delimiter / vehicle's current position. + * @return A new instance created from the given list of resource sets, split at the given + * delimiter. If the delimiter is null, all given resources are added to allocatedResourcesBehind. + */ + public static SplitResources from( + @Nonnull + List<Set<TCSResourceReference<?>>> resourceSets, + TCSObjectReference<?> delimiter + ) { + requireNonNull(resourceSets, "resources"); + if (delimiter == null) { + return new SplitResources(resourceSets, List.of()); + } + + List<Set<TCSResourceReference<?>>> resourcesBehind = new ArrayList<>(); + List<Set<TCSResourceReference<?>>> resourcesAhead = new ArrayList<>(); + List<Set<TCSResourceReference<?>>> resourcesToPutIn = resourcesBehind; + + for (Set<TCSResourceReference<?>> curSet : resourceSets) { + resourcesToPutIn.add(curSet); + + if (curSet.contains(delimiter)) { + resourcesToPutIn = resourcesAhead; + } + } + + return new SplitResources(resourcesBehind, resourcesAhead); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/SslConfiguration.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/SslConfiguration.java new file mode 100644 index 0000000..12ed0a5 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/SslConfiguration.java @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.exchange; + +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; + +/** + * Provides methods to configure the ssl connection. + */ +@ConfigurationPrefix(SslConfiguration.PREFIX) +public interface SslConfiguration { + + /** + * This configuration's prefix. + */ + String PREFIX = "ssl"; + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to use SSL to encrypt RMI connections to the kernel.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_connection_0" + ) + boolean enable(); + + @ConfigurationEntry( + type = "String", + description = "The path to the SSL truststore.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_connection_1" + ) + String truststoreFile(); + + @ConfigurationEntry( + type = "String", + description = "The password for the SSL truststore.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_connection_2" + ) + String truststorePassword(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/AbstractProcessAdapter.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/AbstractProcessAdapter.java new file mode 100644 index 0000000..565990c --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/AbstractProcessAdapter.java @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.exchange.adapter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.opentcs.access.to.model.VisualLayoutCreationTO; +import org.opentcs.data.TCSObject; +import org.opentcs.guing.base.components.properties.type.KeyValueProperty; +import org.opentcs.guing.base.components.properties.type.KeyValueSetProperty; +import org.opentcs.guing.base.components.properties.type.Property; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.common.model.SystemModel; + +/** + * Basic implementation of a <code>ProcessAdapter</code>. + * Synchronizes between the local <code>ModelComponent</code> and the + * corresponding kernel object. + */ +public abstract class AbstractProcessAdapter + implements + ProcessAdapter { + + protected Map<String, String> getKernelProperties(ModelComponent model) { + Map<String, String> result = new HashMap<>(); + + KeyValueSetProperty misc = (KeyValueSetProperty) model.getProperty( + ModelComponent.MISCELLANEOUS + ); + + if (misc != null) { + for (KeyValueProperty p : misc.getItems()) { + result.put(p.getKey(), p.getValue()); + } + } + + return result; + } + + protected void unmarkAllPropertiesChanged(ModelComponent model) { + for (Property prop : model.getProperties().values()) { + prop.unmarkChanged(); + } + } + + /** + * Reads the current misc properties from the kernel and adopts these for the model object. + * + * @param model The model object to adopt the properties for. + * @param tcsObject The <code>TCSObject</code> to read from. + */ + protected void updateMiscModelProperties(ModelComponent model, TCSObject<?> tcsObject) { + List<KeyValueProperty> items = new ArrayList<>(); + + for (Map.Entry<String, String> curEntry : tcsObject.getProperties().entrySet()) { + if (!curEntry.getValue().contains("Unknown")) { + items.add(new KeyValueProperty(model, curEntry.getKey(), curEntry.getValue())); + } + } + + KeyValueSetProperty miscellaneous = (KeyValueSetProperty) model + .getProperty(ModelComponent.MISCELLANEOUS); + miscellaneous.setItems(items); + } + + protected List<VisualLayoutCreationTO> updatedLayouts( + ModelComponent model, + List<VisualLayoutCreationTO> layouts, + SystemModel systemModel + ) { + List<VisualLayoutCreationTO> result = new ArrayList<>(layouts.size()); + + for (VisualLayoutCreationTO layout : layouts) { + result.add(updatedLayout(model, layout, systemModel)); + } + + return result; + } + + protected VisualLayoutCreationTO updatedLayout( + ModelComponent model, + VisualLayoutCreationTO layout, + SystemModel systemModel + ) { + return layout; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/BlockAdapter.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/BlockAdapter.java new file mode 100644 index 0000000..8aa2fa7 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/BlockAdapter.java @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.exchange.adapter; + +import static java.util.Objects.requireNonNull; + +import java.util.HashSet; +import java.util.Set; +import org.opentcs.access.to.model.BlockCreationTO; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.TCSObject; +import org.opentcs.data.model.Block; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.common.model.SystemModel; + +/** + * An adapter for blocks. + */ +public class BlockAdapter + extends + AbstractProcessAdapter { + + /** + * Creates a new instance. + */ + public BlockAdapter() { + } + + @Override // OpenTCSProcessAdapter + public void updateModelProperties( + TCSObject<?> tcsObject, + ModelComponent modelComponent, + SystemModel systemModel, + TCSObjectService objectService + ) { + Block block = requireNonNull((Block) tcsObject, "tcsObject"); + BlockModel model = (BlockModel) modelComponent; + + model.getPropertyName().setText(block.getName()); + model.removeAllCourseElements(); + + updateModelType(model, block); + + for (TCSResourceReference<?> resRef : block.getMembers()) { + ModelComponent blockMember = systemModel.getModelComponent(resRef.getName()); + model.addCourseElement(blockMember); + } + + updateMiscModelProperties(model, block); + updateModelLayoutProperties(model, block); + } + + @Override + public PlantModelCreationTO storeToPlantModel( + ModelComponent modelComponent, + SystemModel systemModel, + PlantModelCreationTO plantModel + ) { + return plantModel + .withBlock( + new BlockCreationTO(modelComponent.getName()) + .withType(getKernelBlockType((BlockModel) modelComponent)) + .withMemberNames(getMemberNames((BlockModel) modelComponent)) + .withProperties(getKernelProperties(modelComponent)) + .withLayout(getLayout((BlockModel) modelComponent)) + ); + } + + private void updateModelLayoutProperties(BlockModel model, Block block) { + model.getPropertyColor().setColor(block.getLayout().getColor()); + } + + private Block.Type getKernelBlockType(BlockModel model) { + return convertBlockType((BlockModel.Type) model.getPropertyType().getValue()); + } + + private Set<String> getMemberNames(BlockModel blockModel) { + Set<String> result = new HashSet<>(); + for (ModelComponent model : blockModel.getChildComponents()) { + result.add(model.getName()); + } + + return result; + } + + private BlockCreationTO.Layout getLayout(BlockModel model) { + return new BlockCreationTO.Layout(model.getPropertyColor().getColor()); + } + + private void updateModelType(BlockModel model, Block block) { + BlockModel.Type value; + + switch (block.getType()) { + case SAME_DIRECTION_ONLY: + value = BlockModel.Type.SAME_DIRECTION_ONLY; + break; + case SINGLE_VEHICLE_ONLY: + default: + value = BlockModel.Type.SINGLE_VEHICLE_ONLY; + } + + model.getPropertyType().setValue(value); + } + + private Block.Type convertBlockType(BlockModel.Type type) { + requireNonNull(type, "type"); + switch (type) { + case SINGLE_VEHICLE_ONLY: + return Block.Type.SINGLE_VEHICLE_ONLY; + case SAME_DIRECTION_ONLY: + return Block.Type.SAME_DIRECTION_ONLY; + default: + throw new IllegalArgumentException("Unhandled block type: " + type); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/LayoutAdapter.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/LayoutAdapter.java new file mode 100644 index 0000000..ddc3760 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/LayoutAdapter.java @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.exchange.adapter; + +import static java.util.Objects.requireNonNull; + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.access.to.model.VisualLayoutCreationTO; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.TCSObject; +import org.opentcs.data.model.visualization.Layer; +import org.opentcs.data.model.visualization.LayerGroup; +import org.opentcs.data.model.visualization.VisualLayout; +import org.opentcs.guing.base.components.layer.LayerWrapper; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.common.model.SystemModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An adapter for VisualLayout instances. + */ +public class LayoutAdapter + extends + AbstractProcessAdapter { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(LayoutAdapter.class); + + /** + * Creates a new instance. + */ + public LayoutAdapter() { + } + + @Override // OpenTCSProcessAdapter + public void updateModelProperties( + TCSObject<?> tcsObject, + ModelComponent modelComponent, + SystemModel systemModel, + TCSObjectService objectService + ) { + VisualLayout layout = requireNonNull((VisualLayout) tcsObject, "tcsObject"); + LayoutModel model = (LayoutModel) modelComponent; + + try { + model.getPropertyName().setText(layout.getName()); + model.getPropertyName().markChanged(); + + model.getPropertyScaleX().setValueAndUnit(layout.getScaleX(), LengthProperty.Unit.MM); + model.getPropertyScaleX().markChanged(); + model.getPropertyScaleY().setValueAndUnit(layout.getScaleY(), LengthProperty.Unit.MM); + model.getPropertyScaleY().markChanged(); + + initLayerGroups(model, layout.getLayerGroups()); + initLayers(model, layout.getLayers()); + model.getPropertyLayerWrappers().markChanged(); + + updateMiscModelProperties(model, layout); + } + catch (IllegalArgumentException e) { + LOG.warn("", e); + } + } + + @Override + public PlantModelCreationTO storeToPlantModel( + ModelComponent modelComponent, + SystemModel systemModel, + PlantModelCreationTO plantModel + ) { + return plantModel.withVisualLayout( + new VisualLayoutCreationTO(modelComponent.getName()) + .withScaleX(getScaleX((LayoutModel) modelComponent)) + .withScaleY(getScaleY((LayoutModel) modelComponent)) + .withProperties(getKernelProperties(modelComponent)) + .withLayers(getLayers((LayoutModel) modelComponent)) + .withLayerGroups(getLayerGroups((LayoutModel) modelComponent)) + ); + } + + private void initLayerGroups(LayoutModel model, Collection<LayerGroup> groups) { + Map<Integer, LayerGroup> layerGroups = model.getPropertyLayerGroups().getValue(); + layerGroups.clear(); + for (LayerGroup group : groups) { + layerGroups.put(group.getId(), group); + } + } + + private void initLayers(LayoutModel model, Collection<Layer> layers) { + Map<Integer, LayerWrapper> layerWrappers = model.getPropertyLayerWrappers().getValue(); + layerWrappers.clear(); + + Map<Integer, LayerGroup> layerGroups = model.getPropertyLayerGroups().getValue(); + for (Layer layer : layers) { + layerWrappers.put( + layer.getId(), + new LayerWrapper(layer, layerGroups.get(layer.getGroupId())) + ); + } + } + + private double getScaleX(LayoutModel model) { + return model.getPropertyScaleX().getValueByUnit(LengthProperty.Unit.MM); + } + + private double getScaleY(LayoutModel model) { + return model.getPropertyScaleY().getValueByUnit(LengthProperty.Unit.MM); + } + + private List<Layer> getLayers(LayoutModel model) { + return model.getPropertyLayerWrappers().getValue().values().stream() + .map(wrapper -> wrapper.getLayer()) + .sorted(Comparator.comparing(layer -> layer.getId())) + .collect(Collectors.toList()); + } + + private List<LayerGroup> getLayerGroups(LayoutModel model) { + return model.getPropertyLayerGroups().getValue().values().stream() + .sorted(Comparator.comparing(group -> group.getId())) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/LinkAdapter.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/LinkAdapter.java new file mode 100644 index 0000000..73f3f4c --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/LinkAdapter.java @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.exchange.adapter; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.access.to.model.LocationCreationTO; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.TCSObject; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.common.model.SystemModel; + +/** + * An adapter for <code>Links</code>. + */ +public class LinkAdapter + extends + AbstractProcessAdapter { + + /** + * Creates a new instance. + */ + public LinkAdapter() { + } + + @Override + public void updateModelProperties( + TCSObject<?> tcsObject, + ModelComponent modelComponent, + SystemModel systemModel, + TCSObjectService objectService + ) { + } + + @Override + public PlantModelCreationTO storeToPlantModel( + ModelComponent modelComponent, + SystemModel systemModel, + PlantModelCreationTO plantModel + ) { + return plantModel.withLocations( + plantModel.getLocations().stream() + .map(loc -> mapLocation((LinkModel) modelComponent, loc)) + .collect(Collectors.toList()) + ); + } + + private LocationCreationTO mapLocation(LinkModel model, LocationCreationTO location) { + if (!Objects.equals(location.getName(), model.getLocation().getName())) { + return location; + } + return location.withLink(model.getPoint().getName(), getAllowedOperations(model)); + } + + private Set<String> getAllowedOperations(LinkModel model) { + return new HashSet<>(model.getPropertyAllowedOperations().getItems()); + } + +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/LocationAdapter.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/LocationAdapter.java new file mode 100644 index 0000000..7cad5ee --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/LocationAdapter.java @@ -0,0 +1,218 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.exchange.adapter; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.data.ObjectPropConstants.LOC_DEFAULT_REPRESENTATION; + +import java.util.Map; +import org.opentcs.access.to.model.LocationCreationTO; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.ObjectPropConstants; +import org.opentcs.data.TCSObject; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Triple; +import org.opentcs.data.model.visualization.LocationRepresentation; +import org.opentcs.guing.base.components.layer.LayerWrapper; +import org.opentcs.guing.base.components.properties.type.CoordinateProperty; +import org.opentcs.guing.base.components.properties.type.KeyValueProperty; +import org.opentcs.guing.base.components.properties.type.KeyValueSetProperty; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.common.model.SystemModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An adapter for locations. + */ +public class LocationAdapter + extends + AbstractProcessAdapter { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(LocationAdapter.class); + + /** + * Creates a new instance. + */ + public LocationAdapter() { + } + + @Override // OpenTCSProcessAdapter + public void updateModelProperties( + TCSObject<?> tcsObject, + ModelComponent modelComponent, + SystemModel systemModel, + TCSObjectService objectService + ) { + Location location = requireNonNull((Location) tcsObject, "tcsObject"); + LocationModel model = (LocationModel) modelComponent; + + try { + // Name + model.getPropertyName().setText(location.getName()); + + // Position in model + model.getPropertyModelPositionX().setValueAndUnit( + location.getPosition().getX(), + LengthProperty.Unit.MM + ); + model.getPropertyModelPositionY().setValueAndUnit( + location.getPosition().getY(), + LengthProperty.Unit.MM + ); + + // Type + model.getPropertyType().setValue(location.getType().getName()); + model.getPropertyLocked().setValue(location.isLocked()); + + // Peripheral information + model.getPropertyPeripheralReservationToken().setText( + location.getPeripheralInformation().getReservationToken() + ); + model.getPropertyPeripheralState().setText( + location.getPeripheralInformation().getState().name() + ); + model.getPropertyPeripheralProcState().setText( + location.getPeripheralInformation().getProcState().name() + ); + model.getPropertyPeripheralJob().setText(extractPeripheralJobName(location)); + + // Misc properties + updateMiscModelProperties(model, location); + updateModelLayoutProperties(model, location, systemModel.getLayoutModel()); + // look for label and symbol + updateRepresentation(model, model.getPropertyMiscellaneous()); + + model.setLocation(location); + model.propertiesChanged(model); + } + catch (IllegalArgumentException e) { + LOG.warn("", e); + } + } + + @Override + public PlantModelCreationTO storeToPlantModel( + ModelComponent modelComponent, + SystemModel systemModel, + PlantModelCreationTO plantModel + ) { + LocationModel locationModel = (LocationModel) modelComponent; + + PlantModelCreationTO result = plantModel + .withLocation( + new LocationCreationTO( + modelComponent.getName(), + locationModel.getLocationType().getName(), + getPosition(locationModel) + ) + .withLocked(getLocked(locationModel)) + .withProperties(getKernelProperties(modelComponent)) + .withLayout(getLayout(locationModel)) + ); + + unmarkAllPropertiesChanged(modelComponent); + + return result; + } + + @Override + protected Map<String, String> getKernelProperties(ModelComponent model) { + Map<String, String> result = super.getKernelProperties(model); + + LocationRepresentation locationRepresentation + = ((LocationModel) model).getPropertyDefaultRepresentation().getLocationRepresentation(); + + if (locationRepresentation != null) { + result.put(LOC_DEFAULT_REPRESENTATION, locationRepresentation.name()); + } + + return result; + } + + private void updateRepresentation(LocationModel model, KeyValueSetProperty miscellaneous) { + for (KeyValueProperty kvp : miscellaneous.getItems()) { + switch (kvp.getKey()) { + case ObjectPropConstants.LOC_DEFAULT_REPRESENTATION: + model.getPropertyDefaultRepresentation().setLocationRepresentation( + LocationRepresentation.valueOf(kvp.getValue()) + ); + break; + default: + } + } + } + + private void updateModelLayoutProperties( + LocationModel model, + Location location, + LayoutModel layoutModel + ) { + model.getPropertyLayoutPositionX() + .setText(String.valueOf(location.getLayout().getPosition().getX())); + model.getPropertyLayoutPositionY() + .setText(String.valueOf(location.getLayout().getPosition().getY())); + model.getPropertyLabelOffsetX() + .setText(String.valueOf(location.getLayout().getLabelOffset().getX())); + model.getPropertyLabelOffsetY() + .setText(String.valueOf(location.getLayout().getLabelOffset().getY())); + model.getPropertyDefaultRepresentation() + .setLocationRepresentation(location.getLayout().getLocationRepresentation()); + LayerWrapper layerWrapper = layoutModel.getPropertyLayerWrappers() + .getValue().get(location.getLayout().getLayerId()); + model.getPropertyLayerWrapper().setValue(layerWrapper); + } + + private String extractPeripheralJobName(Location location) { + return location.getPeripheralInformation().getPeripheralJob() == null + ? null + : location.getPeripheralInformation().getPeripheralJob().getName(); + } + + private Triple getPosition(LocationModel model) { + return convertToTriple( + model.getPropertyModelPositionX(), + model.getPropertyModelPositionY() + ); + } + + private boolean getLocked(LocationModel model) { + if (model.getPropertyLocked().getValue() instanceof Boolean) { + return (boolean) model.getPropertyLocked().getValue(); + } + return false; + } + + private LocationCreationTO.Layout getLayout(LocationModel model) { + return new LocationCreationTO.Layout( + new Couple( + Long.parseLong(model.getPropertyLayoutPositionX().getText()), + Long.parseLong(model.getPropertyLayoutPositionY().getText()) + ), + new Couple( + Long.parseLong(model.getPropertyLabelOffsetX().getText()), + Long.parseLong(model.getPropertyLabelOffsetY().getText()) + ), + model.getPropertyDefaultRepresentation().getLocationRepresentation(), + model.getPropertyLayerWrapper().getValue().getLayer().getId() + ); + } + + private Triple convertToTriple(CoordinateProperty cpx, CoordinateProperty cpy) { + Triple result = new Triple( + (int) cpx.getValueByUnit(LengthProperty.Unit.MM), + (int) cpy.getValueByUnit(LengthProperty.Unit.MM), + 0 + ); + + return result; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/LocationTypeAdapter.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/LocationTypeAdapter.java new file mode 100644 index 0000000..7fc5ab3 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/LocationTypeAdapter.java @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.exchange.adapter; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.data.ObjectPropConstants.LOCTYPE_DEFAULT_REPRESENTATION; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.opentcs.access.to.model.LocationTypeCreationTO; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.ObjectPropConstants; +import org.opentcs.data.TCSObject; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.visualization.LocationRepresentation; +import org.opentcs.guing.base.components.properties.type.KeyValueProperty; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.common.model.SystemModel; + +/** + * An adapter for location types. + */ +public class LocationTypeAdapter + extends + AbstractProcessAdapter { + + /** + * Creates a new instance. + */ + public LocationTypeAdapter() { + } + + @Override // OpenTCSProcessAdapter + public void updateModelProperties( + TCSObject<?> tcsObject, + ModelComponent modelComponent, + SystemModel systemModel, + TCSObjectService objectService + ) { + LocationType locationType = requireNonNull((LocationType) tcsObject, "tcsObject"); + LocationTypeModel model = (LocationTypeModel) modelComponent; + + // Name + model.getPropertyName().setText(locationType.getName()); + // Allowed operations + model.getPropertyAllowedOperations() + .setItems(new ArrayList<>(locationType.getAllowedOperations())); + model.getPropertyAllowedPeripheralOperations() + .setItems(new ArrayList<>(locationType.getAllowedPeripheralOperations())); + updateMiscModelProperties(model, locationType); + updateModelLayoutProperties(model, locationType); + model.setLocationType(locationType); + + for (KeyValueProperty next : model.getPropertyMiscellaneous().getItems()) { + if (next.getKey().equals(ObjectPropConstants.LOCTYPE_DEFAULT_REPRESENTATION)) { + model.getPropertyDefaultRepresentation() + .setLocationRepresentation(LocationRepresentation.valueOf(next.getValue())); + break; + } + } + } + + @Override + public PlantModelCreationTO storeToPlantModel( + ModelComponent modelComponent, + SystemModel systemModel, + PlantModelCreationTO plantModel + ) { + PlantModelCreationTO result = plantModel + .withLocationType( + new LocationTypeCreationTO(modelComponent.getName()) + .withAllowedOperations(getAllowedOperations((LocationTypeModel) modelComponent)) + .withAllowedPeripheralOperations( + getAllowedPeripheralOperations((LocationTypeModel) modelComponent) + ) + .withProperties(getKernelProperties(modelComponent)) + .withLayout(getLayout((LocationTypeModel) modelComponent)) + ); + + unmarkAllPropertiesChanged(modelComponent); + + return result; + } + + private void updateModelLayoutProperties(LocationTypeModel model, LocationType location) { + model.getPropertyDefaultRepresentation() + .setLocationRepresentation(location.getLayout().getLocationRepresentation()); + } + + private List<String> getAllowedOperations(LocationTypeModel model) { + return new ArrayList<>(model.getPropertyAllowedOperations().getItems()); + } + + private List<String> getAllowedPeripheralOperations(LocationTypeModel model) { + return new ArrayList<>(model.getPropertyAllowedPeripheralOperations().getItems()); + } + + private LocationTypeCreationTO.Layout getLayout(LocationTypeModel model) { + return new LocationTypeCreationTO.Layout( + model.getPropertyDefaultRepresentation().getLocationRepresentation() + ); + } + + @Override + protected Map<String, String> getKernelProperties(ModelComponent model) { + Map<String, String> result = super.getKernelProperties(model); + LocationTypeModel locationTypeModel = (LocationTypeModel) model; + + // Add the location representation (symbol) from the model. + LocationRepresentation locationRepresentation + = locationTypeModel.getPropertyDefaultRepresentation().getLocationRepresentation(); + + if (locationRepresentation != null) { + result.put(LOCTYPE_DEFAULT_REPRESENTATION, locationRepresentation.name()); + } + + return result; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/PathAdapter.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/PathAdapter.java new file mode 100644 index 0000000..7ae94ef --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/PathAdapter.java @@ -0,0 +1,288 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.exchange.adapter; + +import static java.util.Objects.requireNonNull; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.opentcs.access.to.model.PathCreationTO; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.access.to.peripherals.PeripheralOperationCreationTO; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.TCSObject; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Envelope; +import org.opentcs.data.model.Path; +import org.opentcs.data.peripherals.PeripheralOperation; +import org.opentcs.guing.base.components.layer.LayerWrapper; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.components.properties.type.SpeedProperty; +import org.opentcs.guing.base.model.EnvelopeModel; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.PeripheralOperationModel; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.common.model.SystemModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An adapter for Path objects. + */ +public class PathAdapter + extends + AbstractProcessAdapter { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PathAdapter.class); + + /** + * Creates a new instance. + */ + public PathAdapter() { + } + + @Override + public void updateModelProperties( + TCSObject<?> tcsObject, + ModelComponent modelComponent, + SystemModel systemModel, + TCSObjectService objectService + ) { + Path path = requireNonNull((Path) tcsObject, "tcsObject"); + PathModel model = (PathModel) modelComponent; + + model.getPropertyName().setText(path.getName()); + model.getPropertyStartComponent().setText(path.getSourcePoint().getName()); + model.getPropertyEndComponent().setText(path.getDestinationPoint().getName()); + model.getPropertyLength().setValueAndUnit(path.getLength(), LengthProperty.Unit.MM); + model.getPropertyMaxVelocity().setValueAndUnit( + path.getMaxVelocity(), + SpeedProperty.Unit.MM_S + ); + model.getPropertyMaxReverseVelocity().setValueAndUnit( + path.getMaxReverseVelocity(), + SpeedProperty.Unit.MM_S + ); + model.getPropertyLocked().setValue(path.isLocked()); + for (PeripheralOperation operation : path.getPeripheralOperations()) { + model.getPropertyPeripheralOperations().getValue().add( + new PeripheralOperationModel( + operation.getLocation().getName(), + operation.getOperation(), + operation.getExecutionTrigger(), + operation.isCompletionRequired() + ) + ); + } + + for (Map.Entry<String, Envelope> entry : path.getVehicleEnvelopes().entrySet()) { + model.getPropertyVehicleEnvelopes().getValue().add( + new EnvelopeModel(entry.getKey(), entry.getValue().getVertices()) + ); + } + + updateMiscModelProperties(model, path); + updateModelLayoutProperties(model, path, systemModel.getLayoutModel()); + model.propertiesChanged(model); + } + + @Override + public PlantModelCreationTO storeToPlantModel( + ModelComponent modelComponent, + SystemModel systemModel, + PlantModelCreationTO plantModel + ) { + PathModel pathModel = (PathModel) modelComponent; + + LOG.debug( + "Path {}: srcPoint is {}, dstPoint is {}.", + pathModel.getName(), + getSourcePoint(pathModel), + getDestinationPoint(pathModel) + ); + + PlantModelCreationTO result = plantModel + .withPath( + new PathCreationTO( + pathModel.getName(), + getSourcePoint(pathModel), + getDestinationPoint(pathModel) + ) + .withLength(getLength(pathModel)) + .withMaxVelocity(getMaxVelocity(pathModel)) + .withMaxReverseVelocity(getMaxReverseVelocity(pathModel)) + .withProperties(getKernelProperties(pathModel)) + .withLocked(getLocked(pathModel)) + .withPeripheralOperations(getPeripheralOperations(pathModel)) + .withVehicleEnvelopes(getKernelVehicleEnvelopes(pathModel)) + .withLayout(getLayout(pathModel)) + ); + + unmarkAllPropertiesChanged(pathModel); + + return result; + } + + private void updateModelLayoutProperties(PathModel model, Path path, LayoutModel layoutModel) { + model.getPropertyPathConnType() + .setValue(toPathModelConnectionType(path.getLayout().getConnectionType())); + String controlPointsString = path.getLayout().getControlPoints().stream() + .map(point -> String.format("%d,%d", point.getX(), point.getY())) + .collect(Collectors.joining(";")); + model.getPropertyPathControlPoints().setText(controlPointsString); + LayerWrapper layerWrapper = layoutModel.getPropertyLayerWrappers() + .getValue().get(path.getLayout().getLayerId()); + model.getPropertyLayerWrapper().setValue(layerWrapper); + } + + private PathModel.Type toPathModelConnectionType(Path.Layout.ConnectionType connectionType) { + PathModel.Type result = PathModel.Type.DIRECT; + + switch (connectionType) { + case DIRECT: + result = PathModel.Type.DIRECT; + break; + case ELBOW: + result = PathModel.Type.ELBOW; + break; + case SLANTED: + result = PathModel.Type.SLANTED; + break; + case POLYPATH: + result = PathModel.Type.POLYPATH; + break; + case BEZIER: + result = PathModel.Type.BEZIER; + break; + case BEZIER_3: + result = PathModel.Type.BEZIER_3; + break; + default: + throw new IllegalArgumentException("Unhandled connection type: " + connectionType); + } + + return result; + } + + private boolean getLocked(PathModel model) { + if (model.getPropertyLocked().getValue() instanceof Boolean) { + return (boolean) model.getPropertyLocked().getValue(); + } + return false; + } + + private int getMaxVelocity(PathModel model) { + return (int) Math.abs( + model.getPropertyMaxVelocity() + .getValueByUnit(SpeedProperty.Unit.MM_S) + ); + } + + private int getMaxReverseVelocity(PathModel model) { + return (int) Math.abs( + model.getPropertyMaxReverseVelocity() + .getValueByUnit(SpeedProperty.Unit.MM_S) + ); + } + + private String getSourcePoint(PathModel model) { + return model.getPropertyStartComponent().getText(); + } + + private String getDestinationPoint(PathModel model) { + return model.getPropertyEndComponent().getText(); + } + + private long getLength(PathModel model) { + LengthProperty pLength = model.getPropertyLength(); + + if ((double) pLength.getValue() <= 0) { + try { + pLength.setValueAndUnit(1.0, pLength.getUnit()); + pLength.markChanged(); + } + catch (IllegalArgumentException ex) { + LOG.warn("", ex); + } + } + + return (long) pLength.getValueByUnit(LengthProperty.Unit.MM); + } + + private Map<String, Envelope> getKernelVehicleEnvelopes(PathModel model) { + return model.getPropertyVehicleEnvelopes().getValue().stream() + .collect( + Collectors.toMap( + EnvelopeModel::getKey, + envelopeModel -> new Envelope(envelopeModel.getVertices()) + ) + ); + } + + private PathCreationTO.Layout getLayout(PathModel model) { + List<Couple> controlPoints + = Arrays.asList(model.getPropertyPathControlPoints().getText().split(";")).stream() + .filter(controlPointString -> !controlPointString.isEmpty()) + .map(controlPointString -> { + String[] coordinateStrings = controlPointString.split(","); + return new Couple( + Long.parseLong(coordinateStrings[0]), + Long.parseLong(coordinateStrings[1]) + ); + }) + .collect(Collectors.toList()); + + return new PathCreationTO.Layout( + toPathConnectionType((PathModel.Type) model.getPropertyPathConnType().getValue()), + controlPoints, + model.getPropertyLayerWrapper().getValue().getLayer().getId() + ); + } + + private Path.Layout.ConnectionType toPathConnectionType(PathModel.Type type) { + Path.Layout.ConnectionType result = Path.Layout.ConnectionType.DIRECT; + + switch (type) { + case DIRECT: + result = Path.Layout.ConnectionType.DIRECT; + break; + case ELBOW: + result = Path.Layout.ConnectionType.ELBOW; + break; + case SLANTED: + result = Path.Layout.ConnectionType.SLANTED; + break; + case POLYPATH: + result = Path.Layout.ConnectionType.POLYPATH; + break; + case BEZIER: + result = Path.Layout.ConnectionType.BEZIER; + break; + case BEZIER_3: + result = Path.Layout.ConnectionType.BEZIER_3; + break; + default: + throw new IllegalArgumentException("Unhandled connection type: " + type); + } + + return result; + } + + private List<PeripheralOperationCreationTO> getPeripheralOperations(PathModel path) { + return path.getPropertyPeripheralOperations().getValue().stream() + .map( + model -> new PeripheralOperationCreationTO( + model.getOperation(), model.getLocationName() + ) + .withExecutionTrigger(model.getExecutionTrigger()) + .withCompletionRequired(model.isCompletionRequired()) + ) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/PointAdapter.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/PointAdapter.java new file mode 100644 index 0000000..ce14bc4 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/PointAdapter.java @@ -0,0 +1,224 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.exchange.adapter; + +import static java.util.Objects.requireNonNull; + +import java.util.Map; +import java.util.stream.Collectors; +import org.opentcs.access.to.model.BoundingBoxCreationTO; +import org.opentcs.access.to.model.CoupleCreationTO; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.access.to.model.PointCreationTO; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.TCSObject; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Envelope; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Pose; +import org.opentcs.data.model.Triple; +import org.opentcs.guing.base.components.layer.LayerWrapper; +import org.opentcs.guing.base.components.properties.type.AngleProperty; +import org.opentcs.guing.base.components.properties.type.CoordinateProperty; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.model.BoundingBoxModel; +import org.opentcs.guing.base.model.EnvelopeModel; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.common.model.SystemModel; + +/** + * An adapter for points. + */ +public class PointAdapter + extends + AbstractProcessAdapter { + + /** + * Creates a new instance. + */ + public PointAdapter() { + } + + @Override // OpenTCSProcessAdapter + public void updateModelProperties( + TCSObject<?> tcsObject, + ModelComponent modelComponent, + SystemModel systemModel, + TCSObjectService objectService + ) { + Point point = requireNonNull((Point) tcsObject, "tcsObject"); + PointModel model = (PointModel) modelComponent; + + // Name + model.getPropertyName().setText(point.getName()); + + // Position in model + model.getPropertyModelPositionX().setValueAndUnit( + point.getPose().getPosition().getX(), + LengthProperty.Unit.MM + ); + model.getPropertyModelPositionY().setValueAndUnit( + point.getPose().getPosition().getY(), + LengthProperty.Unit.MM + ); + model.getPropertyVehicleOrientationAngle() + .setValueAndUnit(point.getPose().getOrientationAngle(), AngleProperty.Unit.DEG); + + updateModelType(model, point); + + for (Map.Entry<String, Envelope> entry : point.getVehicleEnvelopes().entrySet()) { + model.getPropertyVehicleEnvelopes().getValue().add( + new EnvelopeModel(entry.getKey(), entry.getValue().getVertices()) + ); + } + + model.getPropertyMaxVehicleBoundingBox() + .setValue( + new BoundingBoxModel( + point.getMaxVehicleBoundingBox().getLength(), + point.getMaxVehicleBoundingBox().getWidth(), + point.getMaxVehicleBoundingBox().getHeight(), + new Couple( + point.getMaxVehicleBoundingBox().getReferenceOffset().getX(), + point.getMaxVehicleBoundingBox().getReferenceOffset().getY() + ) + ) + ); + + updateMiscModelProperties(model, point); + updateModelLayoutProperties(model, point, systemModel.getLayoutModel()); + } + + @Override + public PlantModelCreationTO storeToPlantModel( + ModelComponent modelComponent, + SystemModel systemModel, + PlantModelCreationTO plantModel + ) { + PlantModelCreationTO result = plantModel + .withPoint( + new PointCreationTO(modelComponent.getName()) + .withPose( + new Pose( + getKernelCoordinates((PointModel) modelComponent), + getKernelVehicleAngle((PointModel) modelComponent) + ) + ) + .withType(getKernelPointType((PointModel) modelComponent)) + .withVehicleEnvelopes(getKernelVehicleEnvelopes((PointModel) modelComponent)) + .withMaxVehicleBoundingBox( + getKernelMaxVehicleBoundingBox((PointModel) modelComponent) + ) + .withProperties(getKernelProperties(modelComponent)) + .withLayout(getLayout((PointModel) modelComponent)) + ); + + unmarkAllPropertiesChanged(modelComponent); + + return result; + } + + private void updateModelLayoutProperties(PointModel model, Point point, LayoutModel layoutModel) { + model.getPropertyLayoutPosX().setText(String.valueOf(point.getLayout().getPosition().getX())); + model.getPropertyLayoutPosY().setText(String.valueOf(point.getLayout().getPosition().getY())); + model.getPropertyPointLabelOffsetX() + .setText(String.valueOf(point.getLayout().getLabelOffset().getX())); + model.getPropertyPointLabelOffsetY() + .setText(String.valueOf(point.getLayout().getLabelOffset().getY())); + LayerWrapper layerWrapper = layoutModel.getPropertyLayerWrappers() + .getValue().get(point.getLayout().getLayerId()); + model.getPropertyLayerWrapper().setValue(layerWrapper); + } + + private void updateModelType(PointModel model, Point point) { + PointModel.Type value; + + switch (point.getType()) { + case PARK_POSITION: + value = PointModel.Type.PARK; + break; + case HALT_POSITION: + default: + value = PointModel.Type.HALT; + } + + model.getPropertyType().setValue(value); + } + + private Point.Type getKernelPointType(PointModel model) { + return convertPointType((PointModel.Type) model.getPropertyType().getValue()); + } + + private Triple getKernelCoordinates(PointModel model) { + return convertToTriple( + model.getPropertyModelPositionX(), + model.getPropertyModelPositionY() + ); + } + + private double getKernelVehicleAngle(PointModel model) { + return model.getPropertyVehicleOrientationAngle().getValueByUnit(AngleProperty.Unit.DEG); + } + + private Map<String, Envelope> getKernelVehicleEnvelopes(PointModel model) { + return model.getPropertyVehicleEnvelopes().getValue().stream() + .collect( + Collectors.toMap( + EnvelopeModel::getKey, + envelopeModel -> new Envelope(envelopeModel.getVertices()) + ) + ); + } + + private BoundingBoxCreationTO getKernelMaxVehicleBoundingBox(PointModel model) { + return new BoundingBoxCreationTO( + model.getPropertyMaxVehicleBoundingBox().getValue().getLength(), + model.getPropertyMaxVehicleBoundingBox().getValue().getWidth(), + model.getPropertyMaxVehicleBoundingBox().getValue().getHeight() + ) + .withReferenceOffset( + new CoupleCreationTO( + model.getPropertyMaxVehicleBoundingBox().getValue().getReferenceOffset().getX(), + model.getPropertyMaxVehicleBoundingBox().getValue().getReferenceOffset().getY() + ) + ); + } + + private PointCreationTO.Layout getLayout(PointModel model) { + return new PointCreationTO.Layout( + new Couple( + Long.parseLong(model.getPropertyLayoutPosX().getText()), + Long.parseLong(model.getPropertyLayoutPosY().getText()) + ), + new Couple( + Long.parseLong(model.getPropertyPointLabelOffsetX().getText()), + Long.parseLong(model.getPropertyPointLabelOffsetY().getText()) + ), + model.getPropertyLayerWrapper().getValue().getLayer().getId() + ); + } + + private Point.Type convertPointType(PointModel.Type type) { + requireNonNull(type, "type"); + switch (type) { + case PARK: + return Point.Type.PARK_POSITION; + case HALT: + return Point.Type.HALT_POSITION; + default: + throw new IllegalArgumentException("Unhandled type: " + type); + } + } + + private Triple convertToTriple(CoordinateProperty cpx, CoordinateProperty cpy) { + Triple result = new Triple( + (int) cpx.getValueByUnit(LengthProperty.Unit.MM), + (int) cpy.getValueByUnit(LengthProperty.Unit.MM), + 0 + ); + + return result; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/ProcessAdapter.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/ProcessAdapter.java new file mode 100644 index 0000000..1383aca --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/ProcessAdapter.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.exchange.adapter; + +import java.io.Serializable; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.TCSObject; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.common.model.SystemModel; + +/** + * Receives messages from a <code>ModelComponent</code> and its kernel + * equivalent and delegates them to the respective other one. + */ +public interface ProcessAdapter + extends + Serializable { + + /** + * Reads the current properties from the kernel and adopts these for the model object. + * + * @param objectService A reference to the object service, in case the adapter needs to access + * data that is not contained in the given kernel object. + * @param modelComponent The model component to adopt the properties for. + * @param systemModel A reference to the system model, in case the adapter needs to access + * other model components as well. + * @param tcsObject The kernel's object. + */ + void updateModelProperties( + TCSObject<?> tcsObject, + ModelComponent modelComponent, + SystemModel systemModel, + TCSObjectService objectService + ); + + /** + * Reads the current properties from the model component and adopts these for the kernel object. + * + * @param modelComponent The model component to read properties from. + * @param systemModel A reference to the system model, in case the adapter needs to access + * other model components as well. + * @param plantModel A transfer object describing the current plant model data. + * @return A new transfer object, describing the plant model data with the model component's data + * merged. + */ + PlantModelCreationTO storeToPlantModel( + ModelComponent modelComponent, + SystemModel systemModel, + PlantModelCreationTO plantModel + ); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/ProcessAdapterUtil.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/ProcessAdapterUtil.java new file mode 100644 index 0000000..b99dab9 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/ProcessAdapterUtil.java @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.exchange.adapter; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; + +/** + * A utility class providing process adapters. + */ +public class ProcessAdapterUtil { + + private final BlockAdapter blockAdapter; + private final LayoutAdapter layoutAdapter; + private final LinkAdapter linkAdapter; + private final LocationAdapter locationAdapter; + private final LocationTypeAdapter locationTypeAdapter; + private final PathAdapter pathAdapter; + private final PointAdapter pointAdapter; + private final VehicleAdapter vehicleAdapter; + + @Inject + public ProcessAdapterUtil( + BlockAdapter blockAdapter, + LayoutAdapter layoutAdapter, + LinkAdapter linkAdapter, + LocationAdapter locationAdapter, + LocationTypeAdapter locationTypeAdapter, + PathAdapter pathAdapter, + PointAdapter pointAdapter, + VehicleAdapter vehicleAdapter + ) { + this.blockAdapter = requireNonNull(blockAdapter, "blockAdapter"); + this.layoutAdapter = requireNonNull(layoutAdapter, "layoutAdapter"); + this.linkAdapter = requireNonNull(linkAdapter, "linkAdapter"); + this.locationAdapter = requireNonNull(locationAdapter, "locationAdapter"); + this.locationTypeAdapter = requireNonNull(locationTypeAdapter, "locationTypeAdapter"); + this.pathAdapter = requireNonNull(pathAdapter, "pathAdapter"); + this.pointAdapter = requireNonNull(pointAdapter, "pointAdapter"); + this.vehicleAdapter = requireNonNull(vehicleAdapter, "vehicleAdapter"); + } + + public ProcessAdapter processAdapterFor(ModelComponent model) { + + if (model instanceof PointModel) { + return pointAdapter; + } + else if (model instanceof PathModel) { + return pathAdapter; + } + else if (model instanceof LocationTypeModel) { + return locationTypeAdapter; + } + else if (model instanceof LocationModel) { + return locationAdapter; + } + else if (model instanceof BlockModel) { + return blockAdapter; + } + else if (model instanceof VehicleModel) { + return vehicleAdapter; + } + else if (model instanceof LinkModel) { + return linkAdapter; + } + else if (model instanceof LayoutModel) { + return layoutAdapter; + } + else { + // Just in case the set of model classes ever changes. + throw new IllegalArgumentException("Unhandled model class: " + model.getClass()); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/VehicleAdapter.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/VehicleAdapter.java new file mode 100644 index 0000000..df8477a --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/exchange/adapter/VehicleAdapter.java @@ -0,0 +1,286 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.exchange.adapter; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.opentcs.access.CredentialsException; +import org.opentcs.access.to.model.BoundingBoxCreationTO; +import org.opentcs.access.to.model.CoupleCreationTO; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.access.to.model.VehicleCreationTO; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.TCSObject; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Vehicle; +import org.opentcs.guing.base.components.properties.event.NullAttributesChangeListener; +import org.opentcs.guing.base.components.properties.type.AngleProperty; +import org.opentcs.guing.base.components.properties.type.EnergyLevelThresholdSetModel; +import org.opentcs.guing.base.components.properties.type.KeyValueProperty; +import org.opentcs.guing.base.components.properties.type.PercentProperty; +import org.opentcs.guing.base.components.properties.type.SpeedProperty; +import org.opentcs.guing.base.components.properties.type.SpeedProperty.Unit; +import org.opentcs.guing.base.model.BoundingBoxModel; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.model.SystemModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An adapter for vehicles. + */ +public class VehicleAdapter + extends + AbstractProcessAdapter { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(VehicleAdapter.class); + + public VehicleAdapter() { + } + + @Override // OpenTCSProcessAdapter + public void updateModelProperties( + TCSObject<?> tcsObject, + ModelComponent modelComponent, + SystemModel systemModel, + TCSObjectService objectService + ) { + requireNonNull(objectService, "objectService"); + Vehicle vehicle = requireNonNull((Vehicle) tcsObject, "tcsObject"); + VehicleModel model = (VehicleModel) modelComponent; + + try { + model.getPropertyName().setText(vehicle.getName()); + model.getPropertyBoundingBox().setValue( + new BoundingBoxModel( + vehicle.getBoundingBox().getLength(), + vehicle.getBoundingBox().getWidth(), + vehicle.getBoundingBox().getHeight(), + new Couple( + vehicle.getBoundingBox().getReferenceOffset().getX(), + vehicle.getBoundingBox().getReferenceOffset().getY() + ) + ) + ); + model.getPropertyMaxVelocity().setValueAndUnit(vehicle.getMaxVelocity(), Unit.MM_S); + model.getPropertyMaxReverseVelocity().setValueAndUnit( + vehicle.getMaxReverseVelocity(), + Unit.MM_S + ); + model.getPropertyEnergyLevel().setValueAndUnit( + vehicle.getEnergyLevel(), + PercentProperty.Unit.PERCENT + ); + + model.getPropertyEnergyLevelThresholdSet().setValue( + new EnergyLevelThresholdSetModel( + vehicle.getEnergyLevelThresholdSet().getEnergyLevelCritical(), + vehicle.getEnergyLevelThresholdSet().getEnergyLevelGood(), + vehicle.getEnergyLevelThresholdSet().getEnergyLevelSufficientlyRecharged(), + vehicle.getEnergyLevelThresholdSet().getEnergyLevelFullyRecharged() + ) + ); + + model.getPropertyLoaded().setValue( + vehicle.getLoadHandlingDevices().stream().anyMatch(lhe -> lhe.isFull()) + ); + model.getPropertyState().setValue(vehicle.getState()); + model.getPropertyProcState().setValue(vehicle.getProcState()); + model.getPropertyIntegrationLevel().setValue(vehicle.getIntegrationLevel()); + model.getPropertyPaused().setValue(vehicle.isPaused()); + + updateModelCurrentPoint(model, vehicle, systemModel); + updateModelNextPoint(model, vehicle, systemModel); + + model.getPropertyPrecisePosition().setValue(vehicle.getPose().getPosition()); + model.setPrecisePosition(vehicle.getPose().getPosition()); + + model.getPropertyOrientationAngle().setValueAndUnit( + vehicle.getPose().getOrientationAngle(), + AngleProperty.Unit.DEG + ); + model.setOrientationAngle(vehicle.getPose().getOrientationAngle()); + + updateCurrentTransportName(vehicle, model); + updateCurrentOrderSequenceName(vehicle, model); + + model.getPropertyAllowedOrderTypes().setItems(vehicle.getAllowedOrderTypes()); + model.setVehicle(vehicle); + + model.getPropertyEnvelopeKey().setText(vehicle.getEnvelopeKey()); + + updateMiscModelProperties(model, vehicle); + updateModelDriveOrder(objectService, vehicle, model, systemModel); + updateModelLayoutProperties(model, vehicle); + + model.getAllocatedResources().setItems(vehicle.getAllocatedResources()); + model.getClaimedResources().setItems(vehicle.getClaimedResources()); + + model.propertiesChanged(new NullAttributesChangeListener()); + } + catch (CredentialsException e) { + LOG.warn("", e); + } + } + + @Override // OpenTCSProcessAdapter + public PlantModelCreationTO storeToPlantModel( + ModelComponent modelComponent, + SystemModel systemModel, + PlantModelCreationTO plantModel + ) { + VehicleModel vehicleModel = (VehicleModel) modelComponent; + return plantModel + .withVehicle( + new VehicleCreationTO(vehicleModel.getName()) + .withBoundingBox(getBoundingBox(vehicleModel)) + .withEnergyLevelThresholdSet(getEnergyLevelThresholdSet(vehicleModel)) + .withMaxVelocity(getMaximumVelocity(vehicleModel)) + .withMaxReverseVelocity(getMaximumReverseVelocity(vehicleModel)) + .withEnvelopeKey(getEnvelopeKey(vehicleModel)) + .withProperties(getKernelProperties(vehicleModel)) + .withLayout(getLayout(vehicleModel)) + ); + } + + protected void updateModelDriveOrder( + TCSObjectService objectService, + Vehicle vehicle, + VehicleModel vehicleModel, + SystemModel systemModel + ) + throws CredentialsException { + vehicleModel.setDriveOrderDestination(null); + vehicleModel.setCurrentDriveOrderPath(null); + } + + private void updateModelNextPoint( + VehicleModel vehicleModel, + Vehicle vehicle, + SystemModel systemModel + ) { + if (vehicle.getNextPosition() != null) { + PointModel pointModel = systemModel.getPointModel(vehicle.getNextPosition().getName()); + vehicleModel.setNextPoint(pointModel); + vehicleModel.getPropertyNextPoint().setText(vehicle.getNextPosition().getName()); + } + else { + vehicleModel.setNextPoint(null); + vehicleModel.getPropertyNextPoint().setText("null"); + } + } + + private void updateModelCurrentPoint( + VehicleModel vehicleModel, + Vehicle vehicle, + SystemModel systemModel + ) { + if (vehicle.getCurrentPosition() != null) { + PointModel pointModel = systemModel.getPointModel(vehicle.getCurrentPosition().getName()); + + if (pointModel == null) { + LOG.error("Error: Point " + vehicle.getCurrentPosition().getName() + "not found."); + } + else { + vehicleModel.placeOnPoint(pointModel); + vehicleModel.getPropertyPoint().setText(vehicle.getCurrentPosition().getName()); + } + } + else { + vehicleModel.placeOnPoint(null); + vehicleModel.getPropertyPoint().setText("null"); + } + } + + private void updateCurrentTransportName( + Vehicle vehicle, + VehicleModel vehicleModel + ) { + if (vehicle.getTransportOrder() == null) { + vehicleModel.getPropertyCurrentOrderName().setText("null"); + } + else { + vehicleModel.getPropertyCurrentOrderName().setText(vehicle.getTransportOrder().getName()); + } + + } + + private void updateCurrentOrderSequenceName( + Vehicle vehicle, + VehicleModel vehicleModel + ) { + if (vehicle.getOrderSequence() == null) { + vehicleModel.getPropertyCurrentSequenceName().setText("null"); + } + else { + vehicleModel.getPropertyCurrentSequenceName().setText(vehicle.getOrderSequence().getName()); + } + + } + + private BoundingBoxCreationTO getBoundingBox(VehicleModel model) { + return new BoundingBoxCreationTO( + model.getPropertyBoundingBox().getValue().getLength(), + model.getPropertyBoundingBox().getValue().getWidth(), + model.getPropertyBoundingBox().getValue().getHeight() + ).withReferenceOffset( + new CoupleCreationTO( + model.getPropertyBoundingBox().getValue().getReferenceOffset().getX(), + model.getPropertyBoundingBox().getValue().getReferenceOffset().getY() + ) + ); + } + + private int getMaximumReverseVelocity(VehicleModel model) { + return ((Double) model.getPropertyMaxReverseVelocity().getValueByUnit(SpeedProperty.Unit.MM_S)) + .intValue(); + } + + private int getMaximumVelocity(VehicleModel model) { + return ((Double) model.getPropertyMaxVelocity().getValueByUnit(SpeedProperty.Unit.MM_S)) + .intValue(); + } + + private VehicleCreationTO.EnergyLevelThresholdSet getEnergyLevelThresholdSet(VehicleModel model) { + return new VehicleCreationTO.EnergyLevelThresholdSet( + model.getPropertyEnergyLevelThresholdSet().getValue().getEnergyLevelCritical(), + model.getPropertyEnergyLevelThresholdSet().getValue().getEnergyLevelGood(), + model.getPropertyEnergyLevelThresholdSet().getValue().getEnergyLevelSufficientlyRecharged(), + model.getPropertyEnergyLevelThresholdSet().getValue().getEnergyLevelFullyRecharged() + ); + } + + private String getEnvelopeKey(VehicleModel model) { + return model.getPropertyEnvelopeKey().getText(); + } + + private VehicleCreationTO.Layout getLayout(VehicleModel model) { + return new VehicleCreationTO.Layout(model.getPropertyRouteColor().getColor()); + } + + @Override // OpenTCSProcessAdapter + protected void updateMiscModelProperties(ModelComponent model, TCSObject<?> tcsObject) { + VehicleModel vehicleModel = (VehicleModel) model; + List<KeyValueProperty> items = new ArrayList<>(); + + for (Map.Entry<String, String> curEntry : tcsObject.getProperties().entrySet()) { + if (!curEntry.getValue().contains("Unknown")) { + items.add(new KeyValueProperty(vehicleModel, curEntry.getKey(), curEntry.getValue())); + } + } + + vehicleModel.getPropertyMiscellaneous().setItems(items); + } + + private void updateModelLayoutProperties(VehicleModel model, Vehicle vehicle) { + model.getPropertyRouteColor().setColor(vehicle.getLayout().getRouteColor()); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/model/ComponentSelectionListener.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/model/ComponentSelectionListener.java new file mode 100644 index 0000000..f1ad70a --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/model/ComponentSelectionListener.java @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.model; + +import org.opentcs.guing.base.model.ModelComponent; + +/** + */ +public interface ComponentSelectionListener { + + /** + * Informs this listener that the given component has been selected. + * + * @param model The selected component. + */ + void componentSelected(ModelComponent model); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/model/StandardSystemModel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/model/StandardSystemModel.java new file mode 100644 index 0000000..aa3e531 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/model/StandardSystemModel.java @@ -0,0 +1,476 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.model; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.guing.base.model.ModelComponent.MISCELLANEOUS; +import static org.opentcs.guing.base.model.ModelComponent.NAME; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.jhotdraw.draw.DefaultDrawing; +import org.jhotdraw.draw.Drawing; +import org.jhotdraw.draw.Figure; +import org.opentcs.data.model.ModelConstants; +import org.opentcs.data.model.visualization.Layer; +import org.opentcs.data.model.visualization.LayerGroup; +import org.opentcs.guing.base.components.layer.LayerWrapper; +import org.opentcs.guing.base.components.properties.type.KeyValueSetProperty; +import org.opentcs.guing.base.components.properties.type.StringProperty; +import org.opentcs.guing.base.model.CompositeModelComponent; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.SimpleFolder; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.base.model.elements.OtherGraphicalElement; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.drawing.course.CoordinateBasedDrawingMethod; +import org.opentcs.guing.common.components.drawing.course.DrawingMethod; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.guing.common.util.ModelComponentFactory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Base implementation for a SystemModel. + * Holds the vehicles and the layout of the model. The SystemModel has a map of base components + * for each component type (e.g. points, locations, vehicles, ...). + */ +public class StandardSystemModel + extends + CompositeModelComponent + implements + SystemModel { + + /** + * A Map that represents the main folder of the system model. + * Maps a folder key to the main model component for that folder. + */ + private final Map<FolderKey, ModelComponent> fMainFolders = new HashMap<>(); + /** + * Maps a model component class to a main folder component. + * Practically determines which folder a model component belongs to. + */ + private final Map<Class<?>, ModelComponent> fParentFolders = new HashMap<>(); + /** + * Maps model components to the corresponding figures. + */ + private final Map<ModelComponent, Figure> figuresMap = new HashMap<>(); + /** + * The drawing. + */ + private final Drawing fDrawing = new DefaultDrawing(); + /** + * The used drawing method. + */ + private final DrawingMethod fDrawingMethod = new CoordinateBasedDrawingMethod(); + + private final ModelComponentFactory modelComponentFactory; + + /** + * Creates a new instance with a default drawing method. + */ + @Inject + @SuppressWarnings("this-escape") + public StandardSystemModel(ModelComponentFactory modelComponentFactory) { + super("Model"); + this.modelComponentFactory = requireNonNull(modelComponentFactory, "modelComponentFactory"); + + createMainFolders(); + setupParentFolders(); + createProperties(); + } + + @Override + public KeyValueSetProperty getPropertyMiscellaneous() { + return (KeyValueSetProperty) getProperty(MISCELLANEOUS); + } + + @Override // SystemModel + public void addMainFolder(FolderKey key, ModelComponent component) { + fMainFolders.put(key, component); + } + + @Override // SystemModel + public ModelComponent getMainFolder(FolderKey key) { + return fMainFolders.get(key); + } + + @Override // SystemModel + public ModelComponent getFolder(ModelComponent item) { + if (item == null) { + return null; + } + + for (Class<?> c : fParentFolders.keySet()) { + if (item.getClass() == c || c.isInstance(item)) { + return fParentFolders.get(c); + } + } + + return null; + } + + @Override // SystemModel + public <T> List<T> getAll(FolderKey foldername, Class<T> classType) { + List<T> items = new ArrayList<>(); + for (ModelComponent o : getMainFolder(foldername).getChildComponents()) { + if (classType.isInstance(o)) { + items.add(classType.cast(o)); + } + } + + return items; + } + + @Override + public List<ModelComponent> getAll() { + List<ModelComponent> items = new ArrayList<>(); + for (ModelComponent o : fMainFolders.values()) { //Iterate over folders + if (o instanceof CompositeModelComponent) { + items.addAll(getAll((CompositeModelComponent) o)); + } + else { + items.add(o); + } + } + return items; + } + + @Override + public void onRestorationComplete() { + } + + @Override + public void registerFigure(ModelComponent component, Figure figure) { + figuresMap.put(component, figure); + } + + @Override + public Figure getFigure(ModelComponent component) { + return figuresMap.get(component); + } + + @Override // SystemModel + public Drawing getDrawing() { + return fDrawing; + } + + @Override // SystemModel + public DrawingMethod getDrawingMethod() { + return fDrawingMethod; + } + + @Override + public ModelComponent getModelComponent(String name) { + for (ModelComponent folder : fMainFolders.values()) { + ModelComponent component = getModelComponent(name, folder); + if (component != null) { + return component; + } + } + + return null; + } + + @Override // SystemModel + public List<VehicleModel> getVehicleModels() { + return getAll(FolderKey.VEHICLES, VehicleModel.class); + } + + @Override // SystemModel + public VehicleModel getVehicleModel(String name) { + for (VehicleModel vehicle : getVehicleModels()) { + if (vehicle.getName().equals(name)) { + return vehicle; + } + } + + return null; + } + + @Override // SystemModel + public LayoutModel getLayoutModel() { + return (LayoutModel) getMainFolder(FolderKey.LAYOUT); + } + + @Override // SystemModel + public List<PointModel> getPointModels() { + return getAll(FolderKey.POINTS, PointModel.class); + } + + @Override // SystemModel + public PointModel getPointModel(String name) { + for (PointModel point : getPointModels()) { + if (point.getName().equals(name)) { + return point; + } + } + + return null; + } + + @Override // SystemModel + public List<LocationModel> getLocationModels() { + return getAll(FolderKey.LOCATIONS, LocationModel.class); + } + + @Override // SystemModel + public List<LocationModel> getLocationModels(LocationTypeModel type) { + List<LocationModel> items = new ArrayList<>(); + for (LocationModel location : getLocationModels()) { + if (location.getLocationType() == type) { + items.add(location); + } + } + + return items; + } + + @Override // SystemModel + public LocationModel getLocationModel(String name) { + for (LocationModel location : getLocationModels()) { + if (location.getName().equals(name)) { + return location; + } + } + + return null; + } + + @Override // SystemModel + public List<PathModel> getPathModels() { + return getAll(FolderKey.PATHS, PathModel.class); + } + + @Override + public PathModel getPathModel(String name) { + for (PathModel path : getPathModels()) { + if (path.getName().equals(name)) { + return path; + } + } + + return null; + } + + @Override // SystemModel + public List<LinkModel> getLinkModels() { + return getAll(FolderKey.LINKS, LinkModel.class); + } + + @Override // SystemModel + public List<LinkModel> getLinkModels(LocationTypeModel locationType) { + List<LinkModel> items = new ArrayList<>(); + for (LinkModel ref : getLinkModels()) { + if (ref.getLocation().getLocationType() == locationType) { + items.add(ref); + } + } + + return items; + } + + @Override // SystemModel + public List<LocationTypeModel> getLocationTypeModels() { + return getAll(FolderKey.LOCATION_TYPES, LocationTypeModel.class); + } + + @Override // SystemModel + public LocationTypeModel getLocationTypeModel(String name) { + for (LocationTypeModel t : getLocationTypeModels()) { + if (t.getName().equals(name)) { + return t; + } + } + + return null; + } + + @Override + public BlockModel getBlockModel(String name) { + for (BlockModel block : getBlockModels()) { + if (block.getName().equals(name)) { + return block; + } + } + + return null; + } + + @Override // SystemModel + public List<BlockModel> getBlockModels() { + return getAll(FolderKey.BLOCKS, BlockModel.class); + } + + @Override // SystemModel + public List<OtherGraphicalElement> getOtherGraphicalElements() { + return getAll(FolderKey.OTHER_GRAPHICAL_ELEMENTS, OtherGraphicalElement.class); + } + + /** + * Return all model components in a folder. + * + * @param folder The folder of which to get the components from. + * @return A list of all model components in that folder. + */ + private List<ModelComponent> getAll(CompositeModelComponent folder) { + List<ModelComponent> result = new ArrayList<>(); + for (ModelComponent component : folder.getChildComponents()) { + if (component instanceof CompositeModelComponent) { + result.addAll(getAll((CompositeModelComponent) component)); + } + else { + result.add(component); + } + } + return result; + } + + private ModelComponent getModelComponent(String name, ModelComponent root) { + if (root instanceof CompositeModelComponent) { + for (ModelComponent subComponent : root.getChildComponents()) { + ModelComponent result = getModelComponent(name, subComponent); + if (result != null) { + return result; + } + } + } + else if (Objects.equals(name, root.getName())) { + return root; + } + return null; + } + + /** + * Creates the main folder in the system model. + */ + private void createMainFolders() { + ResourceBundleUtil bundle = ResourceBundleUtil.getBundle(I18nPlantOverview.TREEVIEW_PATH); + createMainFolder( + this, + FolderKey.VEHICLES, + new SimpleFolder( + bundle.getString("standardSystemModel.folder_vehicles.name") + ) + ); + + createMainFolder(this, FolderKey.LAYOUT, createDefaultLayoutModel()); + + createMainFolder( + getMainFolder(FolderKey.LAYOUT), + FolderKey.POINTS, + new SimpleFolder(bundle.getString("standardSystemModel.folder_points.name")) + ); + createMainFolder( + getMainFolder(FolderKey.LAYOUT), + FolderKey.PATHS, + new SimpleFolder(bundle.getString("standardSystemModel.folder_paths.name")) + ); + createMainFolder( + getMainFolder(FolderKey.LAYOUT), + FolderKey.LOCATIONS, + new SimpleFolder( + bundle.getString("standardSystemModel.folder_locations.name") + ) + ); + createMainFolder( + getMainFolder(FolderKey.LAYOUT), + FolderKey.LOCATION_TYPES, + new SimpleFolder( + bundle.getString("standardSystemModel.folder_locationTypes.name") + ) + ); + createMainFolder( + getMainFolder(FolderKey.LAYOUT), + FolderKey.LINKS, + new SimpleFolder(bundle.getString("standardSystemModel.folder_links.name")) + ); + createMainFolder( + getMainFolder(FolderKey.LAYOUT), + FolderKey.BLOCKS, + new SimpleFolder(bundle.getString("standardSystemModel.folder_blocks.name")) + ); + createMainFolder( + getMainFolder(FolderKey.LAYOUT), + FolderKey.OTHER_GRAPHICAL_ELEMENTS, + new SimpleFolder( + bundle.getString("standardSystemModel.folder_otherGraphicalElements.name") + ) + ); + } + + private LayoutModel createDefaultLayoutModel() { + LayoutModel layoutModel = modelComponentFactory.createLayoutModel(); + layoutModel.setName(ModelConstants.DEFAULT_VISUAL_LAYOUT_NAME); + LayerGroup defaultGroup = new LayerGroup( + ModelConstants.DEFAULT_LAYER_GROUP_ID, + ModelConstants.DEFAULT_LAYER_GROUP_NAME, + true + ); + layoutModel.getPropertyLayerGroups().getValue().put(defaultGroup.getId(), defaultGroup); + Layer defaultLayer = new Layer( + ModelConstants.DEFAULT_LAYER_ID, + ModelConstants.DEFAULT_LAYER_ORDINAL, + true, + ModelConstants.DEFAULT_LAYER_NAME, + ModelConstants.DEFAULT_LAYER_GROUP_ID + ); + layoutModel.getPropertyLayerWrappers().getValue() + .put(defaultLayer.getId(), new LayerWrapper(defaultLayer, defaultGroup)); + + return layoutModel; + } + + private void createProperties() { + ResourceBundleUtil bundle = ResourceBundleUtil.getBundle(I18nPlantOverview.TREEVIEW_PATH); + + StringProperty pName = new StringProperty(this); + pName.setDescription(bundle.getString("standardSystemModel.property_name.description")); + pName.setHelptext(bundle.getString("standardSystemModel.property_name.helptext")); + setProperty(NAME, pName); + + KeyValueSetProperty pMiscellaneous = new KeyValueSetProperty(this); + pMiscellaneous.setDescription("Miscellaneous properties"); + pMiscellaneous.setHelptext("Miscellaneous properties"); + setProperty(MISCELLANEOUS, pMiscellaneous); + } + + /** + * Creates a folder for the system model and the TreeView. + */ + private void createMainFolder( + ModelComponent parentFolder, + FolderKey key, + ModelComponent newFolder + ) { + addMainFolder(key, newFolder); + parentFolder.add(newFolder); + } + + /** + * Initialises the association between a main folder and a model component class. + */ + private void setupParentFolders() { + fParentFolders.put(VehicleModel.class, getMainFolder(FolderKey.VEHICLES)); + fParentFolders.put(LayoutModel.class, getMainFolder(FolderKey.LAYOUT)); + fParentFolders.put(PointModel.class, getMainFolder(FolderKey.POINTS)); + fParentFolders.put(PathModel.class, getMainFolder(FolderKey.PATHS)); + fParentFolders.put(LocationModel.class, getMainFolder(FolderKey.LOCATIONS)); + fParentFolders.put(LocationTypeModel.class, getMainFolder(FolderKey.LOCATION_TYPES)); + fParentFolders.put(LinkModel.class, getMainFolder(FolderKey.LINKS)); + fParentFolders.put(BlockModel.class, getMainFolder(FolderKey.BLOCKS)); + fParentFolders.put( + OtherGraphicalElement.class, + getMainFolder(FolderKey.OTHER_GRAPHICAL_ELEMENTS) + ); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/model/SystemModel.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/model/SystemModel.java new file mode 100644 index 0000000..47df60b --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/model/SystemModel.java @@ -0,0 +1,288 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.model; + +import java.util.List; +import org.jhotdraw.draw.Drawing; +import org.jhotdraw.draw.Figure; +import org.opentcs.guing.base.components.properties.type.KeyValueSetProperty; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.base.model.elements.OtherGraphicalElement; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.components.drawing.course.DrawingMethod; + +/** + * Interface for the date model of the whole system model. + */ +public interface SystemModel + extends + ModelComponent { + + /** + * Returns this model's set of miscellaneous properties. + * + * @return This model's set of miscellaneous properties. + */ + KeyValueSetProperty getPropertyMiscellaneous(); + + /** + * Add a main component to the system model. + * + * @param key The folder key + * @param component The model component to be added to the folder + */ + void addMainFolder(FolderKey key, ModelComponent component); + + /** + * Return the main model component for the folder key. + * + * @param key The folder key + * @return The model component that represents the main folder + */ + ModelComponent getMainFolder(FolderKey key); + + /** + * Returns the parent folder for a specified model component. + * + * @param item The model component where a parent folder should be found from + * @return The parent folder of the item + */ + ModelComponent getFolder(ModelComponent item); + + /** + * Return all object of a specified class in a folder. + * + * @param foldername Key of the folder in which to search. + * @param classType The class of the objects to find. + * @param <T> The type of objects returned. + * @return List of all object of specified type in the folder. + */ + <T> List<T> getAll(FolderKey foldername, Class<T> classType); + + /** + * Return all model components in the system model. + * + * @return List of all model components in the system model. + */ + List<ModelComponent> getAll(); + + /** + * Notifies the model that all elements have been restored. + */ + void onRestorationComplete(); + + /** + * Registers the given figure and associates it with the given model component. + * + * @param component The model component. + * @param figure The figure to register. + */ + void registerFigure(ModelComponent component, Figure figure); + + /** + * Returns the figure for the given model component. + * + * @param component The model component. + * @return The figure for the given model component. + */ + Figure getFigure(ModelComponent component); + + /** + * Return the drawing. + * + * @return The drawing. + */ + Drawing getDrawing(); + + /** + * Return the drawing method. + * + * @return The drawing method + */ + DrawingMethod getDrawingMethod(); + + /** + * Returns the component with the given name, if it exists. + * + * @param name The name. + * @return The component with the given name, or {@code null}, if it does not exist. + */ + ModelComponent getModelComponent(String name); + + /** + * Return a list of all vehicles. + * + * @return The list of vehicle models + */ + List<VehicleModel> getVehicleModels(); + + /** + * Return a vehicle with a specified name. + * + * @param name The name of the vehicle to search. + * @return The vehicle or null if vehicle is not found. + */ + VehicleModel getVehicleModel(String name); + + /** + * Returns the layout model. + * + * @return The layout model. + */ + LayoutModel getLayoutModel(); + + /** + * Return a list of all points. + * + * @return The list of points + */ + List<PointModel> getPointModels(); + + /** + * Return a point with the specified name. + * + * @param name The name of the point to return. + * @return The point or null if the name is not found. + */ + PointModel getPointModel(String name); + + /** + * Return all locations. + * + * @return List of all locations. + */ + List<LocationModel> getLocationModels(); + + /** + * Return all locations with a specified location type. + * + * @param locationType The location type. + * @return List of locations with the location type. + */ + List<LocationModel> getLocationModels(LocationTypeModel locationType); + + /** + * Return a location with the specified name. + * + * @param name The name of the location to return. + * @return The location or null if the name is not found. + */ + LocationModel getLocationModel(String name); + + /** + * Return all paths. + * + * @return A list of all paths. + */ + List<PathModel> getPathModels(); + + /** + * Return the PathModel with the given name. + * + * @param name Name of the path. + * @return The PathModel. + */ + PathModel getPathModel(String name); + + /** + * Return all links in the model. + * + * @return A list of all links. + */ + List<LinkModel> getLinkModels(); + + /** + * Return all links attached to a location with a specified location type. + * + * @param locationType The location type. + * @return A list of all links attached to a location with a specified location type. + */ + List<LinkModel> getLinkModels(LocationTypeModel locationType); + + /** + * Return all location types. + * + * @return List of all location types. + */ + List<LocationTypeModel> getLocationTypeModels(); + + /** + * Return the location type with the specified name. + * + * @param name The name of the location type to return. + * @return The location type. + */ + LocationTypeModel getLocationTypeModel(String name); + + /** + * Returns the block model for the given name. + * + * @param name The block's name. + * @return The block model. + */ + BlockModel getBlockModel(String name); + + /** + * Return all block models. + * + * @return A list of all block models. + */ + List<BlockModel> getBlockModels(); + + /** + * Return all figures that are used for visualisation purposes. + * + * @return A list of all figures that are used for visualisation purposes. + */ + List<OtherGraphicalElement> getOtherGraphicalElements(); + + /** + * Supported keys for the folders in a SystemModel. + */ + enum FolderKey { + + /** + * Vehicles. + */ + VEHICLES, + /** + * Layout. + */ + LAYOUT, + /** + * Points. + */ + POINTS, + /** + * Locations. + */ + LOCATIONS, + /** + * Paths. + */ + PATHS, + /** + * Links. + */ + LINKS, + /** + * Location types. + */ + LOCATION_TYPES, + /** + * Blocks. + */ + BLOCKS, + /** + * Other graphical elements. + */ + OTHER_GRAPHICAL_ELEMENTS + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/ModelExportAdapter.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/ModelExportAdapter.java new file mode 100644 index 0000000..eb10d18 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/ModelExportAdapter.java @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.persistence; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.Map; +import java.util.stream.Collectors; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.guing.base.components.properties.type.KeyValueProperty; +import org.opentcs.guing.base.components.properties.type.KeyValueSetProperty; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.exchange.adapter.ProcessAdapter; +import org.opentcs.guing.common.exchange.adapter.ProcessAdapterUtil; +import org.opentcs.guing.common.model.SystemModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Converts {@link SystemModel} instances to plant model data. + */ +public class ModelExportAdapter { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ModelExportAdapter.class); + /** + * Creates process adapter instances. + */ + private final ProcessAdapterUtil processAdapterUtil; + + /** + * Creates a new instance. + * + * @param processAdapterUtil Creates process adapter instances. + */ + @Inject + public ModelExportAdapter(ProcessAdapterUtil processAdapterUtil) { + this.processAdapterUtil = requireNonNull(processAdapterUtil, "processAdapterUtil"); + } + + /** + * Converts the given {@link SystemModel} instance to plant model data. + * + * @param systemModel The model to be converted. + * @return The converted model data. + * @throws IllegalArgumentException If the given plant model was inconsistent in some way. + */ + @Nonnull + public PlantModelCreationTO convert(SystemModel systemModel) + throws IllegalArgumentException { + requireNonNull(systemModel, "model"); + + PlantModelCreationTO plantModel = new PlantModelCreationTO(systemModel.getName()) + .withProperties(convertProperties(systemModel.getPropertyMiscellaneous())); + + long timeBefore = System.currentTimeMillis(); + plantModel = persist(systemModel.getLayoutModel(), systemModel, plantModel); + LOG.debug( + "Converting the LayoutModel took {} milliseconds.", + System.currentTimeMillis() - timeBefore + ); + + timeBefore = System.currentTimeMillis(); + for (PointModel model : systemModel.getPointModels()) { + plantModel = persist(model, systemModel, plantModel); + } + LOG.debug( + "Converting PointModels took {} milliseconds.", + System.currentTimeMillis() - timeBefore + ); + + timeBefore = System.currentTimeMillis(); + for (PathModel model : systemModel.getPathModels()) { + plantModel = persist(model, systemModel, plantModel); + } + LOG.debug( + "Converting PathModels took {} milliseconds.", + System.currentTimeMillis() - timeBefore + ); + + timeBefore = System.currentTimeMillis(); + for (LocationTypeModel model : systemModel.getLocationTypeModels()) { + plantModel = persist(model, systemModel, plantModel); + } + LOG.debug( + "Converting LocationTypeModels took {} milliseconds.", + System.currentTimeMillis() - timeBefore + ); + + timeBefore = System.currentTimeMillis(); + for (LocationModel model : systemModel.getLocationModels()) { + plantModel = persist(model, systemModel, plantModel); + } + LOG.debug( + "Converting LocationModels took {} milliseconds.", + System.currentTimeMillis() - timeBefore + ); + + timeBefore = System.currentTimeMillis(); + for (LinkModel model : systemModel.getLinkModels()) { + plantModel = persist(model, systemModel, plantModel); + } + LOG.debug( + "Converting LinkModels took {} milliseconds.", + System.currentTimeMillis() - timeBefore + ); + + timeBefore = System.currentTimeMillis(); + for (BlockModel model : systemModel.getBlockModels()) { + plantModel = persist(model, systemModel, plantModel); + } + LOG.debug( + "Converting BlockModels took {} milliseconds.", + System.currentTimeMillis() - timeBefore + ); + + timeBefore = System.currentTimeMillis(); + for (VehicleModel model : systemModel.getVehicleModels()) { + plantModel = persist(model, systemModel, plantModel); + } + LOG.debug( + "Converting VehicleModels took {} milliseconds.", + System.currentTimeMillis() - timeBefore + ); + + return plantModel; + } + + private Map<String, String> convertProperties(KeyValueSetProperty kvsp) { + return kvsp.getItems().stream() + .collect(Collectors.toMap(KeyValueProperty::getKey, KeyValueProperty::getValue)); + } + + private PlantModelCreationTO persist( + ModelComponent component, + SystemModel systemModel, + PlantModelCreationTO plantModel + ) { + ProcessAdapter adapter = processAdapterUtil.processAdapterFor(component); + return adapter.storeToPlantModel(component, systemModel, plantModel); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/ModelFilePersistor.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/ModelFilePersistor.java new file mode 100644 index 0000000..1c93efd --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/ModelFilePersistor.java @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.persistence; + +import jakarta.annotation.Nonnull; +import java.io.File; +import java.io.IOException; +import javax.swing.filechooser.FileFilter; +import org.opentcs.access.to.model.PlantModelCreationTO; + +/** + * Interface to persist a {@link PlantModelCreationTO} to a local file. + */ +public interface ModelFilePersistor { + + /** + * Persist the model to the given file. + * + * @param model The model to be serialized. + * @param file The file to serialize into. + * @return {@code true} if, and only if, the model was successfully serialized + * @throws java.io.IOException If an exception occurs + */ + boolean serialize(PlantModelCreationTO model, File file) + throws IOException; + + /** + * Returns the filter that declares which files are supported with this persistor. + * + * @return The filter that declares which files are supported with this persistor + */ + @Nonnull + FileFilter getDialogFileFilter(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/ModelFileReader.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/ModelFileReader.java new file mode 100644 index 0000000..6f41322 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/ModelFileReader.java @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.persistence; + +import jakarta.annotation.Nonnull; +import java.io.File; +import java.io.IOException; +import java.util.Optional; +import javax.swing.filechooser.FileFilter; +import org.opentcs.access.to.model.PlantModelCreationTO; + +/** + * Interface to read a file containing a {@link PlantModelCreationTO}. + */ +public interface ModelFileReader { + + /** + * Deserializes the model contained in the given file. + * + * @param file The {@link File} containing the model. + * @return The deserialized {@link PlantModelCreationTO} or {@link Optional#EMPTY} if + * deserialzation canceled. + * @throws java.io.IOException If an exception occured while reading + * the file. + */ + Optional<PlantModelCreationTO> deserialize(File file) + throws IOException; + + /** + * Returns the filter that declares which files are supported with this reader. + * + * @return The filter that declares which files are supported with this reader + */ + @Nonnull + FileFilter getDialogFileFilter(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/ModelManager.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/ModelManager.java new file mode 100644 index 0000000..018c1aa --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/ModelManager.java @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.persistence; + +import org.opentcs.access.KernelServicePortal; +import org.opentcs.guing.common.model.SystemModel; + +/** + * Manages (loads, persists and keeps) the driving course model. + */ +public interface ModelManager { + + /** + * Returns the current system model. + * + * @return The current system model. + */ + SystemModel getModel(); + + /** + * Creates a new, empty system model. + */ + void createEmptyModel(); + + /** + * Saves the given system model to a file. + * + * @param chooseName Whether a dialog to choose a file name shall be shown. + * @return Whether the model was actually saved. + */ + boolean saveModelToFile(boolean chooseName); + + /** + * Creates figures and process adapters for all model components in the current system model. + */ + void restoreModel(); + + /** + * Loads all model objects from the kernel and creates the corresponding + * figures. + * + * @param portal The kernel client portal. + */ + void restoreModel(KernelServicePortal portal); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/OpenTCSModelManager.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/OpenTCSModelManager.java new file mode 100644 index 0000000..f6d9d7e --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/OpenTCSModelManager.java @@ -0,0 +1,1060 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.persistence; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.guing.common.util.I18nPlantOverview.MISC_PATH; +import static org.opentcs.util.Assertions.checkState; + +import com.google.common.base.Strings; +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import java.awt.geom.Point2D; +import java.io.File; +import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JOptionPane; +import org.jhotdraw.draw.Figure; +import org.opentcs.access.Kernel; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.customizations.ApplicationHome; +import org.opentcs.customizations.plantoverview.ApplicationFrame; +import org.opentcs.data.ObjectPropConstants; +import org.opentcs.data.model.Block; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Location.Link; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.ModelConstants; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.model.visualization.VisualLayout; +import org.opentcs.guing.base.components.properties.event.NullAttributesChangeListener; +import org.opentcs.guing.base.components.properties.type.KeyValueProperty; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.model.FigureDecorationDetails; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.base.model.elements.VehicleModel; +import org.opentcs.guing.common.application.ModelRestorationProgressStatus; +import org.opentcs.guing.common.application.ProgressIndicator; +import org.opentcs.guing.common.application.StatusPanel; +import org.opentcs.guing.common.components.drawing.course.Origin; +import org.opentcs.guing.common.components.drawing.course.OriginChangeListener; +import org.opentcs.guing.common.components.drawing.figures.FigureConstants; +import org.opentcs.guing.common.components.drawing.figures.LabeledLocationFigure; +import org.opentcs.guing.common.components.drawing.figures.LabeledPointFigure; +import org.opentcs.guing.common.components.drawing.figures.LinkConnection; +import org.opentcs.guing.common.components.drawing.figures.LocationFigure; +import org.opentcs.guing.common.components.drawing.figures.PathConnection; +import org.opentcs.guing.common.components.drawing.figures.PointFigure; +import org.opentcs.guing.common.components.drawing.figures.TCSLabelFigure; +import org.opentcs.guing.common.exchange.adapter.ProcessAdapterUtil; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.guing.common.util.CourseObjectFactory; +import org.opentcs.guing.common.util.ModelComponentFactory; +import org.opentcs.guing.common.util.SynchronizedFileChooser; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages (loads, persists and keeps) the driving course model. + */ +public class OpenTCSModelManager + implements + ModelManager { + + /** + * Identifier for the default layout object. + */ + public static final String DEFAULT_LAYOUT = "Default"; + /** + * Directory where models will be persisted. + */ + public static final String MODEL_DIRECTORY = "data/"; + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(OpenTCSModelManager.class); + /** + * Provides string localization. + */ + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MISC_PATH); + /** + * The application's main frame. + */ + private final JFrame applicationFrame; + /** + * The StatusPanel at the bottom to log messages. + */ + private final StatusPanel statusPanel; + /** + * A course object factory to be used. + */ + private final CourseObjectFactory crsObjFactory; + /** + * The model component factory to be used. + */ + private final ModelComponentFactory modelComponentFactory; + /** + * Provides new instances of SystemModel. + */ + private final Provider<SystemModel> systemModelProvider; + /** + * A utility class for process adapters. + */ + private final ProcessAdapterUtil procAdapterUtil; + /** + * A file chooser for selecting model files to be saved. + */ + private final JFileChooser modelPersistorFileChooser; + /** + * The file filters for different model persistors that can be used to save models to a file. + */ + private final ModelFilePersistor modelPersistor; + /** + * Converts model data on export. + */ + private final ModelExportAdapter modelExportAdapter; + /** + * The progress indicator to be used. + */ + private final ProgressIndicator progressIndicator; + /** + * The model currently loaded. + */ + private SystemModel systemModel; + /** + * The current system model's name. + */ + private String fModelName = ""; + /** + * The file which the current model is loaded from/written to. + */ + private File currentModelFile; + + /** + * Creates a new instance. + * + * @param applicationFrame The application's main frame. + * @param crsObjFactory A course object factory to be used. + * @param modelComponentFactory The model component factory to be used. + * @param procAdapterUtil A utility class for process adapters. + * @param systemModelProvider Provides instances of SystemModel. + * @param statusPanel StatusPanel to log messages. + * @param homeDir The application's home directory. + * @param modelPersistor The model persistor. + * @param modelExportAdapter Converts model data on export. + * @param progressIndicator The progress indicator to be used. + */ + @Inject + @SuppressWarnings("this-escape") + public OpenTCSModelManager( + @ApplicationFrame + JFrame applicationFrame, + CourseObjectFactory crsObjFactory, + ModelComponentFactory modelComponentFactory, + ProcessAdapterUtil procAdapterUtil, + Provider<SystemModel> systemModelProvider, + StatusPanel statusPanel, + @ApplicationHome + File homeDir, + ModelFilePersistor modelPersistor, + ModelExportAdapter modelExportAdapter, + ProgressIndicator progressIndicator + ) { + this.applicationFrame = requireNonNull(applicationFrame, "applicationFrame"); + this.crsObjFactory = requireNonNull(crsObjFactory, "crsObjFactory"); + this.modelComponentFactory = requireNonNull(modelComponentFactory, "modelComponentFactory"); + this.procAdapterUtil = requireNonNull(procAdapterUtil, "procAdapterUtil"); + this.systemModelProvider = requireNonNull(systemModelProvider, "systemModelProvider"); + this.statusPanel = requireNonNull(statusPanel, "statusPanel"); + requireNonNull(homeDir, "homeDir"); + + this.modelPersistor = requireNonNull(modelPersistor, "modelPersistor"); + this.modelPersistorFileChooser = new SynchronizedFileChooser(new File(homeDir, "data")); + this.modelPersistorFileChooser.setAcceptAllFileFilterUsed(false); + this.modelPersistorFileChooser.setFileFilter(modelPersistor.getDialogFileFilter()); + + this.modelExportAdapter = requireNonNull(modelExportAdapter, "modelExportAdapter"); + this.progressIndicator = progressIndicator; + + this.systemModel = systemModelProvider.get(); + initializeSystemModel(systemModel); + } + + @Override + public SystemModel getModel() { + return systemModel; + } + + @Override + public boolean saveModelToFile(boolean chooseName) { + fModelName = systemModel.getName(); + if (chooseName + || currentModelFile == null + || fModelName == null + || fModelName.isEmpty() + || fModelName.equals(Kernel.DEFAULT_MODEL_NAME)) { + File selectedFile = showSaveDialog(); + if (selectedFile == null) { + return false; + } + currentModelFile = selectedFile; + } + try { + statusPanel.clear(); + + // Set the last-modified time stamp of the model to right now, as we're saving the model file. + systemModel.getPropertyMiscellaneous().addItem( + new KeyValueProperty( + systemModel, + ObjectPropConstants.MODEL_FILE_LAST_MODIFIED, + Instant.now().truncatedTo(ChronoUnit.SECONDS).toString() + ) + ); + + return persistModel( + systemModel, + currentModelFile, + modelPersistor + ); + + } + catch (IOException e) { + statusPanel.setLogMessage( + Level.SEVERE, + BUNDLE.getString("openTcsModelManager.message_notSaved.text") + ); + LOG.warn("Exception persisting model", e); + return false; + } + catch (IllegalArgumentException e) { + JOptionPane.showConfirmDialog(statusPanel, e.getMessage()); + return true; + } + } + + @Override + public void createEmptyModel() { + systemModel = systemModelProvider.get(); + initializeSystemModel(systemModel); + systemModel.setName(Kernel.DEFAULT_MODEL_NAME); + fModelName = systemModel.getName(); + } + + /** + * Persist model with the persistor. + * + * @param systemModel The system model to be persisted. + * @param file The file to persist the system model into. + * @param persistor The persistor to be used. + */ + private boolean persistModel( + SystemModel systemModel, + File file, + ModelFilePersistor persistor + ) + throws IOException, + KernelRuntimeException { + requireNonNull(systemModel, "systemModel"); + requireNonNull(persistor, "persistor"); + + if (!persistor.serialize(modelExportAdapter.convert(systemModel), file)) { + return false; + } + + systemModel.setName(fModelName); + return true; + } + + @Override + public void restoreModel() { + fModelName = systemModel.getName(); + + LayoutModel layoutModel = (LayoutModel) systemModel.getMainFolder(SystemModel.FolderKey.LAYOUT); + double scaleX = layoutModel.getPropertyScaleX().getValueByUnit(LengthProperty.Unit.MM); + double scaleY = layoutModel.getPropertyScaleY().getValueByUnit(LengthProperty.Unit.MM); + + Origin origin = systemModel.getDrawingMethod().getOrigin(); + + // Create figures and process adapters + progressIndicator.setProgress(ModelRestorationProgressStatus.LOADING_POINTS); + List<Figure> points = restorePointsInModel(systemModel.getPointModels(), scaleX, scaleY); + systemModel.getDrawing().addAll(associateFiguresWithOrigin(points, origin)); + + progressIndicator.setProgress(ModelRestorationProgressStatus.LOADING_PATHS); + List<Figure> paths = restorePathsInModel(systemModel.getPathModels(), systemModel); + systemModel.getDrawing().addAll(associateFiguresWithOrigin(paths, origin)); + + progressIndicator.setProgress(ModelRestorationProgressStatus.LOADING_LOCATIONS); + List<Figure> locations = restoreLocationsInModel( + systemModel.getLocationModels(), + scaleX, + scaleY, + systemModel + ); + systemModel.getDrawing().addAll(associateFiguresWithOrigin(locations, origin)); + + progressIndicator.setProgress(ModelRestorationProgressStatus.LOADING_BLOCKS); + List<Figure> blocks = restoreBlocksInModel(systemModel.getBlockModels(), systemModel); + systemModel.getDrawing().addAll(associateFiguresWithOrigin(blocks, origin)); + restoreBlockComponentDecorationDetails(systemModel); + } + + private List<Figure> associateFiguresWithOrigin(List<Figure> figures, Origin origin) { + for (Figure figure : figures) { + if (figure instanceof OriginChangeListener) { + origin.addListener((OriginChangeListener) figure); + figure.set(FigureConstants.ORIGIN, origin); + } + } + return figures; + } + + @Override + public void restoreModel(KernelServicePortal portal) { + requireNonNull(portal, "portal"); + + createEmptyModel(); + + fModelName = portal.getPlantModelService().getModelName(); + systemModel.getPropertyName().setText(fModelName); + portal.getPlantModelService().getModelProperties().entrySet().stream() + .forEach( + entry -> systemModel.getPropertyMiscellaneous().addItem( + new KeyValueProperty(systemModel, entry.getKey(), entry.getValue()) + ) + ); + + TCSObjectService objectService = (TCSObjectService) portal.getPlantModelService(); + + Set<VisualLayout> allVisualLayouts = objectService.fetchObjects(VisualLayout.class); + checkState( + allVisualLayouts.size() == 1, + "There has to be one, and only one, visual layout. Number of visual layouts: %d", + allVisualLayouts.size() + ); + Set<Vehicle> allVehicles = objectService.fetchObjects(Vehicle.class); + Set<Point> allPoints = objectService.fetchObjects(Point.class); + Set<LocationType> allLocationTypes = objectService.fetchObjects(LocationType.class); + Set<Location> allLocations = objectService.fetchObjects(Location.class); + Set<Path> allPaths = objectService.fetchObjects(Path.class); + Set<Block> allBlocks = objectService.fetchObjects(Block.class); + + List<Figure> restoredFigures = new ArrayList<>(); + + Origin origin = systemModel.getDrawingMethod().getOrigin(); + + VisualLayout visualLayout = allVisualLayouts.iterator().next(); + if (visualLayout.getScaleX() != 0.0 && visualLayout.getScaleY() != 0.0) { + origin.setScale(visualLayout.getScaleX(), visualLayout.getScaleY()); + } + + LayoutModel layoutModel + = (LayoutModel) systemModel.getMainFolder(SystemModel.FolderKey.LAYOUT); + + prepareLayout(layoutModel, systemModel, origin, objectService, visualLayout); + + progressIndicator.setProgress(ModelRestorationProgressStatus.LOADING_POINTS); + restoreModelPoints(allPoints, systemModel, origin, restoredFigures, objectService); + progressIndicator.setProgress(ModelRestorationProgressStatus.LOADING_PATHS); + restoreModelPaths(allPaths, systemModel, origin, restoredFigures, objectService); + progressIndicator.setProgress(ModelRestorationProgressStatus.LOADING_LOCATIONS); + restoreModelLocationTypes(allLocationTypes, systemModel, objectService); + restoreModelLocations(allLocations, systemModel, origin, restoredFigures, objectService); + progressIndicator.setProgress(ModelRestorationProgressStatus.LOADING_VEHICLES); + restoreModelVehicles(allVehicles, systemModel, objectService); + progressIndicator.setProgress(ModelRestorationProgressStatus.LOADING_BLOCKS); + restoreModelBlocks(allBlocks, systemModel, objectService); + restoreBlockComponentDecorationDetails(systemModel); + + systemModel.getDrawing().addAll(restoredFigures); + systemModel.onRestorationComplete(); + } + + private void restoreBlockComponentDecorationDetails(SystemModel systemModel) { + for (BlockModel model : systemModel.getBlockModels()) { + for (ModelComponent blockElement : model.getChildComponents()) { + if (!(blockElement instanceof FigureDecorationDetails)) { + continue; + } + + ((FigureDecorationDetails) blockElement).addBlockModel(model); + } + } + } + + private void prepareLayout( + LayoutModel layoutModel, + SystemModel systemModel, + Origin origin, + TCSObjectService objectService, + @Nullable + VisualLayout layout + ) { + layoutModel.getPropertyName().setText(ModelConstants.DEFAULT_VISUAL_LAYOUT_NAME); + layoutModel.getPropertyScaleX().setValueAndUnit( + origin.getScaleX(), + LengthProperty.Unit.MM + ); + layoutModel.getPropertyScaleY().setValueAndUnit( + origin.getScaleY(), + LengthProperty.Unit.MM + ); + + if (layout != null) { + procAdapterUtil.processAdapterFor(layoutModel) + .updateModelProperties(layout, layoutModel, systemModel, objectService); + } + } + + private List<Figure> restoreBlocksInModel(List<BlockModel> blockModels, SystemModel systemModel) { + for (BlockModel blockModel : blockModels) { + // XXX This should probably be done when the block model is created, not here. + for (String elementName : blockModel.getPropertyElements().getItems()) { + ModelComponent modelComponent = getBlockMember(systemModel, elementName); + if (modelComponent != null) { + blockModel.addCourseElement(modelComponent); + } + } + } + + return new ArrayList<>(); + } + + private void restoreModelBlocks( + Set<Block> allBlocks, SystemModel systemModel, + TCSObjectService objectService + ) { + for (Block block : allBlocks) { + BlockModel blockModel = modelComponentFactory.createBlockModel(); + procAdapterUtil.processAdapterFor(blockModel) + .updateModelProperties(block, blockModel, systemModel, objectService); + + systemModel.getMainFolder(SystemModel.FolderKey.BLOCKS).add(blockModel); + } + } + + private List<Figure> restoreLocationsInModel( + List<LocationModel> locationModels, + double scaleX, + double scaleY, + SystemModel systemModel + ) { + List<Figure> restoredFigures = new ArrayList<>(locationModels.size()); + + for (LocationModel locationModel : locationModels) { + LabeledLocationFigure llf = createLocationFigure(locationModel, scaleX, scaleY); + + systemModel.registerFigure(locationModel, llf); + locationModel.addAttributesChangeListener(llf); + + String locationTypeName = (String) locationModel.getPropertyType().getValue(); + locationModel.setLocationType(getLocationTypeComponent(systemModel, locationTypeName)); + + for (LinkModel linkModel : getAttachedLinks(systemModel, locationModel)) { + LinkConnection linkConnection = createLinkFigure(linkModel, llf); + + systemModel.registerFigure(linkModel, linkConnection); + linkModel.addAttributesChangeListener(linkConnection); + restoredFigures.add(linkConnection); + } + + locationModel.propertiesChanged(new NullAttributesChangeListener()); + restoredFigures.add(llf); + } + + return restoredFigures; + } + + private LabeledLocationFigure createLocationFigure( + LocationModel locationModel, + double scaleX, + double scaleY + ) { + LabeledLocationFigure llf = crsObjFactory.createLocationFigure(); + LocationFigure locationFigure = llf.getPresentationFigure(); + locationFigure.set(FigureConstants.MODEL, locationModel); + + // The corresponding label + TCSLabelFigure label = new TCSLabelFigure(locationModel.getName()); + + Point2D.Double labelPosition; + Point2D.Double figurePosition; + double figurePositionX = 0; + double figurePositionY = 0; + String locPosX = locationModel.getPropertyLayoutPositionX().getText(); + String locPosY = locationModel.getPropertyLayoutPositionY().getText(); + if (locPosX != null && locPosY != null) { + try { + figurePositionX = Integer.parseInt(locPosX); + figurePositionY = Integer.parseInt(locPosY); + } + catch (NumberFormatException ex) { + } + } + + // Label + String labelOffsetX = locationModel.getPropertyLabelOffsetX().getText(); + String labelOffsetY = locationModel.getPropertyLabelOffsetY().getText(); + + int labelPositionX; + int labelPositionY; + if (labelOffsetX != null && labelOffsetY != null) { + try { + labelPositionX = Integer.parseInt(labelOffsetX); + labelPositionY = Integer.parseInt(labelOffsetY); + } + catch (NumberFormatException ex) { + // XXX This does not look right. + labelPositionX = -20; + labelPositionY = -20; + } + + label.setOffset(labelPositionX, labelPositionY); + } + figurePosition = new Point2D.Double(figurePositionX / scaleX, -figurePositionY / scaleY); + locationFigure.setBounds(figurePosition, figurePosition); + + labelPosition = locationFigure.getStartPoint(); + labelPosition.x += label.getOffset().x; + labelPosition.y += label.getOffset().y; + label.setBounds(labelPosition, null); + llf.setLabel(label); + + return llf; + } + + private LinkConnection createLinkFigure(LinkModel linkModel, LabeledLocationFigure llf) { + PointModel pointModel = linkModel.getPoint(); + LabeledPointFigure lpf = (LabeledPointFigure) systemModel.getFigure(pointModel); + LinkConnection linkConnection = crsObjFactory.createLinkConnection(); + linkConnection.set(FigureConstants.MODEL, linkModel); + linkConnection.connect(lpf, llf); + linkConnection.getModel().updateName(); + + return linkConnection; + } + + private void restoreModelLocations( + Set<Location> allLocations, + SystemModel systemModel, + Origin origin, + List<Figure> restoredFigures, + TCSObjectService objectService + ) { + for (Location location : allLocations) { + LocationModel locationModel = new LocationModel(); + procAdapterUtil.processAdapterFor(locationModel) + .updateModelProperties(location, locationModel, systemModel, objectService); + + LabeledLocationFigure llf = createLocationFigure( + locationModel, + origin.getScaleX(), + origin.getScaleY() + ); + + systemModel.registerFigure(locationModel, llf); + locationModel.addAttributesChangeListener(llf); + systemModel.getMainFolder(SystemModel.FolderKey.LOCATIONS).add(locationModel); + restoredFigures.add(llf); + + LocationTypeModel type = getLocationTypeComponent(systemModel, location.getType().getName()); + locationModel.setLocationType(type); + locationModel.updateTypeProperty(systemModel.getLocationTypeModels()); + locationModel.propertiesChanged(new NullAttributesChangeListener()); + + for (Link link : location.getAttachedLinks()) { + PointModel pointModel = getPointComponent(systemModel, link.getPoint().getName()); + + LinkModel linkModel = new LinkModel(); + + linkModel.setConnectedComponents(pointModel, locationModel); + linkModel.updateName(); + linkModel.getPropertyStartComponent().setText(pointModel.getName()); + linkModel.getPropertyEndComponent().setText(locationModel.getName()); + linkModel.getPropertyAllowedOperations() + .setItems(new ArrayList<>(link.getAllowedOperations())); + linkModel.getPropertyLayerWrapper() + .setValue(locationModel.getPropertyLayerWrapper().getValue()); + + LinkConnection linkConnection = createLinkFigure(linkModel, llf); + + systemModel.registerFigure(linkModel, linkConnection); + linkModel.addAttributesChangeListener(linkConnection); + systemModel.getMainFolder(SystemModel.FolderKey.LINKS).add(linkModel); + restoredFigures.add(linkConnection); + } + + origin.addListener(llf); + llf.set(FigureConstants.ORIGIN, origin); + } + } + + private void restoreModelLocationTypes( + Set<LocationType> allLocationTypes, + SystemModel systemModel, + TCSObjectService objectService + ) { + for (LocationType locationType : allLocationTypes) { + LocationTypeModel locationTypeModel = modelComponentFactory.createLocationTypeModel(); + procAdapterUtil.processAdapterFor(locationTypeModel) + .updateModelProperties(locationType, locationTypeModel, systemModel, objectService); + systemModel.getMainFolder(SystemModel.FolderKey.LOCATION_TYPES).add(locationTypeModel); + } + } + + private void restoreModelVehicles( + Set<Vehicle> allVehicles, + SystemModel systemModel, + TCSObjectService objectService + ) { + for (Vehicle vehicle : allVehicles) { + VehicleModel vehicleModel = modelComponentFactory.createVehicleModel(); + vehicleModel.setVehicle(vehicle); + procAdapterUtil.processAdapterFor(vehicleModel) + .updateModelProperties(vehicle, vehicleModel, systemModel, objectService); + + systemModel.getMainFolder(SystemModel.FolderKey.VEHICLES).add(vehicleModel); + // VehicleFigures will be created in OpenTCSDrawingView.setVehicles(). + } + } + + private List<Figure> restorePathsInModel(List<PathModel> paths, SystemModel systemModel) { + List<Figure> restoredFigures = new ArrayList<>(paths.size()); + + for (PathModel pathModel : paths) { + PathConnection pathFigure = createPathFigure(pathModel, systemModel); + + systemModel.registerFigure(pathModel, pathFigure); + pathModel.addAttributesChangeListener(pathFigure); + + restoredFigures.add(pathFigure); + } + + return restoredFigures; + } + + private PathConnection createPathFigure(PathModel pathModel, SystemModel systemModel) { + PathConnection pathFigure = crsObjFactory.createPathConnection(); + + pathFigure.set(FigureConstants.MODEL, pathModel); + PointModel startPointModel = getPointComponent( + systemModel, + pathModel.getPropertyStartComponent().getText() + ); + PointModel endPointModel = getPointComponent( + systemModel, + pathModel.getPropertyEndComponent().getText() + ); + if (startPointModel != null && endPointModel != null) { + LabeledPointFigure startFigure = (LabeledPointFigure) systemModel.getFigure(startPointModel); + LabeledPointFigure endFigure = (LabeledPointFigure) systemModel.getFigure(endPointModel); + pathFigure.connect(startFigure, endFigure); + } + + PathModel.Type connectionType + = (PathModel.Type) pathModel.getPropertyPathConnType().getValue(); + + if (connectionType != null) { + initPathControlPoints( + connectionType, + pathModel.getPropertyPathControlPoints().getText(), + pathFigure + ); + pathFigure.setLinerByType(connectionType); + } + + pathFigure.updateDecorations(); + + return pathFigure; + } + + private void restoreModelPaths( + Set<Path> allPaths, SystemModel systemModel, + Origin origin, + List<Figure> restoredFigures, + TCSObjectService objectService + ) { + for (Path path : allPaths) { + PathModel pathModel = new PathModel(); + procAdapterUtil.processAdapterFor(pathModel) + .updateModelProperties(path, pathModel, systemModel, objectService); + + PathConnection pathFigure = createPathFigure(pathModel, systemModel); + + systemModel.registerFigure(pathModel, pathFigure); + pathModel.addAttributesChangeListener(pathFigure); + systemModel.getMainFolder(SystemModel.FolderKey.PATHS).add(pathModel); + restoredFigures.add(pathFigure); + origin.addListener(pathFigure); + pathFigure.set(FigureConstants.ORIGIN, origin); + } + } + + private void initPathControlPoints( + PathModel.Type connectionType, + String sControlPoints, + PathConnection pathFigure + ) { + if (connectionType != PathModel.Type.BEZIER + && connectionType != PathModel.Type.BEZIER_3) { + return; + } + if (Strings.isNullOrEmpty(sControlPoints)) { + return; + } + + // Format: x1,y1 or x1,y1;x2,y2 + String[] values = sControlPoints.split("[,;]"); + + try { + if (values.length >= 2) { + int xcp1 = (int) Double.parseDouble(values[0]); + int ycp1 = (int) Double.parseDouble(values[1]); + Point2D.Double cp1 = new Point2D.Double(xcp1, ycp1); + + if (values.length >= 4) { + int xcp2 = (int) Double.parseDouble(values[2]); + int ycp2 = (int) Double.parseDouble(values[3]); + Point2D.Double cp2 = new Point2D.Double(xcp2, ycp2); + + if (values.length >= 10) { + int xcp3 = (int) Double.parseDouble(values[4]); + int ycp3 = (int) Double.parseDouble(values[5]); + int xcp4 = (int) Double.parseDouble(values[6]); + int ycp4 = (int) Double.parseDouble(values[7]); + int xcp5 = (int) Double.parseDouble(values[8]); + int ycp5 = (int) Double.parseDouble(values[9]); + Point2D.Double cp3 = new Point2D.Double(xcp3, ycp3); + Point2D.Double cp4 = new Point2D.Double(xcp4, ycp4); + Point2D.Double cp5 = new Point2D.Double(xcp5, ycp5); + pathFigure.addControlPoints(cp1, cp2, cp3, cp4, cp5); + } + else { + pathFigure.addControlPoints(cp1, cp2); // Cubic curve + } + } + else { + pathFigure.addControlPoints(cp1, cp1); // Quadratic curve + } + } + } + catch (NumberFormatException nfex) { + LOG.info("Error while parsing bezier control points.", nfex); + } + } + + private List<Figure> restorePointsInModel( + List<PointModel> points, + double scaleX, + double scaleY + ) { + List<Figure> restoredFigures = new ArrayList<>(points.size()); + + for (PointModel pointModel : points) { + LabeledPointFigure lpf = createPointFigure(pointModel, scaleX, scaleY); + + systemModel.registerFigure(pointModel, lpf); + pointModel.addAttributesChangeListener(lpf); + restoredFigures.add(lpf); + } + + return restoredFigures; + } + + private LabeledPointFigure createPointFigure( + PointModel pointModel, + double scaleX, + double scaleY + ) { + LabeledPointFigure lpf = crsObjFactory.createPointFigure(); + PointFigure pointFigure = lpf.getPresentationFigure(); + pointFigure.setModel(pointModel); + + // The corresponding label + TCSLabelFigure label = new TCSLabelFigure(pointModel.getName()); + + Point2D.Double labelPosition; + Point2D.Double figurePosition; + double figurePositionX = 0; + double figurePositionY = 0; + String pointPosX = pointModel.getPropertyLayoutPosX().getText(); + String pointPosY = pointModel.getPropertyLayoutPosY().getText(); + if (pointPosX != null && pointPosY != null) { + try { + figurePositionX = Integer.parseInt(pointPosX); + figurePositionY = Integer.parseInt(pointPosY); + } + catch (NumberFormatException ex) { + } + } + + // Label + String labelOffsetX = pointModel.getPropertyPointLabelOffsetX().getText(); + String labelOffsetY = pointModel.getPropertyPointLabelOffsetY().getText(); + + int labelPositionX; + int labelPositionY; + if (labelOffsetX != null && labelOffsetY != null) { + try { + labelPositionX = Integer.parseInt(labelOffsetX); + labelPositionY = Integer.parseInt(labelOffsetY); + } + catch (NumberFormatException ex) { + // XXX This does not look right. + labelPositionX = -20; + labelPositionY = -20; + } + + label.setOffset(labelPositionX, labelPositionY); + } + figurePosition = new Point2D.Double(figurePositionX / scaleX, -figurePositionY / scaleY); + pointFigure.setBounds(figurePosition, figurePosition); + + labelPosition = pointFigure.getStartPoint(); + labelPosition.x += label.getOffset().x; + labelPosition.y += label.getOffset().y; + label.setBounds(labelPosition, null); + lpf.setLabel(label); + + return lpf; + } + + private void restoreModelPoints( + Set<Point> allPoints, SystemModel systemModel, + Origin origin, + List<Figure> restoredFigures, + TCSObjectService objectService + ) { + for (Point point : allPoints) { + PointModel pointModel = new PointModel(); + procAdapterUtil.processAdapterFor(pointModel) + .updateModelProperties(point, pointModel, systemModel, objectService); + + LabeledPointFigure lpf = createPointFigure( + pointModel, + origin.getScaleX(), + origin.getScaleY() + ); + + systemModel.registerFigure(pointModel, lpf); + pointModel.addAttributesChangeListener(lpf); + systemModel.getMainFolder(SystemModel.FolderKey.POINTS).add(pointModel); + restoredFigures.add(lpf); + + origin.addListener(lpf); + lpf.set(FigureConstants.ORIGIN, origin); + } + } + + /** + * Shows a dialog to save a model locally. + * + * @return The selected file or <code>null</code>, if nothing was selected. + */ + private File showSaveDialog() { + if (!modelPersistorFileChooser.getCurrentDirectory().isDirectory()) { + modelPersistorFileChooser.getCurrentDirectory().mkdir(); + } + if (modelPersistorFileChooser.showSaveDialog(applicationFrame) != JFileChooser.APPROVE_OPTION) { + fModelName = Kernel.DEFAULT_MODEL_NAME; + return null; + } + + File selectedFile = ensureXmlExtensionIsPresent(modelPersistorFileChooser.getSelectedFile()); + + if (selectedFile.exists()) { + int response = JOptionPane.showConfirmDialog( + applicationFrame, + BUNDLE.getString("openTcsModelManager.optionPane_fileExists.message"), + BUNDLE.getString("openTcsModelManager.optionPane_fileExists.title"), + JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE + ); + + if (response != JOptionPane.YES_OPTION) { + return null; + } + } + + // Extract the model name from the file name, but without the extension. + fModelName = selectedFile.getName().replaceFirst("[.][^.]+$", ""); + if (fModelName.isEmpty()) { + fModelName = Kernel.DEFAULT_MODEL_NAME; + return null; + } + + return selectedFile; + } + + private File ensureXmlExtensionIsPresent(File file) { + String fileName = file.getName(); + + if (!fileName.endsWith(".xml")) { + return new File(file.getParent(), fileName + ".xml"); + } + return file; + } + + /** + * Return the point model component with the given name from the + * system model. + * + * @param name The name of the point to return. + * @return The PointModel that matches the given name. + */ + private PointModel getPointComponent(SystemModel systemModel, String name) { + for (PointModel modelComponent : systemModel.getPointModels()) { + if (modelComponent.getName().equals(name)) { + return modelComponent; + } + } + return null; + } + + /** + * Returns the location type model component with the given name from the + * system model. + * + * @param name The name of the location type to return. + * @return The LocationModel that matches the given name. + */ + private LocationTypeModel getLocationTypeComponent(SystemModel systemModel, String name) { + for (LocationTypeModel modelComponent : systemModel.getLocationTypeModels()) { + if (modelComponent.getName().equals(name)) { + return modelComponent; + } + } + return null; + } + + /** + * Returns a <code>ModelComponent</code> with the given name that is + * a member of a block. + * + * @param name The name of the ModelComponent to return. + * @return The ModelComponent. + */ + private ModelComponent getBlockMember(SystemModel systemModel, String name) { + for (PointModel pModel : systemModel.getPointModels()) { + if (name.equals(pModel.getName())) { + return pModel; + } + } + for (PathModel pModel : systemModel.getPathModels()) { + if (name.equals(pModel.getName())) { + return pModel; + } + } + for (LocationModel lModel : systemModel.getLocationModels()) { + if (name.equals(lModel.getName())) { + return lModel; + } + } + return null; + } + + /** + * Returns the attached links to the given location model. After persisting + * the LinkModels in the system model contain the names of the + * connected components in the specific properties. The components are + * searched here and are set as the connected components in the link. + * + * @param locationModel The LocationModel for which we need the connected + * links. + * @return A list with the connected links. + */ + private List<LinkModel> getAttachedLinks(SystemModel systemModel, LocationModel locationModel) { + List<LinkModel> links = new ArrayList<>(); + String locationName = locationModel.getName(); + for (LinkModel link : systemModel.getLinkModels()) { + if (link.getPropertyStartComponent().getText().equals(locationName)) { + PointModel pointModel = getPointComponent( + systemModel, + link.getPropertyEndComponent().getText() + ); + link.setConnectedComponents(pointModel, locationModel); + link.updateName(); + links.add(link); + } + else if (link.getPropertyEndComponent().getText().equals(locationName)) { + PointModel pointModel = getPointComponent( + systemModel, + link.getPropertyStartComponent().getText() + ); + link.setConnectedComponents(pointModel, locationModel); + link.updateName(); + links.add(link); + } + } + + return links; + } + + protected void initializeSystemModel(SystemModel systemModel) { + LayoutModel layoutModel = systemModel.getLayoutModel(); + + LengthProperty pScaleX = layoutModel.getPropertyScaleX(); + if (pScaleX.getValueByUnit(LengthProperty.Unit.MM) == 0) { + pScaleX.setValueAndUnit(Origin.DEFAULT_SCALE, LengthProperty.Unit.MM); + } + + LengthProperty pScaleY = layoutModel.getPropertyScaleY(); + if (pScaleY.getValueByUnit(LengthProperty.Unit.MM) == 0) { + pScaleY.setValueAndUnit(Origin.DEFAULT_SCALE, LengthProperty.Unit.MM); + } + + systemModel.getDrawingMethod().getOrigin() + .setScale( + pScaleX.getValueByUnit(LengthProperty.Unit.MM), + pScaleY.getValueByUnit(LengthProperty.Unit.MM) + ); + } + + protected void setCurrentModelFile(File currentModelFile) { + this.currentModelFile = currentModelFile; + } + + protected JFrame getApplicationFrame() { + return applicationFrame; + } + + protected StatusPanel getStatusPanel() { + return statusPanel; + } + + protected ModelExportAdapter getModelExportAdapter() { + return modelExportAdapter; + } + + protected void setModel(SystemModel systemModel) { + this.systemModel = systemModel; + } + + protected String getModelName() { + return fModelName; + } + + protected void setModelName(String modelName) { + this.fModelName = modelName; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/unified/UnifiedModelConstants.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/unified/UnifiedModelConstants.java new file mode 100644 index 0000000..654f5b1 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/unified/UnifiedModelConstants.java @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.persistence.unified; + +import javax.swing.filechooser.FileFilter; +import javax.swing.filechooser.FileNameExtensionFilter; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Constants related to reading and writing the kernel's model file format. + */ +public interface UnifiedModelConstants { + + /** + * File ending of locally saved models as xml file. + */ + String FILE_ENDING_XML = "xml"; + /** + * The file filter this persistor supports. + */ + FileFilter DIALOG_FILE_FILTER + = new FileNameExtensionFilter( + ResourceBundleUtil.getBundle(I18nPlantOverview.SYSTEM_PATH) + .getFormatted("unifiedModelConstants.dialogFileFilter.description", FILE_ENDING_XML), + FILE_ENDING_XML + ); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/unified/UnifiedModelPersistor.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/unified/UnifiedModelPersistor.java new file mode 100644 index 0000000..568ce2b --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/persistence/unified/UnifiedModelPersistor.java @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.persistence.unified; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.io.File; +import java.io.IOException; +import javax.swing.filechooser.FileFilter; +import org.opentcs.access.to.model.PlantModelCreationTO; +import org.opentcs.guing.common.persistence.ModelFilePersistor; +import org.opentcs.util.persistence.ModelParser; + +/** + * Serializes the data kept in a {@link PlantModelCreationTO} to a xml file. + */ +public class UnifiedModelPersistor + implements + ModelFilePersistor { + + /** + * The model parser. + */ + private final ModelParser modelParser; + + /** + * Create a new instance. + */ + @Inject + public UnifiedModelPersistor(ModelParser modelParser) { + this.modelParser = requireNonNull(modelParser, "modelParser"); + } + + @Override + public boolean serialize(PlantModelCreationTO model, File file) + throws IOException { + requireNonNull(model, "model"); + requireNonNull(file, "file"); + + writeFile(model, file); + + return true; + } + + @Override + public FileFilter getDialogFileFilter() { + return UnifiedModelConstants.DIALOG_FILE_FILTER; + } + + private void writeFile(PlantModelCreationTO plantModel, File file) + throws IOException { + File outFile = file.getName().endsWith(UnifiedModelConstants.FILE_ENDING_XML) + ? file + : new File( + file.getParentFile(), + file.getName() + "." + UnifiedModelConstants.FILE_ENDING_XML + ); + + modelParser.writeModel(plantModel, outFile); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/transport/OrderTypeSuggestionsPool.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/transport/OrderTypeSuggestionsPool.java new file mode 100644 index 0000000..bd40c86 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/transport/OrderTypeSuggestionsPool.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.transport; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Set; +import java.util.TreeSet; +import org.opentcs.components.plantoverview.OrderTypeSuggestions; + +/** + * A collection of all transport order types suggested. + */ +public class OrderTypeSuggestionsPool + implements + OrderTypeSuggestions { + + /** + * The transport order type suggestions. + */ + private final Set<String> typeSuggestions = new TreeSet<>(); + + @Inject + public OrderTypeSuggestionsPool(Set<OrderTypeSuggestions> typeSuggestions) { + requireNonNull(typeSuggestions, "typeSuggestions"); + + for (OrderTypeSuggestions typeSuggestion : typeSuggestions) { + this.typeSuggestions.addAll(typeSuggestion.getTypeSuggestions()); + } + } + + @Override + public Set<String> getTypeSuggestions() { + return typeSuggestions; + } + + /** + * Adds an additional type to the pool. + * + * @param typeSuggestion The type to add. + */ + public void addTypeSuggestion(String typeSuggestion) { + typeSuggestions.add(typeSuggestion); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/BlockSelector.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/BlockSelector.java new file mode 100644 index 0000000..829c064 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/BlockSelector.java @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.util; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.awt.geom.Rectangle2D; +import java.util.List; +import org.jhotdraw.draw.Figure; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingView; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * A helper for selecting blocks/block elements. + */ +public class BlockSelector { + + /** + * The application's model manager. + */ + private final ModelManager modelManager; + /** + * The application's drawing editor. + */ + private final OpenTCSDrawingEditor drawingEditor; + + /** + * Creates a new instance. + * + * @param modelManager The application's model manager. + * @param drawingEditor The application's drawing editor. + */ + @Inject + public BlockSelector(ModelManager modelManager, OpenTCSDrawingEditor drawingEditor) { + this.modelManager = requireNonNull(modelManager, "modelManager"); + this.drawingEditor = requireNonNull(drawingEditor, "drawingEditor"); + } + + /** + * Called when a block was selected, for instance in the tree view. + * Should select all figures in the drawing view belonging to the block. + * + * @param block The selected block. + */ + public void blockSelected(BlockModel block) { + requireNonNull(block, "block"); + + Rectangle2D r = null; + + List<Figure> blockElementFigures + = ModelComponentUtil.getChildFigures(block, modelManager.getModel()); + + for (Figure figure : blockElementFigures) { + Rectangle2D displayBox = figure.getDrawingArea(); + + if (r == null) { + r = displayBox; + } + else { + r.add(displayBox); + } + } + + if (r != null) { + OpenTCSDrawingView drawingView = drawingEditor.getActiveView(); + + drawingView.clearSelection(); + + for (Figure figure : blockElementFigures) { + drawingView.addToSelection(figure); + } + + drawingView.updateBlock(block); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/CompatibilityChecker.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/CompatibilityChecker.java new file mode 100644 index 0000000..04f670b --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/CompatibilityChecker.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.util; + +import jakarta.annotation.Nullable; +import javax.swing.JOptionPane; + +/** + * Provides environment compatibility checks. + */ +public class CompatibilityChecker { + + /** + * Prevents unwanted instantiation. + */ + private CompatibilityChecker() { + } + + /** + * Checks whether the given Java version string is compatible with the Docking Frames library. + * Docking Frames expects two periods with an integer between them, e.g. "x.y.z". Version numbers + * that do not follow this pattern lead to an exception on startup. + * + * @param version The version string. + * @return Whether the version string is compatible with the Docking Frames library. + */ + public static boolean versionCompatibleWithDockingFrames( + @Nullable + String version + ) { + return version != null && version.matches(".*\\.[0-9]+\\..*"); + } + + /** + * Shows a message dialog explaining that the Java version is incompatible with Docking Frames. + */ + public static void showVersionIncompatibleWithDockingFramesMessage() { + JOptionPane.showMessageDialog( + null, + "Your Java Runtime Environment is incompatible with this application.\n" + + "Please use a different JRE. Recommended: Adoptium (see https://adoptium.net/)", + "Incompatible Java version", + JOptionPane.ERROR_MESSAGE + ); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/CourseObjectFactory.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/CourseObjectFactory.java new file mode 100644 index 0000000..bca0209 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/CourseObjectFactory.java @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.util; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.guing.base.model.elements.LinkModel; +import org.opentcs.guing.base.model.elements.LocationModel; +import org.opentcs.guing.base.model.elements.PathModel; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.common.components.drawing.figures.FigureFactory; +import org.opentcs.guing.common.components.drawing.figures.LabeledLocationFigure; +import org.opentcs.guing.common.components.drawing.figures.LabeledPointFigure; +import org.opentcs.guing.common.components.drawing.figures.LinkConnection; +import org.opentcs.guing.common.components.drawing.figures.LocationFigure; +import org.opentcs.guing.common.components.drawing.figures.OffsetFigure; +import org.opentcs.guing.common.components.drawing.figures.PathConnection; +import org.opentcs.guing.common.components.drawing.figures.PointFigure; + +/** + * A factory for Figures and ModelComponents. + */ +public class CourseObjectFactory { + + /** + * A factory for figures. + */ + private final FigureFactory figureFactory; + + /** + * Creates a new instance. + * + * @param figureFactory A factory for figures. + */ + @Inject + public CourseObjectFactory(FigureFactory figureFactory) { + this.figureFactory = requireNonNull(figureFactory, "figureFactory"); + } + + public LabeledPointFigure createPointFigure() { + PointFigure pointFigure = figureFactory.createPointFigure(new PointModel()); + return figureFactory.createLabeledPointFigure(pointFigure); + } + + public PathConnection createPathConnection() { + return figureFactory.createPathConnection(new PathModel()); + } + + public LabeledLocationFigure createLocationFigure() { + LocationFigure location = figureFactory.createLocationFigure(new LocationModel()); + return figureFactory.createLabeledLocationFigure(location); + } + + public LinkConnection createLinkConnection() { + return figureFactory.createLinkConnection(new LinkModel()); + } + + public OffsetFigure createOffsetFigure() { + return figureFactory.createOffsetFigure(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/I18nPlantOverview.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/I18nPlantOverview.java new file mode 100644 index 0000000..b233786 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/I18nPlantOverview.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.util; + +/** + * Defines constants regarding internationalization. + */ +public interface I18nPlantOverview { + + /** + * Path to the resources related the menu bar. + */ + String MENU_PATH = "i18n.org.opentcs.plantoverview.mainMenu"; + /** + * Path to miscellaneous resources + */ + String MISC_PATH = "i18n.org.opentcs.plantoverview.miscellaneous"; + /** + * Path to the resources related to the system. + */ + String SYSTEM_PATH = "i18n.org.opentcs.plantoverview.system"; + /** + * Path to the resources related to toolbars. + */ + String TOOLBAR_PATH = "i18n.org.opentcs.plantoverview.toolbar"; + /** + * Path to the resources related to model properties dialog. + */ + String MODELPROPERTIES_PATH = "i18n.org.opentcs.plantoverview.dialogs.modelProperties"; + /** + * Path to the resources related to the modelview panel. + */ + String MODELVIEW_PATH = "i18n.org.opentcs.plantoverview.panels.modelView"; + /** + * Path to the resources related to the properties panel. + */ + String PROPERTIES_PATH = "i18n.org.opentcs.plantoverview.panels.propertyEditing"; + /** + * Path to the resources related to the tree view. + */ + String TREEVIEW_PATH = "i18n.org.opentcs.plantoverview.panels.componentTrees"; + /** + * Path to the resources related to layers. + */ + String LAYERS_PATH = "i18n.org.opentcs.plantoverview.panels.layers"; +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/IconToolkit.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/IconToolkit.java new file mode 100644 index 0000000..22523ab --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/IconToolkit.java @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.util; + +import java.net.URL; +import javax.swing.ImageIcon; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A utility class for loading icons. + */ +public class IconToolkit { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(IconToolkit.class); + /** + * The default path for icons. + */ + private static final String DEFAULT_PATH = "/org/opentcs/guing/res/symbols/"; + /** + * The single instance of this class. + */ + private static IconToolkit fInstance; + + /** + * Creates a new instance. + */ + private IconToolkit() { + } + + /** + * Returns the single instance of this class. + * + * @return The single instance of this class. + */ + public static IconToolkit instance() { + if (fInstance == null) { + fInstance = new IconToolkit(); + } + + return fInstance; + } + + /** + * Creates an ImageIcon. + * + * @param fullPath The full (absolute) path of the icon file. + * @return The icon, or <code>null</code>, if the file does not exist. + */ + public ImageIcon getImageIconByFullPath(String fullPath) { + URL url = getClass().getResource(fullPath); + + if (url != null) { + return new ImageIcon(url); + } + else { + LOG.warn("Icon not found: {}", fullPath); + return null; + } + } + + /** + * Creates an ImageIcon. + * + * @param relativePath The relative path of the icon file. + * @return The icon, or <code>null</code>, if the file does not exist. + */ + public ImageIcon createImageIcon(String relativePath) { + return getImageIconByFullPath(DEFAULT_PATH + relativePath); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/ImageDirectory.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/ImageDirectory.java new file mode 100644 index 0000000..a69f96d --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/ImageDirectory.java @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.util; + +import javax.swing.ImageIcon; + +/** + * This utility class declares the main image directory. + * It also provides utility methods to get the url to files in the directory. + */ +public final class ImageDirectory { + + /** + * The directory where GUI images and icons are stored. + */ + public static final String DIR = "/org/opentcs/guing/res/symbols"; + + /** + * Prevents instantiation. + */ + private ImageDirectory() { + } + + /** + * Returns an ImageIcon from a relative path inside the image directory. + * + * @param relPath path to file inside the image directory. + * @return the new ImageIcon + */ + public static ImageIcon getImageIcon(String relPath) { + return new ImageIcon(ImageDirectory.class.getResource(DIR + relPath)); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/ModelComponentFactory.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/ModelComponentFactory.java new file mode 100644 index 0000000..8650ce2 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/ModelComponentFactory.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.util; + +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.base.model.elements.LayoutModel; +import org.opentcs.guing.base.model.elements.LocationTypeModel; +import org.opentcs.guing.base.model.elements.VehicleModel; + +/** + * A factory for ModelComponents. + */ +public class ModelComponentFactory { + + + /** + * Creates a new instance. + */ + public ModelComponentFactory() { + } + + public LayoutModel createLayoutModel() { + return new LayoutModel(); + } + + public VehicleModel createVehicleModel() { + return new VehicleModel(); + } + + public LocationTypeModel createLocationTypeModel() { + return new LocationTypeModel(); + } + + public BlockModel createBlockModel() { + return new BlockModel(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/ModelComponentUtil.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/ModelComponentUtil.java new file mode 100644 index 0000000..ed488f9 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/ModelComponentUtil.java @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.util; + +import java.util.ArrayList; +import java.util.List; +import org.jhotdraw.draw.Figure; +import org.opentcs.guing.base.model.CompositeModelComponent; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.common.model.SystemModel; + +/** + * Provides utility methods for {@link ModelComponent}s. + */ +public class ModelComponentUtil { + + /** + * Prevent instantiation. + */ + private ModelComponentUtil() { + } + + public static List<Figure> getChildFigures( + CompositeModelComponent parent, + SystemModel systemModel + ) { + List<Figure> figures = new ArrayList<>(); + + List<ModelComponent> childComps = parent.getChildComponents(); + synchronized (childComps) { + for (ModelComponent component : childComps) { + Figure figure = systemModel.getFigure(component); + if (figure != null) { + figures.add(figure); + } + } + } + + return figures; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/PanelRegistry.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/PanelRegistry.java new file mode 100644 index 0000000..dcca8e6 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/PanelRegistry.java @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.util; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.opentcs.components.plantoverview.PluggablePanelFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A registry for all plugin panel factories. + */ +public class PanelRegistry { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PanelRegistry.class); + /** + * The registered factories. + */ + private final List<PluggablePanelFactory> factories = new ArrayList<>(); + + /** + * Creates a new instance. + * + * @param factories The factories. + */ + @Inject + public PanelRegistry(Set<PluggablePanelFactory> factories) { + requireNonNull(factories, "factories"); + + // Auto-detect generic client panel factories. + for (PluggablePanelFactory factory : factories) { + LOG.info("Setting up plugin panel factory: {}", factory.getClass().getName()); + this.factories.add(factory); + } + if (this.factories.isEmpty()) { + LOG.info("No plugin panel factories found."); + } + } + + /** + * Returns the registered factories. + * + * @return The registered factories. + */ + public List<PluggablePanelFactory> getFactories() { + return new ArrayList<>(factories); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/SynchronizedFileChooser.form b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/SynchronizedFileChooser.form new file mode 100644 index 0000000..8b1ff5f --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/SynchronizedFileChooser.form @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.3" maxVersion="1.9"> + <Events> + <EventHandler event="propertyChange" listener="java.beans.PropertyChangeListener" parameters="java.beans.PropertyChangeEvent" handler="formPropertyChange"/> + </Events> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,44,0,0,1,-87"/> + </AuxValues> +</Form> diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/SynchronizedFileChooser.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/SynchronizedFileChooser.java new file mode 100644 index 0000000..ffc2d93 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/SynchronizedFileChooser.java @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.util; + +import java.io.File; +import javax.swing.JFileChooser; +import javax.swing.filechooser.FileFilter; +import javax.swing.filechooser.FileNameExtensionFilter; + +/** + * A {@link JFileChooser} which synchronizes the selected file with a correct file filter + * and vice versa. + * The file filters are only checked if they are of type {@link FileNameExtensionFilter}, because + * with a normal {@link FileFilter} the value of a file extension is unknown. + */ +public class SynchronizedFileChooser + extends + javax.swing.JFileChooser { + + /** + * The selected file of this file chooser. + */ + private String selectedFileName; + + /** + * Creates a new instance. + * + * @param currentDirectory The current directory of the file chooser + */ + @SuppressWarnings("this-escape") + public SynchronizedFileChooser(File currentDirectory) { + super(currentDirectory); + initComponents(); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + addPropertyChangeListener(new java.beans.PropertyChangeListener() { + public void propertyChange(java.beans.PropertyChangeEvent evt) { + formPropertyChange(evt); + } + }); + }// </editor-fold>//GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void formPropertyChange(java.beans.PropertyChangeEvent evt) {//GEN-FIRST:event_formPropertyChange + //Synchronize if the user selects a different filter + if (evt.getPropertyName().equals(FILE_FILTER_CHANGED_PROPERTY)) { + if (selectedFileName == null + || (getSelectedFile() != null && selectedFileName.equals(getSelectedFile().getName()))) { + return; + } + File file = new File(getCurrentDirectory(), selectedFileName); + //We currently have no file selected so we dont need to modify it + //The equals check ensures that both events wont trigger each other over and over + Object filter = evt.getNewValue(); + if (filter instanceof FileNameExtensionFilter) { + FileNameExtensionFilter extensionFilter = (FileNameExtensionFilter) filter; + int fileExtensionIndex = getFileExtensionIndex(file); + //Select the first extension of the filter as the new file extension + String newExtension = extensionFilter.getExtensions()[0]; + //If the file has a known file ending, replace it. Else just add the new extension + //at the end + StringBuilder newPathBuilder = new StringBuilder(); + newPathBuilder.append( + fileExtensionIndex >= 0 + ? file.getName().substring(0, fileExtensionIndex) : file.getName() + ); + if (!newPathBuilder.toString().endsWith(".")) { + newPathBuilder.append("."); + } + newPathBuilder.append(newExtension); + //Update the chosen file + setSelectedFile(new File(file.getParent(), newPathBuilder.toString())); + } + } + //Synchronize if the user selects a different file + else if (evt.getPropertyName().equals(SELECTED_FILE_CHANGED_PROPERTY)) { + Object newFile = evt.getNewValue(); + if (newFile instanceof File) { + this.selectedFileName = ((File) newFile).getName(); + } + } + }//GEN-LAST:event_formPropertyChange + + /** + * Returns the index of the file extension in the file name if the current file ending is + * known by one of the file name extension filters. + * + * @param file The file + * @return The index of a known file extension in the file's name or <code>-1</code> + */ + private int getFileExtensionIndex(File file) { + for (FileFilter filter : getChoosableFileFilters()) { + if (filter instanceof FileNameExtensionFilter) { + FileNameExtensionFilter extensionFilter = (FileNameExtensionFilter) filter; + for (String extension : extensionFilter.getExtensions()) { + if (file.getName().endsWith(extension)) { + return file.getName().length() - extension.length(); + } + } + } + } + return -1; + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/UserMessageHelper.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/UserMessageHelper.java new file mode 100644 index 0000000..d641180 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/guing/common/util/UserMessageHelper.java @@ -0,0 +1,186 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.util; + +import javax.swing.JOptionPane; + +/** + * A helper class that shows a message dedicated to the user in a + * dialog. + */ +public class UserMessageHelper { + + /** + * Creates a new instance. + */ + public UserMessageHelper() { + } + + /** + * Shows a message dialog to the user centered in the screen. + * + * @param title The title of the dialog. + * @param message The message to be shown. + * @param type The type of the message. + */ + public void showMessageDialog( + String title, + String message, + Type type + ) { + showJOptionPane(title, message, type); + } + + /** + * Shows a dialog with the given options to choose from. + * + * @param title The title of the dialog. + * @param message The message to be shown. + * @param type The type of the message. + * @param options The options that shall be selectable. + * @return An int indicating the selected value or -1, if it was closed. + */ + public int showOptionsDialog( + String title, + String message, + Type type, + String[] options + ) { + if (options == null || options.length == 0) { + return -1; + } + return showJOptionsDialog(title, message, type, options); + } + + /** + * Shows a confirm dialog, offering three options: Yes, No, Cancel. + * + * @param title The title of the dialog. + * @param message The message to be shown. + * @param type The type of the message. + * @return A {@link ReturnType} indicating the selected value. + */ + public ReturnType showConfirmDialog( + String title, + String message, + Type type + ) { + return translateJOptionReturnType(showJOptionConfirmDialog(title, message, type)); + } + + private int showJOptionConfirmDialog( + String title, + String message, + Type type + ) { + int jOptionType = translateType(type); + return JOptionPane.showConfirmDialog(null, message, title, jOptionType); + } + + private int showJOptionsDialog( + String title, + String message, + Type type, + String[] options + ) { + int n = JOptionPane.showOptionDialog( + null, + message, + title, + JOptionPane.DEFAULT_OPTION, + translateType(type), + null, + options, + options[0] + ); + return n; + } + + private int translateType(Type type) { + int jOptionType; + switch (type) { + case ERROR: + jOptionType = JOptionPane.ERROR_MESSAGE; + break; + case INFO: + jOptionType = JOptionPane.INFORMATION_MESSAGE; + break; + case QUESTION: + jOptionType = JOptionPane.YES_NO_OPTION; + break; + default: + jOptionType = JOptionPane.PLAIN_MESSAGE; + } + return jOptionType; + } + + private ReturnType translateJOptionReturnType(int type) { + switch (type) { + case JOptionPane.OK_OPTION: + return ReturnType.OK; + case JOptionPane.NO_OPTION: + return ReturnType.NO; + case JOptionPane.CANCEL_OPTION: + return ReturnType.CANCEL; + default: + return ReturnType.CANCEL; + } + } + + private void showJOptionPane( + String title, + String message, + Type type + ) { + int jOptionType; + jOptionType = translateType(type); + JOptionPane.showMessageDialog( + null, + message, + title, + jOptionType + ); + } + + /** + * Supported types of user messages. + */ + public enum Type { + + /** + * A plain message. + */ + PLAIN, + /** + * An info message. + */ + INFO, + /** + * An error message. + */ + ERROR, + /** + * A question. + */ + QUESTION; + } + + /** + * Possible return types of the dialog. + */ + public enum ReturnType { + + /** + * OK. + */ + OK, + /** + * No. + */ + NO, + /** + * Cancel. + */ + CANCEL; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/action/draw/MoveAction.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/action/draw/MoveAction.java new file mode 100644 index 0000000..8c34e79 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/action/draw/MoveAction.java @@ -0,0 +1,186 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.guing.common.jhotdraw.application.action.draw; + +import static javax.swing.Action.SMALL_ICON; +import static org.opentcs.guing.common.util.I18nPlantOverview.TOOLBAR_PATH; + +import java.awt.event.ActionEvent; +import java.awt.geom.AffineTransform; +import java.util.HashSet; +import java.util.Set; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.action.AbstractSelectedAction; +import org.jhotdraw.draw.event.TransformEdit; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Moves the selected figures by one unit. + * + * @author Werner Randelshofer + */ +public abstract class MoveAction + extends + AbstractSelectedAction { + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(TOOLBAR_PATH); + /** + * The X offset by which to move. + */ + private final int dx; + /** + * The Y offset by which to move. + */ + private final int dy; + + /** + * Creates a new instance. + * + * @param editor The application's drawing editor. + * @param dx The X offset by which to move. + * @param dy The Y offset by which to move. + */ + @SuppressWarnings("this-escape") + public MoveAction(DrawingEditor editor, int dx, int dy) { + super(editor); + this.dx = dx; + this.dy = dy; + updateEnabledState(); + } + + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + AffineTransform tx = new AffineTransform(); + + // TODO: Make these factors configurable? + if ((e.getModifiers() & ActionEvent.CTRL_MASK) > 0) { + tx.translate(dx * 10, dy * 10); + } + else if ((e.getModifiers() & ActionEvent.SHIFT_MASK) > 0) { + tx.translate(dx, dy); + } + else { + tx.translate(dx * 5, dy * 5); + } + + Set<Figure> transformedFigures = new HashSet<>(); + + for (Figure f : getView().getSelectedFigures()) { + if (f.isTransformable()) { + transformedFigures.add(f); + f.willChange(); + f.transform(tx); + f.changed(); + } + } + + fireUndoableEditHappened(new TransformEdit(transformedFigures, tx)); + } + + /** + * Moves the selected figures to the right. + */ + public static class East + extends + MoveAction { + + /** + * This action's ID. + */ + public static final String ID = "edit.moveEast"; + + /** + * Creates a new instance. + * + * @param editor The application's drawing editor. + */ + @SuppressWarnings("this-escape") + public East(DrawingEditor editor) { + super(editor, 1, 0); + + putValue(SHORT_DESCRIPTION, BUNDLE.getString("moveAction.east.shortDescription")); + putValue(SMALL_ICON, ImageDirectory.getImageIcon("/toolbar/draw-arrow-forward.png")); + } + } + + /** + * Moves the selected figures to the right. + */ + public static class West + extends + MoveAction { + + /** + * This action's ID. + */ + public static final String ID = "edit.moveWest"; + + /** + * Creates a new instance. + * + * @param editor The application's drawing editor. + */ + @SuppressWarnings("this-escape") + public West(DrawingEditor editor) { + super(editor, -1, 0); + + putValue(SHORT_DESCRIPTION, BUNDLE.getString("moveAction.west.shortDescription")); + putValue(SMALL_ICON, ImageDirectory.getImageIcon("/toolbar/draw-arrow-back.png")); + } + } + + /** + * Moves the selected figures upwards. + */ + public static class North + extends + MoveAction { + + /** + * This action's ID. + */ + public static final String ID = "edit.moveNorth"; + + /** + * Creates a new instance. + * + * @param editor The application's drawing editor. + */ + @SuppressWarnings("this-escape") + public North(DrawingEditor editor) { + super(editor, 0, -1); + + putValue(SHORT_DESCRIPTION, BUNDLE.getString("moveAction.north.shortDescription")); + putValue(SMALL_ICON, ImageDirectory.getImageIcon("/toolbar/draw-arrow-up.png")); + } + } + + /** + * Moves the selected figures downwards. + */ + public static class South + extends + MoveAction { + + /** + * This action's ID. + */ + public static final String ID = "edit.moveSouth"; + + /** + * Creates a new instance. + * + * @param editor The application's drawing editor. + */ + @SuppressWarnings("this-escape") + public South(DrawingEditor editor) { + super(editor, 0, 1); + + putValue(SHORT_DESCRIPTION, BUNDLE.getString("moveAction.south.shortDescription")); + putValue(SMALL_ICON, ImageDirectory.getImageIcon("/toolbar/draw-arrow-down.png")); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/action/draw/SelectSameAction.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/action/draw/SelectSameAction.java new file mode 100644 index 0000000..82ab4bc --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/action/draw/SelectSameAction.java @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.guing.common.jhotdraw.application.action.draw; + +import static javax.swing.Action.SMALL_ICON; +import static org.opentcs.guing.common.util.I18nPlantOverview.MODELVIEW_PATH; + +import java.util.HashSet; +import java.util.Set; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.action.AbstractSelectedAction; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * SelectSameAction. + * + * @author Werner Randelshofer + */ +public class SelectSameAction + extends + AbstractSelectedAction { + + /** + * This action's ID. + */ + public static final String ID = "edit.selectSame"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MODELVIEW_PATH); + + /** + * Creates a new instance. + * + * @param editor The drawing editor + */ + @SuppressWarnings("this-escape") + public SelectSameAction(DrawingEditor editor) { + super(editor); + + putValue(NAME, BUNDLE.getString("selectSameAction.name")); + putValue(SMALL_ICON, ImageDirectory.getImageIcon("/menu/kcharselect.png")); + + updateEnabledState(); + } + + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + selectSame(); + } + + public void selectSame() { + Set<Class<?>> selectedClasses = new HashSet<>(); + + for (Figure selected : getView().getSelectedFigures()) { + selectedClasses.add(selected.getClass()); + } + + for (Figure f : getDrawing().getChildren()) { + if (selectedClasses.contains(f.getClass())) { + getView().addToSelection(f); + } + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/action/edit/DeleteAction.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/action/edit/DeleteAction.java new file mode 100644 index 0000000..e6a5e8f --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/action/edit/DeleteAction.java @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit; + +import static org.opentcs.guing.common.util.I18nPlantOverview.MENU_PATH; + +import java.awt.Component; +import java.awt.KeyboardFocusManager; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.KeyStroke; +import javax.swing.text.BadLocationException; +import javax.swing.text.Caret; +import javax.swing.text.JTextComponent; +import javax.swing.text.TextAction; +import org.opentcs.guing.common.components.EditableComponent; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Deletes the region at (or after) the caret position. + * This action acts on the last EditableComponent} / {@code JTextComponent} + * which had the focus when the {@code ActionEvent} was generated. + * This action is called when the user selects the "Delete" item in + * the Edit menu. The menu item is automatically created by the application. + * + * @author Werner Randelshofer + */ +public class DeleteAction + extends + TextAction { + + /** + * This action's ID. + */ + public static final String ID = "edit.delete"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + + /** + * Creates a new instance which acts on the currently focused component. + */ + @SuppressWarnings("this-escape") + public DeleteAction() { + super(ID); + + putValue(NAME, BUNDLE.getString("deleteAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("deleteAction.shortDescription")); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke("DEL")); + + ImageIcon image = ImageDirectory.getImageIcon("/menu/edit-delete-2.png"); + putValue(SMALL_ICON, image); + putValue(LARGE_ICON_KEY, image); + } + + @Override + public void actionPerformed(ActionEvent evt) { + Component cFocusOwner + = KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner(); + + if (cFocusOwner instanceof JComponent) { + if (cFocusOwner.isEnabled()) { + if (cFocusOwner instanceof EditableComponent) { + // Delete all selected UserObjects from the tree or + // delete all selected Figures from the DrawingView + ((EditableComponent) cFocusOwner).delete(); + } + else { + deleteNextChar(evt); + } + } + } + } + + /** + * This method was copied from + * DefaultEditorKit.DeleteNextCharAction.actionPerformed(ActionEvent). + * + * @param e + */ + public void deleteNextChar(ActionEvent e) { + JTextComponent c = getTextComponent(e); + boolean beep = true; + + if ((c != null) && (c.isEditable())) { + try { + javax.swing.text.Document doc = c.getDocument(); + Caret caret = c.getCaret(); + int dot = caret.getDot(); + int mark = caret.getMark(); + + if (dot != mark) { + doc.remove(Math.min(dot, mark), Math.abs(dot - mark)); + beep = false; + } + else if (dot < doc.getLength()) { + doc.remove(dot, 1); + beep = false; + } + } + catch (BadLocationException bl) { + } + } + + if (beep) { + Toolkit.getDefaultToolkit().beep(); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/action/edit/SelectAllAction.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/action/edit/SelectAllAction.java new file mode 100644 index 0000000..6f9982c --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/action/edit/SelectAllAction.java @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit; + +import static javax.swing.Action.ACCELERATOR_KEY; +import static javax.swing.Action.LARGE_ICON_KEY; +import static javax.swing.Action.SMALL_ICON; +import static org.opentcs.guing.common.util.I18nPlantOverview.MENU_PATH; + +import java.awt.Component; +import java.awt.KeyboardFocusManager; +import java.awt.event.ActionEvent; +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.KeyStroke; +import javax.swing.text.JTextComponent; +import org.opentcs.guing.common.components.EditableComponent; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; + +/** + * Selects all items. + * This action acts on the last EditableComponent / {@code JTextComponent} + * which had the focus when the {@code ActionEvent} was generated. + * This action is called when the user selects the "Select All" item in the + * Edit menu. The menu item is automatically created by the application. + * + * @author Werner Randelshofer. + */ +public class SelectAllAction + extends + org.jhotdraw.app.action.edit.AbstractSelectionAction { + + /** + * This action's ID. + */ + public static final String ID = "edit.selectAll"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + + /** + * Creates a new instance which acts on the currently focused component. + */ + public SelectAllAction() { + this(null); + } + + /** + * Creates a new instance which acts on the specified component. + * + * @param target The target of the action. Specify null for the currently + * focused component. + */ + @SuppressWarnings("this-escape") + public SelectAllAction(JComponent target) { + super(target); + + putValue(NAME, BUNDLE.getString("selectAllAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("selectAllAction.shortDescription")); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke("ctrl A")); + + ImageIcon icon = ImageDirectory.getImageIcon("/menu/edit-select-all.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + } + + @Override + public void actionPerformed(ActionEvent evt) { + JComponent cTarget = target; + Component cFocusOwner + = KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner(); + + if (cTarget == null && (cFocusOwner instanceof JComponent)) { + cTarget = (JComponent) cFocusOwner; + } + + if (cTarget != null && cTarget.isEnabled()) { + if (cTarget instanceof EditableComponent) { + ((EditableComponent) cTarget).selectAll(); + } + else if (cTarget instanceof JTextComponent) { + ((JTextComponent) cTarget).selectAll(); + } + else { + cTarget.getToolkit().beep(); + } + } + } + + @Override + protected void updateEnabled() { + if (target != null) { + setEnabled(target.isEnabled()); + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/action/edit/UndoRedoManager.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/action/edit/UndoRedoManager.java new file mode 100644 index 0000000..894ac48 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/action/edit/UndoRedoManager.java @@ -0,0 +1,366 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.guing.common.jhotdraw.application.action.edit; + +import static javax.swing.Action.ACCELERATOR_KEY; +import static javax.swing.Action.LARGE_ICON_KEY; +import static javax.swing.Action.SMALL_ICON; +import static org.opentcs.guing.common.event.SystemModelTransitionEvent.Stage.UNLOADING; +import static org.opentcs.guing.common.util.I18nPlantOverview.MENU_PATH; + +import jakarta.inject.Inject; +import java.awt.event.ActionEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.ImageIcon; +import javax.swing.KeyStroke; +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; +import javax.swing.undo.CompoundEdit; +import javax.swing.undo.UndoManager; +import javax.swing.undo.UndoableEdit; +import org.opentcs.guing.common.event.SystemModelTransitionEvent; +import org.opentcs.guing.common.util.I18nPlantOverview; +import org.opentcs.guing.common.util.ImageDirectory; +import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Same as javax.swing.UndoManager but provides actions for undo and redo + * operations. + * + * @author Werner Randelshofer + */ +public class UndoRedoManager + extends + UndoManager + implements + EventHandler { + + /** + * An undo action's ID. + */ + public static final String UNDO_ACTION_ID = "edit.undo"; + /** + * A redo action's ID. + */ + public static final String REDO_ACTION_ID = "edit.redo"; + + private static final ResourceBundleUtil BUNDLE = ResourceBundleUtil.getBundle(MENU_PATH); + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(UndoRedoManager.class); + + @SuppressWarnings("this-escape") + protected PropertyChangeSupport propertySupport = new PropertyChangeSupport(this); + /** + * This flag is set to true when at least one significant UndoableEdit + * has been added to the manager since the last call to discardAllEdits. + */ + private boolean hasSignificantEdits; + /** + * This flag is set to true when an undo or redo operation is in progress. + * The UndoRedoManager ignores all incoming UndoableEdit events while + * this flag is true. + */ + private boolean undoOrRedoInProgress; + /** + * The undo action instance. + */ + private final UndoAction undoAction; + /** + * The redo action instance. + */ + private final RedoAction redoAction; + + /** + * Creates a new UndoRedoManager. + */ + @Inject + public UndoRedoManager() { + undoAction = new UndoAction(); + redoAction = new RedoAction(); + } + + @Override + public synchronized void discardAllEdits() { + super.discardAllEdits(); + updateActions(); + setHasSignificantEdits(false); + } + + @Override + public void onEvent(Object event) { + if (event instanceof SystemModelTransitionEvent) { + handleSystemModelTransition((SystemModelTransitionEvent) event); + } + } + + private void handleSystemModelTransition(SystemModelTransitionEvent evt) { + switch (evt.getStage()) { + case UNLOADING: + discardAllEdits(); + break; + case LOADED: + discardAllEdits(); + break; + default: + // Do nada. + } + } + + public void setHasSignificantEdits(boolean newValue) { + boolean oldValue = hasSignificantEdits; + hasSignificantEdits = newValue; + firePropertyChange("hasSignificantEdits", oldValue, newValue); + } + + /** + * Returns true if at least one significant UndoableEdit + * has been added since the last call to discardAllEdits. + * + * @return + */ + public boolean hasSignificantEdits() { + return hasSignificantEdits; + } + + /** + * If inProgress, inserts anEdit at indexOfNextAdd, and removes + * any old edits that were at indexOfNextAdd or later. The die + * method is called on each edit that is removed is sent, in the + * reverse of the order the edits were added. Updates + * indexOfNextAdd. + * <p> + * If not inProgress, acts as a CompoundEdit</p> + * <p> + * Regardless of inProgress, if undoOrRedoInProgress, + * calls die on each edit that is sent.</p> + * + * @return + * @see CompoundEdit#end + * @see CompoundEdit#addEdit + */ + @Override + public synchronized boolean addEdit(UndoableEdit anEdit) { + if (undoOrRedoInProgress) { + anEdit.die(); + return true; + } + + boolean success = super.addEdit(anEdit); + + updateActions(); + + if (success && anEdit.isSignificant() && editToBeUndone() == anEdit) { + setHasSignificantEdits(true); + } + + return success; + } + + /** + * Gets the undo action for use as an + * + * @return Undo menu item. + */ + public Action getUndoAction() { + return undoAction; + } + + /** + * Gets the redo action for use as a Redo menu item. + * + * @return + */ + public Action getRedoAction() { + return redoAction; + } + + /** + * Updates the properties of the UndoAction and of the RedoAction. + */ + private void updateActions() { + String label; + + if (canUndo()) { + undoAction.setEnabled(true); + label = getUndoPresentationName(); + } + else { + undoAction.setEnabled(false); + label = ResourceBundleUtil.getBundle(I18nPlantOverview.MENU_PATH) + .getString("undoRedoManager.undoAction.name"); + } + + undoAction.putValue(Action.NAME, label); + undoAction.putValue(Action.SHORT_DESCRIPTION, label); + + if (canRedo()) { + redoAction.setEnabled(true); + label = getRedoPresentationName(); + } + else { + redoAction.setEnabled(false); + label = ResourceBundleUtil.getBundle(I18nPlantOverview.MENU_PATH) + .getString("undoRedoManager.redoAction.name"); + } + + redoAction.putValue(Action.NAME, label); + redoAction.putValue(Action.SHORT_DESCRIPTION, label); + } + + /** + * Undoes the last edit event. + * The UndoRedoManager ignores all incoming UndoableEdit events, + * while undo is in progress. + */ + @Override + public void undo() + throws CannotUndoException { + undoOrRedoInProgress = true; + + try { + super.undo(); + } + finally { + undoOrRedoInProgress = false; + updateActions(); + } + } + + /** + * Redoes the last undone edit event. + * The UndoRedoManager ignores all incoming UndoableEdit events, + * while redo is in progress. + */ + @Override + public void redo() + throws CannotUndoException { + undoOrRedoInProgress = true; + + try { + super.redo(); + } + finally { + undoOrRedoInProgress = false; + updateActions(); + } + } + + /** + * Undoes or redoes the last edit event. + * The UndoRedoManager ignores all incoming UndoableEdit events, + * while undo or redo is in progress. + */ + @Override + public void undoOrRedo() + throws CannotUndoException, + CannotRedoException { + undoOrRedoInProgress = true; + + try { + super.undoOrRedo(); + } + finally { + undoOrRedoInProgress = false; + updateActions(); + } + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + propertySupport.addPropertyChangeListener(listener); + } + + public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { + propertySupport.addPropertyChangeListener(propertyName, listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + propertySupport.removePropertyChangeListener(listener); + } + + public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { + propertySupport.removePropertyChangeListener(propertyName, listener); + } + + protected void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) { + propertySupport.firePropertyChange(propertyName, oldValue, newValue); + } + + protected void firePropertyChange(String propertyName, int oldValue, int newValue) { + propertySupport.firePropertyChange(propertyName, oldValue, newValue); + } + + protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) { + propertySupport.firePropertyChange(propertyName, oldValue, newValue); + } + + /** + * Undo Action for use in a menu bar. + */ + private class UndoAction + extends + AbstractAction { + + UndoAction() { + putValue(NAME, BUNDLE.getString("undoRedoManager.undoAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("undoRedoManager.undoAction.shortDescription")); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke("ctrl Z")); + + ImageIcon icon = ImageDirectory.getImageIcon("/menu/edit-undo.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + + setEnabled(false); + } + + @Override + public void actionPerformed(ActionEvent evt) { + try { + undo(); + } + catch (CannotUndoException e) { + LOG.debug("Cannot undo: {}", e); + } + } + } + + /** + * Redo Action for use in a menu bar. + */ + private class RedoAction + extends + AbstractAction { + + RedoAction() { + putValue(NAME, BUNDLE.getString("undoRedoManager.redoAction.name")); + putValue(SHORT_DESCRIPTION, BUNDLE.getString("undoRedoManager.redoAction.shortDescription")); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke("ctrl Y")); + + ImageIcon icon = ImageDirectory.getImageIcon("/menu/edit-redo.png"); + putValue(SMALL_ICON, icon); + putValue(LARGE_ICON_KEY, icon); + + setEnabled(false); + } + + @Override + public void actionPerformed(ActionEvent evt) { + try { + redo(); + } + catch (CannotRedoException e) { + LOG.debug("Cannot redo: {}", e); + } + } + } + +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/toolbar/AbstractMultipleSelectionTool.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/toolbar/AbstractMultipleSelectionTool.java new file mode 100644 index 0000000..8156c3c --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/toolbar/AbstractMultipleSelectionTool.java @@ -0,0 +1,270 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.guing.common.jhotdraw.application.toolbar; + +import static java.util.Objects.requireNonNull; + +import java.awt.Component; +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import javax.swing.AbstractButton; +import javax.swing.Action; +import javax.swing.ButtonGroup; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.JRadioButtonMenuItem; +import org.jhotdraw.app.action.ActionUtil; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.handle.BezierOutlineHandle; +import org.jhotdraw.draw.handle.Handle; +import org.jhotdraw.draw.tool.DelegationSelectionTool; +import org.jhotdraw.draw.tool.DragTracker; +import org.jhotdraw.draw.tool.SelectAreaTracker; +import org.jhotdraw.draw.tool.Tool; +import org.opentcs.guing.common.application.ApplicationState; + +/** + * The default selection tool. + */ +public abstract class AbstractMultipleSelectionTool + extends + DelegationSelectionTool { + + /** + * A bit mask for the left mouse button being clicked and the ctrl key being + * pressed. + */ + private static final int CTRL_LEFT_MASK + = MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.CTRL_DOWN_MASK; + /** + * Stores the application's current state. + */ + private final ApplicationState appState; + /** + * We store the last mouse click here, to support multi-click behavior, that + * is, a behavior that is invoked, when the user clicks on the same spot + * multiple times, but in a longer interval than needed for a double click. + */ + private MouseEvent lastClickEvent; + /** + * Drawing-related actions for popup menus created by this tool. + */ + private final Collection<Action> drawingActions; + /** + * Selection-related actions for popup menus created by this tool. + */ + private final Collection<Action> selectionActions; + + /** + * Creates a new instance. + * + * @param appState Stores the application's current state. + * @param selectAreaTracker The tracker to be used for area selections in the drawing. + * @param dragTracker The tracker to be used for dragging figures. + * @param drawingActions Drawing-related actions for the popup menus created by this tool. + * @param selectionActions Selection-related actions for the popup menus created by this tool. + */ + @SuppressWarnings("this-escape") + public AbstractMultipleSelectionTool( + ApplicationState appState, + SelectAreaTracker selectAreaTracker, + DragTracker dragTracker, + Collection<Action> drawingActions, + Collection<Action> selectionActions + ) { + super(drawingActions, selectionActions); + this.appState = requireNonNull(appState, "appState"); + requireNonNull(selectAreaTracker, "selectAreaTracker"); + requireNonNull(dragTracker, "dragTracker"); + this.drawingActions = requireNonNull(drawingActions, "drawingActions"); + this.selectionActions = requireNonNull(selectionActions, "selectionActions"); + + setSelectAreaTracker(selectAreaTracker); + setDragTracker(dragTracker); + } + + @Override // DelegationSelectionTool + public void mouseClicked(MouseEvent evt) { + if (!evt.isConsumed()) { + if (evt.getClickCount() >= 2 && evt.getButton() == MouseEvent.BUTTON1) { + handleDoubleClick(evt); + } + else if (evt.getButton() == MouseEvent.BUTTON3) { + // Handle right click as double click + handleDoubleClick(evt); + } + else if (evt.getClickCount() == 1 + && lastClickEvent != null + && lastClickEvent.getClickCount() == 1 + && lastClickEvent.getX() == evt.getX() + && lastClickEvent.getY() == evt.getY()) { + // click with ctrl + if (((evt.getModifiersEx() & CTRL_LEFT_MASK) > 0) + && ((lastClickEvent.getModifiersEx() & CTRL_LEFT_MASK) > 0)) { + handleMultiClick(evt); + } + } + } + + lastClickEvent = evt; + } + + @Override // DelegationSelectionTool + protected void handleDoubleClick(MouseEvent evt) { + DrawingView v = getView(); + Point pos = new Point(evt.getX(), evt.getY()); + Handle handle = v.findHandle(pos); + + // Special case PathConnection: Ignore double click + if (handle != null && !(handle instanceof BezierOutlineHandle)) { + handle.trackDoubleClick(pos, evt.getModifiersEx()); + } + else { + Point2D.Double p = viewToDrawing(pos); + + // Note: The search sequence used here, must be + // consistent with the search sequence used by the + // HandleTracker, the SelectAreaTracker and SelectionTool. + // If possible, continue to work with the current selection + Figure figure = null; + + if (isSelectBehindEnabled()) { + for (Figure f : v.getSelectedFigures()) { + if (f.contains(p)) { + figure = f; + break; + } + } + } + // If the point is not contained in the current selection, + // search for a figure in the drawing. + if (figure == null) { + figure = v.findFigure(pos); + } + + Figure outerFigure = figure; + + if (figure != null && figure.isSelectable()) { + Tool figureTool = figure.getTool(p); + + if (figureTool == null) { + figure = getDrawing().findFigureInside(p); + + if (figure != null) { + figureTool = figure.getTool(p); + } + } + + if (figureTool != null) { + setTracker(figureTool); + figureTool.mousePressed(evt); + } + else { + if (outerFigure.handleMouseClick(p, evt, getView())) { + v.clearSelection(); + v.addToSelection(outerFigure); + } + else { + v.clearSelection(); + v.addToSelection(outerFigure); + } + } + } + } + + evt.consume(); + } + + @Override + protected void showPopupMenu(Figure figure, Point p, Component c) { + // --- JHotDraw code starts here --- + JPopupMenu menu = new JPopupMenu(); + JMenu submenu = null; + String submenuName = null; + List<Action> popupActions = new ArrayList<>(); + if (figure != null) { + List<Action> figureActions = new ArrayList<>(figure.getActions(viewToDrawing(p))); + if (!popupActions.isEmpty() && !figureActions.isEmpty()) { + popupActions.add(null); + } + popupActions.addAll(figureActions); + if (!popupActions.isEmpty() && !selectionActions.isEmpty()) { + popupActions.add(null); + } + popupActions.addAll(selectionActions); + } + if (!popupActions.isEmpty() && !drawingActions.isEmpty()) { + popupActions.add(null); + } + popupActions.addAll(drawingActions); + + HashMap<Object, ButtonGroup> buttonGroups = new HashMap<>(); + for (Action a : popupActions) { + if (a != null && a.getValue(ActionUtil.SUBMENU_KEY) != null) { + if (submenuName == null || !submenuName.equals(a.getValue(ActionUtil.SUBMENU_KEY))) { + submenuName = (String) a.getValue(ActionUtil.SUBMENU_KEY); + submenu = new JMenu(submenuName); + menu.add(submenu); + } + } + else { + submenuName = null; + submenu = null; + } + if (a == null) { + if (submenu != null) { + submenu.addSeparator(); + } + else { + menu.addSeparator(); + } + } + else { + AbstractButton button; + + if (a.getValue(ActionUtil.BUTTON_GROUP_KEY) != null) { + ButtonGroup bg = buttonGroups.get(a.getValue(ActionUtil.BUTTON_GROUP_KEY)); + if (bg == null) { + bg = new ButtonGroup(); + buttonGroups.put(a.getValue(ActionUtil.BUTTON_GROUP_KEY), bg); + } + button = new JRadioButtonMenuItem(a); + bg.add(button); + button.setSelected(a.getValue(ActionUtil.SELECTED_KEY) == Boolean.TRUE); + } + else if (a.getValue(ActionUtil.SELECTED_KEY) != null) { + button = new JCheckBoxMenuItem(a); + button.setSelected(a.getValue(ActionUtil.SELECTED_KEY) == Boolean.TRUE); + } + else { + button = new JMenuItem(a); + } + + if (submenu != null) { + submenu.add(button); + } + else { + menu.add(button); + } + } + } + // --- JHotDraw code ends here --- + + for (JMenuItem action : customPopupMenuItems()) { + menu.add(action); + } + + menu.show(c, p.x, p.y); + } + + public abstract List<JMenuItem> customPopupMenuItems(); +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/toolbar/OpenTCSDragTracker.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/toolbar/OpenTCSDragTracker.java new file mode 100644 index 0000000..0a943f1 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/toolbar/OpenTCSDragTracker.java @@ -0,0 +1,217 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.guing.common.jhotdraw.application.toolbar; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.awt.Container; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.MouseEvent; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.HashSet; +import java.util.Set; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.event.TransformEdit; +import org.jhotdraw.draw.tool.DefaultDragTracker; +import org.opentcs.guing.common.application.ApplicationState; +import org.opentcs.guing.common.components.drawing.figures.PathConnection; +import org.opentcs.guing.common.components.drawing.figures.liner.BezierLinerEdit; + +/** + * Utility to follow the drags made by the user. + */ +public class OpenTCSDragTracker + extends + DefaultDragTracker { + + /** + * Stores the application's current state. + */ + private final ApplicationState appState; + /** + * The affected figures. + */ + private Set<Figure> transformedFigures; + /** + * Indicates whether the user is dragging the mouse or not. + */ + private boolean isDragging; + + /** + * Creates a new instance. + * + * @param appState Stores the application's current state. + */ + @Inject + public OpenTCSDragTracker(ApplicationState appState) { + this.appState = requireNonNull(appState, "appState"); + } + + @Override + public void mousePressed(MouseEvent evt) { + DrawingView view = editor.findView((Container) evt.getSource()); + view.requestFocus(); + anchor = new Point(evt.getX(), evt.getY()); + isWorking = true; + fireToolStarted(view); + view = getView(); + + if (evt.isShiftDown()) { + view.toggleSelection(anchorFigure); + + if (!view.isFigureSelected(anchorFigure)) { + anchorFigure = null; + } + } + else if (!view.isFigureSelected(anchorFigure)) { + view.clearSelection(); + view.addToSelection(anchorFigure); + } + + if (!view.getSelectedFigures().isEmpty()) { + dragRect = null; + transformedFigures = new HashSet<>(); + + for (Figure f : view.getSelectedFigures()) { + if (f.isTransformable()) { + transformedFigures.add(f); + + if (dragRect == null) { + dragRect = f.getBounds(); + } + else { + dragRect.add(f.getBounds()); + } + } + } + + if (dragRect != null) { + anchorPoint = view.viewToDrawing(anchor); + previousPoint = anchorPoint; + anchorOrigin = new Point2D.Double(dragRect.x, dragRect.y); + previousOrigin = anchorOrigin; + } + } + } + + @Override + public void mouseDragged(MouseEvent evt) { + DrawingView drawingView = getView(); + + switch (appState.getOperationMode()) { + case MODELLING: + if (!transformedFigures.isEmpty()) { + if (!isDragging) { + isDragging = true; + updateCursor( + editor.findView((Container) evt.getSource()), + new Point(evt.getX(), evt.getY()) + ); + } + + Point2D.Double currentPoint + = drawingView.viewToDrawing(new Point(evt.getX(), evt.getY())); + double offsetX = currentPoint.x - previousPoint.x; + double offsetY = currentPoint.y - previousPoint.y; + dragRect.x += offsetX; + dragRect.y += offsetY; + Rectangle2D.Double constrainedRect = (Rectangle2D.Double) dragRect.clone(); + + if (drawingView.getConstrainer() != null) { + drawingView.getConstrainer().constrainRectangle(constrainedRect); + } + + AffineTransform tx = new AffineTransform(); + tx.translate( + constrainedRect.x - previousOrigin.x, + constrainedRect.y - previousOrigin.y + ); + + for (Figure f : transformedFigures) { + f.willChange(); + f.transform(tx); + f.changed(); + } + + previousPoint = currentPoint; + previousOrigin = new Point2D.Double(constrainedRect.x, constrainedRect.y); + } + + break; + + case OPERATING: + default: + } + } + + @Override // DefaultDragTracker + public void mouseReleased(MouseEvent evt) { + isWorking = false; + DrawingView view = getView(); + + if (transformedFigures != null && !transformedFigures.isEmpty()) { + isDragging = false; + int x = evt.getX(); + int y = evt.getY(); + updateCursor(editor.findView((Container) evt.getSource()), new Point(x, y)); + Point2D.Double newPoint = view.viewToDrawing(new Point(x, y)); + Figure dropTarget = getDrawing().findFigureExcept(newPoint, transformedFigures); + + if (dropTarget != null) { + boolean snapBack = dropTarget.handleDrop(newPoint, transformedFigures, view); + + if (snapBack) { + AffineTransform tx = new AffineTransform(); + tx.translate( + anchorOrigin.x - previousOrigin.x, + anchorOrigin.y - previousOrigin.y + ); + + for (Figure f : transformedFigures) { + f.willChange(); + f.transform(tx); + f.changed(); + } + + Rectangle r = new Rectangle(anchor.x, anchor.y, 0, 0); + r.add(evt.getX(), evt.getY()); + maybeFireBoundsInvalidated(r); + fireToolDone(); + + return; + } + } + + AffineTransform tx = new AffineTransform(); + tx.translate( + -anchorOrigin.x + previousOrigin.x, + -anchorOrigin.y + previousOrigin.y + ); + + if (!tx.isIdentity()) { + getDrawing().fireUndoableEditHappened( + new TransformEdit(transformedFigures, tx) + ); + } + + // On changes on a path + Object[] aFigures = transformedFigures.toArray(); + + if (aFigures[0] instanceof PathConnection) { + getDrawing().fireUndoableEditHappened(new BezierLinerEdit((PathConnection) aFigures[0])); + } + } + + Rectangle r = new Rectangle(anchor.x, anchor.y, 0, 0); + r.add(evt.getX(), evt.getY()); + maybeFireBoundsInvalidated(r); + transformedFigures = null; + fireToolDone(); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/toolbar/OpenTCSSelectAreaTracker.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/toolbar/OpenTCSSelectAreaTracker.java new file mode 100644 index 0000000..2424168 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/toolbar/OpenTCSSelectAreaTracker.java @@ -0,0 +1,225 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.guing.common.jhotdraw.application.toolbar; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Container; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Stroke; +import java.awt.event.MouseEvent; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.handle.Handle; +import org.jhotdraw.draw.tool.DefaultSelectAreaTracker; +import org.opentcs.guing.common.application.ApplicationState; + +/** + * Utility to track area selections made by the user. + */ +public class OpenTCSSelectAreaTracker + extends + DefaultSelectAreaTracker { + + /** + * Stores the application's current state. + */ + private final ApplicationState appState; + /** + * The bounds of the rubberband. + */ + private final Rectangle rubberband = new Rectangle(); + /** + * Rubberband stroke. + */ + private final Stroke rubberbandStroke = new BasicStroke(); + /** + * Rubberband color. When this is null, the tracker does not draw the + * rubberband. + */ + private final Color rubberbandColor = Color.MAGENTA; + /** + * The hover handles, are the handles of the figure over which the mouse + * pointer is currently hovering. + */ + private final List<Handle> hoverHandles = new ArrayList<>(); + /** + * The hover Figure is the figure, over which the mouse is currently hovering. + */ + private Figure hoverFigure; + + /** + * Creates a new instance. + * + * @param appState Stores the application's current state. + */ + @Inject + public OpenTCSSelectAreaTracker(ApplicationState appState) { + this.appState = requireNonNull(appState, "appState"); + } + + @Override // DefaultSelectAreaTracker + public void mousePressed(MouseEvent evt) { + super.mousePressed(evt); + clearRubberBand(); + } + + @Override // DefaultSelectAreaTracker + public void mouseReleased(MouseEvent evt) { + selectGroup(); + clearRubberBand(); + } + + @Override // DefaultSelectAreaTracker + public void mouseDragged(MouseEvent evt) { + Rectangle invalidatedArea = (Rectangle) rubberband.clone(); + rubberband.setBounds( + Math.min(anchor.x, evt.getX()), + Math.min(anchor.y, evt.getY()), + Math.abs(anchor.x - evt.getX()), + Math.abs(anchor.y - evt.getY()) + ); + + if (invalidatedArea.isEmpty()) { + invalidatedArea = (Rectangle) rubberband.clone(); + } + else { + invalidatedArea = invalidatedArea.union(rubberband); + } + + fireAreaInvalidated(invalidatedArea); + } + + @Override // DefaultSelectAreaTracker + public void mouseMoved(MouseEvent evt) { + clearRubberBand(); + Point point = evt.getPoint(); + DrawingView view = editor.findView((Container) evt.getSource()); + updateCursor(view, point); + + if (view == null || editor.getActiveView() != view) { + clearHoverHandles(); + } + else { + // Search first, if one of the selected figures contains + // the current mouse location, and is selectable. + // Only then search for other + // figures. This search sequence is consistent with the + // search sequence of the SelectionTool. + Figure figure = null; + Point2D.Double p = view.viewToDrawing(point); + + for (Figure f : view.getSelectedFigures()) { + if (f != null && f.contains(p)) { + figure = f; + } + } + + if (figure == null) { + figure = view.findFigure(point); + + while (figure != null && !figure.isSelectable()) { + figure = view.getDrawing().findFigureBehind(p, figure); + } + } + + updateHoverHandles(view, figure); + } + } + + private void clearRubberBand() { + if (!rubberband.isEmpty()) { + fireAreaInvalidated(rubberband); + rubberband.width = -1; + } + } + + /** + * Overrides DefaultSelectAreaTracker. + */ + private void selectGroup() { + Collection<Figure> figures = getView().findFiguresWithin(rubberband); + + for (Figure f : figures) { + if (f.isSelectable()) { + getView().addToSelection(f); + } + } + } + + @Override // DefaultSelectAreaTracker + public void draw(Graphics2D g) { + g.setStroke(rubberbandStroke); + g.setColor(rubberbandColor); + g.drawRect(rubberband.x, rubberband.y, rubberband.width - 1, rubberband.height - 1); + + if (!hoverHandles.isEmpty()) { /// && !getView().isFigureSelected(hoverFigure)) { + for (Handle h : hoverHandles) { + h.draw(g); + } + } + } + + @Override + protected void updateHoverHandles(DrawingView drawingView, Figure figure) { + if (figure != hoverFigure) { + Rectangle r = null; + + if (hoverFigure != null) { + for (Handle h : hoverHandles) { + if (r == null) { + r = h.getDrawingArea(); + } + else { + r.add(h.getDrawingArea()); + } + + h.setView(null); + h.dispose(); + } + + hoverHandles.clear(); + } + + hoverFigure = figure; + + if (hoverFigure != null && figure.isSelectable()) { + switch (appState.getOperationMode()) { + case MODELLING: + case OPERATING: + hoverHandles.addAll(hoverFigure.createHandles(-1)); + + for (Handle h : hoverHandles) { + h.setView(drawingView); + + if (r == null) { + r = h.getDrawingArea(); + } + else { + r.add(h.getDrawingArea()); + } + } + + break; + default: + // Do nada. + } + } + + if (r != null) { + r.grow(1, 1); + fireAreaInvalidated(r); + } + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/toolbar/PaletteToolBarBorder.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/toolbar/PaletteToolBarBorder.java new file mode 100644 index 0000000..39e15aa --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/application/toolbar/PaletteToolBarBorder.java @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.guing.common.jhotdraw.application.toolbar; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Cursor; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Insets; +import java.awt.LinearGradientPaint; +import java.awt.MultipleGradientPaint; +import java.awt.RenderingHints; +import java.awt.geom.Point2D; +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JToolBar; +import org.jhotdraw.gui.plaf.palette.PaletteToolBarUI; + +/** + * A toolbar border. + */ +public class PaletteToolBarBorder + extends + org.jhotdraw.gui.plaf.palette.PaletteToolBarBorder { + + private static final float[] ENABLED_STOPS = new float[]{0f, 0.5f, 1f}; + private static final Color[] ENABLED_STOP_COLORS = new Color[]{ + new Color(0xf8f8f8), new Color( + 0xc8c8c8 + ), new Color(0xf8f8f8) + }; + + /** + * Creates a new instance. + */ + public PaletteToolBarBorder() { + } + + @Override + public void paintBorder(Component component, Graphics gr, int x, int y, int w, int h) { + Graphics2D g = (Graphics2D) gr; + + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint( + RenderingHints.KEY_FRACTIONALMETRICS, + RenderingHints.VALUE_FRACTIONALMETRICS_ON + ); + g.setRenderingHint( + RenderingHints.KEY_TEXT_ANTIALIASING, + RenderingHints.VALUE_TEXT_ANTIALIAS_ON + ); + + if (component instanceof JToolBar) { + JToolBar c = (JToolBar) component; + + if (c.isFloatable()) { + int borderColor = 0x80ff0000; + float[] stops = ENABLED_STOPS; + Color[] stopColors = ENABLED_STOP_COLORS; + + g.setColor(new Color(borderColor, true)); + LinearGradientPaint lgp = new LinearGradientPaint( + new Point2D.Float(1, 1), new Point2D.Float(19, 1), + stops, stopColors, + MultipleGradientPaint.CycleMethod.REPEAT + ); + g.setPaint(lgp); + g.fillRect(1, 1, 7 - 2, h - 2); + ImageIcon icon = new ImageIcon( + getClass().getResource("/org/opentcs/guing/res/symbols/toolbar/border.jpg") + ); + + if (c.getComponentCount() != 0 && !(c.getComponents()[0] instanceof JLabel)) { + JLabel label = new JLabel(icon); + label.setFocusable(false); + c.add(label, 0); + label.getParent().setBackground(label.getBackground()); + label.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); + } + } + } + } + + @Override + public Insets getBorderInsets(Component component, Insets insets) { + Insets newInsets = (insets == null) ? new Insets(0, 0, 0, 0) : insets; + + JComponent c = (JComponent) component; + + if (c.getClientProperty(PaletteToolBarUI.TOOLBAR_INSETS_OVERRIDE_PROPERTY) instanceof Insets) { + Insets override + = (Insets) c.getClientProperty(PaletteToolBarUI.TOOLBAR_INSETS_OVERRIDE_PROPERTY); + newInsets.top = override.top; + newInsets.left = override.left; + newInsets.bottom = override.bottom; + newInsets.right = override.right; + + return newInsets; + } + + newInsets.top = 1; + newInsets.left = 1; + newInsets.bottom = 0; + newInsets.right = 0; + + return newInsets; + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/components/drawing/AbstractOpenTCSDrawingView.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/components/drawing/AbstractOpenTCSDrawingView.java new file mode 100644 index 0000000..a2afb37 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/components/drawing/AbstractOpenTCSDrawingView.java @@ -0,0 +1,752 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.guing.common.jhotdraw.components.drawing; + +import static java.util.Objects.requireNonNull; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.MultipleGradientPaint; +import java.awt.Point; +import java.awt.RadialGradientPaint; +import java.awt.Rectangle; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.event.ComponentListener; +import java.awt.event.FocusEvent; +import java.awt.event.KeyEvent; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.File; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.ArrayList; +import java.util.Collection; +import java.util.ConcurrentModificationException; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import javax.swing.JViewport; +import javax.swing.SwingUtilities; +import org.jhotdraw.draw.AbstractFigure; +import org.jhotdraw.draw.DefaultDrawingView; +import org.jhotdraw.draw.Drawing; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.Figure; +import org.jhotdraw.draw.GridConstrainer; +import org.jhotdraw.draw.event.CompositeFigureEvent; +import org.jhotdraw.draw.event.FigureEvent; +import org.jhotdraw.gui.datatransfer.ClipboardUtil; +import org.opentcs.guing.base.model.elements.BlockModel; +import org.opentcs.guing.common.application.ApplicationState; +import org.opentcs.guing.common.application.OperationMode; +import org.opentcs.guing.common.components.EditableComponent; +import org.opentcs.guing.common.components.drawing.BezierLinerEditHandler; +import org.opentcs.guing.common.components.drawing.OffsetListener; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingEditor; +import org.opentcs.guing.common.components.drawing.OpenTCSDrawingView; +import org.opentcs.guing.common.components.drawing.course.Origin; +import org.opentcs.guing.common.components.drawing.figures.BitmapFigure; +import org.opentcs.guing.common.components.drawing.figures.LabeledFigure; +import org.opentcs.guing.common.components.drawing.figures.OriginFigure; +import org.opentcs.guing.common.components.drawing.figures.PathConnection; +import org.opentcs.guing.common.components.drawing.figures.SimpleLineConnection; +import org.opentcs.guing.common.components.drawing.figures.TCSLabelFigure; +import org.opentcs.guing.common.event.SystemModelTransitionEvent; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.guing.common.persistence.ModelManager; +import org.opentcs.guing.common.util.ModelComponentUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A DrawingView implementation for the openTCS plant overview. + */ +public abstract class AbstractOpenTCSDrawingView + extends + DefaultDrawingView + implements + OpenTCSDrawingView, + EditableComponent, + PropertyChangeListener { + + /** + * A property name for 'focus gained'. + */ + public static final String FOCUS_GAINED = "focusGained"; + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(AbstractOpenTCSDrawingView.class); + /** + * Stores the application's current state. + */ + private final ApplicationState appState; + /** + * The manager keeping/providing the currently loaded model. + */ + private final ModelManager modelManager; + /** + * This listener sets the position of the invisible offsetFigure after the + * frame was resized. + */ + private ComponentListener offsetListener; + /** + * Flag whether the labels shall be drawn. + */ + private boolean labelsVisible = true; + /** + * A pointer to a figure that shall be highlighted by a red circle. + */ + private Figure fFocusFigure; + /** + * Prohibits a loop in repaint(). + */ + private boolean doRepaint = true; + /** + * Background figures set to this DrawingView. + */ + private final List<BitmapFigure> bitmapFigures = new ArrayList<>(); + /** + * Handles edits of bezier liners. + */ + private final BezierLinerEditHandler bezierLinerEditHandler = new BezierLinerEditHandler(); + + /** + * Creates new instance. + * + * @param appState Stores the application's current state. + * @param modelManager Provides the current system model. + */ + @SuppressWarnings("this-escape") + public AbstractOpenTCSDrawingView(ApplicationState appState, ModelManager modelManager) { + this.appState = requireNonNull(appState, "appState"); + this.modelManager = requireNonNull(modelManager, "modelManager"); + + // Set a dummy tool tip text to turn tooltips on + setToolTipText(" "); + setBackground(Color.LIGHT_GRAY); + setAutoscrolls(true); + GridConstrainer gridConstrainer = new ExtendedGridConstrainer(10, 10); + gridConstrainer.setMajorGridSpacing(10); + setVisibleConstrainer(gridConstrainer); + setConstrainerVisible(true); + } + + // ### + // Methods of EventHandler start here. + // ### + @Override + public void onEvent(Object event) { + if (event instanceof SystemModelTransitionEvent) { + handleSystemModelTransition((SystemModelTransitionEvent) event); + } + } + + // ### + // Methods of PropertyChangeListener start here. + // ### + @Override // PropertyChangeListener + public void propertyChange(PropertyChangeEvent evt) { + } + + // ### + // Methods of DrawingView start here. + // ### + @Override // DrawingView + public void setDrawing(Drawing newValue) { + Drawing oldValue = this.getDrawing(); + + if (oldValue != null) { + oldValue.removeUndoableEditListener(bezierLinerEditHandler); + } + if (newValue != null) { + SystemModel model = modelManager.getModel(); + if (model != null) { + Origin origin = model.getDrawingMethod().getOrigin(); + OriginFigure originFigure = origin.getFigure(); + if (!newValue.contains(originFigure)) { + newValue.add(originFigure); + } + } + + newValue.addUndoableEditListener(bezierLinerEditHandler); + } + + super.setDrawing(newValue); + } + + @Override + public OpenTCSDrawingEditor getEditor() { + return (OpenTCSDrawingEditor) super.getEditor(); + } + + @Override // DrawingView + public void addToSelection(Figure figure) { + refreshDetailLevel(); + + super.addToSelection(figure); + + getEditor().figuresSelected(new ArrayList<>(getSelectedFigures())); + repaint(); + } + + @Override // DrawingView + public void addToSelection(Collection<Figure> figures) { + refreshDetailLevel(); + + super.addToSelection(figures); + } + + @Override // DrawingView + public void removeFromSelection(Figure figure) { + super.removeFromSelection(figure); + + // The OpenTCSDrawingEditor may be null here, although it shouldn't. + // A possible explanation at the moment is: When restoring the default + // layout all drawing views are removed and recreated. Currently this also + // means that all bitmap figures are removed, which fire an event to all + // listeners, that they were removed. It seems that one of the + // removed drawing views receives this event. It may be possible + // JHotDraw just didn't remove the view from the listener list. + if (getEditor() != null) { + getEditor().figuresSelected(new ArrayList<>(getSelectedFigures())); + repaint(); + } + } + + @Override // DrawingView + public void selectAll() { + super.selectAll(); + getEditor().figuresSelected(new ArrayList<>(getSelectedFigures())); + } + + @Override // DrawingView + public void clearSelection() { + super.clearSelection(); + + if (getSelectionCount() > 0) { + getEditor().figuresSelected(new ArrayList<>(getSelectedFigures())); + } + } + + @Override // DrawingView + public void setScaleFactor(final double newValue) { + if (newValue == getScaleFactor()) { + return; + } + + // Save the (drawing) coordinates of the current center point to jump back to there after the + // zoom. + Rectangle2D.Double visibleViewRect = viewToDrawing(getVisibleRect()); + final int centerX = (int) (visibleViewRect.getCenterX() + 0.5); + final int centerY = (int) -(visibleViewRect.getCenterY() + 0.5); + for (BitmapFigure bmFigure : bitmapFigures) { + bmFigure.setScaleFactor(getScaleFactor(), newValue); + } + + SwingUtilities.invokeLater(() -> { + super.setScaleFactor(newValue); + scrollTo(centerX, centerY); + }); + } + + @Override // DrawingView + public void addNotify(DrawingEditor editor) { + addOffsetListener((OpenTCSDrawingEditor) editor); + + super.addNotify(editor); + } + + @Override // DrawingView + public void removeNotify(DrawingEditor editor) { + // super.removeNotify(editor) sets the drawing editor of this drawing view to null. + // Handles that exist when this method is invoked, will therefore afterwards have a reference to + // a drawing view without a drawing editor. To avoid NPEs on such handles, clear the selection + // which removes and implicitly invalidates any handles. + clearSelection(); + + super.removeNotify(editor); + } + + // ### + // Methods of JComponent start here. + // ### + @Override + public void processKeyEvent(KeyEvent e) { + if ((e.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) != 0) { + // Cut, copy, paste and duplicate + if (e.getKeyCode() == KeyEvent.VK_X + || e.getKeyCode() == KeyEvent.VK_C + || e.getKeyCode() == KeyEvent.VK_V + || e.getKeyCode() == KeyEvent.VK_D) { + if (!appState.hasOperationMode(OperationMode.MODELLING)) { + return; + } + processCutPasteKeyEvent(); + } + } + + super.processKeyEvent(e); + } + + // ### + // Methods not declared in interfaces or superclasses start here. + // ### + @Override + public void drawingOptionsChanged() { + repaintDrawingArea(); + } + + private void repaintDrawingArea() { + // We need to add the visible rect to the dirty area before we repaint. + repaintDrawingArea(viewToDrawing(getVisibleRect())); + } + + @Override + public boolean isLabelsVisible() { + return labelsVisible; + } + + @Override + public void setLabelsVisible(boolean newValue) { + labelsVisible = newValue; + + if (getDrawing() == null) { + return; + } + + for (Figure figure : getDrawing().getChildren()) { + if (figure instanceof LabeledFigure) { + LabeledFigure lf = (LabeledFigure) figure; + lf.setLabelVisible(newValue); + } + } + // Repaint the whole layout. + repaintDrawingArea(); + } + + /** + * Returns the background images for this drawing view. + * + * @return List containing the associated bitmap figures. + */ + public List<BitmapFigure> getBackgroundBitmaps() { + return bitmapFigures; + } + + @Override + public void addBackgroundBitmap(File file) { + addBackgroundBitmap(new BitmapFigure(file)); + } + + @Override + public void addBackgroundBitmap(BitmapFigure bitmapFigure) { + bitmapFigures.add(bitmapFigure); + getDrawing().add(bitmapFigure); + getDrawing().sendToBack(bitmapFigure); + } + + @Override + public void scrollTo(Figure figure) { + if (figure == null) { + return; + } + + fFocusFigure = figure; + + scrollRectToVisible(computeVisibleRectangleForFigure(figure)); + + repaint(); + } + + @Override + public void updateBlock(BlockModel block) { + for (Figure figure : ModelComponentUtil.getChildFigures(block, modelManager.getModel())) { + ((AbstractFigure) figure).fireFigureChanged(); + } + } + + @Override + public boolean containsPointOnScreen(Point p) { + return (p.x >= getLocationOnScreen().x && p.x < (getLocationOnScreen().x + getWidth()) + && p.y >= getLocationOnScreen().y && p.y < (getLocationOnScreen().y + getHeight())); + } + + @Override + public void zoomViewToWindow() { + // 1. Zoom to 100% + setScaleFactor(1.0); + // 2. Zoom delayed + SwingUtilities.invokeLater( + () -> setScaleFactor(computeZoomLevelToDisplayAllFigures(getDrawing())) + ); + } + + @Override + protected void drawDrawing(Graphics2D gr) { + if (getDrawing() == null) { + return; + } + + Graphics2D g2d = (Graphics2D) gr.create(); + AffineTransform tx = g2d.getTransform(); + tx.translate( + getDrawingToViewTransform().getTranslateX(), + getDrawingToViewTransform().getTranslateY() + ); + tx.scale(getScaleFactor(), getScaleFactor()); + g2d.setTransform(tx); + + getDrawing().setFontRenderContext(g2d.getFontRenderContext()); + try { + getDrawing().draw(g2d); + } + catch (ConcurrentModificationException e) { + LOG.warn("Exception from JHotDraw caught while calling DefaultDrawing.draw(), continuing."); + // TODO What to do when it is catched? + } + + g2d.dispose(); + } + + private void handleSystemModelTransition(SystemModelTransitionEvent evt) { + switch (evt.getStage()) { + case UNLOADING: + removeAll(); + break; + default: + // Do nada. + } + } + + private void processCutPasteKeyEvent() { + List<Figure> selectedFigures = new CopyOnWriteArrayList<>(getSelectedFigures()); + + for (Figure figure : selectedFigures) { + if (figure instanceof SimpleLineConnection) { + // A Path may only be selected if the connected start and end Points are selected, too + SimpleLineConnection lineConnection = (SimpleLineConnection) figure; + Figure startFigure = lineConnection.getStartFigure(); + Figure endFigure = lineConnection.getEndFigure(); + + if (!selectedFigures.contains(startFigure) + || !selectedFigures.contains(endFigure)) { + removeFromSelection(figure); + } + } + } + + if (getSelectedFigures().isEmpty()) { + ClipboardUtil.getClipboard().setContents(new Transferable() { + + @Override + public DataFlavor[] getTransferDataFlavors() { + return new DataFlavor[0]; + } + + @Override + public boolean isDataFlavorSupported(DataFlavor flavor) { + return false; + } + + @Override + public Object getTransferData(DataFlavor flavor) { + throw new UnsupportedOperationException("Not supported yet."); + } + }, null); + } + } + + /** + * Moves the view so that the given points will be in the middle of the + * drawing. + * + * @param xCenter The x coord that shall be in the middle. + * @param yCenter The y coord that shall be in the middle. + */ + private void scrollTo(final int xCenter, final int yCenter) { + SwingUtilities.invokeLater(() -> { + Point2D.Double pCenterView = new Point2D.Double(xCenter, -yCenter); // Vorzeichen! + Point pCenterDrawing = drawingToView(pCenterView); + JViewport viewport = (JViewport) getParent(); + int xUpperLeft = pCenterDrawing.x - viewport.getSize().width / 2; + int yUpperLeft = pCenterDrawing.y - viewport.getSize().height / 2; + Point pUpperLeft = new Point(xUpperLeft, yUpperLeft); + Rectangle rCenter = new Rectangle(pUpperLeft, viewport.getSize()); + scrollRectToVisible(rCenter); + }); + } + + @Override + protected void drawConstrainer(Graphics2D g) { + // The super-implementation draws only for positive coordinates, which we don't want. + getConstrainer().draw(g, this); + } + + /** + * Draws a focus circle around the currently selected figure. + * + * @param g2d + */ + protected void highlightFocus(Graphics2D g2d) { + if (fFocusFigure == null || !fFocusFigure.isVisible()) { + return; + } + + final Figure currentFocusFigure = fFocusFigure; + Rectangle2D.Double bounds = fFocusFigure.getBounds(); + double xCenter = bounds.getCenterX(); + double yCenter = bounds.getCenterY(); + + if (fFocusFigure instanceof PathConnection) { + xCenter = ((PathConnection) fFocusFigure).getCenter().x; + yCenter = ((PathConnection) fFocusFigure).getCenter().y; + } + + Point2D.Double pCenterView = new Point2D.Double(xCenter, yCenter); + Point pCenterDrawing = drawingToView(pCenterView); + + // Create a radial gradient, transparent in the middle. + Point2D center + = new Point2D.Float((float) pCenterDrawing.x, (float) pCenterDrawing.y); + float radius = 30; + float[] dist = {0.0f, 0.7f, 0.8f, 1.0f}; + Color[] colors = { + new Color(1.0f, 1.0f, 1.0f, 0.0f), // Focus: 100% transparent + new Color(1.0f, 1.0f, 1.0f, 0.0f), + new Color(1.0f, 0.0f, 0.0f, 0.7f), // Circle: red + new Color(0.9f, 0.9f, 0.9f, 0.5f) // Background + }; + RadialGradientPaint p + = new RadialGradientPaint( + center, radius, dist, colors, + MultipleGradientPaint.CycleMethod.NO_CYCLE + ); + + Graphics2D gFocus = (Graphics2D) g2d.create(); + gFocus.setPaint(p); + gFocus.fillRect(0, 0, getWidth(), getHeight()); + gFocus.dispose(); + + // After drawing the RadialGradientPaint the drawing area needs to be + // repainted, otherwise the GradientPaint isn't drawn correctly or + // the old one isn't removed. We make sure the repaint() call doesn't + // end in an infinite loop. + loopProofRepaintDrawingArea(); + + // after 3 seconds the RadialGradientPaint is removed + new Thread(() -> { + try { + synchronized (AbstractOpenTCSDrawingView.this) { + AbstractOpenTCSDrawingView.this.wait(3000); + } + } + catch (InterruptedException ex) { + } + + // prevents repainting of the drawing area if in the 3 second wait + // time another figure was selected + if (fFocusFigure == currentFocusFigure) { + fFocusFigure = null; + repaint(); + } + }).start(); + } + + protected void loopProofRepaintDrawingArea() { + if (doRepaint) { + repaintDrawingArea(); + doRepaint = false; + } + else { + doRepaint = true; + } + } + + protected ModelManager getModelManager() { + return modelManager; + } + + /** + * The detailLevel indicates what type of Handles are supposed to be shouwn. In Operating mode, + * we require the type of handles for our figures that do not allow moving, that is + * indicated by -1. + * In modelling mode we require the regular Handles, indicated by 0. + */ + private void refreshDetailLevel() { + if (OperationMode.OPERATING.equals(appState.getOperationMode())) { + setHandleDetailLevel(-1); + } + else { + setHandleDetailLevel(0); + } + } + + /** + * Enables the listener for updating the offset figures. + */ + private void addOffsetListener(OpenTCSDrawingEditor newEditor) { + offsetListener = new OffsetListener(newEditor); + addComponentListener(offsetListener); + } + + /** + * Computes the rectangle to draw for scrolling to the given figure. + * + * @param figure The figure to be shown. + * @return The rectangle area to be drawn. + */ + private Rectangle computeVisibleRectangleForFigure(Figure figure) { + // The rectangle that encloses the figure + Rectangle2D.Double bounds = computeBounds(figure); + + final int margin = 50; + + Point pCenterDrawing = drawingToView( + new Point2D.Double( + bounds.getCenterX() - margin, + bounds.getCenterY() - margin + ) + ); + Dimension dBounds = new Dimension( + (int) (bounds.getWidth() + 2 * margin), + (int) (bounds.getHeight() + 2 * margin) + ); + + return new Rectangle(pCenterDrawing, dBounds); + } + + /** + * Computes the rectangle enclosing the given figure. + * + * @param figure The figure. + * @return The rectangle enclosing the given figure. + */ + protected Rectangle2D.Double computeBounds(Figure figure) { + // The rectangle that encloses the figure + Rectangle2D.Double bounds = figure.getBounds(); + + if (figure instanceof LabeledFigure) { + // Also show the label + LabeledFigure lf = (LabeledFigure) figure; + TCSLabelFigure label = lf.getLabel(); + + if (label != null) { + Rectangle2D.Double labelBounds = label.getBounds(); + bounds.add(labelBounds); + } + } + + return bounds; + } + + /** + * Computes a fitting zoom level for displaying all figures currently in the drawing. + * + * @param drawing The drawing. + * @return The zoom level. + */ + private double computeZoomLevelToDisplayAllFigures(Drawing drawing) { + // Rectangle that contains all figures + Rectangle2D.Double drawingArea = drawing.getDrawingArea(); + double wDrawing = drawingArea.width + 2 * 20; + double hDrawing = drawingArea.height + 2 * 20; + // The currently visible rectangle + Rectangle visibleRect = getComponent().getVisibleRect(); + + double xFactor = visibleRect.width / wDrawing; + double yFactor = visibleRect.height / hDrawing; + + // Find the smallest, and limit to 400%. + double newZoom = Math.min(Math.min(xFactor, yFactor), 4.0); + + return roundToTwoDecimalPlaces(newZoom); + } + + /** + * Rounds the given value so that its decimal representation has only two places. + * + * @param value The value. + * @return The rounded value. + */ + private double roundToTwoDecimalPlaces(double value) { + DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(); + symbols.setDecimalSeparator('.'); + DecimalFormat twoDForm = new DecimalFormat("#.##", symbols); + return Double.parseDouble(twoDForm.format(value)); + } + + public abstract class AbstractExtendedEventHandler + extends + DefaultDrawingView.EventHandler { + + @Override // CompositeFigureListener + public void figureRemoved(CompositeFigureEvent evt) { + if (evt.getChildFigure() instanceof BitmapFigure) { + BitmapFigure bmFigure = (BitmapFigure) evt.getChildFigure(); + + if (!bmFigure.isTemporarilyRemoved()) { + BitmapFigure childFigure = (BitmapFigure) evt.getChildFigure(); + bitmapFigures.remove(childFigure); + } + } + + super.figureRemoved(evt); + } + + @Override + public void focusGained(FocusEvent e) { + super.focusGained(e); + + if (getEditor() != null) { + List<BitmapFigure> figuresToRemove = new ArrayList<>(); + + for (Figure fig : getDrawing().getFiguresFrontToBack()) { + if (fig instanceof BitmapFigure) { + figuresToRemove.add((BitmapFigure) fig); + } + // Commented out on 2020-07-17 by Martin Grzenia: + // During the integration of layers this block caused some problems. When a layer is + // hidden, setVisible(false) is called for all figures contained in that particular layer. + // This block caused all figures to be shown again once the drawing view gained focus. + // The purpose of this block is not quite clear, but it seems a bit strange at least. + //if (shouldShowFigure(fig)) { + // ((AbstractFigure) fig).setVisible(true); + //} + } + + for (BitmapFigure figure : figuresToRemove) { + figure.setTemporarilyRemoved(true); + getDrawing().remove(figure); + } + + for (BitmapFigure bmFigure : bitmapFigures) { + bmFigure.setTemporarilyRemoved(false); + getDrawing().add(bmFigure); + getDrawing().sendToBack(bmFigure); + } + } + } + + @Override // FigureListener + public void figureRequestRemove(FigureEvent evt) { + super.figureRequestRemove(evt); + + if (evt.getFigure() instanceof BitmapFigure) { + BitmapFigure bmFigure = (BitmapFigure) evt.getFigure(); + + if (!bmFigure.isTemporarilyRemoved()) { + BitmapFigure figure = (BitmapFigure) evt.getFigure(); + bitmapFigures.remove(figure); + } + } + } + + protected abstract boolean shouldShowFigure(Figure figure); + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/components/drawing/ExtendedGridConstrainer.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/components/drawing/ExtendedGridConstrainer.java new file mode 100644 index 0000000..0346b19 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/components/drawing/ExtendedGridConstrainer.java @@ -0,0 +1,172 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.guing.common.jhotdraw.components.drawing; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import org.jhotdraw.draw.DrawingView; +import org.jhotdraw.draw.GridConstrainer; + +/** + * Constrains a point such that it falls on a grid. + * + * @author Werner Randelshofer + */ +public class ExtendedGridConstrainer + extends + GridConstrainer { + + private static final int MIN_GRID_SPACING = 4; + /** + * The spacing factor for a medium grid cell. + */ + private static final int MEDIUM_GRID_SPACING = 5; + /** + * The color for minor grid cells. + */ + private static final Color MINOR_COLOR = new Color(0xd4d4d4); + /** + * The color for major grid cells. + */ + private static final Color MAJOR_COLOR = new Color(0xc0c0c0); + /** + * The color for medium grid cells. + */ + private static final Color MEDIUM_COLOR = new Color(0xd0d0d0); + /** + * The color for x- and y-axis. + */ + private static final Color AXIS_COLOR = new Color(0xC040C0); + + public ExtendedGridConstrainer() { + super(); + } + + public ExtendedGridConstrainer(double width, double height) { + super(width, height); + } + + public ExtendedGridConstrainer(double width, double height, boolean visible) { + super(width, height, visible); + } + + public ExtendedGridConstrainer(double width, double height, double theta, boolean visible) { + super(width, height, theta, visible); + } + + @Override + public void draw(Graphics2D g, DrawingView view) { + if (isVisible()) { + double width = getWidth(); + double height = getHeight(); + int majorGridSpacing = getMajorGridSpacing(); + + AffineTransform t = view.getDrawingToViewTransform(); + Rectangle viewBounds = g.getClipBounds(); + Rectangle2D.Double bounds = view.viewToDrawing(viewBounds); + + Point2D.Double origin = constrainPoint(new Point2D.Double(bounds.x, bounds.y)); + Point2D.Double point = new Point2D.Double(); + Point2D.Double viewPoint = new Point2D.Double(); + + // Vertical grid lines are only drawn, if they are at least [MIN_GRID_SPACING] + // pixels apart on the view coordinate system. + if (width * view.getScaleFactor() > MIN_GRID_SPACING) { + for (int i = (int) (origin.x / width), m = (int) ((origin.x + bounds.width) / width) + 1; + i <= m; i++) { + point.x = width * i; + t.transform(point, viewPoint); + + if (i == 0) { + g.setColor(AXIS_COLOR); + } + else if (i % majorGridSpacing == 0) { + g.setColor(MAJOR_COLOR); + } + else if (i % MEDIUM_GRID_SPACING == 0) { + g.setColor(MEDIUM_COLOR); + } + else { + g.setColor(MINOR_COLOR); + } + + g.drawLine( + (int) viewPoint.x, + viewBounds.y, + (int) viewPoint.x, + viewBounds.y + viewBounds.height + ); + } + } + else if (width * majorGridSpacing * view.getScaleFactor() > 2) { + g.setColor(MAJOR_COLOR); + + for (int i = (int) (origin.x / width), m = (int) ((origin.x + bounds.width) / width) + 1; + i <= m; i++) { + if (i % majorGridSpacing == 0) { + point.x = width * i; + t.transform(point, viewPoint); + g.drawLine( + (int) viewPoint.x, + viewBounds.y, + (int) viewPoint.x, + viewBounds.y + viewBounds.height + ); + } + } + } + + // Horizontal grid lines are only drawn, if they are at least [MIN_GRID_SPACING] + // pixels apart on the view coordinate system. + if (height * view.getScaleFactor() > MIN_GRID_SPACING) { + for (int i = (int) (origin.y / height), m = (int) ((origin.y + bounds.height) / height) + 1; + i <= m; i++) { + point.y = height * i; + t.transform(point, viewPoint); + + if (i == 0) { + g.setColor(AXIS_COLOR); + } + else if (i % majorGridSpacing == 0) { + g.setColor(MAJOR_COLOR); + } + else if (i % MEDIUM_GRID_SPACING == 0) { + g.setColor(MEDIUM_COLOR); + } + else { + g.setColor(MINOR_COLOR); + } + + g.drawLine( + viewBounds.x, + (int) viewPoint.y, + viewBounds.x + viewBounds.width, + (int) viewPoint.y + ); + } + } + else if (height * majorGridSpacing * view.getScaleFactor() > 2) { + g.setColor(MAJOR_COLOR); + + for (int i = (int) (origin.y / height), m = (int) ((origin.y + bounds.height) / height) + 1; + i <= m; i++) { + if (i % majorGridSpacing == 0) { + point.y = height * i; + t.transform(point, viewPoint); + g.drawLine( + viewBounds.x, + (int) viewPoint.y, + viewBounds.x + viewBounds.width, + (int) viewPoint.y + ); + } + } + } + } + } +} diff --git a/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/util/ResourceBundleUtil.java b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/util/ResourceBundleUtil.java new file mode 100644 index 0000000..0ab0577 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/java/org/opentcs/thirdparty/guing/common/jhotdraw/util/ResourceBundleUtil.java @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: The original authors of JHotDraw and all its contributors +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.thirdparty.guing.common.jhotdraw.util; + +import static java.util.Objects.requireNonNull; + +import java.io.Serializable; +import java.text.MessageFormat; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is a convenience wrapper for accessing resources stored in a ResourceBundle. + * + * @author Werner Randelshofer, Hausmatt 10, CH-6405 Immensee, Switzerland + */ +public class ResourceBundleUtil + implements + Serializable { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ResourceBundleUtil.class); + /** + * The global verbose property. + */ + private static final boolean IS_VERBOSE = true; + /** + * The wrapped resource bundle. + */ + private final ResourceBundle resource; + /** + * The base name of the resource bundle. + */ + private final String baseName; + + /** + * Creates a new ResouceBundleUtil which wraps the provided resource bundle. + * + * @param baseName + * @param locale + */ + public ResourceBundleUtil(String baseName, Locale locale) { + this.baseName = requireNonNull(baseName, "baseName"); + requireNonNull(locale, "locale"); + this.resource = ResourceBundle.getBundle(baseName, locale); + } + + /** + * Get a String from the ResourceBundle. <br>Convenience method to save + * casting. + * + * @param key The key of the property. + * @return The value of the property. Returns the key if the property is + * missing. + */ + public String getString(String key) { + try { + return resource.getString(key); + } + catch (MissingResourceException e) { + if (IS_VERBOSE) { + LOG.warn("baseName: {}, '{}' not found.", baseName, key, e); + } + + return key; + } + } + + /** + * Returns a formatted string using javax.text.MessageFormat. + * + * @param key + * @param arguments + * @return formatted String + */ + public String getFormatted(String key, Object... arguments) { + return MessageFormat.format(getString(key), arguments); + } + + public static ResourceBundleUtil getBundle(String baseName) + throws MissingResourceException { + return new ResourceBundleUtil(baseName, Locale.getDefault()); + } + + @Override + public String toString() { + return super.toString() + "[" + baseName + ", " + resource + "]"; + } +} diff --git a/opentcs-plantoverview-common/src/main/resources/REUSE.toml b/opentcs-plantoverview-common/src/main/resources/REUSE.toml new file mode 100644 index 0000000..42aab58 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/resources/REUSE.toml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC0-1.0 + +version = 1 + +[[annotations]] +path = ["**/*.gif", "**/*.jpg", "**/*.png", "**/*.svg"] +precedence = "closest" +SPDX-FileCopyrightText = "The openTCS Authors" +SPDX-License-Identifier = "CC-BY-4.0" diff --git a/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/dialogs/modelProperties.properties b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/dialogs/modelProperties.properties new file mode 100644 index 0000000..cce1465 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/dialogs/modelProperties.properties @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +modelPropertiesAction.optionPane_properties.message.lastModified=Last modified: +modelPropertiesAction.optionPane_properties.message.numberOfBlocks=Number of blocks: +modelPropertiesAction.optionPane_properties.message.numberOfLocationTypes=Number of location types: +modelPropertiesAction.optionPane_properties.message.numberOfLocations=Number of locations: +modelPropertiesAction.optionPane_properties.message.numberOfPaths=Number of paths: +modelPropertiesAction.optionPane_properties.message.numberOfPoints=Number of points: +modelPropertiesAction.optionPane_properties.message.numberOfVehicles=Number of vehicles: diff --git a/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/dialogs/modelProperties_de.properties b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/dialogs/modelProperties_de.properties new file mode 100644 index 0000000..14213bc --- /dev/null +++ b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/dialogs/modelProperties_de.properties @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +modelPropertiesAction.optionPane_properties.message.lastModified=Letzte \u00c4nderung: +modelPropertiesAction.optionPane_properties.message.numberOfBlocks=Anzahl Bl\u00f6cke: +modelPropertiesAction.optionPane_properties.message.numberOfLocationTypes=Anzahl Stationstypen: +modelPropertiesAction.optionPane_properties.message.numberOfLocations=Anzahl Stationen: +modelPropertiesAction.optionPane_properties.message.numberOfPaths=Anzahl Pfade: +modelPropertiesAction.optionPane_properties.message.numberOfPoints=Anzahl Punkte: +modelPropertiesAction.optionPane_properties.message.numberOfVehicles=Anzahl Fahrzeuge: diff --git a/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/mainMenu.properties b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/mainMenu.properties new file mode 100644 index 0000000..d839d4b --- /dev/null +++ b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/mainMenu.properties @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +abstractTreeViewPanel.deleteEdit.presentationName=Delete +abstractTreeViewPanel.pasteEdit.presentationName=Paste +deleteAction.name=Delete +deleteAction.shortDescription=Delete the selected figures +fileModeMenu.text=Mode +modelPropertiesAction.name=Show model properties +modelPropertiesAction.shortDescription=Show the current model's properties +saveModelAction.name=Save Model +saveModelAction.shortDescription=Save model to file +saveModelAsAction.name=Save Model As... +saveModelAsAction.shortDescription=Save the current model with a a new name +selectAllAction.name=Select All +selectAllAction.shortDescription=Select all figures in the drawing +undoRedoManager.redoAction.name=Redo +undoRedoManager.redoAction.shortDescription=Redo the last action +undoRedoManager.undoAction.name=Undo +undoRedoManager.undoAction.shortDescription=Undo the last action +viewPluginPanelsMenu.menuItem_none.text=No plugins available +viewPluginPanelsMenu.text=Plugins diff --git a/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/mainMenu_de.properties b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/mainMenu_de.properties new file mode 100644 index 0000000..b0edea5 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/mainMenu_de.properties @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +abstractTreeViewPanel.deleteEdit.presentationName=L\u00f6schen +abstractTreeViewPanel.pasteEdit.presentationName=Einf\u00fcgen +deleteAction.name=L\u00f6schen +deleteAction.shortDescription=Selektierte Figuren L\u00f6schen +file.loadModel.couldNotLoad.message=Konnte Modell "{0}" nicht laden. +file.loadModel.fileDoesNotExist.message=Das Modell "{0}" existiert nicht. +file.saveModel.couldNotSave.message=Konnte Modell nicht in die Datei "{0}" speichern. +fileModeMenu.text=Modus +modelPropertiesAction.name=Modelleigenschaften zeigen +modelPropertiesAction.shortDescription=Eigenschaften des aktuellen Modells anzeigen +saveModelAction.name=Anlagenmodell speichern +saveModelAction.shortDescription=Anlagenmodell in Datei speichern +saveModelAsAction.name=Anlagenmodell speichern als... +saveModelAsAction.shortDescription=Anlagenmodell unter neuem Namen speichern +selectAllAction.name=Alles ausw\u00e4hlen +selectAllAction.shortDescription=Alle Figuren ausw\u00e4hlen +undoRedoManager.redoAction.name=Wiederholen +undoRedoManager.redoAction.shortDescription=Die letzte Aktion wiederholen +undoRedoManager.undoAction.name=R\u00fcckg\u00e4ngig +undoRedoManager.undoAction.shortDescription=Die letzte Aktion r\u00fcckg\u00e4ngig machen +viewPluginPanelsMenu.menuItem_none.text=Keine Plugins verf\u00fcgbar +viewPluginPanelsMenu.text=Plugins diff --git a/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/miscellaneous.properties b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/miscellaneous.properties new file mode 100644 index 0000000..e7eaa09 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/miscellaneous.properties @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +bezierLinerEdit.presentationName=Move control point +modelRestorationProgressStatus.description.cleanup=Cleaning up working area. +modelRestorationProgressStatus.description.setUpDirectoryTree=Setting up the model directory view. +modelRestorationProgressStatus.description.setUpModelView=Setting up the model view. +modelRestorationProgressStatus.description.setUpWorkingArea=Model restored. Setting up working area. +modelRestorationProgressStatus.description.startLoadingBlocks=Loading blocks. +modelRestorationProgressStatus.description.startLoadingLocations=Loading locations. +modelRestorationProgressStatus.description.startLoadingModel=Start loading the model. +modelRestorationProgressStatus.description.startLoadingPaths=Loading paths. +modelRestorationProgressStatus.description.startLoadingPoints=Loading points. +modelRestorationProgressStatus.description.startLoadingVehicles=Loading vehicles. +openTcsModelManager.message_notSaved.text=An error occurred while saving the model. The model was not saved. +openTcsModelManager.optionPane_fileExists.message=The file already exists. Do you want to overwrite it? +openTcsModelManager.optionPane_fileExists.title=File already exists +startupProgressStatus.description.initializeModel=Initialize system model +startupProgressStatus.description.initialized=openTCS view initialized +startupProgressStatus.description.showPlantOverview=Launch openTCS visualization application +startupProgressStatus.description.startPlantOverview=Start openTCS visualization diff --git a/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/miscellaneous_de.properties b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/miscellaneous_de.properties new file mode 100644 index 0000000..f2accba --- /dev/null +++ b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/miscellaneous_de.properties @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +bezierLinerEdit.presentationName=Kontrollpunkt verschieben +modelRestorationProgressStatus.description.cleanup=R\u00e4ume Arbeitsfl\u00e4che auf. +modelRestorationProgressStatus.description.setUpDirectoryTree=Erstelle Baumdarstellung des Modells. +modelRestorationProgressStatus.description.setUpModelView=Erstelle visuelle Ansicht des Modells. +modelRestorationProgressStatus.description.setUpWorkingArea=Modell geladen. Richte Arbeitsfl\u00e4che ein. +modelRestorationProgressStatus.description.startLoadingBlocks=Lade Bl\u00f6cke. +modelRestorationProgressStatus.description.startLoadingLocations=Lade Stationen. +modelRestorationProgressStatus.description.startLoadingModel=Lade Modell. +modelRestorationProgressStatus.description.startLoadingPaths=Lade Pfade. +modelRestorationProgressStatus.description.startLoadingPoints=Lade Punkte. +modelRestorationProgressStatus.description.startLoadingVehicles=Lade Fahrzeuge. +openTcsModelManager.message_notSaved.text=Ein Fehler ist aufgetreten w\u00e4hrend des Speicherns des Modells. Das Modell wurde nicht gespeichert. +openTcsModelManager.optionPane_fileExists.message=Die Datei existiert bereits. Möchten Sie sie überschreiben? +openTcsModelManager.optionPane_fileExists.title=Datei existiert bereits +startupProgressStatus.description.initializeModel=Lade Systemmodell +startupProgressStatus.description.initialized=openTCS-Ansicht initialisiert +startupProgressStatus.description.showPlantOverview=Zeige openTCS-Visualisierung an +startupProgressStatus.description.startPlantOverview=Starte openTCS-Visualisierung diff --git a/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/componentTrees.properties b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/componentTrees.properties new file mode 100644 index 0000000..e92c9b2 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/componentTrees.properties @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +blockMouseListener.popupMenuItem_addToBlock.text=Add selected elements to block +blockMouseListener.popupMenuItem_removeFromBlock.text=Remove selected elements from block +blockMouseListener.popupMenuItem_selectAllElements.text=Select all elements of block +groupsMouseAdapter.popupMenuItem_addToGroup.text=Add selection to this group +groupsMouseAdapter.popupMenuItem_deleteGroup.text=Delete group +groupsMouseAdapter.popupMenuItem_removeFromGroup.text=Remove from group +groupsMouseAdapter.popupMenuItem_show.text=Show in "{0}" +groupsMouseAdapter.popupMenuItem_showInAll.text=Show in all driving courses +standardSystemModel.folder_blocks.name=Blocks +standardSystemModel.folder_links.name=Links +standardSystemModel.folder_locationTypes.name=Location types +standardSystemModel.folder_locations.name=Locations +standardSystemModel.folder_otherGraphicalElements.name=Other graphical elements +standardSystemModel.folder_paths.name=Paths +standardSystemModel.folder_points.name=Points +standardSystemModel.folder_vehicles.name=Vehicles +standardSystemModel.property_name.description=Name +standardSystemModel.property_name.helptext=The name of this model. +treeMouseAdapter.popupMenuItem_closeAllFolders.text=Close all folders +treeMouseAdapter.popupMenuItem_closeAllFolders.tooltipText=Close all folders +treeMouseAdapter.popupMenuItem_expandAllFolders.text=Expand all folders +treeMouseAdapter.popupMenuItem_expandAllFolders.tooltipText=Expand all folders +treeMouseAdapter.popupMenuItem_sortAllItems.text=Sort all items +treeMouseAdapter.popupMenuItem_sortAllItems.tooltipText=Sort all items diff --git a/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/componentTrees_de.properties b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/componentTrees_de.properties new file mode 100644 index 0000000..52a33cc --- /dev/null +++ b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/componentTrees_de.properties @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +blockMouseListener.popupMenuItem_addToBlock.text=Markierte Elemente dem Blockbereich zuweisen +blockMouseListener.popupMenuItem_removeFromBlock.text=Markierte Elemente aus dem Blockbereich entfernen +blockMouseListener.popupMenuItem_selectAllElements.text=Alle Elemente des Blockbereiches markieren +groupsMouseAdapter.popupMenuItem_addToGroup.text=Selektierung zu dieser Gruppe hinzuf\u00fcgen +groupsMouseAdapter.popupMenuItem_deleteGroup.text=Gruppe l\u00f6schen +groupsMouseAdapter.popupMenuItem_removeFromGroup.text=Aus Gruppe entfernen +groupsMouseAdapter.popupMenuItem_show.text=Zeige in "{0}" an +groupsMouseAdapter.popupMenuItem_showInAll.text=In allen Fahrkursen anzeigen +standardSystemModel.folder_blocks.name=Blockbereiche +standardSystemModel.folder_links.name=Referenzen +standardSystemModel.folder_locationTypes.name=Stationstypen +standardSystemModel.folder_locations.name=Stationen +standardSystemModel.folder_otherGraphicalElements.name=Sonstige graphische Elemente +standardSystemModel.folder_paths.name=Strecken +standardSystemModel.folder_points.name=Punkte +standardSystemModel.folder_vehicles.name= Fahrzeuge +standardSystemModel.property_name.description=Name +standardSystemModel.property_name.helptext=Der Name dieses Modells. +treeMouseAdapter.popupMenuItem_closeAllFolders.text=Alle Ordner schlie\u00dfen +treeMouseAdapter.popupMenuItem_closeAllFolders.tooltipText=Alle Ordner schlie\u00dfen +treeMouseAdapter.popupMenuItem_expandAllFolders.text=Alle Ordner \u00f6ffnen +treeMouseAdapter.popupMenuItem_expandAllFolders.tooltipText=Alle Ordner \u00f6ffnen +treeMouseAdapter.popupMenuItem_sortAllItems.text=Sortieren +treeMouseAdapter.popupMenuItem_sortAllItems.tooltipText=Sortieren diff --git a/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/layers.properties b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/layers.properties new file mode 100644 index 0000000..4c37189 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/layers.properties @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +abstractLayerGroupsTableModel.column_id.headerText=ID +abstractLayerGroupsTableModel.column_name.headerText=Name +abstractLayerGroupsTableModel.column_visible.headerText=Visible diff --git a/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/layers_de.properties b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/layers_de.properties new file mode 100644 index 0000000..d94f879 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/layers_de.properties @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +abstractLayerGroupsTableModel.column_id.headerText=ID +abstractLayerGroupsTableModel.column_name.headerText=Name +abstractLayerGroupsTableModel.column_visible.headerText=Sichtbar diff --git a/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/modelView.properties b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/modelView.properties new file mode 100644 index 0000000..a95c817 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/modelView.properties @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +drawingViewPlacardPanel.button_toggleBlocks.tooltipText=Show blocks +drawingViewPlacardPanel.button_toggleGrid.tooltipText=Show grid +drawingViewPlacardPanel.button_toggleLabels.tooltipText=Show labels +drawingViewPlacardPanel.button_toggleRulers.tooltipText=Show rulers +drawingViewPlacardPanel.button_zoomViewToWindow.tooltipText=Zoom view to window +selectSameAction.name=Select same diff --git a/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/modelView_de.properties b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/modelView_de.properties new file mode 100644 index 0000000..93dd7b7 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/modelView_de.properties @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +drawingViewPlacardPanel.button_toggleBlocks.tooltipText=Blockbereiche anzeigen +drawingViewPlacardPanel.button_toggleGrid.tooltipText=Gitternetz Ein/Aus +drawingViewPlacardPanel.button_toggleLabels.tooltipText=Beschriftungen anzeigen +drawingViewPlacardPanel.button_toggleRulers.tooltipText=Lineale anzeigen +selectSameAction.name=Gleichartige Ausw\u00e4hlen diff --git a/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/propertyEditing.properties b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/propertyEditing.properties new file mode 100644 index 0000000..9981940 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/propertyEditing.properties @@ -0,0 +1,110 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +boundingBoxPropertyEditorPanel.label_height=Height: +boundingBoxPropertyEditorPanel.label_length=Length: +boundingBoxPropertyEditorPanel.label_referenceOffsetX=X: +boundingBoxPropertyEditorPanel.label_referenceOffsetY=Y: +boundingBoxPropertyEditorPanel.label_width=Width: +boundingBoxPropertyEditorPanel.optionPane_numberFormatError.message=Invalid number format +boundingBoxPropertyEditorPanel.optionPane_numberFormatError.title=Please enter only valid integer numbers. +boundingBoxPropertyEditorPanel.panel_dimensions.border.title=Dimensions (in mm) +boundingBoxPropertyEditorPanel.panel_referenceOffset.border.title=Reference offset (in mm) +boundingBoxPropertyEditorPanel.title=Bounding box editor +colorPropertyCellEditor.dialog_colorSelection.title=Choose color +energyLevelThresholdSetPropertyEditorPanel.title=Energy level threshold set editor +energyLevelThresholdSetPropertyEditorPanel.label_energyLevelGood.text=Energy good at: +energyLevelThresholdSetPropertyEditorPanel.label_energyLevelCritical.text=Energy critical at: +energyLevelThresholdSetPropertyEditorPanel.label_energyLevelSufficientlyRecharged.text=Energy sufficiently recharged at: +energyLevelThresholdSetPropertyEditorPanel.label_energyLevelFullyRecharged.text=Energy fully recharged at: +energyLevelThresholdSetPropertyEditorPanel.optionPane_fullyRechargedSmallerSufficientlyRecharged.message="Energy fully recharged at" must be >= "Energy sufficiently recharged at". +energyLevelThresholdSetPropertyEditorPanel.optionPane_fullyRechargedSmallerSufficientlyRecharged.title=Implausible input +energyLevelThresholdSetPropertyEditorPanel.optionPane_goodSmallerCritical.message="Energy good at" must be >= "Energy critical at". +energyLevelThresholdSetPropertyEditorPanel.optionPane_goodSmallerCritical.title=Implausible input +energyLevelThresholdSetPropertyEditorPanel.optionPane_numberFormatError.message=Please enter only valid integer numbers. +energyLevelThresholdSetPropertyEditorPanel.optionPane_numberFormatError.title=Invalid number format +energyLevelThresholdSetPropertyEditorPanel.optionPane_thresholdsNotInRange.message=Please enter only values in [0..100]. +energyLevelThresholdSetPropertyEditorPanel.optionPane_thresholdsNotInRange.title=Threshold not within valid range +energyLevelThresholdSetPropertyEditorPanel.panel_thresholds.border.title=Energy level thresholds (in %) +envelopePanel.button_add.text=Add +envelopePanel.button_down.text=Down +envelopePanel.button_remove.text=Remove +envelopePanel.button_up.text=Up +envelopePanel.label_envelopeCoordinates.text=Envelope coordinates: +envelopePanel.label_envelopeKey.text=Envelope key (drop down for keys used in plant model): +envelopePanel.label_envelopeValidation.text=Envelope validation: +envelopePanel.optionPane_invalidNumberError.message=Please enter a valid integer number. +envelopePanel.table_couples.column_x.headerText=X +envelopePanel.table_couples.column_y.headerText=Y +envelopePanel.textArea_validation.text.envelopeKeyAlreadyDefinedError=The envelope key is already defined for this model element. +envelopePanel.textArea_validation.text.envelopeValid=The envelope is valid. +envelopePanel.textArea_validation.text.firstAndLastCoordianteNotEqualError=The first and last coordinate must be equal. +envelopePanel.textArea_validation.text.lessThanFourCoordinatesError=Please enter at least four coordinates. +envelopePanel.title=Edit envelope +envelopesPropertyEditorPanel.button_add.text=Add +envelopesPropertyEditorPanel.button_edit.text=Edit +envelopesPropertyEditorPanel.button_remove.text=Remove +envelopesPropertyEditorPanel.table_envelopes.column_coordinates.headerText=Coordinates +envelopesPropertyEditorPanel.table_envelopes.column_key.headerText=Key +envelopesPropertyEditorPanel.title=Envelopes +keyValuePropertyEditorPanel.label_key.text=Key: +keyValuePropertyEditorPanel.label_value.text=Value: +keyValuePropertyEditorPanel.title=Attribute editor +keyValueSetPropertyEditorPanel.button_add.text=Add +keyValueSetPropertyEditorPanel.button_edit.text=Edit +keyValueSetPropertyEditorPanel.button_remove.text=Remove +keyValueSetPropertyEditorPanel.optionPane_keyAlreadyExists.message=A property with this key already exists +keyValueSetPropertyEditorPanel.title=Edit key-value pairs +keyValueSetPropertyViewerEditorPanel.table_properties.column_key.headerText=Key +keyValueSetPropertyViewerEditorPanel.table_properties.column_value.headerText=Value +keyValueSetPropertyViewerEditorPanel.title=View key-value pairs +linkActionsEditorPanel.dialog_actionSelection.label_action.text=Action +linkActionsEditorPanel.dialog_actionSelectionAdd.title=Add action +linkActionsEditorPanel.dialog_actionSelectionEdit.title=Edit action +linkActionsEditorPanel.title=Actions +locationTypeActionsEditorPanel.dialog_actionDefinition.label_action.text=Action +locationTypeActionsEditorPanel.dialog_actionDefinitionAdd.title=Add action +locationTypeActionsEditorPanel.dialog_actionDefinitionEdit.title=Edit action +locationTypeActionsEditorPanel.title=Actions +orderTypesPropertyEditorPanel.button_add.text=Add +orderTypesPropertyEditorPanel.button_remove.text=Remove +orderTypesPropertyEditorPanel.optionPane_typeAlreadyPresentError.message=Type already in the list. +orderTypesPropertyEditorPanel.title=Allowed order types +peripheralOperationPanel.label_location.text=Location: +peripheralOperationPanel.label_operation.text=Operation: +peripheralOperationPanel.label_requireComplete.text=Completion required: +peripheralOperationPanel.label_trigger.text=Execution trigger: +peripheralOperationPanel.title=Peripheral operation editor +peripheralOperationsPropertyEditorPanel.button_add.text=Add +peripheralOperationsPropertyEditorPanel.button_edit.text=Edit +peripheralOperationsPropertyEditorPanel.button_remove.text=Remove +peripheralOperationsPropertyEditorPanel.button_up.text=Up +peripheralOperationsPropertyEditorPanel.table_resources.column_completion.headerText=Completion required +peripheralOperationsPropertyEditorPanel.table_resources.column_location.headerText=Location +peripheralOperationsPropertyEditorPanel.table_resources.column_operation.headerText=Operation +peripheralOperationsPropertyEditorPanel.table_resources.column_trigger.headerText=Trigger +peripheralOperationsPropertyEditorPanel.title=Peripheral operations +peripheralOperationsPropertyEditorPanell.button_down.text=Down +propertiesTableContent.column_attribute.headerText=Attribute +propertiesTableContent.column_value.headerText=Value +quantityCellEditor.dialog_error.title=No valid value +quantityCellEditor.dialog_errorFormat.message=Please insert a value of the form '<value> <unit>' (e.g. '10.0 cm'). +quantityCellEditor.dialog_errorNumber.message=Please insert a number followed by a unit (e.g. '10.0 cm'). +quantityCellEditor.dialog_errorRange.message=The value entered is outside the valid range for this property.\nThe valid range is: {0} - {1} +quantityCellEditor.dialog_errorScale.message=Please insert a value other than 0.0. +quantityCellEditor.dialog_errorUnit.message=The unit entered is not a valid unit for this property.\nValid units are: {0} +quantityEditorPanel.title=Quantity editor +resourcePropertyViewerEditorPanel.table_resources.column_location.headerText=Location +resourcePropertyViewerEditorPanel.table_resources.column_path.headerText=Path +resourcePropertyViewerEditorPanel.table_resources.column_point.headerText=Point +resourcePropertyViewerEditorPanel.title=View resources +selectionPropertyEditorPanel.label_value.text=Value +selectionPropertyEditorPanel.title=Selection editor +stringPropertyEditorPanel.title=String editor +stringSetPropertyEditorPanel.button_add.text=Add +stringSetPropertyEditorPanel.button_down.text=Down +stringSetPropertyEditorPanel.button_edit.text=Edit +stringSetPropertyEditorPanel.button_remove.text=Remove +stringSetPropertyEditorPanel.button_up.text=Up +symbolPropertyEditorPanel.button_removeSymbol.text=Remove symbol +symbolPropertyEditorPanel.title=Select a symbol diff --git a/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/propertyEditing_de.properties b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/propertyEditing_de.properties new file mode 100644 index 0000000..b31679e --- /dev/null +++ b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/panels/propertyEditing_de.properties @@ -0,0 +1,110 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +boundingBoxPropertyEditorPanel.label_height=H\u00f6he: +boundingBoxPropertyEditorPanel.label_length=L\u00e4nge: +boundingBoxPropertyEditorPanel.label_referenceOffsetX=X: +boundingBoxPropertyEditorPanel.label_referenceOffsetY=Y: +boundingBoxPropertyEditorPanel.label_width=Breite: +boundingBoxPropertyEditorPanel.optionPane_numberFormatError.message=Ung\u00fcltiges Zahlenformat +boundingBoxPropertyEditorPanel.optionPane_numberFormatError.title=Bitte geben Sie ausschlie\u00dflich g\u00fcltige ganzzahlige Werte ein. +boundingBoxPropertyEditorPanel.panel_dimensions.border.title=Dimensionen (in mm) +boundingBoxPropertyEditorPanel.panel_referenceOffset.border.title=Referenz-Offset (in mm) +boundingBoxPropertyEditorPanel.title=Bounding-Box-Editor +colorPropertyCellEditor.dialog_colorSelection.title=Bitte Farbe w\u00e4hlen +energyLevelThresholdSetPropertyEditorPanel.title=Energielevelschwellwerte-Editor +energyLevelThresholdSetPropertyEditorPanel.label_energyLevelGood.text=Energie gut bei: +energyLevelThresholdSetPropertyEditorPanel.label_energyLevelCritical.text=Energie kritisch bei: +energyLevelThresholdSetPropertyEditorPanel.label_energyLevelSufficientlyRecharged.text=Energie ausreichend geladen bei: +energyLevelThresholdSetPropertyEditorPanel.label_energyLevelFullyRecharged.text=Energie vollst\u00e4ndig geladen bei: +energyLevelThresholdSetPropertyEditorPanel.optionPane_fullyRechargedSmallerSufficientlyRecharged.message="Energie vollst\u00e4ndig geladen bei" muss >= "Energie ausreichend geladen bei" sein. +energyLevelThresholdSetPropertyEditorPanel.optionPane_fullyRechargedSmallerSufficientlyRecharged.title=Implausible input +energyLevelThresholdSetPropertyEditorPanel.optionPane_goodSmallerCritical.message="Energie gut bei" muss >= "Energie kritisch bei" sein. +energyLevelThresholdSetPropertyEditorPanel.optionPane_goodSmallerCritical.title=Unplausible Eingabe +energyLevelThresholdSetPropertyEditorPanel.optionPane_numberFormatError.message=Bitte geben Sie ausschlie\u00dflich g\u00fcltige ganzzahlige Werte ein. +energyLevelThresholdSetPropertyEditorPanel.optionPane_numberFormatError.title=Ung\u00fcltiges Zahlenformat +energyLevelThresholdSetPropertyEditorPanel.optionPane_thresholdsNotInRange.message=Bitte geben sie nur Werte im Bereich [0..100] ein. +energyLevelThresholdSetPropertyEditorPanel.optionPane_thresholdsNotInRange.title=Schwellenwert nicht im g\u00fcltigen Bereich +energyLevelThresholdSetPropertyEditorPanel.panel_thresholds.border.title=Energielevelschwellwerte (in %) +envelopePanel.button_add.text=Hinzuf\u00fcgen +envelopePanel.button_down.text=Nach unten +envelopePanel.button_remove.text=Entfernen +envelopePanel.button_up.text=Nach oben +envelopePanel.label_envelopeCoordinates.text=H\u00fcllkurvenkoordinaten: +envelopePanel.label_envelopeKey.text=H\u00fcllkurvenschl\u00fcssel (Aufklappen zeigt Schl\u00fcssel im Anlagenmodell): +envelopePanel.label_envelopeValidation.text=H\u00fcllkurvenvalidierung: +envelopePanel.optionPane_invalidNumberError.message=Bitte geben Sie eine g\u00fcltige ganze Zahl ein. +envelopePanel.table_couples.column_x.headerText=X +envelopePanel.table_couples.column_y.headerText=Y +envelopePanel.textArea_validation.text.envelopeKeyAlreadyDefinedError=Der H\u00fcllkurvenschl\u00fcssel ist f\u00fcr dieses Modellelement bereits definiert. +envelopePanel.textArea_validation.text.envelopeValid=Die H\u00fcllkurve ist g\u00fcltig. +envelopePanel.textArea_validation.text.firstAndLastCoordianteNotEqualError=Die erste und die letzte Koordinate m\u00fcssen gleich sein. +envelopePanel.textArea_validation.text.lessThanFourCoordinatesError=Bitte geben Sie mindestens vier Koordinaten ein. +envelopePanel.title=H\u00fcllkurve bearbeiten +envelopesPropertyEditorPanel.button_add.text=Hinzuf\u00fcgen +envelopesPropertyEditorPanel.button_edit.text=Bearbeiten +envelopesPropertyEditorPanel.button_remove.text=Entfernen +envelopesPropertyEditorPanel.table_envelopes.column_coordinates.headerText=Koordinaten +envelopesPropertyEditorPanel.table_envelopes.column_key.headerText=Schl\u00fcssel +envelopesPropertyEditorPanel.title=H\u00fcllkurven +keyValuePropertyEditorPanel.label_key.text=Schl\u00fcssel: +keyValuePropertyEditorPanel.label_value.text=Wert: +keyValuePropertyEditorPanel.title=Attribut-Editor +keyValueSetPropertyEditorPanel.button_add.text=Hinzuf\u00fcgen +keyValueSetPropertyEditorPanel.button_edit.text=Bearbeiten +keyValueSetPropertyEditorPanel.button_remove.text=Entfernen +keyValueSetPropertyEditorPanel.optionPane_keyAlreadyExists.message=Ein Property mit diesem Schl\u00fcssel existiert bereits +keyValueSetPropertyEditorPanel.title=Schl\u00fcssel-Wert-Paare bearbeiten +keyValueSetPropertyViewerEditorPanel.table_properties.column_key.headerText=Schl\u00fcssel +keyValueSetPropertyViewerEditorPanel.table_properties.column_value.headerText=Wert +keyValueSetPropertyViewerEditorPanel.title=Schl\u00fcssel-Wert-Paare ansehen +linkActionsEditorPanel.dialog_actionSelection.label_action.text=Aktion +linkActionsEditorPanel.dialog_actionSelectionAdd.title=Aktion hinzuf\u00fcgen +linkActionsEditorPanel.dialog_actionSelectionEdit.title=Aktion bearbeiten +linkActionsEditorPanel.title=Aktionen +locationTypeActionsEditorPanel.dialog_actionDefinition.label_action.text=Aktionen +locationTypeActionsEditorPanel.dialog_actionDefinitionAdd.title=Aktion hinzuf\u00fcgen +locationTypeActionsEditorPanel.dialog_actionDefinitionEdit.title=Aktion bearbeiten +locationTypeActionsEditorPanel.title=Aktionen +orderTypesPropertyEditorPanel.button_add.text=Hinzuf\u00fcgen +orderTypesPropertyEditorPanel.button_remove.text=Entfernen +orderTypesPropertyEditorPanel.optionPane_typeAlreadyPresentError.message=Typ bereits in der Liste enthalten. +orderTypesPropertyEditorPanel.title=Erlaubte Auftragstypen +peripheralOperationPanel.label_location.text=Station: +peripheralOperationPanel.label_operation.text=Operation: +peripheralOperationPanel.label_requireComplete.text=Fertigstellung erforderlich: +peripheralOperationPanel.label_trigger.text=Ausf\u00fchrungsausl\u00f6ser: +peripheralOperationPanel.title=Peripheral operation editor +peripheralOperationsPropertyEditorPanel.button_add.text=Hinzuf\u00fcgen +peripheralOperationsPropertyEditorPanel.button_edit.text=Bearbeiten +peripheralOperationsPropertyEditorPanel.button_remove.text=Entfernen +peripheralOperationsPropertyEditorPanel.button_up.text=Nach oben +peripheralOperationsPropertyEditorPanel.table_resources.column_completion.headerText=Fertigstellung erforderlich +peripheralOperationsPropertyEditorPanel.table_resources.column_location.headerText=Station +peripheralOperationsPropertyEditorPanel.table_resources.column_operation.headerText=Operation +peripheralOperationsPropertyEditorPanel.table_resources.column_trigger.headerText=Ausl\u00f6ser +peripheralOperationsPropertyEditorPanel.title=Peripherieoperationen +peripheralOperationsPropertyEditorPanell.button_down.text=Nach unten +propertiesTableContent.column_attribute.headerText=Attribut +propertiesTableContent.column_value.headerText=Wert +quantityCellEditor.dialog_error.title=Kein g\u00fcltiger Wert +quantityCellEditor.dialog_errorFormat.message=Bitte geben Sie einen g\u00fcltigen Wert in der Form '<Wert> <Einheit>' an (z.B. '10.0 cm'). +quantityCellEditor.dialog_errorNumber.message=Bitte geben sie eine Zahl gefolgt von einer Einheit an (z.b. '10.0 mm'). +quantityCellEditor.dialog_errorRange.message=Der eingegebene Wert liegt au\u00dferhalb des g\u00fcltigen Bereichs f\u00fcr diese Eigenschaft.\nDer g\u00fcltige Bereich ist: {0} - {1} +quantityCellEditor.dialog_errorScale.message=Bitte geben Sie einen anderen Wert als 0.0 ein. +quantityCellEditor.dialog_errorUnit.message=Die eingegebene Einheit ist keine g\u00fcltige Einheit f\u00fcr diese Eigenschaft.\nG\u00fcltige Einheiten sind: {0} +quantityEditorPanel.title=Gr\u00f6\u00dfen-Editor +resourcePropertyViewerEditorPanel.table_resources.column_location.headerText=Station +resourcePropertyViewerEditorPanel.table_resources.column_path.headerText=Pfad +resourcePropertyViewerEditorPanel.table_resources.column_point.headerText=Punkt +resourcePropertyViewerEditorPanel.title=Ressourcen ansehen +selectionPropertyEditorPanel.label_value.text=Wert +selectionPropertyEditorPanel.title=Wert ausw\u00e4hlen +stringPropertyEditorPanel.title=String-Editor +stringSetPropertyEditorPanel.button_add.text=Hinzuf\u00fcgen +stringSetPropertyEditorPanel.button_down.text=Nach unten +stringSetPropertyEditorPanel.button_edit.text=Bearbeiten +stringSetPropertyEditorPanel.button_remove.text=Entfernen +stringSetPropertyEditorPanel.button_up.text=Nach oben +symbolPropertyEditorPanel.button_removeSymbol.text=Symbol entfernen +symbolPropertyEditorPanel.title=Symbol ausw\u00e4hlen diff --git a/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/system.properties b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/system.properties new file mode 100644 index 0000000..f82355e --- /dev/null +++ b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/system.properties @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +closableDialog.button_close.text=Close +splashFrame.label_message.text=Starting openTCS... +splashFrame.title.text=Plant Overview +standardContentDialog.button_apply.text=Apply +standardContentDialog.button_cancel.text=Cancel +standardContentDialog.button_close.text=Close +standardContentDialog.button_ok.text=Ok +standardDetailsDialog.button_cancel.text=Cancel +standardDetailsDialog.button_ok.text=Ok +standardDialog.button_cancel.text=Cancel +standardDialog.button_ok.text=Ok +unifiedModelConstants.dialogFileFilter.description=Kernel model files (*.{0}) diff --git a/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/system_de.properties b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/system_de.properties new file mode 100644 index 0000000..5d798c2 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/system_de.properties @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +closableDialog.button_close.text=Schlie\u00dfen +splashFrame.label_message.text=Starte openTCS... +splashFrame.title.text=Anlagen\u00fcbersicht +standardContentDialog.button_apply.text=\u00dcbernehmen +standardContentDialog.button_cancel.text=Abbrechen +standardContentDialog.button_close.text=Schlie\u00dfen +standardDetailsDialog.button_cancel.text=Abbrechen +standardDialog.button_cancel.text=Abbrechen +unifiedModelConstants.dialogFileFilter.description=Modelldateien des Kernels (*.{0}) diff --git a/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/toolbar.properties b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/toolbar.properties new file mode 100644 index 0000000..588be78 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/toolbar.properties @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +moveAction.east.shortDescription=Move right +moveAction.north.shortDescription=Move up +moveAction.south.shortDescription=Move down +moveAction.west.shortDescription=Move left diff --git a/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/toolbar_de.properties b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/toolbar_de.properties new file mode 100644 index 0000000..35e6427 --- /dev/null +++ b/opentcs-plantoverview-common/src/main/resources/i18n/org/opentcs/plantoverview/toolbar_de.properties @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +moveAction.east.shortDescription=Nach rechts verschieben +moveAction.north.shortDescription=Nach oben verschieben +moveAction.south.shortDescription=Nach unten verschieben +moveAction.west.shortDescription=Nach links verschieben diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/comment-add.16.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/comment-add.16.png new file mode 100644 index 0000000..298cfb5 Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/comment-add.16.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/document-save-as.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/document-save-as.png new file mode 100644 index 0000000..a487a32 Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/document-save-as.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/document-save.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/document-save.png new file mode 100644 index 0000000..8c8f2f6 Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/document-save.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-delete-2.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-delete-2.png new file mode 100644 index 0000000..aa6b89a Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-delete-2.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-redo.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-redo.png new file mode 100644 index 0000000..f470193 Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-redo.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-select-all.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-select-all.png new file mode 100644 index 0000000..85bd2f0 Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-select-all.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-undo.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-undo.png new file mode 100644 index 0000000..b7659bf Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/edit-undo.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/kcharselect.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/kcharselect.png new file mode 100644 index 0000000..115867a Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/kcharselect.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/view-split.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/view-split.png new file mode 100644 index 0000000..444dc4d Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/view-split.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/zoom-fit-best-4.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/zoom-fit-best-4.png new file mode 100644 index 0000000..c757be0 Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/menu/zoom-fit-best-4.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/openTCS/splash.320x152.gif b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/openTCS/splash.320x152.gif new file mode 100644 index 0000000..9e6f131 Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/openTCS/splash.320x152.gif differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/panel/back.24x24.gif b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/panel/back.24x24.gif new file mode 100644 index 0000000..787518c Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/panel/back.24x24.gif differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/panel/forward.24x24.gif b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/panel/forward.24x24.gif new file mode 100644 index 0000000..1936fd4 Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/panel/forward.24x24.gif differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/border.jpg b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/border.jpg new file mode 100644 index 0000000..f742f27 Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/border.jpg differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/document-page-setup.16x16.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/document-page-setup.16x16.png new file mode 100644 index 0000000..bf35c3d Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/document-page-setup.16x16.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/draw-arrow-back.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/draw-arrow-back.png new file mode 100644 index 0000000..1fb4afe Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/draw-arrow-back.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/draw-arrow-down.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/draw-arrow-down.png new file mode 100644 index 0000000..8f5e54c Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/draw-arrow-down.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/draw-arrow-forward.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/draw-arrow-forward.png new file mode 100644 index 0000000..a8ff1eb Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/draw-arrow-forward.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/draw-arrow-up.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/draw-arrow-up.png new file mode 100644 index 0000000..fe44019 Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/draw-arrow-up.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-delete-2.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-delete-2.png new file mode 100644 index 0000000..d27b8ad Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-delete-2.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-redo.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-redo.png new file mode 100644 index 0000000..008a016 Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-redo.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-select-all.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-select-all.png new file mode 100644 index 0000000..888ba2c Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-select-all.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-undo.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-undo.png new file mode 100644 index 0000000..2f221ff Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/toolbar/edit-undo.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/block.18x18.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/block.18x18.png new file mode 100644 index 0000000..2eae5ef Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/block.18x18.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/figure.18x18.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/figure.18x18.png new file mode 100644 index 0000000..3ef2f71 Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/figure.18x18.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/link.18x18.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/link.18x18.png new file mode 100644 index 0000000..cd4eb99 Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/link.18x18.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/location.18x18.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/location.18x18.png new file mode 100644 index 0000000..a17ddb3 Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/location.18x18.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/locationType.18x18.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/locationType.18x18.png new file mode 100644 index 0000000..5db64c6 Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/locationType.18x18.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/path.18x18.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/path.18x18.png new file mode 100644 index 0000000..b6a1360 Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/path.18x18.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/point.18x18.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/point.18x18.png new file mode 100644 index 0000000..1900fd2 Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/point.18x18.png differ diff --git a/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/vehicle.18x18.png b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/vehicle.18x18.png new file mode 100644 index 0000000..572dc7d Binary files /dev/null and b/opentcs-plantoverview-common/src/main/resources/org/opentcs/guing/res/symbols/tree/vehicle.18x18.png differ diff --git a/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/components/drawing/figures/ToolTipTextGeneratorTest.java b/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/components/drawing/figures/ToolTipTextGeneratorTest.java new file mode 100644 index 0000000..80ca406 --- /dev/null +++ b/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/components/drawing/figures/ToolTipTextGeneratorTest.java @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.drawing.figures; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThan; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.guing.base.components.properties.type.KeyValueProperty; +import org.opentcs.guing.base.model.elements.PointModel; +import org.opentcs.guing.common.model.SystemModel; +import org.opentcs.guing.common.persistence.ModelManager; + +/** + * Tests the {@link ToolTipTextGenerator}. + */ +class ToolTipTextGeneratorTest { + + private ModelManager modelManager; + + private ToolTipTextGenerator toolTipTextGenerator; + + @BeforeEach + void setUp() { + SystemModel systemModel = mock(SystemModel.class); + when(systemModel.getBlockModels()).thenReturn(new ArrayList<>()); + + modelManager = mock(ModelManager.class); + when(modelManager.getModel()).thenReturn(systemModel); + toolTipTextGenerator = new ToolTipTextGenerator(modelManager); + } + + @Test + void sortsPropertiesLexicographically() { + final String propKey1 = "prop1"; + final String propKey2 = "prop2"; + final String propKey3 = "prop3"; + final String propkey4 = "prop4"; + + PointModel pointModel = new PointModel(); + pointModel.getPropertyMiscellaneous().addItem( + new KeyValueProperty( + pointModel, + propkey4, + "some-value" + ) + ); + pointModel.getPropertyMiscellaneous().addItem( + new KeyValueProperty( + pointModel, + propKey2, + "some-value" + ) + ); + pointModel.getPropertyMiscellaneous().addItem( + new KeyValueProperty( + pointModel, + propKey3, + "some-value" + ) + ); + pointModel.getPropertyMiscellaneous().addItem( + new KeyValueProperty( + pointModel, + propKey1, + "some-value" + ) + ); + + String toolTipText = toolTipTextGenerator.getToolTipText(pointModel); + + assertThat(toolTipText.indexOf(propKey1), is(lessThan(toolTipText.indexOf(propKey2)))); + assertThat(toolTipText.indexOf(propKey2), is(lessThan(toolTipText.indexOf(propKey3)))); + assertThat(toolTipText.indexOf(propKey3), is(lessThan(toolTipText.indexOf(propkey4)))); + } +} diff --git a/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/components/properties/table/QuantityCellEditorTest.java b/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/components/properties/table/QuantityCellEditorTest.java new file mode 100644 index 0000000..adcb479 --- /dev/null +++ b/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/components/properties/table/QuantityCellEditorTest.java @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.table; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +import javax.swing.JTable; +import javax.swing.JTextField; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.guing.base.components.properties.type.LengthProperty; +import org.opentcs.guing.base.components.properties.type.LengthProperty.Unit; +import org.opentcs.guing.base.model.ModelComponent; +import org.opentcs.guing.common.util.UserMessageHelper; + +/** + * A test for the {@link QuantityCellEditor}. + */ +class QuantityCellEditorTest { + + private JTextField textField; + private QuantityCellEditor quantityCellEditor; + private LengthProperty lp; + + QuantityCellEditorTest() { + } + + @BeforeEach + void setUp() { + textField = new JTextField(); + lp = new LengthProperty(mock(ModelComponent.class), 10, LengthProperty.Unit.CM); + quantityCellEditor = new QuantityCellEditor(textField, mock(UserMessageHelper.class)); + quantityCellEditor.getTableCellEditorComponent(mock(JTable.class), lp, true, 0, 0); + } + + @Test + void testPropertyTextFieldContent() { + assertEquals(textField.getText(), "10.0 cm"); + } + + @Test + void allowValueInRange() { + textField.setText("100 cm"); + quantityCellEditor.getCellEditorValue(); + assertEquals(100.0, lp.getValue()); + assertEquals(Unit.CM, lp.getUnit()); + } + + @Test + void disallowValueOutOfRange() { + // Value out of range, changes mustn't be saved to the property + textField.setText("-100 cm"); + quantityCellEditor.getCellEditorValue(); + assertEquals(10.0, lp.getValue()); + assertEquals(Unit.CM, lp.getUnit()); + } + + @Test + void allowKnownUnit() { + textField.setText("100 mm"); + quantityCellEditor.getCellEditorValue(); + assertEquals(100.0, lp.getValue()); + assertEquals(Unit.MM, lp.getUnit()); + } + + @Test + void disallowUnknownUnit() { + // Unknown unit, changes mustn't be saved to the property + textField.setText("100 liter"); + quantityCellEditor.getCellEditorValue(); + assertEquals(10.0, lp.getValue()); + assertEquals(Unit.CM, lp.getUnit()); + } + + @Test + void disallowWrongFormat() { + // Strings without a blank index not allowed, changes mustn't be saved to the property + textField.setText("100cm"); + quantityCellEditor.getCellEditorValue(); + assertEquals(10.0, lp.getValue()); + assertEquals(Unit.CM, lp.getUnit()); + } + + @Test + void disallowEmptyInputString() { + // Empty string not allowed, changes mustn't be saved to the property + textField.setText(""); + quantityCellEditor.getCellEditorValue(); + assertEquals(10.0, lp.getValue()); + assertEquals(Unit.CM, lp.getUnit()); + } + + @Test + void disallowCharactersInValue() { + // Values mixed with text not allowed, changes musnt be saved to the property + textField.setText("55asd.5 cm"); + quantityCellEditor.getCellEditorValue(); + assertEquals(10.0, lp.getValue()); + assertEquals(Unit.CM, lp.getUnit()); + } +} diff --git a/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/components/properties/type/MergedPropertySuggestionsTest.java b/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/components/properties/type/MergedPropertySuggestionsTest.java new file mode 100644 index 0000000..66cf42f --- /dev/null +++ b/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/components/properties/type/MergedPropertySuggestionsTest.java @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.components.properties.type; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.common.collect.Sets; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.plantoverview.PropertySuggestions; + +/** + */ +class MergedPropertySuggestionsTest { + + private PropertySuggestions instance1; + private PropertySuggestions instance2; + private PropertySuggestions emptyInstance; + + @BeforeEach + void setUp() { + emptyInstance = new PropertySuggestionsImpl(); + + instance1 = new PropertySuggestionsImpl(); + instance1.getKeySuggestions().add("key1"); + instance1.getKeySuggestions().add("key2"); + instance1.getKeySuggestions().add("key3"); + instance1.getKeySuggestions().add("doubleKeyTest"); + instance1.getValueSuggestions().add("value1"); + instance1.getValueSuggestions().add("value2"); + instance1.getValueSuggestions().add("value3"); + instance1.getValueSuggestions().add("doubleValueTest"); + + instance2 = new PropertySuggestionsImpl(); + instance2.getKeySuggestions().add("doubleKeyTest"); + instance2.getKeySuggestions().add("key4"); + instance2.getKeySuggestions().add("key5"); + instance2.getKeySuggestions().add("key6"); + instance2.getValueSuggestions().add("doubleValueTest"); + instance2.getValueSuggestions().add("value1"); + instance2.getValueSuggestions().add("value2"); + instance2.getValueSuggestions().add("value3"); + } + + @Test + void shouldRemainEmptyForNoSuggestions() { + MergedPropertySuggestions mergedSuggestions = new MergedPropertySuggestions(new HashSet<>()); + assertTrue(mergedSuggestions.getKeySuggestions().isEmpty()); + assertTrue(mergedSuggestions.getValueSuggestions().isEmpty()); + } + + @Test + void shouldRemainEmptyForEmptySuggestions() { + Set<PropertySuggestions> sugSet = new HashSet<>(Arrays.asList(emptyInstance)); + MergedPropertySuggestions mergedSuggestions = new MergedPropertySuggestions(sugSet); + assertTrue(mergedSuggestions.getKeySuggestions().isEmpty()); + assertTrue(mergedSuggestions.getValueSuggestions().isEmpty()); + } + + @Test + void shouldMergeSuggestions() { + Set<PropertySuggestions> sugSet = new HashSet<>(Arrays.asList(instance1, instance2)); + MergedPropertySuggestions mergedSuggestions = new MergedPropertySuggestions(sugSet); + assertEquals( + Sets.union(instance1.getKeySuggestions(), instance2.getKeySuggestions()), + mergedSuggestions.getKeySuggestions() + ); + assertEquals( + Sets.union(instance1.getValueSuggestions(), instance2.getValueSuggestions()), + mergedSuggestions.getValueSuggestions() + ); + } + + private class PropertySuggestionsImpl + implements + PropertySuggestions { + + private final Set<String> keySuggestions = new HashSet<>(); + private final Set<String> valueSuggestions = new HashSet<>(); + + PropertySuggestionsImpl() { + } + + @Override + public Set<String> getKeySuggestions() { + return keySuggestions; + } + + @Override + public Set<String> getValueSuggestions() { + return valueSuggestions; + } + + } +} diff --git a/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/exchange/AllocationHistoryTest.java b/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/exchange/AllocationHistoryTest.java new file mode 100644 index 0000000..5cba628 --- /dev/null +++ b/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/exchange/AllocationHistoryTest.java @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.exchange; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; + +/** + * Tests for {@link AllocationHistory}. + */ +class AllocationHistoryTest { + + private AllocationHistory allocationHistory; + private Vehicle vehicle = new Vehicle("some-vehicle"); + private Point point1 = new Point("point1"); + private Point point2 = new Point("point2"); + private Point point3 = new Point("point3"); + private Path path1 = new Path("path1", point1.getReference(), point2.getReference()); + private Path path2 = new Path("path2", point2.getReference(), point3.getReference()); + + @BeforeEach + void setUp() { + allocationHistory = new AllocationHistory(); + } + + @Test + void returnEmptyEntryForVehicleClaimingAndAllocatingNoResources() { + AllocationHistory.Entry result = allocationHistory.updateHistory(vehicle); + + assertThat(result.getCurrentClaimedResources()).isEmpty(); + assertThat(result.getCurrentAllocatedResourcesAhead()).isEmpty(); + assertThat(result.getCurrentAllocatedResourcesBehind()).isEmpty(); + assertThat(result.getPreviouslyClaimedOrAllocatedResources()).isEmpty(); + } + + @Test + void returnEmptyPreviousAllocationsOnFirstUpdate() { + vehicle = vehicle + .withAllocatedResources(List.of(Set.of(point1.getReference()))) + .withClaimedResources(List.of(Set.of(path1.getReference(), point2.getReference()))); + + AllocationHistory.Entry result = allocationHistory.updateHistory(vehicle); + + assertThat(result.getCurrentAllocatedResourcesAhead()).isEmpty(); + assertThat(result.getCurrentAllocatedResourcesBehind()) + .hasSize(1) + .contains(point1.getReference()); + assertThat(result.getCurrentClaimedResources()) + .hasSize(2) + .contains(path1.getReference(), point2.getReference()); + assertThat(result.getPreviouslyClaimedOrAllocatedResources()).isEmpty(); + } + + @Test + void returnPreviousAllocationsOnConsecutiveUpdate() { + vehicle = vehicle + .withAllocatedResources(List.of(Set.of(point1.getReference()))) + .withClaimedResources(List.of(Set.of(path1.getReference(), point2.getReference()))); + + allocationHistory.updateHistory(vehicle); + + vehicle = vehicle + .withAllocatedResources(List.of(Set.of(path1.getReference(), point2.getReference()))) + .withClaimedResources(List.of(Set.of(path2.getReference(), point3.getReference()))); + + AllocationHistory.Entry result = allocationHistory.updateHistory(vehicle); + + assertThat(result.getCurrentAllocatedResourcesAhead()).isEmpty(); + assertThat(result.getCurrentAllocatedResourcesBehind()) + .hasSize(2) + .contains(path1.getReference(), point2.getReference()); + assertThat(result.getCurrentClaimedResources()) + .hasSize(2) + .contains(path2.getReference(), point3.getReference()); + assertThat(result.getPreviouslyClaimedOrAllocatedResources()) + .hasSize(1) + .contains(point1.getReference()); + } + + @Test + void returnCorrectlyDividedAllocationsAheadOrBehind() { + vehicle = vehicle + .withCurrentPosition(point2.getReference()) + .withNextPosition(point3.getReference()) + .withAllocatedResources( + List.of( + Set.of(point1.getReference()), + Set.of(point2.getReference(), path1.getReference()), + Set.of(point3.getReference(), path2.getReference()) + ) + ); + + allocationHistory.updateHistory(vehicle); + + AllocationHistory.Entry result = allocationHistory.updateHistory(vehicle); + + assertThat(result.getCurrentAllocatedResourcesBehind()) + .contains(path1.getReference(), point1.getReference(), point2.getReference()); + assertThat(result.getCurrentAllocatedResourcesAhead()) + .contains(path2.getReference(), point3.getReference()); + } +} diff --git a/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/exchange/ApplicationKernelProviderTest.java b/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/exchange/ApplicationKernelProviderTest.java new file mode 100644 index 0000000..30ccbe5 --- /dev/null +++ b/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/exchange/ApplicationKernelProviderTest.java @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.exchange; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.opentcs.access.KernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.common.PortalManager; +import org.opentcs.util.gui.dialog.ConnectToServerDialog; + +/** + */ +class ApplicationKernelProviderTest { + + /** + * A (mocked) portal manager. + */ + private PortalManager portalManager; + /** + * A (mocked) connection dialog. + */ + private ConnectToServerDialog dialog; + /** + * A (mocked) configuration. + */ + private ApplicationPortalProviderConfiguration appConfig; + /** + * The portal provider to be tested. + */ + private SharedKernelServicePortalProvider portalProvider; + + @BeforeEach + void setUp() { + portalManager = mock(PortalManager.class); + dialog = mock(ConnectToServerDialog.class); + appConfig = mock(ApplicationPortalProviderConfiguration.class); + portalProvider = new ApplicationPortalProvider( + portalManager, + appConfig + ); + } + + @Disabled + @Test + void shouldConnectOnClientRegistration() { + when(portalManager.isConnected()).thenReturn(false, false, true); + when(portalManager.getPortal()).thenReturn(mock(KernelServicePortal.class)); + + portalProvider.register(); + + verify(dialog).setVisible(true); + } + + @Disabled + @Test + void shouldNotConnectIfAlreadyConnected() { + when(portalManager.isConnected()).thenReturn(true); + when(portalManager.getPortal()).thenReturn(mock(KernelServicePortal.class)); + + portalProvider.register(); + + verify(dialog, never()).setVisible(anyBoolean()); + } +} diff --git a/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/exchange/SplitResourcesTest.java b/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/exchange/SplitResourcesTest.java new file mode 100644 index 0000000..26f26d9 --- /dev/null +++ b/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/exchange/SplitResourcesTest.java @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.exchange; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResourceReference; + +/** + * Tests for {@link SplitResources}. + */ +class SplitResourcesTest { + + private Point pointA; + private Point pointB; + private Point pointC; + private Point pointD; + + private Path pathAB; + private Path pathBC; + private Path pathCD; + + private List<Set<TCSResourceReference<?>>> allResources; + + @BeforeEach + void setUp() { + pointA = new Point("A"); + pointB = new Point("B"); + pointC = new Point("C"); + pointD = new Point("D"); + + pathAB = new Path("AB", pointA.getReference(), pointB.getReference()); + pathBC = new Path("BC", pointB.getReference(), pointC.getReference()); + pathCD = new Path("CD", pointC.getReference(), pointD.getReference()); + + allResources = List.of( + Set.of(pathAB.getReference(), pointB.getReference()), + Set.of(pathBC.getReference(), pointC.getReference()), + Set.of(pathCD.getReference(), pointD.getReference()) + ); + } + + @Test + void handleEmptyResourceSets() { + SplitResources result = SplitResources.from(List.of(), pointB.getReference()); + + assertThat(result, is(notNullValue())); + assertThat(result.getAllocatedResourcesBehind(), is(empty())); + assertThat(result.getAllocatedResourcesAhead(), is(empty())); + } + + @Test + void treatAllResourcesAsBehindForNullDelimiter() { + SplitResources result = SplitResources.from(allResources, null); + + assertThat(result, is(notNullValue())); + assertThat(result.getAllocatedResourcesBehind(), is(equalTo(allResources))); + assertThat(result.getAllocatedResourcesAhead(), is(empty())); + } + + @Test + void treatDelimiterAsBehind() { + SplitResources result = SplitResources.from(allResources, pointC.getReference()); + + assertThat(result, is(notNullValue())); + assertThat( + result.getAllocatedResourcesBehind(), is( + equalTo( + List.of( + Set.of(pathAB.getReference(), pointB.getReference()), + Set.of(pathBC.getReference(), pointC.getReference()) + ) + ) + ) + ); + assertThat( + result.getAllocatedResourcesAhead(), is( + equalTo( + List.of( + Set.of(pathCD.getReference(), pointD.getReference()) + ) + ) + ) + ); + } +} diff --git a/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/util/CompatibilityCheckerTest.java b/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/util/CompatibilityCheckerTest.java new file mode 100644 index 0000000..4cbc28c --- /dev/null +++ b/opentcs-plantoverview-common/src/test/java/org/opentcs/guing/common/util/CompatibilityCheckerTest.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.common.util; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +/** + */ +class CompatibilityCheckerTest { + + @Test + void acceptCompatibleVersion() { + // "21.0.3" is a string that is returned by Eclipse Temurin 21 and Oracle JDK 21 + assertThat(CompatibilityChecker.versionCompatibleWithDockingFrames("21.0.3"), is(true)); + } + + @Test + void refuseIncompatibleVersion() { + // "21" is a string that may be returned by some other Java distribution + assertThat(CompatibilityChecker.versionCompatibleWithDockingFrames("21"), is(false)); + } +} diff --git a/opentcs-plantoverview-panel-loadgenerator/build.gradle b/opentcs-plantoverview-panel-loadgenerator/build.gradle new file mode 100644 index 0000000..f6741e7 --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/build.gradle @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply from: "${rootDir}/gradle/java-project.gradle" +apply from: "${rootDir}/gradle/java-codequality.gradle" +apply from: "${rootDir}/gradle/guice-project.gradle" +apply from: "${rootDir}/gradle/publishing-java.gradle" + +dependencies { + api project(':opentcs-api-injection') + api project(':opentcs-common') +} + +task release { + dependsOn build +} diff --git a/opentcs-plantoverview-panel-loadgenerator/gradle.properties b/opentcs-plantoverview-panel-loadgenerator/gradle.properties new file mode 100644 index 0000000..14431ac --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/gradle.properties @@ -0,0 +1,40 @@ +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineMethodParams=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAfterDotInChainedMethodCalls=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineDisjunctiveCatchTypes=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineFor=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineImplements=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.sortMembersByVisibility=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.visibilityOrder=PUBLIC;PROTECTED;DEFAULT;PRIVATE +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.enable-indent=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineArrayInit=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineCallArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.keepGettersAndSettersTogether=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classMembersOrder=STATIC FIELD;FIELD;STATIC_INIT;CONSTRUCTOR;INSTANCE_INIT;STATIC METHOD;METHOD;STATIC CLASS;CLASS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapCommentText=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.importGroupsOrder=* +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize=4 +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineAnnotationArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineTryResources=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.preserveNewLinesInComments=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineParenthesized=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineThrows=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=100 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project diff --git a/opentcs-plantoverview-panel-loadgenerator/src/guiceConfig/java/org/opentcs/guing/plugins/panels/loadgenerator/LoadGeneratorPanelModule.java b/opentcs-plantoverview-panel-loadgenerator/src/guiceConfig/java/org/opentcs/guing/plugins/panels/loadgenerator/LoadGeneratorPanelModule.java new file mode 100644 index 0000000..ad15d05 --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/guiceConfig/java/org/opentcs/guing/plugins/panels/loadgenerator/LoadGeneratorPanelModule.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator; + +import org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Configures the load generator panel. + */ +public class LoadGeneratorPanelModule + extends + PlantOverviewInjectionModule { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(LoadGeneratorPanelModule.class); + + /** + * Creates a new instance. + */ + public LoadGeneratorPanelModule() { + } + + @Override + protected void configure() { + ContinuousLoadPanelConfiguration configuration + = getConfigBindingProvider().get( + ContinuousLoadPanelConfiguration.PREFIX, + ContinuousLoadPanelConfiguration.class + ); + + if (!configuration.enable()) { + LOG.info("Continuous load panel disabled by configuration."); + return; + } + + // tag::documentation_createPluginPanelModule[] + pluggablePanelFactoryBinder().addBinding().to(ContinuousLoadPanelFactory.class); + // end::documentation_createPluginPanelModule[] + } +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule b/opentcs-plantoverview-panel-loadgenerator/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule new file mode 100644 index 0000000..79b831a --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT + +org.opentcs.guing.plugins.panels.loadgenerator.LoadGeneratorPanelModule diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/ContinuousLoadPanel.form b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/ContinuousLoadPanel.form new file mode 100644 index 0000000..7abcdb4 --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/ContinuousLoadPanel.form @@ -0,0 +1,724 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.6" maxVersion="1.7" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <NonVisualComponents> + <Component class="javax.swing.ButtonGroup" name="orderSpecButtonGroup"> + </Component> + <Component class="javax.swing.ButtonGroup" name="triggerButtonGroup"> + </Component> + <Component class="javax.swing.JComboBox" name="operationTypesComboBox"> + <Properties> + <Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor"> + <StringArray count="0"/> + </Property> + <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.comboBox_operationTypes.tooltipText" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="<String>"/> + </AuxValues> + </Component> + <Component class="javax.swing.JComboBox" name="locationsComboBox"> + <Properties> + <Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor"> + <StringArray count="0"/> + </Property> + <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.comboBox_locations.tooltipText" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="itemStateChanged" listener="java.awt.event.ItemListener" parameters="java.awt.event.ItemEvent" handler="locationsComboBoxItemStateChanged"/> + </Events> + <AuxValues> + <AuxValue name="JavaCodeGenerator_CreateCodePost" type="java.lang.String" value="locationsComboBox.setRenderer(new LocationComboBoxRenderer());"/> + <AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="<TCSObjectReference<Location>>"/> + </AuxValues> + </Component> + <Component class="javax.swing.JFileChooser" name="fileChooser"> + <Properties> + <Property name="fileFilter" type="javax.swing.filechooser.FileFilter" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> + <Connection code="new FileNameExtensionFilter("*.xml", "xml")" type="code"/> + </Property> + </Properties> + </Component> + </NonVisualComponents> + <Properties> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[520, 700]"/> + </Property> + </Properties> + <AccessibilityProperties> + <Property name="AccessibleContext.accessibleName" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.accessibleName" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </AccessibilityProperties> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_formBundle" type="java.lang.String" value="org/opentcs/genericclient/panels/loadgenerator/Bundle"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,-8,0,0,2,44"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Container class="javax.swing.JPanel" name="triggerPanel"> + <Properties> + <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> + <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> + <TitledBorder title="Trigger for generating orders"> + <ResourceString PropertyName="titleX" bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.panel_generateTrigger.border.title" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </TitledBorder> + </Border> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="0" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="18" weightX="1.0" weightY="0.0"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Component class="javax.swing.JRadioButton" name="thresholdTriggerRadioButton"> + <Properties> + <Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor"> + <ComponentRef name="triggerButtonGroup"/> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.radioButton_triggerByOrderThreshold.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="1" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JSpinner" name="thresholdSpinner"> + <Properties> + <Property name="model" type="javax.swing.SpinnerModel" editor="org.netbeans.modules.form.editors2.SpinnerModelEditor"> + <SpinnerModel initial="10" maximum="100" minimum="0" numberType="java.lang.Integer" stepSize="1" type="number"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="1" gridY="1" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JLabel" name="thresholdOrdersLbl"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.label_unitOrdersToBeProcessed.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="2" gridY="1" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JLabel" name="fillingLbl"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="3" gridY="1" gridWidth="1" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="1.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JRadioButton" name="timerTriggerRadioButton"> + <Properties> + <Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor"> + <ComponentRef name="triggerButtonGroup"/> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.radioButton_triggerAfterTimeout.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="2" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JSpinner" name="timerSpinner"> + <Properties> + <Property name="model" type="javax.swing.SpinnerModel" editor="org.netbeans.modules.form.editors2.SpinnerModelEditor"> + <SpinnerModel initial="60" maximum="3600" minimum="1" numberType="java.lang.Integer" stepSize="1" type="number"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="1" gridY="2" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JLabel" name="timerSecondsLbl"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.label_unitSeconds.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="2" gridY="2" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JRadioButton" name="singleTriggerRadioButton"> + <Properties> + <Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor"> + <ComponentRef name="triggerButtonGroup"/> + </Property> + <Property name="selected" type="boolean" value="true"/> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.radioButton_triggerOnce.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="0" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> + </Container> + <Container class="javax.swing.JPanel" name="orderProfilePanel"> + <Properties> + <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> + <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> + <TitledBorder title="Order profile"> + <ResourceString PropertyName="titleX" bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.panel_orderProfile.border.title" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </TitledBorder> + </Border> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="1" gridWidth="1" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="18" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Container class="javax.swing.JPanel" name="randomOrderSpecPanel"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="-1" gridY="-1" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="1.0" weightY="0.0"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Component class="javax.swing.JRadioButton" name="randomOrderSpecButton"> + <Properties> + <Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor"> + <ComponentRef name="orderSpecButtonGroup"/> + </Property> + <Property name="selected" type="boolean" value="true"/> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.radioButton_createOrdersRandomly.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="randomOrderSpecButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="0" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JSpinner" name="randomOrderCountSpinner"> + <Properties> + <Property name="model" type="javax.swing.SpinnerModel" editor="org.netbeans.modules.form.editors2.SpinnerModelEditor"> + <SpinnerModel initial="7" maximum="100" minimum="1" numberType="java.lang.Integer" stepSize="1" type="number"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="-1" gridY="-1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JLabel" name="randomOrderCountLbl"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.label_unitOrdersAtATime.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="-1" gridY="-1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JLabel" name="fillingLbl3"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="5" gridY="0" gridWidth="1" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="1.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JSpinner" name="randomOrderSizeSpinner"> + <Properties> + <Property name="model" type="javax.swing.SpinnerModel" editor="org.netbeans.modules.form.editors2.SpinnerModelEditor"> + <SpinnerModel initial="2" maximum="10" minimum="1" numberType="java.lang.Integer" stepSize="1" type="number"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="3" gridY="0" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JLabel" name="randomOrderSizeLbl"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.label_unitDriveOrdersPerOrder.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="4" gridY="0" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> + </Container> + <Container class="javax.swing.JPanel" name="explicitOrderSpecPanel"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="1" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Component class="javax.swing.JRadioButton" name="explicitOrderSpecButton"> + <Properties> + <Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor"> + <ComponentRef name="orderSpecButtonGroup"/> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.radioButton_createOrdersAccordingDefinition.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="explicitOrderSpecButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="0" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JLabel" name="fillingLbl4"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="-1" gridY="-1" gridWidth="1" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="1.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> + </Container> + </SubComponents> + </Container> + <Container class="javax.swing.JPanel" name="orderGenPanel"> + <Properties> + <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> + <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> + <TitledBorder title="Order generation"> + <ResourceString PropertyName="titleX" bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.panel_orderGeneration.border.title" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </TitledBorder> + </Border> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="3" gridWidth="1" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="18" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Component class="javax.swing.JCheckBox" name="orderGenChkBox"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.checkBox_enableOrderGeneration.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="itemStateChanged" listener="java.awt.event.ItemListener" parameters="java.awt.event.ItemEvent" handler="orderGenChkBoxItemStateChanged"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="0" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JLabel" name="fillingLbl5"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="-1" gridY="-1" gridWidth="1" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="1.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> + </Container> + <Container class="javax.swing.JPanel" name="transportOrderGenPanel"> + <Properties> + <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> + <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> + <TitledBorder title="Transport order modelling"> + <ResourceString PropertyName="titleX" bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.panel_transportOrderModelling.border.title" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </TitledBorder> + </Border> + </Property> + <Property name="enabled" type="boolean" value="false"/> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[1057, 800]"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="2" gridWidth="1" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="18" weightX="1.0" weightY="1.0"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Container class="javax.swing.JPanel" name="transportOrdersPanel"> + <Properties> + <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> + <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> + <TitledBorder title="Transport orders"> + <ResourceString PropertyName="titleX" bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.panel_transportOrders.border.title" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </TitledBorder> + </Border> + </Property> + <Property name="enabled" type="boolean" value="false"/> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[568, 452]"/> + </Property> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="0" gridWidth="2" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="1.0" weightY="1.0"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Container class="javax.swing.JScrollPane" name="jScrollPane2"> + <Properties> + <Property name="horizontalScrollBarPolicy" type="int" value="31"/> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[100, 500]"/> + </Property> + </Properties> + <AuxValues> + <AuxValue name="autoScrollPane" type="java.lang.Boolean" value="true"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="-1" gridY="-1" gridWidth="1" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="1.0" weightY="1.0"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JTable" name="toTable"> + <Properties> + <Property name="model" type="javax.swing.table.TableModel" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> + <Connection code="new TransportOrderTableModel()" type="code"/> + </Property> + <Property name="columnModel" type="javax.swing.table.TableColumnModel" editor="org.netbeans.modules.form.editors2.TableColumnModelEditor"> + <TableColumnModel selectionModel="0"/> + </Property> + <Property name="selectionModel" type="javax.swing.ListSelectionModel" editor="org.netbeans.modules.form.editors2.JTableSelectionModelEditor"> + <JTableSelectionModel selectionMode="0"/> + </Property> + <Property name="tableHeader" type="javax.swing.table.JTableHeader" editor="org.netbeans.modules.form.editors2.JTableHeaderEditor"> + <TableHeader reorderingAllowed="true" resizingAllowed="true"/> + </Property> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_CreateCodePost" type="java.lang.String" value="TOTableSelectionListener listener = new TOTableSelectionListener(toTable); toTable.getSelectionModel().addListSelectionListener(listener);"/> + </AuxValues> + </Component> + </SubComponents> + </Container> + <Container class="javax.swing.JPanel" name="transportOrdersActionPanel"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="3" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="1.0" weightY="0.0"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Component class="javax.swing.JButton" name="addToTOTableButton"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.button_addTransportOrder.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.button_addTransportOrder.tooltipText" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + <Property name="enabled" type="boolean" value="false"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="addToTOTableButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="0" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="1.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JButton" name="deleteFromTOTableButton"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.button_deleteSelectedOrder.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.button_deleteSelectedOrder.tooltipText" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + <Property name="enabled" type="boolean" value="false"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="deleteFromTOTableButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="1" gridY="0" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="0" insetsRight="0" anchor="10" weightX="1.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> + </Container> + </SubComponents> + </Container> + <Container class="javax.swing.JTabbedPane" name="jTabbedPane1"> + <Properties> + <Property name="enabled" type="boolean" value="false"/> + </Properties> + <AccessibilityProperties> + <Property name="AccessibleContext.accessibleName" type="java.lang.String" value="Drive orders"/> + </AccessibilityProperties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="1" gridWidth="2" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="1.0" weightY="1.0"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JTabbedPaneSupportLayout"/> + <SubComponents> + <Container class="javax.swing.JPanel" name="driveOrdersPanel"> + <Properties> + <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> + <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> + <TitledBorder title="Drive orders"> + <ResourceString PropertyName="titleX" bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.panel_driveOrders.border.title" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </TitledBorder> + </Border> + </Property> + <Property name="enabled" type="boolean" value="false"/> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.support.JTabbedPaneSupportLayout" value="org.netbeans.modules.form.compat2.layouts.support.JTabbedPaneSupportLayout$JTabbedPaneConstraintsDescription"> + <JTabbedPaneConstraints tabName="Drive orders"> + <Property name="tabTitle" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.tab_driveOrders.title" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </JTabbedPaneConstraints> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Container class="javax.swing.JScrollPane" name="driveOrdersScrollPane"> + <AuxValues> + <AuxValue name="autoScrollPane" type="java.lang.Boolean" value="true"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="0" gridWidth="3" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="1.0" weightY="1.0"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JTable" name="doTable"> + <Properties> + <Property name="model" type="javax.swing.table.TableModel" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> + <Connection code="new DriveOrderTableModel()" type="code"/> + </Property> + <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="org/opentcs/remote/genericclient/Bundle.properties" key="continuousLoadPanel.table_driveOrders.tooltipText" replaceFormat="bundle.getString("{key}")"/> + </Property> + <Property name="selectionModel" type="javax.swing.ListSelectionModel" editor="org.netbeans.modules.form.editors2.JTableSelectionModelEditor"> + <JTableSelectionModel selectionMode="0"/> + </Property> + </Properties> + </Component> + </SubComponents> + </Container> + <Component class="javax.swing.JButton" name="deleteFromDOTableButton"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.button_deleteSelectedDriveOrder.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="org/opentcs/remote/genericclient/Bundle.properties" key="continuousLoadPanel.button_deleteSelectedDriveOrder.tooltipText" replaceFormat="bundle.getString("{key}")"/> + </Property> + <Property name="enabled" type="boolean" value="false"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="deleteFromDOTableButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="1" gridY="1" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="1.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JButton" name="addDOButton"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.button_addDriveOrder.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + <Property name="enabled" type="boolean" value="false"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="addDOButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="1" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="1.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> + </Container> + <Container class="javax.swing.JPanel" name="propertyPanel"> + <Properties> + <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> + <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> + <TitledBorder title="Properties"> + <ResourceString PropertyName="titleX" bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.panel_properties.border.title" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </TitledBorder> + </Border> + </Property> + <Property name="enabled" type="boolean" value="false"/> + </Properties> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.support.JTabbedPaneSupportLayout" value="org.netbeans.modules.form.compat2.layouts.support.JTabbedPaneSupportLayout$JTabbedPaneConstraintsDescription"> + <JTabbedPaneConstraints tabName="Properties"> + <Property name="tabTitle" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.tab_properties.title" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </JTabbedPaneConstraints> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Container class="javax.swing.JScrollPane" name="jScrollPane1"> + <AuxValues> + <AuxValue name="autoScrollPane" type="java.lang.Boolean" value="true"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="0" gridWidth="2" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="1.0" weightY="1.0"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JTable" name="propertyTable"> + </Component> + </SubComponents> + </Container> + <Component class="javax.swing.JButton" name="addPropertyButton"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.button_addProperty.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + <Property name="enabled" type="boolean" value="false"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="addPropertyButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="1" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="1.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JButton" name="removePropertyButton"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.button_removeProperty.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + <Property name="enabled" type="boolean" value="false"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="removePropertyButtonActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="1" gridY="1" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="1.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> + </Container> + </SubComponents> + </Container> + <Container class="javax.swing.JPanel" name="openSavePanel"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="2" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignFlowLayout"/> + <SubComponents> + <Component class="javax.swing.JButton" name="openButton"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.button_open.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + <Property name="enabled" type="boolean" value="false"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="openButtonActionPerformed"/> + </Events> + </Component> + <Component class="javax.swing.JButton" name="saveButton"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties" key="continuousLoadPanel.button_save.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + <Property name="enabled" type="boolean" value="false"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="saveButtonActionPerformed"/> + </Events> + </Component> + </SubComponents> + </Container> + </SubComponents> + </Container> + </SubComponents> +</Form> diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/ContinuousLoadPanel.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/ContinuousLoadPanel.java new file mode 100644 index 0000000..c327625 --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/ContinuousLoadPanel.java @@ -0,0 +1,1233 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.guing.plugins.panels.loadgenerator.I18nPlantOverviewPanelLoadGenerator.BUNDLE_PATH; + +import jakarta.inject.Inject; +import java.awt.event.ItemEvent; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import javax.swing.DefaultCellEditor; +import javax.swing.JComboBox; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.filechooser.FileNameExtensionFilter; +import javax.swing.table.AbstractTableModel; +import org.opentcs.access.CredentialsException; +import org.opentcs.access.Kernel; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.components.kernel.services.ServiceUnavailableException; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.components.plantoverview.PluggablePanel; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Vehicle; +import org.opentcs.guing.plugins.panels.loadgenerator.PropertyTableModel.PropEntry; +import org.opentcs.guing.plugins.panels.loadgenerator.batchcreator.ExplicitOrderBatchGenerator; +import org.opentcs.guing.plugins.panels.loadgenerator.batchcreator.OrderBatchCreator; +import org.opentcs.guing.plugins.panels.loadgenerator.batchcreator.RandomOrderBatchCreator; +import org.opentcs.guing.plugins.panels.loadgenerator.trigger.OrderGenerationTrigger; +import org.opentcs.guing.plugins.panels.loadgenerator.trigger.SingleOrderGenTrigger; +import org.opentcs.guing.plugins.panels.loadgenerator.trigger.ThresholdOrderGenTrigger; +import org.opentcs.guing.plugins.panels.loadgenerator.trigger.TimeoutOrderGenTrigger; +import org.opentcs.guing.plugins.panels.loadgenerator.xmlbinding.DriveOrderEntry; +import org.opentcs.guing.plugins.panels.loadgenerator.xmlbinding.TransportOrderEntry; +import org.opentcs.guing.plugins.panels.loadgenerator.xmlbinding.TransportOrdersDocument; +import org.opentcs.util.Comparators; +import org.opentcs.util.event.EventSource; +import org.opentcs.util.gui.StringListCellRenderer; +import org.opentcs.util.gui.StringTableCellRenderer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A panel for continously creating transport orders. + */ +public class ContinuousLoadPanel + extends + PluggablePanel { + + /** + * This class' logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ContinuousLoadPanel.class); + /** + * This classe's bundle. + */ + private final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_PATH); + /** + * Provides access to a portal. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * Where we get events from. + */ + private final EventSource eventSource; + /** + * The client that is registered at the kernel. + */ + private SharedKernelServicePortal sharedPortal; + /** + * The object service. + */ + private TCSObjectService objectService; + /** + * The instance trigger creation of new orders. + */ + private volatile OrderGenerationTrigger orderGenTrigger; + /** + * The currently selected transport order data. + */ + private TransportOrderData selectedTrOrder; + /** + * Indicates whether this component is enabled. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param portalProvider The application's portal provider. + * @param eventSource Where components can register for events. + */ + @Inject + @SuppressWarnings("this-escape") + public ContinuousLoadPanel( + SharedKernelServicePortalProvider portalProvider, + @ApplicationEventBus + EventSource eventSource + ) { + this.portalProvider = requireNonNull(portalProvider, "portalProvider"); + this.eventSource = requireNonNull(eventSource, "eventSource"); + + initComponents(); + + JComboBox<TransportOrderData.Deadline> deadlineComboBox = new JComboBox<>(); + deadlineComboBox.addItem(null); + for (TransportOrderData.Deadline i : TransportOrderData.Deadline.values()) { + deadlineComboBox.addItem(i); + } + DefaultCellEditor deadlineEditor = new DefaultCellEditor(deadlineComboBox); + toTable.setDefaultEditor(TransportOrderData.Deadline.class, deadlineEditor); + + toTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); + doTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + // Get a kernel reference. + try { + sharedPortal = portalProvider.register(); + } + catch (ServiceUnavailableException exc) { + LOG.warn("Kernel unavailable", exc); + return; + } + + objectService = (TCSObjectService) sharedPortal.getPortal().getPlantModelService(); + + Set<Vehicle> vehicles = new TreeSet<>(Comparators.objectsByName()); + vehicles.addAll(objectService.fetchObjects(Vehicle.class)); + JComboBox<TCSObjectReference<Vehicle>> vehiclesComboBox = new JComboBox<>(); + vehiclesComboBox.addItem(null); + vehiclesComboBox.setRenderer(new StringListCellRenderer<>(x -> x == null ? "" : x.getName())); + for (Vehicle curVehicle : vehicles) { + vehiclesComboBox.addItem(curVehicle.getReference()); + } + DefaultCellEditor vehicleEditor = new DefaultCellEditor(vehiclesComboBox); + toTable.setDefaultEditor(TCSObjectReference.class, vehicleEditor); + toTable.setDefaultRenderer( + TCSObjectReference.class, + new StringTableCellRenderer<TCSObjectReference<?>>(x -> x == null ? "" : x.getName()) + ); + + doTable.getSelectionModel().addListSelectionListener(event -> updateElementStates()); + propertyTable.getSelectionModel().addListSelectionListener(event -> updateElementStates()); + + updateElementStates(); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + // Disable order generation + orderGenChkBox.setSelected(false); + + sharedPortal.close(); + initialized = false; + } + + private void updateElementStates() { + updateTriggerElementStates(); + updateOrderSpecElementStates(); + + updateRandomOrderElementStates(); + updateExplicitOrderElementStates(); + } + + private void updateTriggerElementStates() { + boolean enabled = !orderGenChkBox.isSelected(); + + singleTriggerRadioButton.setEnabled(enabled); + thresholdTriggerRadioButton.setEnabled(enabled); + thresholdSpinner.setEnabled(enabled); + thresholdOrdersLbl.setEnabled(enabled); + timerTriggerRadioButton.setEnabled(enabled); + timerSpinner.setEnabled(enabled); + timerSecondsLbl.setEnabled(enabled); + } + + private void updateOrderSpecElementStates() { + boolean enabled = !orderGenChkBox.isSelected(); + + randomOrderSpecButton.setEnabled(enabled); + explicitOrderSpecButton.setEnabled(enabled); + } + + private void updateRandomOrderElementStates() { + boolean enabled = randomOrderSpecButton.isSelected() && !orderGenChkBox.isSelected(); + + randomOrderCountSpinner.setEnabled(enabled); + randomOrderCountLbl.setEnabled(enabled); + randomOrderSizeSpinner.setEnabled(enabled); + randomOrderSizeLbl.setEnabled(enabled); + } + + private void updateExplicitOrderElementStates() { + boolean enabled = explicitOrderSpecButton.isSelected() && !orderGenChkBox.isSelected(); + + transportOrderGenPanel.setEnabled(enabled); + transportOrdersPanel.setEnabled(enabled); + addToTOTableButton.setEnabled(enabled); + jTabbedPane1.setEnabled(enabled); + driveOrdersPanel.setEnabled(enabled); + propertyPanel.setEnabled(enabled); + + doTable.setEnabled(enabled); + toTable.setEnabled(enabled); + propertyTable.setEnabled(enabled); + openButton.setEnabled(enabled); + saveButton.setEnabled(enabled); + + boolean transportOrderSelected = enabled && toTable.getSelectedRow() >= 0; + boolean driveOrderSelected = transportOrderSelected && doTable.getSelectedRow() >= 0; + boolean propertySelected = transportOrderSelected && propertyTable.getSelectedRow() >= 0; + // These buttons should only be enabled if an order is selected + deleteFromTOTableButton.setEnabled(transportOrderSelected); + addDOButton.setEnabled(transportOrderSelected); + addPropertyButton.setEnabled(transportOrderSelected); + deleteFromDOTableButton.setEnabled(driveOrderSelected); + removePropertyButton.setEnabled(propertySelected); + } + + /** + * Creates a suitable OrderBatchCreator. + * + * @return A suitable OrderBatchCreator. + */ + private OrderBatchCreator createOrderBatchCreator() { + if (randomOrderSpecButton.isSelected()) { + int orderCount = (Integer) randomOrderCountSpinner.getValue(); + int orderSize = (Integer) randomOrderSizeSpinner.getValue(); + return new RandomOrderBatchCreator( + sharedPortal.getPortal().getTransportOrderService(), + sharedPortal.getPortal().getDispatcherService(), + orderCount, + orderSize + ); + } + else if (explicitOrderSpecButton.isSelected()) { + saveCurrentTableData(); + TransportOrderTableModel tableModel + = (TransportOrderTableModel) toTable.getModel(); + for (TransportOrderData curData : tableModel.getList()) { + if (curData.getDriveOrders().isEmpty()) { + JOptionPane.showMessageDialog( + this, + bundle.getString("continuousLoadPanel.optionPane_driveOrderEmpty.message"), + bundle.getString("continuousLoadPanel.optionPane_driveOrderEmpty.title"), + JOptionPane.ERROR_MESSAGE + ); + return null; + } + else { + for (DriveOrderStructure curDOS : curData.getDriveOrders()) { + if (curDOS.getDriveOrderLocation() == null + || curDOS.getDriveOrderVehicleOperation() == null) { + JOptionPane.showMessageDialog( + this, + bundle.getString("continuousLoadPanel.optionPane_driveOrderIncorrect.message"), + bundle.getString("continuousLoadPanel.optionPane_driveOrderIncorrect.title"), + JOptionPane.ERROR_MESSAGE + ); + return null; + } + } + } + } + return new ExplicitOrderBatchGenerator( + sharedPortal.getPortal().getTransportOrderService(), + sharedPortal.getPortal().getDispatcherService(), + tableModel.getList() + ); + } + else { + throw new UnsupportedOperationException("Unsupported order spec."); + } + } + + /** + * Creates a new order generation trigger. + * + * @return A new order generation trigger + */ + private OrderGenerationTrigger createOrderGenTrigger() { + OrderBatchCreator batchCreator = createOrderBatchCreator(); + if (batchCreator == null) { + return null; + } + if (thresholdTriggerRadioButton.isSelected()) { + return new ThresholdOrderGenTrigger( + eventSource, + objectService, + (Integer) thresholdSpinner.getValue(), + batchCreator + ); + } + else if (timerTriggerRadioButton.isSelected()) { + return new TimeoutOrderGenTrigger( + (Integer) timerSpinner.getValue() * 1000, + batchCreator + ); + } + else if (singleTriggerRadioButton.isSelected()) { + return new SingleOrderGenTrigger(batchCreator); + } + else { + LOG.warn("No trigger selected"); + return null; + } + } + + /** + * Saves the data in the table models to the actual TransportOrderData. + */ + private void saveCurrentTableData() { + if (selectedTrOrder == null) { + return; + } + + // Save the local properties before clearing the table + PropertyTableModel propTableModel = (PropertyTableModel) propertyTable.getModel(); + selectedTrOrder.getProperties().clear(); + for (PropEntry propEntry : propTableModel.getList()) { + selectedTrOrder.addProperty(propEntry.getKey(), propEntry.getValue()); + } + // Save the drive orders + DriveOrderTableModel doTableModel = (DriveOrderTableModel) doTable.getModel(); + selectedTrOrder.getDriveOrders().clear(); + for (DriveOrderStructure curDO : doTableModel.getContent()) { + selectedTrOrder.addDriveOrder(curDO); + } + } + + /** + * Builds the tables when a transport order was selected. + * + * @param selectedRow Indicating which transport order was selected. + */ + private void buildTableModels(int selectedRow) { + if (selectedRow < 0) { + return; + } + + saveCurrentTableData(); + + selectedTrOrder = ((TransportOrderTableModel) toTable.getModel()).getDataAt(selectedRow); + if (selectedTrOrder != null) { + buildTableModels(selectedTrOrder); + } + } + + private void buildTableModels(TransportOrderData transportOrder) { + requireNonNull(transportOrder, "transportOrder"); + + // Drive orders + locationsComboBox.removeAllItems(); + operationTypesComboBox.removeAllItems(); + SortedSet<Location> sortedLocationSet = new TreeSet<>(Comparators.objectsByName()); + sortedLocationSet.addAll(objectService.fetchObjects(Location.class)); + for (Location i : sortedLocationSet) { + locationsComboBox.addItem(i.getReference()); + } + locationsComboBox.addItemListener((ItemEvent e) -> locationsComboBoxItemStateChanged(e)); + doTable.setModel(new DriveOrderTableModel(transportOrder.getDriveOrders())); + doTable.setDefaultEditor(TCSObjectReference.class, new DefaultCellEditor(locationsComboBox)); + doTable.setDefaultEditor(String.class, new DefaultCellEditor(operationTypesComboBox)); + + // Properties + propertyTable.setModel(new PropertyTableModel(transportOrder.getProperties())); + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + orderSpecButtonGroup = new javax.swing.ButtonGroup(); + triggerButtonGroup = new javax.swing.ButtonGroup(); + operationTypesComboBox = new javax.swing.JComboBox<>(); + locationsComboBox = new javax.swing.JComboBox<>(); + locationsComboBox.setRenderer(new LocationComboBoxRenderer()); + fileChooser = new javax.swing.JFileChooser(); + triggerPanel = new javax.swing.JPanel(); + thresholdTriggerRadioButton = new javax.swing.JRadioButton(); + thresholdSpinner = new javax.swing.JSpinner(); + thresholdOrdersLbl = new javax.swing.JLabel(); + fillingLbl = new javax.swing.JLabel(); + timerTriggerRadioButton = new javax.swing.JRadioButton(); + timerSpinner = new javax.swing.JSpinner(); + timerSecondsLbl = new javax.swing.JLabel(); + singleTriggerRadioButton = new javax.swing.JRadioButton(); + orderProfilePanel = new javax.swing.JPanel(); + randomOrderSpecPanel = new javax.swing.JPanel(); + randomOrderSpecButton = new javax.swing.JRadioButton(); + randomOrderCountSpinner = new javax.swing.JSpinner(); + randomOrderCountLbl = new javax.swing.JLabel(); + fillingLbl3 = new javax.swing.JLabel(); + randomOrderSizeSpinner = new javax.swing.JSpinner(); + randomOrderSizeLbl = new javax.swing.JLabel(); + explicitOrderSpecPanel = new javax.swing.JPanel(); + explicitOrderSpecButton = new javax.swing.JRadioButton(); + fillingLbl4 = new javax.swing.JLabel(); + orderGenPanel = new javax.swing.JPanel(); + orderGenChkBox = new javax.swing.JCheckBox(); + fillingLbl5 = new javax.swing.JLabel(); + transportOrderGenPanel = new javax.swing.JPanel(); + transportOrdersPanel = new javax.swing.JPanel(); + jScrollPane2 = new javax.swing.JScrollPane(); + toTable = new javax.swing.JTable(); + TOTableSelectionListener listener = new TOTableSelectionListener(toTable); +toTable.getSelectionModel().addListSelectionListener(listener); + transportOrdersActionPanel = new javax.swing.JPanel(); + addToTOTableButton = new javax.swing.JButton(); + deleteFromTOTableButton = new javax.swing.JButton(); + jTabbedPane1 = new javax.swing.JTabbedPane(); + driveOrdersPanel = new javax.swing.JPanel(); + driveOrdersScrollPane = new javax.swing.JScrollPane(); + doTable = new javax.swing.JTable(); + deleteFromDOTableButton = new javax.swing.JButton(); + addDOButton = new javax.swing.JButton(); + propertyPanel = new javax.swing.JPanel(); + jScrollPane1 = new javax.swing.JScrollPane(); + propertyTable = new javax.swing.JTable(); + addPropertyButton = new javax.swing.JButton(); + removePropertyButton = new javax.swing.JButton(); + openSavePanel = new javax.swing.JPanel(); + openButton = new javax.swing.JButton(); + saveButton = new javax.swing.JButton(); + + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle"); // NOI18N + operationTypesComboBox.setToolTipText(bundle.getString("continuousLoadPanel.comboBox_operationTypes.tooltipText")); // NOI18N + + locationsComboBox.setToolTipText(bundle.getString("continuousLoadPanel.comboBox_locations.tooltipText")); // NOI18N + locationsComboBox.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + locationsComboBoxItemStateChanged(evt); + } + }); + + fileChooser.setFileFilter(new FileNameExtensionFilter("*.xml", "xml")); + + setPreferredSize(new java.awt.Dimension(520, 700)); + setLayout(new java.awt.GridBagLayout()); + + triggerPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("continuousLoadPanel.panel_generateTrigger.border.title"))); // NOI18N + triggerPanel.setLayout(new java.awt.GridBagLayout()); + + triggerButtonGroup.add(thresholdTriggerRadioButton); + thresholdTriggerRadioButton.setText(bundle.getString("continuousLoadPanel.radioButton_triggerByOrderThreshold.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + triggerPanel.add(thresholdTriggerRadioButton, gridBagConstraints); + + thresholdSpinner.setModel(new javax.swing.SpinnerNumberModel(10, 0, 100, 1)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + triggerPanel.add(thresholdSpinner, gridBagConstraints); + + thresholdOrdersLbl.setText(bundle.getString("continuousLoadPanel.label_unitOrdersToBeProcessed.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + triggerPanel.add(thresholdOrdersLbl, gridBagConstraints); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 3; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + triggerPanel.add(fillingLbl, gridBagConstraints); + + triggerButtonGroup.add(timerTriggerRadioButton); + timerTriggerRadioButton.setText(bundle.getString("continuousLoadPanel.radioButton_triggerAfterTimeout.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + triggerPanel.add(timerTriggerRadioButton, gridBagConstraints); + + timerSpinner.setModel(new javax.swing.SpinnerNumberModel(60, 1, 3600, 1)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + triggerPanel.add(timerSpinner, gridBagConstraints); + + timerSecondsLbl.setText(bundle.getString("continuousLoadPanel.label_unitSeconds.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + triggerPanel.add(timerSecondsLbl, gridBagConstraints); + + triggerButtonGroup.add(singleTriggerRadioButton); + singleTriggerRadioButton.setSelected(true); + singleTriggerRadioButton.setText(bundle.getString("continuousLoadPanel.radioButton_triggerOnce.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + triggerPanel.add(singleTriggerRadioButton, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + add(triggerPanel, gridBagConstraints); + + orderProfilePanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("continuousLoadPanel.panel_orderProfile.border.title"))); // NOI18N + orderProfilePanel.setLayout(new java.awt.GridBagLayout()); + + randomOrderSpecPanel.setLayout(new java.awt.GridBagLayout()); + + orderSpecButtonGroup.add(randomOrderSpecButton); + randomOrderSpecButton.setSelected(true); + randomOrderSpecButton.setText(bundle.getString("continuousLoadPanel.radioButton_createOrdersRandomly.text")); // NOI18N + randomOrderSpecButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + randomOrderSpecButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + randomOrderSpecPanel.add(randomOrderSpecButton, gridBagConstraints); + + randomOrderCountSpinner.setModel(new javax.swing.SpinnerNumberModel(7, 1, 100, 1)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + randomOrderSpecPanel.add(randomOrderCountSpinner, gridBagConstraints); + + randomOrderCountLbl.setText(bundle.getString("continuousLoadPanel.label_unitOrdersAtATime.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + randomOrderSpecPanel.add(randomOrderCountLbl, gridBagConstraints); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 5; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + randomOrderSpecPanel.add(fillingLbl3, gridBagConstraints); + + randomOrderSizeSpinner.setModel(new javax.swing.SpinnerNumberModel(2, 1, 10, 1)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 3; + gridBagConstraints.gridy = 0; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + randomOrderSpecPanel.add(randomOrderSizeSpinner, gridBagConstraints); + + randomOrderSizeLbl.setText(bundle.getString("continuousLoadPanel.label_unitDriveOrdersPerOrder.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 4; + gridBagConstraints.gridy = 0; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + randomOrderSpecPanel.add(randomOrderSizeLbl, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + orderProfilePanel.add(randomOrderSpecPanel, gridBagConstraints); + + explicitOrderSpecPanel.setLayout(new java.awt.GridBagLayout()); + + orderSpecButtonGroup.add(explicitOrderSpecButton); + explicitOrderSpecButton.setText(bundle.getString("continuousLoadPanel.radioButton_createOrdersAccordingDefinition.text")); // NOI18N + explicitOrderSpecButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + explicitOrderSpecButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + explicitOrderSpecPanel.add(explicitOrderSpecButton, gridBagConstraints); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + explicitOrderSpecPanel.add(fillingLbl4, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + orderProfilePanel.add(explicitOrderSpecPanel, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + add(orderProfilePanel, gridBagConstraints); + + orderGenPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("continuousLoadPanel.panel_orderGeneration.border.title"))); // NOI18N + orderGenPanel.setLayout(new java.awt.GridBagLayout()); + + orderGenChkBox.setText(bundle.getString("continuousLoadPanel.checkBox_enableOrderGeneration.text")); // NOI18N + orderGenChkBox.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + orderGenChkBoxItemStateChanged(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + orderGenPanel.add(orderGenChkBox, gridBagConstraints); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + orderGenPanel.add(fillingLbl5, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + add(orderGenPanel, gridBagConstraints); + + transportOrderGenPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("continuousLoadPanel.panel_transportOrderModelling.border.title"))); // NOI18N + transportOrderGenPanel.setEnabled(false); + transportOrderGenPanel.setPreferredSize(new java.awt.Dimension(1057, 800)); + transportOrderGenPanel.setLayout(new java.awt.GridBagLayout()); + + transportOrdersPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("continuousLoadPanel.panel_transportOrders.border.title"))); // NOI18N + transportOrdersPanel.setEnabled(false); + transportOrdersPanel.setPreferredSize(new java.awt.Dimension(568, 452)); + transportOrdersPanel.setLayout(new java.awt.GridBagLayout()); + + jScrollPane2.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + jScrollPane2.setPreferredSize(new java.awt.Dimension(100, 500)); + + toTable.setModel(new TransportOrderTableModel()); + toTable.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + jScrollPane2.setViewportView(toTable); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + transportOrdersPanel.add(jScrollPane2, gridBagConstraints); + + transportOrdersActionPanel.setLayout(new java.awt.GridBagLayout()); + + addToTOTableButton.setText(bundle.getString("continuousLoadPanel.button_addTransportOrder.text")); // NOI18N + addToTOTableButton.setToolTipText(bundle.getString("continuousLoadPanel.button_addTransportOrder.tooltipText")); // NOI18N + addToTOTableButton.setEnabled(false); + addToTOTableButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addToTOTableButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + transportOrdersActionPanel.add(addToTOTableButton, gridBagConstraints); + + deleteFromTOTableButton.setText(bundle.getString("continuousLoadPanel.button_deleteSelectedOrder.text")); // NOI18N + deleteFromTOTableButton.setToolTipText(bundle.getString("continuousLoadPanel.button_deleteSelectedOrder.tooltipText")); // NOI18N + deleteFromTOTableButton.setEnabled(false); + deleteFromTOTableButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + deleteFromTOTableButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + transportOrdersActionPanel.add(deleteFromTOTableButton, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.weightx = 1.0; + transportOrdersPanel.add(transportOrdersActionPanel, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + transportOrderGenPanel.add(transportOrdersPanel, gridBagConstraints); + + jTabbedPane1.setEnabled(false); + + driveOrdersPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("continuousLoadPanel.panel_driveOrders.border.title"))); // NOI18N + driveOrdersPanel.setEnabled(false); + driveOrdersPanel.setLayout(new java.awt.GridBagLayout()); + + doTable.setModel(new DriveOrderTableModel()); + doTable.setToolTipText(bundle.getString("continuousLoadPanel.table_driveOrders.tooltipText")); // NOI18N + doTable.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + driveOrdersScrollPane.setViewportView(doTable); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + driveOrdersPanel.add(driveOrdersScrollPane, gridBagConstraints); + + deleteFromDOTableButton.setText(bundle.getString("continuousLoadPanel.button_deleteSelectedDriveOrder.text")); // NOI18N + deleteFromDOTableButton.setToolTipText(bundle.getString("continuousLoadPanel.button_deleteSelectedDriveOrder.tooltipText")); // NOI18N + deleteFromDOTableButton.setEnabled(false); + deleteFromDOTableButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + deleteFromDOTableButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + driveOrdersPanel.add(deleteFromDOTableButton, gridBagConstraints); + + addDOButton.setText(bundle.getString("continuousLoadPanel.button_addDriveOrder.text")); // NOI18N + addDOButton.setEnabled(false); + addDOButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addDOButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + driveOrdersPanel.add(addDOButton, gridBagConstraints); + + jTabbedPane1.addTab(bundle.getString("continuousLoadPanel.tab_driveOrders.title"), driveOrdersPanel); // NOI18N + + propertyPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("continuousLoadPanel.panel_properties.border.title"))); // NOI18N + propertyPanel.setEnabled(false); + propertyPanel.setLayout(new java.awt.GridBagLayout()); + + jScrollPane1.setViewportView(propertyTable); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + propertyPanel.add(jScrollPane1, gridBagConstraints); + + addPropertyButton.setText(bundle.getString("continuousLoadPanel.button_addProperty.text")); // NOI18N + addPropertyButton.setEnabled(false); + addPropertyButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addPropertyButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + propertyPanel.add(addPropertyButton, gridBagConstraints); + + removePropertyButton.setText(bundle.getString("continuousLoadPanel.button_removeProperty.text")); // NOI18N + removePropertyButton.setEnabled(false); + removePropertyButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + removePropertyButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + propertyPanel.add(removePropertyButton, gridBagConstraints); + + jTabbedPane1.addTab(bundle.getString("continuousLoadPanel.tab_properties.title"), propertyPanel); // NOI18N + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + transportOrderGenPanel.add(jTabbedPane1, gridBagConstraints); + jTabbedPane1.getAccessibleContext().setAccessibleName("Drive orders"); + + openButton.setText(bundle.getString("continuousLoadPanel.button_open.text")); // NOI18N + openButton.setEnabled(false); + openButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + openButtonActionPerformed(evt); + } + }); + openSavePanel.add(openButton); + + saveButton.setText(bundle.getString("continuousLoadPanel.button_save.text")); // NOI18N + saveButton.setEnabled(false); + saveButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + saveButtonActionPerformed(evt); + } + }); + openSavePanel.add(saveButton); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + transportOrderGenPanel.add(openSavePanel, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + add(transportOrderGenPanel, gridBagConstraints); + + getAccessibleContext().setAccessibleName(bundle.getString("continuousLoadPanel.accessibleName")); // NOI18N + }// </editor-fold>//GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void randomOrderSpecButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_randomOrderSpecButtonActionPerformed + updateElementStates(); + }//GEN-LAST:event_randomOrderSpecButtonActionPerformed + + private void explicitOrderSpecButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_explicitOrderSpecButtonActionPerformed + updateElementStates(); + }//GEN-LAST:event_explicitOrderSpecButtonActionPerformed + + private void orderGenChkBoxItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_orderGenChkBoxItemStateChanged + if (evt.getStateChange() == ItemEvent.SELECTED) { + if (sharedPortal.getPortal().getState().equals(Kernel.State.OPERATING)) { + orderGenTrigger = createOrderGenTrigger(); + if (orderGenTrigger == null) { + return; + } + orderGenTrigger.setTriggeringEnabled(true); + } + } + else if (orderGenTrigger != null) { + orderGenTrigger.setTriggeringEnabled(false); + orderGenTrigger = null; + } + + updateElementStates(); + }//GEN-LAST:event_orderGenChkBoxItemStateChanged + + private void addToTOTableButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addToTOTableButtonActionPerformed + TransportOrderTableModel model = (TransportOrderTableModel) toTable.getModel(); + model.addData(new TransportOrderData()); + + updateElementStates(); + }//GEN-LAST:event_addToTOTableButtonActionPerformed + + private void deleteFromTOTableButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deleteFromTOTableButtonActionPerformed + if (toTable.getSelectedRowCount() == 0) { + return; + } + + TransportOrderTableModel model = (TransportOrderTableModel) toTable.getModel(); + // Removes the selected row from the table. + int selectedIndex = toTable.getSelectedRow(); + model.removeData(selectedIndex); + if (model.getRowCount() > 0) { + int indexToBeSelected = Math.min(selectedIndex, model.getRowCount() - 1); + // Update the drive orders and properties tables before updating the selection. + // XXX This is not the cleanest way to do it and should be fixed. + TransportOrderData selectedOrder = model.getDataAt(indexToBeSelected); + buildTableModels(selectedOrder); + toTable.changeSelection(indexToBeSelected, 0, false, false); + } + else { + doTable.setModel(new DriveOrderTableModel()); + propertyTable.setModel(new PropertyTableModel()); + } + + updateElementStates(); + }//GEN-LAST:event_deleteFromTOTableButtonActionPerformed + + private void deleteFromDOTableButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deleteFromDOTableButtonActionPerformed + if (doTable.getSelectedRow() == -1) { + return; + } + + int selectedRow = doTable.getSelectedRow(); + DriveOrderTableModel doTableModel = (DriveOrderTableModel) doTable.getModel(); + doTableModel.removeData(selectedRow); + int indexToBeSelected = Math.min(selectedRow, doTableModel.getRowCount() - 1); + doTable.changeSelection(indexToBeSelected, 0, true, false); + + updateElementStates(); + }//GEN-LAST:event_deleteFromDOTableButtonActionPerformed + + private void addDOButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addDOButtonActionPerformed + DriveOrderTableModel model = (DriveOrderTableModel) doTable.getModel(); + model.addData(new DriveOrderStructure()); + + updateElementStates(); + }//GEN-LAST:event_addDOButtonActionPerformed + + @SuppressWarnings("unchecked") + private void locationsComboBoxItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_locationsComboBoxItemStateChanged + operationTypesComboBox.removeAllItems(); + if (locationsComboBox.getSelectedItem() == null) { + return; + } + + TCSObjectReference<Location> loc + = (TCSObjectReference<Location>) locationsComboBox.getSelectedItem(); + Location location = objectService.fetchObject(Location.class, loc); + TCSObjectReference<LocationType> locationRef = location.getType(); + LocationType locationType = objectService.fetchObject(LocationType.class, locationRef); + Set<String> operationTypes = new TreeSet<>(locationType.getAllowedOperations()); + for (String j : operationTypes) { + operationTypesComboBox.addItem(j); + } + + // When selecting an item in the locationsComboBox we have + // to update the vehicle operation in the DriveOrderTable manually, + // otherwise the old value will persist and that could be a value + // the new location doesn't support + int selectedRow = doTable.getSelectedRow(); + if (selectedRow >= 0) { + DriveOrderTableModel model = (DriveOrderTableModel) doTable.getModel(); + DriveOrderStructure dos = model.getDataAt(selectedRow); + if (dos != null) { + if (!operationTypes.isEmpty()) { + dos.setDriveOrderVehicleOperation(operationTypes.iterator().next()); + } + else { + dos.setDriveOrderVehicleOperation(null); + } + } + ((AbstractTableModel) doTable.getModel()).fireTableDataChanged(); + } + }//GEN-LAST:event_locationsComboBoxItemStateChanged + + private void addPropertyButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addPropertyButtonActionPerformed + PropertyTableModel model = (PropertyTableModel) propertyTable.getModel(); + model.addData(new PropEntry()); + + updateElementStates(); + }//GEN-LAST:event_addPropertyButtonActionPerformed + + private void removePropertyButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removePropertyButtonActionPerformed + if (propertyTable.getSelectedRow() == -1) { + return; + } + + PropertyTableModel model = (PropertyTableModel) propertyTable.getModel(); + model.removeData(propertyTable.getSelectedRow()); + + updateElementStates(); + }//GEN-LAST:event_removePropertyButtonActionPerformed + + private void saveButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveButtonActionPerformed + saveCurrentTableData(); + int dialogResult = fileChooser.showSaveDialog(this); + if (dialogResult != JFileChooser.APPROVE_OPTION) { + return; + } + + TransportOrderTableModel model = (TransportOrderTableModel) toTable.getModel(); + File targetFile = fileChooser.getSelectedFile(); + if (!targetFile.getName().endsWith(".xml")) { + targetFile = new File(targetFile.getParentFile(), targetFile.getName() + ".xml"); + } + if (targetFile.exists()) { + dialogResult = JOptionPane.showConfirmDialog( + this, + bundle.getString("continuousLoadPanel.optionPane_overwriteFileConfirmation.message"), + bundle.getString("continuousLoadPanel.optionPane_overwriteFileConfirmation.title"), + JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE + ); + if (dialogResult != JOptionPane.YES_OPTION) { + return; + } + } + try { + model.toXmlDocument().toFile(targetFile); + } + catch (IOException exc) { + LOG.warn("Exception saving to " + targetFile.getPath(), exc); + JOptionPane.showMessageDialog( + this, + "Exception saving property set: " + exc.getMessage(), + "Exception saving property set", JOptionPane.ERROR_MESSAGE + ); + } + }//GEN-LAST:event_saveButtonActionPerformed + + private void openButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_openButtonActionPerformed + int dialogResult = fileChooser.showOpenDialog(this); + if (dialogResult != JFileChooser.APPROVE_OPTION) { + return; + } + + File targetFile = fileChooser.getSelectedFile(); + if (!targetFile.exists()) { + JOptionPane.showMessageDialog( + this, + bundle.getString("continuousLoadPanel.optionPane_fileDoesNotExist.message"), + bundle.getString("continuousLoadPanel.optionPane_fileDoesNotExist.title"), + JOptionPane.ERROR_MESSAGE + ); + return; + } + try { + // unmarshal + TransportOrdersDocument doc = TransportOrdersDocument.fromFile(targetFile); + List<TransportOrderData> newOrders = new ArrayList<>(); + for (TransportOrderEntry curStruc : doc.getTransportOrders()) { + TransportOrderData data = new TransportOrderData(); + switch (curStruc.getDeadline()) { + case MINUS_FIVE_MINUTES: + data.setDeadline(TransportOrderData.Deadline.MINUS_FIVE_MINUTES); + break; + case PLUS_FIVE_MINUTES: + data.setDeadline(TransportOrderData.Deadline.PLUS_FIVE_MINUTES); + break; + case MINUS_HALF_HOUR: + data.setDeadline(TransportOrderData.Deadline.MINUS_HALF_HOUR); + break; + case PLUS_HALF_HOUR: + data.setDeadline(TransportOrderData.Deadline.PLUS_HALF_HOUR); + break; + case PLUS_ONE_HOUR: + data.setDeadline(TransportOrderData.Deadline.PLUS_ONE_HOUR); + break; + case PLUS_TWO_HOURS: + default: + data.setDeadline(TransportOrderData.Deadline.PLUS_TWO_HOURS); + break; + } + data.setIntendedVehicle( + curStruc.getIntendedVehicle() == null ? null + : objectService.fetchObject( + Vehicle.class, + curStruc.getIntendedVehicle() + ).getReference() + ); + for (TransportOrderEntry.XMLMapEntry curEntry : curStruc.getProperties()) { + data.addProperty(curEntry.getKey(), curEntry.getValue()); + } + for (DriveOrderEntry curDOXMLS : curStruc.getDriveOrders()) { + DriveOrderStructure newDOS + = new DriveOrderStructure( + objectService.fetchObject( + Location.class, curDOXMLS.getLocationName() + ).getReference(), + curDOXMLS.getVehicleOperation() + ); + data.addDriveOrder(newDOS); + } + newOrders.add(data); + } + // clear tables + doTable.setModel(new DriveOrderTableModel()); + propertyTable.setModel(new PropertyTableModel()); + TransportOrderTableModel model = new TransportOrderTableModel(); + for (TransportOrderData curData : newOrders) { + model.addData(curData); + } + toTable.setModel(model); + } + catch (IOException | CredentialsException exc) { + LOG.warn("Exception reading property set from " + targetFile.getPath(), exc); + JOptionPane.showMessageDialog( + this, + "Exception reading property set:\n" + exc.getMessage(), + "Exception reading property set", + JOptionPane.ERROR_MESSAGE + ); + } + catch (NullPointerException e) { + JOptionPane.showMessageDialog( + this, + "Objects in this file seem not to " + + "appear in this model, please check your model.", + "Error", + JOptionPane.ERROR_MESSAGE + ); + } + + updateElementStates(); + }//GEN-LAST:event_openButtonActionPerformed + + // CHECKSTYLE:OFF + // FORMATTER:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton addDOButton; + private javax.swing.JButton addPropertyButton; + private javax.swing.JButton addToTOTableButton; + private javax.swing.JButton deleteFromDOTableButton; + private javax.swing.JButton deleteFromTOTableButton; + private javax.swing.JTable doTable; + private javax.swing.JPanel driveOrdersPanel; + private javax.swing.JScrollPane driveOrdersScrollPane; + private javax.swing.JRadioButton explicitOrderSpecButton; + private javax.swing.JPanel explicitOrderSpecPanel; + private javax.swing.JFileChooser fileChooser; + private javax.swing.JLabel fillingLbl; + private javax.swing.JLabel fillingLbl3; + private javax.swing.JLabel fillingLbl4; + private javax.swing.JLabel fillingLbl5; + private javax.swing.JScrollPane jScrollPane1; + private javax.swing.JScrollPane jScrollPane2; + private javax.swing.JTabbedPane jTabbedPane1; + private javax.swing.JComboBox<TCSObjectReference<Location>> locationsComboBox; + private javax.swing.JButton openButton; + private javax.swing.JPanel openSavePanel; + private javax.swing.JComboBox<String> operationTypesComboBox; + private javax.swing.JCheckBox orderGenChkBox; + private javax.swing.JPanel orderGenPanel; + private javax.swing.JPanel orderProfilePanel; + private javax.swing.ButtonGroup orderSpecButtonGroup; + private javax.swing.JPanel propertyPanel; + private javax.swing.JTable propertyTable; + private javax.swing.JLabel randomOrderCountLbl; + private javax.swing.JSpinner randomOrderCountSpinner; + private javax.swing.JLabel randomOrderSizeLbl; + private javax.swing.JSpinner randomOrderSizeSpinner; + private javax.swing.JRadioButton randomOrderSpecButton; + private javax.swing.JPanel randomOrderSpecPanel; + private javax.swing.JButton removePropertyButton; + private javax.swing.JButton saveButton; + private javax.swing.JRadioButton singleTriggerRadioButton; + private javax.swing.JLabel thresholdOrdersLbl; + private javax.swing.JSpinner thresholdSpinner; + private javax.swing.JRadioButton thresholdTriggerRadioButton; + private javax.swing.JLabel timerSecondsLbl; + private javax.swing.JSpinner timerSpinner; + private javax.swing.JRadioButton timerTriggerRadioButton; + private javax.swing.JTable toTable; + private javax.swing.JPanel transportOrderGenPanel; + private javax.swing.JPanel transportOrdersActionPanel; + private javax.swing.JPanel transportOrdersPanel; + private javax.swing.ButtonGroup triggerButtonGroup; + private javax.swing.JPanel triggerPanel; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON + + /** + * Creates a new selection listener for the transport order table. + */ + private class TOTableSelectionListener + implements + ListSelectionListener { + + /** + * The transport order table. + */ + private final JTable table; + + /** + * Creates a new TOTableSelectionListener. + * + * @param table The transport order table + */ + TOTableSelectionListener(JTable table) { + this.table = requireNonNull(table, "table"); + } + + @Override + public void valueChanged(ListSelectionEvent e) { + if (e.getSource() == table.getSelectionModel() && table.getRowSelectionAllowed()) { + ListSelectionModel model = (ListSelectionModel) e.getSource(); + int row = model.getMinSelectionIndex(); + buildTableModels(row); + updateElementStates(); + } + } + } +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/ContinuousLoadPanelConfiguration.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/ContinuousLoadPanelConfiguration.java new file mode 100644 index 0000000..effb50d --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/ContinuousLoadPanelConfiguration.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator; + +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; + +/** + * Provides methods to configure the continuous load panel. + */ +@ConfigurationPrefix(ContinuousLoadPanelConfiguration.PREFIX) +public interface ContinuousLoadPanelConfiguration { + + /** + * This configuration's prefix. + */ + String PREFIX = "continuousloadpanel"; + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to enable to register/enable the continuous load panel.", + orderKey = "0_enable" + ) + boolean enable(); +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/ContinuousLoadPanelFactory.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/ContinuousLoadPanelFactory.java new file mode 100644 index 0000000..403576a --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/ContinuousLoadPanelFactory.java @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.guing.plugins.panels.loadgenerator.I18nPlantOverviewPanelLoadGenerator.BUNDLE_PATH; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import java.util.ResourceBundle; +import org.opentcs.access.Kernel; +import org.opentcs.components.plantoverview.PluggablePanel; +import org.opentcs.components.plantoverview.PluggablePanelFactory; + +/** + * Creates load generator panels. + */ +public class ContinuousLoadPanelFactory + implements + PluggablePanelFactory { + + /** + * This class's bundle. + */ + private final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_PATH); + /** + * A provider for the actual panels. + */ + private final Provider<ContinuousLoadPanel> panelProvider; + + /** + * Creates a new instance. + * + * @param panelProvider A provider for the actual panels. + */ + @Inject + public ContinuousLoadPanelFactory(Provider<ContinuousLoadPanel> panelProvider) { + this.panelProvider = requireNonNull(panelProvider, "panelProvider"); + } + + @Override + public String getPanelDescription() { + return bundle.getString("continuousLoadPanelFactory.panelDescription"); + } + + @Override + public PluggablePanel createPanel(Kernel.State state) { + if (!providesPanel(state)) { + return null; + } + + return panelProvider.get(); + } + + @Override + public boolean providesPanel(Kernel.State state) { + return Kernel.State.OPERATING.equals(state); + } +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/DriveOrderStructure.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/DriveOrderStructure.java new file mode 100644 index 0000000..a239896 --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/DriveOrderStructure.java @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator; + +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; + +/** + * This class provides the data structure for storing + * and holding the elements typically drive order consists of: + * location and available operation type at this location. + */ +public class DriveOrderStructure { + + /** + * The reference to this drive order's location. + */ + private TCSObjectReference<Location> location; + /** + * The available oparation type at the location. + */ + private String vehicleOperation; + + /** + * Creates the new instance of DriveOrderStructure. + * + * @param referenceToLocation The reference to the drive order's location. + * @param newVehicleOperation The available operation type at the location. + */ + public DriveOrderStructure( + TCSObjectReference<Location> referenceToLocation, + String newVehicleOperation + ) { + if (referenceToLocation == null) { + throw new IllegalArgumentException("location argument is null!"); + } + if (newVehicleOperation == null) { + throw new IllegalArgumentException("vehicleOperation argument is null!"); + } + location = referenceToLocation; + vehicleOperation = newVehicleOperation; + } + + /** + * Creates an empty DriveOrderStructure. + */ + public DriveOrderStructure() { + location = null; + vehicleOperation = null; + } + + /** + * Returns a reference to the location. + * + * @return The reference to the location. + */ + public TCSObjectReference<Location> getDriveOrderLocation() { + return this.location; + } + + /** + * Sets the location of this order. + * + * @param loc The new location + */ + public void setDriveOrderLocation(TCSObjectReference<Location> loc) { + location = loc; + } + + /** + * Sets the vehicle operation of this order. + * + * @param op The new operation + */ + public void setDriveOrderVehicleOperation(String op) { + vehicleOperation = op; + } + + /** + * Returns this drive order's operation type. + * + * @return The drive order's operation type. + */ + public String getDriveOrderVehicleOperation() { + return this.vehicleOperation; + } +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/DriveOrderTableModel.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/DriveOrderTableModel.java new file mode 100644 index 0000000..86abe17 --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/DriveOrderTableModel.java @@ -0,0 +1,198 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.guing.plugins.panels.loadgenerator.I18nPlantOverviewPanelLoadGenerator.BUNDLE_PATH; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ResourceBundle; +import javax.swing.table.AbstractTableModel; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; + +/** + * A table model for drive orders. + */ +class DriveOrderTableModel + extends + AbstractTableModel { + + /** + * This classe's bundle. + */ + private final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_PATH); + /** + * The column names. + */ + private final String[] columnNames + = new String[]{ + bundle.getString( + "driveOrderTableModel.column_location.headerText" + ), + bundle.getString( + "driveOrderTableModel.column_operation.headerText" + ) + }; + /** + * The column classes. + */ + private final Class<?>[] columnClasses + = new Class<?>[]{ + TCSObjectReference.class, + String.class + }; + /** + * The actual content. + */ + private final List<DriveOrderStructure> driveOrderDataList = new ArrayList<>(); + + /** + * Creates a new instance. + * + * @param driveOrders The actual list of drive orders. + */ + DriveOrderTableModel(List<DriveOrderStructure> driveOrders) { + requireNonNull(driveOrders, "driveOrders"); + + for (DriveOrderStructure curDOS : driveOrders) { + driveOrderDataList.add(curDOS); + } + } + + /** + * Creates a new instance. + */ + DriveOrderTableModel() { + } + + @Override + public int getRowCount() { + return driveOrderDataList.size(); + } + + @Override + public int getColumnCount() { + return columnNames.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= driveOrderDataList.size()) { + return null; + } + DriveOrderStructure data = driveOrderDataList.get(rowIndex); + + if (data == null) { + return null; + } + switch (columnIndex) { + case 0: + if (data.getDriveOrderLocation() == null) { + return null; + } + else { + return data.getDriveOrderLocation().getName(); + } + + case 1: + return data.getDriveOrderVehicleOperation(); + default: + throw new IllegalArgumentException("Invalid columnIndex: " + columnIndex); + } + } + + @Override + public String getColumnName(int columnIndex) { + return columnNames[columnIndex]; + } + + @Override + public Class<?> getColumnClass(int columnIndex) { + return columnClasses[columnIndex]; + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + switch (columnIndex) { + case 0: + return true; + case 1: + return true; + default: + throw new IllegalArgumentException("Invalid columnIndex: " + columnIndex); + } + } + + @SuppressWarnings("unchecked") + @Override + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= driveOrderDataList.size()) { + return; + } + DriveOrderStructure data = driveOrderDataList.get(rowIndex); + if (aValue == null) { + return; + } + switch (columnIndex) { + case 0: + data.setDriveOrderLocation((TCSObjectReference<Location>) aValue); + break; + case 1: + data.setDriveOrderVehicleOperation((String) aValue); + break; + default: + throw new IllegalArgumentException("Unhandled columnIndex: " + columnIndex); + } + } + + /** + * Returns this model's complete content. + * + * @return This model's complete content. The result list is unmodifiable. + */ + public List<DriveOrderStructure> getContent() { + return Collections.unmodifiableList(driveOrderDataList); + } + + /** + * Returns the DriveOrderStructure at the given index. + * + * @param row Index which DriveOrderStructure shall be returned + * @return The DriveOrderStructure + */ + public DriveOrderStructure getDataAt(int row) { + if (row < 0 || row >= driveOrderDataList.size()) { + return null; + } + return driveOrderDataList.get(row); + } + + /** + * Adds drive order data to the end of the model/list. + * + * @param driveOrder The new drive order data + */ + public void addData(DriveOrderStructure driveOrder) { + requireNonNull(driveOrder, "driveOrder"); + + driveOrderDataList.add(driveOrder); + fireTableDataChanged(); + } + + /** + * Removes the DriveOrderStructure at the given index. + * Does nothing if <code>row</code> is not in scope. + * + * @param row Index which DriveOrderStructure shall be removed + */ + public void removeData(int row) { + if (row < 0 || row >= driveOrderDataList.size()) { + return; + } + driveOrderDataList.remove(row); + fireTableDataChanged(); + } +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/I18nPlantOverviewPanelLoadGenerator.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/I18nPlantOverviewPanelLoadGenerator.java new file mode 100644 index 0000000..c5d0a32 --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/I18nPlantOverviewPanelLoadGenerator.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator; + +/** + * Defines constants regarding internationalization. + */ +public interface I18nPlantOverviewPanelLoadGenerator { + + /** + * The path to the project's resource bundle. + */ + String BUNDLE_PATH = "i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle"; +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/LocationComboBoxRenderer.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/LocationComboBoxRenderer.java new file mode 100644 index 0000000..c3c6202 --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/LocationComboBoxRenderer.java @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator; + +import java.awt.Component; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.ListCellRenderer; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; + +/** + * A combo box renderer for locations. + */ +class LocationComboBoxRenderer + extends + JLabel + implements + ListCellRenderer<TCSObjectReference<Location>> { + + /** + * Creates a new instance. + */ + LocationComboBoxRenderer() { + } + + @Override + public Component getListCellRendererComponent( + JList<? extends TCSObjectReference<Location>> list, + TCSObjectReference<Location> value, + int index, + boolean isSelected, + boolean cellHasFocus + ) { + if (value == null) { + setText(""); + } + else { + setText(value.getName()); + } + return this; + } +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/PropertyTableModel.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/PropertyTableModel.java new file mode 100644 index 0000000..f0f413b --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/PropertyTableModel.java @@ -0,0 +1,245 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.guing.plugins.panels.loadgenerator.I18nPlantOverviewPanelLoadGenerator.BUNDLE_PATH; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.ResourceBundle; +import javax.swing.table.AbstractTableModel; + +/** + * Table model for transport order proerties. + */ +class PropertyTableModel + extends + AbstractTableModel { + + /** + * This classe's bundle. + */ + private static final ResourceBundle BUNDLE = ResourceBundle.getBundle(BUNDLE_PATH); + + /** + * The column names. + */ + private static final String[] COLUMN_NAMES + = new String[]{ + BUNDLE.getString( + "propertyTableModel.column_key.headerText" + ), + BUNDLE.getString( + "propertyTableModel.column_value.headerText" + ) + }; + /** + * The column classes. + */ + private static final Class<?>[] COLUMN_CLASSES + = new Class<?>[]{ + String.class, + String.class + }; + /** + * The properties we're maintaining. + */ + private List<PropEntry> data = new ArrayList<>(); + + /** + * Creates a new instance. + * + * @param data The properties. + */ + PropertyTableModel(Map<String, String> data) { + requireNonNull(data, "data"); + + for (Entry<String, String> entry : data.entrySet()) { + this.data.add(new PropEntry(entry.getKey(), entry.getValue())); + } + } + + /** + * Creates a new instance. + */ + PropertyTableModel() { + } + + @Override + public int getRowCount() { + return data.size(); + } + + @Override + public int getColumnCount() { + return COLUMN_NAMES.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= data.size()) { + return null; + } + PropEntry entry = data.get(rowIndex); + + switch (columnIndex) { + case 0: + return entry.getKey(); + case 1: + return entry.getValue(); + default: + throw new IllegalArgumentException("Invalid columnIndex: " + columnIndex); + } + } + + @Override + public String getColumnName(int columnIndex) { + return COLUMN_NAMES[columnIndex]; + } + + @Override + public Class<?> getColumnClass(int columnIndex) { + return COLUMN_CLASSES[columnIndex]; + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + switch (columnIndex) { + case 0: + return true; + case 1: + return true; + default: + throw new IllegalArgumentException("Invalid columnIndex: " + columnIndex); + } + } + + @Override + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= data.size()) { + return; + } + PropEntry entry = data.get(rowIndex); + + if (aValue == null) { + return; + } + switch (columnIndex) { + case 0: + entry.setKey((String) aValue); + break; + case 1: + entry.setValue((String) aValue); + break; + default: + throw new IllegalArgumentException("Unhandled columnIndex: " + columnIndex); + } + } + + /** + * Returns this model's complete content. + * + * @return This model's complete content. The result list is unmodifiable. + */ + public List<PropEntry> getList() { + return Collections.unmodifiableList(data); + } + + /** + * Adds an entry to the end of the model/list. + * + * @param propEntry The new entry. + */ + public void addData(PropEntry propEntry) { + requireNonNull(propEntry, "propEntry"); + + data.add(propEntry); + fireTableDataChanged(); + } + + /** + * Removes the entry at the given index. + * Does nothing if <code>row</code> does not exist. + * + * @param row Index of entry to be removed. + */ + public void removeData(int row) { + if (row < 0 || row >= data.size()) { + return; + } + data.remove(row); + fireTableDataChanged(); + } + + /** + * A class for editing properties. + */ + public static class PropEntry { + + /** + * The key. + */ + private String key = ""; + /** + * The value. + */ + private String value = ""; + + /** + * Creates a new instance. + */ + PropEntry() { + } + + /** + * Creates a new PropEntry. + * + * @param key The key + * @param value The value + */ + PropEntry(String key, String value) { + this.key = requireNonNull(key, "key"); + this.value = requireNonNull(value, "value"); + } + + /** + * Returns the key. + * + * @return The key + */ + public String getKey() { + return key; + } + + /** + * Sets the key. + * + * @param key The new key + */ + public void setKey(String key) { + this.key = requireNonNull(key, "key"); + } + + /** + * Returns the value. + * + * @return The value + */ + public String getValue() { + return value; + } + + /** + * Sets the value. + * + * @param value The new value + */ + public void setValue(String value) { + this.value = requireNonNull(value, "value"); + } + } +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/TransportOrderData.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/TransportOrderData.java new file mode 100644 index 0000000..73547ae --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/TransportOrderData.java @@ -0,0 +1,229 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; + +/** + * <code>TransportOrderData</code> implements functionalities for creating + * and for local storing of xml telegrams. + * The transport/drive oder data is stored in this class. + */ +public class TransportOrderData { + + /** + * The new transport order's deadline. + */ + private Deadline deadline = Deadline.PLUS_ONE_HOUR; + /** + * The drive orders the transport order consists of. + */ + private final List<DriveOrderStructure> driveOrders = new ArrayList<>(); + /** + * A reference to the vehicle intended to process the transport order. + */ + private TCSObjectReference<Vehicle> intendedVehicle; + /** + * Properties of the transport order data. + */ + private final Map<String, String> properties = new TreeMap<>(); + + /** + * Creates a new instance of TransportOrderData, that performes + * XML telegram generation. This class implements also functions for + * management of data structures that contains the data of + * gui-generated elements and data need for creating a new XML telegram and + * for creating of a new transport order instances. + */ + public TransportOrderData() { + } + + /** + * Adds a new DriveOrderStructure to this transport order. + * + * @param driveOrder The drive order that shall be added + */ + public void addDriveOrder(DriveOrderStructure driveOrder) { + Objects.requireNonNull(driveOrder, "driveOrder is null"); + driveOrders.add(driveOrder); + } + + /** + * Removes a <code>DriveOrderStructure</code> from the list of drive orders. + * + * @param index The index of the drive order in the list. + */ + public void removeDriveOrder(int index) { + driveOrders.remove(index); + } + + /** + * Returns a list of matched destinations that have to be travelled when + * processing the new generated transport order. + * + * @return The list of destinations that have to be travelled when processing + * the new generated transport order. + */ + public List<DriveOrder.Destination> getDestinations() { + List<DriveOrder.Destination> destinations = new ArrayList<>(driveOrders.size()); + for (DriveOrderStructure i : driveOrders) { + destinations.add( + new DriveOrder.Destination(i.getDriveOrderLocation()) + .withOperation(i.getDriveOrderVehicleOperation()) + ); + } + return destinations; + } + + /** + * Returns the properties of this transport order data. + * + * @return The properties + */ + public Map<String, String> getProperties() { + return properties; + } + + /** + * Adds a property. + * + * @param key The key + * @param value The value + */ + public void addProperty(String key, String value) { + if (key == null || value == null) { + return; + } + properties.put(key, value); + } + + /** + * Returns a list of drive orders. + * + * @return The list of drive orders. + */ + public List<DriveOrderStructure> getDriveOrders() { + return driveOrders; + } + + /** + * Returns a deadline. + * + * @return The deadline. + */ + public Deadline getDeadline() { + return this.deadline; + } + + /** + * Sets the currect deadline for representing transport order. + * + * @param newDeadline The new deadline. + */ + public void setDeadline(Deadline newDeadline) { + this.deadline = newDeadline; + } + + /** + * Returns a reference to the vehicle intended to process the order. + * + * @return A reference to the vehicle intended to process the order. + */ + public TCSObjectReference<Vehicle> getIntendedVehicle() { + return intendedVehicle; + } + + /** + * Sets a reference to the vehicle intended to process the order. + * + * @param vehicle A reference to the vehicle intended to process the order. + */ + public void setIntendedVehicle(TCSObjectReference<Vehicle> vehicle) { + intendedVehicle = vehicle; + } + + /** + * The enumeration of possible default deadline values. + */ + public enum Deadline { + + /** + * The deadline value is five minutes in the past. + */ + MINUS_FIVE_MINUTES("-5 min.", -60 * 5 * 1000), + /** + * The deadline value is five minutes in the future. + */ + PLUS_FIVE_MINUTES("5 min.", 60 * 5 * 1000), + /** + * The deadline value is a half hour in the past. + */ + MINUS_HALF_HOUR("-30 min.", -60 * 30 * 1000), + /** + * The deadline value is a half hour in the future. + */ + PLUS_HALF_HOUR("30 min.", 60 * 30 * 1000), + /** + * The deadline value is a one hour in the future. + */ + PLUS_ONE_HOUR(" 1 h.", 60 * 60 * 1000), + /** + * The deadline value is two hours in the future. + */ + PLUS_TWO_HOURS(" 2 h.", 2 * 60 * 60 * 1000); + + /** + * The deadline value (in ms). + */ + private final int millis; + /** + * The deadline label. + */ + private final String label; + + /** + * Creates a new Deadline. + * + * @param newLabel This deadline as a string + * @param deadline The value + */ + Deadline(String newLabel, int deadline) { + this.millis = deadline; + this.label = newLabel; + } + + /** + * Returns a deadline value (in ms relative to the current time). + * + * @return The deadline value (in ms relative to the current time). + */ + public int getTime() { + return this.millis; + } + + /** + * Returns an absolute time. + * + * @return The absolute time. + */ + public Date getAbsoluteTime() { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MILLISECOND, this.getTime()); + return calendar.getTime(); + } + + @Override + public String toString() { + return this.label; + } + } +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/TransportOrderTableModel.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/TransportOrderTableModel.java new file mode 100644 index 0000000..a3add77 --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/TransportOrderTableModel.java @@ -0,0 +1,187 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator; + +import static org.opentcs.guing.plugins.panels.loadgenerator.I18nPlantOverviewPanelLoadGenerator.BUNDLE_PATH; + +import java.util.ArrayList; +import java.util.List; +import java.util.ResourceBundle; +import javax.swing.table.AbstractTableModel; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.guing.plugins.panels.loadgenerator.xmlbinding.TransportOrderEntry; +import org.opentcs.guing.plugins.panels.loadgenerator.xmlbinding.TransportOrdersDocument; + +/** + * A table model for transport orders. + */ +class TransportOrderTableModel + extends + AbstractTableModel { + + /** + * This classe's bundle. + */ + private static final ResourceBundle BUNDLE = ResourceBundle.getBundle(BUNDLE_PATH); + /** + * The column names. + */ + private static final String[] COLUMN_NAMES + = new String[]{ + "#", + BUNDLE.getString( + "transportOrderTableModel.column_deadline.headerText" + ), + BUNDLE.getString( + "transportOrderTableModel.column_vehicle.headerText" + ) + }; + /** + * The column classes. + */ + private static final Class<?>[] COLUMN_CLASSES + = new Class<?>[]{ + Integer.class, + TransportOrderData.Deadline.class, + TCSObjectReference.class + }; + /** + * The actual content. + */ + private final List<TransportOrderData> transportOrderDataList = new ArrayList<>(); + + /** + * Creates a new instance. + */ + TransportOrderTableModel() { + } + + /** + * Adds a <code>TransportOrderData</code>. + * + * @param data The new transport order data + */ + public void addData(TransportOrderData data) { + int newIndex = transportOrderDataList.size(); + transportOrderDataList.add(data); + fireTableRowsInserted(newIndex, newIndex); + } + + /** + * Removes a <code>TransportOrderData</code>. + * + * @param row Index indicating which transport order data shall be removed + */ + public void removeData(int row) { + transportOrderDataList.remove(row); + fireTableRowsDeleted(row, row); + } + + /** + * Returns the <code>TransportOrderData</code> at the given index. + * + * @param row Index indicating which data shall be returned + * @return The transport order data at the given index + */ + public TransportOrderData getDataAt(int row) { + if (row >= 0) { + return transportOrderDataList.get(row); + } + else { + return null; + } + } + + @Override + public int getRowCount() { + return transportOrderDataList.size(); + } + + @Override + public int getColumnCount() { + return COLUMN_NAMES.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + TransportOrderData data = transportOrderDataList.get(rowIndex); + + switch (columnIndex) { + case 0: + return rowIndex + 1; + case 1: + return data.getDeadline(); + case 2: + return data.getIntendedVehicle(); + default: + throw new IllegalArgumentException("Invalid columnIndex: " + columnIndex); + } + } + + @Override + public String getColumnName(int columnIndex) { + return COLUMN_NAMES[columnIndex]; + } + + @Override + public Class<?> getColumnClass(int columnIndex) { + return COLUMN_CLASSES[columnIndex]; + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + switch (columnIndex) { + case 0: + return false; + case 1: + return true; + case 2: + return true; + default: + throw new IllegalArgumentException("Invalid columnIndex: " + columnIndex); + } + } + + @SuppressWarnings("unchecked") + @Override + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + TransportOrderData data = transportOrderDataList.get(rowIndex); + switch (columnIndex) { + case 1: + data.setDeadline((TransportOrderData.Deadline) aValue); + break; + case 2: + data.setIntendedVehicle((TCSObjectReference<Vehicle>) aValue); + break; + default: + throw new IllegalArgumentException("Invalid columnIndex: " + columnIndex); + } + } + + public TransportOrdersDocument toXmlDocument() { + TransportOrdersDocument result = new TransportOrdersDocument(); + + for (TransportOrderData curData : transportOrderDataList) { + result.getTransportOrders().add( + new TransportOrderEntry( + curData.getDeadline(), + curData.getDriveOrders(), + curData.getIntendedVehicle() == null ? null : curData.getIntendedVehicle().getName(), + curData.getProperties() + ) + ); + } + + return result; + } + + /** + * Returns the list containing all <code>TransportOrderData</code>. + * + * @return The list containing all data + */ + public List<TransportOrderData> getList() { + return transportOrderDataList; + } +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/batchcreator/ExplicitOrderBatchGenerator.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/batchcreator/ExplicitOrderBatchGenerator.java new file mode 100644 index 0000000..3bcc1b6 --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/batchcreator/ExplicitOrderBatchGenerator.java @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator.batchcreator; + +import static java.util.Objects.requireNonNull; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.to.order.DestinationCreationTO; +import org.opentcs.access.to.order.TransportOrderCreationTO; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.components.kernel.services.TransportOrderService; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.guing.plugins.panels.loadgenerator.DriveOrderStructure; +import org.opentcs.guing.plugins.panels.loadgenerator.TransportOrderData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A batch generator for creating explicit transport orders. + */ +public class ExplicitOrderBatchGenerator + implements + OrderBatchCreator { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ExplicitOrderBatchGenerator.class); + /** + * The transport order service we talk to. + */ + private final TransportOrderService transportOrderService; + /** + * The dispatcher service. + */ + private final DispatcherService dispatcherService; + /** + * The TransportOrderData we're building the transport orders from. + */ + private final List<TransportOrderData> data; + + /** + * Creates a new ExplicitOrderBatchGenerator. + * + * @param transportOrderService The portal. + * @param dispatcherService The dispatcher service. + * @param data The transport order data. + */ + public ExplicitOrderBatchGenerator( + TransportOrderService transportOrderService, + DispatcherService dispatcherService, + List<TransportOrderData> data + ) { + this.transportOrderService = requireNonNull(transportOrderService, "transportOrderService"); + this.dispatcherService = requireNonNull(dispatcherService, "dispatcherService"); + this.data = requireNonNull(data, "data"); + } + + @Override + public Set<TransportOrder> createOrderBatch() + throws KernelRuntimeException { + Set<TransportOrder> createdOrders = new HashSet<>(); + for (TransportOrderData curData : data) { + createdOrders.add(createSingleOrder(curData)); + } + + dispatcherService.dispatch(); + + return createdOrders; + } + + private TransportOrder createSingleOrder(TransportOrderData curData) + throws KernelRuntimeException { + TransportOrder newOrder = transportOrderService.createTransportOrder( + new TransportOrderCreationTO( + "TOrder-", + createDestinations(curData.getDriveOrders()) + ) + .withIncompleteName(true) + .withDeadline(Instant.now().plusSeconds(curData.getDeadline().getTime() / 1000)) + .withIntendedVehicleName( + curData.getIntendedVehicle() == null + ? null + : curData.getIntendedVehicle().getName() + ) + .withProperties(curData.getProperties()) + ); + + return newOrder; + } + + private List<DestinationCreationTO> createDestinations(List<DriveOrderStructure> structures) { + List<DestinationCreationTO> result = new ArrayList<>(); + for (DriveOrderStructure currentOrder : structures) { + result.add( + new DestinationCreationTO( + currentOrder.getDriveOrderLocation().getName(), + currentOrder.getDriveOrderVehicleOperation() + ) + ); + } + return result; + } +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/batchcreator/OrderBatchCreator.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/batchcreator/OrderBatchCreator.java new file mode 100644 index 0000000..f921eb7 --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/batchcreator/OrderBatchCreator.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator.batchcreator; + +import java.util.Set; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.data.order.TransportOrder; + +/** + * Declares the methods of transport order batch creators. + */ +public interface OrderBatchCreator { + + /** + * Creates a new transport order batch. + * + * @return The created transport orders + * @throws KernelRuntimeException In case the kernel threw an exception when + * creating the transport orders. + */ + Set<TransportOrder> createOrderBatch() + throws KernelRuntimeException; +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/batchcreator/RandomOrderBatchCreator.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/batchcreator/RandomOrderBatchCreator.java new file mode 100644 index 0000000..08966a1 --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/batchcreator/RandomOrderBatchCreator.java @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator.batchcreator; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.access.to.order.DestinationCreationTO; +import org.opentcs.access.to.order.TransportOrderCreationTO; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.components.kernel.services.TransportOrderService; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.order.DriveOrder.Destination; +import org.opentcs.data.order.TransportOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Randomly creates batches of transport orders. + * Destinations and operations chosen are random and not guaranteed to work in + * a real plant. + */ +public class RandomOrderBatchCreator + implements + OrderBatchCreator { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(RandomOrderBatchCreator.class); + /** + * The transport order sergice we talk to. + */ + private final TransportOrderService transportOrderService; + /** + * The dispatcher service. + */ + private final DispatcherService dispatcherService; + /** + * The number of transport orders per batch. + */ + private final int batchSize; + /** + * The number of drive orders per transport order. + */ + private final int orderSize; + /** + * The locations in the model. + */ + private final List<Location> locations; + /** + * A random number generator for selecting locations and operations. + */ + private final Random random = new Random(); + + /** + * Creates a new RandomOrderBatchCreator. + * + * @param transportOrderService The transport order service. + * @param dispatcherService The dispatcher service. + * @param batchSize The number of transport orders per batch. + * @param orderSize The number of drive orders per transport order. + */ + public RandomOrderBatchCreator( + TransportOrderService transportOrderService, + DispatcherService dispatcherService, + int batchSize, + int orderSize + ) { + this.transportOrderService = requireNonNull(transportOrderService, "transportOrderService"); + this.dispatcherService = requireNonNull(dispatcherService, "dispatcherService"); + this.batchSize = batchSize; + this.orderSize = orderSize; + this.locations = initializeLocations(); + } + + @Override + public Set<TransportOrder> createOrderBatch() + throws KernelRuntimeException { + Set<TransportOrder> createdOrders = new HashSet<>(); + + if (this.locations.isEmpty()) { + LOG.info("Could not find suitable destination locations"); + return createdOrders; + } + for (int i = 0; i < batchSize; i++) { + createdOrders.add(createSingleOrder()); + } + + dispatcherService.dispatch(); + + return createdOrders; + } + + private TransportOrder createSingleOrder() + throws KernelRuntimeException { + List<DestinationCreationTO> dests = new ArrayList<>(); + for (int j = 0; j < orderSize; j++) { + Location destLoc = this.locations.get(random.nextInt(this.locations.size())); + dests.add(new DestinationCreationTO(destLoc.getName(), Destination.OP_NOP)); + } + return transportOrderService.createTransportOrder( + new TransportOrderCreationTO("TOrder-", dests).withIncompleteName(true) + ); + } + + private List<Location> initializeLocations() { + Set<TCSObjectReference<LocationType>> suitableLocationTypeRefs + = transportOrderService.fetchObjects(LocationType.class) + .stream() + .filter(locationType -> locationType.isAllowedOperation(Destination.OP_NOP)) + .map(TCSObject::getReference) + .collect(Collectors.toSet()); + + return transportOrderService.fetchObjects(Location.class) + .stream() + .filter(location -> !location.getAttachedLinks().isEmpty()) + .filter(location -> suitableLocationTypeRefs.contains(location.getType())) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/package-info.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/package-info.java new file mode 100644 index 0000000..ab1dab6 --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/package-info.java @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +/** + * A plugin panel providing a basic load generator. + */ +package org.opentcs.guing.plugins.panels.loadgenerator; diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/trigger/OrderGenerationTrigger.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/trigger/OrderGenerationTrigger.java new file mode 100644 index 0000000..ff9bcca --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/trigger/OrderGenerationTrigger.java @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator.trigger; + +import org.opentcs.access.KernelRuntimeException; + +/** + * Declares the methods of transport order generation triggers. + */ +public interface OrderGenerationTrigger { + + /** + * Enables or disabled order generation. + * + * @param enabled true to enable, false to disable + */ + void setTriggeringEnabled(boolean enabled); + + /** + * Triggers order generation. + * + * @throws KernelRuntimeException In case the kernel threw an exception when + * creating the transport orders. + */ + void triggerOrderGeneration() + throws KernelRuntimeException; +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/trigger/SingleOrderGenTrigger.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/trigger/SingleOrderGenTrigger.java new file mode 100644 index 0000000..dd7e688 --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/trigger/SingleOrderGenTrigger.java @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator.trigger; + +import java.util.Objects; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.guing.plugins.panels.loadgenerator.batchcreator.OrderBatchCreator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Triggers creation of transport orders only once. + */ +public class SingleOrderGenTrigger + implements + OrderGenerationTrigger { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(SingleOrderGenTrigger.class); + /** + * The instance actually creating the new orders. + */ + private final OrderBatchCreator orderBatchCreator; + + /** + * Creates a new SingleOrderGenTrigger. + * + * @param orderBatchCreator The order batch creator + */ + public SingleOrderGenTrigger(final OrderBatchCreator orderBatchCreator) { + this.orderBatchCreator = Objects.requireNonNull( + orderBatchCreator, + "orderBatchCreator is null" + ); + } + + @Override + public void setTriggeringEnabled(boolean enabled) { + if (enabled) { + triggerOrderGeneration(); + } + } + + @Override + public void triggerOrderGeneration() + throws KernelRuntimeException { + try { + if (orderBatchCreator != null) { + orderBatchCreator.createOrderBatch(); + } + } + catch (KernelRuntimeException exc) { + LOG.warn("Exception triggering order generation, terminating triggering", exc); + } + } +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/trigger/ThresholdOrderGenTrigger.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/trigger/ThresholdOrderGenTrigger.java new file mode 100644 index 0000000..e22967f --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/trigger/ThresholdOrderGenTrigger.java @@ -0,0 +1,149 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator.trigger; + +import static java.util.Objects.requireNonNull; + +import java.util.LinkedHashSet; +import java.util.Set; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.guing.plugins.panels.loadgenerator.batchcreator.OrderBatchCreator; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.event.EventSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Triggers creation of a batch of orders if the number of transport orders + * in progress drop to or below a given threshold. + */ +public class ThresholdOrderGenTrigger + implements + EventHandler, + OrderGenerationTrigger { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ThresholdOrderGenTrigger.class); + /** + * Where we get events from. + */ + private final EventSource eventSource; + /** + * The object service we talk to. + */ + private final TCSObjectService objectService; + /** + * The orders that we know are in the system. + */ + private final Set<TransportOrder> knownOrders = new LinkedHashSet<>(); + /** + * The threshold for order generation. If the number of orders "in progress" + * drops to or below this number, a batch of new orders is generated. + */ + private final int threshold; + /** + * The instance actually creating the new orders. + */ + private final OrderBatchCreator orderBatchCreator; + + /** + * Creates a new instance. + * + * @param eventSource Where this instance registers for events. + * @param objectService The object service. + * @param threshold The threshold when new order are being created + * @param orderBatchCreator The order batch creator + */ + public ThresholdOrderGenTrigger( + final @ApplicationEventBus EventSource eventSource, + final TCSObjectService objectService, + final int threshold, + final OrderBatchCreator orderBatchCreator + ) { + this.eventSource = requireNonNull(eventSource, "eventSource"); + this.objectService = requireNonNull(objectService, "objectService"); + this.threshold = threshold; + this.orderBatchCreator = requireNonNull(orderBatchCreator, "orderBatchCreator"); + } + + @Override + public void setTriggeringEnabled(boolean enabled) { + synchronized (knownOrders) { + if (enabled) { + // Remember all orders that are not finished, failed etc. + for (TransportOrder curOrder : objectService.fetchObjects(TransportOrder.class)) { + if (!curOrder.getState().isFinalState()) { + knownOrders.add(curOrder); + } + } + eventSource.subscribe(this); + if (knownOrders.size() <= threshold) { + triggerOrderGeneration(); + } + } + else { + eventSource.unsubscribe(this); + knownOrders.clear(); + } + } + } + + @Override + public void triggerOrderGeneration() + throws KernelRuntimeException { + knownOrders.addAll(orderBatchCreator.createOrderBatch()); + } + + @Override + public void onEvent(Object event) { + if (!(event instanceof TCSObjectEvent)) { + return; + } + TCSObjectEvent objEvent = (TCSObjectEvent) event; + if (!(objEvent.getCurrentOrPreviousObjectState() instanceof TransportOrder)) { + return; + } + + synchronized (knownOrders) { + TransportOrder eventOrder + = (TransportOrder) objEvent.getCurrentOrPreviousObjectState(); + // If a new order was created, add it to the set of known orders. + if (TCSObjectEvent.Type.OBJECT_CREATED.equals(objEvent.getType())) { + knownOrders.add(eventOrder); + } + // If an order was removed, remove it here, too. + else if (TCSObjectEvent.Type.OBJECT_REMOVED.equals(objEvent.getType())) { + knownOrders.remove(eventOrder); + } + // If an order was modified, check if it's NOT "in progress". If it's not, + // i.e. if it's now finished, failed etc., remove it here, too. + else if (eventOrder.getState().isFinalState()) { + knownOrders.remove(eventOrder); + } + // Now let's check if the number of orders "in progress" has dropped below + // the threshold. If so, create a new batch of orders. + if (knownOrders.size() <= threshold) { + LOG.debug("orderCount = " + knownOrders.size() + ", triggering..."); + trigger(); + } + } + } + + private void trigger() { + try { + triggerOrderGeneration(); + } + catch (KernelRuntimeException exc) { + LOG.warn("Exception triggering order generation, terminating triggering", exc); + setTriggeringEnabled(false); + + } + } + +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/trigger/TimeoutOrderGenTrigger.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/trigger/TimeoutOrderGenTrigger.java new file mode 100644 index 0000000..508ca13 --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/trigger/TimeoutOrderGenTrigger.java @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator.trigger; + +import java.util.Objects; +import org.opentcs.access.KernelRuntimeException; +import org.opentcs.guing.plugins.panels.loadgenerator.batchcreator.OrderBatchCreator; +import org.opentcs.util.CyclicTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Triggers creation of a batch of orders after a given timeout. + */ +public class TimeoutOrderGenTrigger + implements + OrderGenerationTrigger { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(TimeoutOrderGenTrigger.class); + /** + * The timeout after which to trigger (in ms). + */ + private final int timeout; + /** + * The instance actually creating the new orders. + */ + private final OrderBatchCreator orderBatchCreator; + /** + * The actual task triggering order generation. + */ + private volatile TriggerTask triggerTask; + + /** + * Creates a new TimeoutOrderGenTrigger. + * + * @param timeout The timeout after which to trigger (in ms). + * @param orderBatchCreator The order batch creator + */ + public TimeoutOrderGenTrigger( + final int timeout, + final OrderBatchCreator orderBatchCreator + ) { + this.timeout = timeout; + this.orderBatchCreator = Objects.requireNonNull( + orderBatchCreator, + "orderBatchCreator is null" + ); + } + + @Override + public void setTriggeringEnabled(boolean enabled) { + if (enabled) { + triggerTask = new TriggerTask(timeout); + Thread triggerThread = new Thread(triggerTask, "triggerTask"); + triggerThread.start(); + } + else { + if (triggerTask != null) { + triggerTask.terminate(); + triggerTask = null; + } + } + } + + @Override + public void triggerOrderGeneration() + throws KernelRuntimeException { + orderBatchCreator.createOrderBatch(); + } + + /** + * A task that repeatedly triggers order generation after a given timeout. + */ + private final class TriggerTask + extends + CyclicTask { + + /** + * Creates a new instance. + * + * @param timeout The timeout after which to trigger order generation. + */ + private TriggerTask(int timeout) { + super(timeout); + } + + @Override + protected void runActualTask() { + try { + triggerOrderGeneration(); + } + catch (KernelRuntimeException exc) { + LOG.warn("Exception triggering order generation, terminating trigger task", exc); + this.terminate(); + } + } + } +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/xmlbinding/DriveOrderEntry.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/xmlbinding/DriveOrderEntry.java new file mode 100644 index 0000000..aa1becd --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/xmlbinding/DriveOrderEntry.java @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator.xmlbinding; + +import static java.util.Objects.requireNonNull; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlType; + +/** + * Stores a drive order definition for XML marshalling/unmarshalling. + */ +@XmlType(propOrder = {"locationName", "vehicleOperation"}) +public class DriveOrderEntry { + + /** + * The name of this drive order's location. + */ + private String locationName; + /** + * The operation to be executed at the location. + */ + private String vehicleOperation; + + /** + * Creates a new instance. + * + * @param locationName The reference to the drive order's location. + * @param vehicleOperation The available operation type at the location. + */ + public DriveOrderEntry(String locationName, String vehicleOperation) { + this.locationName = requireNonNull(locationName, "locationName"); + this.vehicleOperation = requireNonNull(vehicleOperation, "vehicleOperation"); + } + + /** + * Creates a new instance. + */ + public DriveOrderEntry() { + } + + /** + * Returns a reference to the location. + * + * @return The reference to the location. + */ + @XmlAttribute(name = "locationName", required = true) + public String getLocationName() { + return locationName; + } + + /** + * Sets the drive order location. + * + * @param locationName The new location. + */ + public void setLocationName(String locationName) { + this.locationName = locationName; + } + + /** + * Returns this drive order's operation. + * + * @return The drive order's operation. + */ + @XmlAttribute(name = "vehicleOperation", required = true) + public String getVehicleOperation() { + return this.vehicleOperation; + } + + /** + * Sets the drive order vehicle operation. + * + * @param vehicleOperation The new operation + */ + public void setVehicleOperation(String vehicleOperation) { + this.vehicleOperation = vehicleOperation; + } +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/xmlbinding/TransportOrderEntry.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/xmlbinding/TransportOrderEntry.java new file mode 100644 index 0000000..608eb3d --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/xmlbinding/TransportOrderEntry.java @@ -0,0 +1,236 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator.xmlbinding; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; +import org.opentcs.guing.plugins.panels.loadgenerator.DriveOrderStructure; +import org.opentcs.guing.plugins.panels.loadgenerator.TransportOrderData; + +/** + * Stores a transport order definition for XML marshalling/unmarshalling. + */ +@XmlType(propOrder = {"properties", "driveOrders", "deadline", "intendedVehicle"}) +public class TransportOrderEntry { + + /** + * The transport order's deadline. + */ + private Deadline deadline = Deadline.PLUS_TWO_HOURS; + /** + * The drive orders the transport order consists of. + */ + private final List<DriveOrderEntry> driveOrders = new ArrayList<>(); + /** + * The name of the vehicle intended to process the transport order. + */ + private String intendedVehicle; + /** + * Properties of the transport order data. + */ + private List<XMLMapEntry> properties = new ArrayList<>(); + + /** + * Creates a new instance. + * + * @param deadline The deadline of this transport order + * @param driveOrders A list containing the drive orders + * @param intendedVehicle The intended vehicle for this transport order + * @param properties A map containing the properties of this transport order + */ + public TransportOrderEntry( + TransportOrderData.Deadline deadline, + List<DriveOrderStructure> driveOrders, + String intendedVehicle, + Map<String, String> properties + ) { + switch (deadline) { + case MINUS_FIVE_MINUTES: + this.deadline = Deadline.MINUS_FIVE_MINUTES; + break; + case PLUS_FIVE_MINUTES: + this.deadline = Deadline.PLUS_FIVE_MINUTES; + break; + case MINUS_HALF_HOUR: + this.deadline = Deadline.MINUS_HALF_HOUR; + break; + case PLUS_HALF_HOUR: + this.deadline = Deadline.PLUS_HALF_HOUR; + break; + case PLUS_ONE_HOUR: + this.deadline = Deadline.PLUS_ONE_HOUR; + break; + case PLUS_TWO_HOURS: + default: + this.deadline = Deadline.PLUS_TWO_HOURS; + break; + } + this.intendedVehicle = intendedVehicle; + for (Map.Entry<String, String> curEntry : properties.entrySet()) { + this.properties.add( + new XMLMapEntry(curEntry.getKey(), curEntry.getValue()) + ); + } + for (DriveOrderStructure curDOS : driveOrders) { + this.driveOrders.add( + new DriveOrderEntry( + curDOS.getDriveOrderLocation().getName(), + curDOS.getDriveOrderVehicleOperation() + ) + ); + } + } + + /** + * Creates a new instance. + */ + public TransportOrderEntry() { + } + + /** + * Returns the properties of this transport order. + * + * @return The properties + */ + @XmlElement(name = "property", required = true) + //@XmlJavaTypeAdapter(MapAdapter.class) + public List<XMLMapEntry> getProperties() { + return properties; + } + + /** + * Returns the list of drive orders. + * + * @return The list of drive orders. + */ + @XmlElement(name = "driveOrder", required = true) + public List<DriveOrderEntry> getDriveOrders() { + return driveOrders; + } + + /** + * Returns a deadline. + * + * @return The deadline. + */ + @XmlAttribute(name = "deadline", required = true) + public Deadline getDeadline() { + return this.deadline; + } + + /** + * Sets the transport order's deadline. + * + * @param deadline The new deadline. + */ + public void setDeadline(Deadline deadline) { + this.deadline = requireNonNull(deadline, "deadline"); + } + + /** + * Returns a reference to the vehicle intended to process the order. + * + * @return A reference to the vehicle intended to process the order. + */ + @XmlAttribute(name = "intendedVehicle", required = true) + public String getIntendedVehicle() { + return intendedVehicle; + } + + /** + * Sets a reference to the vehicle intended to process the order. + * + * @param vehicle A reference to the vehicle intended to process the order. + */ + public void setIntendedVehicle(String vehicle) { + intendedVehicle = vehicle; + } + + /** + * The enumeration of possible default deadline values. + */ + public enum Deadline { + + /** + * The deadline value is five minutes in the past. + */ + MINUS_FIVE_MINUTES, + /** + * The deadline value is five minutes in the future. + */ + PLUS_FIVE_MINUTES, + /** + * The deadline value is a half hour in the past. + */ + MINUS_HALF_HOUR, + /** + * The deadline value is a half hour in the future. + */ + PLUS_HALF_HOUR, + /** + * The deadline value is a one hour in the future. + */ + PLUS_ONE_HOUR, + /** + * The deadline value is two hours in the future. + */ + PLUS_TWO_HOURS; + } + + /** + * A class to marshal the map of properties. + */ + @XmlType + public static class XMLMapEntry { + + /** + * The key. + */ + private String key; + /** + * The value. + */ + private String value; + + /** + * Creates an empty XMLMalEntry. + */ + public XMLMapEntry() { + } + + /** + * Creates a new XMLMapEntry. + * + * @param key The key + * @param value The value + */ + public XMLMapEntry(String key, String value) { + this.key = key; + this.value = value; + } + + @XmlAttribute + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @XmlAttribute + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/xmlbinding/TransportOrdersDocument.java b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/xmlbinding/TransportOrdersDocument.java new file mode 100644 index 0000000..e320be8 --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/java/org/opentcs/guing/plugins/panels/loadgenerator/xmlbinding/TransportOrdersDocument.java @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator.xmlbinding; + +import static java.util.Objects.requireNonNull; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + */ +@XmlRootElement(name = "transportOrders") +@XmlType(propOrder = {"transportOrders"}) +public class TransportOrdersDocument { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(TransportOrdersDocument.class); + + /** + * The transport orders. + */ + private final List<TransportOrderEntry> transportOrders = new ArrayList<>(); + + /** + * Creates a new instance. + */ + public TransportOrdersDocument() { + } + + @XmlElement(name = "transportOrder") + public List<TransportOrderEntry> getTransportOrders() { + return transportOrders; + } + + /** + * Marshals the data. + * + * @return Data as XML string + */ + public String toXml() { + StringWriter stringWriter = new StringWriter(); + try { + JAXBContext jc = JAXBContext.newInstance(TransportOrdersDocument.class); + Marshaller marshaller = jc.createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + marshaller.marshal(this, stringWriter); + } + catch (JAXBException exc) { + LOG.warn("Exception marshalling data", exc); + throw new IllegalStateException("Exception marshalling data", exc); + } + return stringWriter.toString(); + } + + /** + * Writes the file. + * + * @param file The file to write + * @throws IOException If an exception occured while writing + */ + public void toFile(File file) + throws IOException { + requireNonNull(file, "file"); + + try (OutputStream outStream = new FileOutputStream(file)) { + outStream.write(toXml().getBytes()); + outStream.flush(); + } + } + + /** + * Reads a list of <code>TransportOrderXMLStructure</code>s from an XML file. + * + * @param xmlData The XML data + * @return The list of data + */ + public static TransportOrdersDocument fromXml(String xmlData) { + requireNonNull(xmlData, "xmlData"); + + StringReader stringReader = new StringReader(xmlData); + try { + JAXBContext jc = JAXBContext.newInstance(TransportOrdersDocument.class); + Unmarshaller unmarshaller = jc.createUnmarshaller(); + Object o = unmarshaller.unmarshal(stringReader); + return (TransportOrdersDocument) o; + } + catch (JAXBException exc) { + LOG.warn("Exception unmarshalling data", exc); + throw new IllegalStateException("Exception unmarshalling data", exc); + } + } + + /** + * Reads a list of <code>TransportOrderXMLStructure</code>s from a file. + * + * @param sourceFile The file + * @return The list of data + * @throws IOException If an exception occured while reading + */ + public static TransportOrdersDocument fromFile(File sourceFile) + throws IOException { + requireNonNull(sourceFile, "sourceFile"); + + final String path = sourceFile.getAbsolutePath(); + if (!sourceFile.isFile() || !sourceFile.canRead()) { + throw new IOException(path + ": file not a regular file or unreadable"); + } + int fileSize = (int) sourceFile.length(); + byte[] buffer = new byte[fileSize]; + try (InputStream inStream = new FileInputStream(sourceFile)) { + int bytesRead = inStream.read(buffer); + if (bytesRead != fileSize) { + throw new IOException( + "read() returned unexpected value: " + bytesRead + ", should be :" + fileSize + ); + } + } + String fileContent = new String(buffer); + return fromXml(fileContent); + } + +} diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/resources/i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties b/opentcs-plantoverview-panel-loadgenerator/src/main/resources/i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties new file mode 100644 index 0000000..8fe9f64 --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/resources/i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle.properties @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +continuousLoadPanel.accessibleName=Continuous load +continuousLoadPanel.button_addDriveOrder.text=New drive order +continuousLoadPanel.button_addProperty.text=Add property +continuousLoadPanel.button_addTransportOrder.text=Add new order +continuousLoadPanel.button_addTransportOrder.tooltipText=Add empty transport order +continuousLoadPanel.button_deleteSelectedDriveOrder.text=Delete selected +continuousLoadPanel.button_deleteSelectedDriveOrder.tooltipText=Remove selected drive order +continuousLoadPanel.button_deleteSelectedOrder.text=Delete selected +continuousLoadPanel.button_deleteSelectedOrder.tooltipText=Remove selected transport order +continuousLoadPanel.button_open.text=Open +continuousLoadPanel.button_removeProperty.text=Remove property +continuousLoadPanel.button_save.text=Save +continuousLoadPanel.checkBox_enableOrderGeneration.text=Enable order generation +continuousLoadPanel.comboBox_locations.tooltipText=Available locations +continuousLoadPanel.comboBox_operationTypes.tooltipText=Allowed operations +continuousLoadPanel.label_unitDriveOrdersPerOrder.text=drive orders per transport order +continuousLoadPanel.label_unitOrdersAtATime.text=orders at a time, +continuousLoadPanel.label_unitOrdersToBeProcessed.text=orders to be processed +continuousLoadPanel.label_unitSeconds.text=seconds +continuousLoadPanel.optionPane_driveOrderEmpty.message=Every transport order must have at least one drive order. +continuousLoadPanel.optionPane_driveOrderEmpty.title=Drive orders missing +continuousLoadPanel.optionPane_driveOrderIncorrect.message=Every drive order must have a location and an operation. +continuousLoadPanel.optionPane_driveOrderIncorrect.title=Drive orders incorrect +continuousLoadPanel.optionPane_fileDoesNotExist.message=The chosen input file does not exist. +continuousLoadPanel.optionPane_fileDoesNotExist.title=File does not exist +continuousLoadPanel.optionPane_overwriteFileConfirmation.message=A file with the chosen name already exists, do you want to overwrite it? +continuousLoadPanel.optionPane_overwriteFileConfirmation.title=File exists +continuousLoadPanel.panel_driveOrders.border.title=Drive orders +continuousLoadPanel.panel_generateTrigger.border.title=Trigger for generating orders +continuousLoadPanel.panel_orderGeneration.border.title=Order generation +continuousLoadPanel.panel_orderProfile.border.title=Order profile +continuousLoadPanel.panel_properties.border.title=Properties +continuousLoadPanel.panel_transportOrderModelling.border.title=Transport order modelling +continuousLoadPanel.panel_transportOrders.border.title=Transport orders +continuousLoadPanel.radioButton_createOrdersAccordingDefinition.text=Create orders according to definition +continuousLoadPanel.radioButton_createOrdersRandomly.text=Create orders randomly: +continuousLoadPanel.radioButton_triggerAfterTimeout.text=After a timeout of +continuousLoadPanel.radioButton_triggerByOrderThreshold.text=If there are no more than +continuousLoadPanel.radioButton_triggerOnce.text=Create orders only once +continuousLoadPanel.tab_driveOrders.title=Drive orders +continuousLoadPanel.tab_properties.title=Properties +continuousLoadPanel.table_driveOrders.tooltipText=Drive orders in selected transport order +continuousLoadPanelFactory.panelDescription=Continuous load +driveOrderTableModel.column_location.headerText=Location +driveOrderTableModel.column_operation.headerText=Operation +propertyTableModel.column_key.headerText=Key +propertyTableModel.column_value.headerText=Value +transportOrderTableModel.column_deadline.headerText=Deadline +transportOrderTableModel.column_vehicle.headerText=Vehicle diff --git a/opentcs-plantoverview-panel-loadgenerator/src/main/resources/i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle_de.properties b/opentcs-plantoverview-panel-loadgenerator/src/main/resources/i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle_de.properties new file mode 100644 index 0000000..b47392b --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/main/resources/i18n/org/opentcs/plantoverview/loadGeneratorPanel/Bundle_de.properties @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +continuousLoadPanel.accessibleName=Kontinuierliche Last +continuousLoadPanel.button_addDriveOrder.text=Neuer Fahrauftrag +continuousLoadPanel.button_addProperty.text=Hinzuf\u00fcgen +continuousLoadPanel.button_addTransportOrder.text=Transportauftrag hinzuf\u00fcgen +continuousLoadPanel.button_addTransportOrder.tooltipText=Leeren Transportauftrag hinzuf\u00fcgen +continuousLoadPanel.button_deleteSelectedDriveOrder.tooltipText=Ausgew\u00e4hlten Fahrauftrag l\u00f6schen +continuousLoadPanel.button_deleteSelectedOrder.text=Auswahl l\u00f6schen +continuousLoadPanel.button_deleteSelectedOrder.tooltipText=Ausgew\u00e4hlten Transportauftrag l\u00f6schen +continuousLoadPanel.button_open.text=\u00d6ffnen +continuousLoadPanel.button_removeProperty.text=Entfernen +continuousLoadPanel.button_save.text=Speichern +continuousLoadPanel.checkBox_enableOrderGeneration.text=Auftragsgenerator einschalten +continuousLoadPanel.comboBox_locations.tooltipText=Verf\u00fcgbare Orte +continuousLoadPanel.comboBox_operationTypes.tooltipText=Erlaubte Operationen +continuousLoadPanel.label_unitDriveOrdersPerOrder.text=Fahrauftr\u00e4ge pro Transportauftrag +continuousLoadPanel.label_unitOrdersAtATime.text=Auftr\u00e4ge auf einmal, +continuousLoadPanel.label_unitOrdersToBeProcessed.text=zu bearbeitende Auftr\u00e4ge +continuousLoadPanel.label_unitSeconds.text=Sekunden +continuousLoadPanel.optionPane_driveOrderEmpty.message=Jeder Transportauftrag muss wenigstens einen Fahrauftrag besitzen. +continuousLoadPanel.optionPane_driveOrderEmpty.title=Fahrauftr\u00e4ge fehlen +continuousLoadPanel.optionPane_driveOrderIncorrect.message=Jeder Fahrauftrag muss eine Station und eine Operation besitzen\n. +continuousLoadPanel.optionPane_driveOrderIncorrect.title=Fahrauftr\u00e4ge fehlerhaft +continuousLoadPanel.optionPane_fileDoesNotExist.message=Die ausgew\u00e4hlte Datei existiert nicht. +continuousLoadPanel.optionPane_fileDoesNotExist.title=Datei existiert nicht +continuousLoadPanel.optionPane_overwriteFileConfirmation.message=Eine Datei mit dem gew\u00e4hlten Namen existiert bereits, wollen Sie sie \u00fcberschreiben? +continuousLoadPanel.optionPane_overwriteFileConfirmation.title=Datei existiert +continuousLoadPanel.panel_driveOrders.border.title=Fahrauftr\u00e4ge +continuousLoadPanel.panel_generateTrigger.border.title=Ausl\u00f6ser f\u00fcr Auftragsgenerierung +continuousLoadPanel.panel_orderGeneration.border.title=Auftragsgenerierung +continuousLoadPanel.panel_orderProfile.border.title=Auftragsprofil +continuousLoadPanel.panel_transportOrderModelling.border.title=Transportauftragsmodellierung +continuousLoadPanel.panel_transportOrders.border.title=Transportauftr\u00e4ge +continuousLoadPanel.radioButton_createOrdersAccordingDefinition.text=Auftr\u00e4ge nach Vorgabe erzeugen +continuousLoadPanel.radioButton_createOrdersRandomly.text=Auftr\u00e4ge zuf\u00e4llig erzeugen: +continuousLoadPanel.radioButton_triggerAfterTimeout.text=Nach einer Wartezeit von +continuousLoadPanel.radioButton_triggerByOrderThreshold.text=Falls nicht mehr als +continuousLoadPanel.radioButton_triggerOnce.text=Auftr\u00e4ge nur einmal erzeugen +continuousLoadPanel.tab_driveOrders.title=Fahrauftr\u00e4ge +continuousLoadPanel.tab_properties.title=Eigenschaften +continuousLoadPanel.table_driveOrders.tooltipText=Fahrauftr\u00e4ge im ausgew\u00e4hlten Transportauftrag +driveOrderTableModel.column_location.headerText=Station +driveOrderTableModel.column_operation.headerText=Operation +propertyTableModel.column_key.headerText=Schl\u00fcssel +propertyTableModel.column_value.headerText=Wert +transportOrderTableModel.column_deadline.headerText=Frist +transportOrderTableModel.column_vehicle.headerText=Fahrzeug diff --git a/opentcs-plantoverview-panel-loadgenerator/src/test/java/org/opentcs/guing/plugins/panels/loadgenerator/batchcreator/RandomOrderBatchCreatorTest.java b/opentcs-plantoverview-panel-loadgenerator/src/test/java/org/opentcs/guing/plugins/panels/loadgenerator/batchcreator/RandomOrderBatchCreatorTest.java new file mode 100644 index 0000000..56531a8 --- /dev/null +++ b/opentcs-plantoverview-panel-loadgenerator/src/test/java/org/opentcs/guing/plugins/panels/loadgenerator/batchcreator/RandomOrderBatchCreatorTest.java @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.loadgenerator.batchcreator; + +import static java.util.UUID.randomUUID; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.access.to.order.TransportOrderCreationTO; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.components.kernel.services.TransportOrderService; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Point; +import org.opentcs.data.order.TransportOrder; + +/** + * Tests for {@link RandomOrderBatchCreator}. + */ +class RandomOrderBatchCreatorTest { + + private final Point point = new Point("point1"); + private final LocationType unsuitableLocType = new LocationType("unsuitableLocType") + .withAllowedOperations(List.of("park")); + private final LocationType suitableLocType = new LocationType("suitableLocType") + .withAllowedOperations(List.of("park", "NOP")); + private final Location unsuitableLoc = new Location( + "unsuitableLoc", + unsuitableLocType.getReference() + ); + private final Location suitableLoc = new Location( + "suitableLoc", + suitableLocType.getReference() + ); + private DispatcherService dispatcherService; + private TransportOrderService transportOrderService; + + @BeforeEach + void setUp() { + dispatcherService = mock(DispatcherService.class); + transportOrderService = mock(TransportOrderService.class); + } + + @Test + void givenUnsuitableTypeWithoutLinkThenCreateNoOrders() { + when(transportOrderService.fetchObjects(LocationType.class)) + .thenReturn(Set.of(unsuitableLocType)); + when(transportOrderService.fetchObjects(Location.class)) + .thenReturn(Set.of(unsuitableLoc)); + RandomOrderBatchCreator batchCreator = new RandomOrderBatchCreator(transportOrderService, + dispatcherService, + 10, + 3); + + Set<TransportOrder> result = batchCreator.createOrderBatch(); + + assertThat(result, is(empty())); + verify(transportOrderService, never()) + .createTransportOrder(any(TransportOrderCreationTO.class)); + } + + @Test + void givenSuitableTypeWithoutLinkThenCreateNoOrders() { + when(transportOrderService.fetchObjects(LocationType.class)) + .thenReturn(Set.of(suitableLocType)); + when(transportOrderService.fetchObjects(Location.class)) + .thenReturn(Set.of(suitableLoc)); + RandomOrderBatchCreator batchCreator = new RandomOrderBatchCreator(transportOrderService, + dispatcherService, + 10, + 3); + + Set<TransportOrder> result = batchCreator.createOrderBatch(); + + assertThat(result, is(empty())); + verify(transportOrderService, never()) + .createTransportOrder(any(TransportOrderCreationTO.class)); + } + + @Test + void givenUnsuitableTypeWithLinkThenCreateNoOrders() { + Location.Link unsuitableLink = new Location.Link( + unsuitableLoc.getReference(), + point.getReference() + ); + when(transportOrderService.fetchObjects(LocationType.class)) + .thenReturn(Set.of(unsuitableLocType)); + when(transportOrderService.fetchObjects(Location.class)) + .thenReturn(Set.of(unsuitableLoc.withAttachedLinks(Set.of(unsuitableLink)))); + RandomOrderBatchCreator batchCreator = new RandomOrderBatchCreator( + transportOrderService, + dispatcherService, + 10, + 3 + ); + + Set<TransportOrder> result = batchCreator.createOrderBatch(); + + assertThat(result, is(empty())); + verify(transportOrderService, never()) + .createTransportOrder(any(TransportOrderCreationTO.class)); + } + + @Test + void givenSuitableTypeWithLinkThenCreateOrders() { + Location.Link suitableLink = new Location.Link( + suitableLoc.getReference(), + point.getReference() + ); + when(transportOrderService.fetchObjects(LocationType.class)) + .thenReturn(Set.of(suitableLocType)); + when(transportOrderService.fetchObjects(Location.class)) + .thenReturn(Set.of(suitableLoc.withAttachedLinks(Set.of(suitableLink)))); + when(transportOrderService.createTransportOrder(any(TransportOrderCreationTO.class))) + .thenAnswer( + invocation -> new TransportOrder( + randomUUID().toString(), + List.of() + ) + ); + RandomOrderBatchCreator batchCreator = new RandomOrderBatchCreator( + transportOrderService, + dispatcherService, + 10, + 3 + ); + + Set<TransportOrder> result = batchCreator.createOrderBatch(); + + assertThat(result, hasSize(10)); + verify(transportOrderService, times(10)) + .createTransportOrder(any(TransportOrderCreationTO.class)); + } +} diff --git a/opentcs-plantoverview-panel-resourceallocation/build.gradle b/opentcs-plantoverview-panel-resourceallocation/build.gradle new file mode 100644 index 0000000..f6741e7 --- /dev/null +++ b/opentcs-plantoverview-panel-resourceallocation/build.gradle @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply from: "${rootDir}/gradle/java-project.gradle" +apply from: "${rootDir}/gradle/java-codequality.gradle" +apply from: "${rootDir}/gradle/guice-project.gradle" +apply from: "${rootDir}/gradle/publishing-java.gradle" + +dependencies { + api project(':opentcs-api-injection') + api project(':opentcs-common') +} + +task release { + dependsOn build +} diff --git a/opentcs-plantoverview-panel-resourceallocation/gradle.properties b/opentcs-plantoverview-panel-resourceallocation/gradle.properties new file mode 100644 index 0000000..14431ac --- /dev/null +++ b/opentcs-plantoverview-panel-resourceallocation/gradle.properties @@ -0,0 +1,40 @@ +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineMethodParams=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAfterDotInChainedMethodCalls=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineDisjunctiveCatchTypes=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineFor=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineImplements=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.sortMembersByVisibility=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.visibilityOrder=PUBLIC;PROTECTED;DEFAULT;PRIVATE +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.enable-indent=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineArrayInit=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineCallArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.keepGettersAndSettersTogether=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classMembersOrder=STATIC FIELD;FIELD;STATIC_INIT;CONSTRUCTOR;INSTANCE_INIT;STATIC METHOD;METHOD;STATIC CLASS;CLASS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapCommentText=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.importGroupsOrder=* +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize=4 +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineAnnotationArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineTryResources=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.preserveNewLinesInComments=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineParenthesized=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineThrows=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=100 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project diff --git a/opentcs-plantoverview-panel-resourceallocation/src/guiceConfig/java/org/opentcs/guing/plugins/panels/allocation/AllocationPanelModule.java b/opentcs-plantoverview-panel-resourceallocation/src/guiceConfig/java/org/opentcs/guing/plugins/panels/allocation/AllocationPanelModule.java new file mode 100644 index 0000000..93418f8 --- /dev/null +++ b/opentcs-plantoverview-panel-resourceallocation/src/guiceConfig/java/org/opentcs/guing/plugins/panels/allocation/AllocationPanelModule.java @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.allocation; + +import org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Configures the resource allocation panel. + */ +public class AllocationPanelModule + extends + PlantOverviewInjectionModule { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(AllocationPanelModule.class); + + /** + * Creates a new instance. + */ + public AllocationPanelModule() { + } + + @Override + protected void configure() { + ResourceAllocationPanelConfiguration configuration + = getConfigBindingProvider().get( + ResourceAllocationPanelConfiguration.PREFIX, + ResourceAllocationPanelConfiguration.class + ); + + if (!configuration.enable()) { + LOG.info("Resource allocation panel disabled by configuration."); + return; + } + + pluggablePanelFactoryBinder().addBinding().to(ResourceAllocationPanelFactory.class); + } +} diff --git a/opentcs-plantoverview-panel-resourceallocation/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule b/opentcs-plantoverview-panel-resourceallocation/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule new file mode 100644 index 0000000..290efbd --- /dev/null +++ b/opentcs-plantoverview-panel-resourceallocation/src/guiceConfig/resources/META-INF/services/org.opentcs.customizations.plantoverview.PlantOverviewInjectionModule @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: MIT + +org.opentcs.guing.plugins.panels.allocation.AllocationPanelModule diff --git a/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/AllocationTreeCellRenderer.java b/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/AllocationTreeCellRenderer.java new file mode 100644 index 0000000..0851e4d --- /dev/null +++ b/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/AllocationTreeCellRenderer.java @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.allocation; + +import static java.util.Objects.requireNonNull; + +import java.awt.Component; +import java.net.URL; +import javax.swing.ImageIcon; +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResourceReference; + +/** + * Renders the tree nodes with vehicle, point and path icons. + */ +public class AllocationTreeCellRenderer + extends + DefaultTreeCellRenderer { + + /** + * The icon for vehicles in the tree view. + */ + private final ImageIcon vehicleIcon; + /** + * The icon for points in the tree view. + */ + private final ImageIcon pointIcon; + /** + * The icon for paths in the tree view. + */ + private final ImageIcon pathIcon; + + /** + * Creates a new instance. + */ + public AllocationTreeCellRenderer() { + vehicleIcon = iconByFullPath( + "/org/opentcs/guing/plugins/panels/allocation/symbols/vehicle.18x18.png" + ); + pointIcon = iconByFullPath( + "/org/opentcs/guing/plugins/panels/allocation/symbols/point.18x18.png" + ); + pathIcon = iconByFullPath( + "/org/opentcs/guing/plugins/panels/allocation/symbols/path.18x18.png" + ); + } + + @Override + public Component getTreeCellRendererComponent( + JTree tree, Object node, boolean selected, + boolean expanded, boolean isLeaf, int row, + boolean hasFocus + ) { + //Let the superclass handle all its stuff related to rendering + super.getTreeCellRendererComponent(tree, node, selected, expanded, isLeaf, row, hasFocus); + if (node instanceof DefaultMutableTreeNode) { + DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) node; + //User object is of type string only if the node contains a vehicle or is the root node + if (treeNode.getUserObject() instanceof String) { + setIcon(vehicleIcon); + } + //User object is of type TCSResource only if the node contains a path or a point + else if (treeNode.getUserObject() instanceof TCSResourceReference) { + TCSResourceReference<?> resource = (TCSResourceReference<?>) treeNode.getUserObject(); + setText(resource.getName()); + if (resource.getReferentClass() == Path.class) { + setIcon(pathIcon); + } + else if (resource.getReferentClass() == Point.class) { + setIcon(pointIcon); + } + } + } + return this; + } + + /** + * Creates an ImageIcon. + * + * @param fullPath The full (absolute) path of the icon file. + * @return The icon, or <code>null</code>, if the file does not exist. + */ + private ImageIcon iconByFullPath(String fullPath) { + requireNonNull(fullPath, "fullPath"); + + URL url = getClass().getResource(fullPath); + + if (url == null) { + throw new IllegalArgumentException("Icon file not found: " + fullPath); + } + return new ImageIcon(url); + } + +} diff --git a/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/AllocationTreeModel.java b/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/AllocationTreeModel.java new file mode 100644 index 0000000..f6de527 --- /dev/null +++ b/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/AllocationTreeModel.java @@ -0,0 +1,178 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.allocation; + +import static org.opentcs.guing.plugins.panels.allocation.I18nPlantOverviewPanelResourceAllocation.BUNDLE_PATH; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.ResourceBundle; +import java.util.stream.Collectors; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.MutableTreeNode; +import org.opentcs.data.model.TCSResourceReference; + +/** + * A model for a resource allocation tree to display an alphabetically ordered view for vehicle + * names and their not ordered allocated resources. + */ +public class AllocationTreeModel + extends + DefaultTreeModel { + + /** + * This class' bundle. + */ + private static final ResourceBundle BUNDLE = ResourceBundle.getBundle(BUNDLE_PATH); + + /** + * Creates a new instance. + */ + public AllocationTreeModel() { + super( + new DefaultMutableTreeNode(BUNDLE.getString("resourceAllocationPanel.treeRoot.text")), + true + ); + } + + /** + * Updates the vehicle resource allocations displayed in this tree model. + * + * @param vehicleName The name of the vehicle + * @param newAllocations The new vehicle resource allocations + */ + public void updateAllocations(String vehicleName, List<TCSResourceReference<?>> newAllocations) { + updateVehicleAllocation(vehicleName, newAllocations); + removeNotAllocatedVehicles(vehicleName, newAllocations); + } + + /** + * Removes all vehicle tree nodes where the vehicle does not have any resources allocated. + * + * @param allocatedVehicles The vehicles which have a resource allocation + */ + private void removeNotAllocatedVehicles( + String vehicleName, + List<TCSResourceReference<?>> resources + ) { + @SuppressWarnings("unchecked") + List<DefaultMutableTreeNode> rootChildren + = Collections.list((Enumeration<DefaultMutableTreeNode>) root.children()); + + for (DefaultMutableTreeNode currentNode : rootChildren) { + Object userObject = currentNode.getUserObject(); + //If we have a vehicle node but the vehicle name is not in the set, remove it + if (userObject.equals(vehicleName) && resources.isEmpty()) { + removeNodeFromParent(currentNode); + } + } + } + + /** + * Updates the allocated resources for a specified vehicle if necessarry. + * + * @param vehicleName The name of the vehicle + * @param resources The allocated resources of the vehicle + */ + private void updateVehicleAllocation( + String vehicleName, + List<TCSResourceReference<?>> resources + ) { + DefaultMutableTreeNode vehicleNode = null; + for (int x = 0; x < root.getChildCount(); x++) { + DefaultMutableTreeNode currentNode = (DefaultMutableTreeNode) root.getChildAt(x); + if (currentNode.getUserObject().equals(vehicleName)) { + vehicleNode = currentNode; + } + } + if (vehicleNode == null) { + vehicleNode = createNewVehicleNode(vehicleName); + } + //Remove all children that are not in the new allocation + List<DefaultMutableTreeNode> vehicleChildren = Collections.list(vehicleNode.children()).stream() + .map(treeNode -> (DefaultMutableTreeNode) treeNode) + .collect(Collectors.toList()); + for (DefaultMutableTreeNode current : vehicleChildren) { + if (!resources.contains((TCSResourceReference<?>) current.getUserObject())) { + vehicleNode.remove(current); + } + } + //Add new resources that are not in the jtree already at the correct position + int index = 0; + for (TCSResourceReference<?> resource : resources) { + if (vehicleNode.getChildCount() <= index) { + //Insert the resource at this position + vehicleNode.insert(new DefaultMutableTreeNode(resource, false), index); + } + else { + DefaultMutableTreeNode current = (DefaultMutableTreeNode) vehicleNode.getChildAt(index); + TCSResourceReference<?> resource2 = (TCSResourceReference<?>) current.getUserObject(); + //Check if the resource exists at the current position - then we dont have to do anything + if (!resource.equals(resource2)) { + //Check if the resource exists at another position in the children list + int existIndex = getChildIndexOf(resource, vehicleNode); + //If the resource already exists at another point, move it to the index + if (existIndex > 0) { + vehicleNode.remove(existIndex); + } + //Insert the resource at this position + vehicleNode.insert(new DefaultMutableTreeNode(resource, false), index); + } + } + index++; + } + + reload(vehicleNode); + } + + /** + * Returns the index of the first children containing the resource as user object. + * + * @param resource The resource to search for + * @param vehicleNode The parent node + * @return The index of the node containing the resource or -1 if not found + */ + private int getChildIndexOf( + TCSResourceReference<?> resource, + DefaultMutableTreeNode vehicleNode + ) { + int index = 0; + + List<DefaultMutableTreeNode> vehicleChildren = Collections.list(vehicleNode.children()).stream() + .map(treeNode -> (DefaultMutableTreeNode) treeNode) + .collect(Collectors.toList()); + + for (DefaultMutableTreeNode child : vehicleChildren) { + if (child.getUserObject().equals(resource)) { + return index; + } + index++; + } + return -1; + } + + /** + * Creates a new vehicle node and adds it to the root node in alphabetical order. + * + * @param vehicleName The name of the vehicle and the user object of the new vehicle node + * @return The vehicle node + */ + private DefaultMutableTreeNode createNewVehicleNode(String vehicleName) { + boolean inserted = false; + DefaultMutableTreeNode vehicleNode = new DefaultMutableTreeNode(vehicleName); + for (int x = 0; x < root.getChildCount(); x++) { + DefaultMutableTreeNode currentNode = (DefaultMutableTreeNode) root.getChildAt(x); + //Insert node alphabetically + if (!inserted && vehicleName.compareTo((String) currentNode.getUserObject()) <= 0) { + insertNodeInto(vehicleNode, (MutableTreeNode) root, x); + inserted = true; + } + } + if (!inserted) { + insertNodeInto(vehicleNode, (MutableTreeNode) root, root.getChildCount()); + } + return vehicleNode; + } +} diff --git a/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/I18nPlantOverviewPanelResourceAllocation.java b/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/I18nPlantOverviewPanelResourceAllocation.java new file mode 100644 index 0000000..39297fa --- /dev/null +++ b/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/I18nPlantOverviewPanelResourceAllocation.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.allocation; + +/** + * Defines constants regarding internationalization. + */ +public interface I18nPlantOverviewPanelResourceAllocation { + + /** + * The path to the project's resource bundle. + */ + String BUNDLE_PATH = "i18n/org/opentcs/plantoverview/resourceAllocationPanel/Bundle"; +} diff --git a/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/ResourceAllocationPanel.form b/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/ResourceAllocationPanel.form new file mode 100644 index 0000000..68390b5 --- /dev/null +++ b/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/ResourceAllocationPanel.form @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.3" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,44,0,0,1,-112"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/> + <SubComponents> + <Container class="javax.swing.JPanel" name="optionsPanel"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription"> + <BorderConstraints direction="First"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Component class="javax.swing.JCheckBox" name="enableUpdatesCheckbox"> + <Properties> + <Property name="selected" type="boolean" value="true"/> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="i18n/org/opentcs/plantoverview/resourceAllocationPanel/Bundle.properties" key="resourceAllocationPanel.checkBox_enableUpdates.text" replaceFormat="java.util.ResourceBundle.getBundle("{bundleNameSlashes}").getString("{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="enableUpdatesCheckboxActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="-1" gridY="-1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> + </Container> + <Container class="javax.swing.JScrollPane" name="allocationScrollPane"> + <AuxValues> + <AuxValue name="autoScrollPane" type="java.lang.Boolean" value="true"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription"> + <BorderConstraints direction="Center"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JTree" name="allocationTable"> + <Properties> + <Property name="model" type="javax.swing.tree.TreeModel" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> + <Connection code="new AllocationTreeModel()" type="code"/> + </Property> + <Property name="cellRenderer" type="javax.swing.tree.TreeCellRenderer" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> + <Connection code="new AllocationTreeCellRenderer()" type="code"/> + </Property> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="4"/> + </AuxValues> + </Component> + </SubComponents> + </Container> + </SubComponents> +</Form> diff --git a/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/ResourceAllocationPanel.java b/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/ResourceAllocationPanel.java new file mode 100644 index 0000000..c386bb9 --- /dev/null +++ b/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/ResourceAllocationPanel.java @@ -0,0 +1,243 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.allocation; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.swing.SwingUtilities; +import org.opentcs.access.SharedKernelServicePortal; +import org.opentcs.access.SharedKernelServicePortalProvider; +import org.opentcs.components.kernel.services.ServiceUnavailableException; +import org.opentcs.components.plantoverview.PluggablePanel; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.util.event.EventHandler; +import org.opentcs.util.event.EventSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A panel to display the allocated resources of each vehicle with atleast one allocation. + */ +public class ResourceAllocationPanel + extends + PluggablePanel + implements + EventHandler { + + /** + * This class' logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ResourceAllocationPanel.class); + /** + * The kernel to query allocations from. + */ + private final SharedKernelServicePortalProvider portalProvider; + /** + * Where we register for events. + */ + private final EventSource eventSource; + /** + * The client that is registered with the kernel provider. + */ + private SharedKernelServicePortal sharedPortal; + /** + * Whether this panel was initialized. + */ + private boolean initialized; + /** + * If the table model should update its contents if an event arrives. + */ + private boolean enableUpdates = true; + + /** + * Creates a new instance. + * + * @param kernelProvider The kernel provider. + * @param eventSource Where this instance registers for events. + */ + @Inject + @SuppressWarnings("this-escape") + public ResourceAllocationPanel( + SharedKernelServicePortalProvider kernelProvider, + @ApplicationEventBus + EventSource eventSource + ) { + this.portalProvider = requireNonNull(kernelProvider, "kernelProvider"); + this.eventSource = requireNonNull(eventSource, "eventSource"); + initComponents(); + } + + @Override + public void initialize() { + if (isInitialized()) { + LOG.debug("Already initialized - skipping."); + return; + } + // Register event listener in the kernel. + try { + sharedPortal = portalProvider.register(); + } + catch (ServiceUnavailableException exc) { + LOG.warn("Kernel unavailable", exc); + return; + } + + eventSource.subscribe(this); + + // Trigger an update to the table model. + updateAllVehicleAllocations(); + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + LOG.debug("Already terminated - skipping."); + return; + } + // Remove event listener in the kernel. + eventSource.unsubscribe(this); + sharedPortal.close(); + + initialized = false; + } + + @Override + public void onEvent(Object event) { + requireNonNull(event, "event"); + + //Skip event if we dont want any updates + if (!enableUpdates) { + return; + } + //Skip non object events as were only interested in vehicle updates + if (!(event instanceof TCSObjectEvent)) { + LOG.debug("Event is not a TCSObjectEvent, ignoring."); + return; + } + //Skip non vehicle events + TCSObjectEvent tcsObjectEvent = (TCSObjectEvent) event; + if (!(tcsObjectEvent.getCurrentOrPreviousObjectState() instanceof Vehicle)) { + LOG.debug("TCSObjectEvent is not about a Vehicle, ignoring."); + return; + } + + SwingUtilities.invokeLater( + () -> handleVehicleStateChange((Vehicle) tcsObjectEvent.getCurrentOrPreviousObjectState()) + ); + } + + private void updateAllVehicleAllocations() { + SwingUtilities.invokeLater( + () -> sharedPortal.getPortal().getVehicleService().fetchObjects(Vehicle.class).stream() + .forEach(this::handleVehicleStateChange) + ); + } + + /** + * Handles a vehicle update. + * Queries the kernel for the resource allocations of all vehicles and updates the table model. + * + * @param vehicle The vehicle which changed + */ + private void handleVehicleStateChange(Vehicle vehicle) { + ((AllocationTreeModel) allocationTable.getModel()).updateAllocations( + vehicle.getName(), + vehicle.getAllocatedResources().stream() + .flatMap(set -> inClassOrder(set).stream()) + .collect(Collectors.toList()) + ); + } + + private List<TCSResourceReference<?>> inClassOrder(Set<TCSResourceReference<?>> resources) { + List<TCSResourceReference<?>> result = new ArrayList<>(); + List<TCSResourceReference<?>> points = new ArrayList<>(); + List<TCSResourceReference<?>> locations = new ArrayList<>(); + resources.forEach(resource -> { + if (resource.getReferentClass() == Path.class) { + result.add(resource); + } + else if (resource.getReferentClass() == Point.class) { + points.add(resource); + } + else if (resource.getReferentClass() == Location.class) { + locations.add(resource); + } + }); + result.addAll(points); + result.addAll(locations); + return result; + } + + // FORMATTER:OFF + // CHECKSTYLE:OFF + /** + * This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + optionsPanel = new javax.swing.JPanel(); + enableUpdatesCheckbox = new javax.swing.JCheckBox(); + allocationScrollPane = new javax.swing.JScrollPane(); + allocationTable = new javax.swing.JTree(); + + setLayout(new java.awt.BorderLayout()); + + optionsPanel.setLayout(new java.awt.GridBagLayout()); + + enableUpdatesCheckbox.setSelected(true); + java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/resourceAllocationPanel/Bundle"); // NOI18N + enableUpdatesCheckbox.setText(bundle.getString("resourceAllocationPanel.checkBox_enableUpdates.text")); // NOI18N + enableUpdatesCheckbox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + enableUpdatesCheckboxActionPerformed(evt); + } + }); + optionsPanel.add(enableUpdatesCheckbox, new java.awt.GridBagConstraints()); + + add(optionsPanel, java.awt.BorderLayout.PAGE_START); + + allocationTable.setModel(new AllocationTreeModel()); + allocationTable.setCellRenderer(new AllocationTreeCellRenderer()); + allocationScrollPane.setViewportView(allocationTable); + + add(allocationScrollPane, java.awt.BorderLayout.CENTER); + }// </editor-fold>//GEN-END:initComponents + // CHECKSTYLE:ON + // FORMATTER:ON + + private void enableUpdatesCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_enableUpdatesCheckboxActionPerformed + enableUpdates = enableUpdatesCheckbox.isSelected(); + }//GEN-LAST:event_enableUpdatesCheckboxActionPerformed + + // FORMATTER:OFF + // CHECKSTYLE:OFF + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JScrollPane allocationScrollPane; + protected javax.swing.JTree allocationTable; + private javax.swing.JCheckBox enableUpdatesCheckbox; + private javax.swing.JPanel optionsPanel; + // End of variables declaration//GEN-END:variables + // CHECKSTYLE:ON + // FORMATTER:ON +} diff --git a/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/ResourceAllocationPanelConfiguration.java b/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/ResourceAllocationPanelConfiguration.java new file mode 100644 index 0000000..698fec4 --- /dev/null +++ b/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/ResourceAllocationPanelConfiguration.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.allocation; + +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; + +/** + * Provides methods to configure the continuous load panel. + */ +@ConfigurationPrefix(ResourceAllocationPanelConfiguration.PREFIX) +public interface ResourceAllocationPanelConfiguration { + + /** + * This configuration's prefix. + */ + String PREFIX = "resourceallocationpanel"; + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to enable to register/enable the resource allocation panel.", + orderKey = "0_enable" + ) + boolean enable(); +} diff --git a/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/ResourceAllocationPanelFactory.java b/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/ResourceAllocationPanelFactory.java new file mode 100644 index 0000000..2295b73 --- /dev/null +++ b/opentcs-plantoverview-panel-resourceallocation/src/main/java/org/opentcs/guing/plugins/panels/allocation/ResourceAllocationPanelFactory.java @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.panels.allocation; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.guing.plugins.panels.allocation.I18nPlantOverviewPanelResourceAllocation.BUNDLE_PATH; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import java.util.ResourceBundle; +import org.opentcs.access.Kernel; +import org.opentcs.components.plantoverview.PluggablePanel; +import org.opentcs.components.plantoverview.PluggablePanelFactory; + +/** + * Provides a {@link ResourceAllocationPanel} for the plant overview if the kernel is in operating + * state. + */ +public class ResourceAllocationPanelFactory + implements + PluggablePanelFactory { + + /** + * This class's bundle. + */ + private final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_PATH); + + /** + * The provider for the panel this factory wants to create. + */ + private final Provider<ResourceAllocationPanel> panelProvider; + + /** + * Creates a new instance. + * + * @param panelProvider the provider for the panel + */ + @Inject + public ResourceAllocationPanelFactory(Provider<ResourceAllocationPanel> panelProvider) { + this.panelProvider = requireNonNull(panelProvider, "panelProvider"); + } + + @Override + public boolean providesPanel(Kernel.State state) { + return (state == Kernel.State.OPERATING); + } + + @Override + public String getPanelDescription() { + return bundle.getString("resourceAllocationPanelFactory.panelDescription"); + } + + @Override + public PluggablePanel createPanel(Kernel.State state) { + if (!providesPanel(state)) { + return null; + } + + return panelProvider.get(); + } +} diff --git a/opentcs-plantoverview-panel-resourceallocation/src/main/resources/REUSE.toml b/opentcs-plantoverview-panel-resourceallocation/src/main/resources/REUSE.toml new file mode 100644 index 0000000..42aab58 --- /dev/null +++ b/opentcs-plantoverview-panel-resourceallocation/src/main/resources/REUSE.toml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC0-1.0 + +version = 1 + +[[annotations]] +path = ["**/*.gif", "**/*.jpg", "**/*.png", "**/*.svg"] +precedence = "closest" +SPDX-FileCopyrightText = "The openTCS Authors" +SPDX-License-Identifier = "CC-BY-4.0" diff --git a/opentcs-plantoverview-panel-resourceallocation/src/main/resources/i18n/org/opentcs/plantoverview/resourceAllocationPanel/Bundle.properties b/opentcs-plantoverview-panel-resourceallocation/src/main/resources/i18n/org/opentcs/plantoverview/resourceAllocationPanel/Bundle.properties new file mode 100644 index 0000000..5a5437e --- /dev/null +++ b/opentcs-plantoverview-panel-resourceallocation/src/main/resources/i18n/org/opentcs/plantoverview/resourceAllocationPanel/Bundle.properties @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +resourceAllocationPanel.checkBox_enableUpdates.text=Enable updates +resourceAllocationPanel.treeRoot.text=Vehicles +resourceAllocationPanelFactory.panelDescription=Resource allocation diff --git a/opentcs-plantoverview-panel-resourceallocation/src/main/resources/i18n/org/opentcs/plantoverview/resourceAllocationPanel/Bundle_de.properties b/opentcs-plantoverview-panel-resourceallocation/src/main/resources/i18n/org/opentcs/plantoverview/resourceAllocationPanel/Bundle_de.properties new file mode 100644 index 0000000..3a30982 --- /dev/null +++ b/opentcs-plantoverview-panel-resourceallocation/src/main/resources/i18n/org/opentcs/plantoverview/resourceAllocationPanel/Bundle_de.properties @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC-BY-4.0 + +resourceAllocationPanel.checkBox_enableUpdates.text=Aktualisierungen einschalten +resourceAllocationPanel.treeRoot.text=Fahrzeuge +resourceAllocationPanelFactory.panelDescription=Ressourcenzuweisung diff --git a/opentcs-plantoverview-panel-resourceallocation/src/main/resources/org/opentcs/guing/plugins/panels/allocation/symbols/path.18x18.png b/opentcs-plantoverview-panel-resourceallocation/src/main/resources/org/opentcs/guing/plugins/panels/allocation/symbols/path.18x18.png new file mode 100644 index 0000000..b6a1360 Binary files /dev/null and b/opentcs-plantoverview-panel-resourceallocation/src/main/resources/org/opentcs/guing/plugins/panels/allocation/symbols/path.18x18.png differ diff --git a/opentcs-plantoverview-panel-resourceallocation/src/main/resources/org/opentcs/guing/plugins/panels/allocation/symbols/point.18x18.png b/opentcs-plantoverview-panel-resourceallocation/src/main/resources/org/opentcs/guing/plugins/panels/allocation/symbols/point.18x18.png new file mode 100644 index 0000000..1900fd2 Binary files /dev/null and b/opentcs-plantoverview-panel-resourceallocation/src/main/resources/org/opentcs/guing/plugins/panels/allocation/symbols/point.18x18.png differ diff --git a/opentcs-plantoverview-panel-resourceallocation/src/main/resources/org/opentcs/guing/plugins/panels/allocation/symbols/vehicle.18x18.png b/opentcs-plantoverview-panel-resourceallocation/src/main/resources/org/opentcs/guing/plugins/panels/allocation/symbols/vehicle.18x18.png new file mode 100644 index 0000000..572dc7d Binary files /dev/null and b/opentcs-plantoverview-panel-resourceallocation/src/main/resources/org/opentcs/guing/plugins/panels/allocation/symbols/vehicle.18x18.png differ diff --git a/opentcs-plantoverview-themes-default/build.gradle b/opentcs-plantoverview-themes-default/build.gradle new file mode 100644 index 0000000..b2a2e9b --- /dev/null +++ b/opentcs-plantoverview-themes-default/build.gradle @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply from: "${rootDir}/gradle/java-project.gradle" +apply from: "${rootDir}/gradle/java-codequality.gradle" +apply from: "${rootDir}/gradle/publishing-java.gradle" + +dependencies { + api project(':opentcs-api-base') +} + +task release { + dependsOn build +} diff --git a/opentcs-plantoverview-themes-default/gradle.properties b/opentcs-plantoverview-themes-default/gradle.properties new file mode 100644 index 0000000..14431ac --- /dev/null +++ b/opentcs-plantoverview-themes-default/gradle.properties @@ -0,0 +1,40 @@ +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineMethodParams=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAfterDotInChainedMethodCalls=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineDisjunctiveCatchTypes=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineFor=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineImplements=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.sortMembersByVisibility=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.visibilityOrder=PUBLIC;PROTECTED;DEFAULT;PRIVATE +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.enable-indent=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineArrayInit=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineCallArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.keepGettersAndSettersTogether=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classMembersOrder=STATIC FIELD;FIELD;STATIC_INIT;CONSTRUCTOR;INSTANCE_INIT;STATIC METHOD;METHOD;STATIC CLASS;CLASS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapCommentText=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.importGroupsOrder=* +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize=4 +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineAnnotationArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineTryResources=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.preserveNewLinesInComments=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineParenthesized=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineThrows=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=100 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project diff --git a/opentcs-plantoverview-themes-default/src/main/java/org/opentcs/guing/plugins/themes/DefaultLocationTheme.java b/opentcs-plantoverview-themes-default/src/main/java/org/opentcs/guing/plugins/themes/DefaultLocationTheme.java new file mode 100644 index 0000000..1e92a85 --- /dev/null +++ b/opentcs-plantoverview-themes-default/src/main/java/org/opentcs/guing/plugins/themes/DefaultLocationTheme.java @@ -0,0 +1,171 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.themes; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.awt.Image; +import java.io.IOException; +import java.net.URL; +import java.util.EnumMap; +import java.util.Map; +import javax.imageio.ImageIO; +import org.opentcs.components.plantoverview.LocationTheme; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.visualization.LocationRepresentation; + +/** + * Default location theme implementation. + */ +public class DefaultLocationTheme + implements + LocationTheme { + + /** + * The path containing the images. + */ + private static final String PATH = "/org/opentcs/guing/plugins/themes/symbols/location/"; + /** + * The available symbols. + */ + private static final String[] LOCTYPE_REPRESENTATION_SYMBOLS + = { + "TransferStation.20x20.png", // 0 + "WorkingStation.20x20.png", // 1 + "ChargingStation.20x20.png", // 2 + "None.20x20.png", // 3 + }; + /** + * A map of property values to image file names. + */ + private final Map<LocationRepresentation, Image> symbolMap + = new EnumMap<>(LocationRepresentation.class); + + /** + * Creates a new instance. + */ + public DefaultLocationTheme() { + initSymbolMap(); + } + + @Override + @Nonnull + public Image getImageFor( + @Nonnull + LocationRepresentation representation + ) { + requireNonNull(representation, "representation"); + + return symbolMap.get(representation); + } + + @Override + @Nonnull + public Image getImageFor( + @Nonnull + Location location, + @Nonnull + LocationType locationType + ) { + requireNonNull(location, "location"); + requireNonNull(locationType, "locationType"); + + LocationRepresentation representation = location.getLayout().getLocationRepresentation(); + if (representation == null || representation == LocationRepresentation.DEFAULT) { + representation = locationType.getLayout().getLocationRepresentation(); + } + return getImageFor(representation); + } + + private void initSymbolMap() { + // NONE: A location without further description + symbolMap.put( + LocationRepresentation.NONE, + loadImageFromPath(LOCTYPE_REPRESENTATION_SYMBOLS[3]) + ); + + // LOAD_TRANSFER_GENERIC: A generic location for vehicle load transfers. + symbolMap.put( + LocationRepresentation.LOAD_TRANSFER_GENERIC, + loadImageFromPath(LOCTYPE_REPRESENTATION_SYMBOLS[0]) + ); + // LOAD_TRANSFER_ALT_1: A location for vehicle load transfers, variant 1. + symbolMap.put( + LocationRepresentation.LOAD_TRANSFER_ALT_1, + loadImageFromPath(LOCTYPE_REPRESENTATION_SYMBOLS[0]) + ); + // LOAD_TRANSFER_ALT_2: A location for vehicle load transfers, variant 2. + symbolMap.put( + LocationRepresentation.LOAD_TRANSFER_ALT_2, + loadImageFromPath(LOCTYPE_REPRESENTATION_SYMBOLS[0]) + ); + // LOAD_TRANSFER_ALT_3: A location for vehicle load transfers, variant 3. + symbolMap.put( + LocationRepresentation.LOAD_TRANSFER_ALT_3, + loadImageFromPath(LOCTYPE_REPRESENTATION_SYMBOLS[0]) + ); + // LOAD_TRANSFER_ALT_4: A location for vehicle load transfers, variant 4. + symbolMap.put( + LocationRepresentation.LOAD_TRANSFER_ALT_4, + loadImageFromPath(LOCTYPE_REPRESENTATION_SYMBOLS[0]) + ); + // LOAD_TRANSFER_ALT_5: A location for vehicle load transfers, variant 5. + symbolMap.put( + LocationRepresentation.LOAD_TRANSFER_ALT_5, + loadImageFromPath(LOCTYPE_REPRESENTATION_SYMBOLS[0]) + ); + + // WORKING_GENERIC: A location for some generic processing, generic variant. + symbolMap.put( + LocationRepresentation.WORKING_GENERIC, + loadImageFromPath(LOCTYPE_REPRESENTATION_SYMBOLS[1]) + ); + // WORKING_ALT_1: A location for some generic processing, variant 1. + symbolMap.put( + LocationRepresentation.WORKING_ALT_1, + loadImageFromPath(LOCTYPE_REPRESENTATION_SYMBOLS[1]) + ); + // WORKING_ALT_2: A location for some generic processing, variant 2. + symbolMap.put( + LocationRepresentation.WORKING_ALT_2, + loadImageFromPath(LOCTYPE_REPRESENTATION_SYMBOLS[1]) + ); + + // RECHARGE_GENERIC: A location for recharging a vehicle, generic variant. + symbolMap.put( + LocationRepresentation.RECHARGE_GENERIC, + loadImageFromPath(LOCTYPE_REPRESENTATION_SYMBOLS[2]) + ); + // RECHARGE_ALT_1: A location for recharging a vehicle, variant 1. + symbolMap.put( + LocationRepresentation.RECHARGE_ALT_1, + loadImageFromPath(LOCTYPE_REPRESENTATION_SYMBOLS[2]) + ); + // RECHARGE_ALT_2: A location for recharging a vehicle, variant 2. + symbolMap.put( + LocationRepresentation.RECHARGE_ALT_2, + loadImageFromPath(LOCTYPE_REPRESENTATION_SYMBOLS[2]) + ); + } + + private Image loadImageFromPath(String fileName) { + return loadImage(PATH + fileName); + } + + private Image loadImage(String fileName) { + requireNonNull(fileName, "fileName"); + + URL url = getClass().getResource(fileName); + if (url == null) { + throw new IllegalArgumentException("Invalid image file name " + fileName); + } + try { + return ImageIO.read(url); + } + catch (IOException exc) { + throw new IllegalArgumentException("Exception loading image", exc); + } + } +} diff --git a/opentcs-plantoverview-themes-default/src/main/java/org/opentcs/guing/plugins/themes/StatefulImageVehicleTheme.java b/opentcs-plantoverview-themes-default/src/main/java/org/opentcs/guing/plugins/themes/StatefulImageVehicleTheme.java new file mode 100644 index 0000000..0981791 --- /dev/null +++ b/opentcs-plantoverview-themes-default/src/main/java/org/opentcs/guing/plugins/themes/StatefulImageVehicleTheme.java @@ -0,0 +1,180 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.themes; + +import static java.util.Objects.requireNonNull; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Image; +import java.io.IOException; +import java.net.URL; +import java.util.EnumMap; +import java.util.Map; +import javax.imageio.ImageIO; +import org.opentcs.components.plantoverview.VehicleTheme; +import org.opentcs.data.model.Vehicle; + +/** + * An implementation of <code>VehicleTheme</code> using different images for different vehicle + * states. + */ +public class StatefulImageVehicleTheme + implements + VehicleTheme { + + /** + * The path containing the images. + */ + private static final String PATH = "/org/opentcs/guing/plugins/themes/symbols/vehicle/"; + /** + * The font to be used for labels. + */ + private static final Font LABEL_FONT = new Font("Arial", Font.BOLD, 12); + /** + * Map containing images for a specific vehicle state when it's in a default state. + */ + private final Map<Vehicle.State, Image> stateMapDefault + = new EnumMap<>(Vehicle.State.class); + /** + * Map containing images for a specific vehicle state when it's loaded. + */ + private final Map<Vehicle.State, Image> stateMapLoaded + = new EnumMap<>(Vehicle.State.class); + /** + * Map containing images for a specific vehicle state when it's paused. + */ + private final Map<Vehicle.State, Image> stateMapPaused + = new EnumMap<>(Vehicle.State.class); + /** + * Map containing images for a specific vehicle state when it's loaded and paused. + */ + private final Map<Vehicle.State, Image> stateMapLoadedPaused + = new EnumMap<>(Vehicle.State.class); + + /** + * Creates a new instance. + */ + public StatefulImageVehicleTheme() { + initMaps(); + } + + @Override + public Image statelessImage(Vehicle vehicle) { + requireNonNull(vehicle, "vehicle"); + + return stateMapDefault.get(Vehicle.State.IDLE); + } + + @Override + public Image statefulImage(Vehicle vehicle) { + requireNonNull(vehicle, "vehicle"); + + if (loaded(vehicle)) { + return vehicle.isPaused() + ? stateMapLoadedPaused.get(vehicle.getState()) + : stateMapLoaded.get(vehicle.getState()); + } + else { + return vehicle.isPaused() + ? stateMapPaused.get(vehicle.getState()) + : stateMapDefault.get(vehicle.getState()); + } + } + + @Override + public Font labelFont() { + return LABEL_FONT; + } + + @Override + public Color labelColor() { + return Color.BLUE; + } + + @Override + public int labelOffsetY() { + return 25; + } + + @Override + public int labelOffsetX() { + return -15; + } + + @Override + public String label(Vehicle vehicle) { + return vehicle.getName(); + } + + /** + * Initializes the maps with values. + */ + private void initMaps() { + stateMapDefault.put(Vehicle.State.CHARGING, loadImage(PATH + "charging.png")); + stateMapDefault.put(Vehicle.State.ERROR, loadImage(PATH + "error.png")); + stateMapDefault.put(Vehicle.State.EXECUTING, loadImage(PATH + "normal.png")); + stateMapDefault.put(Vehicle.State.IDLE, loadImage(PATH + "normal.png")); + stateMapDefault.put(Vehicle.State.UNAVAILABLE, loadImage(PATH + "normal.png")); + stateMapDefault.put(Vehicle.State.UNKNOWN, loadImage(PATH + "normal.png")); + + stateMapLoaded.put(Vehicle.State.CHARGING, loadImage(PATH + "charging_loaded.png")); + stateMapLoaded.put(Vehicle.State.ERROR, loadImage(PATH + "error_loaded.png")); + stateMapLoaded.put(Vehicle.State.EXECUTING, loadImage(PATH + "normal_loaded.png")); + stateMapLoaded.put(Vehicle.State.IDLE, loadImage(PATH + "normal_loaded.png")); + stateMapLoaded.put(Vehicle.State.UNAVAILABLE, loadImage(PATH + "normal_loaded.png")); + stateMapLoaded.put(Vehicle.State.UNKNOWN, loadImage(PATH + "normal_loaded.png")); + + stateMapPaused.put(Vehicle.State.CHARGING, loadImage(PATH + "charging_paused.png")); + stateMapPaused.put(Vehicle.State.ERROR, loadImage(PATH + "error_paused.png")); + stateMapPaused.put(Vehicle.State.EXECUTING, loadImage(PATH + "normal_paused.png")); + stateMapPaused.put(Vehicle.State.IDLE, loadImage(PATH + "normal_paused.png")); + stateMapPaused.put(Vehicle.State.UNAVAILABLE, loadImage(PATH + "normal_paused.png")); + stateMapPaused.put(Vehicle.State.UNKNOWN, loadImage(PATH + "normal_paused.png")); + + stateMapLoadedPaused.put( + Vehicle.State.CHARGING, + loadImage(PATH + "charging_loaded_paused.png") + ); + stateMapLoadedPaused.put(Vehicle.State.ERROR, loadImage(PATH + "error_loaded_paused.png")); + stateMapLoadedPaused.put(Vehicle.State.EXECUTING, loadImage(PATH + "normal_loaded_paused.png")); + stateMapLoadedPaused.put(Vehicle.State.IDLE, loadImage(PATH + "normal_loaded_paused.png")); + stateMapLoadedPaused.put( + Vehicle.State.UNAVAILABLE, + loadImage(PATH + "normal_loaded_paused.png") + ); + stateMapLoadedPaused.put(Vehicle.State.UNKNOWN, loadImage(PATH + "normal_loaded_paused.png")); + } + + /** + * Checks if a given vehicle is loaded. + * + * @param vehicle The vehicle. + * @return Flag indicating if it is loaded. + */ + private boolean loaded(Vehicle vehicle) { + return vehicle.getLoadHandlingDevices().stream() + .anyMatch(lhd -> lhd.isFull()); + } + + /** + * Loads an image from the file with the given name. + * + * @param fileName The name of the file from which to load the image. + * @return The image. + */ + private Image loadImage(String fileName) { + requireNonNull(fileName, "fileName"); + + URL url = getClass().getResource(fileName); + if (url == null) { + throw new IllegalArgumentException("Invalid image file name " + fileName); + } + try { + return ImageIO.read(url); + } + catch (IOException exc) { + throw new IllegalArgumentException("Exception loading image", exc); + } + } +} diff --git a/opentcs-plantoverview-themes-default/src/main/java/org/opentcs/guing/plugins/themes/StatelessImageVehicleTheme.java b/opentcs-plantoverview-themes-default/src/main/java/org/opentcs/guing/plugins/themes/StatelessImageVehicleTheme.java new file mode 100644 index 0000000..381c82d --- /dev/null +++ b/opentcs-plantoverview-themes-default/src/main/java/org/opentcs/guing/plugins/themes/StatelessImageVehicleTheme.java @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.guing.plugins.themes; + +import static java.util.Objects.requireNonNull; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Image; +import java.io.IOException; +import java.net.URL; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.imageio.ImageIO; +import org.opentcs.components.plantoverview.VehicleTheme; +import org.opentcs.data.model.Vehicle; + +/** + * An implementation of <code>VehicleTheme</code> using a single image, disregarding vehicles' + * states. + */ +public class StatelessImageVehicleTheme + implements + VehicleTheme { + + /** + * The path containing the images. + */ + private static final String PATH + = "/org/opentcs/guing/plugins/themes/symbols/vehicle/Vehicle24.png"; + /** + * The single image used for representing vehicles, regardless of their state. + */ + private final Image image; + + /** + * Creates a new instance. + */ + public StatelessImageVehicleTheme() { + this.image = loadImage(PATH); + } + + @Override + public Image statelessImage(Vehicle vehicle) { + return image; + } + + @Override + public Image statefulImage(Vehicle vehicle) { + return image; + } + + /** + * Loads an image from the file with the given name. + * + * @param fileName The name of the file from which to load the image. + * @return The image. + */ + private Image loadImage(String fileName) { + requireNonNull(fileName, "fileName"); + + URL url = getClass().getResource(fileName); + if (url == null) { + throw new IllegalArgumentException("Invalid image file name " + fileName); + } + try { + return ImageIO.read(url); + } + catch (IOException exc) { + throw new IllegalArgumentException("Exception loading image", exc); + } + } + + @Override + public String label(Vehicle vehicle) { + String name = vehicle.getName(); + + // Find digits. + Pattern p = Pattern.compile("\\d+"); + Matcher m = p.matcher(name); + + // If at least one group of digits was found, use the first one. + if (m.find()) { + return m.group(); + } + + return name; + } + + @Override + public int labelOffsetX() { + return -8; + } + + @Override + public int labelOffsetY() { + return 5; + } + + @Override + public Color labelColor() { + return Color.BLUE; + } + + @Override + public Font labelFont() { + return new Font("Arial", Font.BOLD, 12); + } +} diff --git a/opentcs-plantoverview-themes-default/src/main/resources/REUSE.toml b/opentcs-plantoverview-themes-default/src/main/resources/REUSE.toml new file mode 100644 index 0000000..42aab58 --- /dev/null +++ b/opentcs-plantoverview-themes-default/src/main/resources/REUSE.toml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: The openTCS Authors +# SPDX-License-Identifier: CC0-1.0 + +version = 1 + +[[annotations]] +path = ["**/*.gif", "**/*.jpg", "**/*.png", "**/*.svg"] +precedence = "closest" +SPDX-FileCopyrightText = "The openTCS Authors" +SPDX-License-Identifier = "CC-BY-4.0" diff --git a/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/location/ChargingStation.20x20.png b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/location/ChargingStation.20x20.png new file mode 100644 index 0000000..ba308f8 Binary files /dev/null and b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/location/ChargingStation.20x20.png differ diff --git a/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/location/None.20x20.png b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/location/None.20x20.png new file mode 100644 index 0000000..34d9ebb Binary files /dev/null and b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/location/None.20x20.png differ diff --git a/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/location/TransferStation.20x20.png b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/location/TransferStation.20x20.png new file mode 100644 index 0000000..9255af2 Binary files /dev/null and b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/location/TransferStation.20x20.png differ diff --git a/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/location/WorkingStation.20x20.png b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/location/WorkingStation.20x20.png new file mode 100644 index 0000000..b86a859 Binary files /dev/null and b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/location/WorkingStation.20x20.png differ diff --git a/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/Vehicle24.png b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/Vehicle24.png new file mode 100644 index 0000000..e90b6a5 Binary files /dev/null and b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/Vehicle24.png differ diff --git a/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/charging.png b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/charging.png new file mode 100644 index 0000000..434b1e4 Binary files /dev/null and b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/charging.png differ diff --git a/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/charging_loaded.png b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/charging_loaded.png new file mode 100644 index 0000000..ccfe5f5 Binary files /dev/null and b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/charging_loaded.png differ diff --git a/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/charging_loaded_paused.png b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/charging_loaded_paused.png new file mode 100644 index 0000000..069f54c Binary files /dev/null and b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/charging_loaded_paused.png differ diff --git a/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/charging_paused.png b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/charging_paused.png new file mode 100644 index 0000000..8156778 Binary files /dev/null and b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/charging_paused.png differ diff --git a/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/error.png b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/error.png new file mode 100644 index 0000000..925080c Binary files /dev/null and b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/error.png differ diff --git a/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/error_loaded.png b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/error_loaded.png new file mode 100644 index 0000000..41befac Binary files /dev/null and b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/error_loaded.png differ diff --git a/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/error_loaded_paused.png b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/error_loaded_paused.png new file mode 100644 index 0000000..9cea61a Binary files /dev/null and b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/error_loaded_paused.png differ diff --git a/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/error_paused.png b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/error_paused.png new file mode 100644 index 0000000..f303114 Binary files /dev/null and b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/error_paused.png differ diff --git a/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/normal.png b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/normal.png new file mode 100644 index 0000000..f8c32d2 Binary files /dev/null and b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/normal.png differ diff --git a/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/normal_loaded.png b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/normal_loaded.png new file mode 100644 index 0000000..77ad2fb Binary files /dev/null and b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/normal_loaded.png differ diff --git a/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/normal_loaded_paused.png b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/normal_loaded_paused.png new file mode 100644 index 0000000..b122eff Binary files /dev/null and b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/normal_loaded_paused.png differ diff --git a/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/normal_paused.png b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/normal_paused.png new file mode 100644 index 0000000..104f231 Binary files /dev/null and b/opentcs-plantoverview-themes-default/src/main/resources/org/opentcs/guing/plugins/themes/symbols/vehicle/normal_paused.png differ diff --git a/opentcs-strategies-default/build.gradle b/opentcs-strategies-default/build.gradle new file mode 100644 index 0000000..ca52778 --- /dev/null +++ b/opentcs-strategies-default/build.gradle @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +apply from: "${rootDir}/gradle/java-project.gradle" +apply from: "${rootDir}/gradle/java-codequality.gradle" +apply from: "${rootDir}/gradle/guice-project.gradle" +apply from: "${rootDir}/gradle/publishing-java.gradle" + +dependencies { + api project(':opentcs-api-injection') + api project(':opentcs-common') + + implementation group: 'org.jgrapht', name: 'jgrapht-core', version: '1.5.2' + implementation group: 'org.locationtech.jts', name: 'jts-core', version: '1.19.0' +} + +task release { + dependsOn build +} diff --git a/opentcs-strategies-default/gradle.properties b/opentcs-strategies-default/gradle.properties new file mode 100644 index 0000000..14431ac --- /dev/null +++ b/opentcs-strategies-default/gradle.properties @@ -0,0 +1,40 @@ +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAnnotationArgs=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineMethodParams=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAfterDotInChainedMethodCalls=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineDisjunctiveCatchTypes=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineFor=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineImplements=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapFor=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.sortMembersByVisibility=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.visibilityOrder=PUBLIC;PROTECTED;DEFAULT;PRIVATE +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeFinallyOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapMethodParams=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.enable-indent=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineArrayInit=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineCallArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapDisjunctiveCatchTypes=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.keepGettersAndSettersTogether=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsList=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapExtendsImplementsKeyword=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classMembersOrder=STATIC FIELD;FIELD;STATIC_INIT;CONSTRUCTOR;INSTANCE_INIT;STATIC METHOD;METHOD;STATIC CLASS;CLASS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapEnumConstants=WRAP_ALWAYS +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapCommentText=false +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapThrowsList=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.wrapAssert=WRAP_IF_LONG +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.importGroupsOrder=* +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.continuationIndentSize=4 +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeElseOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.placeCatchOnNewLine=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineAnnotationArgs=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineTryResources=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.preserveNewLinesInComments=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineParenthesized=true +netbeans.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.alignMultilineThrows=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=2 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=100 +netbeans.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +netbeans.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project diff --git a/opentcs-strategies-default/src/guiceConfig/java/org/opentcs/strategies/basic/dispatching/DefaultDispatcherModule.java b/opentcs-strategies-default/src/guiceConfig/java/org/opentcs/strategies/basic/dispatching/DefaultDispatcherModule.java new file mode 100644 index 0000000..2d1d654 --- /dev/null +++ b/opentcs-strategies-default/src/guiceConfig/java/org/opentcs/strategies/basic/dispatching/DefaultDispatcherModule.java @@ -0,0 +1,231 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching; + +import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.MapBinder; +import com.google.inject.multibindings.Multibinder; +import jakarta.inject.Singleton; +import java.util.Comparator; +import org.opentcs.customizations.kernel.KernelInjectionModule; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.phase.parking.DefaultParkingPositionSupplier; +import org.opentcs.strategies.basic.dispatching.phase.parking.ParkingPositionSupplier; +import org.opentcs.strategies.basic.dispatching.phase.recharging.DefaultRechargePositionSupplier; +import org.opentcs.strategies.basic.dispatching.phase.recharging.RechargePositionSupplier; +import org.opentcs.strategies.basic.dispatching.priorization.CompositeOrderCandidateComparator; +import org.opentcs.strategies.basic.dispatching.priorization.CompositeOrderComparator; +import org.opentcs.strategies.basic.dispatching.priorization.CompositeVehicleCandidateComparator; +import org.opentcs.strategies.basic.dispatching.priorization.CompositeVehicleComparator; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorByCompleteRoutingCosts; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorByDeadline; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorByEnergyLevel; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorByInitialRoutingCosts; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorByOrderAge; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorByOrderName; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorByVehicleName; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorDeadlineAtRiskFirst; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorIdleFirst; +import org.opentcs.strategies.basic.dispatching.priorization.transportorder.TransportOrderComparatorByAge; +import org.opentcs.strategies.basic.dispatching.priorization.transportorder.TransportOrderComparatorByDeadline; +import org.opentcs.strategies.basic.dispatching.priorization.transportorder.TransportOrderComparatorByName; +import org.opentcs.strategies.basic.dispatching.priorization.transportorder.TransportOrderComparatorDeadlineAtRiskFirst; +import org.opentcs.strategies.basic.dispatching.priorization.vehicle.VehicleComparatorByEnergyLevel; +import org.opentcs.strategies.basic.dispatching.priorization.vehicle.VehicleComparatorByName; +import org.opentcs.strategies.basic.dispatching.priorization.vehicle.VehicleComparatorIdleFirst; +import org.opentcs.strategies.basic.dispatching.rerouting.ForcedReroutingStrategy; +import org.opentcs.strategies.basic.dispatching.rerouting.RegularDriveOrderMerger; +import org.opentcs.strategies.basic.dispatching.rerouting.RegularReroutingStrategy; +import org.opentcs.strategies.basic.dispatching.rerouting.ReroutingStrategy; +import org.opentcs.strategies.basic.dispatching.selection.AssignmentCandidateSelectionFilter; +import org.opentcs.strategies.basic.dispatching.selection.ParkVehicleSelectionFilter; +import org.opentcs.strategies.basic.dispatching.selection.RechargeVehicleSelectionFilter; +import org.opentcs.strategies.basic.dispatching.selection.ReparkVehicleSelectionFilter; +import org.opentcs.strategies.basic.dispatching.selection.TransportOrderSelectionFilter; +import org.opentcs.strategies.basic.dispatching.selection.VehicleSelectionFilter; +import org.opentcs.strategies.basic.dispatching.selection.candidates.CompositeAssignmentCandidateSelectionFilter; +import org.opentcs.strategies.basic.dispatching.selection.candidates.IsProcessable; +import org.opentcs.strategies.basic.dispatching.selection.orders.CompositeTransportOrderSelectionFilter; +import org.opentcs.strategies.basic.dispatching.selection.orders.ContainsLockedTargetLocations; +import org.opentcs.strategies.basic.dispatching.selection.vehicles.CompositeParkVehicleSelectionFilter; +import org.opentcs.strategies.basic.dispatching.selection.vehicles.CompositeRechargeVehicleSelectionFilter; +import org.opentcs.strategies.basic.dispatching.selection.vehicles.CompositeReparkVehicleSelectionFilter; +import org.opentcs.strategies.basic.dispatching.selection.vehicles.CompositeVehicleSelectionFilter; +import org.opentcs.strategies.basic.dispatching.selection.vehicles.IsIdleAndDegraded; +import org.opentcs.strategies.basic.dispatching.selection.vehicles.IsParkable; +import org.opentcs.strategies.basic.dispatching.selection.vehicles.IsReparkable; + +/** + * Guice configuration for the default dispatcher. + */ +public class DefaultDispatcherModule + extends + KernelInjectionModule { + + /** + * Creates a new instance. + */ + public DefaultDispatcherModule() { + } + + @Override + protected void configure() { + configureDispatcherDependencies(); + bindDispatcher(DefaultDispatcher.class); + } + + private void configureDispatcherDependencies() { + Multibinder.newSetBinder(binder(), VehicleSelectionFilter.class); + Multibinder.newSetBinder(binder(), TransportOrderSelectionFilter.class) + .addBinding().to(ContainsLockedTargetLocations.class); + Multibinder.newSetBinder(binder(), ParkVehicleSelectionFilter.class) + .addBinding().to(IsParkable.class); + Multibinder.newSetBinder(binder(), ReparkVehicleSelectionFilter.class) + .addBinding().to(IsReparkable.class); + Multibinder.newSetBinder(binder(), RechargeVehicleSelectionFilter.class) + .addBinding().to(IsIdleAndDegraded.class); + Multibinder.newSetBinder(binder(), AssignmentCandidateSelectionFilter.class) + .addBinding().to(IsProcessable.class); + + bind(CompositeParkVehicleSelectionFilter.class) + .in(Singleton.class); + bind(CompositeReparkVehicleSelectionFilter.class) + .in(Singleton.class); + bind(CompositeRechargeVehicleSelectionFilter.class) + .in(Singleton.class); + bind(CompositeTransportOrderSelectionFilter.class) + .in(Singleton.class); + bind(CompositeVehicleSelectionFilter.class) + .in(Singleton.class); + bind(CompositeAssignmentCandidateSelectionFilter.class) + .in(Singleton.class); + + bind(DefaultDispatcherConfiguration.class) + .toInstance( + getConfigBindingProvider().get( + DefaultDispatcherConfiguration.PREFIX, + DefaultDispatcherConfiguration.class + ) + ); + + bind(OrderReservationPool.class) + .in(Singleton.class); + + bind(ParkingPositionSupplier.class) + .to(DefaultParkingPositionSupplier.class) + .in(Singleton.class); + bind(RechargePositionSupplier.class) + .to(DefaultRechargePositionSupplier.class) + .in(Singleton.class); + + MapBinder<String, Comparator<Vehicle>> vehicleComparatorBinder + = MapBinder.newMapBinder( + binder(), + new TypeLiteral<String>() { + }, + new TypeLiteral<Comparator<Vehicle>>() { + } + ); + vehicleComparatorBinder + .addBinding(VehicleComparatorByEnergyLevel.CONFIGURATION_KEY) + .to(VehicleComparatorByEnergyLevel.class); + vehicleComparatorBinder + .addBinding(VehicleComparatorByName.CONFIGURATION_KEY) + .to(VehicleComparatorByName.class); + vehicleComparatorBinder + .addBinding(VehicleComparatorIdleFirst.CONFIGURATION_KEY) + .to(VehicleComparatorIdleFirst.class); + + MapBinder<String, Comparator<TransportOrder>> orderComparatorBinder + = MapBinder.newMapBinder( + binder(), + new TypeLiteral<String>() { + }, + new TypeLiteral<Comparator<TransportOrder>>() { + } + ); + orderComparatorBinder + .addBinding(TransportOrderComparatorByAge.CONFIGURATION_KEY) + .to(TransportOrderComparatorByAge.class); + orderComparatorBinder + .addBinding(TransportOrderComparatorByDeadline.CONFIGURATION_KEY) + .to(TransportOrderComparatorByDeadline.class); + orderComparatorBinder + .addBinding(TransportOrderComparatorDeadlineAtRiskFirst.CONFIGURATION_KEY) + .to(TransportOrderComparatorDeadlineAtRiskFirst.class); + orderComparatorBinder + .addBinding(TransportOrderComparatorByName.CONFIGURATION_KEY) + .to(TransportOrderComparatorByName.class); + + MapBinder<String, Comparator<AssignmentCandidate>> candidateComparatorBinder + = MapBinder.newMapBinder( + binder(), + new TypeLiteral<String>() { + }, + new TypeLiteral<Comparator<AssignmentCandidate>>() { + } + ); + candidateComparatorBinder + .addBinding(CandidateComparatorByCompleteRoutingCosts.CONFIGURATION_KEY) + .to(CandidateComparatorByCompleteRoutingCosts.class); + candidateComparatorBinder + .addBinding(CandidateComparatorByDeadline.CONFIGURATION_KEY) + .to(CandidateComparatorByDeadline.class); + candidateComparatorBinder + .addBinding(CandidateComparatorDeadlineAtRiskFirst.CONFIGURATION_KEY) + .to(CandidateComparatorDeadlineAtRiskFirst.class); + candidateComparatorBinder + .addBinding(CandidateComparatorByEnergyLevel.CONFIGURATION_KEY) + .to(CandidateComparatorByEnergyLevel.class); + candidateComparatorBinder + .addBinding(CandidateComparatorByInitialRoutingCosts.CONFIGURATION_KEY) + .to(CandidateComparatorByInitialRoutingCosts.class); + candidateComparatorBinder + .addBinding(CandidateComparatorByOrderAge.CONFIGURATION_KEY) + .to(CandidateComparatorByOrderAge.class); + candidateComparatorBinder + .addBinding(CandidateComparatorByOrderName.CONFIGURATION_KEY) + .to(CandidateComparatorByOrderName.class); + candidateComparatorBinder + .addBinding(CandidateComparatorByVehicleName.CONFIGURATION_KEY) + .to(CandidateComparatorByVehicleName.class); + candidateComparatorBinder + .addBinding(CandidateComparatorIdleFirst.CONFIGURATION_KEY) + .to(CandidateComparatorIdleFirst.class); + + bind(CompositeVehicleComparator.class) + .in(Singleton.class); + bind(CompositeOrderComparator.class) + .in(Singleton.class); + bind(CompositeOrderCandidateComparator.class) + .in(Singleton.class); + bind(CompositeVehicleCandidateComparator.class) + .in(Singleton.class); + + bind(TransportOrderUtil.class) + .in(Singleton.class); + + configureRerouteComponents(); + } + + private void configureRerouteComponents() { + bind(RerouteUtil.class).in(Singleton.class); + bind(RegularReroutingStrategy.class).in(Singleton.class); + bind(RegularDriveOrderMerger.class).in(Singleton.class); + + MapBinder<ReroutingType, ReroutingStrategy> reroutingStrategies + = MapBinder.newMapBinder( + binder(), + ReroutingType.class, + ReroutingStrategy.class + ); + reroutingStrategies + .addBinding(ReroutingType.REGULAR) + .to(RegularReroutingStrategy.class); + reroutingStrategies + .addBinding(ReroutingType.FORCED) + .to(ForcedReroutingStrategy.class); + } +} diff --git a/opentcs-strategies-default/src/guiceConfig/java/org/opentcs/strategies/basic/peripherals/dispatching/DefaultPeripheralJobDispatcherModule.java b/opentcs-strategies-default/src/guiceConfig/java/org/opentcs/strategies/basic/peripherals/dispatching/DefaultPeripheralJobDispatcherModule.java new file mode 100644 index 0000000..e0ca54f --- /dev/null +++ b/opentcs-strategies-default/src/guiceConfig/java/org/opentcs/strategies/basic/peripherals/dispatching/DefaultPeripheralJobDispatcherModule.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.peripherals.dispatching; + +import org.opentcs.customizations.kernel.KernelInjectionModule; +import org.opentcs.drivers.peripherals.PeripheralJobCallback; + +/** + * Guice configuration for the default peripheral job dispatcher. + */ +public class DefaultPeripheralJobDispatcherModule + extends + KernelInjectionModule { + + /** + * Creates a new instance. + */ + public DefaultPeripheralJobDispatcherModule() { + } + + @Override + protected void configure() { + configureDispatcherDependencies(); + bindPeripheralJobDispatcher(DefaultPeripheralJobDispatcher.class); + } + + private void configureDispatcherDependencies() { + bind(DefaultPeripheralJobDispatcherConfiguration.class) + .toInstance( + getConfigBindingProvider().get( + DefaultPeripheralJobDispatcherConfiguration.PREFIX, + DefaultPeripheralJobDispatcherConfiguration.class + ) + ); + + bind(PeripheralJobCallback.class).to(DefaultPeripheralJobDispatcher.class); + bind(PeripheralReleaseStrategy.class).to(DefaultPeripheralReleaseStrategy.class); + bind(JobSelectionStrategy.class).to(DefaultJobSelectionStrategy.class); + } +} diff --git a/opentcs-strategies-default/src/guiceConfig/java/org/opentcs/strategies/basic/routing/DefaultRouterModule.java b/opentcs-strategies-default/src/guiceConfig/java/org/opentcs/strategies/basic/routing/DefaultRouterModule.java new file mode 100644 index 0000000..3d5e0ec --- /dev/null +++ b/opentcs-strategies-default/src/guiceConfig/java/org/opentcs/strategies/basic/routing/DefaultRouterModule.java @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing; + +import com.google.inject.assistedinject.FactoryModuleBuilder; +import jakarta.inject.Singleton; +import org.opentcs.components.kernel.routing.GroupMapper; +import org.opentcs.customizations.kernel.KernelInjectionModule; +import org.opentcs.strategies.basic.routing.edgeevaluator.EdgeEvaluatorBoundingBox; +import org.opentcs.strategies.basic.routing.edgeevaluator.EdgeEvaluatorComposite; +import org.opentcs.strategies.basic.routing.edgeevaluator.EdgeEvaluatorDistance; +import org.opentcs.strategies.basic.routing.edgeevaluator.EdgeEvaluatorExplicitProperties; +import org.opentcs.strategies.basic.routing.edgeevaluator.EdgeEvaluatorHops; +import org.opentcs.strategies.basic.routing.edgeevaluator.EdgeEvaluatorTravelTime; +import org.opentcs.strategies.basic.routing.edgeevaluator.ExplicitPropertiesConfiguration; +import org.opentcs.strategies.basic.routing.jgrapht.BellmanFordPointRouterFactory; +import org.opentcs.strategies.basic.routing.jgrapht.DijkstraPointRouterFactory; +import org.opentcs.strategies.basic.routing.jgrapht.FloydWarshallPointRouterFactory; +import org.opentcs.strategies.basic.routing.jgrapht.GraphProvider; +import org.opentcs.strategies.basic.routing.jgrapht.MapperComponentsFactory; +import org.opentcs.strategies.basic.routing.jgrapht.ShortestPathConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Guice configuration for the default router. + */ +public class DefaultRouterModule + extends + KernelInjectionModule { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(DefaultRouterModule.class); + + /** + * Creates a new instance. + */ + public DefaultRouterModule() { + } + + @Override + protected void configure() { + configureRouterDependencies(); + bindRouter(DefaultRouter.class); + } + + private void configureRouterDependencies() { + bind(DefaultRouterConfiguration.class) + .toInstance( + getConfigBindingProvider().get( + DefaultRouterConfiguration.PREFIX, + DefaultRouterConfiguration.class + ) + ); + + ShortestPathConfiguration spConfiguration + = getConfigBindingProvider().get( + ShortestPathConfiguration.PREFIX, + ShortestPathConfiguration.class + ); + bind(ShortestPathConfiguration.class) + .toInstance(spConfiguration); + + install(new FactoryModuleBuilder().build(MapperComponentsFactory.class)); + + bind(GraphProvider.class) + .in(Singleton.class); + + switch (spConfiguration.algorithm()) { + case DIJKSTRA: + bind(PointRouterFactory.class) + .to(DijkstraPointRouterFactory.class); + break; + case BELLMAN_FORD: + bind(PointRouterFactory.class) + .to(BellmanFordPointRouterFactory.class); + break; + case FLOYD_WARSHALL: + bind(PointRouterFactory.class) + .to(FloydWarshallPointRouterFactory.class); + break; + default: + LOG.warn( + "Unhandled algorithm selected ({}), falling back to Dijkstra's algorithm.", + spConfiguration.algorithm() + ); + bind(PointRouterFactory.class) + .to(DijkstraPointRouterFactory.class); + } + + edgeEvaluatorBinder() + .addBinding(EdgeEvaluatorDistance.CONFIGURATION_KEY) + .to(EdgeEvaluatorDistance.class); + edgeEvaluatorBinder() + .addBinding(EdgeEvaluatorExplicitProperties.CONFIGURATION_KEY) + .to(EdgeEvaluatorExplicitProperties.class); + edgeEvaluatorBinder() + .addBinding(EdgeEvaluatorHops.CONFIGURATION_KEY) + .to(EdgeEvaluatorHops.class); + edgeEvaluatorBinder() + .addBinding(EdgeEvaluatorTravelTime.CONFIGURATION_KEY) + .to(EdgeEvaluatorTravelTime.class); + edgeEvaluatorBinder() + .addBinding(EdgeEvaluatorBoundingBox.CONFIGURATION_KEY) + .to(EdgeEvaluatorBoundingBox.class); + + bind(EdgeEvaluatorComposite.class) + .in(Singleton.class); + + bind(ExplicitPropertiesConfiguration.class) + .toInstance( + getConfigBindingProvider().get( + ExplicitPropertiesConfiguration.PREFIX, + ExplicitPropertiesConfiguration.class + ) + ); + + bind(DefaultRoutingGroupMapper.class) + .in(Singleton.class); + bind(GroupMapper.class) + .to(DefaultRoutingGroupMapper.class); + } +} diff --git a/opentcs-strategies-default/src/guiceConfig/java/org/opentcs/strategies/basic/scheduling/DefaultSchedulerModule.java b/opentcs-strategies-default/src/guiceConfig/java/org/opentcs/strategies/basic/scheduling/DefaultSchedulerModule.java new file mode 100644 index 0000000..261f47b --- /dev/null +++ b/opentcs-strategies-default/src/guiceConfig/java/org/opentcs/strategies/basic/scheduling/DefaultSchedulerModule.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling; + +import com.google.inject.multibindings.Multibinder; +import jakarta.inject.Singleton; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.customizations.kernel.KernelInjectionModule; +import org.opentcs.strategies.basic.scheduling.modules.PausedVehicleModule; +import org.opentcs.strategies.basic.scheduling.modules.SameDirectionBlockModule; +import org.opentcs.strategies.basic.scheduling.modules.SingleVehicleBlockModule; +import org.opentcs.strategies.basic.scheduling.modules.areaAllocation.AreaAllocationModule; +import org.opentcs.strategies.basic.scheduling.modules.areaAllocation.AreaAllocations; +import org.opentcs.strategies.basic.scheduling.modules.areaAllocation.AreaProvider; +import org.opentcs.strategies.basic.scheduling.modules.areaAllocation.CachingAreaProvider; + +/** + * Guice configuration for the default scheduler. + */ +public class DefaultSchedulerModule + extends + KernelInjectionModule { + + /** + * Creates a new instance. + */ + public DefaultSchedulerModule() { + } + + @Override + protected void configure() { + configureSchedulerDependencies(); + bindScheduler(DefaultScheduler.class); + } + + private void configureSchedulerDependencies() { + bind(ReservationPool.class).in(Singleton.class); + + Multibinder<Scheduler.Module> moduleBinder = schedulerModuleBinder(); + moduleBinder.addBinding().to(SingleVehicleBlockModule.class); + moduleBinder.addBinding().to(SameDirectionBlockModule.class); + moduleBinder.addBinding().to(PausedVehicleModule.class); + + moduleBinder.addBinding().to(AreaAllocationModule.class); + bind(AreaProvider.class) + .to(CachingAreaProvider.class) + .in(Singleton.class); + bind(AreaAllocations.class).in(Singleton.class); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/AssignmentCandidate.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/AssignmentCandidate.java new file mode 100644 index 0000000..8b2b61c --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/AssignmentCandidate.java @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import java.util.List; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.TransportOrder; + +/** + * Contains information for a potential assignment of a transport order to a vehicle. + */ +public class AssignmentCandidate { + + /** + * The vehicle. + */ + private final Vehicle vehicle; + /** + * The transport order. + */ + private final TransportOrder transportOrder; + /** + * The route/drive orders to be executed upon assignment. + */ + private final List<DriveOrder> driveOrders; + /** + * The completeRoutingCosts for processing the whole order with the vehicle. + */ + private final long completeRoutingCosts; + + /** + * Creates a new instance. + * + * @param vehicle The vehicle that would be assigned to the transport order. + * @param transportOrder The transport order that would be assigned to the vehicle. + * @param driveOrders The drive orders containing the computed route the vehicle would take. + * May not be empty and the route of each drive order may not be null. + */ + public AssignmentCandidate( + Vehicle vehicle, + TransportOrder transportOrder, + List<DriveOrder> driveOrders + ) { + this.vehicle = requireNonNull(vehicle, "vehicle"); + this.transportOrder = requireNonNull(transportOrder, "transportOrder"); + this.driveOrders = requireNonNull(driveOrders, "driveOrders"); + checkArgument(!driveOrders.isEmpty(), "driveOrders is empty"); + driveOrders.forEach( + driveOrder -> checkArgument( + driveOrder.getRoute() != null, + "a drive order's route is null" + ) + ); + this.completeRoutingCosts = cumulatedCosts(driveOrders); + } + + public Vehicle getVehicle() { + return vehicle; + } + + public TransportOrder getTransportOrder() { + return transportOrder; + } + + public List<DriveOrder> getDriveOrders() { + return driveOrders; + } + + /** + * Returns the costs for travelling only the first drive order/reaching the first destination. + * + * @return The costs for travelling only the first drive order. + */ + public long getInitialRoutingCosts() { + return driveOrders.get(0).getRoute().getCosts(); + } + + /** + * Returns the costs for travelling all drive orders. + * + * @return The costs for travelling all drive orders. + */ + public long getCompleteRoutingCosts() { + return completeRoutingCosts; + } + + private static long cumulatedCosts(List<DriveOrder> driveOrders) { + return driveOrders.stream().mapToLong(driveOrder -> driveOrder.getRoute().getCosts()).sum(); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/DefaultDispatcher.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/DefaultDispatcher.java new file mode 100644 index 0000000..2d25894 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/DefaultDispatcher.java @@ -0,0 +1,242 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching; + +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import org.opentcs.components.kernel.Dispatcher; +import org.opentcs.components.kernel.dipatching.TransportOrderAssignmentException; +import org.opentcs.components.kernel.dipatching.TransportOrderAssignmentVeto; +import org.opentcs.components.kernel.services.InternalVehicleService; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.phase.assignment.OrderAssigner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Dispatches transport orders and vehicles. + */ +public class DefaultDispatcher + implements + Dispatcher { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(DefaultDispatcher.class); + /** + * Stores reservations of transport orders for vehicles. + */ + private final OrderReservationPool orderReservationPool; + /** + * Provides services/utility methods for working with transport orders. + */ + private final TransportOrderUtil transportOrderUtil; + /** + * The vehicle service. + */ + private final InternalVehicleService vehicleService; + /** + * The kernel's executor. + */ + private final ScheduledExecutorService kernelExecutor; + + private final FullDispatchTask fullDispatchTask; + + private final Provider<PeriodicVehicleRedispatchingTask> periodicDispatchTaskProvider; + + private final DefaultDispatcherConfiguration configuration; + + private final RerouteUtil rerouteUtil; + + private final OrderAssigner orderAssigner; + + private final TransportOrderAssignmentChecker transportOrderAssignmentChecker; + + private ScheduledFuture<?> periodicDispatchTaskFuture; + /** + * Indicates whether this component is enabled. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param orderReservationPool Stores reservations of transport orders for vehicles. + * @param transportOrderUtil Provides services for working with transport orders. + * @param vehicleService The vehicle service. + * @param kernelExecutor Executes dispatching tasks. + * @param fullDispatchTask The full dispatch task. + * @param periodicDispatchTaskProvider Provides the periodic vehicle redospatching task. + * @param configuration The dispatcher configuration. + * @param rerouteUtil The reroute util. + * @param orderAssigner Handles assignments of transport orders to vehicles. + * @param transportOrderAssignmentChecker Checks whether the assignment of transport orders to + * vehicles is possible. + */ + @Inject + public DefaultDispatcher( + OrderReservationPool orderReservationPool, + TransportOrderUtil transportOrderUtil, + InternalVehicleService vehicleService, + @KernelExecutor + ScheduledExecutorService kernelExecutor, + FullDispatchTask fullDispatchTask, + Provider<PeriodicVehicleRedispatchingTask> periodicDispatchTaskProvider, + DefaultDispatcherConfiguration configuration, + RerouteUtil rerouteUtil, + OrderAssigner orderAssigner, + TransportOrderAssignmentChecker transportOrderAssignmentChecker + ) { + this.orderReservationPool = requireNonNull(orderReservationPool, "orderReservationPool"); + this.transportOrderUtil = requireNonNull(transportOrderUtil, "transportOrderUtil"); + this.vehicleService = requireNonNull(vehicleService, "vehicleService"); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + this.fullDispatchTask = requireNonNull(fullDispatchTask, "fullDispatchTask"); + this.periodicDispatchTaskProvider = requireNonNull( + periodicDispatchTaskProvider, + "periodicDispatchTaskProvider" + ); + this.configuration = requireNonNull(configuration, "configuration"); + this.rerouteUtil = requireNonNull(rerouteUtil, "rerouteUtil"); + this.orderAssigner = requireNonNull(orderAssigner, "orderAssigner"); + this.transportOrderAssignmentChecker = requireNonNull( + transportOrderAssignmentChecker, + "transportOrderAssignmentChecker" + ); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + LOG.debug("Initializing..."); + + transportOrderUtil.initialize(); + orderReservationPool.clear(); + + fullDispatchTask.initialize(); + + LOG.debug( + "Scheduling periodic dispatch task with interval of {} ms...", + configuration.idleVehicleRedispatchingInterval() + ); + periodicDispatchTaskFuture = kernelExecutor.scheduleAtFixedRate( + periodicDispatchTaskProvider.get(), + configuration.idleVehicleRedispatchingInterval(), + configuration.idleVehicleRedispatchingInterval(), + TimeUnit.MILLISECONDS + ); + + initialized = true; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + LOG.debug("Terminating..."); + + periodicDispatchTaskFuture.cancel(false); + periodicDispatchTaskFuture = null; + + fullDispatchTask.terminate(); + + initialized = false; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void dispatch() { + LOG.debug("Executing dispatch task..."); + fullDispatchTask.run(); + } + + @Override + public void withdrawOrder(TransportOrder order, boolean immediateAbort) { + requireNonNull(order, "order"); + checkState(isInitialized(), "Not initialized"); + + LOG.debug( + "Withdrawing transport order '{}' (immediate={})...", + order.getName(), + immediateAbort + ); + transportOrderUtil.abortOrder(order, immediateAbort); + } + + @Override + public void withdrawOrder(Vehicle vehicle, boolean immediateAbort) { + requireNonNull(vehicle, "vehicle"); + checkState(isInitialized(), "Not initialized"); + + LOG.debug( + "Withdrawing transport order for vehicle '{}' (immediate={})...", + vehicle.getName(), + immediateAbort + ); + transportOrderUtil.abortOrder(vehicle, immediateAbort); + } + + @Override + public void reroute(Vehicle vehicle, ReroutingType reroutingType) { + requireNonNull(vehicle, "vehicle"); + requireNonNull(reroutingType, "reroutingType"); + + LOG.info( + "Rerouting vehicle '{}' from its current position '{}' using rerouting type '{}'...", + vehicle.getName(), + vehicle.getCurrentPosition() == null ? null : vehicle.getCurrentPosition().getName(), + reroutingType + ); + rerouteUtil.reroute(vehicle, reroutingType); + } + + @Override + public void rerouteAll(ReroutingType reroutingType) { + requireNonNull(reroutingType, "reroutingType"); + + LOG.info("Rerouting all vehicles using rerouting type '{}'...", reroutingType); + rerouteUtil.reroute(vehicleService.fetchObjects(Vehicle.class), reroutingType); + } + + @Override + public void assignNow(TransportOrder transportOrder) + throws TransportOrderAssignmentException { + requireNonNull(transportOrder, "transportOrder"); + + TransportOrderAssignmentVeto assignmentVeto + = transportOrderAssignmentChecker.checkTransportOrderAssignment(transportOrder); + + if (assignmentVeto != TransportOrderAssignmentVeto.NO_VETO) { + throw new TransportOrderAssignmentException( + transportOrder.getReference(), + transportOrder.getIntendedVehicle(), + assignmentVeto + ); + } + + orderAssigner.tryAssignments( + List.of(vehicleService.fetchObject(Vehicle.class, transportOrder.getIntendedVehicle())), + List.of(transportOrder) + ); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/DefaultDispatcherConfiguration.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/DefaultDispatcherConfiguration.java new file mode 100644 index 0000000..5f43f4b --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/DefaultDispatcherConfiguration.java @@ -0,0 +1,182 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching; + +import java.util.List; +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; + +/** + * Provides methods to configure the {@link DefaultDispatcher}. + */ +@ConfigurationPrefix(DefaultDispatcherConfiguration.PREFIX) +public interface DefaultDispatcherConfiguration { + + /** + * This configuration's prefix. + */ + String PREFIX = "defaultdispatcher"; + + @ConfigurationEntry( + type = "Comma-separated list of strings", + description = {"Keys by which to prioritize transport orders for assignment.", + "Possible values:", + "BY_AGE: Sort by age, oldest first.", + "BY_DEADLINE: Sort by deadline, most urgent first.", + "DEADLINE_AT_RISK_FIRST: Sort orders with deadlines at risk first.", + "BY_NAME: Sort by name, lexicographically."}, + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_assign" + ) + List<String> orderPriorities(); + + @ConfigurationEntry( + type = "Comma-separated list of strings", + description = {"Keys by which to prioritize vehicles for assignment.", + "Possible values:", + "BY_ENERGY_LEVEL: Sort by energy level, highest first.", + "IDLE_FIRST: Sort vehicles with state IDLE first.", + "BY_NAME: Sort by name, lexicographically."}, + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_assign" + ) + List<String> vehiclePriorities(); + + @ConfigurationEntry( + type = "Comma-separated list of strings", + description = {"Keys by which to prioritize vehicle candidates for assignment.", + "Possible values:", + "BY_ENERGY_LEVEL: Sort by energy level of the vehicle, highest first.", + "IDLE_FIRST: Sort vehicles with state IDLE first.", + "BY_COMPLETE_ROUTING_COSTS: Sort by complete routing costs, lowest first.", + "BY_INITIAL_ROUTING_COSTS: Sort by routing costs for the first destination.", + "BY_VEHICLE_NAME: Sort by vehicle name, lexicographically."}, + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_assign" + ) + List<String> vehicleCandidatePriorities(); + + @ConfigurationEntry( + type = "Comma-separated list of strings", + description = {"Keys by which to prioritize transport order candidates for assignment.", + "Possible values:", + "BY_AGE: Sort by transport order age, oldest first.", + "BY_DEADLINE: Sort by transport order deadline, most urgent first.", + "DEADLINE_AT_RISK_FIRST: Sort orders with deadlines at risk first.", + "BY_COMPLETE_ROUTING_COSTS: Sort by complete routing costs, lowest first.", + "BY_INITIAL_ROUTING_COSTS: Sort by routing costs for the first destination.", + "BY_ORDER_NAME: Sort by transport order name, lexicographically."}, + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_assign" + ) + List<String> orderCandidatePriorities(); + + @ConfigurationEntry( + type = "Integer", + description = "The time window (in ms) before its deadline in which an order becomes urgent.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START, + orderKey = "0_assign_special_0" + ) + long deadlineAtRiskPeriod(); + + @ConfigurationEntry( + type = "Boolean", + description = "Whether orders to the current position with no operation should be assigned.", + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "1_orders_special_0" + ) + boolean assignRedundantOrders(); + + @ConfigurationEntry( + type = "Boolean", + description = "Whether unroutable incoming transport orders should be marked as UNROUTABLE.", + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "1_orders_special_1" + ) + boolean dismissUnroutableTransportOrders(); + + @ConfigurationEntry( + type = "String", + description = { + "The strategy to use when rerouting of a vehicle results in no route at all.", + "The vehicle then continues to use the previous route in the configured way.", + "Possible values:", + "IGNORE_PATH_LOCKS: Stick to the previous route, ignoring path locks.", + "PAUSE_IMMEDIATELY: Do not send further orders to the vehicle; wait for another " + + "rerouting opportunity.", + "PAUSE_AT_PATH_LOCK: Send further orders to the vehicle only until it reaches a locked " + + "path; then wait for another rerouting opportunity." + }, + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "1_orders_special_2" + ) + ReroutingImpossibleStrategy reroutingImpossibleStrategy(); + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to automatically create parking orders for idle vehicles.", + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "2_park_0" + ) + boolean parkIdleVehicles(); + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to consider parking position priorities when creating parking orders.", + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "2_park_1" + ) + boolean considerParkingPositionPriorities(); + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to repark vehicles to parking positions with higher priorities.", + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "2_park_2" + ) + boolean reparkVehiclesToHigherPriorityPositions(); + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to automatically create recharge orders for idle vehicles.", + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "3_recharge_0" + ) + boolean rechargeIdleVehicles(); + + @ConfigurationEntry( + type = "Boolean", + description = {"Whether vehicles must be recharged until they are fully charged.", + "If false, vehicle must only be recharged until sufficiently charged."}, + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY, + orderKey = "3_recharge_1" + ) + boolean keepRechargingUntilFullyCharged(); + + @ConfigurationEntry( + type = "Integer", + description = "The interval between redispatching of vehicles.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_NEW_PLANT_MODEL, + orderKey = "9_misc" + ) + long idleVehicleRedispatchingInterval(); + + /** + * The available strategies for situations in which rerouting is not possible. + */ + enum ReroutingImpossibleStrategy { + /** + * Stick to the previous route, ignoring path locks. + */ + IGNORE_PATH_LOCKS, + /** + * Do not send further orders to the vehicle; wait for another rerouting opportunity. + */ + PAUSE_IMMEDIATELY, + /** + * Send further orders to the vehicle only until it reaches a locked path; then wait for another + * rerouting opportunity. + */ + PAUSE_AT_PATH_LOCK; + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/FullDispatchTask.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/FullDispatchTask.java new file mode 100644 index 0000000..b42dd22 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/FullDispatchTask.java @@ -0,0 +1,184 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.Lifecycle; +import org.opentcs.strategies.basic.dispatching.phase.AssignReservedOrdersPhase; +import org.opentcs.strategies.basic.dispatching.phase.AssignSequenceSuccessorsPhase; +import org.opentcs.strategies.basic.dispatching.phase.CheckNewOrdersPhase; +import org.opentcs.strategies.basic.dispatching.phase.FinishWithdrawalsPhase; +import org.opentcs.strategies.basic.dispatching.phase.assignment.AssignFreeOrdersPhase; +import org.opentcs.strategies.basic.dispatching.phase.assignment.AssignNextDriveOrdersPhase; +import org.opentcs.strategies.basic.dispatching.phase.parking.ParkIdleVehiclesPhase; +import org.opentcs.strategies.basic.dispatching.phase.parking.PrioritizedParkingPhase; +import org.opentcs.strategies.basic.dispatching.phase.parking.PrioritizedReparkPhase; +import org.opentcs.strategies.basic.dispatching.phase.recharging.RechargeIdleVehiclesPhase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Performs a full dispatch run. + */ +public class FullDispatchTask + implements + Runnable, + Lifecycle { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(FullDispatchTask.class); + + private final CheckNewOrdersPhase checkNewOrdersPhase; + private final FinishWithdrawalsPhase finishWithdrawalsPhase; + private final AssignNextDriveOrdersPhase assignNextDriveOrdersPhase; + private final AssignReservedOrdersPhase assignReservedOrdersPhase; + private final AssignSequenceSuccessorsPhase assignSequenceSuccessorsPhase; + private final AssignFreeOrdersPhase assignFreeOrdersPhase; + private final RechargeIdleVehiclesPhase rechargeIdleVehiclesPhase; + private final PrioritizedReparkPhase prioritizedReparkPhase; + private final PrioritizedParkingPhase prioritizedParkingPhase; + private final ParkIdleVehiclesPhase parkIdleVehiclesPhase; + /** + * Indicates whether this component is enabled. + */ + private boolean initialized; + + @Inject + public FullDispatchTask( + CheckNewOrdersPhase checkNewOrdersPhase, + FinishWithdrawalsPhase finishWithdrawalsPhase, + AssignNextDriveOrdersPhase assignNextDriveOrdersPhase, + AssignReservedOrdersPhase assignReservedOrdersPhase, + AssignSequenceSuccessorsPhase assignSequenceSuccessorsPhase, + AssignFreeOrdersPhase assignFreeOrdersPhase, + RechargeIdleVehiclesPhase rechargeIdleVehiclesPhase, + PrioritizedReparkPhase prioritizedReparkPhase, + PrioritizedParkingPhase prioritizedParkingPhase, + ParkIdleVehiclesPhase parkIdleVehiclesPhase + ) { + this.checkNewOrdersPhase = requireNonNull(checkNewOrdersPhase, "checkNewOrdersPhase"); + this.finishWithdrawalsPhase = requireNonNull(finishWithdrawalsPhase, "finishWithdrawalsPhase"); + this.assignNextDriveOrdersPhase = requireNonNull( + assignNextDriveOrdersPhase, + "assignNextDriveOrdersPhase" + ); + this.assignReservedOrdersPhase = requireNonNull( + assignReservedOrdersPhase, + "assignReservedOrdersPhase" + ); + this.assignSequenceSuccessorsPhase = requireNonNull( + assignSequenceSuccessorsPhase, + "assignSequenceSuccessorsPhase" + ); + this.assignFreeOrdersPhase = requireNonNull(assignFreeOrdersPhase, "assignFreeOrdersPhase"); + this.rechargeIdleVehiclesPhase = requireNonNull( + rechargeIdleVehiclesPhase, + "rechargeIdleVehiclesPhase" + ); + this.prioritizedReparkPhase = requireNonNull(prioritizedReparkPhase, "prioritizedReparkPhase"); + this.prioritizedParkingPhase = requireNonNull( + prioritizedParkingPhase, + "prioritizedParkingPhase" + ); + this.parkIdleVehiclesPhase = requireNonNull(parkIdleVehiclesPhase, "parkIdleVehiclesPhase"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + checkNewOrdersPhase.initialize(); + finishWithdrawalsPhase.initialize(); + assignNextDriveOrdersPhase.initialize(); + assignReservedOrdersPhase.initialize(); + assignSequenceSuccessorsPhase.initialize(); + assignFreeOrdersPhase.initialize(); + rechargeIdleVehiclesPhase.initialize(); + prioritizedReparkPhase.initialize(); + prioritizedParkingPhase.initialize(); + parkIdleVehiclesPhase.initialize(); + + initialized = true; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + checkNewOrdersPhase.terminate(); + finishWithdrawalsPhase.terminate(); + assignNextDriveOrdersPhase.terminate(); + assignReservedOrdersPhase.terminate(); + assignSequenceSuccessorsPhase.terminate(); + assignFreeOrdersPhase.terminate(); + rechargeIdleVehiclesPhase.terminate(); + prioritizedReparkPhase.terminate(); + prioritizedParkingPhase.terminate(); + parkIdleVehiclesPhase.terminate(); + + initialized = false; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public final void run() { + LOG.debug("Starting full dispatch run..."); + + checkNewOrdersPhase.run(); + // Check what vehicles involved in a process should do. + finishWithdrawalsPhase.run(); + assignNextDriveOrdersPhase.run(); + assignSequenceSuccessorsPhase.run(); + // Check what vehicles not already in a process should do. + assignOrders(); + rechargeVehicles(); + parkVehicles(); + + LOG.debug("Finished full dispatch run."); + } + + /** + * Assignment of orders to vehicles. + * <p> + * Default: Assigns reserved and then free orders to vehicles. + * </p> + */ + protected void assignOrders() { + assignReservedOrdersPhase.run(); + assignFreeOrdersPhase.run(); + } + + /** + * Recharging of vehicles. + * <p> + * Default: Sends idle vehicles with a degraded energy level to recharge locations. + * </p> + */ + protected void rechargeVehicles() { + rechargeIdleVehiclesPhase.run(); + } + + /** + * Parking of vehicles. + * <p> + * Default: Sends idle vehicles to parking positions. + * </p> + */ + protected void parkVehicles() { + prioritizedReparkPhase.run(); + prioritizedParkingPhase.run(); + parkIdleVehiclesPhase.run(); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/OrderReservationPool.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/OrderReservationPool.java new file mode 100644 index 0000000..e9850ec --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/OrderReservationPool.java @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; + +/** + * Stores reservations of orders for vehicles. + */ +public class OrderReservationPool { + + /** + * Reservations of orders for vehicles. + */ + private final Map<TCSObjectReference<TransportOrder>, TCSObjectReference<Vehicle>> reservations + = Collections.synchronizedMap(new HashMap<>()); + + /** + * Creates a new instance. + */ + @Inject + public OrderReservationPool() { + } + + /** + * Clears all reservations. + */ + public void clear() { + reservations.clear(); + } + + /** + * Checks whether there is a reservation of the given transport order for any vehicle. + * + * @param orderRef A reference to the transport order. + * @return <code>true</code> if, and only if, there is a reservation. + */ + public boolean isReserved( + @Nonnull + TCSObjectReference<TransportOrder> orderRef + ) { + return reservations.containsKey(orderRef); + } + + public void addReservation( + @Nonnull + TCSObjectReference<TransportOrder> orderRef, + @Nonnull + TCSObjectReference<Vehicle> vehicleRef + ) { + reservations.put(orderRef, vehicleRef); + } + + public void removeReservation( + @Nonnull + TCSObjectReference<TransportOrder> orderRef + ) { + reservations.remove(orderRef); + } + + public void removeReservations( + @Nonnull + TCSObjectReference<Vehicle> vehicleRef + ) { + reservations.values().removeIf(value -> vehicleRef.equals(value)); + } + + public List<TCSObjectReference<TransportOrder>> findReservations( + @Nonnull + TCSObjectReference<Vehicle> vehicleRef + ) { + return reservations.entrySet().stream() + .filter(entry -> vehicleRef.equals(entry.getValue())) + .map(entry -> entry.getKey()) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/PeriodicVehicleRedispatchingTask.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/PeriodicVehicleRedispatchingTask.java new file mode 100644 index 0000000..a70f635 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/PeriodicVehicleRedispatchingTask.java @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.services.DispatcherService; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Periodically checks for idle vehicles that could process a transport order. + * The main purpose of doing this is retrying to dispatch vehicles that were not in a dispatchable + * state when dispatching them was last tried. + * A potential reason for this is that a vehicle temporarily reported an error because a safety + * sensor was triggered. + */ +public class PeriodicVehicleRedispatchingTask + implements + Runnable { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PeriodicVehicleRedispatchingTask.class); + + private final DispatcherService dispatcherService; + + private final TCSObjectService objectService; + + /** + * Creates a new instance. + * + * @param dispatcherService The dispatcher service used to dispatch vehicles. + * @param objectService The object service. + */ + @Inject + public PeriodicVehicleRedispatchingTask( + DispatcherService dispatcherService, + TCSObjectService objectService + ) { + this.dispatcherService = requireNonNull(dispatcherService, "dispatcherService"); + this.objectService = requireNonNull(objectService, "objectService"); + } + + @Override + public void run() { + // If there are any vehicles that could process a transport order, + // trigger the dispatcher once. + objectService.fetchObjects(Vehicle.class, this::couldProcessTransportOrder).stream() + .findAny() + .ifPresent(vehicle -> { + LOG.debug("Vehicle {} could process transport order, triggering dispatcher ...", vehicle); + dispatcherService.dispatch(); + }); + } + + private boolean couldProcessTransportOrder(Vehicle vehicle) { + return vehicle.getIntegrationLevel() == Vehicle.IntegrationLevel.TO_BE_UTILIZED + && vehicle.getCurrentPosition() != null + && (processesNoOrder(vehicle) + || processesDispensableOrder(vehicle)); + } + + private boolean processesNoOrder(Vehicle vehicle) { + return vehicle.hasProcState(Vehicle.ProcState.IDLE) + && (vehicle.hasState(Vehicle.State.IDLE) + || vehicle.hasState(Vehicle.State.CHARGING)); + } + + private boolean processesDispensableOrder(Vehicle vehicle) { + return vehicle.hasProcState(Vehicle.ProcState.PROCESSING_ORDER) + && objectService.fetchObject(TransportOrder.class, vehicle.getTransportOrder()) + .isDispensable(); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/Phase.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/Phase.java new file mode 100644 index 0000000..fefd6ef --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/Phase.java @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching; + +import org.opentcs.components.Lifecycle; + +/** + * Describes a reusable dispatching (sub-)task with a life cycle. + */ +public interface Phase + extends + Runnable, + Lifecycle { + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/RerouteUtil.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/RerouteUtil.java new file mode 100644 index 0000000..efe3f13 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/RerouteUtil.java @@ -0,0 +1,362 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration.ReroutingImpossibleStrategy.IGNORE_PATH_LOCKS; +import static org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration.ReroutingImpossibleStrategy.PAUSE_AT_PATH_LOCK; +import static org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration.ReroutingImpossibleStrategy.PAUSE_IMMEDIATELY; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.InternalTransportOrderService; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.Route.Step; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.drivers.vehicle.VehicleController; +import org.opentcs.drivers.vehicle.VehicleControllerPool; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration.ReroutingImpossibleStrategy; +import org.opentcs.strategies.basic.dispatching.rerouting.ReroutingStrategy; +import org.opentcs.strategies.basic.dispatching.rerouting.VehiclePositionResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides some utility methods used for rerouting vehicles. + */ +public class RerouteUtil { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(RerouteUtil.class); + /** + * The router. + */ + private final Router router; + /** + * The vehicle controller pool. + */ + private final VehicleControllerPool vehicleControllerPool; + /** + * The object service. + */ + private final InternalTransportOrderService transportOrderService; + + private final DefaultDispatcherConfiguration configuration; + + private final Map<ReroutingType, ReroutingStrategy> reroutingStrategies; + + private final VehiclePositionResolver vehiclePositionResolver; + + /** + * Creates a new instance. + * + * @param router The router. + * @param vehicleControllerPool The vehicle controller pool. + * @param transportOrderService The object service. + * @param configuration The configuration. + * @param reroutingStrategies The rerouting strategies to select from. + * @param vehiclePositionResolver Used to resolve the position of vehicles. + */ + @Inject + public RerouteUtil( + Router router, + VehicleControllerPool vehicleControllerPool, + InternalTransportOrderService transportOrderService, + DefaultDispatcherConfiguration configuration, + Map<ReroutingType, ReroutingStrategy> reroutingStrategies, + VehiclePositionResolver vehiclePositionResolver + ) { + this.router = requireNonNull(router, "router"); + this.vehicleControllerPool = requireNonNull(vehicleControllerPool, "vehicleControllerPool"); + this.transportOrderService = requireNonNull(transportOrderService, "transportOrderService"); + this.configuration = requireNonNull(configuration, "configuration"); + this.reroutingStrategies = requireNonNull(reroutingStrategies, "reroutingStrategies"); + this.vehiclePositionResolver = requireNonNull( + vehiclePositionResolver, + "vehiclePositionResolver" + ); + } + + public void reroute(Collection<Vehicle> vehicles, ReroutingType reroutingType) { + for (Vehicle vehicle : vehicles) { + reroute(vehicle, reroutingType); + } + } + + public void reroute(Vehicle vehicle, ReroutingType reroutingType) { + requireNonNull(vehicle, "vehicle"); + LOG.debug("Trying to reroute vehicle '{}'...", vehicle.getName()); + + if (!vehicle.isProcessingOrder()) { + LOG.debug("{} can't be rerouted without processing a transport order.", vehicle.getName()); + return; + } + + TransportOrder originalOrder = transportOrderService.fetchObject( + TransportOrder.class, + vehicle.getTransportOrder() + ); + + if (reroutingType == ReroutingType.FORCED + && isRelatedToUnfinishedPeripheralJobs(originalOrder)) { + LOG.warn( + "Cannot reroute {} when there are unfinished peripheral jobs " + + "related to the current transport order.", + vehicle.getName() + ); + return; + } + + Optional<List<DriveOrder>> optOrders; + if (reroutingStrategies.containsKey(reroutingType)) { + optOrders = reroutingStrategies.get(reroutingType).reroute(vehicle); + } + else { + LOG.warn( + "Cannot reroute {} for unknown rerouting type: {}", + vehicle.getName(), + reroutingType.name() + ); + optOrders = Optional.empty(); + } + + if (reroutingType == ReroutingType.FORCED && vehicle.getState() != Vehicle.State.IDLE) { + LOG.warn( + "Forcefully rerouting {} although its state is not 'IDLE' but '{}'.", + vehicle.getName(), + vehicle.getState().name() + ); + } + + // Get the drive order with the new route or stick to the old one + List<DriveOrder> newDriveOrders; + if (optOrders.isPresent()) { + newDriveOrders = optOrders.get(); + } + else { + newDriveOrders = updatePathLocksAndRestrictions(vehicle, originalOrder); + } + + LOG.debug("Updating transport order {}...", originalOrder.getName()); + updateTransportOrder(originalOrder, newDriveOrders, vehicle); + } + + private List<DriveOrder> updatePathLocksAndRestrictions( + Vehicle vehicle, + TransportOrder originalOrder + ) { + LOG.debug( + "Couldn't find a new route for {}. Updating the current one...", + vehicle.getName() + ); + // Get all unfinished drive order of the transport order the vehicle is processing + List<DriveOrder> unfinishedOrders = new ArrayList<>(); + unfinishedOrders.add(originalOrder.getCurrentDriveOrder()); + unfinishedOrders.addAll(originalOrder.getFutureDriveOrders()); + + unfinishedOrders = updatePathLocks(unfinishedOrders); + unfinishedOrders = markRestrictedSteps( + unfinishedOrders, + new ExecutionTest( + configuration.reroutingImpossibleStrategy(), + vehiclePositionResolver.getFutureOrCurrentPosition(vehicle) + ) + ); + return unfinishedOrders; + } + + private void updateTransportOrder( + TransportOrder originalOrder, + List<DriveOrder> newDriveOrders, + Vehicle vehicle + ) { + VehicleController controller = vehicleControllerPool.getVehicleController(vehicle.getName()); + + // Restore the transport order's history + List<DriveOrder> newOrders = new ArrayList<>(); + newOrders.addAll(originalOrder.getPastDriveOrders()); + newOrders.addAll(newDriveOrders); + + // Update the transport order's drive orders with the re-routed ones + LOG.debug("{}: Updating drive orders with {}.", originalOrder.getName(), newOrders); + transportOrderService.updateTransportOrderDriveOrders( + originalOrder.getReference(), + newOrders + ); + + // If the vehicle is currently processing a (drive) order (and not waiting to get the next + // drive order) we need to update the vehicle's current drive order with the new one. + if (vehicle.hasProcState(Vehicle.ProcState.PROCESSING_ORDER)) { + controller.setTransportOrder( + transportOrderService.fetchObject(TransportOrder.class, originalOrder.getReference()) + ); + } + + // Let the router know the vehicle selected another route + router.selectRoute(vehicle, newOrders); + } + + private List<DriveOrder> updatePathLocks(List<DriveOrder> orders) { + List<DriveOrder> updatedOrders = new ArrayList<>(); + + for (DriveOrder order : orders) { + List<Step> updatedSteps = new ArrayList<>(); + + for (Step step : order.getRoute().getSteps()) { + if (step.getPath() != null) { + updatedSteps.add( + new Route.Step( + transportOrderService.fetchObject(Path.class, step.getPath().getReference()), + step.getSourcePoint(), + step.getDestinationPoint(), + step.getVehicleOrientation(), + step.getRouteIndex() + ) + ); + } + else { + // If the step doesn't have a path, there are no path locks to be updated and we can + // simply keep the step as it is. + updatedSteps.add(step); + } + } + + Route updatedRoute = new Route(updatedSteps, order.getRoute().getCosts()); + + DriveOrder updatedOrder = new DriveOrder(order.getDestination()) + .withRoute(updatedRoute) + .withState(order.getState()) + .withTransportOrder(order.getTransportOrder()); + updatedOrders.add(updatedOrder); + } + + return updatedOrders; + } + + private List<DriveOrder> markRestrictedSteps( + List<DriveOrder> orders, + Predicate<Step> executionTest + ) { + if (configuration.reroutingImpossibleStrategy() == IGNORE_PATH_LOCKS) { + return orders; + } + if (!containsLockedPath(orders)) { + return orders; + } + + List<DriveOrder> updatedOrders = new ArrayList<>(); + for (DriveOrder order : orders) { + List<Step> updatedSteps = new ArrayList<>(); + + for (Step step : order.getRoute().getSteps()) { + boolean executionAllowed = executionTest.test(step); + LOG.debug("Marking path '{}' allowed: {}", step.getPath(), executionAllowed); + updatedSteps.add( + new Step( + step.getPath(), + step.getSourcePoint(), + step.getDestinationPoint(), + step.getVehicleOrientation(), + step.getRouteIndex(), + executionAllowed + ) + ); + } + + Route updatedRoute = new Route(updatedSteps, order.getRoute().getCosts()); + + DriveOrder updatedOrder = new DriveOrder(order.getDestination()) + .withRoute(updatedRoute) + .withState(order.getState()) + .withTransportOrder(order.getTransportOrder()); + updatedOrders.add(updatedOrder); + } + + return updatedOrders; + } + + private boolean containsLockedPath(List<DriveOrder> orders) { + return orders.stream() + .map(order -> order.getRoute().getSteps()) + .flatMap(steps -> steps.stream()) + .filter(step -> step.getPath() != null) + .anyMatch(step -> step.getPath().isLocked()); + } + + private boolean isRelatedToUnfinishedPeripheralJobs(TransportOrder transportOrder) { + return !transportOrderService.fetchObjects( + PeripheralJob.class, + job -> Objects.equals(job.getRelatedTransportOrder(), transportOrder.getReference()) + && job.getPeripheralOperation().isCompletionRequired() + && !job.getState().isFinalState() + ).isEmpty(); + } + + private class ExecutionTest + implements + Predicate<Step> { + + /** + * The current fallback strategy. + */ + private final ReroutingImpossibleStrategy strategy; + /** + * The (earliest) point from which execution may not be allowed. + */ + private final Point source; + /** + * Whether execution of a step is allowed. + */ + private boolean executionAllowed = true; + + /** + * Creates a new intance. + * + * @param strategy The current fallback strategy. + * @param source The (earliest) point from which execution may not be allowed. + */ + ExecutionTest(ReroutingImpossibleStrategy strategy, Point source) { + this.strategy = requireNonNull(strategy, "strategy"); + this.source = requireNonNull(source, "source"); + } + + @Override + public boolean test(Step step) { + if (!executionAllowed) { + return false; + } + + switch (strategy) { + case PAUSE_IMMEDIATELY: + if (Objects.equals(step.getSourcePoint(), source)) { + executionAllowed = false; + } + break; + case PAUSE_AT_PATH_LOCK: + if (step.getPath() != null && step.getPath().isLocked()) { + executionAllowed = false; + } + break; + default: + executionAllowed = true; + } + + return executionAllowed; + } + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/TransportOrderAssignmentChecker.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/TransportOrderAssignmentChecker.java new file mode 100644 index 0000000..20cd4af --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/TransportOrderAssignmentChecker.java @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.dipatching.TransportOrderAssignmentVeto; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; + +/** + * Provides methods to check if the assignment of a {@link TransportOrder} to a {@link Vehicle} is + * possible. + */ +public class TransportOrderAssignmentChecker { + + private final TCSObjectService objectService; + private final OrderReservationPool orderReservationPool; + + /** + * Creates a new instance. + * + * @param objectService The object service to use. + * @param orderReservationPool The pool of order reservations. + */ + @Inject + public TransportOrderAssignmentChecker( + TCSObjectService objectService, + OrderReservationPool orderReservationPool + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.orderReservationPool = requireNonNull(orderReservationPool, "orderReservationPool"); + } + + /** + * Checks whether the assignment of the given transport order to its + * {@link TransportOrder#getIntendedVehicle() intented vehicle} is possible. + * + * @param transportOrder The transport order to check. + * @return A {@link TransportOrderAssignmentVeto} indicating whether the assignment of the given + * transport order is possible or not. + */ + public TransportOrderAssignmentVeto checkTransportOrderAssignment(TransportOrder transportOrder) { + if (!transportOrder.hasState(TransportOrder.State.DISPATCHABLE)) { + return TransportOrderAssignmentVeto.TRANSPORT_ORDER_STATE_INVALID; + } + + if (transportOrder.getWrappingSequence() != null) { + return TransportOrderAssignmentVeto.TRANSPORT_ORDER_PART_OF_ORDER_SEQUENCE; + } + + if (transportOrder.getIntendedVehicle() == null) { + return TransportOrderAssignmentVeto.TRANSPORT_ORDER_INTENDED_VEHICLE_NOT_SET; + } + + Vehicle intendedVehicle = objectService.fetchObject( + Vehicle.class, + transportOrder.getIntendedVehicle() + ); + if (!intendedVehicle.hasProcState(Vehicle.ProcState.IDLE)) { + return TransportOrderAssignmentVeto.VEHICLE_PROCESSING_STATE_INVALID; + } + + if (!intendedVehicle.hasState(Vehicle.State.IDLE) + && !intendedVehicle.hasState(Vehicle.State.CHARGING)) { + return TransportOrderAssignmentVeto.VEHICLE_STATE_INVALID; + } + + if (intendedVehicle.getIntegrationLevel() != Vehicle.IntegrationLevel.TO_BE_UTILIZED) { + return TransportOrderAssignmentVeto.VEHICLE_INTEGRATION_LEVEL_INVALID; + } + + if (intendedVehicle.getCurrentPosition() == null) { + return TransportOrderAssignmentVeto.VEHICLE_CURRENT_POSITION_UNKNOWN; + } + + if (intendedVehicle.getOrderSequence() != null) { + return TransportOrderAssignmentVeto.VEHICLE_PROCESSING_ORDER_SEQUENCE; + } + + if (!orderReservationPool.findReservations(intendedVehicle.getReference()).isEmpty()) { + return TransportOrderAssignmentVeto.GENERIC_VETO; + } + + return TransportOrderAssignmentVeto.NO_VETO; + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/TransportOrderUtil.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/TransportOrderUtil.java new file mode 100644 index 0000000..dc07408 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/TransportOrderUtil.java @@ -0,0 +1,484 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import org.opentcs.components.Lifecycle; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.InternalTransportOrderService; +import org.opentcs.components.kernel.services.InternalVehicleService; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.drivers.vehicle.VehicleController; +import org.opentcs.drivers.vehicle.VehicleControllerPool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides service functions for working with transport orders and their states. + */ +public class TransportOrderUtil + implements + Lifecycle { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(TransportOrderUtil.class); + /** + * The transport order service. + */ + private final InternalTransportOrderService transportOrderService; + /** + * The vehicle service. + */ + private final InternalVehicleService vehicleService; + /** + * The Router instance calculating route costs. + */ + private final Router router; + /** + * The vehicle controller pool. + */ + private final VehicleControllerPool vehicleControllerPool; + /** + * This class's configuration. + */ + private final DefaultDispatcherConfiguration configuration; + /** + * Whether this instance is initialized. + */ + private boolean initialized; + + @Inject + public TransportOrderUtil( + @Nonnull + InternalTransportOrderService transportOrderService, + @Nonnull + InternalVehicleService vehicleService, + @Nonnull + DefaultDispatcherConfiguration configuration, + @Nonnull + Router router, + @Nonnull + VehicleControllerPool vehicleControllerPool + ) { + this.transportOrderService = requireNonNull(transportOrderService, "transportOrderService"); + this.vehicleService = requireNonNull(vehicleService, "vehicleService"); + this.router = requireNonNull(router, "router"); + this.vehicleControllerPool = requireNonNull(vehicleControllerPool, "vehicleControllerPool"); + this.configuration = requireNonNull(configuration, "configuration"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + initialized = false; + } + + /** + * Checks if a transport order's dependencies are completely satisfied or not. + * + * @param order A reference to the transport order to be checked. + * @return <code>false</code> if all the order's dependencies are finished (or + * don't exist any more), else <code>true</code>. + */ + public boolean hasUnfinishedDependencies(TransportOrder order) { + requireNonNull(order, "order"); + + // Assume that FINISHED orders do not have unfinished dependencies. + if (order.hasState(TransportOrder.State.FINISHED)) { + return false; + } + // Check if any transport order referenced as a an explicit dependency + // (really still exists and) is not finished. + if (order.getDependencies().stream() + .map(depRef -> transportOrderService.fetchObject(TransportOrder.class, depRef)) + .anyMatch(dep -> dep != null && !dep.hasState(TransportOrder.State.FINISHED))) { + return true; + } + + // Check if the transport order is part of an order sequence and if yes, + // if it's the next unfinished order in the sequence. + if (order.getWrappingSequence() != null) { + OrderSequence seq = transportOrderService.fetchObject( + OrderSequence.class, + order.getWrappingSequence() + ); + if (!order.getReference().equals(seq.getNextUnfinishedOrder())) { + return true; + } + } + // All referenced transport orders either don't exist (any more) or have + // been finished already. + return false; + } + + /** + * Finds transport orders that are ACTIVE and do not have any unfinished dependencies (any more), + * marking them as DISPATCHABLE. + */ + public void markNewDispatchableOrders() { + transportOrderService.fetchObjects(TransportOrder.class).stream() + .filter(order -> order.hasState(TransportOrder.State.ACTIVE)) + .filter(order -> !hasUnfinishedDependencies(order)) + .forEach( + order -> updateTransportOrderState( + order.getReference(), + TransportOrder.State.DISPATCHABLE + ) + ); + } + + public void updateTransportOrderState( + @Nonnull + TCSObjectReference<TransportOrder> ref, + @Nonnull + TransportOrder.State newState + ) { + requireNonNull(ref, "ref"); + requireNonNull(newState, "newState"); + + LOG.debug("Updating state of transport order {} to {}...", ref.getName(), newState); + switch (newState) { + case FINISHED: + markOrderAndSequenceAsFinished(ref); + break; + case FAILED: + markOrderAndSequenceAsFailed(ref); + break; + default: + // Set the transport order's state. + transportOrderService.updateTransportOrderState(ref, newState); + } + } + + /** + * Assigns a transport order to a vehicle, stores a route for the vehicle in + * the transport order, adjusts the state of vehicle and transport order + * and starts processing. + * + * @param vehicle The vehicle that is supposed to process the transport order. + * @param transportOrder The transport order to be processed. + * @param driveOrders The list of drive orders describing the route for the vehicle. + */ + public void assignTransportOrder( + Vehicle vehicle, + TransportOrder transportOrder, + List<DriveOrder> driveOrders + ) { + requireNonNull(vehicle, "vehicle"); + requireNonNull(transportOrder, "transportOrder"); + requireNonNull(driveOrders, "driveOrders"); + + LOG.debug("Assigning vehicle {} to order {}.", vehicle.getName(), transportOrder.getName()); + final TCSObjectReference<Vehicle> vehicleRef = vehicle.getReference(); + final TCSObjectReference<TransportOrder> orderRef = transportOrder.getReference(); + // Set the vehicle's and transport order's state. + vehicleService.updateVehicleProcState(vehicleRef, Vehicle.ProcState.PROCESSING_ORDER); + updateTransportOrderState(orderRef, TransportOrder.State.BEING_PROCESSED); + // Add cross references between vehicle and transport order/order sequence. + vehicleService.updateVehicleTransportOrder(vehicleRef, orderRef); + if (transportOrder.getWrappingSequence() != null) { + vehicleService.updateVehicleOrderSequence(vehicleRef, transportOrder.getWrappingSequence()); + transportOrderService + .updateOrderSequenceProcessingVehicle(transportOrder.getWrappingSequence(), vehicleRef); + } + transportOrderService.updateTransportOrderProcessingVehicle(orderRef, vehicleRef, driveOrders); + // Let the router know about the route chosen. + router.selectRoute(vehicle, Collections.unmodifiableList(driveOrders)); + // Update the transport order's copy. + TransportOrder updatedOrder = transportOrderService.fetchObject(TransportOrder.class, orderRef); + // If the drive order must be assigned, do so. + if (mustAssign(updatedOrder.getCurrentDriveOrder(), vehicle)) { + // Let the vehicle controller know about the first drive order. + vehicleControllerPool.getVehicleController(vehicle.getName()) + .setTransportOrder(updatedOrder); + } + // If the drive order need not be assigned, let the kernel know that the + // vehicle is waiting for its next order - it will be dispatched again for + // the next drive order, then. + else { + vehicleService.updateVehicleProcState(vehicleRef, Vehicle.ProcState.AWAITING_ORDER); + } + } // void assignTransportOrder() + + /** + * Checks if the given drive order must be processed or could/should be left out. + * Orders that should be left out are those with destinations at which the + * vehicle is already present and which require no destination operation. + * + * @param driveOrder The drive order to be processed. + * @param vehicle The vehicle that would process the order. + * @return <code>true</code> if, and only if, the given drive order must be + * processed; <code>false</code> if the order should/must be left out. + */ + public boolean mustAssign(DriveOrder driveOrder, Vehicle vehicle) { + requireNonNull(vehicle, "vehicle"); + // Removing a vehicle's drive order is always allowed. + if (driveOrder == null) { + return true; + } + // Check if all orders are to be assigned. + if (configuration.assignRedundantOrders()) { + return true; + } + Point destPoint = driveOrder.getRoute().getFinalDestinationPoint(); + String destOp = driveOrder.getDestination().getOperation(); + // We use startsWith(OP_NOP) here because that makes it possible to have + // multiple different operations ("NOP.*") that all do nothing. + if (destPoint.getReference().equals(vehicle.getCurrentPosition()) + && (destOp.startsWith(DriveOrder.Destination.OP_NOP) + || destOp.equals(DriveOrder.Destination.OP_MOVE))) { + return false; + } + return true; + } + + /** + * Let a given vehicle abort any order it may currently be processing. + * + * @param vehicle The vehicle which should abort its order. + * @param immediateAbort Whether to abort the order immediately instead of + * just withdrawing it for a smooth abortion. + */ + public void abortOrder(Vehicle vehicle, boolean immediateAbort) { + requireNonNull(vehicle, "vehicle"); + + if (vehicle.getTransportOrder() == null) { + return; + } + + abortAssignedOrder( + transportOrderService.fetchObject(TransportOrder.class, vehicle.getTransportOrder()), + vehicle, + immediateAbort + ); + } + + public void abortOrder(TransportOrder order, boolean immediateAbort) { + requireNonNull(order, "order"); + + if (order.getState().isFinalState()) { + LOG.info( + "Transport order '{}' already in final state '{}', skipping withdrawal.", + order.getName(), + order.getState() + ); + return; + } + + if (order.getProcessingVehicle() == null) { + updateTransportOrderState(order.getReference(), TransportOrder.State.FAILED); + } + else { + abortAssignedOrder( + order, + vehicleService.fetchObject(Vehicle.class, order.getProcessingVehicle()), + immediateAbort + ); + } + } + + public void finishAbortion(Vehicle vehicle) { + finishAbortion(vehicle.getTransportOrder(), vehicle); + } + + private void finishAbortion(TCSObjectReference<TransportOrder> orderRef, Vehicle vehicle) { + requireNonNull(orderRef, "orderRef"); + requireNonNull(vehicle, "vehicle"); + + LOG.debug("{}: Aborted order {}", vehicle.getName(), orderRef.getName()); + + // The current transport order has been aborted - update its state + // and that of the vehicle. + updateTransportOrderState(orderRef, TransportOrder.State.FAILED); + + vehicleService.updateVehicleProcState(vehicle.getReference(), Vehicle.ProcState.IDLE); + vehicleService.updateVehicleTransportOrder(vehicle.getReference(), null); + + // Let the router know that the vehicle doesn't have a route any more. + router.selectRoute(vehicle, null); + } + + /** + * Aborts a given transport order known to be assigned to a given vehicle. + * + * @param vehicle The vehicle the order is assigned to. + * @param order The order. + * @param immediateAbort Whether to abort the order immediately instead of + * just withdrawing it for a smooth abortion. + */ + private void abortAssignedOrder( + TransportOrder order, + Vehicle vehicle, + boolean immediateAbort + ) { + requireNonNull(order, "order"); + requireNonNull(vehicle, "vehicle"); + checkArgument( + !order.getState().isFinalState(), + "%s: Order already in final state: %s", + vehicle.getName(), + order.getName() + ); + + // Mark the order as withdrawn so we can react appropriately when the + // vehicle reports the remaining movements as finished. + updateTransportOrderState(order.getReference(), TransportOrder.State.WITHDRAWN); + + VehicleController vehicleController + = vehicleControllerPool.getVehicleController(vehicle.getName()); + + if (immediateAbort) { + LOG.info("{}: Immediate abort of transport order {}...", vehicle.getName(), order.getName()); + vehicleController.abortTransportOrder(true); + finishAbortion(order.getReference(), vehicle); + } + else { + vehicleController.abortTransportOrder(false); + } + } + + /** + * Properly marks a transport order as FINISHED, also updating its wrapping sequence, if any. + * + * @param ref A reference to the transport order to be modified. + */ + private void markOrderAndSequenceAsFinished(TCSObjectReference<TransportOrder> ref) { + requireNonNull(ref, "ref"); + + TransportOrder order = transportOrderService.fetchObject(TransportOrder.class, ref); + Optional<OrderSequence> osOpt = extractWrappingSequence(order); + + // Sanity check: The finished order must be the next one in the sequence. + osOpt.ifPresent( + seq -> checkArgument( + ref.equals(seq.getNextUnfinishedOrder()), + "TO %s != next unfinished TO %s in sequence %s", + ref, + seq.getNextUnfinishedOrder(), + seq + ) + ); + + transportOrderService.updateTransportOrderState(ref, TransportOrder.State.FINISHED); + + osOpt.ifPresent(seq -> { + transportOrderService.updateOrderSequenceFinishedIndex( + seq.getReference(), + seq.getFinishedIndex() + 1 + ); + + // Finish the order sequence, using an up-to-date copy. + finishOrderSequence( + transportOrderService.fetchObject( + OrderSequence.class, + seq.getReference() + ) + ); + }); + } + + /** + * Properly marks a transport order as FAILED, also updating its wrapping sequence, if any. + * + * @param ref A reference to the transport order to be modified. + */ + private void markOrderAndSequenceAsFailed(TCSObjectReference<TransportOrder> ref) { + requireNonNull(ref, "ref"); + + TransportOrder failedOrder = transportOrderService.fetchObject(TransportOrder.class, ref); + transportOrderService.updateTransportOrderState(ref, TransportOrder.State.FAILED); + + Optional<OrderSequence> osOpt = extractWrappingSequence(failedOrder); + osOpt.ifPresent(seq -> { + if (seq.isFailureFatal()) { + // Mark the sequence as complete to make sure no further orders are added. + transportOrderService.markOrderSequenceComplete(seq.getReference()); + // Mark all orders of the sequence that are not in a final state as FAILED. + seq.getOrders().stream() + .map(curRef -> transportOrderService.fetchObject(TransportOrder.class, curRef)) + .filter(o -> !o.getState().isFinalState()) + .forEach( + o -> transportOrderService.updateTransportOrderState( + o.getReference(), + TransportOrder.State.FAILED + ) + ); + // Move the finished index of the sequence to its end. + transportOrderService.updateOrderSequenceFinishedIndex( + seq.getReference(), + seq.getOrders().size() - 1 + ); + } + else { + // Since failure of an order in the sequence is not fatal, increment the + // finished index of the sequence by one to move to the next order. + transportOrderService.updateOrderSequenceFinishedIndex( + seq.getReference(), + seq.getFinishedIndex() + 1 + ); + } + + // Finish the order sequence, using an up-to-date copy. + finishOrderSequence( + transportOrderService.fetchObject( + OrderSequence.class, + failedOrder.getWrappingSequence() + ) + ); + }); + } + + private void finishOrderSequence(OrderSequence sequence) { + // Mark the sequence as finished if there's nothing more to do in it. + if (sequence.isComplete() && sequence.getNextUnfinishedOrder() == null) { + transportOrderService.markOrderSequenceFinished(sequence.getReference()); + // If the sequence was assigned to a vehicle, reset its back reference + // on the sequence to make it available for orders again. + if (sequence.getProcessingVehicle() != null) { + vehicleService.updateVehicleOrderSequence(sequence.getProcessingVehicle(), null); + } + } + } + + private Optional<OrderSequence> extractWrappingSequence(TransportOrder order) { + return order.getWrappingSequence() == null + ? Optional.empty() + : Optional.of( + transportOrderService.fetchObject( + OrderSequence.class, + order.getWrappingSequence() + ) + ); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/AssignReservedOrdersPhase.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/AssignReservedOrdersPhase.java new file mode 100644 index 0000000..3a5ef3d --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/AssignReservedOrdersPhase.java @@ -0,0 +1,168 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Objects; +import java.util.Optional; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.OrderReservationPool; +import org.opentcs.strategies.basic.dispatching.Phase; +import org.opentcs.strategies.basic.dispatching.TransportOrderUtil; +import org.opentcs.strategies.basic.dispatching.selection.candidates.CompositeAssignmentCandidateSelectionFilter; + +/** + * Assigns reserved transport orders (if any) to vehicles that have just finished their withdrawn + * ones. + */ +public class AssignReservedOrdersPhase + implements + Phase { + + /** + * The object service + */ + private final TCSObjectService objectService; + /** + * The Router instance calculating route costs. + */ + private final Router router; + /** + * A collection of predicates for filtering assignment candidates. + */ + private final CompositeAssignmentCandidateSelectionFilter assignmentCandidateSelectionFilter; + /** + * Stores reservations of orders for vehicles. + */ + private final OrderReservationPool orderReservationPool; + + private final TransportOrderUtil transportOrderUtil; + /** + * Indicates whether this component is initialized. + */ + private boolean initialized; + + @Inject + public AssignReservedOrdersPhase( + TCSObjectService objectService, + Router router, + CompositeAssignmentCandidateSelectionFilter assignmentCandidateSelectionFilter, + OrderReservationPool orderReservationPool, + TransportOrderUtil transportOrderUtil + ) { + this.router = requireNonNull(router, "router"); + this.objectService = requireNonNull(objectService, "objectService"); + this.assignmentCandidateSelectionFilter = requireNonNull( + assignmentCandidateSelectionFilter, + "assignmentCandidateSelectionFilter" + ); + this.orderReservationPool = requireNonNull(orderReservationPool, "orderReservationPool"); + this.transportOrderUtil = requireNonNull(transportOrderUtil, "transportOrderUtil"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + initialized = false; + } + + @Override + public void run() { + for (Vehicle vehicle : objectService.fetchObjects(Vehicle.class)) { + if (availableForReservedOrders(vehicle)) { + checkForReservedOrder(vehicle); + } + else if (unusableForReservedOrders(vehicle)) { + orderReservationPool.removeReservations(vehicle.getReference()); + } + } + } + + private void checkForReservedOrder(Vehicle vehicle) { + // Check if there's an order reserved for this vehicle that is in an assignable state. If yes, + // try to assign that. + // Note that we expect no more than a single reserved order, and remove ALL reservations if we + // find at least one, even if it cannot be processed by the vehicle in the end. + orderReservationPool.findReservations(vehicle.getReference()).stream() + .map(orderRef -> objectService.fetchObject(TransportOrder.class, orderRef)) + .filter(order -> order.hasState(TransportOrder.State.DISPATCHABLE)) + // A transport order's intended vehicle can change after its creation and also after + // reservation. Only handle orders where the intended vehicle (still) fits the reservation. + .filter(order -> hasNoOrMatchingIntendedVehicle(order, vehicle)) + .limit(1) + .map( + order -> computeCandidate( + vehicle, + objectService.fetchObject( + Point.class, + vehicle.getCurrentPosition() + ), + order + ) + ) + .filter(optCandidate -> optCandidate.isPresent()) + .map(optCandidate -> optCandidate.get()) + .filter(candidate -> assignmentCandidateSelectionFilter.apply(candidate).isEmpty()) + .findFirst() + .ifPresent( + candidate -> transportOrderUtil.assignTransportOrder( + vehicle, + candidate.getTransportOrder(), + candidate.getDriveOrders() + ) + ); + + // Regardless of whether a reserved order could be assigned to the vehicle or not, remove any + // reservations for the vehicle and allow it to be reserved (again) in the subsequent dispatcher + // phases. + orderReservationPool.removeReservations(vehicle.getReference()); + } + + private boolean availableForReservedOrders(Vehicle vehicle) { + return vehicle.hasProcState(Vehicle.ProcState.IDLE) + && (vehicle.hasState(Vehicle.State.IDLE) + || vehicle.hasState(Vehicle.State.CHARGING)) + && vehicle.getCurrentPosition() != null + && vehicle.getIntegrationLevel() == Vehicle.IntegrationLevel.TO_BE_UTILIZED; + } + + private boolean unusableForReservedOrders(Vehicle vehicle) { + return vehicle.getIntegrationLevel() != Vehicle.IntegrationLevel.TO_BE_UTILIZED; + } + + private boolean hasNoOrMatchingIntendedVehicle(TransportOrder order, Vehicle vehicle) { + return order.getIntendedVehicle() == null + || Objects.equals(order.getIntendedVehicle(), vehicle.getReference()); + } + + private Optional<AssignmentCandidate> computeCandidate( + Vehicle vehicle, + Point vehiclePosition, + TransportOrder order + ) { + return router.getRoute(vehicle, vehiclePosition, order) + .map(driveOrders -> new AssignmentCandidate(vehicle, order, driveOrders)); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/AssignSequenceSuccessorsPhase.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/AssignSequenceSuccessorsPhase.java new file mode 100644 index 0000000..e3c187c --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/AssignSequenceSuccessorsPhase.java @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Optional; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.Phase; +import org.opentcs.strategies.basic.dispatching.TransportOrderUtil; +import org.opentcs.strategies.basic.dispatching.selection.candidates.CompositeAssignmentCandidateSelectionFilter; + +/** + * Assigns vehicles to the next transport orders in their respective order sequences, if any. + */ +public class AssignSequenceSuccessorsPhase + implements + Phase { + + /** + * The object service + */ + private final TCSObjectService objectService; + /** + * The Router instance calculating route costs. + */ + private final Router router; + /** + * A collection of predicates for filtering assignment candidates. + */ + private final CompositeAssignmentCandidateSelectionFilter assignmentCandidateSelectionFilter; + + private final TransportOrderUtil transportOrderUtil; + /** + * Indicates whether this component is initialized. + */ + private boolean initialized; + + @Inject + public AssignSequenceSuccessorsPhase( + TCSObjectService objectService, + Router router, + CompositeAssignmentCandidateSelectionFilter assignmentCandidateSelectionFilter, + TransportOrderUtil transportOrderUtil + ) { + this.router = requireNonNull(router, "router"); + this.objectService = requireNonNull(objectService, "objectService"); + this.assignmentCandidateSelectionFilter = requireNonNull( + assignmentCandidateSelectionFilter, + "assignmentCandidateSelectionFilter" + ); + this.transportOrderUtil = requireNonNull(transportOrderUtil, "transportOrderUtil"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + initialized = false; + } + + @Override + public void run() { + for (Vehicle vehicle : objectService.fetchObjects( + Vehicle.class, + this::readyForNextInSequence + )) { + tryAssignNextOrderInSequence(vehicle); + } + } + + private void tryAssignNextOrderInSequence(Vehicle vehicle) { + nextOrderInCurrentSequence(vehicle) + .map(order -> computeCandidate(vehicle, order)) + .filter(candidate -> assignmentCandidateSelectionFilter.apply(candidate).isEmpty()) + .ifPresent( + candidate -> transportOrderUtil.assignTransportOrder( + vehicle, + candidate.getTransportOrder(), + candidate.getDriveOrders() + ) + ); + } + + private AssignmentCandidate computeCandidate(Vehicle vehicle, TransportOrder order) { + return router.getRoute( + vehicle, + objectService.fetchObject(Point.class, vehicle.getCurrentPosition()), + order + ) + .map(driveOrders -> new AssignmentCandidate(vehicle, order, driveOrders)) + .orElse(null); + } + + private Optional<TransportOrder> nextOrderInCurrentSequence(Vehicle vehicle) { + OrderSequence seq = objectService.fetchObject(OrderSequence.class, vehicle.getOrderSequence()); + + // If the order sequence's next order is not available, yet, the vehicle should wait for it. + if (seq.getNextUnfinishedOrder() == null) { + return Optional.empty(); + } + + // Return the next order to be processed for the sequence. + return Optional.of( + objectService.fetchObject( + TransportOrder.class, + seq.getNextUnfinishedOrder() + ) + ); + } + + private boolean readyForNextInSequence(Vehicle vehicle) { + return vehicle.getIntegrationLevel() == Vehicle.IntegrationLevel.TO_BE_UTILIZED + && vehicle.hasProcState(Vehicle.ProcState.IDLE) + && vehicle.hasState(Vehicle.State.IDLE) + && vehicle.getCurrentPosition() != null + && vehicle.getOrderSequence() != null; + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/AssignmentState.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/AssignmentState.java new file mode 100644 index 0000000..1a3d3b3 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/AssignmentState.java @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; + +/** + * The result of trying to assign a set of vehicles/transport orders. + */ +public class AssignmentState { + + private final List<AssignmentCandidate> assignedCandidates = new ArrayList<>(); + private final List<AssignmentCandidate> reservedCandidates = new ArrayList<>(); + private final Map<TransportOrder, OrderFilterResult> filteredOrders = new HashMap<>(); + + public AssignmentState() { + } + + public List<AssignmentCandidate> getAssignedCandidates() { + return assignedCandidates; + } + + public List<AssignmentCandidate> getReservedCandidates() { + return reservedCandidates; + } + + public Map<TransportOrder, OrderFilterResult> getFilteredOrders() { + return filteredOrders; + } + + public void addFilteredOrder(OrderFilterResult filterResult) { + TransportOrder order = filterResult.getOrder(); + OrderFilterResult result + = filteredOrders.getOrDefault( + order, + new OrderFilterResult(order, new ArrayList<>()) + ); + result.getFilterReasons().addAll(filterResult.getFilterReasons()); + filteredOrders.put(order, result); + } + + /** + * Checks whether the given transport order is still assignable, taking into account the current + * assignment results. + * + * @param order The transport order to check. + * @return {@code true}, if the given transport order was not yet assigned or reserved, otherwise + * {@code false}. + */ + public boolean wasAssignedToVehicle(TransportOrder order) { + return Stream.concat(assignedCandidates.stream(), reservedCandidates.stream()) + .anyMatch(candidate -> Objects.equals(candidate.getTransportOrder(), order)); + } + + /** + * Checks whether the given vehicle is still assignable, taking into account the current + * assignment results. + * + * @param vehicle The vehicle to check. + * @return {@code true}, if a transport order was not yet assigned to or reserved for the given + * vehicle, otherwise {@code false}. + */ + public boolean wasAssignedToOrder(Vehicle vehicle) { + return Stream.concat(assignedCandidates.stream(), reservedCandidates.stream()) + .anyMatch(candidate -> Objects.equals(candidate.getVehicle(), vehicle)); + } + + public boolean wasFiltered(TransportOrder order) { + return filteredOrders.containsKey(order); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/CandidateFilterResult.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/CandidateFilterResult.java new file mode 100644 index 0000000..3b6954b --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/CandidateFilterResult.java @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase; + +import static java.util.Objects.requireNonNull; + +import java.util.Collection; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; + +/** + * The result of an assignment candidate filter operation. + */ +public class CandidateFilterResult { + + private final AssignmentCandidate candidate; + + private final Collection<String> filterReasons; + + public CandidateFilterResult(AssignmentCandidate candidate, Collection<String> filterReasons) { + this.candidate = requireNonNull(candidate, "candidate"); + this.filterReasons = requireNonNull(filterReasons, "filterReasons"); + } + + public AssignmentCandidate getCandidate() { + return candidate; + } + + public Collection<String> getFilterReasons() { + return filterReasons; + } + + public boolean isFiltered() { + return !filterReasons.isEmpty(); + } + + public OrderFilterResult toFilterResult() { + return new OrderFilterResult(candidate.getTransportOrder(), filterReasons); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/CheckNewOrdersPhase.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/CheckNewOrdersPhase.java new file mode 100644 index 0000000..df0898a --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/CheckNewOrdersPhase.java @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration; +import org.opentcs.strategies.basic.dispatching.Phase; +import org.opentcs.strategies.basic.dispatching.TransportOrderUtil; + +/** + * Checks for transport orders that are still in state RAW, and attempts to prepare them for + * assignment. + */ +public class CheckNewOrdersPhase + implements + Phase { + + /** + * The object service + */ + private final TCSObjectService objectService; + /** + * The Router instance calculating route costs. + */ + private final Router router; + private final TransportOrderUtil transportOrderUtil; + /** + * The dispatcher configuration. + */ + private final DefaultDispatcherConfiguration configuration; + /** + * Indicates whether this component is initialized. + */ + private boolean initialized; + + @Inject + public CheckNewOrdersPhase( + TCSObjectService objectService, + Router router, + TransportOrderUtil transportOrderUtil, + DefaultDispatcherConfiguration configuration + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.router = requireNonNull(router, "router"); + this.transportOrderUtil = requireNonNull(transportOrderUtil, "transportOrderUtil"); + this.configuration = requireNonNull(configuration, "configuration"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + initialized = false; + } + + @Override + public void run() { + objectService.fetchObjects(TransportOrder.class, this::inRawState).stream() + .forEach(order -> checkRawTransportOrder(order)); + } + + private void checkRawTransportOrder(TransportOrder order) { + requireNonNull(order, "order"); + + // Check if the transport order is routable. + if (configuration.dismissUnroutableTransportOrders() + && !router.checkGeneralRoutability(order)) { + transportOrderUtil.updateTransportOrderState( + order.getReference(), + TransportOrder.State.UNROUTABLE + ); + return; + } + transportOrderUtil.updateTransportOrderState( + order.getReference(), + TransportOrder.State.ACTIVE + ); + // The transport order has been activated - dispatch it. + // Check if it has unfinished dependencies. + if (!transportOrderUtil.hasUnfinishedDependencies(order)) { + transportOrderUtil.updateTransportOrderState( + order.getReference(), + TransportOrder.State.DISPATCHABLE + ); + } + } + + private boolean inRawState(TransportOrder order) { + return order.hasState(TransportOrder.State.RAW); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/FinishWithdrawalsPhase.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/FinishWithdrawalsPhase.java new file mode 100644 index 0000000..f15affd --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/FinishWithdrawalsPhase.java @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.Phase; +import org.opentcs.strategies.basic.dispatching.TransportOrderUtil; + +/** + * Finishes withdrawals of transport orders after the vehicle has come to a halt. + */ +public class FinishWithdrawalsPhase + implements + Phase { + + /** + * The object service + */ + private final TCSObjectService objectService; + private final TransportOrderUtil transportOrderUtil; + /** + * Indicates whether this component is initialized. + */ + private boolean initialized; + + @Inject + public FinishWithdrawalsPhase( + TCSObjectService objectService, + TransportOrderUtil transportOrderUtil + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.transportOrderUtil = requireNonNull(transportOrderUtil, "transportOrderUtil"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + initialized = false; + } + + @Override + public void run() { + objectService.fetchObjects(Vehicle.class).stream() + .filter(vehicle -> vehicle.hasProcState(Vehicle.ProcState.AWAITING_ORDER)) + .filter(vehicle -> hasWithdrawnTransportOrder(vehicle)) + .forEach(vehicle -> transportOrderUtil.finishAbortion(vehicle)); + } + + private boolean hasWithdrawnTransportOrder(Vehicle vehicle) { + return objectService.fetchObject(TransportOrder.class, vehicle.getTransportOrder()) + .hasState(TransportOrder.State.WITHDRAWN); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/OrderFilterResult.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/OrderFilterResult.java new file mode 100644 index 0000000..4ff51b7 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/OrderFilterResult.java @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase; + +import static java.util.Objects.requireNonNull; + +import java.util.Collection; +import org.opentcs.data.order.TransportOrder; + +/** + * The result of a transport order filter operation. + */ +public class OrderFilterResult { + + private final TransportOrder order; + + private final Collection<String> filterReasons; + + public OrderFilterResult(TransportOrder order, Collection<String> filterReasons) { + this.order = requireNonNull(order, "order"); + this.filterReasons = requireNonNull(filterReasons, "filterReasons"); + } + + public TransportOrder getOrder() { + return order; + } + + public Collection<String> getFilterReasons() { + return filterReasons; + } + + public boolean isFiltered() { + return !filterReasons.isEmpty(); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/VehicleFilterResult.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/VehicleFilterResult.java new file mode 100644 index 0000000..8f25929 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/VehicleFilterResult.java @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase; + +import static java.util.Objects.requireNonNull; + +import java.util.Collection; +import org.opentcs.data.model.Vehicle; + +/** + * The result of a vehicle filter operation. + */ +public class VehicleFilterResult { + + private final Vehicle vehicle; + + private final Collection<String> filterReasons; + + public VehicleFilterResult(Vehicle vehicle, Collection<String> filterReasons) { + this.vehicle = requireNonNull(vehicle, "vehicle"); + this.filterReasons = requireNonNull(filterReasons, "filterReasons"); + } + + public Vehicle getVehicle() { + return vehicle; + } + + public Collection<String> getFilterReasons() { + return filterReasons; + } + + public boolean isFiltered() { + return !filterReasons.isEmpty(); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/assignment/AssignFreeOrdersPhase.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/assignment/AssignFreeOrdersPhase.java new file mode 100644 index 0000000..6e570dd --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/assignment/AssignFreeOrdersPhase.java @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.Phase; +import org.opentcs.strategies.basic.dispatching.phase.OrderFilterResult; +import org.opentcs.strategies.basic.dispatching.phase.VehicleFilterResult; +import org.opentcs.strategies.basic.dispatching.selection.orders.CompositeTransportOrderSelectionFilter; +import org.opentcs.strategies.basic.dispatching.selection.orders.IsFreelyDispatchableToAnyVehicle; +import org.opentcs.strategies.basic.dispatching.selection.vehicles.CompositeVehicleSelectionFilter; +import org.opentcs.strategies.basic.dispatching.selection.vehicles.IsAvailableForAnyOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Assigns transport orders to vehicles that are currently not processing any and are not bound to + * any order sequences. + */ +public class AssignFreeOrdersPhase + implements + Phase { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(AssignFreeOrdersPhase.class); + /** + * The object service. + */ + private final TCSObjectService objectService; + /** + * A collection of predicates for filtering vehicles. + */ + private final CompositeVehicleSelectionFilter vehicleSelectionFilter; + + private final IsAvailableForAnyOrder isAvailableForAnyOrder; + + private final IsFreelyDispatchableToAnyVehicle isFreelyDispatchableToAnyVehicle; + /** + * A collection of predicates for filtering transport orders. + */ + private final CompositeTransportOrderSelectionFilter transportOrderSelectionFilter; + /** + * Handles assignments of transport orders to vehicles. + */ + private final OrderAssigner orderAssigner; + /** + * Provides methods to check and update the dispatching status of transport orders. + */ + private final DispatchingStatusMarker dispatchingStatusMarker; + /** + * Indicates whether this component is initialized. + */ + private boolean initialized; + + @Inject + public AssignFreeOrdersPhase( + TCSObjectService objectService, + CompositeVehicleSelectionFilter vehicleSelectionFilter, + IsAvailableForAnyOrder isAvailableForAnyOrder, + IsFreelyDispatchableToAnyVehicle isFreelyDispatchableToAnyVehicle, + CompositeTransportOrderSelectionFilter transportOrderSelectionFilter, + OrderAssigner orderAssigner, + DispatchingStatusMarker dispatchingStatusMarker + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.vehicleSelectionFilter = requireNonNull(vehicleSelectionFilter, "vehicleSelectionFilter"); + this.isAvailableForAnyOrder = requireNonNull(isAvailableForAnyOrder, "isAvailableForAnyOrder"); + this.isFreelyDispatchableToAnyVehicle = requireNonNull( + isFreelyDispatchableToAnyVehicle, + "isFreelyDispatchableToAnyVehicle" + ); + this.transportOrderSelectionFilter = requireNonNull( + transportOrderSelectionFilter, + "transportOrderSelectionFilter" + ); + this.orderAssigner = requireNonNull(orderAssigner, "orderAssigner"); + this.dispatchingStatusMarker = requireNonNull( + dispatchingStatusMarker, + "dispatchingStatusMarker" + ); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + initialized = false; + } + + @Override + public void run() { + Map<Boolean, List<VehicleFilterResult>> vehiclesSplitByFilter + = objectService.fetchObjects(Vehicle.class, isAvailableForAnyOrder) + .stream() + .map(vehicle -> new VehicleFilterResult(vehicle, vehicleSelectionFilter.apply(vehicle))) + .collect(Collectors.partitioningBy(filterResult -> !filterResult.isFiltered())); + + Collection<Vehicle> availableVehicles = vehiclesSplitByFilter.get(Boolean.TRUE).stream() + .map(VehicleFilterResult::getVehicle) + .collect(Collectors.toList()); + + if (availableVehicles.isEmpty()) { + LOG.debug("No vehicles available, skipping potentially expensive fetching of orders."); + return; + } + + // Select only dispatchable orders first, then apply the composite filter, handle + // the orders that can be tried as usual and mark the others as filtered (if they aren't, yet). + Map<Boolean, List<OrderFilterResult>> ordersSplitByFilter + = objectService.fetchObjects(TransportOrder.class, isFreelyDispatchableToAnyVehicle) + .stream() + .map(order -> new OrderFilterResult(order, transportOrderSelectionFilter.apply(order))) + .collect(Collectors.partitioningBy(filterResult -> !filterResult.isFiltered())); + + markNewlyFilteredOrders(ordersSplitByFilter.get(Boolean.FALSE)); + + orderAssigner.tryAssignments( + availableVehicles, + ordersSplitByFilter.get(Boolean.TRUE).stream() + .map(OrderFilterResult::getOrder) + .collect(Collectors.toList()) + ); + } + + private void markNewlyFilteredOrders(Collection<OrderFilterResult> filterResults) { + filterResults.stream() + .filter( + filterResult -> (!dispatchingStatusMarker.isOrderMarkedAsDeferred( + filterResult.getOrder() + ) + || dispatchingStatusMarker.haveDeferralReasonsForOrderChanged(filterResult)) + ) + .forEach(dispatchingStatusMarker::markOrderAsDeferred); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/assignment/AssignNextDriveOrdersPhase.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/assignment/AssignNextDriveOrdersPhase.java new file mode 100644 index 0000000..7467244 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/assignment/AssignNextDriveOrdersPhase.java @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.InternalTransportOrderService; +import org.opentcs.components.kernel.services.InternalVehicleService; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.drivers.vehicle.VehicleControllerPool; +import org.opentcs.strategies.basic.dispatching.Phase; +import org.opentcs.strategies.basic.dispatching.TransportOrderUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Assigns the next drive order to each vehicle waiting for it, or finishes the respective transport + * order if the vehicle has finished its last drive order. + */ +public class AssignNextDriveOrdersPhase + implements + Phase { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(AssignNextDriveOrdersPhase.class); + private final InternalTransportOrderService transportOrderService; + private final InternalVehicleService vehicleService; + /** + * The Router instance calculating route costs. + */ + private final Router router; + /** + * The vehicle controller pool. + */ + private final VehicleControllerPool vehicleControllerPool; + private final TransportOrderUtil transportOrderUtil; + /** + * Indicates whether this component is initialized. + */ + private boolean initialized; + + @Inject + public AssignNextDriveOrdersPhase( + InternalTransportOrderService transportOrderService, + InternalVehicleService vehicleService, + Router router, + VehicleControllerPool vehicleControllerPool, + TransportOrderUtil transportOrderUtil + ) { + this.transportOrderService = requireNonNull(transportOrderService, "transportOrderService"); + this.vehicleService = requireNonNull(vehicleService, "vehicleService"); + this.router = requireNonNull(router, "router"); + this.vehicleControllerPool = requireNonNull(vehicleControllerPool, "vehicleControllerPool"); + this.transportOrderUtil = requireNonNull(transportOrderUtil, "transportOrderUtil"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + initialized = false; + } + + @Override + public void run() { + transportOrderService.fetchObjects(Vehicle.class).stream() + .filter(vehicle -> vehicle.hasProcState(Vehicle.ProcState.AWAITING_ORDER)) + .forEach(vehicle -> checkForNextDriveOrder(vehicle)); + } + + private void checkForNextDriveOrder(Vehicle vehicle) { + LOG.debug("Vehicle '{}' finished a drive order.", vehicle.getName()); + // The vehicle is processing a transport order and has finished a drive order. + // See if there's another drive order to be processed. + transportOrderService.updateTransportOrderNextDriveOrder(vehicle.getTransportOrder()); + TransportOrder vehicleOrder = transportOrderService.fetchObject( + TransportOrder.class, + vehicle.getTransportOrder() + ); + if (vehicleOrder.getCurrentDriveOrder() == null) { + LOG.debug( + "Vehicle '{}' finished transport order '{}'", + vehicle.getName(), + vehicleOrder.getName() + ); + // The current transport order has been finished - update its state and that of the vehicle. + transportOrderUtil.updateTransportOrderState( + vehicle.getTransportOrder(), + TransportOrder.State.FINISHED + ); + // Update the vehicle's procState, implicitly dispatching it again. + vehicleService.updateVehicleProcState(vehicle.getReference(), Vehicle.ProcState.IDLE); + vehicleService.updateVehicleTransportOrder(vehicle.getReference(), null); + // Let the router know that the vehicle doesn't have a route any more. + router.selectRoute(vehicle, null); + // Update transport orders that are dispatchable now that this one has been finished. + transportOrderUtil.markNewDispatchableOrders(); + } + else { + LOG.debug("Assigning next drive order to vehicle '{}'...", vehicle.getName()); + if (transportOrderUtil.mustAssign(vehicleOrder.getCurrentDriveOrder(), vehicle)) { + // Get an up-to-date copy of the transport order in case the route changed. + vehicleOrder = transportOrderService.fetchObject( + TransportOrder.class, + vehicle.getTransportOrder() + ); + + // Let the vehicle controller know about the new drive order. + vehicleControllerPool.getVehicleController(vehicle.getName()) + .setTransportOrder(vehicleOrder); + + // The vehicle is still processing a transport order. + vehicleService.updateVehicleProcState( + vehicle.getReference(), + Vehicle.ProcState.PROCESSING_ORDER + ); + } + // If the drive order need not be assigned, immediately check for another one. + else { + vehicleService.updateVehicleProcState( + vehicle.getReference(), + Vehicle.ProcState.AWAITING_ORDER + ); + checkForNextDriveOrder(vehicle); + } + } + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/assignment/DispatchingStatusMarker.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/assignment/DispatchingStatusMarker.java new file mode 100644 index 0000000..8ee6ab1 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/assignment/DispatchingStatusMarker.java @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.data.order.TransportOrderHistoryCodes.ORDER_ASSIGNED_TO_VEHICLE; +import static org.opentcs.data.order.TransportOrderHistoryCodes.ORDER_DISPATCHING_DEFERRED; +import static org.opentcs.data.order.TransportOrderHistoryCodes.ORDER_DISPATCHING_RESUMED; +import static org.opentcs.data.order.TransportOrderHistoryCodes.ORDER_RESERVED_FOR_VEHICLE; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.phase.OrderFilterResult; + +/** + * Provides methods to check and update the dispatching status of transport orders. + * <p> + * Examples for a transport order's dispatching status: + * </p> + * <ul> + * <li>Dispatching of a transport order has been deferred.</li> + * <li>A transport order has been assigned to a vehicle.</li> + * </ul> + * + * @author Martin Grzenia (Fraunhofer IML) + */ +public class DispatchingStatusMarker { + + private final TCSObjectService objectService; + + /** + * Creates a new instance. + * + * @param objectService The object service to use. + */ + @Inject + public DispatchingStatusMarker(TCSObjectService objectService) { + this.objectService = requireNonNull(objectService, "objectService"); + } + + /** + * Marks the {@link TransportOrder} referenced in the given {@link OrderFilterResult} as + * deferred. + * + * @param filterResult The filter result. + */ + public void markOrderAsDeferred(OrderFilterResult filterResult) { + objectService.appendObjectHistoryEntry( + filterResult.getOrder().getReference(), + new ObjectHistory.Entry( + ORDER_DISPATCHING_DEFERRED, + Collections.unmodifiableList(new ArrayList<>(filterResult.getFilterReasons())) + ) + ); + } + + /** + * Marks the given {@link TransportOrder} as resumed. + * + * @param transportOrder The transport order. + */ + public void markOrderAsResumed(TransportOrder transportOrder) { + objectService.appendObjectHistoryEntry( + transportOrder.getReference(), + new ObjectHistory.Entry( + ORDER_DISPATCHING_RESUMED, + Collections.unmodifiableList(new ArrayList<>()) + ) + ); + } + + /** + * Checks whether the given {@link TransportOrder} is marked as deferred regarding dispatching. + * + * @param transportOrder The transport order. + * @return {@code true}, if the {@link ObjectHistory} of the given transport order indicates that + * the transport order is currently being deferred regarding dispatching, otherwise {@code false}. + */ + public boolean isOrderMarkedAsDeferred(TransportOrder transportOrder) { + return lastRelevantDeferredHistoryEntry(transportOrder).isPresent(); + } + + /** + * Marks the given {@link TransportOrder} as being assigned to the given {@link Vehicle}. + * + * @param transportOrder The transport order. + * @param vehicle The vehicle. + */ + public void markOrderAsAssigned(TransportOrder transportOrder, Vehicle vehicle) { + objectService.appendObjectHistoryEntry( + transportOrder.getReference(), + new ObjectHistory.Entry(ORDER_ASSIGNED_TO_VEHICLE, vehicle.getName()) + ); + } + + /** + * Marks the given {@link TransportOrder} as being reserved for the given {@link Vehicle}. + * + * @param transportOrder The transport order. + * @param vehicle The vehicle. + */ + public void markOrderAsReserved(TransportOrder transportOrder, Vehicle vehicle) { + objectService.appendObjectHistoryEntry( + transportOrder.getReference(), + new ObjectHistory.Entry(ORDER_RESERVED_FOR_VEHICLE, vehicle.getName()) + ); + } + + /** + * Checks whether the reasons for deferral of the {@link TransportOrder} referenced in the given + * {@link OrderFilterResult} have changed in comparison to the (new) + * {@link OrderFilterResult#getFilterReasons()}. + * + * @param filterResult The filter result. + * @return {@code true}, if the reasons for deferral have changed, otherwise {@code false}. + */ + @SuppressWarnings("unchecked") + public boolean haveDeferralReasonsForOrderChanged(OrderFilterResult filterResult) { + Collection<String> newReasons = filterResult.getFilterReasons(); + Collection<String> oldReasons = lastRelevantDeferredHistoryEntry(filterResult.getOrder()) + .map(entry -> (Collection<String>) entry.getSupplement()) + .orElse(new ArrayList<>()); + + return newReasons.size() != oldReasons.size() + || !newReasons.containsAll(oldReasons); + } + + private Optional<ObjectHistory.Entry> lastRelevantDeferredHistoryEntry( + TransportOrder transportOrder + ) { + return transportOrder.getHistory().getEntries().stream() + .filter( + entry -> equalsAny( + entry.getEventCode(), + ORDER_DISPATCHING_DEFERRED, + ORDER_DISPATCHING_RESUMED + ) + ) + .reduce((firstEntry, secondEntry) -> secondEntry) + .filter(entry -> entry.getEventCode().equals(ORDER_DISPATCHING_DEFERRED)); + } + + private boolean equalsAny(String string, String... others) { + return Arrays.asList(others).stream() + .anyMatch(other -> string.equals(other)); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/assignment/OrderAssigner.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/assignment/OrderAssigner.java new file mode 100644 index 0000000..e8244f8 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/assignment/OrderAssigner.java @@ -0,0 +1,305 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderConstants; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.OrderReservationPool; +import org.opentcs.strategies.basic.dispatching.TransportOrderUtil; +import org.opentcs.strategies.basic.dispatching.phase.AssignmentState; +import org.opentcs.strategies.basic.dispatching.phase.CandidateFilterResult; +import org.opentcs.strategies.basic.dispatching.priorization.CompositeOrderCandidateComparator; +import org.opentcs.strategies.basic.dispatching.priorization.CompositeOrderComparator; +import org.opentcs.strategies.basic.dispatching.priorization.CompositeVehicleCandidateComparator; +import org.opentcs.strategies.basic.dispatching.priorization.CompositeVehicleComparator; +import org.opentcs.strategies.basic.dispatching.selection.candidates.CompositeAssignmentCandidateSelectionFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handles assignments of transport orders to vehicles. + */ +public class OrderAssigner { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(OrderAssigner.class); + /** + * The object service. + */ + private final TCSObjectService objectService; + /** + * The Router instance calculating route costs. + */ + private final Router router; + /** + * Stores reservations of orders for vehicles. + */ + private final OrderReservationPool orderReservationPool; + /** + * Defines the order of vehicles when there are less vehicles than transport orders. + */ + private final Comparator<Vehicle> vehicleComparator; + /** + * Defines the order of transport orders when there are less transport orders than vehicles. + */ + private final Comparator<TransportOrder> orderComparator; + /** + * Sorts candidates when looking for a transport order to be assigned to a vehicle. + */ + private final Comparator<AssignmentCandidate> orderCandidateComparator; + /** + * Sorts candidates when looking for a vehicle to be assigned to a transport order. + */ + private final Comparator<AssignmentCandidate> vehicleCandidateComparator; + /** + * A collection of predicates for filtering assignment candidates. + */ + private final CompositeAssignmentCandidateSelectionFilter assignmentCandidateSelectionFilter; + + private final TransportOrderUtil transportOrderUtil; + /** + * Provides methods to check and update the dispatching status of transport orders. + */ + private final DispatchingStatusMarker dispatchingStatusMarker; + + @Inject + public OrderAssigner( + TCSObjectService objectService, + Router router, + OrderReservationPool orderReservationPool, + CompositeVehicleComparator vehicleComparator, + CompositeOrderComparator orderComparator, + CompositeOrderCandidateComparator orderCandidateComparator, + CompositeVehicleCandidateComparator vehicleCandidateComparator, + CompositeAssignmentCandidateSelectionFilter assignmentCandidateSelectionFilter, + TransportOrderUtil transportOrderUtil, + DispatchingStatusMarker dispatchingStatusMarker + ) { + this.router = requireNonNull(router, "router"); + this.objectService = requireNonNull(objectService, "objectService"); + this.orderReservationPool = requireNonNull(orderReservationPool, "orderReservationPool"); + this.vehicleComparator = requireNonNull(vehicleComparator, "vehicleComparator"); + this.orderComparator = requireNonNull(orderComparator, "orderComparator"); + this.orderCandidateComparator = requireNonNull( + orderCandidateComparator, + "orderCandidateComparator" + ); + this.vehicleCandidateComparator = requireNonNull( + vehicleCandidateComparator, + "vehicleCandidateComparator" + ); + this.assignmentCandidateSelectionFilter = requireNonNull( + assignmentCandidateSelectionFilter, + "assignmentCandidateSelectionFilter" + ); + this.transportOrderUtil = requireNonNull(transportOrderUtil, "transportOrderUtil"); + this.dispatchingStatusMarker = requireNonNull( + dispatchingStatusMarker, + "dispatchingStatusMarker" + ); + } + + /** + * Tries to assign the given tranpsort orders to the given vehicles. + * + * @param availableVehicles The vehicles available for order assignment. + * @param availableOrders The transport order available to be assigned to a vehicle. + */ + public void tryAssignments( + Collection<Vehicle> availableVehicles, + Collection<TransportOrder> availableOrders + ) { + LOG.debug( + "Available for dispatching: {} transport orders and {} vehicles.", + availableOrders.size(), + availableVehicles.size() + ); + + AssignmentState assignmentState = new AssignmentState(); + if (availableVehicles.size() < availableOrders.size()) { + availableVehicles.stream() + .sorted(vehicleComparator) + .forEach(vehicle -> tryAssignOrder(vehicle, availableOrders, assignmentState)); + } + else { + availableOrders.stream() + .sorted(orderComparator) + .forEach(order -> tryAssignVehicle(order, availableVehicles, assignmentState)); + } + + assignmentState.getFilteredOrders().values().stream() + .filter(filterResult -> !assignmentState.wasAssignedToVehicle(filterResult.getOrder())) + .filter(dispatchingStatusMarker::haveDeferralReasonsForOrderChanged) + .forEach(dispatchingStatusMarker::markOrderAsDeferred); + + availableOrders.stream() + .filter( + order -> (!assignmentState.wasFiltered(order) + && !assignmentState.wasAssignedToVehicle(order)) + ) + .filter(dispatchingStatusMarker::isOrderMarkedAsDeferred) + .forEach(dispatchingStatusMarker::markOrderAsResumed); + } + + private void tryAssignOrder( + Vehicle vehicle, + Collection<TransportOrder> availableOrders, + AssignmentState assignmentState + ) { + LOG.debug("Trying to find transport order for vehicle '{}'...", vehicle.getName()); + + Point vehiclePosition = objectService.fetchObject(Point.class, vehicle.getCurrentPosition()); + + Map<Boolean, List<CandidateFilterResult>> ordersSplitByFilter + = availableOrders.stream() + .filter( + order -> (!assignmentState.wasAssignedToVehicle(order) + && vehicleCanTakeOrder(vehicle, order) + && orderAssignableToVehicle(order, vehicle)) + ) + .map(order -> computeCandidate(vehicle, vehiclePosition, order)) + .filter(optCandidate -> optCandidate.isPresent()) + .map(optCandidate -> optCandidate.get()) + .map( + candidate -> new CandidateFilterResult( + candidate, + assignmentCandidateSelectionFilter.apply(candidate) + ) + ) + .collect(Collectors.partitioningBy(filterResult -> !filterResult.isFiltered())); + + ordersSplitByFilter.get(Boolean.FALSE).stream() + .map(CandidateFilterResult::toFilterResult) + .forEach(filterResult -> assignmentState.addFilteredOrder(filterResult)); + + ordersSplitByFilter.get(Boolean.TRUE).stream() + .map(CandidateFilterResult::getCandidate) + .sorted(orderCandidateComparator) + .findFirst() + .ifPresent(candidate -> assignOrder(candidate, assignmentState)); + } + + private void tryAssignVehicle( + TransportOrder order, + Collection<Vehicle> availableVehicles, + AssignmentState assignmentState + ) { + LOG.debug("Trying to find vehicle for transport order '{}'...", order.getName()); + + Map<Boolean, List<CandidateFilterResult>> ordersSplitByFilter + = availableVehicles.stream() + .filter( + vehicle -> (!assignmentState.wasAssignedToOrder(vehicle) + && vehicleCanTakeOrder(vehicle, order) + && orderAssignableToVehicle(order, vehicle)) + ) + .map( + vehicle -> computeCandidate( + vehicle, + objectService.fetchObject(Point.class, vehicle.getCurrentPosition()), + order + ) + ) + .filter(optCandidate -> optCandidate.isPresent()) + .map(optCandidate -> optCandidate.get()) + .map( + candidate -> new CandidateFilterResult( + candidate, + assignmentCandidateSelectionFilter.apply(candidate) + ) + ) + .collect(Collectors.partitioningBy(filterResult -> !filterResult.isFiltered())); + + ordersSplitByFilter.get(Boolean.FALSE).stream() + .map(CandidateFilterResult::toFilterResult) + .forEach(filterResult -> assignmentState.addFilteredOrder(filterResult)); + + ordersSplitByFilter.get(Boolean.TRUE).stream() + .map(CandidateFilterResult::getCandidate) + .sorted(vehicleCandidateComparator) + .findFirst() + .ifPresent(candidate -> assignOrder(candidate, assignmentState)); + } + + private void assignOrder(AssignmentCandidate candidate, AssignmentState assignmentState) { + // If the vehicle currently has a (dispensable) order, we may not assign the new one here + // directly, but must abort the old one (DefaultDispatcher.abortOrder()) and wait for the + // vehicle's ProcState to become IDLE. + if (candidate.getVehicle().getTransportOrder() == null) { + LOG.debug( + "Assigning transport order '{}' to vehicle '{}'...", + candidate.getTransportOrder().getName(), + candidate.getVehicle().getName() + ); + dispatchingStatusMarker.markOrderAsAssigned( + candidate.getTransportOrder(), + candidate.getVehicle() + ); + transportOrderUtil.assignTransportOrder( + candidate.getVehicle(), + candidate.getTransportOrder(), + candidate.getDriveOrders() + ); + assignmentState.getAssignedCandidates().add(candidate); + } + else { + LOG.debug( + "Reserving transport order '{}' for vehicle '{}'...", + candidate.getTransportOrder().getName(), + candidate.getVehicle().getName() + ); + // Remember that the new order is reserved for this vehicle. + dispatchingStatusMarker.markOrderAsReserved( + candidate.getTransportOrder(), + candidate.getVehicle() + ); + orderReservationPool.addReservation( + candidate.getTransportOrder().getReference(), + candidate.getVehicle().getReference() + ); + assignmentState.getReservedCandidates().add(candidate); + transportOrderUtil.abortOrder(candidate.getVehicle(), false); + } + } + + private Optional<AssignmentCandidate> computeCandidate( + Vehicle vehicle, + Point vehiclePosition, + TransportOrder order + ) { + return router.getRoute(vehicle, vehiclePosition, order) + .map(driveOrders -> new AssignmentCandidate(vehicle, order, driveOrders)); + } + + private boolean vehicleCanTakeOrder(Vehicle vehicle, TransportOrder order) { + return !vehicle.isEnergyLevelCritical() + || Objects.equals( + order.getAllDriveOrders().getFirst().getDestination().getOperation(), + vehicle.getRechargeOperation() + ); + } + + private boolean orderAssignableToVehicle(TransportOrder order, Vehicle vehicle) { + return (order.getIntendedVehicle() == null + || Objects.equals(order.getIntendedVehicle(), vehicle.getReference())) + && (vehicle.getAllowedOrderTypes().contains(order.getType()) + || vehicle.getAllowedOrderTypes().contains(OrderConstants.TYPE_ANY)); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/AbstractParkingPhase.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/AbstractParkingPhase.java new file mode 100644 index 0000000..433ab1b --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/AbstractParkingPhase.java @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.parking; + +import static java.util.Objects.requireNonNull; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.opentcs.access.to.order.DestinationCreationTO; +import org.opentcs.access.to.order.TransportOrderCreationTO; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.InternalTransportOrderService; +import org.opentcs.components.kernel.services.TransportOrderService; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.OrderConstants; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration; +import org.opentcs.strategies.basic.dispatching.Phase; +import org.opentcs.strategies.basic.dispatching.TransportOrderUtil; +import org.opentcs.strategies.basic.dispatching.selection.candidates.CompositeAssignmentCandidateSelectionFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The base class for parking phases. + */ +public abstract class AbstractParkingPhase + implements + Phase { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(AbstractParkingPhase.class); + /** + * The transport order service. + */ + private final InternalTransportOrderService orderService; + /** + * The strategy used for finding suitable parking positions. + */ + private final ParkingPositionSupplier parkingPosSupplier; + /** + * The Router instance calculating route costs. + */ + private final Router router; + /** + * A collection of predicates for filtering assignment candidates. + */ + private final CompositeAssignmentCandidateSelectionFilter assignmentCandidateSelectionFilter; + /** + * Provides service functions for working with transport orders. + */ + private final TransportOrderUtil transportOrderUtil; + /** + * The dispatcher configuration. + */ + private final DefaultDispatcherConfiguration configuration; + /** + * Indicates whether this component is initialized. + */ + private boolean initialized; + + public AbstractParkingPhase( + InternalTransportOrderService orderService, + ParkingPositionSupplier parkingPosSupplier, + Router router, + CompositeAssignmentCandidateSelectionFilter assignmentCandidateSelectionFilter, + TransportOrderUtil transportOrderUtil, + DefaultDispatcherConfiguration configuration + ) { + this.router = requireNonNull(router, "router"); + this.orderService = requireNonNull(orderService, "orderService"); + this.parkingPosSupplier = requireNonNull(parkingPosSupplier, "parkingPosSupplier"); + this.assignmentCandidateSelectionFilter = requireNonNull( + assignmentCandidateSelectionFilter, + "assignmentCandidateSelectionFilter" + ); + this.transportOrderUtil = requireNonNull(transportOrderUtil, "transportOrderUtil"); + this.configuration = requireNonNull(configuration, "configuration"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + parkingPosSupplier.initialize(); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + parkingPosSupplier.terminate(); + + initialized = false; + } + + public TransportOrderService getOrderService() { + return orderService; + } + + public DefaultDispatcherConfiguration getConfiguration() { + return configuration; + } + + protected void createParkingOrder(Vehicle vehicle) { + Point vehiclePosition = orderService.fetchObject(Point.class, vehicle.getCurrentPosition()); + + // Get a suitable parking position for the vehicle. + Optional<Point> parkPos = parkingPosSupplier.findParkingPosition(vehicle); + LOG.debug("Parking position for {}: {}", vehicle, parkPos); + // If we could not find a suitable parking position at all, just leave the vehicle where it is. + if (!parkPos.isPresent()) { + LOG.info("{}: Did not find a suitable parking position.", vehicle.getName()); + return; + } + // Create a destination for the point. + List<DestinationCreationTO> parkDests = Arrays.asList( + new DestinationCreationTO(parkPos.get().getName(), DriveOrder.Destination.OP_PARK) + ); + // Create a transport order for parking and verify its processability. + TransportOrder parkOrder = orderService.createTransportOrder( + new TransportOrderCreationTO("Park-", parkDests) + .withIncompleteName(true) + .withDispensable(true) + .withIntendedVehicleName(vehicle.getName()) + .withType(OrderConstants.TYPE_PARK) + ); + Optional<AssignmentCandidate> candidate = computeCandidate(vehicle, vehiclePosition, parkOrder) + .filter(c -> assignmentCandidateSelectionFilter.apply(c).isEmpty()); + // XXX Change this to Optional.ifPresentOrElse() once we're at Java 9+. + if (candidate.isPresent()) { + transportOrderUtil.assignTransportOrder( + candidate.get().getVehicle(), + candidate.get().getTransportOrder(), + candidate.get().getDriveOrders() + ); + } + else { + // Mark the order as failed, since the vehicle cannot execute it. + orderService.updateTransportOrderState(parkOrder.getReference(), TransportOrder.State.FAILED); + } + } + + private Optional<AssignmentCandidate> computeCandidate( + Vehicle vehicle, + Point vehiclePosition, + TransportOrder order + ) { + return router.getRoute(vehicle, vehiclePosition, order) + .map(driveOrders -> new AssignmentCandidate(vehicle, order, driveOrders)); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/AbstractParkingPositionSupplier.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/AbstractParkingPositionSupplier.java new file mode 100644 index 0000000..2c985f4 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/AbstractParkingPositionSupplier.java @@ -0,0 +1,214 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.parking; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nullable; +import java.util.Collections; +import java.util.Comparator; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.InternalPlantModelService; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; + +/** + * An abstract base class for parking position suppliers. + */ +public abstract class AbstractParkingPositionSupplier + implements + ParkingPositionSupplier { + + /** + * The plant model service. + */ + private final InternalPlantModelService plantModelService; + /** + * A router for computing distances to parking positions. + */ + private final Router router; + /** + * Indicates whether this component is initialized. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param plantModelService The plant model service. + * @param router A router for computing distances to parking positions. + */ + protected AbstractParkingPositionSupplier( + InternalPlantModelService plantModelService, + Router router + ) { + this.plantModelService = requireNonNull(plantModelService, "plantModelService"); + this.router = requireNonNull(router, "router"); + } + + @Override + public void initialize() { + if (initialized) { + return; + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!initialized) { + return; + } + + initialized = false; + } + + /** + * Returns the plant model service. + * + * @return The plant model service. + */ + public InternalPlantModelService getPlantModelService() { + return plantModelService; + } + + /** + * Returns the system's router. + * + * @return The system's router. + */ + public Router getRouter() { + return router; + } + + /** + * Returns a set of parking positions usable for the given vehicle (usable in the sense that these + * positions are not occupied by other vehicles). + * + * @param vehicle The vehicles to find parking positions for. + * @return The set of usable parking positions. + */ + protected Set<Point> findUsableParkingPositions(Vehicle vehicle) { + // Find out which points are destination points of the current routes of + // all vehicles, and keep them. (Multiple lookups ahead.) + Set<Point> targetedPoints = getRouter().getTargetedPoints(); + + return fetchAllParkingPositions().stream() + .filter(point -> isPointUnoccupiedFor(point, vehicle, targetedPoints)) + .collect(Collectors.toSet()); + } + + /** + * Returns from the given set of points the one that is nearest to the given + * vehicle. + * + * @param vehicle The vehicle. + * @param points The set of points to select the nearest one from. + * @return The point nearest to the given vehicle. + */ + @Nullable + protected Point nearestPoint(Vehicle vehicle, Set<Point> points) { + requireNonNull(vehicle, "vehicle"); + requireNonNull(points, "points"); + + if (vehicle.getCurrentPosition() == null) { + return null; + } + + Point vehiclePos = plantModelService.fetchObject(Point.class, vehicle.getCurrentPosition()); + + return points.stream() + .map(point -> parkingPositionCandidate(vehicle, vehiclePos, point)) + .filter(candidate -> candidate.costs < Long.MAX_VALUE) + .min(Comparator.comparingLong(candidate -> candidate.costs)) + .map(candidate -> candidate.point) + .orElse(null); + } + + /** + * Gathers a set of all points from all blocks that the given point is a member of. + * + * @param point The point to check. + * @return A set of all points from all blocks that the given point is a member of. + */ + protected Set<Point> expandPoints(Point point) { + return plantModelService.expandResources(Collections.singleton(point.getReference())).stream() + .filter(resource -> Point.class.equals(resource.getReference().getReferentClass())) + .map(resource -> (Point) resource) + .collect(Collectors.toSet()); + } + + protected Set<Point> fetchAllParkingPositions() { + return plantModelService.fetchObjects(Point.class, point -> point.isParkingPosition()); + } + + /** + * Checks if ALL points within the same block as the given access point are NOT occupied or + * targeted by any other vehicle than the given one. + * + * @param accessPoint The point to be checked. + * @param vehicle The vehicle to be checked for. + * @param targetedPoints All currently known targeted points. + * @return <code>true</code> if, and only if, ALL points within the same block as the given access + * point are NOT occupied or targeted by any other vehicle than the given one. + */ + private boolean isPointUnoccupiedFor( + Point accessPoint, + Vehicle vehicle, + Set<Point> targetedPoints + ) { + return expandPoints(accessPoint).stream() + .allMatch( + point -> !pointOccupiedOrTargetedByOtherVehicle( + point, + vehicle, + targetedPoints + ) + ); + } + + private boolean pointOccupiedOrTargetedByOtherVehicle( + Point pointToCheck, + Vehicle vehicle, + Set<Point> targetedPoints + ) { + if (pointToCheck.getOccupyingVehicle() != null + && !pointToCheck.getOccupyingVehicle().equals(vehicle.getReference())) { + return true; + } + else if (targetedPoints.contains(pointToCheck)) { + return true; + } + return false; + } + + private PointCandidate parkingPositionCandidate( + Vehicle vehicle, + Point srcPosition, + Point destPosition + ) { + return new PointCandidate( + destPosition, + router.getCosts(vehicle, srcPosition, destPosition, Set.of()) + ); + } + + private static class PointCandidate { + + private final Point point; + private final long costs; + + PointCandidate(Point point, long costs) { + this.point = point; + this.costs = costs; + } + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/DefaultParkingPositionSupplier.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/DefaultParkingPositionSupplier.java new file mode 100644 index 0000000..f683f67 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/DefaultParkingPositionSupplier.java @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.parking; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.components.kernel.Dispatcher.PROPKEY_ASSIGNED_PARKING_POSITION; +import static org.opentcs.components.kernel.Dispatcher.PROPKEY_PREFERRED_PARKING_POSITION; + +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import java.util.Optional; +import java.util.Set; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.InternalPlantModelService; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A parking position supplier that tries to find parking positions that are unoccupied, + * not on the current route of any other vehicle and as close as possible to the + * parked vehicle's current position. + */ +public class DefaultParkingPositionSupplier + extends + AbstractParkingPositionSupplier { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(DefaultParkingPositionSupplier.class); + + /** + * Creates a new instance. + * + * @param plantModelService The plant model service. + * @param router A router for computing travel costs to parking positions. + */ + @Inject + public DefaultParkingPositionSupplier( + InternalPlantModelService plantModelService, + Router router + ) { + super(plantModelService, router); + } + + @Override + public Optional<Point> findParkingPosition(final Vehicle vehicle) { + requireNonNull(vehicle, "vehicle"); + + if (vehicle.getCurrentPosition() == null) { + return Optional.empty(); + } + + Set<Point> parkingPosCandidates = findUsableParkingPositions(vehicle); + + if (parkingPosCandidates.isEmpty()) { + LOG.debug("No parking position candidates found."); + return Optional.empty(); + } + + // Check if the vehicle has an assigned parking position. + // If yes, return either that (if it's with the available points) or none. + String assignedParkingPosName = vehicle.getProperty(PROPKEY_ASSIGNED_PARKING_POSITION); + if (assignedParkingPosName != null) { + return Optional.ofNullable(pickPointWithName(assignedParkingPosName, parkingPosCandidates)); + } + + // Check if the vehicle has a preferred parking position. + // If yes, and if it's with the available points, return that. + String preferredParkingPosName = vehicle.getProperty(PROPKEY_PREFERRED_PARKING_POSITION); + if (preferredParkingPosName != null) { + Point preferredPoint = pickPointWithName(preferredParkingPosName, parkingPosCandidates); + if (preferredPoint != null) { + return Optional.of(preferredPoint); + } + } + + Point nearestPoint = nearestPoint(vehicle, parkingPosCandidates); + LOG.debug( + "Selected parking position {} for vehicle {} from candidates {}.", + nearestPoint, + vehicle.getName(), + parkingPosCandidates + ); + return Optional.ofNullable(nearestPoint); + } + + @Nullable + private Point pickPointWithName(String name, Set<Point> points) { + return points.stream() + .filter(point -> name.equals(point.getName())) + .findAny() + .orElse(null); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/ParkIdleVehiclesPhase.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/ParkIdleVehiclesPhase.java new file mode 100644 index 0000000..8d9f5f2 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/ParkIdleVehiclesPhase.java @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.parking; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.InternalTransportOrderService; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration; +import org.opentcs.strategies.basic.dispatching.TransportOrderUtil; +import org.opentcs.strategies.basic.dispatching.selection.candidates.CompositeAssignmentCandidateSelectionFilter; +import org.opentcs.strategies.basic.dispatching.selection.vehicles.CompositeParkVehicleSelectionFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Creates parking orders for idle vehicles not already at a parking position considering all + * parking positions. + */ +public class ParkIdleVehiclesPhase + extends + AbstractParkingPhase { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ParkIdleVehiclesPhase.class); + /** + * A filter for selecting vehicles that may be parked. + */ + private final CompositeParkVehicleSelectionFilter vehicleSelectionFilter; + + @Inject + public ParkIdleVehiclesPhase( + InternalTransportOrderService orderService, + ParkingPositionSupplier parkingPosSupplier, + Router router, + CompositeAssignmentCandidateSelectionFilter assignmentCandidateSelectionFilter, + TransportOrderUtil transportOrderUtil, + DefaultDispatcherConfiguration configuration, + CompositeParkVehicleSelectionFilter vehicleSelectionFilter + ) { + super( + orderService, + parkingPosSupplier, + router, + assignmentCandidateSelectionFilter, + transportOrderUtil, + configuration + ); + this.vehicleSelectionFilter = requireNonNull(vehicleSelectionFilter, "vehicleSelectionFilter"); + } + + @Override + public void run() { + if (!getConfiguration().parkIdleVehicles()) { + return; + } + + LOG.debug("Looking for vehicles to send to parking positions..."); + + getOrderService().fetchObjects(Vehicle.class).stream() + .filter(vehicle -> vehicleSelectionFilter.apply(vehicle).isEmpty()) + .forEach(vehicle -> createParkingOrder(vehicle)); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/ParkingPositionPriorityComparator.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/ParkingPositionPriorityComparator.java new file mode 100644 index 0000000..4cc97cb --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/ParkingPositionPriorityComparator.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.parking; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Comparator; +import org.opentcs.data.model.Point; + +/** + * Compares parking positions by their priorities. + */ +public class ParkingPositionPriorityComparator + implements + Comparator<Point> { + + /** + * A function computing the priority of a parking position. + */ + private final ParkingPositionToPriorityFunction priorityFunction; + + /** + * Creates a new instance. + * + * @param priorityFunction A function computing the priority of a parking position. + */ + @Inject + public ParkingPositionPriorityComparator(ParkingPositionToPriorityFunction priorityFunction) { + this.priorityFunction = requireNonNull(priorityFunction, "priorityFunction"); + } + + @Override + public int compare(Point point1, Point point2) { + requireNonNull(point1, "point1"); + requireNonNull(point2, "point2"); + + Integer point1Prio = priorityFunction.apply(point1); + Integer point2Prio = priorityFunction.apply(point2); + + if (point1Prio != null && point2Prio == null) { + return -1; + } + else if (point1Prio == null && point2Prio != null) { + return 1; + } + else if (point1Prio == null && point2Prio == null) { + return point1.getName().compareTo(point2.getName()); + } + return point1Prio.compareTo(point2Prio); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/ParkingPositionSupplier.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/ParkingPositionSupplier.java new file mode 100644 index 0000000..8a647c4 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/ParkingPositionSupplier.java @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.parking; + +import jakarta.annotation.Nonnull; +import java.util.Optional; +import org.opentcs.components.Lifecycle; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; + +/** + * A strategy for finding parking positions for vehicles. + */ +public interface ParkingPositionSupplier + extends + Lifecycle { + + /** + * Returns a suitable parking position for the given vehicle. + * + * @param vehicle The vehicle to find a parking position for. + * @return A parking position for the given vehicle, or an empty Optional, if no suitable parking + * position is available. + */ + @Nonnull + Optional<Point> findParkingPosition( + @Nonnull + Vehicle vehicle + ); +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/ParkingPositionToPriorityFunction.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/ParkingPositionToPriorityFunction.java new file mode 100644 index 0000000..c422b20 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/ParkingPositionToPriorityFunction.java @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.parking; + +import com.google.common.base.Strings; +import java.util.function.Function; +import org.opentcs.components.kernel.Dispatcher; +import org.opentcs.data.model.Point; + +/** + * Returns the priority of a parking position, if it has any, or <code>null</code>. + * <p> + * A priority is returned if the given point is a parking position and has a property with key + * {@link Dispatcher#PROPKEY_PARKING_POSITION_PRIORITY} and a numeric (decimal) value as understood + * by {@link Integer#parseInt(java.lang.String)}. + * If these prerequisites are not met, <code>null</code> is returned. + * </p> + */ +public class ParkingPositionToPriorityFunction + implements + Function<Point, Integer> { + + /** + * Creates a new instance. + */ + public ParkingPositionToPriorityFunction() { + } + + @Override + public Integer apply(Point point) { + if (!point.isParkingPosition()) { + return null; + } + String priorityString = point.getProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY); + if (Strings.isNullOrEmpty(priorityString)) { + return null; + } + try { + return Integer.parseInt(priorityString); + } + catch (NumberFormatException e) { + return null; + } + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/PrioritizedParkingPhase.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/PrioritizedParkingPhase.java new file mode 100644 index 0000000..eb45116 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/PrioritizedParkingPhase.java @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.parking; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.InternalTransportOrderService; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration; +import org.opentcs.strategies.basic.dispatching.TransportOrderUtil; +import org.opentcs.strategies.basic.dispatching.selection.candidates.CompositeAssignmentCandidateSelectionFilter; +import org.opentcs.strategies.basic.dispatching.selection.vehicles.CompositeParkVehicleSelectionFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Creates parking orders for idle vehicles not already at a parking position considering only + * prioritized parking positions. + */ +public class PrioritizedParkingPhase + extends + AbstractParkingPhase { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PrioritizedParkingPhase.class); + /** + * A filter for selecting vehicles that may be parked. + */ + private final CompositeParkVehicleSelectionFilter vehicleSelectionFilter; + + @Inject + public PrioritizedParkingPhase( + InternalTransportOrderService orderService, + PrioritizedParkingPositionSupplier parkingPosSupplier, + Router router, + CompositeAssignmentCandidateSelectionFilter assignmentCandidateSelectionFilter, + TransportOrderUtil transportOrderUtil, + DefaultDispatcherConfiguration configuration, + CompositeParkVehicleSelectionFilter vehicleSelectionFilter + ) { + super( + orderService, + parkingPosSupplier, + router, + assignmentCandidateSelectionFilter, + transportOrderUtil, + configuration + ); + this.vehicleSelectionFilter = requireNonNull(vehicleSelectionFilter, "vehicleSelectionFilter"); + } + + @Override + public void run() { + if (!getConfiguration().parkIdleVehicles() + || !getConfiguration().considerParkingPositionPriorities()) { + return; + } + + LOG.debug("Looking for vehicles to send to prioritized parking positions..."); + + getOrderService().fetchObjects(Vehicle.class).stream() + .filter(vehicle -> vehicleSelectionFilter.apply(vehicle).isEmpty()) + .forEach(vehicle -> createParkingOrder(vehicle)); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/PrioritizedParkingPositionSupplier.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/PrioritizedParkingPositionSupplier.java new file mode 100644 index 0000000..f56b74a --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/PrioritizedParkingPositionSupplier.java @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.parking; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.inject.Inject; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.InternalPlantModelService; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A parking position supplier that tries to find the parking position with the highest priority + * that is unoccupied, not on the current route of any other vehicle and as close as possible to the + * vehicle's current position. + */ +public class PrioritizedParkingPositionSupplier + extends + AbstractParkingPositionSupplier { + + /** + * This class's Logger. + */ + private static final Logger LOG + = LoggerFactory.getLogger(PrioritizedParkingPositionSupplier.class); + /** + * A function computing the priority of a parking position. + */ + private final ParkingPositionToPriorityFunction priorityFunction; + + /** + * Creates a new instance. + * + * @param plantModelService The plant model service. + * @param router A router for computing travel costs to parking positions. + * @param priorityFunction A function computing the priority of a parking position. + */ + @Inject + public PrioritizedParkingPositionSupplier( + InternalPlantModelService plantModelService, + Router router, + ParkingPositionToPriorityFunction priorityFunction + ) { + super(plantModelService, router); + this.priorityFunction = requireNonNull(priorityFunction, "priorityFunction"); + } + + @Override + public Optional<Point> findParkingPosition(final Vehicle vehicle) { + requireNonNull(vehicle, "vehicle"); + + if (vehicle.getCurrentPosition() == null) { + return Optional.empty(); + } + + int currentPriority = priorityOfCurrentPosition(vehicle); + Set<Point> parkingPosCandidates = findUsableParkingPositions(vehicle).stream() + .filter(point -> hasHigherPriorityThan(point, currentPriority)) + .collect(Collectors.toSet()); + + if (parkingPosCandidates.isEmpty()) { + LOG.debug("{}: No parking position candidates found.", vehicle.getName()); + return Optional.empty(); + } + + LOG.debug( + "{}: Selecting parking position from candidates {}.", + vehicle.getName(), + parkingPosCandidates + ); + + parkingPosCandidates = filterPositionsWithHighestPriority(parkingPosCandidates); + Point parkingPos = nearestPoint(vehicle, parkingPosCandidates); + + LOG.debug("{}: Selected parking position {}.", vehicle.getName(), parkingPos); + + return Optional.ofNullable(parkingPos); + } + + private int priorityOfCurrentPosition(Vehicle vehicle) { + Point currentPos = getPlantModelService().fetchObject( + Point.class, + vehicle.getCurrentPosition() + ); + return priorityFunction + .andThen(priority -> priority != null ? priority : Integer.MAX_VALUE) + .apply(currentPos); + } + + private boolean hasHigherPriorityThan(Point point, Integer priority) { + Integer pointPriority = priorityFunction.apply(point); + if (pointPriority == null) { + return false; + } + + return pointPriority < priority; + } + + private Set<Point> filterPositionsWithHighestPriority(Set<Point> positions) { + checkArgument(!positions.isEmpty(), "'positions' must not be empty"); + + Map<Integer, List<Point>> prioritiesToPositions = positions.stream() + .collect(Collectors.groupingBy(point -> priorityFunction.apply(point))); + + Integer highestPriority = prioritiesToPositions.keySet().stream() + .reduce(Integer::min) + .get(); + + return new HashSet<>(prioritiesToPositions.get(highestPriority)); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/PrioritizedReparkPhase.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/PrioritizedReparkPhase.java new file mode 100644 index 0000000..13fbabe --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/parking/PrioritizedReparkPhase.java @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.parking; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.InternalTransportOrderService; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration; +import org.opentcs.strategies.basic.dispatching.TransportOrderUtil; +import org.opentcs.strategies.basic.dispatching.selection.candidates.CompositeAssignmentCandidateSelectionFilter; +import org.opentcs.strategies.basic.dispatching.selection.vehicles.CompositeReparkVehicleSelectionFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Creates parking orders for idle vehicles already at a parking position to send them to higher + * prioritized parking positions. + */ +public class PrioritizedReparkPhase + extends + AbstractParkingPhase { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PrioritizedReparkPhase.class); + + private final CompositeReparkVehicleSelectionFilter vehicleSelectionFilter; + private final ParkingPositionPriorityComparator priorityComparator; + + @Inject + public PrioritizedReparkPhase( + InternalTransportOrderService orderService, + PrioritizedParkingPositionSupplier parkingPosSupplier, + Router router, + CompositeAssignmentCandidateSelectionFilter assignmentCandidateSelectionFilter, + TransportOrderUtil transportOrderUtil, + DefaultDispatcherConfiguration configuration, + CompositeReparkVehicleSelectionFilter vehicleSelectionFilter, + ParkingPositionPriorityComparator priorityComparator + ) { + super( + orderService, + parkingPosSupplier, + router, + assignmentCandidateSelectionFilter, + transportOrderUtil, + configuration + ); + this.vehicleSelectionFilter = requireNonNull(vehicleSelectionFilter, "vehicleSelectionFilter"); + this.priorityComparator = requireNonNull(priorityComparator, "priorityComparator"); + } + + @Override + public void run() { + if (!getConfiguration().parkIdleVehicles() + || !getConfiguration().considerParkingPositionPriorities() + || !getConfiguration().reparkVehiclesToHigherPriorityPositions()) { + return; + } + + LOG.debug("Looking for parking vehicles to send to higher prioritized parking positions..."); + + getOrderService().fetchObjects(Vehicle.class).stream() + .filter(vehicle -> vehicleSelectionFilter.apply(vehicle).isEmpty()) + .sorted((vehicle1, vehicle2) -> { + // Sort the vehicles based on the priority of the parking position they occupy + Point point1 = getOrderService().fetchObject(Point.class, vehicle1.getCurrentPosition()); + Point point2 = getOrderService().fetchObject(Point.class, vehicle2.getCurrentPosition()); + return priorityComparator.compare(point1, point2); + }) + .forEach(vehicle -> createParkingOrder(vehicle)); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/recharging/DefaultRechargePositionSupplier.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/recharging/DefaultRechargePositionSupplier.java new file mode 100644 index 0000000..a835ed7 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/recharging/DefaultRechargePositionSupplier.java @@ -0,0 +1,295 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.recharging; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.components.kernel.Dispatcher.PROPKEY_ASSIGNED_RECHARGE_LOCATION; +import static org.opentcs.components.kernel.Dispatcher.PROPKEY_PREFERRED_RECHARGE_LOCATION; + +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.InternalPlantModelService; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; + +/** + * Finds assigned, preferred or (routing-wise) cheapest recharge locations for vehicles. + */ +public class DefaultRechargePositionSupplier + implements + RechargePositionSupplier { + + /** + * The plant model service. + */ + private final InternalPlantModelService plantModelService; + /** + * Our router. + */ + private final Router router; + /** + * Indicates whether this component is enabled. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param plantModelService The plant model service. + * @param router The router to use. + */ + @Inject + public DefaultRechargePositionSupplier( + InternalPlantModelService plantModelService, + Router router + ) { + this.plantModelService = requireNonNull(plantModelService, "plantModelService"); + this.router = requireNonNull(router, "router"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + initialized = false; + } + + @Override + public List<DriveOrder.Destination> findRechargeSequence(Vehicle vehicle) { + requireNonNull(vehicle, "vehicle"); + + if (vehicle.getCurrentPosition() == null) { + return List.of(); + } + + Map<Location, Set<Point>> rechargeLocations + = findLocationsForOperation( + vehicle.getRechargeOperation(), + vehicle, + router.getTargetedPoints() + ); + + String assignedRechargeLocationName = vehicle.getProperty(PROPKEY_ASSIGNED_RECHARGE_LOCATION); + if (assignedRechargeLocationName != null) { + Location location = pickLocationWithName( + assignedRechargeLocationName, + rechargeLocations.keySet() + ); + if (location == null) { + return List.of(); + } + // XXX We should check whether there actually is a viable route to the location. + return List.of(createDestination(location, vehicle.getRechargeOperation())); + } + + // XXX We should check whether there actually is a viable route to the chosen location. + return Optional.ofNullable(vehicle.getProperty(PROPKEY_PREFERRED_RECHARGE_LOCATION)) + .map(name -> pickLocationWithName(name, rechargeLocations.keySet())) + .or(() -> Optional.ofNullable(findCheapestLocation(rechargeLocations, vehicle))) + .map(location -> List.of(createDestination(location, vehicle.getRechargeOperation()))) + .orElse(List.of()); + } + + @Nullable + private Location findCheapestLocation(Map<Location, Set<Point>> locations, Vehicle vehicle) { + Point curPos = plantModelService.fetchObject(Point.class, vehicle.getCurrentPosition()); + + return locations.entrySet().stream() + .map(entry -> bestAccessPointCandidate(vehicle, curPos, entry.getKey(), entry.getValue())) + .filter(candidate -> candidate.isPresent()) + .map(candidate -> candidate.get()) + .min(Comparator.comparingLong(candidate -> candidate.costs)) + .map(candidate -> candidate.location) + .orElse(null); + } + + private DriveOrder.Destination createDestination(Location location, String operation) { + return new DriveOrder.Destination(location.getReference()) + .withOperation(operation); + } + + @Nullable + private Location pickLocationWithName(String name, Set<Location> locations) { + return locations.stream() + .filter(location -> name.equals(location.getName())) + .findAny() + .orElse(null); + } + + /** + * Finds locations allowing the given operation, and the points they would be accessible from for + * the given vehicle. + * + * @param operation The operation. + * @param vehicle The vehicle. + * @param targetedPoints The points that are currently targeted by vehicles. + * @return The locations allowing the given operation, and the points they would be accessible + * from. + */ + private Map<Location, Set<Point>> findLocationsForOperation( + String operation, + Vehicle vehicle, + Set<Point> targetedPoints + ) { + Map<Location, Set<Point>> result = new HashMap<>(); + + for (Location curLoc : plantModelService.fetchObjects(Location.class)) { + LocationType lType = plantModelService.fetchObject(LocationType.class, curLoc.getType()); + if (lType.isAllowedOperation(operation)) { + Set<Point> points = findUnoccupiedAccessPointsForOperation( + curLoc, + operation, + vehicle, + targetedPoints + ); + if (!points.isEmpty()) { + result.put(curLoc, points); + } + } + } + + return result; + } + + private Set<Point> findUnoccupiedAccessPointsForOperation( + Location location, + String rechargeOp, + Vehicle vehicle, + Set<Point> targetedPoints + ) { + return location.getAttachedLinks().stream() + .filter(link -> allowsOperation(link, rechargeOp)) + .map(link -> plantModelService.fetchObject(Point.class, link.getPoint())) + .filter(accessPoint -> isPointUnoccupiedFor(accessPoint, vehicle, targetedPoints)) + .collect(Collectors.toSet()); + } + + /** + * Checks if the given link either does not define any allowed operations at all (meaning it does + * not override the allowed operations of the corresponding location's location type), or - if it + * does - explicitly allows the required recharge operation. + * + * @param link The link to be checked. + * @param operation The operation to be checked for. + * @return <code>true</code> if, and only if, the given link does not disallow the given + * operation. + */ + private boolean allowsOperation(Location.Link link, String operation) { + // This link is only interesting if it either does not define any allowed operations (does + // not override the allowed operations of the corresponding location) at all or, if it does, + // allows the required recharge operation. + return link.getAllowedOperations().isEmpty() || link.hasAllowedOperation(operation); + } + + private Optional<LocationCandidate> bestAccessPointCandidate( + Vehicle vehicle, + Point srcPosition, + Location location, + Set<Point> destPositions + ) { + return destPositions.stream() + .map( + point -> new LocationCandidate( + location, + router.getCosts( + vehicle, + srcPosition, + point, + Set.of() + ) + ) + ) + .min(Comparator.comparingLong(candidate -> candidate.costs)); + } + + /** + * Checks if ALL points within the same block as the given access point are NOT occupied or + * targeted by any other vehicle than the given one. + * + * @param accessPoint The point to be checked. + * @param vehicle The vehicle to be checked for. + * @param targetedPoints All currently known targeted points. + * @return <code>true</code> if, and only if, ALL points within the same block as the given access + * point are NOT occupied or targeted by any other vehicle than the given one. + */ + private boolean isPointUnoccupiedFor( + Point accessPoint, + Vehicle vehicle, + Set<Point> targetedPoints + ) { + return expandPoints(accessPoint).stream() + .noneMatch( + point -> pointOccupiedOrTargetedByOtherVehicle( + point, + vehicle, + targetedPoints + ) + ); + } + + private boolean pointOccupiedOrTargetedByOtherVehicle( + Point pointToCheck, + Vehicle vehicle, + Set<Point> targetedPoints + ) { + if (pointToCheck.getOccupyingVehicle() != null + && !pointToCheck.getOccupyingVehicle().equals(vehicle.getReference())) { + return true; + } + else if (targetedPoints.contains(pointToCheck)) { + return true; + } + return false; + } + + /** + * Gathers a set of all points from all blocks that the given point is a member of. + * + * @param point The point to check. + * @return A set of all points from all blocks that the given point is a member of. + */ + private Set<Point> expandPoints(Point point) { + return plantModelService.expandResources(Set.of(point.getReference())).stream() + .filter(resource -> Point.class.equals(resource.getReference().getReferentClass())) + .map(resource -> (Point) resource) + .collect(Collectors.toSet()); + } + + private static class LocationCandidate { + + private final Location location; + private final long costs; + + LocationCandidate(Location location, long costs) { + this.location = location; + this.costs = costs; + } + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/recharging/RechargeIdleVehiclesPhase.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/recharging/RechargeIdleVehiclesPhase.java new file mode 100644 index 0000000..948487d --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/recharging/RechargeIdleVehiclesPhase.java @@ -0,0 +1,187 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.recharging; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.opentcs.access.to.order.DestinationCreationTO; +import org.opentcs.access.to.order.TransportOrderCreationTO; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.InternalTransportOrderService; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.OrderConstants; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration; +import org.opentcs.strategies.basic.dispatching.Phase; +import org.opentcs.strategies.basic.dispatching.TransportOrderUtil; +import org.opentcs.strategies.basic.dispatching.selection.candidates.CompositeAssignmentCandidateSelectionFilter; +import org.opentcs.strategies.basic.dispatching.selection.vehicles.CompositeRechargeVehicleSelectionFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Creates recharging orders for any vehicles with a degraded energy level. + */ +public class RechargeIdleVehiclesPhase + implements + Phase { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(RechargeIdleVehiclesPhase.class); + /** + * The transport order service. + */ + private final InternalTransportOrderService orderService; + /** + * The strategy used for finding suitable recharge locations. + */ + private final RechargePositionSupplier rechargePosSupplier; + /** + * The Router instance calculating route costs. + */ + private final Router router; + /** + * A collection of predicates for filtering assignment candidates. + */ + private final CompositeAssignmentCandidateSelectionFilter assignmentCandidateSelectionFilter; + + private final CompositeRechargeVehicleSelectionFilter vehicleSelectionFilter; + + private final TransportOrderUtil transportOrderUtil; + /** + * The dispatcher configuration. + */ + private final DefaultDispatcherConfiguration configuration; + /** + * Indicates whether this component is initialized. + */ + private boolean initialized; + + @Inject + public RechargeIdleVehiclesPhase( + InternalTransportOrderService orderService, + RechargePositionSupplier rechargePosSupplier, + Router router, + CompositeAssignmentCandidateSelectionFilter assignmentCandidateSelectionFilter, + CompositeRechargeVehicleSelectionFilter vehicleSelectionFilter, + TransportOrderUtil transportOrderUtil, + DefaultDispatcherConfiguration configuration + ) { + this.router = requireNonNull(router, "router"); + this.orderService = requireNonNull(orderService, "orderService"); + this.rechargePosSupplier = requireNonNull(rechargePosSupplier, "rechargePosSupplier"); + this.assignmentCandidateSelectionFilter = requireNonNull( + assignmentCandidateSelectionFilter, + "assignmentCandidateSelectionFilter" + ); + this.vehicleSelectionFilter = requireNonNull(vehicleSelectionFilter, "vehicleSelectionFilter"); + this.transportOrderUtil = requireNonNull(transportOrderUtil, "transportOrderUtil"); + this.configuration = requireNonNull(configuration, "configuration"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + rechargePosSupplier.initialize(); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + rechargePosSupplier.terminate(); + + initialized = false; + } + + @Override + public void run() { + if (!configuration.rechargeIdleVehicles()) { + return; + } + + orderService.fetchObjects(Vehicle.class).stream() + .filter(vehicle -> vehicleSelectionFilter.apply(vehicle).isEmpty()) + .forEach(vehicle -> createRechargeOrder(vehicle)); + } + + private void createRechargeOrder(Vehicle vehicle) { + List<DriveOrder.Destination> rechargeDests = rechargePosSupplier.findRechargeSequence(vehicle); + LOG.debug("Recharge sequence for {}: {}", vehicle, rechargeDests); + + if (rechargeDests.isEmpty()) { + LOG.info("{}: Did not find a suitable recharge sequence.", vehicle.getName()); + return; + } + + List<DestinationCreationTO> chargeDests = new ArrayList<>(rechargeDests.size()); + for (DriveOrder.Destination dest : rechargeDests) { + chargeDests.add( + new DestinationCreationTO(dest.getDestination().getName(), dest.getOperation()) + .withProperties(dest.getProperties()) + ); + } + // Create a transport order for recharging and verify its processability. + // The recharge order may be withdrawn unless its energy level is critical. + TransportOrder rechargeOrder = orderService.createTransportOrder( + new TransportOrderCreationTO("Recharge-", chargeDests) + .withIncompleteName(true) + .withIntendedVehicleName(vehicle.getName()) + .withDispensable(!vehicle.isEnergyLevelCritical()) + .withType(OrderConstants.TYPE_CHARGE) + ); + + Point vehiclePosition = orderService.fetchObject(Point.class, vehicle.getCurrentPosition()); + Optional<AssignmentCandidate> candidate = computeCandidate( + vehicle, + vehiclePosition, + rechargeOrder + ) + .filter(c -> assignmentCandidateSelectionFilter.apply(c).isEmpty()); + // XXX Change this to Optional.ifPresentOrElse() once we're at Java 9+. + if (candidate.isPresent()) { + transportOrderUtil.assignTransportOrder( + candidate.get().getVehicle(), + candidate.get().getTransportOrder(), + candidate.get().getDriveOrders() + ); + } + else { + // Mark the order as failed, since the vehicle cannot execute it. + orderService.updateTransportOrderState( + rechargeOrder.getReference(), + TransportOrder.State.FAILED + ); + } + } + + private Optional<AssignmentCandidate> computeCandidate( + Vehicle vehicle, + Point vehiclePosition, + TransportOrder order + ) { + return router.getRoute(vehicle, vehiclePosition, order) + .map(driveOrders -> new AssignmentCandidate(vehicle, order, driveOrders)); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/recharging/RechargePositionSupplier.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/recharging/RechargePositionSupplier.java new file mode 100644 index 0000000..5a76f78 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/recharging/RechargePositionSupplier.java @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.recharging; + +import jakarta.annotation.Nonnull; +import java.util.List; +import org.opentcs.components.Lifecycle; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; + +/** + * A strategy for finding locations suitable for recharging vehicles. + */ +public interface RechargePositionSupplier + extends + Lifecycle { + + /** + * Returns a sequence of destinations for recharging the given vehicle. + * In most cases, the sequence will probably consist of only one destination. Some vehicles may + * require to visit more than one destination for recharging, though, e.g. to drop off a battery + * pack at one location and get a fresh one at another location. + * + * @param vehicle The vehicle to be recharged. + * @return A sequence of destinations including operations for recharging the given vehicle. If + * no suitable sequence was found, the returned sequence will be empty. + */ + @Nonnull + List<DriveOrder.Destination> findRechargeSequence( + @Nonnull + Vehicle vehicle + ); +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/CompositeOrderCandidateComparator.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/CompositeOrderCandidateComparator.java new file mode 100644 index 0000000..4f4ddae --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/CompositeOrderCandidateComparator.java @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.priorization; + +import static org.opentcs.util.Assertions.checkArgument; + +import com.google.common.collect.Lists; +import jakarta.inject.Inject; +import java.util.Comparator; +import java.util.Map; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorByOrderAge; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorByOrderName; + +/** + * A composite of all configured transport order candidate comparators. + */ +public class CompositeOrderCandidateComparator + implements + Comparator<AssignmentCandidate> { + + /** + * A comparator composed of all configured comparators, in the configured order. + */ + private final Comparator<AssignmentCandidate> compositeComparator; + + @Inject + public CompositeOrderCandidateComparator( + DefaultDispatcherConfiguration configuration, + Map<String, Comparator<AssignmentCandidate>> availableComparators + ) { + // At the end, if all other comparators failed to see a difference, compare by age. + // As the age of two distinct transport orders may still be the same, finally compare by name. + // Add configured comparators before these two. + Comparator<AssignmentCandidate> composite + = new CandidateComparatorByOrderAge().thenComparing(new CandidateComparatorByOrderName()); + + for (String priorityKey : Lists.reverse(configuration.orderCandidatePriorities())) { + Comparator<AssignmentCandidate> configuredComparator = availableComparators.get(priorityKey); + checkArgument( + configuredComparator != null, + "Unknown order candidate priority key: '%s'", + priorityKey + ); + composite = configuredComparator.thenComparing(composite); + } + this.compositeComparator = composite; + } + + @Override + public int compare(AssignmentCandidate o1, AssignmentCandidate o2) { + return compositeComparator.compare(o1, o2); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/CompositeOrderComparator.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/CompositeOrderComparator.java new file mode 100644 index 0000000..11ca34c --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/CompositeOrderComparator.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.priorization; + +import static org.opentcs.util.Assertions.checkArgument; + +import com.google.common.collect.Lists; +import jakarta.inject.Inject; +import java.util.Comparator; +import java.util.Map; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration; +import org.opentcs.strategies.basic.dispatching.priorization.transportorder.TransportOrderComparatorByAge; +import org.opentcs.strategies.basic.dispatching.priorization.transportorder.TransportOrderComparatorByName; + +/** + * A composite of all configured transport order comparators. + */ +public class CompositeOrderComparator + implements + Comparator<TransportOrder> { + + /** + * A comparator composed of all configured comparators, in the configured order. + */ + private final Comparator<TransportOrder> compositeComparator; + + @Inject + public CompositeOrderComparator( + DefaultDispatcherConfiguration configuration, + Map<String, Comparator<TransportOrder>> availableComparators + ) { + // At the end, if all other comparators failed to see a difference, compare by age. + // As the age of two distinct transport orders may still be the same, finally compare by name. + // Add configured comparators before these two. + Comparator<TransportOrder> composite + = new TransportOrderComparatorByAge().thenComparing(new TransportOrderComparatorByName()); + + for (String priorityKey : Lists.reverse(configuration.orderPriorities())) { + Comparator<TransportOrder> configuredComparator = availableComparators.get(priorityKey); + checkArgument(configuredComparator != null, "Unknown order priority key: '%s'", priorityKey); + composite = configuredComparator.thenComparing(composite); + } + this.compositeComparator = composite; + } + + @Override + public int compare(TransportOrder o1, TransportOrder o2) { + return compositeComparator.compare(o1, o2); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/CompositeVehicleCandidateComparator.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/CompositeVehicleCandidateComparator.java new file mode 100644 index 0000000..0f995b1 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/CompositeVehicleCandidateComparator.java @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.priorization; + +import static org.opentcs.util.Assertions.checkArgument; + +import com.google.common.collect.Lists; +import jakarta.inject.Inject; +import java.util.Comparator; +import java.util.Map; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorByEnergyLevel; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorByVehicleName; + +/** + * A composite of all configured vehicle candidate comparators. + */ +public class CompositeVehicleCandidateComparator + implements + Comparator<AssignmentCandidate> { + + /** + * A comparator composed of all configured comparators, in the configured order. + */ + private final Comparator<AssignmentCandidate> compositeComparator; + + @Inject + public CompositeVehicleCandidateComparator( + DefaultDispatcherConfiguration configuration, + Map<String, Comparator<AssignmentCandidate>> availableComparators + ) { + // At the end, if all other comparators failed to see a difference, compare by energy level. + // As the energy level of two distinct vehicles may still be the same, finally compare by name. + // Add configured comparators before these two. + Comparator<AssignmentCandidate> composite + = new CandidateComparatorByEnergyLevel() + .thenComparing(new CandidateComparatorByVehicleName()); + + for (String priorityKey : Lists.reverse(configuration.vehicleCandidatePriorities())) { + Comparator<AssignmentCandidate> configuredComparator = availableComparators.get(priorityKey); + checkArgument( + configuredComparator != null, + "Unknown vehicle candidate priority key: '%s'", + priorityKey + ); + composite = configuredComparator.thenComparing(composite); + } + this.compositeComparator = composite; + } + + @Override + public int compare(AssignmentCandidate o1, AssignmentCandidate o2) { + return compositeComparator.compare(o1, o2); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/CompositeVehicleComparator.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/CompositeVehicleComparator.java new file mode 100644 index 0000000..d4e465d --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/CompositeVehicleComparator.java @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.priorization; + +import static org.opentcs.util.Assertions.checkArgument; + +import com.google.common.collect.Lists; +import jakarta.inject.Inject; +import java.util.Comparator; +import java.util.Map; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration; +import org.opentcs.strategies.basic.dispatching.priorization.vehicle.VehicleComparatorByEnergyLevel; +import org.opentcs.strategies.basic.dispatching.priorization.vehicle.VehicleComparatorByName; + +/** + * A composite of all configured vehicle comparators. + */ +public class CompositeVehicleComparator + implements + Comparator<Vehicle> { + + /** + * A comparator composed of all configured comparators, in the configured order. + */ + private final Comparator<Vehicle> compositeComparator; + + @Inject + public CompositeVehicleComparator( + DefaultDispatcherConfiguration configuration, + Map<String, Comparator<Vehicle>> availableComparators + ) { + // At the end, if all other comparators failed to see a difference, compare by energy level. + // As the energy level of two distinct vehicles may still be the same, finally compare by name. + // Add configured comparators before these two. + Comparator<Vehicle> composite + = new VehicleComparatorByEnergyLevel().thenComparing(new VehicleComparatorByName()); + + for (String priorityKey : Lists.reverse(configuration.vehiclePriorities())) { + Comparator<Vehicle> configuredComparator = availableComparators.get(priorityKey); + checkArgument( + configuredComparator != null, + "Unknown vehicle priority key: '%s'", + priorityKey + ); + composite = configuredComparator.thenComparing(composite); + } + this.compositeComparator = composite; + } + + @Override + public int compare(Vehicle o1, Vehicle o2) { + return compositeComparator.compare(o1, o2); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByCompleteRoutingCosts.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByCompleteRoutingCosts.java new file mode 100644 index 0000000..0cb10eb --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByCompleteRoutingCosts.java @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.priorization.candidate; + +import java.util.Comparator; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; + +/** + * Compares {@link AssignmentCandidate}s by routing costs. + * Note: this comparator imposes orderings that are inconsistent with equals. + */ +public class CandidateComparatorByCompleteRoutingCosts + implements + Comparator<AssignmentCandidate> { + + /** + * A key used for selecting this comparator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "BY_COMPLETE_ROUTING_COSTS"; + + /** + * Creates a new instance. + */ + public CandidateComparatorByCompleteRoutingCosts() { + } + + /** + * Compares two candidates by their routing costs. + * Note: this comparator imposes orderings that are inconsistent with equals. + * + * @see Comparator#compare(java.lang.Object, java.lang.Object) + * @param candidate1 The first candidate + * @param candidate2 The second candidate + * @return the value 0 if candidate1 and candidate2 have the same routing costs; + * a value less than 0 if candidate1 has a lower routing cost than candidate2; + * and a value greater than 0 otherwise. + */ + @Override + public int compare(AssignmentCandidate candidate1, AssignmentCandidate candidate2) { + // Lower routing costs are better. + return Long.compare(candidate1.getCompleteRoutingCosts(), candidate2.getCompleteRoutingCosts()); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByDeadline.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByDeadline.java new file mode 100644 index 0000000..4c69d4d --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByDeadline.java @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.priorization.candidate; + +import java.util.Comparator; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.priorization.transportorder.TransportOrderComparatorByDeadline; + +/** + * Compares {@link AssignmentCandidate}s by deadline of the order. + * Note: this comparator imposes orderings that are inconsistent with equals. + */ +public class CandidateComparatorByDeadline + implements + Comparator<AssignmentCandidate> { + + /** + * A key used for selecting this comparator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "BY_DEADLINE"; + + private final Comparator<TransportOrder> delegate = new TransportOrderComparatorByDeadline(); + + /** + * Creates a new instance. + */ + public CandidateComparatorByDeadline() { + } + + /** + * Compares two candidates by the deadline of their transport order. + * Note: this comparator imposes orderings that are inconsistent with equals. + * + * @see Comparator#compare(java.lang.Object, java.lang.Object) + * @param candidate1 The first candidate + * @param candidate2 The second candidate + * @return the value 0 if candidate1 and candidate2 have the same deadline; + * a value less than 0 if candidate1 has an earlier deadline than candidate2; + * and a value greater than 0 otherwise. + */ + @Override + public int compare(AssignmentCandidate candidate1, AssignmentCandidate candidate2) { + return delegate.compare(candidate1.getTransportOrder(), candidate2.getTransportOrder()); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByEnergyLevel.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByEnergyLevel.java new file mode 100644 index 0000000..b582e83 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByEnergyLevel.java @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.priorization.candidate; + +import java.util.Comparator; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.priorization.vehicle.VehicleComparatorByEnergyLevel; + +/** + * Compares {@link AssignmentCandidate}s by the energy level of their vehicles. + * Note: this comparator imposes orderings that are inconsistent with equals. + */ +public class CandidateComparatorByEnergyLevel + implements + Comparator<AssignmentCandidate> { + + /** + * A key used for selecting this comparator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "BY_ENERGY_LEVEL"; + + private final Comparator<Vehicle> delegate = new VehicleComparatorByEnergyLevel(); + + /** + * Creates a new instance. + */ + public CandidateComparatorByEnergyLevel() { + } + + /** + * Compares two candidates by the energy level of their vehicles. + * Note: this comparator imposes orderings that are inconsistent with equals. + * + * @see Comparator#compare(java.lang.Object, java.lang.Object) + * @param candidate1 The first candidate. + * @param candidate2 The second candidate. + * @return the value 0 if candidate1 and candidate2 have the same energy level; + * a value less than 0 if candidate1 has a higher energy level than candidate2; + * and a value greater than 0 otherwise. + */ + @Override + public int compare(AssignmentCandidate candidate1, AssignmentCandidate candidate2) { + return delegate.compare(candidate1.getVehicle(), candidate2.getVehicle()); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByInitialRoutingCosts.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByInitialRoutingCosts.java new file mode 100644 index 0000000..807493f --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByInitialRoutingCosts.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.priorization.candidate; + +import java.util.Comparator; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; + +/** + * Compares {@link AssignmentCandidate}s by routing costs to the transport order's first + * destination. + * Note: this comparator imposes orderings that are inconsistent with equals. + */ +public class CandidateComparatorByInitialRoutingCosts + implements + Comparator<AssignmentCandidate> { + + /** + * A key used for selecting this comparator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "BY_INITIAL_ROUTING_COSTS"; + + /** + * Creates a new instance. + */ + public CandidateComparatorByInitialRoutingCosts() { + } + + /** + * Compares two candidates by their inital routing cost. + * Note: this comparator imposes orderings that are inconsistent with equals. + * + * @see Comparator#compare(java.lang.Object, java.lang.Object) + * @param candidate1 The first candidate. + * @param candidate2 The second candidate. + * @return the value 0 if candidate1 and candidate2 have the same routing cost; + * a value less than 0 if candidate1 has lower routing cost than candidate2; + * and a value greater than 0 otherwise. + */ + @Override + public int compare(AssignmentCandidate candidate1, AssignmentCandidate candidate2) { + // Lower routing costs are better. + return Long.compare(candidate1.getInitialRoutingCosts(), candidate2.getInitialRoutingCosts()); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByOrderAge.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByOrderAge.java new file mode 100644 index 0000000..2c0fac7 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByOrderAge.java @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.priorization.candidate; + +import java.util.Comparator; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.priorization.transportorder.TransportOrderComparatorByAge; + +/** + * Compares {@link AssignmentCandidate}s by age of the order. + * Note: this comparator imposes orderings that are inconsistent with equals. + */ +public class CandidateComparatorByOrderAge + implements + Comparator<AssignmentCandidate> { + + /** + * A key used for selecting this comparator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "BY_AGE"; + + private final Comparator<TransportOrder> delegate = new TransportOrderComparatorByAge(); + + /** + * Creates a new instance. + */ + public CandidateComparatorByOrderAge() { + } + + /** + * Compares two candidate by the age of the order. + * Note: this comparator imposes orderings that are inconsistent with equals. + * + * @see Comparator#compare(java.lang.Object, java.lang.Object) + * @param candidate1 The first candidate. + * @param candidate2 The second candidate. + * @return the value zero, if the transport order of + * candidate1 and candidate2 have the same creation time; + * a value less than zero, if candidate1 is older than candidate2. + * a value greater than zero otherwise. + */ + @Override + public int compare(AssignmentCandidate candidate1, AssignmentCandidate candidate2) { + return delegate.compare(candidate1.getTransportOrder(), candidate2.getTransportOrder()); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByOrderName.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByOrderName.java new file mode 100644 index 0000000..04589ee --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByOrderName.java @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.priorization.candidate; + +import java.util.Comparator; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.priorization.transportorder.TransportOrderComparatorByName; + +/** + * Compares {@link AssignmentCandidate}s by name of the order. + */ +public class CandidateComparatorByOrderName + implements + Comparator<AssignmentCandidate> { + + /** + * A key used for selecting this comparator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "BY_ORDER_NAME"; + + private final Comparator<TransportOrder> delegate = new TransportOrderComparatorByName(); + + /** + * Creates a new instance. + */ + public CandidateComparatorByOrderName() { + } + + @Override + public int compare(AssignmentCandidate candidate1, AssignmentCandidate candidate2) { + return delegate.compare(candidate1.getTransportOrder(), candidate2.getTransportOrder()); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByVehicleName.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByVehicleName.java new file mode 100644 index 0000000..6f82bf4 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorByVehicleName.java @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.priorization.candidate; + +import java.util.Comparator; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.priorization.vehicle.VehicleComparatorByName; + +/** + * Compares {@link AssignmentCandidate}s by name of the vehicle. + */ +public class CandidateComparatorByVehicleName + implements + Comparator<AssignmentCandidate> { + + /** + * A key used for selecting this comparator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "BY_VEHICLE_NAME"; + + private final Comparator<Vehicle> delegate = new VehicleComparatorByName(); + + /** + * Creates a new instance. + */ + public CandidateComparatorByVehicleName() { + } + + @Override + public int compare(AssignmentCandidate candidate1, AssignmentCandidate candidate2) { + return delegate.compare(candidate1.getVehicle(), candidate2.getVehicle()); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorDeadlineAtRiskFirst.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorDeadlineAtRiskFirst.java new file mode 100644 index 0000000..0ee86f4 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorDeadlineAtRiskFirst.java @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.priorization.candidate; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Comparator; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.priorization.transportorder.TransportOrderComparatorDeadlineAtRiskFirst; + +/** + * Compares {@link AssignmentCandidate}s by their transport order's deadlines, ordering those with a + * deadline at risk first. + * Note: this comparator imposes orderings that are inconsistent with equals. + */ +public class CandidateComparatorDeadlineAtRiskFirst + implements + Comparator<AssignmentCandidate> { + + /** + * A key used for selecting this comparator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "DEADLINE_AT_RISK_FIRST"; + + /** + * The comparator that compares the deadlines of transport orders, taking the critical threshold + * into account. + */ + private final Comparator<TransportOrder> delegate; + + @Inject + public CandidateComparatorDeadlineAtRiskFirst( + TransportOrderComparatorDeadlineAtRiskFirst delegate + ) { + this.delegate = requireNonNull(delegate, "delegate"); + } + + /** + * Compares two candidates by the deadline of their transport order and the given threshold + * indicating whether the remaining time for the deadline is considered critical + * Note: this comparator imposes orderings that are inconsistent with equals. + * + * @see Comparator#compare(java.lang.Object, java.lang.Object) + * @param candidate1 The first candidate + * @param candidate2 The second candidate + * @return the value 0 if the deadlines of candidate1 and candidate2 are both at risk or not; + * a value less than 0 if only the deadline of candidate1 is at risk + * and a value greater than 0 in all other cases. + */ + @Override + public int compare(AssignmentCandidate candidate1, AssignmentCandidate candidate2) { + return delegate.compare(candidate1.getTransportOrder(), candidate2.getTransportOrder()); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorIdleFirst.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorIdleFirst.java new file mode 100644 index 0000000..f2abd0f --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/candidate/CandidateComparatorIdleFirst.java @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.priorization.candidate; + +import java.util.Comparator; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.priorization.vehicle.VehicleComparatorIdleFirst; + +/** + * Compares {@link AssignmentCandidate}s by vehicles' states, ordering IDLE vehicles first. + * Note: this comparator imposes orderings that are inconsistent with equals. + */ +public class CandidateComparatorIdleFirst + implements + Comparator<AssignmentCandidate> { + + /** + * A key used for selecting this comparator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "IDLE_FIRST"; + + private final Comparator<Vehicle> delegate = new VehicleComparatorIdleFirst(); + + /** + * Creates a new instance. + */ + public CandidateComparatorIdleFirst() { + } + + /** + * Compares two candidates by the state of their vehicles. + * Note: this comparator imposes orderings that are inconsistent with equals. + * + * @see Comparator#compare(java.lang.Object, java.lang.Object) + * @param candidate1 The first candiate. + * @param candidate2 The second candidate. + * @return The value zero if the vehicles of candidate1 and candidate2 have the same state; + * a value grater zero, if the vehicle state of candidate1 is idle, unlike candidate2; + * a value less than zero otherwise. + */ + @Override + public int compare(AssignmentCandidate candidate1, AssignmentCandidate candidate2) { + return delegate.compare(candidate1.getVehicle(), candidate2.getVehicle()); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/transportorder/TransportOrderComparatorByAge.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/transportorder/TransportOrderComparatorByAge.java new file mode 100644 index 0000000..b900531 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/transportorder/TransportOrderComparatorByAge.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.priorization.transportorder; + +import java.util.Comparator; +import org.opentcs.data.order.TransportOrder; + +/** + * Compares {@link TransportOrder}s by age. + * Note: this comparator imposes orderings that are inconsistent with equals. + */ +public class TransportOrderComparatorByAge + implements + Comparator<TransportOrder> { + + /** + * A key used for selecting this comparator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "BY_AGE"; + + /** + * Creates a new instance. + */ + public TransportOrderComparatorByAge() { + } + + /** + * Compares two orders by their age. + * Note: this comparator imposes orderings that are inconsistent with equals. + * + * @see Comparator#compare(java.lang.Object, java.lang.Object) + * @param order1 The first order. + * @param order2 The second order. + * @return the value zero, if the transport order have the same creation time; + * a value less than zero, if order1 is older than order2. + * a value greater than zero otherwise. + */ + @Override + public int compare(TransportOrder order1, TransportOrder order2) { + return order1.getCreationTime().compareTo(order2.getCreationTime()); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/transportorder/TransportOrderComparatorByDeadline.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/transportorder/TransportOrderComparatorByDeadline.java new file mode 100644 index 0000000..aa45fe2 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/transportorder/TransportOrderComparatorByDeadline.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.priorization.transportorder; + +import java.util.Comparator; +import org.opentcs.data.order.TransportOrder; + +/** + * Compares {@link TransportOrder}s by age. + * Note: this comparator imposes orderings that are inconsistent with equals. + */ +public class TransportOrderComparatorByDeadline + implements + Comparator<TransportOrder> { + + /** + * A key used for selecting this comparator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "BY_DEADLINE"; + + /** + * Creates a new instance. + */ + public TransportOrderComparatorByDeadline() { + } + + /** + * Compares two orders by their deadline. + * Note: this comparator imposes orderings that are inconsistent with equals. + * + * @see Comparator#compare(java.lang.Object, java.lang.Object) + * @param order1 The first order. + * @param order2 The second order. + * @return the value 0 if order1 and order2 have the same deadline; + * a value less than 0 if order1 has an earlier deadline than order2; + * and a value greater than 0 otherwise. + */ + @Override + public int compare(TransportOrder order1, TransportOrder order2) { + return order1.getDeadline().compareTo(order2.getDeadline()); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/transportorder/TransportOrderComparatorByName.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/transportorder/TransportOrderComparatorByName.java new file mode 100644 index 0000000..cddd844 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/transportorder/TransportOrderComparatorByName.java @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.priorization.transportorder; + +import java.util.Comparator; +import org.opentcs.data.order.TransportOrder; + +/** + * Compares {@link TransportOrder}s by their names. + */ +public class TransportOrderComparatorByName + implements + Comparator<TransportOrder> { + + /** + * A key used for selecting this comparator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "BY_NAME"; + + /** + * Creates a new instance. + */ + public TransportOrderComparatorByName() { + } + + @Override + public int compare(TransportOrder order1, TransportOrder order2) { + return order1.getName().compareTo(order2.getName()); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/transportorder/TransportOrderComparatorDeadlineAtRiskFirst.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/transportorder/TransportOrderComparatorDeadlineAtRiskFirst.java new file mode 100644 index 0000000..681fae8 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/transportorder/TransportOrderComparatorDeadlineAtRiskFirst.java @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.priorization.transportorder; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.time.Instant; +import java.util.Comparator; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration; + +/** + * Compares {@link TransportOrder}s by their deadlines, ordering those with a deadline at risk + * first. + * Note: this comparator imposes orderings that are inconsistent with equals. + */ +public class TransportOrderComparatorDeadlineAtRiskFirst + implements + Comparator<TransportOrder> { + + /** + * A key used for selecting this comparator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "DEADLINE_AT_RISK_FIRST"; + + /** + * The time window (in ms) before its deadline in which an order becomes urgent. + */ + private final long deadlineAtRiskPeriod; + + @Inject + public TransportOrderComparatorDeadlineAtRiskFirst(DefaultDispatcherConfiguration configuration) { + requireNonNull(configuration, "configuration"); + + this.deadlineAtRiskPeriod = configuration.deadlineAtRiskPeriod(); + } + + /** + * Compares two orders by their deadline's criticality. + * Note: this comparator imposes orderings that are inconsistent with equals. + * + * @see Comparator#compare(java.lang.Object, java.lang.Object) + * @param order1 The first order. + * @param order2 The second order. + * @return the value 0 if the deadlines of order1 and order2 are both at risk or not, + * a value less than 0 if only the deadline of order1 is at risk + * and a value greater than 0 in all other cases. + */ + @Override + public int compare(TransportOrder order1, TransportOrder order2) { + boolean order1AtRisk = deadlineAtRisk(order1); + boolean order2AtRisk = deadlineAtRisk(order2); + + if (order1AtRisk == order2AtRisk) { + return 0; + } + else if (order1AtRisk) { + return -1; + } + else { + return 1; + } + } + + private boolean deadlineAtRisk(TransportOrder order) { + return order.getDeadline().minusMillis(deadlineAtRiskPeriod).isBefore(Instant.now()); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/vehicle/VehicleComparatorByEnergyLevel.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/vehicle/VehicleComparatorByEnergyLevel.java new file mode 100644 index 0000000..e3c2bf4 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/vehicle/VehicleComparatorByEnergyLevel.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.priorization.vehicle; + +import java.util.Comparator; +import org.opentcs.data.model.Vehicle; + +/** + * Compares {@link Vehicle}s by energy level, sorting higher energy levels up. + * Note: this comparator imposes orderings that are inconsistent with equals. + */ +public class VehicleComparatorByEnergyLevel + implements + Comparator<Vehicle> { + + /** + * A key used for selecting this comparator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "BY_ENERGY_LEVEL"; + + /** + * Creates a new instance. + */ + public VehicleComparatorByEnergyLevel() { + } + + /** + * Compares two vehicles by their energy level. + * Note: this comparator imposes orderings that are inconsistent with equals. + * + * @see Comparator#compare(java.lang.Object, java.lang.Object) + * @param vehicle1 The first vehicle. + * @param vehicle2 The second vehicel. + * @return the value 0 if vehicle1 and vehicle2 have the same energy level; + * a value less than 0 if vehicle1 has a higher energy level than vehicle2; + * and a value greater than 0 otherwise. + */ + @Override + public int compare(Vehicle vehicle1, Vehicle vehicle2) { + return -Integer.compare(vehicle1.getEnergyLevel(), vehicle2.getEnergyLevel()); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/vehicle/VehicleComparatorByName.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/vehicle/VehicleComparatorByName.java new file mode 100644 index 0000000..f8bb815 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/vehicle/VehicleComparatorByName.java @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.priorization.vehicle; + +import java.util.Comparator; +import org.opentcs.data.model.Vehicle; + +/** + * Compares {@link Vehicle}s by their names. + */ +public class VehicleComparatorByName + implements + Comparator<Vehicle> { + + /** + * A key used for selecting this comparator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "BY_NAME"; + + /** + * Creates a new instance. + */ + public VehicleComparatorByName() { + } + + @Override + public int compare(Vehicle vehicle1, Vehicle vehicle2) { + return vehicle1.getName().compareTo(vehicle2.getName()); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/vehicle/VehicleComparatorIdleFirst.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/vehicle/VehicleComparatorIdleFirst.java new file mode 100644 index 0000000..85f6560 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/priorization/vehicle/VehicleComparatorIdleFirst.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.priorization.vehicle; + +import java.util.Comparator; +import org.opentcs.data.model.Vehicle; + +/** + * Compares {@link Vehicle}s by their states, ordering IDLE vehicles first. + * Note: this comparator imposes orderings that are inconsistent with equals. + */ +public class VehicleComparatorIdleFirst + implements + Comparator<Vehicle> { + + /** + * A key used for selecting this comparator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "IDLE_FIRST"; + + /** + * Creates a new instance. + */ + public VehicleComparatorIdleFirst() { + } + + /** + * Compares two vehicles by their state. + * Note: this comparator imposes orderings that are inconsistent with equals. + * + * @see Comparator#compare(java.lang.Object, java.lang.Object) + * @param vehicle1 The first vehicle. + * @param vehicle2 The second vehicle. + * @return The value zero if vehicle1 and vehicle2 have the same state; + * a value grater zero, if the state of vehicle1 is idle, unlike vehicle2; + * a value less than zero otherwise. + */ + @Override + public int compare(Vehicle vehicle1, Vehicle vehicle2) { + if (vehicle1.getState() == vehicle2.getState()) { + return 0; + } + else if (vehicle1.getState() == Vehicle.State.IDLE) { + return -1; + } + else { + return 1; + } + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/AbstractDriveOrderMerger.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/AbstractDriveOrderMerger.java new file mode 100644 index 0000000..6c83640 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/AbstractDriveOrderMerger.java @@ -0,0 +1,149 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.rerouting; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import org.opentcs.components.kernel.Router; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.routing.ResourceAvoidanceExtractor; + +/** + * An abstract implementation of {@link DriveOrderMerger} defining the basic merging algorithm. + */ +public abstract class AbstractDriveOrderMerger + implements + DriveOrderMerger { + + private final Router router; + private final ResourceAvoidanceExtractor resourceAvoidanceExtractor; + + /** + * Creates a new instance. + * + * @param router The router to use. + * @param resourceAvoidanceExtractor Extracts resources to be avoided from transport orders. + */ + protected AbstractDriveOrderMerger( + Router router, + ResourceAvoidanceExtractor resourceAvoidanceExtractor + ) { + this.router = requireNonNull(router, "router"); + this.resourceAvoidanceExtractor + = requireNonNull(resourceAvoidanceExtractor, "resourceAvoidanceExtractor"); + } + + @Override + public DriveOrder mergeDriveOrders( + @Nonnull + DriveOrder orderA, + @Nonnull + DriveOrder orderB, + @Nonnull + TransportOrder originalOrder, + int currentRouteStepIndex, + Vehicle vehicle + ) { + requireNonNull(orderA, "orderA"); + requireNonNull(orderB, "orderB"); + requireNonNull(originalOrder, "originalOrder"); + + return new DriveOrder(orderA.getDestination()) + .withState(orderA.getState()) + .withTransportOrder(orderA.getTransportOrder()) + .withRoute( + mergeRoutes( + orderA.getRoute(), + orderB.getRoute(), + originalOrder, + currentRouteStepIndex, + vehicle + ) + ); + } + + /** + * Merges the two given {@link Route}s. + * + * @param routeA A route. + * @param routeB A route to be merged with {@code routeA}. + * @param originalOrder The transport order to merge the drive orders for. + * @param currentRouteStepIndex The index of the last route step travelled for {@code routeA}. + * @param vehicle The {@link Vehicle} to merge the routes for. + * @return The (new) merged route. + */ + protected Route mergeRoutes( + Route routeA, + Route routeB, + TransportOrder originalOrder, + int currentRouteStepIndex, + Vehicle vehicle + ) { + // Merge the route steps + List<Route.Step> mergedSteps = mergeSteps( + routeA.getSteps(), + routeB.getSteps(), + currentRouteStepIndex + ); + + // Calculate the costs for merged route + return new Route( + mergedSteps, + router.getCosts( + vehicle, + mergedSteps.get(0).getSourcePoint(), + mergedSteps.get(mergedSteps.size() - 1).getDestinationPoint(), + resourceAvoidanceExtractor + .extractResourcesToAvoid(originalOrder) + .toResourceReferenceSet() + ) + ); + } + + /** + * Merges the two given lists of {@link Route.Step}s. + * + * @param stepsA A list of steps. + * @param stepsB A list of steps to be merged with {@code stepsA}. + * @param currentRouteStepIndex The index of the last route step travelled for {@code stepsA}. + * @return The (new) merged list of steps. + */ + protected abstract List<Route.Step> mergeSteps( + List<Route.Step> stepsA, + List<Route.Step> stepsB, + int currentRouteStepIndex + ); + + protected List<Route.Step> updateRouteIndices(List<Route.Step> steps) { + List<Route.Step> updatedSteps = new ArrayList<>(); + for (int i = 0; i < steps.size(); i++) { + Route.Step currStep = steps.get(i); + updatedSteps.add( + new Route.Step( + currStep.getPath(), + currStep.getSourcePoint(), + currStep.getDestinationPoint(), + currStep.getVehicleOrientation(), + i, + currStep.isExecutionAllowed(), + currStep.getReroutingType() + ) + ); + } + return updatedSteps; + } + + protected List<Path> stepsToPaths(List<Route.Step> steps) { + return steps.stream() + .map(step -> step.getPath()) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/AbstractReroutingStrategy.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/AbstractReroutingStrategy.java new file mode 100644 index 0000000..0c33159 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/AbstractReroutingStrategy.java @@ -0,0 +1,188 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.rerouting; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.TransportOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An abstract implementation of {@link ReroutingStrategy} defining the basic rerouting algorithm. + */ +public abstract class AbstractReroutingStrategy + implements + ReroutingStrategy { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractReroutingStrategy.class); + private final Router router; + private final TCSObjectService objectService; + private final DriveOrderMerger driveOrderMerger; + + /** + * Creates a new instance. + * + * @param router The router to use. + * @param objectService The object service to use. + * @param driveOrderMerger Used to restore drive order history for a newly computed route. + */ + protected AbstractReroutingStrategy( + Router router, + TCSObjectService objectService, + DriveOrderMerger driveOrderMerger + ) { + this.router = requireNonNull(router, "router"); + this.objectService = requireNonNull(objectService, "objectService"); + this.driveOrderMerger = requireNonNull(driveOrderMerger, "driveOrderMerger"); + } + + @Override + public Optional<List<DriveOrder>> reroute(Vehicle vehicle) { + TransportOrder currentTransportOrder = objectService.fetchObject( + TransportOrder.class, + vehicle.getTransportOrder() + ); + + LOG.debug("{}: Determining the reroute source...", vehicle.getName()); + Optional<Point> optRerouteSource = determineRerouteSource(vehicle); + if (optRerouteSource.isEmpty()) { + LOG.warn( + "{}: Could not determine the reroute source. Not trying to reroute.", + vehicle.getName() + ); + return Optional.empty(); + } + Point rerouteSource = optRerouteSource.get(); + + // Get all unfinished drive order of the transport order the vehicle is processing. + List<DriveOrder> unfinishedOrders = getUnfinishedDriveOrders(currentTransportOrder); + + // Try to get a new route for the unfinished drive orders from the reroute source. + Optional<List<DriveOrder>> optOrders = tryReroute(vehicle, unfinishedOrders, rerouteSource); + + if (optOrders.isEmpty()) { + return Optional.empty(); + } + + List<DriveOrder> newDriveOrders = optOrders.get(); + LOG.debug( + "Found a new route for {} from point {}: {}", + vehicle.getName(), + rerouteSource.getName(), + newDriveOrders + ); + restoreCurrentDriveOrderHistory(newDriveOrders, vehicle, currentTransportOrder, rerouteSource); + + return Optional.of(newDriveOrders); + } + + protected TCSObjectService getObjectService() { + return objectService; + } + + /** + * Determines the {@link Point} that should be the source point for the rerouting. + * + * @param vehicle The vehicle to determine the reroute source point for. + * @return The {@link Point} wrapped in an {@link Optional} or {@link Optional#EMPTY}, if a + * source point for the rerouting could not be determined. + */ + protected abstract Optional<Point> determineRerouteSource(Vehicle vehicle); + + /** + * Returns a list of drive orders that haven't been finished for the given transport order, yet. + * + * @param order The transport order to get unfinished drive orders from. + * @return The list of unfinished drive orders. + */ + private List<DriveOrder> getUnfinishedDriveOrders(TransportOrder order) { + List<DriveOrder> result = new ArrayList<>(); + result.add(order.getCurrentDriveOrder()); + result.addAll(order.getFutureDriveOrders()); + return result; + } + + /** + * Tries to reroute the given vehicle for the given drive orders. + * + * @param vehicle The vehicle to reroute. + * @param driveOrders The drive orders for which to get a new route. + * @param sourcePoint The source point to reroute from. + * @return If rerouting is possible, an {@link Optional} containing the rerouted list of drive + * orders, otherwise {@link Optional#EMPTY}. + */ + private Optional<List<DriveOrder>> tryReroute( + Vehicle vehicle, + List<DriveOrder> driveOrders, + Point sourcePoint + ) { + LOG.debug( + "Trying to reroute drive orders for {} from {}. Current drive orders: {}", + vehicle.getName(), + sourcePoint, + driveOrders + ); + TransportOrder vehicleOrder = objectService.fetchObject( + TransportOrder.class, + vehicle.getTransportOrder() + ); + return router.getRoute( + vehicle, + sourcePoint, + new TransportOrder("reroute-dummy", driveOrders) + .withProperties(vehicleOrder.getProperties()) + ); + } + + private void restoreCurrentDriveOrderHistory( + List<DriveOrder> newDriveOrders, + Vehicle vehicle, + TransportOrder originalOrder, + Point rerouteSource + ) { + // If the vehicle is currently not processing a (drive) order or waiting to get the next + // drive order (i.e. if it's idle) there is nothing to be restored. + if (vehicle.hasProcState(Vehicle.ProcState.IDLE)) { + return; + } + + // XXX Is a distinction even necessary here, or could the else-part be performed in general? + if (isPointDestinationOfOrder(rerouteSource, originalOrder.getCurrentDriveOrder())) { + // The current drive order could not get rerouted, because the vehicle already + // received all commands for it. Therefore we want to keep the original drive order. + newDriveOrders.set(0, originalOrder.getCurrentDriveOrder()); + } + else { + // Restore the current drive order's history + DriveOrder newCurrentOrder + = driveOrderMerger.mergeDriveOrders( + originalOrder.getCurrentDriveOrder(), + newDriveOrders.get(0), + originalOrder, + originalOrder.getCurrentRouteStepIndex(), + vehicle + ); + newDriveOrders.set(0, newCurrentOrder); + } + } + + private boolean isPointDestinationOfOrder(Point point, DriveOrder order) { + if (point == null || order == null) { + return false; + } + if (order.getRoute() == null) { + return false; + } + return Objects.equals(point, order.getRoute().getFinalDestinationPoint()); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/DriveOrderMerger.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/DriveOrderMerger.java new file mode 100644 index 0000000..0f25601 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/DriveOrderMerger.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.rerouting; + +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.TransportOrder; + +/** + * Provides a method to merge two {@link DriveOrder}s. + */ +public interface DriveOrderMerger { + + /** + * Merges the two given {@link DriveOrder}s. + * + * @param orderA A drive order. + * @param orderB A drive order to be merged with {@code orderA}. + * @param originalOrder The transport order to merge the drive orders for. + * @param currentRouteStepIndex The index of the last route step travelled for {@code orderA}. + * @param vehicle The {@link Vehicle} to merge the drive orders for. + * @return The (new) merged drive order. + */ + DriveOrder mergeDriveOrders( + DriveOrder orderA, + DriveOrder orderB, + TransportOrder originalOrder, + int currentRouteStepIndex, + Vehicle vehicle + ); +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/ForcedDriveOrderMerger.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/ForcedDriveOrderMerger.java new file mode 100644 index 0000000..37e58d2 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/ForcedDriveOrderMerger.java @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.rerouting; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.List; +import org.opentcs.components.kernel.Router; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.data.order.Route; +import org.opentcs.strategies.basic.routing.ResourceAvoidanceExtractor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link DriveOrderMerger} implementation for {@link ReroutingType#FORCED}. + * <p> + * Merges two drive orders so that the merged drive order follows the route of {@code orderA} up to + * the current route progress index reported by the vehicle that is processing the drive order. From + * there, the merged drive order follows the route of {@code orderB}. This means that the merged + * drive order may contain a gap/may not be continuous. + */ +public class ForcedDriveOrderMerger + extends + AbstractDriveOrderMerger { + + private static final Logger LOG = LoggerFactory.getLogger(ForcedDriveOrderMerger.class); + + /** + * Creates a new instance. + * + * @param router The router to use. + * @param resourceAvoidanceExtractor Extracts resources to be avoided from transport orders. + */ + @Inject + public ForcedDriveOrderMerger( + Router router, + ResourceAvoidanceExtractor resourceAvoidanceExtractor + ) { + super(router, resourceAvoidanceExtractor); + } + + @Override + protected List<Route.Step> mergeSteps( + List<Route.Step> stepsA, + List<Route.Step> stepsB, + int currentRouteStepIndex + ) { + LOG.debug("Merging steps {} with {}", stepsToPaths(stepsA), stepsToPaths(stepsB)); + List<Route.Step> mergedSteps = new ArrayList<>(); + + // Get the steps that the vehicle has already travelled. + mergedSteps.addAll(stepsA.subList(0, currentRouteStepIndex + 1)); + + // Set the rerouting type for the first step in the new route. + Route.Step firstStepOfNewRoute = stepsB.get(0); + List<Route.Step> modifiedStepsB = new ArrayList<>(stepsB); + modifiedStepsB.set( + 0, new Route.Step( + firstStepOfNewRoute.getPath(), + firstStepOfNewRoute.getSourcePoint(), + firstStepOfNewRoute.getDestinationPoint(), + firstStepOfNewRoute.getVehicleOrientation(), + firstStepOfNewRoute.getRouteIndex(), + firstStepOfNewRoute.isExecutionAllowed(), + ReroutingType.FORCED + ) + ); + + mergedSteps.addAll(modifiedStepsB); + + // Update the steps route indices since they originate from two different drive orders + mergedSteps = updateRouteIndices(mergedSteps); + + return mergedSteps; + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/ForcedReroutingStrategy.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/ForcedReroutingStrategy.java new file mode 100644 index 0000000..6545be0 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/ForcedReroutingStrategy.java @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.rerouting; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Optional; +import java.util.Set; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.InternalTransportOrderService; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.drivers.vehicle.VehicleController; +import org.opentcs.drivers.vehicle.VehicleControllerPool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ReroutingStrategy} implementation for {@link ReroutingType#FORCED}. + * <p> + * Reroutes a {@link Vehicle} from its current position, but only if the vehicle is allowed to + * allocated the resources for that position. + */ +public class ForcedReroutingStrategy + extends + AbstractReroutingStrategy { + + private static final Logger LOG = LoggerFactory.getLogger(ForcedReroutingStrategy.class); + private final VehicleControllerPool vehicleControllerPool; + private final InternalTransportOrderService transportOrderService; + + @Inject + public ForcedReroutingStrategy( + Router router, + InternalTransportOrderService transportOrderService, + VehicleControllerPool vehicleControllerPool, + ForcedDriveOrderMerger driveOrderMerger + ) { + super(router, transportOrderService, driveOrderMerger); + this.transportOrderService = requireNonNull(transportOrderService, "transportOrderService"); + this.vehicleControllerPool = requireNonNull(vehicleControllerPool, "vehicleControllerPool"); + } + + @Override + protected Optional<Point> determineRerouteSource(Vehicle vehicle) { + Point currentVehiclePosition = transportOrderService.fetchObject( + Point.class, + vehicle.getCurrentPosition() + ); + + if (currentVehiclePosition == null) { + return Optional.empty(); + } + + VehicleController vehicleController + = vehicleControllerPool.getVehicleController(vehicle.getName()); + if (!vehicleController.mayAllocateNow(Set.of(currentVehiclePosition))) { + LOG.warn( + "{}: The resources for the current position are unavailable. " + + "Unable to determine the reroute source.", + vehicle.getName() + ); + return Optional.empty(); + } + + return Optional.of(currentVehiclePosition); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/RegularDriveOrderMerger.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/RegularDriveOrderMerger.java new file mode 100644 index 0000000..36d0e92 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/RegularDriveOrderMerger.java @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.rerouting; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.opentcs.components.kernel.Router; +import org.opentcs.data.model.Point; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.data.order.Route; +import org.opentcs.strategies.basic.routing.ResourceAvoidanceExtractor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link DriveOrderMerger} implementation for {@link ReroutingType#REGULAR}. + * <p> + * Merges two drive orders so that the merged drive order follows the route of {@code orderA} up to + * the point where both drive orders ({@code orderA} and {@code orderB}) start to diverge. From + * there, the merged drive order follows the route of {@code orderB}. + */ +public class RegularDriveOrderMerger + extends + AbstractDriveOrderMerger { + + private static final Logger LOG = LoggerFactory.getLogger(RegularDriveOrderMerger.class); + + /** + * Creates a new instance. + * + * @param router The router to use. + * @param resourceAvoidanceExtractor Extracts resources to be avoided from transport orders. + */ + @Inject + public RegularDriveOrderMerger( + Router router, + ResourceAvoidanceExtractor resourceAvoidanceExtractor + ) { + super(router, resourceAvoidanceExtractor); + } + + @Override + protected List<Route.Step> mergeSteps( + List<Route.Step> stepsA, + List<Route.Step> stepsB, + int currentRouteStepIndex + ) { + LOG.debug("Merging steps {} with {}", stepsToPaths(stepsA), stepsToPaths(stepsB)); + List<Route.Step> mergedSteps = new ArrayList<>(); + + // Get the step where stepsB starts to diverge from stepsA (i.e. the step where routeA and + // routeB share the same source point). + Route.Step divergingStep = findStepWithSource(stepsB.get(0).getSourcePoint(), stepsA); + int divergingIndex = stepsA.indexOf(divergingStep); + mergedSteps.addAll(stepsA.subList(0, divergingIndex)); + + // Set the rerouting type for the first step in the new route. + Route.Step firstStepOfNewRoute = stepsB.get(0); + List<Route.Step> modifiedStepsB = new ArrayList<>(stepsB); + modifiedStepsB.set( + 0, new Route.Step( + firstStepOfNewRoute.getPath(), + firstStepOfNewRoute.getSourcePoint(), + firstStepOfNewRoute.getDestinationPoint(), + firstStepOfNewRoute.getVehicleOrientation(), + firstStepOfNewRoute.getRouteIndex(), + firstStepOfNewRoute.isExecutionAllowed(), + ReroutingType.REGULAR + ) + ); + + mergedSteps.addAll(modifiedStepsB); + + // Update the steps route indices since they originate from two different drive orders. + mergedSteps = updateRouteIndices(mergedSteps); + + return mergedSteps; + } + + private Route.Step findStepWithSource(Point sourcePoint, List<Route.Step> steps) { + LOG.debug( + "Looking for a step with source point {} in {}", + sourcePoint, + stepsToPaths(steps) + ); + return steps.stream() + .filter(step -> Objects.equals(step.getSourcePoint(), sourcePoint)) + .findFirst() + .get(); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/RegularReroutingStrategy.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/RegularReroutingStrategy.java new file mode 100644 index 0000000..b38a23b --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/RegularReroutingStrategy.java @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.rerouting; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.ReroutingType; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ReroutingStrategy} implementation for {@link ReroutingType#REGULAR}. + * <p> + * Reroutes a {@link Vehicle} from its future or current position according to + * {@link VehiclePositionResolver#getFutureOrCurrentPosition(org.opentcs.data.model.Vehicle)}. + */ +public class RegularReroutingStrategy + extends + AbstractReroutingStrategy + implements + ReroutingStrategy { + + private static final Logger LOG = LoggerFactory.getLogger(RegularReroutingStrategy.class); + private final VehiclePositionResolver vehiclePositionResolver; + + @Inject + public RegularReroutingStrategy( + Router router, + TCSObjectService objectService, + RegularDriveOrderMerger driveOrderMerger, + VehiclePositionResolver vehiclePositionResolver + ) { + super(router, objectService, driveOrderMerger); + this.vehiclePositionResolver = requireNonNull( + vehiclePositionResolver, + "vehiclePositionResolver" + ); + } + + @Override + public Optional<List<DriveOrder>> reroute(Vehicle vehicle) { + if (!isVehicleAtExpectedPosition(vehicle)) { + LOG.warn( + "Can't perform regular rerouting for {} located at unexpected position.", + vehicle.getName() + ); + return Optional.empty(); + } + + return super.reroute(vehicle); + } + + @Override + protected Optional<Point> determineRerouteSource(Vehicle vehicle) { + return Optional.of(vehiclePositionResolver.getFutureOrCurrentPosition(vehicle)); + } + + private boolean isVehicleAtExpectedPosition(Vehicle vehicle) { + TransportOrder currentTransportOrder + = getObjectService().fetchObject(TransportOrder.class, vehicle.getTransportOrder()); + TCSObjectReference<Point> currentVehiclePosition = vehicle.getCurrentPosition(); + DriveOrder currentDriveOrder = currentTransportOrder.getCurrentDriveOrder(); + if (currentVehiclePosition == null || currentDriveOrder == null) { + return false; + } + + int routeProgressIndex = currentTransportOrder.getCurrentRouteStepIndex(); + if (routeProgressIndex == TransportOrder.ROUTE_STEP_INDEX_DEFAULT) { + Route.Step step = currentDriveOrder.getRoute().getSteps().get(0); + Point expectedVehiclePosition + = step.getSourcePoint() != null ? step.getSourcePoint() : step.getDestinationPoint(); + return Objects.equals(expectedVehiclePosition.getReference(), currentVehiclePosition); + } + + Route.Step step = currentDriveOrder.getRoute().getSteps().get(routeProgressIndex); + Point expectedVehiclePosition = step.getDestinationPoint(); + return Objects.equals(expectedVehiclePosition.getReference(), currentVehiclePosition); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/ReroutingStrategy.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/ReroutingStrategy.java new file mode 100644 index 0000000..70a0f86 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/ReroutingStrategy.java @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.rerouting; + +import java.util.List; +import java.util.Optional; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.TransportOrder; + +/** + * A strategy for rerouting {@link Vehicle}s. + */ +public interface ReroutingStrategy { + + /** + * Tries to calculate a new route for the given {@link Vehicle} and the {@link TransportOrder} + * it's currently processing. + * <p> + * The new route should consider the given vehicle's transport order progress so that the returned + * list of {@link DriveOrder}s doesn't contain any drive orders that the vehicle already finished. + * + * @param vehicle The vehicle to calculate a new route for. + * @return An {@link Optional} containing the new drive orders or {@link Optional#EMPTY}, if + * no new route could be calculated (e.g. because the given vehicle is not processing a transport + * order). + */ + Optional<List<DriveOrder>> reroute(Vehicle vehicle); +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/VehiclePositionResolver.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/VehiclePositionResolver.java new file mode 100644 index 0000000..ddb64e4 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/rerouting/VehiclePositionResolver.java @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.rerouting; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.List; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.drivers.vehicle.MovementCommand; +import org.opentcs.drivers.vehicle.VehicleCommAdapter; +import org.opentcs.drivers.vehicle.VehicleController; +import org.opentcs.drivers.vehicle.VehicleControllerPool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides methods to resolve the position of a {@link Vehicle}. + */ +public class VehiclePositionResolver { + + private static final Logger LOG = LoggerFactory.getLogger(VehiclePositionResolver.class); + private final VehicleControllerPool vehicleControllerPool; + private final TCSObjectService objectService; + + /** + * Creates a new instance. + * + * @param vehicleControllerPool The pool of {@link VehicleController}s. + * @param objectService The object service to use. + */ + @Inject + public VehiclePositionResolver( + VehicleControllerPool vehicleControllerPool, + TCSObjectService objectService + ) { + this.vehicleControllerPool = requireNonNull(vehicleControllerPool, "vehicleControllerPool"); + this.objectService = requireNonNull(objectService, "objectService"); + } + + /** + * Returns the position the given {@link Vehicle} will be at after processing all commands that + * have been or are to be sent to its {@link VehicleCommAdapter}, or its current position, if + * there are no such commands. + * + * @param vehicle The vehicle to get the position for. + * @return The position as a {@link Point}. + */ + public Point getFutureOrCurrentPosition(Vehicle vehicle) { + VehicleController controller = vehicleControllerPool.getVehicleController(vehicle.getName()); + if (controller.getCommandsSent().isEmpty() + && controller.getInteractionsPendingCommand().isEmpty()) { + LOG.debug( + "{}: No commands expected to be executed. Using current position: {}", + vehicle.getName(), + vehicle.getCurrentPosition() + ); + return objectService.fetchObject(Point.class, vehicle.getCurrentPosition()); + } + + if (controller.getInteractionsPendingCommand().isPresent()) { + LOG.debug( + "{}: Command with pending peripheral operations present. Using its destination point: {}", + vehicle.getName(), + controller.getInteractionsPendingCommand().get().getStep().getDestinationPoint() + ); + return controller.getInteractionsPendingCommand().get().getStep().getDestinationPoint(); + } + + List<MovementCommand> commandsSent = new ArrayList<>(controller.getCommandsSent()); + MovementCommand lastCommandSent = commandsSent.get(commandsSent.size() - 1); + LOG.debug( + "{}: Using the last command sent to the communication adapter: {}", + vehicle.getName(), + lastCommandSent + ); + return lastCommandSent.getStep().getDestinationPoint(); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/AssignmentCandidateSelectionFilter.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/AssignmentCandidateSelectionFilter.java new file mode 100644 index 0000000..1502c9f --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/AssignmentCandidateSelectionFilter.java @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection; + +import java.util.Collection; +import java.util.function.Function; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; + +/** + * A filter for {@link AssignmentCandidate}s. + * Returns a collection of reasons for filtering the assignment candidate. + * If the returned collection is empty, no reason to filter it was encountered. + */ +public interface AssignmentCandidateSelectionFilter + extends + Function<AssignmentCandidate, Collection<String>> { +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/ParkVehicleSelectionFilter.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/ParkVehicleSelectionFilter.java new file mode 100644 index 0000000..d9b1c00 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/ParkVehicleSelectionFilter.java @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection; + +import java.util.Collection; +import java.util.function.Function; +import org.opentcs.data.model.Vehicle; + +/** + * A filter for selecting {@link Vehicle}s for parking. + * Returns a collection of reasons for filtering the vehicle. + * If the returned collection is empty, no reason to filter it was encountered. + */ +public interface ParkVehicleSelectionFilter + extends + Function<Vehicle, Collection<String>> { +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/RechargeVehicleSelectionFilter.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/RechargeVehicleSelectionFilter.java new file mode 100644 index 0000000..86734de --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/RechargeVehicleSelectionFilter.java @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection; + +import java.util.Collection; +import java.util.function.Function; +import org.opentcs.data.model.Vehicle; + +/** + * A filter for selecting {@link Vehicle}s for recharge orders. + * Returns a collection of reasons for filtering the vehicle. + * If the returned collection is empty, no reason to filter it was encountered. + */ +public interface RechargeVehicleSelectionFilter + extends + Function<Vehicle, Collection<String>> { +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/ReparkVehicleSelectionFilter.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/ReparkVehicleSelectionFilter.java new file mode 100644 index 0000000..15ec6e6 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/ReparkVehicleSelectionFilter.java @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection; + +import java.util.Collection; +import java.util.function.Function; +import org.opentcs.data.model.Vehicle; + +/** + * A filter for selecting {@link Vehicle}s for reparking. + * Returns a collection of reasons for filtering the vehicle. + * If the returned collection is empty, no reason to filter it was encountered. + */ +public interface ReparkVehicleSelectionFilter + extends + Function<Vehicle, Collection<String>> { +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/TransportOrderSelectionFilter.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/TransportOrderSelectionFilter.java new file mode 100644 index 0000000..0f6bbff --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/TransportOrderSelectionFilter.java @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection; + +import java.util.Collection; +import java.util.function.Function; +import org.opentcs.data.order.TransportOrder; + +/** + * A filter for {@link TransportOrder}s. + * Returns a collection of reasons for filtering the transport order. + * If the returned collection is empty, no reason to filter it was encountered. + */ +public interface TransportOrderSelectionFilter + extends + Function<TransportOrder, Collection<String>> { +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/VehicleSelectionFilter.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/VehicleSelectionFilter.java new file mode 100644 index 0000000..6296423 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/VehicleSelectionFilter.java @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection; + +import java.util.Collection; +import java.util.function.Function; +import org.opentcs.data.model.Vehicle; + +/** + * A filter for {@link Vehicle}s. + * Returns a collection of reasons for filtering the vehicle. + * If the returned collection is empty, no reason to filter it was encountered. + */ +public interface VehicleSelectionFilter + extends + Function<Vehicle, Collection<String>> { +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/candidates/CompositeAssignmentCandidateSelectionFilter.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/candidates/CompositeAssignmentCandidateSelectionFilter.java new file mode 100644 index 0000000..2b0c884 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/candidates/CompositeAssignmentCandidateSelectionFilter.java @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection.candidates; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.selection.AssignmentCandidateSelectionFilter; + +/** + * A collection of {@link AssignmentCandidateSelectionFilter}s. + */ +public class CompositeAssignmentCandidateSelectionFilter + implements + AssignmentCandidateSelectionFilter { + + /** + * The {@link AssignmentCandidateSelectionFilter}s. + */ + private final Set<AssignmentCandidateSelectionFilter> filters; + + @Inject + public CompositeAssignmentCandidateSelectionFilter( + Set<AssignmentCandidateSelectionFilter> filters + ) { + this.filters = requireNonNull(filters, "filters"); + } + + @Override + public Collection<String> apply(AssignmentCandidate candidate) { + return filters.stream() + .flatMap(filter -> filter.apply(candidate).stream()) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/candidates/IsProcessable.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/candidates/IsProcessable.java new file mode 100644 index 0000000..856694f --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/candidates/IsProcessable.java @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection.candidates; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderConstants; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.drivers.vehicle.VehicleControllerPool; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.selection.AssignmentCandidateSelectionFilter; +import org.opentcs.util.ExplainedBoolean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Filters assignment candidates with which the transport order is actually processable by the + * vehicle. + */ +public class IsProcessable + implements + AssignmentCandidateSelectionFilter { + + /** + * An error code indicating that there's a conflict between the type of a transport order and + * the types a vehicle is allowed to process. + */ + private static final String ORDER_TYPE_CONFLICT = "notAllowedOrderType"; + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(IsProcessable.class); + /** + * The vehicle controller pool. + */ + private final VehicleControllerPool vehicleControllerPool; + + /** + * Creates a new instance. + * + * @param vehicleControllerPool The controller pool to be worked with. + */ + @Inject + public IsProcessable(VehicleControllerPool vehicleControllerPool) { + this.vehicleControllerPool = requireNonNull(vehicleControllerPool, "vehicleControllerPool"); + } + + @Override + public Collection<String> apply(AssignmentCandidate candidate) { + ExplainedBoolean result = checkProcessability( + candidate.getVehicle(), + candidate.getTransportOrder() + ); + return result.getValue() + ? new ArrayList<>() + : Arrays.asList(candidate.getVehicle().getName() + "(" + result.getReason() + ")"); + } + + /** + * Checks if the given vehicle could process the given order right now. + * + * @param vehicle The vehicle. + * @param order The order. + * @return <code>true</code> if, and only if, the given vehicle can process the given order. + */ + private ExplainedBoolean checkProcessability(Vehicle vehicle, TransportOrder order) { + requireNonNull(vehicle, "vehicle"); + requireNonNull(order, "order"); + + // Check for matching order types + if (!vehicle.getAllowedOrderTypes().contains(OrderConstants.TYPE_ANY) + && !vehicle.getAllowedOrderTypes().contains(order.getType())) { + LOG.debug( + "Type '{}' of order '{}' not in allowed types '{}' of vehicle '{}'.", + order.getType(), + order.getName(), + vehicle.getAllowedOrderTypes(), + vehicle.getName() + ); + return new ExplainedBoolean(false, ORDER_TYPE_CONFLICT); + } + + return vehicleControllerPool.getVehicleController(vehicle.getName()).canProcess(order); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/orders/CompositeTransportOrderSelectionFilter.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/orders/CompositeTransportOrderSelectionFilter.java new file mode 100644 index 0000000..1e3f215 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/orders/CompositeTransportOrderSelectionFilter.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection.orders; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.selection.TransportOrderSelectionFilter; + +/** + * A collection of {@link TransportOrderSelectionFilter}s. + */ +public class CompositeTransportOrderSelectionFilter + implements + TransportOrderSelectionFilter { + + /** + * The {@link TransportOrderSelectionFilter}s. + */ + private final Set<TransportOrderSelectionFilter> filters; + + @Inject + public CompositeTransportOrderSelectionFilter(Set<TransportOrderSelectionFilter> filters) { + this.filters = requireNonNull(filters, "filters"); + } + + @Override + public Collection<String> apply(TransportOrder order) { + return filters.stream() + .flatMap(filter -> filter.apply(order).stream()) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/orders/ContainsLockedTargetLocations.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/orders/ContainsLockedTargetLocations.java new file mode 100644 index 0000000..3a672b7 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/orders/ContainsLockedTargetLocations.java @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection.orders; + +import static java.util.Objects.requireNonNull; + +import com.google.common.base.Objects; +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.selection.TransportOrderSelectionFilter; + +/** + * Filters transport orders that contain locked target locations. + */ +public class ContainsLockedTargetLocations + implements + TransportOrderSelectionFilter { + + /** + * The object service. + */ + private final TCSObjectService objectService; + + @Inject + public ContainsLockedTargetLocations(TCSObjectService objectService) { + this.objectService = requireNonNull(objectService, "objectService"); + } + + @Override + public Collection<String> apply(TransportOrder order) { + return !lockedLocations(order) ? new ArrayList<>() : Arrays.asList(getClass().getName()); + } + + @SuppressWarnings("unchecked") + private boolean lockedLocations(TransportOrder order) { + return order.getAllDriveOrders().stream() + .map(driveOrder -> driveOrder.getDestination().getDestination()) + .filter(destination -> Objects.equal(destination.getReferentClass(), Location.class)) + .map(destination -> (TCSObjectReference<Location>) destination) + .map(locationReference -> objectService.fetchObject(Location.class, locationReference)) + .anyMatch(location -> location.isLocked()); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/orders/IsFreelyDispatchableToAnyVehicle.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/orders/IsFreelyDispatchableToAnyVehicle.java new file mode 100644 index 0000000..86aba18 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/orders/IsFreelyDispatchableToAnyVehicle.java @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection.orders; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.function.Predicate; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.OrderReservationPool; +import org.opentcs.strategies.basic.dispatching.selection.TransportOrderSelectionFilter; + +/** + * Filters transport orders that are dispatchable and available to <em>any</em> vehicle. + * + * <p> + * Note: This filter is not a {@link TransportOrderSelectionFilter} by intention, since it is not + * intended to be used in contexts where {@link ObjectHistory} entries are created. + * </p> + */ +public class IsFreelyDispatchableToAnyVehicle + implements + Predicate<TransportOrder> { + + /** + * The order service. + */ + private final TCSObjectService objectService; + /** + * Stores reservations of orders for vehicles. + */ + private final OrderReservationPool orderReservationPool; + + /** + * Creates a new isntance. + * + * @param objectService The order service. + * @param orderReservationPool Stores reservations of orders for vehicles. + */ + @Inject + public IsFreelyDispatchableToAnyVehicle( + TCSObjectService objectService, + OrderReservationPool orderReservationPool + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.orderReservationPool = requireNonNull(orderReservationPool, "orderReservationPool"); + } + + @Override + public boolean test(TransportOrder order) { + // We only want to check dispatchable transport orders. + // Filter out transport orders that are intended for other vehicles. + // Also filter out all transport orders with reservations. We assume that a check for reserved + // orders has been performed already, and if any had been found, we wouldn't have been called. + return order.hasState(TransportOrder.State.DISPATCHABLE) + && !partOfAnyVehiclesSequence(order) + && !orderReservationPool.isReserved(order.getReference()); + } + + private boolean partOfAnyVehiclesSequence(TransportOrder order) { + if (order.getWrappingSequence() == null) { + return false; + } + OrderSequence seq = objectService.fetchObject( + OrderSequence.class, + order.getWrappingSequence() + ); + return seq != null && seq.getProcessingVehicle() != null; + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/CompositeParkVehicleSelectionFilter.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/CompositeParkVehicleSelectionFilter.java new file mode 100644 index 0000000..4283c5f --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/CompositeParkVehicleSelectionFilter.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection.vehicles; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.dispatching.selection.ParkVehicleSelectionFilter; + +/** + * A collection of {@link ParkVehicleSelectionFilter}s. + */ +public class CompositeParkVehicleSelectionFilter + implements + ParkVehicleSelectionFilter { + + /** + * The {@link ParkVehicleSelectionFilter}s. + */ + private final Set<ParkVehicleSelectionFilter> filters; + + @Inject + public CompositeParkVehicleSelectionFilter(Set<ParkVehicleSelectionFilter> filters) { + this.filters = requireNonNull(filters, "filters"); + } + + @Override + public Collection<String> apply(Vehicle vehicle) { + return filters.stream() + .flatMap(filter -> filter.apply(vehicle).stream()) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/CompositeRechargeVehicleSelectionFilter.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/CompositeRechargeVehicleSelectionFilter.java new file mode 100644 index 0000000..e55308e --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/CompositeRechargeVehicleSelectionFilter.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection.vehicles; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.dispatching.selection.RechargeVehicleSelectionFilter; + +/** + * A collection of {@link RechargeVehicleSelectionFilter}s. + */ +public class CompositeRechargeVehicleSelectionFilter + implements + RechargeVehicleSelectionFilter { + + /** + * The {@link RechargeVehicleSelectionFilter}s. + */ + private final Set<RechargeVehicleSelectionFilter> filters; + + @Inject + public CompositeRechargeVehicleSelectionFilter(Set<RechargeVehicleSelectionFilter> filters) { + this.filters = requireNonNull(filters, "filters"); + } + + @Override + public Collection<String> apply(Vehicle vehicle) { + return filters.stream() + .flatMap(filter -> filter.apply(vehicle).stream()) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/CompositeReparkVehicleSelectionFilter.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/CompositeReparkVehicleSelectionFilter.java new file mode 100644 index 0000000..8cdb280 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/CompositeReparkVehicleSelectionFilter.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection.vehicles; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.dispatching.selection.ReparkVehicleSelectionFilter; + +/** + * A collection of {@link ReparkVehicleSelectionFilter}s. + */ +public class CompositeReparkVehicleSelectionFilter + implements + ReparkVehicleSelectionFilter { + + /** + * The {@link ParkVehicleSelectionFilter}s. + */ + private final Set<ReparkVehicleSelectionFilter> filters; + + @Inject + public CompositeReparkVehicleSelectionFilter(Set<ReparkVehicleSelectionFilter> filters) { + this.filters = requireNonNull(filters, "filters"); + } + + @Override + public Collection<String> apply(Vehicle vehicle) { + return filters.stream() + .flatMap(filter -> filter.apply(vehicle).stream()) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/CompositeVehicleSelectionFilter.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/CompositeVehicleSelectionFilter.java new file mode 100644 index 0000000..511194f --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/CompositeVehicleSelectionFilter.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection.vehicles; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.dispatching.selection.VehicleSelectionFilter; + +/** + * A collection of {@link VehicleSelectionFilter}s. + */ +public class CompositeVehicleSelectionFilter + implements + VehicleSelectionFilter { + + /** + * The {@link VehicleSelectionFilter}s. + */ + private final Set<VehicleSelectionFilter> filters; + + @Inject + public CompositeVehicleSelectionFilter(Set<VehicleSelectionFilter> filters) { + this.filters = requireNonNull(filters, "filters"); + } + + @Override + public Collection<String> apply(Vehicle vehicle) { + return filters.stream() + .flatMap(filter -> filter.apply(vehicle).stream()) + .collect(Collectors.toList()); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsAvailableForAnyOrder.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsAvailableForAnyOrder.java new file mode 100644 index 0000000..8291d3b --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsAvailableForAnyOrder.java @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection.vehicles; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.function.Predicate; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration; +import org.opentcs.strategies.basic.dispatching.OrderReservationPool; +import org.opentcs.strategies.basic.dispatching.selection.VehicleSelectionFilter; + +/** + * Filters vehicles that are generally available for transport orders. + * + * <p> + * Note: This filter is not a {@link VehicleSelectionFilter} by intention, since it is not + * intended to be used in contexts where {@link ObjectHistory} entries are created. + * </p> + */ +public class IsAvailableForAnyOrder + implements + Predicate<Vehicle> { + + /** + * The object service. + */ + private final TCSObjectService objectService; + /** + * Stores reservations of orders for vehicles. + */ + private final OrderReservationPool orderReservationPool; + /** + * The default dispatcher configuration. + */ + private final DefaultDispatcherConfiguration configuration; + + /** + * Creates a new instance. + * + * @param objectService The object service. + * @param orderReservationPool Stores reservations of orders for vehicles. + * @param configuration The default dispatcher configuration. + */ + @Inject + public IsAvailableForAnyOrder( + TCSObjectService objectService, + OrderReservationPool orderReservationPool, + DefaultDispatcherConfiguration configuration + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.orderReservationPool = requireNonNull(orderReservationPool, "orderReservationPool"); + this.configuration = requireNonNull(configuration, "configuration"); + } + + @Override + public boolean test(Vehicle vehicle) { + return vehicle.getIntegrationLevel() == Vehicle.IntegrationLevel.TO_BE_UTILIZED + && vehicle.getCurrentPosition() != null + && vehicle.getOrderSequence() == null + && !needsMoreCharging(vehicle) + && (processesNoOrder(vehicle) + || processesDispensableOrder(vehicle)) + && !hasOrderReservation(vehicle) + && !vehicle.isPaused(); + } + + private boolean needsMoreCharging(Vehicle vehicle) { + return vehicle.hasState(Vehicle.State.CHARGING) + && !rechargeThresholdReached(vehicle); + } + + private boolean rechargeThresholdReached(Vehicle vehicle) { + return configuration.keepRechargingUntilFullyCharged() + ? vehicle.isEnergyLevelFullyRecharged() + : vehicle.isEnergyLevelSufficientlyRecharged(); + } + + private boolean processesNoOrder(Vehicle vehicle) { + return vehicle.hasProcState(Vehicle.ProcState.IDLE) + && (vehicle.hasState(Vehicle.State.IDLE) + || vehicle.hasState(Vehicle.State.CHARGING)); + } + + private boolean processesDispensableOrder(Vehicle vehicle) { + return vehicle.hasProcState(Vehicle.ProcState.PROCESSING_ORDER) + && objectService.fetchObject(TransportOrder.class, vehicle.getTransportOrder()) + .isDispensable(); + } + + private boolean hasOrderReservation(Vehicle vehicle) { + return !orderReservationPool.findReservations(vehicle.getReference()).isEmpty(); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsIdleAndDegraded.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsIdleAndDegraded.java new file mode 100644 index 0000000..bc9e42e --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsIdleAndDegraded.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection.vehicles; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderConstants; +import org.opentcs.strategies.basic.dispatching.selection.RechargeVehicleSelectionFilter; + +/** + * Filters vehicles that are idle and have a degraded energy level. + */ +public class IsIdleAndDegraded + implements + RechargeVehicleSelectionFilter { + + /** + * Creates a new instance. + */ + public IsIdleAndDegraded() { + } + + @Override + public Collection<String> apply(Vehicle vehicle) { + return idleAndDegraded(vehicle) ? new ArrayList<>() : Arrays.asList(getClass().getName()); + } + + private boolean idleAndDegraded(Vehicle vehicle) { + return vehicle.getIntegrationLevel() == Vehicle.IntegrationLevel.TO_BE_UTILIZED + && vehicle.hasProcState(Vehicle.ProcState.IDLE) + && vehicle.hasState(Vehicle.State.IDLE) + && vehicle.getCurrentPosition() != null + && vehicle.getOrderSequence() == null + && vehicle.isEnergyLevelDegraded() + && hasAllowedOrderTypesForCharging(vehicle); + } + + private boolean hasAllowedOrderTypesForCharging(Vehicle vehicle) { + return vehicle.getAllowedOrderTypes().contains(OrderConstants.TYPE_CHARGE) + || vehicle.getAllowedOrderTypes().contains(OrderConstants.TYPE_ANY); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsParkable.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsParkable.java new file mode 100644 index 0000000..9a051da --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsParkable.java @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection.vehicles; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderConstants; +import org.opentcs.strategies.basic.dispatching.selection.ParkVehicleSelectionFilter; + +/** + * Filters vehicles that are parkable. + */ +public class IsParkable + implements + ParkVehicleSelectionFilter { + + /** + * The object service. + */ + private final TCSObjectService objectService; + + /** + * Creates a new instance. + * + * @param objectService The object service. + */ + @Inject + public IsParkable(TCSObjectService objectService) { + this.objectService = requireNonNull(objectService, "objectService"); + } + + @Override + public Collection<String> apply(Vehicle vehicle) { + return parkable(vehicle) ? new ArrayList<>() : Arrays.asList(getClass().getName()); + } + + private boolean parkable(Vehicle vehicle) { + return vehicle.getIntegrationLevel() == Vehicle.IntegrationLevel.TO_BE_UTILIZED + && vehicle.hasProcState(Vehicle.ProcState.IDLE) + && vehicle.hasState(Vehicle.State.IDLE) + && !isParkingPosition(vehicle.getCurrentPosition()) + && vehicle.getOrderSequence() == null + && hasAllowedOrderTypesForParking(vehicle); + } + + private boolean isParkingPosition(TCSObjectReference<Point> positionRef) { + if (positionRef == null) { + return false; + } + + Point position = objectService.fetchObject(Point.class, positionRef); + return position.isParkingPosition(); + } + + private boolean hasAllowedOrderTypesForParking(Vehicle vehicle) { + return vehicle.getAllowedOrderTypes().contains(OrderConstants.TYPE_PARK) + || vehicle.getAllowedOrderTypes().contains(OrderConstants.TYPE_ANY); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsReparkable.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsReparkable.java new file mode 100644 index 0000000..1183255 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsReparkable.java @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection.vehicles; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderConstants; +import org.opentcs.strategies.basic.dispatching.selection.ReparkVehicleSelectionFilter; + +/** + * Filters vehicles that are reparkable. + */ +public class IsReparkable + implements + ReparkVehicleSelectionFilter { + + /** + * The object service. + */ + private final TCSObjectService objectService; + + /** + * Creates a new instance. + * + * @param objectService The object service. + */ + @Inject + public IsReparkable(TCSObjectService objectService) { + this.objectService = requireNonNull(objectService, "objectService"); + } + + @Override + public Collection<String> apply(Vehicle vehicle) { + return reparkable(vehicle) ? new ArrayList<>() : Arrays.asList(getClass().getName()); + } + + private boolean reparkable(Vehicle vehicle) { + return vehicle.getIntegrationLevel() == Vehicle.IntegrationLevel.TO_BE_UTILIZED + && vehicle.hasProcState(Vehicle.ProcState.IDLE) + && vehicle.hasState(Vehicle.State.IDLE) + && isParkingPosition(vehicle.getCurrentPosition()) + && vehicle.getOrderSequence() == null + && hasAllowedOrderTypesForParking(vehicle); + } + + private boolean isParkingPosition(TCSObjectReference<Point> positionRef) { + if (positionRef == null) { + return false; + } + + Point position = objectService.fetchObject(Point.class, positionRef); + return position.isParkingPosition(); + } + + private boolean hasAllowedOrderTypesForParking(Vehicle vehicle) { + return vehicle.getAllowedOrderTypes().contains(OrderConstants.TYPE_PARK) + || vehicle.getAllowedOrderTypes().contains(OrderConstants.TYPE_ANY); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/DefaultJobSelectionStrategy.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/DefaultJobSelectionStrategy.java new file mode 100644 index 0000000..97f1505 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/DefaultJobSelectionStrategy.java @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.peripherals.dispatching; + +import static org.opentcs.util.Assertions.checkArgument; + +import java.util.Collection; +import java.util.Objects; +import java.util.Optional; +import org.opentcs.data.model.Location; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.util.Comparators; + +/** + * The default implementation of {@link JobSelectionStrategy}. + * Selects a job by applying the following rules: + * <ul> + * <li>The location of a job's operation has to match the given location.</li> + * <li>If this applies to multiple jobs, the oldest one is selected.</li> + * </ul> + */ +public class DefaultJobSelectionStrategy + implements + JobSelectionStrategy { + + /** + * Creates a new instance. + */ + public DefaultJobSelectionStrategy() { + } + + @Override + public Optional<PeripheralJob> select(Collection<PeripheralJob> jobs, Location location) { + checkArgument( + jobs.stream().allMatch(job -> matchesLocation(job, location)), + "All jobs are expected to match the given location: %s", location.getName() + ); + + return jobs.stream() + .sorted(Comparators.jobsByAge()) + .findFirst(); + } + + private boolean matchesLocation(PeripheralJob job, Location location) { + return Objects.equals(job.getPeripheralOperation().getLocation(), location.getReference()); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/DefaultPeripheralJobDispatcher.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/DefaultPeripheralJobDispatcher.java new file mode 100644 index 0000000..fddad3e --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/DefaultPeripheralJobDispatcher.java @@ -0,0 +1,303 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.peripherals.dispatching; + +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import org.opentcs.components.kernel.PeripheralJobDispatcher; +import org.opentcs.components.kernel.services.InternalPeripheralJobService; +import org.opentcs.components.kernel.services.InternalPeripheralService; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.PeripheralInformation; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.drivers.peripherals.PeripheralControllerPool; +import org.opentcs.drivers.peripherals.PeripheralJobCallback; +import org.opentcs.util.event.EventSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Dispatches peripheral jobs and peripheral devices represented by locations. + */ +public class DefaultPeripheralJobDispatcher + implements + PeripheralJobDispatcher, + PeripheralJobCallback { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(DefaultPeripheralJobDispatcher.class); + /** + * The peripheral service to use. + */ + private final InternalPeripheralService peripheralService; + /** + * The peripheral job service to use. + */ + private final InternalPeripheralJobService peripheralJobService; + /** + * The controller pool. + */ + private final PeripheralControllerPool controllerPool; + /** + * Where we register for application events. + */ + private final EventSource eventSource; + /** + * The kernel's executor. + */ + private final ScheduledExecutorService kernelExecutor; + /** + * Performs a full dispatch run. + */ + private final FullDispatchTask fullDispatchTask; + /** + * A task to periodically trigger the job dispatcher. + */ + private final Provider<PeriodicPeripheralRedispatchingTask> periodicDispatchTaskProvider; + /** + * A provider for an event handler to trigger the job dispatcher on certain events. + */ + private final Provider<ImplicitDispatchTrigger> implicitDispatchTriggerProvider; + /** + * The peripheral job dispatcher's configuration. + */ + private final DefaultPeripheralJobDispatcherConfiguration configuration; + /** + * The future for the periodic dispatch task. + */ + private ScheduledFuture<?> periodicDispatchTaskFuture; + /** + * An event handler to trigger the job dispatcher on certain events. + */ + private ImplicitDispatchTrigger implicitDispatchTrigger; + /** + * Indicates whether this component is enabled. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param peripheralService The peripheral service to use. + * @param peripheralJobService The peripheral job service to use. + * @param controllerPool The controller pool. + * @param eventSource Where this instance registers for application events. + * @param kernelExecutor Executes dispatching tasks. + * @param fullDispatchTask Performs a full dispatch run. + * @param periodicDispatchTaskProvider A task to periodically trigger the job dispatcher. + * @param implicitDispatchTriggerProvider A provider for an event handler to trigger the job + * dispatcher on certain events. + * @param configuration The peripheral job dispatcher's configuration. + */ + @Inject + public DefaultPeripheralJobDispatcher( + InternalPeripheralService peripheralService, + InternalPeripheralJobService peripheralJobService, + PeripheralControllerPool controllerPool, + @ApplicationEventBus + EventSource eventSource, + @KernelExecutor + ScheduledExecutorService kernelExecutor, + FullDispatchTask fullDispatchTask, + Provider<PeriodicPeripheralRedispatchingTask> periodicDispatchTaskProvider, + Provider<ImplicitDispatchTrigger> implicitDispatchTriggerProvider, + DefaultPeripheralJobDispatcherConfiguration configuration + ) { + this.peripheralService = requireNonNull(peripheralService, "peripheralService"); + this.peripheralJobService = requireNonNull(peripheralJobService, "peripheralJobService"); + this.controllerPool = requireNonNull(controllerPool, "controllerPool"); + this.eventSource = requireNonNull(eventSource, "eventSource"); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + this.fullDispatchTask = requireNonNull(fullDispatchTask, "fullDispatchTask"); + this.periodicDispatchTaskProvider = requireNonNull( + periodicDispatchTaskProvider, + "periodicDispatchTaskProvider" + ); + this.implicitDispatchTriggerProvider = requireNonNull( + implicitDispatchTriggerProvider, + "implicitDispatchTriggerProvider" + ); + this.configuration = requireNonNull(configuration, "configuration"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + LOG.debug("Initializing..."); + fullDispatchTask.initialize(); + + implicitDispatchTrigger = implicitDispatchTriggerProvider.get(); + eventSource.subscribe(implicitDispatchTrigger); + + LOG.debug( + "Scheduling periodic peripheral job dispatch task with interval of {} ms...", + configuration.idlePeripheralRedispatchingInterval() + ); + periodicDispatchTaskFuture = kernelExecutor.scheduleAtFixedRate( + periodicDispatchTaskProvider.get(), + configuration.idlePeripheralRedispatchingInterval(), + configuration.idlePeripheralRedispatchingInterval(), + TimeUnit.MILLISECONDS + ); + + initialized = true; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + LOG.debug("Terminating..."); + + periodicDispatchTaskFuture.cancel(false); + periodicDispatchTaskFuture = null; + + eventSource.unsubscribe(implicitDispatchTrigger); + implicitDispatchTrigger = null; + + fullDispatchTask.terminate(); + + initialized = false; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void dispatch() { + LOG.debug("Scheduling dispatch task..."); + fullDispatchTask.run(); + } + + @Override + public void withdrawJob(Location location) { + requireNonNull(location, "location"); + checkState(isInitialized(), "Not initialized"); + + LOG.debug( + "Withdrawing peripheral job for location '{}' ({})...", + location.getName(), + location.getPeripheralInformation().getPeripheralJob() + ); + if (location.getPeripheralInformation().getPeripheralJob() == null) { + return; + } + + withdrawJob( + peripheralService.fetchObject( + PeripheralJob.class, + location.getPeripheralInformation().getPeripheralJob() + ) + ); + } + + @Override + public void withdrawJob(PeripheralJob job) { + requireNonNull(job, "job"); + checkState(isInitialized(), "Not initialized"); + if (job.getState().isFinalState()) { + LOG.info( + "Peripheral job '{}' already in final state '{}', skipping withdrawal.", + job.getName(), + job.getState() + ); + return; + } + checkArgument( + !isRelatedToNonFinalTransportOrder(job), + "Cannot withdraw job because it is related to transport order in non-final state: %s", + job.getName() + ); + + LOG.debug("Withdrawing peripheral job '{}'...", job.getName()); + + if (job.getState() == PeripheralJob.State.BEING_PROCESSED) { + controllerPool + .getPeripheralController(job.getPeripheralOperation().getLocation()) + .abortJob(); + } + + finalizeJob(job, PeripheralJob.State.FAILED); + } + + private boolean isRelatedToNonFinalTransportOrder(PeripheralJob job) { + return job.getRelatedTransportOrder() != null + && !peripheralService.fetchObject(TransportOrder.class, job.getRelatedTransportOrder()) + .getState().isFinalState(); + } + + @Override + public void peripheralJobFinished( + @Nonnull + TCSObjectReference<PeripheralJob> ref + ) { + requireNonNull(ref, "ref"); + + PeripheralJob job = peripheralJobService.fetchObject(PeripheralJob.class, ref); + if (job.getState() != PeripheralJob.State.BEING_PROCESSED) { + LOG.info( + "Peripheral job not in state BEING_PROCESSED, ignoring: {} ({})", + job.getName(), + job.getState() + ); + return; + } + + finalizeJob(job, PeripheralJob.State.FINISHED); + dispatch(); + } + + @Override + public void peripheralJobFailed( + @Nonnull + TCSObjectReference<PeripheralJob> ref + ) { + requireNonNull(ref, "ref"); + + PeripheralJob job = peripheralJobService.fetchObject(PeripheralJob.class, ref); + if (job.getState() != PeripheralJob.State.BEING_PROCESSED) { + LOG.info( + "Peripheral job not in state BEING_PROCESSED, ignoring: {} ({})", + job.getName(), + job.getState() + ); + return; + } + + finalizeJob(job, PeripheralJob.State.FAILED); + dispatch(); + } + + private void finalizeJob(PeripheralJob job, PeripheralJob.State state) { + if (job.getState() == PeripheralJob.State.BEING_PROCESSED) { + peripheralService.updatePeripheralProcState( + job.getPeripheralOperation().getLocation(), + PeripheralInformation.ProcState.IDLE + ); + peripheralService.updatePeripheralJob(job.getPeripheralOperation().getLocation(), null); + } + + peripheralJobService.updatePeripheralJobState(job.getReference(), state); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/DefaultPeripheralJobDispatcherConfiguration.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/DefaultPeripheralJobDispatcherConfiguration.java new file mode 100644 index 0000000..76ae332 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/DefaultPeripheralJobDispatcherConfiguration.java @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.peripherals.dispatching; + +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; + +/** + * Provides methods to configure the {@link DefaultPeripheralJobDispatcher} + */ +@ConfigurationPrefix(DefaultPeripheralJobDispatcherConfiguration.PREFIX) +public interface DefaultPeripheralJobDispatcherConfiguration { + + /** + * This configuration's prefix. + */ + String PREFIX = "defaultperipheraljobdispatcher"; + + @ConfigurationEntry( + type = "Integer", + description = "The interval between redispatching of peripheral devices.", + changesApplied = ConfigurationEntry.ChangesApplied.ON_NEW_PLANT_MODEL, + orderKey = "9_misc" + ) + long idlePeripheralRedispatchingInterval(); +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/DefaultPeripheralReleaseStrategy.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/DefaultPeripheralReleaseStrategy.java new file mode 100644 index 0000000..635e5df --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/DefaultPeripheralReleaseStrategy.java @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.peripherals.dispatching; + +import java.util.Collection; +import java.util.stream.Collectors; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.PeripheralInformation; + +/** + * The default implementation of {@link PeripheralReleaseStrategy}. + * Selects peripherals to be released by applying the following rules: + * <ul> + * <li>A peripheral's state must be {@link PeripheralInformation.State#IDLE}</li> + * <li>A peripheral's processing state must be {@link PeripheralInformation.ProcState#IDLE}</li> + * <li>A peripheral's reservation token must be set.</li> + * </ul> + */ +public class DefaultPeripheralReleaseStrategy + implements + PeripheralReleaseStrategy { + + /** + * Creates a new instance. + */ + public DefaultPeripheralReleaseStrategy() { + } + + @Override + public Collection<Location> selectPeripheralsToRelease(Collection<Location> locations) { + return locations.stream() + .filter(this::idleAndReserved) + .collect(Collectors.toSet()); + } + + private boolean idleAndReserved(Location location) { + return processesNoJob(location) && hasReservationToken(location); + } + + private boolean processesNoJob(Location location) { + return location.getPeripheralInformation() + .getProcState() == PeripheralInformation.ProcState.IDLE + && location.getPeripheralInformation().getState() == PeripheralInformation.State.IDLE; + } + + private boolean hasReservationToken(Location location) { + return location.getPeripheralInformation().getReservationToken() != null; + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/FullDispatchTask.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/FullDispatchTask.java new file mode 100644 index 0000000..f735415 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/FullDispatchTask.java @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.peripherals.dispatching; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.Lifecycle; +import org.opentcs.strategies.basic.peripherals.dispatching.phase.AssignFreePeripheralsPhase; +import org.opentcs.strategies.basic.peripherals.dispatching.phase.AssignReservedPeripheralsPhase; +import org.opentcs.strategies.basic.peripherals.dispatching.phase.FinishWithdrawalsPhase; +import org.opentcs.strategies.basic.peripherals.dispatching.phase.ReleasePeripheralsPhase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Performs a full dispatch run. + */ +public class FullDispatchTask + implements + Runnable, + Lifecycle { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(FullDispatchTask.class); + + private final FinishWithdrawalsPhase finishWithdrawalsPhase; + private final AssignReservedPeripheralsPhase assignReservedPeripheralsPhase; + private final ReleasePeripheralsPhase releasePeripheralsPhase; + private final AssignFreePeripheralsPhase assignFreePeripheralsPhase; + /** + * Indicates whether this component is enabled. + */ + private boolean initialized; + + @Inject + public FullDispatchTask( + FinishWithdrawalsPhase finishWithdrawalsPhase, + AssignReservedPeripheralsPhase assignReservedPeripheralsPhase, + ReleasePeripheralsPhase releasePeripheralsPhase, + AssignFreePeripheralsPhase assignFreePeripheralsPhase + ) { + this.finishWithdrawalsPhase = requireNonNull(finishWithdrawalsPhase, "finishWithdrawalsPhase"); + this.assignReservedPeripheralsPhase = requireNonNull( + assignReservedPeripheralsPhase, + "assignReservedPeripheralsPhase" + ); + this.releasePeripheralsPhase = requireNonNull( + releasePeripheralsPhase, + "releasePeripheralsPhase" + ); + this.assignFreePeripheralsPhase = requireNonNull( + assignFreePeripheralsPhase, + "assignFreePeripheralsPhase" + ); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + finishWithdrawalsPhase.initialize(); + assignReservedPeripheralsPhase.initialize(); + releasePeripheralsPhase.initialize(); + assignFreePeripheralsPhase.initialize(); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + assignFreePeripheralsPhase.terminate(); + releasePeripheralsPhase.terminate(); + assignReservedPeripheralsPhase.terminate(); + finishWithdrawalsPhase.terminate(); + + initialized = false; + } + + @Override + public void run() { + LOG.debug("Starting full dispatch run..."); + + finishWithdrawalsPhase.run(); + assignReservedPeripheralsPhase.run(); + releasePeripheralsPhase.run(); + assignFreePeripheralsPhase.run(); + + LOG.debug("Finished full dispatch run."); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/ImplicitDispatchTrigger.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/ImplicitDispatchTrigger.java new file mode 100644 index 0000000..b1283fe --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/ImplicitDispatchTrigger.java @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.peripherals.dispatching; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.PeripheralJobDispatcher; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An event listener that triggers the peripheral job dispatcher on certain events. + */ +public class ImplicitDispatchTrigger + implements + EventHandler { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ImplicitDispatchTrigger.class); + /** + * The dispatcher in use. + */ + private final PeripheralJobDispatcher dispatcher; + + /** + * Creates a new instance. + * + * @param dispatcher The dispatcher in use. + */ + @Inject + public ImplicitDispatchTrigger(PeripheralJobDispatcher dispatcher) { + this.dispatcher = requireNonNull(dispatcher, "dispatcher"); + } + + @Override + public void onEvent(Object event) { + if (!(event instanceof TCSObjectEvent)) { + return; + } + TCSObjectEvent objectEvent = (TCSObjectEvent) event; + if (objectEvent.getType() == TCSObjectEvent.Type.OBJECT_MODIFIED + && objectEvent.getCurrentOrPreviousObjectState() instanceof TransportOrder) { + checkTransportOrderChange( + (TransportOrder) objectEvent.getPreviousObjectState(), + (TransportOrder) objectEvent.getCurrentObjectState() + ); + } + } + + private void checkTransportOrderChange(TransportOrder oldOrder, TransportOrder newOrder) { + if (newOrder.getState() != oldOrder.getState() + && newOrder.getState() == TransportOrder.State.FAILED) { + LOG.debug("Dispatching for {}...", newOrder); + dispatcher.dispatch(); + } + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/JobSelectionStrategy.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/JobSelectionStrategy.java new file mode 100644 index 0000000..6cf2454 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/JobSelectionStrategy.java @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.peripherals.dispatching; + +import java.util.Collection; +import java.util.Optional; +import org.opentcs.data.model.Location; +import org.opentcs.data.peripherals.PeripheralJob; + +/** + * A strategy for selecting a peripheral job to be processed next. + */ +public interface JobSelectionStrategy { + + /** + * Selects a peripheral job to be processed next by the given location out of the given + * collection of peripheral jobs. + * + * @param jobs The peripheral jobs to select from. + * @param location The location to select a peripheral job for. + * @return The selected peripheral job. + */ + Optional<PeripheralJob> select(Collection<PeripheralJob> jobs, Location location); +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/PeriodicPeripheralRedispatchingTask.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/PeriodicPeripheralRedispatchingTask.java new file mode 100644 index 0000000..2055ca2 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/PeriodicPeripheralRedispatchingTask.java @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.peripherals.dispatching; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.services.PeripheralDispatcherService; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.PeripheralInformation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Periodically checks for idle peripheral devices that could process a peripheral job. + */ +public class PeriodicPeripheralRedispatchingTask + implements + Runnable { + + /** + * This class's Logger. + */ + private static final Logger LOG + = LoggerFactory.getLogger(PeriodicPeripheralRedispatchingTask.class); + + private final PeripheralDispatcherService dispatcherService; + + private final TCSObjectService objectService; + + /** + * Creates a new instance. + * + * @param dispatcherService The dispatcher service used to dispatch peripheral devices. + * @param objectService The object service. + */ + @Inject + public PeriodicPeripheralRedispatchingTask( + PeripheralDispatcherService dispatcherService, + TCSObjectService objectService + ) { + this.dispatcherService = requireNonNull(dispatcherService, "dispatcherService"); + this.objectService = requireNonNull(objectService, "objectService"); + } + + @Override + public void run() { + // If there are any peripheral devices that could process a peripheral job, + // trigger the dispatcher once. + objectService.fetchObjects(Location.class, this::couldProcessJob).stream() + .findAny() + .ifPresent(location -> { + LOG.debug( + "Peripheral {} could process peripheral job, triggering dispatcher ...", + location + ); + dispatcherService.dispatch(); + }); + } + + private boolean couldProcessJob(Location loc) { + return loc.getPeripheralInformation().getState() != PeripheralInformation.State.NO_PERIPHERAL + && processesNoJob(loc); + } + + private boolean processesNoJob(Location location) { + return location.getPeripheralInformation() + .getProcState() == PeripheralInformation.ProcState.IDLE + && location.getPeripheralInformation().getState() == PeripheralInformation.State.IDLE; + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/PeripheralDispatcherPhase.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/PeripheralDispatcherPhase.java new file mode 100644 index 0000000..c38f811 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/PeripheralDispatcherPhase.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.peripherals.dispatching; + +import org.opentcs.components.Lifecycle; + +/** + * Describes a reusable dispatching (sub-)task with a life cycle. + */ +public interface PeripheralDispatcherPhase + extends + Runnable, + Lifecycle { +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/PeripheralJobUtil.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/PeripheralJobUtil.java new file mode 100644 index 0000000..7d00f0e --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/PeripheralJobUtil.java @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.peripherals.dispatching; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.services.InternalPeripheralJobService; +import org.opentcs.components.kernel.services.InternalPeripheralService; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.PeripheralInformation; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.drivers.peripherals.PeripheralControllerPool; +import org.opentcs.drivers.peripherals.PeripheralJobCallback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides service functions for working with peripheral jobs and their states. + */ +public class PeripheralJobUtil { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PeripheralJobUtil.class); + /** + * The peripheral service to use. + */ + private final InternalPeripheralService peripheralService; + /** + * The peripheral job service to use. + */ + private final InternalPeripheralJobService peripheralJobService; + /** + * The peripheral controller pool. + */ + private final PeripheralControllerPool peripheralControllerPool; + /** + * The peripheral job callback to use. + */ + private final PeripheralJobCallback peripheralJobCallback; + + @Inject + public PeripheralJobUtil( + InternalPeripheralService peripheralService, + InternalPeripheralJobService peripheralJobService, + PeripheralControllerPool peripheralControllerPool, + PeripheralJobCallback peripheralJobCallback + ) { + this.peripheralService = requireNonNull(peripheralService, "peripheralService"); + this.peripheralJobService = requireNonNull(peripheralJobService, "peripheralJobService"); + this.peripheralControllerPool = requireNonNull( + peripheralControllerPool, + "peripheralControllerPool" + ); + this.peripheralJobCallback = requireNonNull(peripheralJobCallback, "peripheralJobCallback"); + } + + public void assignPeripheralJob(Location location, PeripheralJob peripheralJob) { + requireNonNull(location, "location"); + requireNonNull(peripheralJob, "peripheralJob"); + + LOG.debug("Assigning location {} to job {}.", location.getName(), peripheralJob.getName()); + final TCSResourceReference<Location> locationRef = location.getReference(); + final TCSObjectReference<PeripheralJob> jobRef = peripheralJob.getReference(); + // Set the locations's and peripheral job's state. + peripheralService.updatePeripheralProcState( + locationRef, + PeripheralInformation.ProcState.PROCESSING_JOB + ); + peripheralService.updatePeripheralReservationToken( + locationRef, + peripheralJob.getReservationToken() + ); + peripheralService.updatePeripheralJob(locationRef, jobRef); + peripheralJobService.updatePeripheralJobState(jobRef, PeripheralJob.State.BEING_PROCESSED); + + peripheralControllerPool.getPeripheralController(locationRef).process( + peripheralJobService.fetchObject(PeripheralJob.class, jobRef), + peripheralJobCallback + ); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/PeripheralReleaseStrategy.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/PeripheralReleaseStrategy.java new file mode 100644 index 0000000..f199b78 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/PeripheralReleaseStrategy.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.peripherals.dispatching; + +import java.util.Collection; +import org.opentcs.data.model.Location; + +/** + * A strategy that determines peripherals whose reservations are to be released. + */ +public interface PeripheralReleaseStrategy { + + /** + * Selects the peripherals whose reservations are to be released. + * + * @param locations The peripherals to select from. + * @return The selected peripherals. + */ + Collection<Location> selectPeripheralsToRelease(Collection<Location> locations); +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/phase/AssignFreePeripheralsPhase.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/phase/AssignFreePeripheralsPhase.java new file mode 100644 index 0000000..6743047 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/phase/AssignFreePeripheralsPhase.java @@ -0,0 +1,176 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.peripherals.dispatching.phase; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Collection; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.PeripheralInformation; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.drivers.peripherals.PeripheralControllerPool; +import org.opentcs.strategies.basic.peripherals.dispatching.JobSelectionStrategy; +import org.opentcs.strategies.basic.peripherals.dispatching.PeripheralDispatcherPhase; +import org.opentcs.strategies.basic.peripherals.dispatching.PeripheralJobUtil; +import org.opentcs.util.ExplainedBoolean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Assigns peripheral jobs to peripheral devices that are currently not processing any and are + * not reserved for any reservation token. + */ +public class AssignFreePeripheralsPhase + implements + PeripheralDispatcherPhase { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(AssignFreePeripheralsPhase.class); + /** + * The object service. + */ + private final TCSObjectService objectService; + /** + * The strategy to use for selecting jobs for peripheral devices. + */ + private final JobSelectionStrategy jobSelectionStrategy; + /** + * The peripheral controller pool. + */ + private final PeripheralControllerPool peripheralControllerPool; + /** + * Provides service functions for working with peripheral jobs and their states. + */ + private final PeripheralJobUtil peripheralJobUtil; + /** + * Indicates whether this component is initialized. + */ + private boolean initialized; + + @Inject + public AssignFreePeripheralsPhase( + TCSObjectService objectService, + JobSelectionStrategy jobSelectionStrategy, + PeripheralControllerPool peripheralControllerPool, + PeripheralJobUtil peripheralJobUtil + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.jobSelectionStrategy = requireNonNull(jobSelectionStrategy, "jobSelectionStrategy"); + this.peripheralControllerPool = requireNonNull( + peripheralControllerPool, + "peripheralControllerPool" + ); + this.peripheralJobUtil = requireNonNull(peripheralJobUtil, "peripheralJobUtil"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + initialized = false; + } + + @Override + public void run() { + Set<Location> availablePeripherals = objectService.fetchObjects( + Location.class, + this::availableForAnyJob + ); + if (availablePeripherals.isEmpty()) { + LOG.debug("No peripherals available, skipping potentially expensive fetching of jobs."); + return; + } + Set<PeripheralJob> jobsToBeProcessed = objectService.fetchObjects( + PeripheralJob.class, + this::toBeProcessed + ); + LOG.debug( + "Available for dispatching: {} peripheral jobs and {} peripheral devices.", + jobsToBeProcessed.size(), + availablePeripherals.size() + ); + + for (Location location : availablePeripherals) { + tryAssignJob(location, jobsToBeProcessed); + } + } + + private boolean availableForAnyJob(Location location) { + return processesNoJob(location) && !hasReservationToken(location); + } + + private boolean processesNoJob(Location location) { + return location.getPeripheralInformation() + .getProcState() == PeripheralInformation.ProcState.IDLE + && location.getPeripheralInformation().getState() == PeripheralInformation.State.IDLE; + } + + private boolean hasReservationToken(Location location) { + return location.getPeripheralInformation().getReservationToken() != null; + } + + private boolean toBeProcessed(PeripheralJob job) { + return job.getState() == PeripheralJob.State.TO_BE_PROCESSED; + } + + private void tryAssignJob(Location location, Collection<PeripheralJob> availableJobs) { + LOG.debug("Trying to find job for peripheral '{}'...", location.getName()); + jobSelectionStrategy + .select( + availableJobs.stream() + .filter(job -> matchesLocation(job, location)) + .collect(Collectors.toList()), + location + ) + .filter(job -> canProcess(location, job)) + .ifPresent(job -> assignJob(job, location)); + } + + private boolean matchesLocation(PeripheralJob job, Location location) { + return Objects.equals( + job.getPeripheralOperation().getLocation(), + location.getReference() + ); + } + + private boolean canProcess(Location location, PeripheralJob job) { + ExplainedBoolean canProcess + = peripheralControllerPool.getPeripheralController(location.getReference()).canProcess(job); + if (!canProcess.getValue()) { + LOG.debug( + "{} cannot process peripheral job {}: {}", + location.getName(), + job.getName(), + canProcess.getReason() + ); + } + + return canProcess.getValue(); + } + + private void assignJob(PeripheralJob job, Location location) { + LOG.debug("Assigning job '{}' to peripheral '{}'...", job.getName(), location.getName()); + peripheralJobUtil.assignPeripheralJob(location, job); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/phase/AssignReservedPeripheralsPhase.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/phase/AssignReservedPeripheralsPhase.java new file mode 100644 index 0000000..dce6b30 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/phase/AssignReservedPeripheralsPhase.java @@ -0,0 +1,162 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.peripherals.dispatching.phase; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Objects; +import java.util.Set; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.PeripheralInformation; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.drivers.peripherals.PeripheralControllerPool; +import org.opentcs.strategies.basic.peripherals.dispatching.PeripheralDispatcherPhase; +import org.opentcs.strategies.basic.peripherals.dispatching.PeripheralJobUtil; +import org.opentcs.util.Comparators; +import org.opentcs.util.ExplainedBoolean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Assigns the next peripheral job that matches a peripheral's reservation token to peripherals that + * are currently not processing any. + * Peripherals with no reservation token set are not cosidered in this phase. + */ +public class AssignReservedPeripheralsPhase + implements + PeripheralDispatcherPhase { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(AssignReservedPeripheralsPhase.class); + /** + * The object service. + */ + private final TCSObjectService objectService; + /** + * The peripheral controller pool. + */ + private final PeripheralControllerPool peripheralControllerPool; + /** + * Provides service functions for working with peripheral jobs and their states. + */ + private final PeripheralJobUtil peripheralJobUtil; + /** + * Indicates whether this component is initialized. + */ + private boolean initialized; + + @Inject + public AssignReservedPeripheralsPhase( + TCSObjectService objectService, + PeripheralControllerPool peripheralControllerPool, + PeripheralJobUtil peripheralJobUtil + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.peripheralControllerPool = requireNonNull( + peripheralControllerPool, + "peripheralControllerPool" + ); + this.peripheralJobUtil = requireNonNull(peripheralJobUtil, "peripheralJobUtil"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + initialized = false; + } + + @Override + public void run() { + Set<Location> availablePeripherals = objectService.fetchObjects( + Location.class, + this::reservedAndAvailable + ); + LOG.debug("Available for dispatching: {} peripheral devices.", availablePeripherals.size()); + for (Location location : availablePeripherals) { + checkForReservedJobs(location); + } + } + + private boolean reservedAndAvailable(Location location) { + return processesNoJob(location) && hasReservationToken(location); + } + + private boolean processesNoJob(Location location) { + return location.getPeripheralInformation() + .getProcState() == PeripheralInformation.ProcState.IDLE + && location.getPeripheralInformation().getState() == PeripheralInformation.State.IDLE; + } + + private boolean hasReservationToken(Location location) { + return location.getPeripheralInformation().getReservationToken() != null; + } + + private void checkForReservedJobs(Location location) { + LOG.debug("Trying to find job for peripheral '{}'...", location.getName()); + objectService.fetchObjects(PeripheralJob.class, this::toBeProcessed).stream() + .filter(job -> matchesReservationToken(job, location)) + .filter(job -> matchesLocation(job, location)) + .filter(job -> canProcess(location, job)) + .sorted(Comparators.jobsByAge()) + .findFirst() + .ifPresent(job -> assignJob(job, location)); + } + + private boolean toBeProcessed(PeripheralJob job) { + return job.getState() == PeripheralJob.State.TO_BE_PROCESSED; + } + + private boolean matchesReservationToken(PeripheralJob job, Location location) { + return Objects.equals( + job.getReservationToken(), + location.getPeripheralInformation().getReservationToken() + ); + } + + private boolean matchesLocation(PeripheralJob job, Location location) { + return Objects.equals( + job.getPeripheralOperation().getLocation(), + location.getReference() + ); + } + + private boolean canProcess(Location location, PeripheralJob job) { + ExplainedBoolean canProcess + = peripheralControllerPool.getPeripheralController(location.getReference()).canProcess(job); + if (!canProcess.getValue()) { + LOG.debug( + "{} cannot process peripheral job {}: {}", + location.getName(), + job.getName(), + canProcess.getReason() + ); + + } + + return canProcess.getValue(); + } + + private void assignJob(PeripheralJob job, Location location) { + LOG.debug("Assigning job '{}' to peripheral '{}'...", job.getName(), location.getName()); + peripheralJobUtil.assignPeripheralJob(location, job); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/phase/FinishWithdrawalsPhase.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/phase/FinishWithdrawalsPhase.java new file mode 100644 index 0000000..4984200 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/phase/FinishWithdrawalsPhase.java @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.peripherals.dispatching.phase; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.components.kernel.services.InternalPeripheralJobService; +import org.opentcs.components.kernel.services.InternalPeripheralService; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.PeripheralInformation; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.data.peripherals.PeripheralJob; +import org.opentcs.drivers.peripherals.PeripheralControllerPool; +import org.opentcs.strategies.basic.peripherals.dispatching.PeripheralDispatcherPhase; + +/** + * Finishes withdrawals of peripheral jobs after their related transport order has failed. + */ +public class FinishWithdrawalsPhase + implements + PeripheralDispatcherPhase { + + /** + * The object service. + */ + private final TCSObjectService objectService; + + private final InternalPeripheralService peripheralService; + + private final InternalPeripheralJobService peripheralJobService; + /** + * The controller pool. + */ + private final PeripheralControllerPool controllerPool; + /** + * Indicates whether this component is initialized. + */ + private boolean initialized; + + @Inject + public FinishWithdrawalsPhase( + @Nonnull + TCSObjectService objectService, + @Nonnull + InternalPeripheralService peripheralService, + @Nonnull + InternalPeripheralJobService peripheralJobService, + @Nonnull + PeripheralControllerPool controllerPool + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.peripheralService = requireNonNull(peripheralService, "peripheralService"); + this.peripheralJobService = requireNonNull(peripheralJobService, "peripheralJobService"); + this.controllerPool = requireNonNull(controllerPool, "controllerPool"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + initialized = false; + } + + @Override + public void run() { + // Get all non-final peripheral jobs that are related to a transport order, and if their + // transport order is marked as FAILED, abort them. + Set<PeripheralJob> jobs + = objectService.fetchObjects( + PeripheralJob.class, + this::isRelatedToTransportOrderAndNotInFinalState + ); + + Set<TCSObjectReference<TransportOrder>> failedOrderRefs + = jobs.stream() + .map(PeripheralJob::getRelatedTransportOrder) + .distinct() + .map(orderRef -> objectService.fetchObject(TransportOrder.class, orderRef)) + .filter(order -> order.hasState(TransportOrder.State.FAILED)) + .map(TransportOrder::getReference) + .collect(Collectors.toSet()); + + jobs.stream() + .filter(job -> failedOrderRefs.contains(job.getRelatedTransportOrder())) + .forEach(job -> abortJob(job)); + } + + private boolean isRelatedToTransportOrderAndNotInFinalState(PeripheralJob job) { + return job.getRelatedTransportOrder() != null && !job.getState().isFinalState(); + } + + private void abortJob(PeripheralJob job) { + if (job.getState() == PeripheralJob.State.BEING_PROCESSED) { + controllerPool.getPeripheralController(job.getPeripheralOperation().getLocation()).abortJob(); + peripheralService.updatePeripheralProcState( + job.getPeripheralOperation().getLocation(), + PeripheralInformation.ProcState.IDLE + ); + peripheralService.updatePeripheralJob(job.getPeripheralOperation().getLocation(), null); + } + + peripheralJobService.updatePeripheralJobState(job.getReference(), PeripheralJob.State.FAILED); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/phase/ReleasePeripheralsPhase.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/phase/ReleasePeripheralsPhase.java new file mode 100644 index 0000000..972cde7 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/peripherals/dispatching/phase/ReleasePeripheralsPhase.java @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.peripherals.dispatching.phase; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Collection; +import org.opentcs.components.kernel.services.InternalPeripheralService; +import org.opentcs.data.model.Location; +import org.opentcs.strategies.basic.peripherals.dispatching.PeripheralDispatcherPhase; +import org.opentcs.strategies.basic.peripherals.dispatching.PeripheralReleaseStrategy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Releases the reservations of peripherals. + */ +public class ReleasePeripheralsPhase + implements + PeripheralDispatcherPhase { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ReleasePeripheralsPhase.class); + /** + * The peripheral service. + */ + private final InternalPeripheralService peripheralService; + /** + * The release strategy to use. + */ + private final PeripheralReleaseStrategy releaseStrategy; + /** + * Indicates whether this component is initialized. + */ + private boolean initialized; + + @Inject + public ReleasePeripheralsPhase( + InternalPeripheralService peripheralService, + PeripheralReleaseStrategy releaseStrategy + ) { + this.peripheralService = requireNonNull(peripheralService, "peripheralService"); + this.releaseStrategy = requireNonNull(releaseStrategy, "releaseStrategy"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + initialized = false; + } + + @Override + public void run() { + Collection<Location> peripheralsToBeRelease + = releaseStrategy.selectPeripheralsToRelease( + peripheralService.fetchObjects(Location.class) + ); + for (Location location : peripheralsToBeRelease) { + releasePeripheral(location); + } + } + + private void releasePeripheral(Location location) { + LOG.debug("Releasing peripheral '{}'...", location.getName()); + peripheralService.updatePeripheralReservationToken(location.getReference(), null); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/DefaultRouter.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/DefaultRouter.java new file mode 100644 index 0000000..a8a7e53 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/DefaultRouter.java @@ -0,0 +1,551 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.strategies.basic.routing.PointRouter.INFINITE_COSTS; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.routing.GroupMapper; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.DriveOrder.Destination; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.routing.jgrapht.PointRouterProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A basic {@link Router} implementation. + */ +public class DefaultRouter + implements + Router { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(DefaultRouter.class); + /** + * This class's configuration. + */ + private final DefaultRouterConfiguration configuration; + /** + * The object service providing the model data. + */ + private final TCSObjectService objectService; + /** + * Provides point routers for vehicles. + */ + private final PointRouterProvider pointRouterProvider; + /** + * Used to map vehicles to their routing groups. + */ + private final GroupMapper routingGroupMapper; + /** + * The routes selected for each vehicle. + */ + private final Map<Vehicle, List<DriveOrder>> routesByVehicle = new ConcurrentHashMap<>(); + /** + * Indicates whether this component is enabled. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param objectService The object service providing the model data. + * @param pointRouterProvider Provides point routers for vehicles. + * @param routingGroupMapper Used to map vehicles to their routing groups. + * @param configuration This class's configuration. + */ + @Inject + public DefaultRouter( + TCSObjectService objectService, + PointRouterProvider pointRouterProvider, + GroupMapper routingGroupMapper, + DefaultRouterConfiguration configuration + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.pointRouterProvider = requireNonNull(pointRouterProvider, "pointRouterProvider"); + this.routingGroupMapper = requireNonNull(routingGroupMapper, "routingGroupMapper"); + this.configuration = requireNonNull(configuration, "configuration"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + synchronized (this) { + routesByVehicle.clear(); + pointRouterProvider.invalidate(); + initialized = true; + } + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + synchronized (this) { + routesByVehicle.clear(); + pointRouterProvider.invalidate(); + initialized = false; + } + } + + @Override + public void updateRoutingTopology(Set<Path> paths) { + requireNonNull(paths, "paths"); + + synchronized (this) { + pointRouterProvider.updateRoutingTopology(paths); + } + } + + @Override + public Set<Vehicle> checkRoutability(TransportOrder order) { + requireNonNull(order, "order"); + + synchronized (this) { + Set<Vehicle> result = new HashSet<>(); + List<DriveOrder> driveOrderList = order.getFutureDriveOrders(); + DriveOrder[] driveOrders + = driveOrderList.toArray(new DriveOrder[driveOrderList.size()]); + + for (Map.Entry<String, PointRouter> curEntry : pointRouterProvider + .getPointRoutersByVehicleGroup().entrySet()) { + // Get all points at the first location at which a vehicle of the current + // type can execute the desired operation and check if an acceptable route + // originating in one of them exists. + for (Point curStartPoint : getDestinationPoints(driveOrders[0])) { + if (isRoutable(curStartPoint, driveOrders, 1, curEntry.getValue())) { + result.addAll(getVehiclesByRoutingGroup(curEntry.getKey())); + break; + } + } + } + return result; + } + } + + @Override + public boolean checkGeneralRoutability(TransportOrder order) { + requireNonNull(order, "order"); + + synchronized (this) { + List<DriveOrder> driveOrderList = order.getFutureDriveOrders(); + DriveOrder[] driveOrders + = driveOrderList.toArray(new DriveOrder[driveOrderList.size()]); + + PointRouter generalPointRouter = pointRouterProvider.getGeneralPointRouter(order); + + for (Point curStartPoint : getDestinationPoints(driveOrders[0])) { + if (!isRoutable(curStartPoint, driveOrders, 1, generalPointRouter)) { + return false; + } + } + return true; + } + } + + @Override + public Optional<List<DriveOrder>> getRoute( + Vehicle vehicle, + Point sourcePoint, + TransportOrder transportOrder + ) { + requireNonNull(vehicle, "vehicle"); + requireNonNull(sourcePoint, "sourcePoint"); + requireNonNull(transportOrder, "transportOrder"); + + synchronized (this) { + List<DriveOrder> driveOrderList = transportOrder.getFutureDriveOrders(); + DriveOrder[] driveOrders = driveOrderList.toArray(new DriveOrder[driveOrderList.size()]); + PointRouter pointRouter = pointRouterProvider.getPointRouterForVehicle( + vehicle, + transportOrder + ); + OrderRouteParameterStruct params = new OrderRouteParameterStruct(driveOrders, pointRouter); + OrderRouteResultStruct resultStruct = new OrderRouteResultStruct(driveOrderList.size()); + computeCheapestOrderRoute(sourcePoint, params, 0, resultStruct); + return (resultStruct.bestCosts == Long.MAX_VALUE) + ? Optional.empty() + : Optional.of(Arrays.asList(resultStruct.bestRoute)); + } + } + + @Override + public Optional<Route> getRoute( + Vehicle vehicle, + Point sourcePoint, + Point destinationPoint, + Set<TCSResourceReference<?>> resourcesToAvoid + ) { + requireNonNull(vehicle, "vehicle"); + requireNonNull(sourcePoint, "sourcePoint"); + requireNonNull(destinationPoint, "destinationPoint"); + requireNonNull(resourcesToAvoid, "resourcesToAvoid"); + + synchronized (this) { + PointRouter pointRouter = pointRouterProvider + .getPointRouterForVehicle(vehicle, resourcesToAvoid); + long costs = pointRouter.getCosts(sourcePoint, destinationPoint); + if (costs == INFINITE_COSTS) { + return Optional.empty(); + } + List<Route.Step> steps = pointRouter.getRouteSteps(sourcePoint, destinationPoint); + if (steps.isEmpty()) { + // If the list of steps is empty, we're already at the destination point + // Create a single step without a path. + steps.add(new Route.Step(null, null, sourcePoint, Vehicle.Orientation.UNDEFINED, 0)); + } + return Optional.of(new Route(steps, costs)); + } + } + + @Override + public long getCosts( + Vehicle vehicle, + Point sourcePoint, + Point destinationPoint, + Set<TCSResourceReference<?>> resourcesToAvoid + ) { + requireNonNull(vehicle, "vehicle"); + requireNonNull(sourcePoint, "sourcePoint"); + requireNonNull(destinationPoint, "destinationPoint"); + requireNonNull(resourcesToAvoid, "resourcesToAvoid"); + + synchronized (this) { + return pointRouterProvider + .getPointRouterForVehicle(vehicle, resourcesToAvoid) + .getCosts(sourcePoint, destinationPoint); + } + } + + @Override + public void selectRoute(Vehicle vehicle, List<DriveOrder> driveOrders) { + requireNonNull(vehicle, "vehicle"); + + synchronized (this) { + if (driveOrders == null) { + // XXX Should we remember the vehicle's current position, maybe? + routesByVehicle.remove(vehicle); + } + else { + routesByVehicle.put(vehicle, driveOrders); + } + } + } + + @Override + public Map<Vehicle, List<DriveOrder>> getSelectedRoutes() { + synchronized (this) { + return new HashMap<>(routesByVehicle); + } + } + + @Override + public Set<Point> getTargetedPoints() { + synchronized (this) { + Set<Point> result = new HashSet<>(); + for (List<DriveOrder> curOrderList : routesByVehicle.values()) { + DriveOrder finalOrder = curOrderList.get(curOrderList.size() - 1); + result.add(finalOrder.getRoute().getFinalDestinationPoint()); + } + return result; + } + } + + /** + * Checks if a route exists for a vehicle of a given type which allows the + * vehicle to process a given list of drive orders. + * + * @param startPoint The point at which the route is supposed to start. + * @param driveOrders The list of drive orders, in the order they are to be + * processed. + * @param nextHopIndex The index of the next drive order in the list. + * @param pointRouter The point router to use. + * @return <code>true</code> if, and only if, at least one route exists which + * would allow a vehicle of the given type to process the whole list of drive + * orders. + */ + private boolean isRoutable( + Point startPoint, + DriveOrder[] driveOrders, + int nextHopIndex, + PointRouter pointRouter + ) { + assert startPoint != null; + assert driveOrders != null; + assert pointRouter != null; + + if (nextHopIndex < driveOrders.length) { + for (Point curPoint : getDestinationPoints(driveOrders[nextHopIndex])) { + // Check if there is a route from the starting point to the current + // point and if the rest of the orders are routable from there, too. + if (pointRouter.getCosts(startPoint, curPoint) != INFINITE_COSTS + && isRoutable(curPoint, driveOrders, nextHopIndex + 1, pointRouter)) { + // If it was possible to reach the end of the order list from here, + // propagate the result back to the caller. + return true; + } + } + // If we haven't found an acceptable route, return false. + return false; + } + // If we have reached the end of the list, it seems we have found a route. + else { + return true; + } + } + + /** + * Compute the cheapest route along a list of drive orders/checkpoints. + * + * @param startPoint The current checkpoint which to start at. + * @param params A struct describing parameters for the route to be computed. + * @param hopIndex The current index in the list of drive orders/checkpoints. + * @param result A struct for keeping the (partial) result in. + */ + private void computeCheapestOrderRoute( + Point startPoint, + OrderRouteParameterStruct params, + int hopIndex, + OrderRouteResultStruct result + ) { + assert startPoint != null; + assert params != null; + assert result != null; + // If we haven't reached the final drive order in the list, yet... + if (hopIndex < params.driveOrders.length) { + // ...try every possible destination point of the current drive order as + // the next checkpoint and recursively route from there. + final long currentRouteCosts = result.currentCosts; + Set<Point> destPoints = getDestinationPoints(params.driveOrders[hopIndex]); + // If the set of destination points contains the starting point, keep only + // that one. This is just a shortcut - it is the cheapest way to go. + if (!configuration.routeToCurrentPosition() && destPoints.contains(startPoint)) { + LOG.debug("Shortcutting route to {}", startPoint); + destPoints.clear(); + destPoints.add(startPoint); + } + boolean routable = false; + for (Point curDestPoint : destPoints) { + final long hopCosts = params.pointRouter.getCosts(startPoint, curDestPoint); + if (hopCosts == INFINITE_COSTS) { + continue; + } + // Get the list of steps for the route of the current drive order. + List<Route.Step> steps = params.pointRouter.getRouteSteps(startPoint, curDestPoint); + if (steps.isEmpty()) { + // If the list of steps returned is empty, we're already at the + // destination point of the drive order - create a single step + // without a path. + steps = new ArrayList<>(1); + steps.add( + new Route.Step( + null, + null, + startPoint, + Vehicle.Orientation.UNDEFINED, + 0 + ) + ); + } + // Create a route from the list of steps gathered. + Route hopRoute = new Route(steps, hopCosts); + // Copy the current drive order, add the computed route to it and + // place it in the result struct. + DriveOrder hopOrder = params.driveOrders[hopIndex].withRoute(hopRoute); + result.currentRoute[hopIndex] = hopOrder; + // Calculate the costs for the route so far, too. + result.currentCosts = currentRouteCosts + hopRoute.getCosts(); + computeCheapestOrderRoute(curDestPoint, params, hopIndex + 1, result); + // Remember that we did find at least one route that works. + routable = true; + } + if (!routable) { + // Setting currentCosts is not strictly necessary for this algorithm, + // but might help with debugging. + result.currentCosts = Long.MAX_VALUE; + } + } + // If we have reached the final drive order, ... + else // If the route computed is cheaper than the best route found so far, + // replace the latter. + if (result.currentCosts < result.bestCosts) { + System.arraycopy(result.currentRoute, 0, result.bestRoute, 0, result.currentRoute.length); + result.bestCosts = result.currentCosts; + } + } + + /** + * Returns all points at which a vehicle could process the given drive order. + * + * @param driveOrder The drive order to be processed. + * @return A set of acceptable destination points at which a vehicle could + * execute the given drive order's operation. If no such points exist, the + * returned set will be empty. + */ + private Set<Point> getDestinationPoints(DriveOrder driveOrder) { + assert driveOrder != null; + + final DriveOrder.Destination dest = driveOrder.getDestination(); + // If the destination references a point and the operation is "just move" or + // "park the vehicle", this is an order to send the vehicle to an explicitly + // selected point - return an appropriate set with only that point. + if (dest.getDestination().getReferentClass() == Point.class + && (Destination.OP_MOVE.equals(dest.getOperation()) + || Destination.OP_PARK.equals(dest.getOperation()))) { + // Route the vehicle to an user selected point if halting is allowed there. + Point destPoint = objectService.fetchObject(Point.class, dest.getDestination().getName()); + requireNonNull(destPoint, "destPoint"); + final Set<Point> result = new HashSet<>(); + result.add(destPoint); + return result; + } + // If it's a "normal" transport order, look for destination points adjacent + // to the destination location. + else if (dest.getDestination().getReferentClass() == Location.class) { + final Set<Point> result = new HashSet<>(); + final Location destLoc = objectService.fetchObject( + Location.class, + dest.getDestination().getName() + ); + final LocationType destLocType = objectService.fetchObject( + LocationType.class, + destLoc.getType() + ); + for (Location.Link curLink : destLoc.getAttachedLinks()) { + // A link is acceptable if any of the following conditions are true: + // - The destination operation is OP_NOP, which is allowed everywhere. + // - The destination operation is explicitly allowed with the link. + // - The link's set of allowed operations is empty and the destination + // operation is explicitly allowed with the location's type. + // Furthermore, the point to be routed at must allow halting. + if (Destination.OP_NOP.equals(dest.getOperation()) + || curLink.hasAllowedOperation(dest.getOperation()) + || (curLink.getAllowedOperations().isEmpty() + && destLocType.isAllowedOperation(dest.getOperation()))) { + Point destPoint = objectService.fetchObject(Point.class, curLink.getPoint()); + result.add(destPoint); + } + } + return result; + } + else { + return new HashSet<>(); + } + } + + /** + * Returns all vehicles within the given routing group. + * + * @param routingGroup The routing group the returned vehicles should belong to. + * @return The vehicles which have the given routing group + */ + private Set<Vehicle> getVehiclesByRoutingGroup(String routingGroup) { + Set<Vehicle> result = new HashSet<>(); + for (Vehicle curVehicle : objectService.fetchObjects(Vehicle.class)) { + if (Objects.equals(routingGroupMapper.apply(curVehicle), routingGroup)) { + result.add(curVehicle); + } + } + return result; + } + + /** + * Contains parameters for a route to be computed. + */ + private static final class OrderRouteParameterStruct { + + /** + * The drive orders containing the route's checkpoints. + */ + private final DriveOrder[] driveOrders; + /** + * The point router for the vehicle type. + */ + private final PointRouter pointRouter; + + /** + * Creates a new OrderRouteParameterStruct. + * + * @param driveOrders A list of drive orders to be processed as checkpoints + * of the route to be computed. + * @param pointRouter The point router for the vehicle type. + */ + OrderRouteParameterStruct( + DriveOrder[] driveOrders, + PointRouter pointRouter + ) { + this.driveOrders = requireNonNull(driveOrders, "driveOrders"); + this.pointRouter = requireNonNull(pointRouter, "pointRouter"); + } + } + + /** + * A struct supporting cheapest route calculation. + */ + private static final class OrderRouteResultStruct { + + /** + * The (possibly partial) route currently being examined. + */ + private DriveOrder[] currentRoute; + /** + * The costs of the route currently being examined. + */ + private long currentCosts; + /** + * The best route found so far. + */ + private DriveOrder[] bestRoute; + /** + * The costs of the best route found so far. + */ + private long bestCosts; + + /** + * Creates a new OrderRouteResultStruct. + * + * @param driveOrderCount The number of <code>DriveOrder</code>s in the + * <code>TransportOrder</code> for which this struct is to store the + * routing result. + */ + OrderRouteResultStruct(int driveOrderCount) { + currentRoute = new DriveOrder[driveOrderCount]; + currentCosts = 0; + bestRoute = new DriveOrder[driveOrderCount]; + bestCosts = Long.MAX_VALUE; + } + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/DefaultRouterConfiguration.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/DefaultRouterConfiguration.java new file mode 100644 index 0000000..36d96f3 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/DefaultRouterConfiguration.java @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing; + +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; + +/** + * Provides methods to configure the {@link DefaultRouter}. + */ +@ConfigurationPrefix(DefaultRouterConfiguration.PREFIX) +public interface DefaultRouterConfiguration { + + /** + * This configuration's prefix. + */ + String PREFIX = "defaultrouter"; + + @ConfigurationEntry( + type = "Boolean", + description = "Whether to compute a route even if the vehicle is already at the destination.", + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY + ) + boolean routeToCurrentPosition(); + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/DefaultRoutingGroupMapper.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/DefaultRoutingGroupMapper.java new file mode 100644 index 0000000..5d4a0d0 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/DefaultRoutingGroupMapper.java @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing; + +import static org.opentcs.components.kernel.Router.PROPKEY_ROUTING_GROUP; + +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.routing.GroupMapper; +import org.opentcs.data.model.Vehicle; + +/** + * Determines a vehicle's routing group by reading it's {@link Router#PROPKEY_ROUTING_GROUP} + * property. Returns {@link #DEFAULT_ROUTING_GROUP} if the property does not exist or is invalid. + */ +public class DefaultRoutingGroupMapper + implements + GroupMapper { + + /** + * The default value of a vehicle's routing group. + */ + private static final String DEFAULT_ROUTING_GROUP = ""; + + /** + * Creates a new instance. + */ + public DefaultRoutingGroupMapper() { + } + + @Override + public String apply(Vehicle vehicle) { + String propVal = vehicle.getProperty(PROPKEY_ROUTING_GROUP); + + return propVal == null ? DEFAULT_ROUTING_GROUP : propVal; + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/PointRouter.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/PointRouter.java new file mode 100644 index 0000000..ad23ac2 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/PointRouter.java @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Point; +import org.opentcs.data.order.Route; + +/** + * Computes routes between points. + */ +public interface PointRouter { + + /** + * A constant for marking the costs for a route as infinite. + */ + long INFINITE_COSTS = Long.MAX_VALUE; + + /** + * Returns a list of route steps to travel from a given source point to a given destination point. + * + * @param srcPoint The source point. + * @param destPoint The destination point. + * @return A list of steps in the order they are to be travelled from the source point to the + * destination point. + * The returned list does not include a step for the source point. + * If source point and destination point are identical, the returned list will be empty. + * If no route exists, <code>null</code> will be returned. + */ + List<Route.Step> getRouteSteps(Point srcPoint, Point destPoint); + + /** + * Returns the costs for travelling the shortest route from one point to another. + * + * @param srcPointRef The starting point reference. + * @param destPointRef The destination point reference. + * @return The costs for travelling the shortest route from the starting point to the destination + * point. + * If no route exists, {@link #INFINITE_COSTS INFINITE_COSTS} will be returned. + */ + long getCosts( + TCSObjectReference<Point> srcPointRef, + TCSObjectReference<Point> destPointRef + ); + + /** + * Returns the costs for travelling the shortest route from one point to another. + * + * @param srcPoint The starting point. + * @param destPoint The destination point. + * @return The costs for travelling the shortest route from the starting point to the destination + * point. + * If no route exists, {@link #INFINITE_COSTS INFINITE_COSTS} will be returned. + */ + default long getCosts(Point srcPoint, Point destPoint) { + requireNonNull(srcPoint, "srcPoint"); + requireNonNull(destPoint, "destPoint"); + + return getCosts(srcPoint.getReference(), destPoint.getReference()); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/PointRouterFactory.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/PointRouterFactory.java new file mode 100644 index 0000000..12a4328 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/PointRouterFactory.java @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing; + +import java.util.Set; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; + +/** + * Implementations of this interface construct point routers. + */ +public interface PointRouterFactory { + + /** + * Creates a point router for the given vehicle while excluding the given set of points and paths + * from it. + * + * @param vehicle The vehicle. + * @param pointsToExclude The set of points to be excluded. + * @param pathsToExclude The set of paths to be excluded. + * @return The point router. + */ + PointRouter createPointRouter( + Vehicle vehicle, + Set<Point> pointsToExclude, + Set<Path> pathsToExclude + ); + + /** + * Creates a general point router while excluding the given set of points and paths from it. + * <p> + * In contrast to point routers that are created via + * {@link #createPointRouter(Vehicle, Set, Set)}, a general point router is not affected by any + * path properties or any configured edge evaluators. This means, for example, that a general + * point router <em>always</em> considers paths regardless of whether the path is locked or not. + * (Unless, of course, it is contained in the set of paths to be excluded.) + * </p> + * + * @param pointsToExclude The set of points to be excluded. + * @param pathsToExclude The set of paths to be excluded. + * @return The point router. + */ + PointRouter createGeneralPointRouter( + Set<Point> pointsToExclude, + Set<Path> pathsToExclude + ); +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/ResourceAvoidanceExtractor.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/ResourceAvoidanceExtractor.java new file mode 100644 index 0000000..5ff8b8e --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/ResourceAvoidanceExtractor.java @@ -0,0 +1,212 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.ObjectPropConstants; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.order.TransportOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides methods for extracting {@link Point}s, {@link Path}s and {@link Location}s to be + * avoided by vehicles processing a {@link TransportOrder} where the + * {@link ObjectPropConstants#TRANSPORT_ORDER_RESOURCES_TO_AVOID} property is set. + */ +public class ResourceAvoidanceExtractor { + + private static final Logger LOG = LoggerFactory.getLogger(ResourceAvoidanceExtractor.class); + private final TCSObjectService objectService; + + /** + * Creates a new instance. + * + * @param objectService The objects service to be used. + */ + @Inject + public ResourceAvoidanceExtractor(TCSObjectService objectService) { + this.objectService = requireNonNull(objectService, "objectService"); + } + + /** + * Extracts resources that are referenced in the + * {@link ObjectPropConstants#TRANSPORT_ORDER_RESOURCES_TO_AVOID} property of the given + * {@link TransportOrder}. + * <p> + * The extraction result will contain {@link Point}s and {@link Path}s that are referenced by + * their name in the property's value and also points that are linked to a {@link Location}, + * whereby the name of the location is referenced in the property's value. + * </p> + * + * @param order The transport order. + * @return The extracted resources. + */ + @Nonnull + public ResourcesToAvoid extractResourcesToAvoid( + @Nullable + TransportOrder order + ) { + if (order == null) { + return ResourcesToAvoid.EMPTY; + } + + String resourcesToAvoidString + = order.getProperty(ObjectPropConstants.TRANSPORT_ORDER_RESOURCES_TO_AVOID); + if (resourcesToAvoidString == null) { + return ResourcesToAvoid.EMPTY; + } + + Set<Point> pointsToAvoid = new HashSet<>(); + Set<Path> pathsToAvoid = new HashSet<>(); + Set<String> resourcesToAvoidByName = Set.of(resourcesToAvoidString.split(",")); + for (String resourceToAvoid : resourcesToAvoidByName) { + Point point = objectService.fetchObject(Point.class, resourceToAvoid); + if (point != null) { + pointsToAvoid.add(point); + continue; + } + + Path path = objectService.fetchObject(Path.class, resourceToAvoid); + if (path != null) { + pathsToAvoid.add(path); + continue; + } + + Location location = objectService.fetchObject(Location.class, resourceToAvoid); + if (location != null) { + for (Location.Link link : location.getAttachedLinks()) { + pointsToAvoid.add(objectService.fetchObject(Point.class, link.getPoint())); + } + continue; + } + + LOG.debug("Ignoring resource '{}' which is not a point, path or location.", resourceToAvoid); + } + + return new ResourcesToAvoid(pointsToAvoid, pathsToAvoid); + } + + /** + * Extracts resources in the given set of references. + * <p> + * The extraction result will contain {@link Point}s and {@link Path}s referenced in the given + * set, and also points that are linked to {@link Location}s referenced in the given set. + * </p> + * + * @param resourcesToAvoid The set of references. + * @return The extracted resources. + */ + @Nonnull + public ResourcesToAvoid extractResourcesToAvoid(Set<TCSResourceReference<?>> resourcesToAvoid) { + requireNonNull(resourcesToAvoid, "resourcesToAvoid"); + + if (resourcesToAvoid.isEmpty()) { + return ResourcesToAvoid.EMPTY; + } + + Set<Point> pointsToAvoid = new HashSet<>(); + Set<Path> pathsToAvoid = new HashSet<>(); + + for (TCSResourceReference<?> resourceToAvoid : resourcesToAvoid) { + Point point = objectService.fetchObject(Point.class, resourceToAvoid.getName()); + if (point != null) { + pointsToAvoid.add(point); + continue; + } + + Path path = objectService.fetchObject(Path.class, resourceToAvoid.getName()); + if (path != null) { + pathsToAvoid.add(path); + continue; + } + + Location location = objectService.fetchObject(Location.class, resourceToAvoid.getName()); + if (location != null) { + for (Location.Link link : location.getAttachedLinks()) { + pointsToAvoid.add(objectService.fetchObject(Point.class, link.getPoint())); + } + continue; + } + + LOG.debug("Ignoring resource '{}' which is not a point, path or location.", resourceToAvoid); + } + + return new ResourcesToAvoid(pointsToAvoid, pathsToAvoid); + } + + /** + * A wrapper for resources to be avoided. + */ + public static class ResourcesToAvoid { + + /** + * An instance representing no resources to be avoided. + */ + public static final ResourcesToAvoid EMPTY = new ResourcesToAvoid(Set.of(), Set.of()); + private final Set<Point> points; + private final Set<Path> paths; + + /** + * Creates a new instance. + * + * @param points The set of points to be avoided. + * @param paths The set of paths to be avoided. + */ + private ResourcesToAvoid(Set<Point> points, Set<Path> paths) { + this.points = requireNonNull(points, "points"); + this.paths = requireNonNull(paths, "paths"); + } + + /** + * Returns the set of points to be avoided. + * + * @return The set of points to be avoided. + */ + public Set<Point> getPoints() { + return points; + } + + /** + * Returns the set of paths to avoid. + * + * @return The set of paths to avoid. + */ + public Set<Path> getPaths() { + return paths; + } + + /** + * Checks whether there are any resources to be avoided. + * + * @return {@code true}, if there are any resources to be avoided, otherwise {@code false}. + */ + public boolean isEmpty() { + return points.isEmpty() && paths.isEmpty(); + } + + /** + * Transforms the sets of paths and points to avoid to a single set of TCSResourceReferences. + * + * @return A set of TCSResourceReferences referencing the points and paths to avoid. + */ + public Set<TCSResourceReference<?>> toResourceReferenceSet() { + return Stream.concat( + paths.stream().map(path -> path.getReference()), + points.stream().map(point -> point.getReference()) + ).collect(Collectors.toSet()); + } + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/BoundingBoxProtrusionCheck.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/BoundingBoxProtrusionCheck.java new file mode 100644 index 0000000..a2e0889 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/BoundingBoxProtrusionCheck.java @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.edgeevaluator; + +import org.opentcs.data.model.BoundingBox; + +/** + * Provides a method for checking whether one bounding box protrudes beyond another one. + */ +public class BoundingBoxProtrusionCheck { + + public BoundingBoxProtrusionCheck() { + } + + /** + * Checks whether one (inner) bounding box protrudes beyond another (outer) one. + * + * @param inner The inner bounding box. + * @param outer The outer bounding box. + * @return The result of the check, indicating where and how much the inner bounding box protrudes + * beyond the outer one. + */ + public BoundingBoxProtrusion checkProtrusion(BoundingBox inner, BoundingBox outer) { + return new BoundingBoxProtrusion( + (inner.getLength() / 2.0 - inner.getReferenceOffset().getX()) - + (outer.getLength() / 2.0 - outer.getReferenceOffset().getX()), + (inner.getLength() / 2.0 + inner.getReferenceOffset().getX()) - + (outer.getLength() / 2.0 + outer.getReferenceOffset().getX()), + (inner.getWidth() / 2.0 - inner.getReferenceOffset().getY()) - + (outer.getWidth() / 2.0 - outer.getReferenceOffset().getY()), + (inner.getWidth() / 2.0 + inner.getReferenceOffset().getY()) - + (outer.getWidth() / 2.0 + outer.getReferenceOffset().getY()), + inner.getHeight() - outer.getHeight() + ); + } + + /** + * Describes where and how much an inner bounding box protrudes beyond an outer one. + */ + public static class BoundingBoxProtrusion { + + private final double front; + private final double back; + private final double left; + private final double right; + private final double top; + + /** + * Creates a new instance. + * + * @param front The protrusion from the front. + * @param back The protrusion from the back. + * @param left The protrusion from the left. + * @param right The protrusion from the right. + * @param top The protrusion from the top. + */ + public BoundingBoxProtrusion(double front, double back, double left, double right, double top) { + this.front = Math.max(0, front); + this.back = Math.max(0, back); + this.left = Math.max(0, left); + this.right = Math.max(0, right); + this.top = Math.max(0, top); + } + + /** + * Indicates whether there is a protrusion from the front. + * + * @return {@code true}, if there is a protrusion from the front, otherwise {@code false}. + */ + public boolean protrudesFront() { + return front > 0; + } + + /** + * Indicates whether there is a protrusion from the back. + * + * @return {@code true}, if there is a protrusion from the back, otherwise {@code false}. + */ + public boolean protrudesBack() { + return back > 0; + } + + /** + * Indicates whether there is a protrusion from the left. + * + * @return {@code true}, if there is a protrusion from the left, otherwise {@code false}. + */ + public boolean protrudesLeft() { + return left > 0; + } + + /** + * Indicates whether there is a protrusion from the right. + * + * @return {@code true}, if there is a protrusion from the right, otherwise {@code false}. + */ + public boolean protrudesRight() { + return right > 0; + } + + /** + * Indicates whether there is a protrusion from the top. + * + * @return {@code true}, if there is a protrusion from the top, otherwise {@code false}. + */ + public boolean protrudesTop() { + return top > 0; + } + + /** + * Indicates whether there is a protrusion anywhere (i.e. from the front, back, left, right or + * top). + * + * @return {@code true}, if there is a protrusion anywhere, otherwise {@code false}. + */ + public boolean protrudesAnywhere() { + return protrudesFront() || protrudesBack() + || protrudesLeft() || protrudesRight() + || protrudesTop(); + } + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorBoundingBox.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorBoundingBox.java new file mode 100644 index 0000000..ff138a9 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorBoundingBox.java @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.edgeevaluator; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.components.kernel.routing.EdgeEvaluator; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.routing.edgeevaluator.BoundingBoxProtrusionCheck.BoundingBoxProtrusion; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Compares the bounding box of a vehicle with the maximum allowed bounding box at the destination + * point of an edge and uses {@link Double#POSITIVE_INFINITY} as the edge's weight (effectively + * excluding the edge from routing) if the vehicle's bounding box protrudes the one of the point; + * otherwise, it uses 0. + */ +public class EdgeEvaluatorBoundingBox + implements + EdgeEvaluator { + + /** + * A key used for selecting this evaluator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "BOUNDING_BOX"; + private static final Logger LOG = LoggerFactory.getLogger(EdgeEvaluatorBoundingBox.class); + private final TCSObjectService objectService; + private final BoundingBoxProtrusionCheck protrusionCheck; + + /** + * Creates a new instance. + * + * @param objectService The object service. + * @param protrusionCheck Checks whether one bounding box protrudes beyond another one. + */ + @Inject + public EdgeEvaluatorBoundingBox( + TCSObjectService objectService, + BoundingBoxProtrusionCheck protrusionCheck + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.protrusionCheck = requireNonNull(protrusionCheck, "protrusionCheck"); + } + + @Override + public void onGraphComputationStart( + @Nonnull + Vehicle vehicle + ) { + } + + @Override + public void onGraphComputationEnd( + @Nonnull + Vehicle vehicle + ) { + } + + @Override + public double computeWeight( + @Nonnull + Edge edge, + @Nonnull + Vehicle vehicle + ) { + Point targetPoint = objectService.fetchObject(Point.class, edge.getTargetVertex()); + BoundingBoxProtrusion protrusion = protrusionCheck.checkProtrusion( + vehicle.getBoundingBox(), targetPoint.getMaxVehicleBoundingBox() + ); + + if (protrusion.protrudesAnywhere()) { + LOG.debug( + "Excluding edge '{}'. Bounding box of '{}' > max bounding box at '{}': {} > {}", + edge, + vehicle.getName(), + targetPoint.getName(), + vehicle.getBoundingBox(), + targetPoint.getMaxVehicleBoundingBox() + ); + return Double.POSITIVE_INFINITY; + } + + return 0; + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorComposite.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorComposite.java new file mode 100644 index 0000000..b869325 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorComposite.java @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.edgeevaluator; + +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.inject.Inject; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.components.kernel.routing.EdgeEvaluator; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.routing.jgrapht.ShortestPathConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@link EdgeEvaluator} computing costs as the sum of the costs computed by all configured + * evaluators. + */ +public class EdgeEvaluatorComposite + implements + EdgeEvaluator { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(EdgeEvaluatorComposite.class); + /** + * The evaluators. + */ + private final Set<EdgeEvaluator> evaluators = new HashSet<>(); + + /** + * Creates a new instance. + * + * @param configuration The configuration to use. + * @param availableEvaluators The configured evaluators to use. + */ + @Inject + public EdgeEvaluatorComposite( + ShortestPathConfiguration configuration, + Map<String, EdgeEvaluator> availableEvaluators + ) { + if (availableEvaluators.isEmpty()) { + LOG.warn("No edge evaluator enabled, falling back to distance-based evaluation."); + evaluators.add(new EdgeEvaluatorDistance()); + } + else { + for (String evaluatorKey : configuration.edgeEvaluators()) { + checkArgument( + availableEvaluators.containsKey(evaluatorKey), + "Unknown edge evaluator key: %s", + evaluatorKey + ); + evaluators.add(availableEvaluators.get(evaluatorKey)); + } + } + } + + @Override + public void onGraphComputationStart(Vehicle vehicle) { + for (EdgeEvaluator component : evaluators) { + component.onGraphComputationStart(vehicle); + } + } + + @Override + public void onGraphComputationEnd(Vehicle vehicle) { + for (EdgeEvaluator component : evaluators) { + component.onGraphComputationEnd(vehicle); + } + } + + @Override + public double computeWeight(Edge edge, Vehicle vehicle) { + double result = 0.0; + for (EdgeEvaluator component : evaluators) { + result += component.computeWeight(edge, vehicle); + } + return result; + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorDistance.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorDistance.java new file mode 100644 index 0000000..b25ba71 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorDistance.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.edgeevaluator; + +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.components.kernel.routing.EdgeEvaluator; +import org.opentcs.data.model.Vehicle; + +/** + * Uses an edge's length as its weight. + */ +public class EdgeEvaluatorDistance + implements + EdgeEvaluator { + + /** + * A key used for selecting this evaluator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "DISTANCE"; + + public EdgeEvaluatorDistance() { + } + + @Override + public void onGraphComputationStart(Vehicle vehicle) { + } + + @Override + public void onGraphComputationEnd(Vehicle vehicle) { + } + + @Override + public double computeWeight(Edge edge, Vehicle vehicle) { + return edge.getPath().getLength(); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorExplicitProperties.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorExplicitProperties.java new file mode 100644 index 0000000..d4173ee --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorExplicitProperties.java @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.edgeevaluator; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.components.kernel.Router.PROPKEY_ROUTING_COST_FORWARD; +import static org.opentcs.components.kernel.Router.PROPKEY_ROUTING_COST_REVERSE; +import static org.opentcs.components.kernel.Router.PROPKEY_ROUTING_GROUP; + +import jakarta.inject.Inject; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.components.kernel.routing.EdgeEvaluator; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Vehicle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Uses an edge's explicit routing cost (given as a property value) as its weight. + */ +public class EdgeEvaluatorExplicitProperties + implements + EdgeEvaluator { + + /** + * A key used for selecting this evaluator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "EXPLICIT_PROPERTIES"; + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(EdgeEvaluatorExplicitProperties.class); + /** + * This class's configuration. + */ + private final ExplicitPropertiesConfiguration configuration; + + @Inject + public EdgeEvaluatorExplicitProperties(ExplicitPropertiesConfiguration configuration) { + this.configuration = requireNonNull(configuration, "configuration"); + } + + @Override + public void onGraphComputationStart(Vehicle vehicle) { + } + + @Override + public void onGraphComputationEnd(Vehicle vehicle) { + } + + @Override + public double computeWeight(Edge edge, Vehicle vehicle) { + requireNonNull(edge, "edge"); + requireNonNull(vehicle, "vehicle"); + + String group = extractVehicleGroup(vehicle); + + if (edge.isTravellingReverse()) { + return parseCosts( + extractRoutingCostString( + edge.getPath(), + PROPKEY_ROUTING_COST_REVERSE + group + ) + ); + } + else { + return parseCosts( + extractRoutingCostString( + edge.getPath(), + PROPKEY_ROUTING_COST_FORWARD + group + ) + ); + } + } + + private String extractVehicleGroup(Vehicle vehicle) { + String group = vehicle.getProperty(PROPKEY_ROUTING_GROUP); + + return group == null ? "" : group; + } + + private String extractRoutingCostString(Path path, String propertyKey) { + String propVal = path.getProperty(propertyKey); + + if (propVal == null) { + LOG.warn( + "No routing cost property value for key '{}' in path '{}'. Using configured default: {}", + propertyKey, + path, + configuration.defaultValue() + ); + return configuration.defaultValue(); + } + return propVal; + } + + private double parseCosts(String costs) { + try { + return Double.parseDouble(costs); + } + catch (NumberFormatException exc) { + LOG.warn("Exception parsing routing cost value '{}'.", costs, exc); + return Double.POSITIVE_INFINITY; + } + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorHops.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorHops.java new file mode 100644 index 0000000..9f9297d --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorHops.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.edgeevaluator; + +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.components.kernel.routing.EdgeEvaluator; +import org.opentcs.data.model.Vehicle; + +/** + * Uses a weight of 1 for every edge. + */ +public class EdgeEvaluatorHops + implements + EdgeEvaluator { + + /** + * A key used for selecting this evaluator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "HOPS"; + + public EdgeEvaluatorHops() { + } + + @Override + public void onGraphComputationStart(Vehicle vehicle) { + } + + @Override + public void onGraphComputationEnd(Vehicle vehicle) { + } + + @Override + public double computeWeight(Edge edge, Vehicle vehicle) { + return 1; + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorTravelTime.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorTravelTime.java new file mode 100644 index 0000000..0ec04a5 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorTravelTime.java @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.edgeevaluator; + +import static org.opentcs.strategies.basic.routing.PointRouter.INFINITE_COSTS; + +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.components.kernel.routing.EdgeEvaluator; +import org.opentcs.data.model.Vehicle; + +/** + * Uses the estimated travel time (length/maximum velocity) for an edge as its weight. + */ +public class EdgeEvaluatorTravelTime + implements + EdgeEvaluator { + + /** + * A key used for selecting this evaluator in a configuration setting. + * Should be unique among all keys. + */ + public static final String CONFIGURATION_KEY = "TRAVELTIME"; + + public EdgeEvaluatorTravelTime() { + } + + @Override + public void onGraphComputationStart(Vehicle vehicle) { + } + + @Override + public void onGraphComputationEnd(Vehicle vehicle) { + } + + @Override + public double computeWeight(Edge edge, Vehicle vehicle) { + int maxVelocity; + if (edge.isTravellingReverse()) { + maxVelocity = Math.min( + vehicle.getMaxReverseVelocity(), + edge.getPath().getMaxReverseVelocity() + ); + } + else { + maxVelocity = Math.min(vehicle.getMaxVelocity(), edge.getPath().getMaxVelocity()); + } + return (maxVelocity == 0) ? INFINITE_COSTS : edge.getPath().getLength() / maxVelocity; + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/ExplicitPropertiesConfiguration.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/ExplicitPropertiesConfiguration.java new file mode 100644 index 0000000..00bdf0c --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/edgeevaluator/ExplicitPropertiesConfiguration.java @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.edgeevaluator; + +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; + +/** + * Provides methods to configure {@link EdgeEvaluatorExplicitProperties}. + */ +@ConfigurationPrefix(ExplicitPropertiesConfiguration.PREFIX) +public interface ExplicitPropertiesConfiguration { + + /** + * This configuration's prefix. + */ + String PREFIX = "defaultrouter.edgeevaluator.explicitproperties"; + + @ConfigurationEntry( + type = "String", + description = { + "The default value used as the routing cost of an edge if no property is set on the " + + "corresponding path.", + "The value should be an integer. If it is not, the edge is excluded from routing."}, + changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY + ) + String defaultValue(); +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/AbstractModelGraphMapper.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/AbstractModelGraphMapper.java new file mode 100644 index 0000000..34fa7d8 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/AbstractModelGraphMapper.java @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.jgrapht.Graph; +import org.jgrapht.graph.DirectedWeightedMultigraph; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Mapper to translate a collection of points and paths into a weighted graph. + */ +public abstract class AbstractModelGraphMapper + implements + ModelGraphMapper { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractModelGraphMapper.class); + private final PointVertexMapper pointVertexMapper; + private final PathEdgeMapper pathEdgeMapper; + + /** + * Creates a new instance. + * + * @param pointVertexMapper Translates a collection of points to vertices. + * @param pathEdgeMapper Translates a collection of paths to weighted edges. + */ + public AbstractModelGraphMapper( + @Nonnull + PointVertexMapper pointVertexMapper, + @Nonnull + PathEdgeMapper pathEdgeMapper + ) { + this.pointVertexMapper = requireNonNull(pointVertexMapper, "pointVertextMapper"); + this.pathEdgeMapper = requireNonNull(pathEdgeMapper, "pathEdgeMapper"); + } + + @Override + public Graph<String, Edge> translateModel( + Collection<Point> points, + Collection<Path> paths, + Vehicle vehicle + ) { + requireNonNull(points, "points"); + requireNonNull(paths, "paths"); + requireNonNull(vehicle, "vehicle"); + + LOG.debug("Translating model for {}...", vehicle.getName()); + long timeStampBefore = System.currentTimeMillis(); + + Graph<String, Edge> graph = new DirectedWeightedMultigraph<>(Edge.class); + + for (String vertex : pointVertexMapper.translatePoints(points)) { + graph.addVertex(vertex); + } + + for (Map.Entry<Edge, Double> edgeEntry : pathEdgeMapper.translatePaths(paths, vehicle) + .entrySet()) { + graph.addEdge( + edgeEntry.getKey().getSourceVertex(), + edgeEntry.getKey().getTargetVertex(), + edgeEntry.getKey() + ); + graph.setEdgeWeight(edgeEntry.getKey(), edgeEntry.getValue()); + } + + LOG.debug( + "Translated model for {} in {} milliseconds.", + vehicle.getName(), + System.currentTimeMillis() - timeStampBefore + ); + + return graph; + } + + @Override + public Graph<String, Edge> updateGraph( + Collection<Path> paths, + Vehicle vehicle, + Graph<String, Edge> graph + ) { + requireNonNull(paths, "paths"); + requireNonNull(vehicle, "vehicle"); + requireNonNull(graph, "graph"); + + LOG.debug("Updating graph for {}...", vehicle.getName()); + long timeStampBefore = System.currentTimeMillis(); + + Graph<String, Edge> updatedGraph = new DirectedWeightedMultigraph<>(Edge.class); + + // First, copy all points from the original graph. + for (String vertex : graph.vertexSet()) { + updatedGraph.addVertex(vertex); + } + + // Then, determine the edges that have not changed and copy these from the original graph as + // well. + Set<String> pathNames = paths.stream() + .map(Path::getName) + .collect(Collectors.toSet()); + Set<Edge> unchangedEdges = graph.edgeSet().stream() + .filter(edge -> !pathNames.contains(edge.getPath().getName())) + .collect(Collectors.toSet()); + LOG.debug( + "Adding {} (unchanged) edges (out of originally {}) to the graph...", + unchangedEdges.size(), + graph.edgeSet().size() + ); + for (Edge edge : unchangedEdges) { + updatedGraph.addEdge(edge.getSourceVertex(), edge.getTargetVertex(), edge); + updatedGraph.setEdgeWeight(edge, graph.getEdgeWeight(edge)); + } + + // Finally, map all paths that have changed and add the corresponding edges. + Map<Edge, Double> changedEdges = pathEdgeMapper.translatePaths(paths, vehicle); + LOG.debug("Adding {} (changed) edges to the graph...", changedEdges.size()); + for (Map.Entry<Edge, Double> edgeEntry : changedEdges.entrySet()) { + updatedGraph.addEdge( + edgeEntry.getKey().getSourceVertex(), + edgeEntry.getKey().getTargetVertex(), + edgeEntry.getKey() + ); + updatedGraph.setEdgeWeight(edgeEntry.getKey(), edgeEntry.getValue()); + } + + LOG.debug( + "Updated graph for {} in {} milliseconds.", + vehicle.getName(), + System.currentTimeMillis() - timeStampBefore + ); + + return updatedGraph; + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/AbstractPointRouterFactory.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/AbstractPointRouterFactory.java new file mode 100644 index 0000000..535b93f --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/AbstractPointRouterFactory.java @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import org.jgrapht.Graph; +import org.jgrapht.alg.interfaces.ShortestPathAlgorithm; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.routing.PointRouter; +import org.opentcs.strategies.basic.routing.PointRouterFactory; +import org.opentcs.strategies.basic.routing.jgrapht.GraphProvider.GraphResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Creates {@link PointRouter} instances with algorithm implementations created by subclasses. + */ +public abstract class AbstractPointRouterFactory + implements + PointRouterFactory { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractPointRouterFactory.class); + private final GraphProvider graphProvider; + + /** + * Creates a new instance. + * + * @param graphProvider Provides routing graphs for vehicles. + */ + public AbstractPointRouterFactory( + @Nonnull + GraphProvider graphProvider + ) { + this.graphProvider = requireNonNull(graphProvider, "graphProvider"); + } + + @Override + public PointRouter createPointRouter( + @Nonnull + Vehicle vehicle, + @Nonnull + Set<Point> pointsToExclude, + @Nonnull + Set<Path> pathsToExclude + ) { + requireNonNull(vehicle, "vehicle"); + requireNonNull(pointsToExclude, "pointsToExclude"); + requireNonNull(pathsToExclude, "pathsToExclude"); + + long timeStampBefore = System.currentTimeMillis(); + + GraphProvider.GraphResult graphResult; + if (pointsToExclude.isEmpty() && pathsToExclude.isEmpty()) { + graphResult = graphProvider.getGraphResult(vehicle); + } + else { + graphResult = graphProvider.getDerivedGraphResult(vehicle, pointsToExclude, pathsToExclude); + } + + PointRouter router = createPointRouter(graphResult); + + LOG.debug( + "Created point router for {} in {} milliseconds.", + vehicle.getName(), + System.currentTimeMillis() - timeStampBefore + ); + + return router; + } + + @Override + public PointRouter createGeneralPointRouter( + @Nonnull + Set<Point> pointsToExclude, + @Nonnull + Set<Path> pathsToExclude + ) { + requireNonNull(pointsToExclude, "pointsToExclude"); + requireNonNull(pathsToExclude, "pathsToExclude"); + + long timeStampBefore = System.currentTimeMillis(); + + GraphProvider.GraphResult graphResult; + if (pointsToExclude.isEmpty() && pathsToExclude.isEmpty()) { + graphResult = graphProvider.getGeneralGraphResult(); + } + else { + graphResult = graphProvider.getDerivedGeneralGraphResult(pointsToExclude, pathsToExclude); + } + + PointRouter router = createPointRouter(graphResult); + + LOG.debug( + "Created a general point router in {} milliseconds.", + System.currentTimeMillis() - timeStampBefore + ); + + return router; + } + + /** + * Returns a shortest path algorithm implementation working on the given graph. + * + * @param graph The graph. + * @return A shortest path algorithm implementation working on the given graph. + */ + protected abstract ShortestPathAlgorithm<String, Edge> createShortestPathAlgorithm( + Graph<String, Edge> graph + ); + + private PointRouter createPointRouter(GraphResult graphResult) { + Set<Point> points = new HashSet<>(graphResult.getPointBase()); + points.removeAll(graphResult.getExcludedPoints()); + + PointRouter router = new ShortestPathPointRouter( + createShortestPathAlgorithm(graphResult.getGraph()), + points + ); + // Make a single request for a route from one point to a different one to make sure the + // point router is primed. (Some implementations are initialized lazily.) + if (points.size() >= 2) { + Iterator<Point> pointIter = points.iterator(); + router.getRouteSteps(pointIter.next(), pointIter.next()); + } + + return router; + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/BellmanFordPointRouterFactory.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/BellmanFordPointRouterFactory.java new file mode 100644 index 0000000..c8bb045 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/BellmanFordPointRouterFactory.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import org.jgrapht.Graph; +import org.jgrapht.alg.interfaces.ShortestPathAlgorithm; +import org.jgrapht.alg.shortestpath.BellmanFordShortestPath; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.strategies.basic.routing.PointRouter; + +/** + * Creates {@link PointRouter} instances based on the Bellman-Ford algorithm. + */ +public class BellmanFordPointRouterFactory + extends + AbstractPointRouterFactory { + + /** + * Creates a new instance. + * + * @param graphProvider Provides routing graphs for vehicles. + */ + @Inject + public BellmanFordPointRouterFactory( + @Nonnull + GraphProvider graphProvider + ) { + super(graphProvider); + } + + @Override + protected ShortestPathAlgorithm<String, Edge> createShortestPathAlgorithm( + Graph<String, Edge> graph + ) { + return new BellmanFordShortestPath<>(graph); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/DefaultModelGraphMapper.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/DefaultModelGraphMapper.java new file mode 100644 index 0000000..c7b8560 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/DefaultModelGraphMapper.java @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import org.opentcs.strategies.basic.routing.edgeevaluator.EdgeEvaluatorComposite; + +/** + * Uses {@link EdgeEvaluatorComposite} to translate a collection of points and paths into a + * weighted graph. + */ +public class DefaultModelGraphMapper + extends + AbstractModelGraphMapper { + + /** + * Creates a new instance. + * + * @param edgeEvaluator Computes the weights of single edges in the graph. + * @param mapperComponentsFactory A factory for creating mapper-related components. + */ + @Inject + public DefaultModelGraphMapper( + @Nonnull + EdgeEvaluatorComposite edgeEvaluator, + @Nonnull + MapperComponentsFactory mapperComponentsFactory + ) { + super( + mapperComponentsFactory.createPointVertexMapper(), + mapperComponentsFactory.createPathEdgeMapper(edgeEvaluator, true) + ); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/DijkstraPointRouterFactory.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/DijkstraPointRouterFactory.java new file mode 100644 index 0000000..ac496c4 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/DijkstraPointRouterFactory.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import org.jgrapht.Graph; +import org.jgrapht.alg.interfaces.ShortestPathAlgorithm; +import org.jgrapht.alg.shortestpath.DijkstraShortestPath; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.strategies.basic.routing.PointRouter; + +/** + * Creates {@link PointRouter} instances based on the Dijkstra algorithm. + */ +public class DijkstraPointRouterFactory + extends + AbstractPointRouterFactory { + + /** + * Creates a new instance. + * + * @param graphProvider Provides routing graphs for vehicles. + */ + @Inject + public DijkstraPointRouterFactory( + @Nonnull + GraphProvider graphProvider + ) { + super(graphProvider); + } + + @Override + protected ShortestPathAlgorithm<String, Edge> createShortestPathAlgorithm( + Graph<String, Edge> graph + ) { + return new DijkstraShortestPath<>(graph); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/FloydWarshallPointRouterFactory.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/FloydWarshallPointRouterFactory.java new file mode 100644 index 0000000..9cb7053 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/FloydWarshallPointRouterFactory.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import org.jgrapht.Graph; +import org.jgrapht.alg.interfaces.ShortestPathAlgorithm; +import org.jgrapht.alg.shortestpath.FloydWarshallShortestPaths; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.strategies.basic.routing.PointRouter; + +/** + * Creates {@link PointRouter} instances based on the Floyd-Warshall algorithm. + */ +public class FloydWarshallPointRouterFactory + extends + AbstractPointRouterFactory { + + /** + * Creates a new instance. + * + * @param graphProvider Provides routing graphs for vehicles. + */ + @Inject + public FloydWarshallPointRouterFactory( + @Nonnull + GraphProvider graphProvider + ) { + super(graphProvider); + } + + @Override + protected ShortestPathAlgorithm<String, Edge> createShortestPathAlgorithm( + Graph<String, Edge> graph + ) { + return new FloydWarshallShortestPaths<>(graph); + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/GeneralModelGraphMapper.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/GeneralModelGraphMapper.java new file mode 100644 index 0000000..d9c5f5d --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/GeneralModelGraphMapper.java @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import org.opentcs.strategies.basic.routing.edgeevaluator.EdgeEvaluatorHops; + +/** + * Uses {@link EdgeEvaluatorHops} to translate a collection of points and paths into a + * weighted graph. + */ +public class GeneralModelGraphMapper + extends + AbstractModelGraphMapper { + + /** + * Creates a new instance. + * + * @param edgeEvaluatorHops Computes the weights of single edges in the graph. + * @param mapperComponentsFactory A factory for creating mapper-related components. + */ + @Inject + public GeneralModelGraphMapper( + @Nonnull + EdgeEvaluatorHops edgeEvaluatorHops, + @Nonnull + MapperComponentsFactory mapperComponentsFactory + ) { + super( + mapperComponentsFactory.createPointVertexMapper(), + mapperComponentsFactory.createPathEdgeMapper(edgeEvaluatorHops, false) + ); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/GraphMutator.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/GraphMutator.java new file mode 100644 index 0000000..8c6efc2 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/GraphMutator.java @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; +import org.jgrapht.Graph; +import org.jgrapht.graph.DirectedWeightedMultigraph; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.strategies.basic.routing.jgrapht.GraphProvider.GraphResult; + +/** + * Provides methods for mutating {@link GraphResult}s. + */ +public class GraphMutator { + + /** + * Creates a new instance. + */ + public GraphMutator() { + } + + /** + * Creates a graph that is derived from the given base graph by excluding the given sets of points + * and paths from the derived graph. + * + * @param pointsToExclude The set of points to exclude from the derived graph. + * @param pathsToExclude The set of paths to exclude from the derived graph. + * @param baseGraph The base graph. + * @return The derived graph. + */ + public GraphResult deriveGraph( + @Nonnull + Set<Point> pointsToExclude, + @Nonnull + Set<Path> pathsToExclude, + @Nonnull + GraphResult baseGraph + ) { + requireNonNull(pointsToExclude, "pointsToExclude"); + requireNonNull(pathsToExclude, "pathsToExclude"); + requireNonNull(baseGraph, "baseGraph"); + + // Determine the derived point base and path base. + Set<Point> derivedPointBase = new HashSet<>(baseGraph.getPointBase()); + derivedPointBase.removeAll(pointsToExclude); + Set<Path> derivedPathBase = new HashSet<>(baseGraph.getPathBase()); + derivedPathBase.removeAll(pathsToExclude); + + Graph<String, Edge> derivedGraph = new DirectedWeightedMultigraph<>(Edge.class); + + // Determine the vertices that should be included and add them to the derived graph. + Set<String> pointsToIncludeByName = derivedPointBase.stream() + .map(Point::getName) + .collect(Collectors.toSet()); + baseGraph.getGraph().vertexSet().stream() + .filter(vertex -> pointsToIncludeByName.contains(vertex)) + .forEach(vertex -> derivedGraph.addVertex(vertex)); + + // Determine the edges that should be included and add them to the derived graph. + Set<String> pathsToIncludeByName = derivedPathBase.stream() + .map(Path::getName) + .collect(Collectors.toSet()); + baseGraph.getGraph().edgeSet().stream() + .filter(edge -> pathsToIncludeByName.contains(edge.getPath().getName())) + // Ensure that edges are only added if their source and target vertices are contained in the + // derived graph. This is relevant when there are points to be excluded from the derived + // graph, as adding an edge whose source or target vertex is not present in the graph will + // result in an IllegalArgumentException. + .filter( + edge -> pointsToIncludeByName.contains(edge.getSourceVertex()) + && pointsToIncludeByName.contains(edge.getTargetVertex()) + ) + .forEach(edge -> { + derivedGraph.addEdge(edge.getSourceVertex(), edge.getTargetVertex(), edge); + derivedGraph.setEdgeWeight(edge, baseGraph.getGraph().getEdgeWeight(edge)); + }); + + return new GraphResult( + baseGraph.getVehicle(), + baseGraph.getPointBase(), + baseGraph.getPathBase(), + pointsToExclude, + pathsToExclude, + derivedGraph + ); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/GraphProvider.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/GraphProvider.java new file mode 100644 index 0000000..4b3063d --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/GraphProvider.java @@ -0,0 +1,375 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.WeakHashMap; +import org.jgrapht.Graph; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.components.kernel.routing.GroupMapper; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; + +/** + * Provides routing graphs for vehicles. + * <p> + * This provider caches computed routing graphs until it is {@link #invalidate() invalidated}. + * </p> + */ +public class GraphProvider { + + private final TCSObjectService objectService; + private final ModelGraphMapper defaultModelGraphMapper; + private final ModelGraphMapper generalModelGraphMapper; + private final GroupMapper routingGroupMapper; + private final GraphMutator graphMutator; + /** + * Contains {@link GraphResult}s mapped to (vehicle) routing groups. + */ + private final Map<String, GraphResult> graphResultsByRoutingGroup = new HashMap<>(); + /** + * A cache for derived {@link GraphResult}s. + */ + private final Map<String, GraphResult> derivedGraphResults = new WeakHashMap<>(); + /** + * The set of points that is currently used for computing routing graphs. + */ + private final HashedResourceSet<Point> currentPointBase + = new HashedResourceSet<>(this::pointsHashCode); + /** + * The set of paths that is currently used for computing routing graphs. + */ + private final HashedResourceSet<Path> currentPathBase + = new HashedResourceSet<>(this::pathsHashCode); + /** + * The general {@link GraphResult}. + */ + private GraphResult generalGraphResult; + + /** + * Creates a new instance. + * + * @param objectService The object service providing the model data. + * @param defaultModelGraphMapper Maps the points and paths to a graph. + * @param generalModelGraphMapper Maps the points and paths to a graph. + * @param routingGroupMapper Used to map vehicles to their routing groups. + * @param graphMutator Provides methods for mutating {@link GraphResult}s. + */ + @Inject + public GraphProvider( + @Nonnull + TCSObjectService objectService, + @Nonnull + GeneralModelGraphMapper generalModelGraphMapper, + @Nonnull + DefaultModelGraphMapper defaultModelGraphMapper, + @Nonnull + GroupMapper routingGroupMapper, + @Nonnull + GraphMutator graphMutator + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.defaultModelGraphMapper = requireNonNull( + defaultModelGraphMapper, + "defaultModelGraphMapper" + ); + this.generalModelGraphMapper = requireNonNull( + generalModelGraphMapper, + "generalModelGraphMapper" + ); + this.routingGroupMapper = requireNonNull(routingGroupMapper, "routingGroupMapper"); + this.graphMutator = requireNonNull(graphMutator, "graphMutator"); + } + + /** + * Invalidates any graphs that have already been calculated. + */ + public void invalidate() { + currentPointBase.clear(); + currentPathBase.clear(); + graphResultsByRoutingGroup.clear(); + derivedGraphResults.clear(); + generalGraphResult = null; + } + + /** + * Returns a {@link GraphResult} containing the routing graph for the given vehicle. + * + * @param vehicle The vehicle. + * @return A {@link GraphResult} containing the routing graph for the given vehicle. + */ + public GraphResult getGraphResult(Vehicle vehicle) { + return graphResultsByRoutingGroup.computeIfAbsent( + routingGroupMapper.apply(vehicle), + routingGroup -> new GraphResult( + vehicle, + getCurrentPointBase().getResources(), + getCurrentPathBase().getResources(), + Set.of(), + Set.of(), + defaultModelGraphMapper.translateModel( + getCurrentPointBase().getResources(), + getCurrentPathBase().getResources(), + vehicle + ) + ) + ); + } + + /** + * Returns a {@link GraphResult} containing a general routing graph that is not affected by any + * path properties or any configured edge evaluators. + * + * @return A {@link GraphResult} containing the routing graph. + */ + public GraphResult getGeneralGraphResult() { + if (generalGraphResult == null) { + generalGraphResult = new GraphResult( + new Vehicle("Dummy"), + getCurrentPointBase().getResources(), + getCurrentPathBase().getResources(), + Set.of(), + Set.of(), + generalModelGraphMapper.translateModel( + getCurrentPointBase().getResources(), + getCurrentPathBase().getResources(), + new Vehicle("Dummy") + ) + ); + } + + return generalGraphResult; + } + + /** + * Returns a {@link GraphResult} that is derived from the given vehicle's "default" routing graph + * in such a way that the given sets of points and paths are not included. + * + * @param vehicle The vehicle. + * @param pointsToExclude The set of points to not include in the derived routing graph. + * @param pathsToExclude The set of paths to not include in the derived routing graph. + * @return The derived {@link GraphResult}. + */ + public GraphResult getDerivedGraphResult( + @Nonnull + Vehicle vehicle, + @Nonnull + Set<Point> pointsToExclude, + @Nonnull + Set<Path> pathsToExclude + ) { + requireNonNull(vehicle, "vehicle"); + requireNonNull(pointsToExclude, "pointsToExclude"); + requireNonNull(pathsToExclude, "pathsToExclude"); + + return derivedGraphResults.computeIfAbsent( + derivedGraphResultCacheKey(vehicle, pointsToExclude, pathsToExclude), + key -> graphMutator.deriveGraph(pointsToExclude, pathsToExclude, getGraphResult(vehicle)) + ); + } + + /** + * Returns a {@link GraphResult} that is derived from the general routing graph in such a way + * that the given sets of points and paths are not included. + * + * @param pointsToExclude The set of points to not include in the derived routing graph. + * @param pathsToExclude The set of paths to not include in the derived routing graph. + * @return The derived {@link GraphResult}. + */ + public GraphResult getDerivedGeneralGraphResult( + @Nonnull + Set<Point> pointsToExclude, + @Nonnull + Set<Path> pathsToExclude + ) { + requireNonNull(pointsToExclude, "pointsToExclude"); + requireNonNull(pathsToExclude, "pathsToExclude"); + + return graphMutator.deriveGraph(pointsToExclude, pathsToExclude, getGeneralGraphResult()); + } + + /** + * Updates any {@link GraphResult}s that have already been calculated using the given paths. + * <p> + * The general graph result will not be updated as it does not consider locked paths + * and therefore always stays the same. + * </p> + * + * @param paths The paths to use for the update. + */ + public void updateGraphResults( + @Nonnull + Collection<Path> paths + ) { + requireNonNull(paths, "paths"); + + if (paths.isEmpty()) { + return; + } + + // Ensure the path base is up-to-date. + getCurrentPathBase().updateResources(paths); + + for (Map.Entry<String, GraphResult> entry : Set.copyOf(graphResultsByRoutingGroup.entrySet())) { + graphResultsByRoutingGroup.put( + entry.getKey(), + new GraphResult( + entry.getValue().getVehicle(), + entry.getValue().getPointBase(), + getCurrentPathBase().getResources(), + Set.of(), + Set.of(), + defaultModelGraphMapper.updateGraph( + paths, + entry.getValue().getVehicle(), + entry.getValue().getGraph() + ) + ) + ); + } + } + + private String derivedGraphResultCacheKey( + Vehicle vehicle, + Set<Point> pointsToExclude, + Set<Path> pathsToExclude + ) { + // Concat the different hash code values (e.g. instead of simply calculating the sum) to + // minimize risk of hash collisions. + return String.format( + "routingGroup(%s)_pointBase(%s)_pathBase(%s)_excludedPoints(%s)_excludedPaths(%s)", + routingGroupMapper.apply(vehicle).hashCode(), + getCurrentPointBase().getHash(), + getCurrentPathBase().getHash(), + pointsHashCode(pointsToExclude), + pathsHashCode(pathsToExclude) + ); + } + + private HashedResourceSet<Point> getCurrentPointBase() { + if (currentPointBase.isEmpty()) { + currentPointBase.overrideResources(objectService.fetchObjects(Point.class)); + } + + return currentPointBase; + } + + private HashedResourceSet<Path> getCurrentPathBase() { + if (currentPathBase.isEmpty()) { + currentPathBase.overrideResources(objectService.fetchObjects(Path.class)); + } + + return currentPathBase; + } + + private int pointsHashCode(Set<Point> points) { + return points.hashCode(); + } + + private int pathsHashCode(Set<Path> paths) { + int result = 0; + + for (Path path : paths) { + result += Objects.hash(path.getName(), path.isLocked(), path.getProperties()); + } + + return result; + } + + /** + * Contains the result of a graph computation. + */ + public static class GraphResult { + + private final Vehicle vehicle; + private final Set<Point> pointBase; + private final Set<Path> pathBase; + private final Set<Point> excludedPoints; + private final Set<Path> excludedPaths; + private final Graph<String, Edge> graph; + + /** + * Creates a new instance. + * + * @param vehicle The vehicle for which the given graph was computed. + * @param pointBase The set of points that was used to compute the given graph. + * @param pathBase The set of paths that was used to compute the given graph. + * @param excludedPoints The set of points that were excluded when computing the given graph. + * @param excludedPaths The set of paths that were excluded when computing the given graph. + * @param graph The computed graph. + */ + public GraphResult( + Vehicle vehicle, + Set<Point> pointBase, + Set<Path> pathBase, + Set<Point> excludedPoints, + Set<Path> excludedPaths, + Graph<String, Edge> graph + ) { + this.pointBase = Collections.unmodifiableSet(requireNonNull(pointBase, "pointBase")); + this.pathBase = Collections.unmodifiableSet(requireNonNull(pathBase, "pathBase")); + this.excludedPoints + = Collections.unmodifiableSet(requireNonNull(excludedPoints, "excludedPoints")); + this.excludedPaths + = Collections.unmodifiableSet(requireNonNull(excludedPaths, "excludedPaths")); + this.graph = requireNonNull(graph, "graph"); + this.vehicle = requireNonNull(vehicle, "vehicle"); + } + + /** + * Returns the vehicle for which the graph was computed. + * + * @return The vehicle for which the graph was computed. + */ + public Vehicle getVehicle() { + return vehicle; + } + + /** + * Returns the set of points that was used to compute the graph. + * + * @return The set of points that was used to compute the graph. + */ + public Set<Point> getPointBase() { + return pointBase; + } + + /** + * Returns the set of paths that was used to compute the graph. + * + * @return The set of paths that was used to compute the graph. + */ + public Set<Path> getPathBase() { + return pathBase; + } + + public Set<Point> getExcludedPoints() { + return excludedPoints; + } + + public Set<Path> getExcludedPaths() { + return excludedPaths; + } + + /** + * Returns the computed graph. + * + * @return The computed graph. + */ + public Graph<String, Edge> getGraph() { + return graph; + } + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/HashedResourceSet.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/HashedResourceSet.java new file mode 100644 index 0000000..6020ef6 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/HashedResourceSet.java @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; + +/** + * A wrapper for a set of that also provides a hash value for that set according to a given hash + * function. + * + * @param <T> The type of the set. + */ +public class HashedResourceSet<T> { + + private final Function<Set<T>, Integer> hashFunction; + private final Set<T> resources = new HashSet<>(); + private int hash; + + /** + * Creates a new instance. + * + * @param hashFunction The function to use for calculating the set's hash value. + */ + public HashedResourceSet( + @Nonnull + Function<Set<T>, Integer> hashFunction + ) { + this.hashFunction = requireNonNull(hashFunction, "hashFunction"); + updateHash(); + } + + /** + * Indicates whether the underlying set is empty. + * + * @return {@code true}, if the set is empty, otherwise {@code false}. + */ + public boolean isEmpty() { + return resources.isEmpty(); + } + + /** + * Clears the underlying set. + */ + public void clear() { + resources.clear(); + updateHash(); + } + + /** + * Updates the underlying set so that it only contains the elements contained in the given + * collection. + * + * @param collection The collection. + */ + public void overrideResources( + @Nonnull + Collection<T> collection + ) { + requireNonNull(collection, "collection"); + + this.resources.clear(); + this.resources.addAll(collection); + updateHash(); + } + + /** + * Updates the underlying set by adding the elements contained in the given collection or + * replacing them if they already exist. + * + * @param collection The collection. + */ + public void updateResources( + @Nonnull + Collection<T> collection + ) { + requireNonNull(collection, "collection"); + + this.resources.removeAll(collection); + this.resources.addAll(collection); + updateHash(); + } + + /** + * Returns the underlying set. + * + * @return The underlying set. + */ + @Nonnull + public Set<T> getResources() { + return resources; + } + + /** + * Returns the hash value for the underlying set. + * + * @return The hash value. + */ + public int getHash() { + return hash; + } + + private void updateHash() { + hash = hashFunction.apply(resources); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/MapperComponentsFactory.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/MapperComponentsFactory.java new file mode 100644 index 0000000..9ff6f20 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/MapperComponentsFactory.java @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import jakarta.annotation.Nonnull; +import org.opentcs.components.kernel.routing.EdgeEvaluator; + +/** + * A factory for creating mapper-related components. + */ +public interface MapperComponentsFactory { + + /** + * Creates a {@link PointVertexMapper}. + * + * @return A {@link PointVertexMapper}. + */ + PointVertexMapper createPointVertexMapper(); + + /** + * Creates a {@link PathEdgeMapper}. + * + * @param edgeEvaluator Computes the weight of single edges. + * @param excludeLockedPaths Whether locked paths should be excluded from mapping. + * @return A {@link PathEdgeMapper}. + */ + PathEdgeMapper createPathEdgeMapper( + @Nonnull + EdgeEvaluator edgeEvaluator, + boolean excludeLockedPaths + ); +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/ModelGraphMapper.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/ModelGraphMapper.java new file mode 100644 index 0000000..f51caa7 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/ModelGraphMapper.java @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import java.util.Collection; +import org.jgrapht.Graph; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; + +/** + * Translates model data to weighted graphs. + */ +public interface ModelGraphMapper { + + /** + * Translates the given points and paths to a weighted graph. + * + * @param points The points of the model. + * @param paths The paths of the model. + * @param vehicle The vehicle for which to build the graph. + * @return A weighted graph representing the topology to be used for the given vehicle. + */ + Graph<String, Edge> translateModel( + Collection<Point> points, + Collection<Path> paths, + Vehicle vehicle + ); + + /** + * Re-translates the given paths and replaces corresponding edges in a copy of the provided graph. + * <p> + * If a path cannnot be translated to an edge but the provided graph contained an edge for that + * path, the edge will <em>not</em> be contained in the returned graph copy. + * </p> + * + * @param paths The paths to re-translate. + * @param vehicle The vehicle for which to update the graph. + * @param graph The graph to whose copy the re-translated paths are to be added. + * @return A copy of the provided graph including the re-translated paths. + */ + Graph<String, Edge> updateGraph( + Collection<Path> paths, + Vehicle vehicle, + Graph<String, Edge> graph + ); +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/PathEdgeMapper.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/PathEdgeMapper.java new file mode 100644 index 0000000..ff4103e --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/PathEdgeMapper.java @@ -0,0 +1,137 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import static java.util.Objects.requireNonNull; + +import com.google.inject.assistedinject.Assisted; +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.components.kernel.routing.EdgeEvaluator; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Vehicle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Mapper to translate a collection of {@link Path}s to weighted {@link Edge}s. + */ +public class PathEdgeMapper { + + private static final Logger LOG = LoggerFactory.getLogger(PathEdgeMapper.class); + private final EdgeEvaluator edgeEvaluator; + private final boolean excludeLockedPaths; + private final ShortestPathConfiguration configuration; + + /** + * Creates a new instance. + * + * @param edgeEvaluator Computes the weights of single edges. + * @param excludeLockedPaths Whether locked paths should be excluded from mapping. + * @param configuration The configuration. + */ + @Inject + public PathEdgeMapper( + @Nonnull + @Assisted + EdgeEvaluator edgeEvaluator, + @Assisted + boolean excludeLockedPaths, + @Nonnull + ShortestPathConfiguration configuration + ) { + this.edgeEvaluator = requireNonNull(edgeEvaluator, "edgeEvaluator"); + this.excludeLockedPaths = excludeLockedPaths; + this.configuration = requireNonNull(configuration, "configuration"); + } + + /** + * Translates the given {@link Path}s to weighted {@link Edge}s. + * + * @param paths The paths to translate to edges. + * @param vehicle The vehicle for which the edge weights are to be evaluated. + * @return The translated edges mapped to their corresponding edge weights. + */ + public Map<Edge, Double> translatePaths(Collection<Path> paths, Vehicle vehicle) { + requireNonNull(paths, "paths"); + requireNonNull(vehicle, "vehicle"); + + Map<Edge, Double> weightedEdges = new HashMap<>(); + + boolean allowNegativeEdgeWeights = configuration.algorithm().isHandlingNegativeCosts(); + + edgeEvaluator.onGraphComputationStart(vehicle); + + for (Path path : paths) { + if (shouldAddForwardEdge(path)) { + Edge edge = new Edge(path, false); + double weight = edgeEvaluator.computeWeight(edge, vehicle); + + if (weight < 0 && !allowNegativeEdgeWeights) { + LOG.warn( + "Edge {} with weight {} ignored. Algorithm {} cannot handle negative weights.", + edge, + weight, + configuration.algorithm().name() + ); + } + else if (weight == Double.POSITIVE_INFINITY) { + LOG.debug("Edge {} with infinite weight ignored.", edge); + } + else { + weightedEdges.put(edge, weight); + } + } + + if (shouldAddReverseEdge(path)) { + Edge edge = new Edge(path, true); + double weight = edgeEvaluator.computeWeight(edge, vehicle); + + if (weight < 0 && !allowNegativeEdgeWeights) { + LOG.warn( + "Edge {} with weight {} ignored. Algorithm {} cannot handle negative weights.", + edge, + weight, + configuration.algorithm().name() + ); + } + else if (weight == Double.POSITIVE_INFINITY) { + LOG.debug("Edge {} with infinite weight ignored.", edge); + } + else { + weightedEdges.put(edge, weight); + } + } + } + + edgeEvaluator.onGraphComputationEnd(vehicle); + + return weightedEdges; + } + + /** + * Checks whether an edge from the source of the given path to its destination should be added to + * the graph. + * + * @param path The path. + * @return <code>true</code> if and only if the edge should be added to the graph. + */ + private boolean shouldAddForwardEdge(Path path) { + return excludeLockedPaths ? path.isNavigableForward() : path.getMaxVelocity() != 0; + } + + /** + * Checks whether an edge from the destination of the given path to its source should be added to + * the graph. + * + * @param path The path. + * @return <code>true</code> if and only if the edge should be added to the graph. + */ + private boolean shouldAddReverseEdge(Path path) { + return excludeLockedPaths ? path.isNavigableReverse() : path.getMaxReverseVelocity() != 0; + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/PointRouterProvider.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/PointRouterProvider.java new file mode 100644 index 0000000..305ed10 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/PointRouterProvider.java @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.opentcs.components.kernel.routing.GroupMapper; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.routing.PointRouter; +import org.opentcs.strategies.basic.routing.PointRouterFactory; +import org.opentcs.strategies.basic.routing.ResourceAvoidanceExtractor; +import org.opentcs.strategies.basic.routing.ResourceAvoidanceExtractor.ResourcesToAvoid; + +/** + * Provides point routers for vehicles (more specifically for routing groups of vehicles). + * <p> + * This provider caches constructed point routers until it is {@link #invalidate() invalidated}. + * </p> + */ +public class PointRouterProvider { + + private final TCSObjectService objectService; + private final ResourceAvoidanceExtractor resourceAvoidanceExtractor; + private final GroupMapper routingGroupMapper; + private final PointRouterFactory pointRouterFactory; + private final GraphProvider graphProvider; + /** + * The point routers by vehicle routing group. + */ + private final Map<String, PointRouter> pointRoutersByVehicleGroup = new ConcurrentHashMap<>(); + + /** + * Creates a new instance. + * + * @param objectService The object service providing the model data. + * @param resourceAvoidanceExtractor Extracts resources to be avoided from transport orders. + * @param routingGroupMapper Used to map vehicles to their routing groups. + * @param pointRouterFactory A builder for constructing point routers (i.e., the routing tables). + * @param graphProvider Provides routing graphs for vehicles. + */ + @Inject + public PointRouterProvider( + TCSObjectService objectService, + ResourceAvoidanceExtractor resourceAvoidanceExtractor, + GroupMapper routingGroupMapper, + PointRouterFactory pointRouterFactory, + GraphProvider graphProvider + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.resourceAvoidanceExtractor = requireNonNull( + resourceAvoidanceExtractor, + "resourceAvoidanceExtractor" + ); + this.routingGroupMapper = requireNonNull(routingGroupMapper, "routingGroupMapper"); + this.pointRouterFactory = requireNonNull(pointRouterFactory, "pointRouterFactory"); + this.graphProvider = requireNonNull(graphProvider, "graphProvider"); + } + + /** + * Invalidates any point routers that have already been constructed. + */ + public void invalidate() { + pointRoutersByVehicleGroup.clear(); + graphProvider.invalidate(); + } + + /** + * Updates the routing topology with respect to the given paths. + * + * @param paths The paths to update in the routing topology. An empty set of paths results in any + * constructed point routers to be invalidated. + */ + public void updateRoutingTopology( + @Nonnull + Set<Path> paths + ) { + requireNonNull(paths, "paths"); + + pointRoutersByVehicleGroup.clear(); + + if (paths.isEmpty()) { + graphProvider.invalidate(); + } + else { + graphProvider.updateGraphResults(paths); + } + } + + /** + * Returns the {@link PointRouter} for the given vehicle considering the vehicle's routing group + * and the given transport order. + * + * @param vehicle The vehicle to get the point router for. + * @param order The transport order to be processed by the vehicle. + * @return The point router. + */ + public PointRouter getPointRouterForVehicle( + @Nonnull + Vehicle vehicle, + @Nullable + TransportOrder order + ) { + requireNonNull(vehicle, "vehicle"); + + return getPointRouterForVehicle( + vehicle, + resourceAvoidanceExtractor + .extractResourcesToAvoid(order) + ); + } + + /** + * Returns the {@link PointRouter} for the given vehicle considering the vehicle's routing group + * and the given set of resources to avoid. + * + * @param vehicle The vehicle to get the point router for. + * @param resourcesToAvoid The resources to avoid when computing the route. + * @return The point router. + */ + public PointRouter getPointRouterForVehicle( + @Nonnull + Vehicle vehicle, + @Nonnull + Set<TCSResourceReference<?>> resourcesToAvoid + ) { + requireNonNull(vehicle, "vehicle"); + requireNonNull(resourcesToAvoid, "resourcesToAvoid"); + + return getPointRouterForVehicle( + vehicle, + resourceAvoidanceExtractor + .extractResourcesToAvoid(resourcesToAvoid) + ); + } + + /** + * Returns all point routers mapped to the vehicle routing group they belong to. + * + * @return All point routers mapped to the vehicle routing group they belong to. + */ + public Map<String, PointRouter> getPointRoutersByVehicleGroup() { + // Since point routers get reset on topology changes, make sure there are point routers for + // all routing groups. + createMissingPointRouters(); + + return Collections.unmodifiableMap(pointRoutersByVehicleGroup); + } + + /** + * Returns a general point router that is not affected by any path properties or any configured + * edge evaluators. + * + * @param order The transport order to create the point router for. + * @return A general point router. + */ + public PointRouter getGeneralPointRouter( + @Nullable + TransportOrder order + ) { + ResourcesToAvoid resourcesToAvoid = resourceAvoidanceExtractor.extractResourcesToAvoid(order); + return pointRouterFactory.createGeneralPointRouter( + resourcesToAvoid.getPoints(), + resourcesToAvoid.getPaths() + ); + } + + private void createMissingPointRouters() { + Map<String, Vehicle> distinctRoutingGroups = new HashMap<>(); + for (Vehicle vehicle : objectService.fetchObjects(Vehicle.class)) { + distinctRoutingGroups.putIfAbsent(routingGroupMapper.apply(vehicle), vehicle); + } + + // Lazily create point routers if they don't exist. + distinctRoutingGroups.forEach( + (routingGroup, vehicle) -> getPointRouterForVehicle(vehicle, (TransportOrder) null) + ); + } + + private PointRouter getPointRouterForVehicle(Vehicle vehicle, ResourcesToAvoid resourcesToAvoid) { + if (!resourcesToAvoid.isEmpty()) { + return pointRouterFactory.createPointRouter( + vehicle, + resourcesToAvoid.getPoints(), + resourcesToAvoid.getPaths() + ); + } + + // In all other cases, create a point router if it does not yet exist for the vehicle's routing + // group. + return pointRoutersByVehicleGroup.computeIfAbsent( + routingGroupMapper.apply(vehicle), + routingGroup -> pointRouterFactory.createPointRouter(vehicle, Set.of(), Set.of()) + ); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/PointVertexMapper.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/PointVertexMapper.java new file mode 100644 index 0000000..d7f4424 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/PointVertexMapper.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import static java.util.Objects.requireNonNull; + +import jakarta.inject.Inject; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.data.model.Point; + +/** + * Mapper to translate a collection of {@link Point}s to names of vertices. + */ +public class PointVertexMapper { + + /** + * Creates a new instance. + */ + @Inject + public PointVertexMapper() { + } + + /** + * Translates the given {@link Point}s to names of vertices. + * + * @param points The points to translate to names of vertices. + * @return The translated names of vertices. + */ + public Set<String> translatePoints(Collection<Point> points) { + requireNonNull(points, "points"); + + return points.stream() + .map(Point::getName) + .collect(Collectors.toSet()); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/ShortestPathConfiguration.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/ShortestPathConfiguration.java new file mode 100644 index 0000000..3d51349 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/ShortestPathConfiguration.java @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import java.util.List; +import org.opentcs.configuration.ConfigurationEntry; +import org.opentcs.configuration.ConfigurationPrefix; + +/** + * Provides methods to configure the shortest path algorithm. + */ +@ConfigurationPrefix(ShortestPathConfiguration.PREFIX) +public interface ShortestPathConfiguration { + + /** + * This configuration's prefix. + */ + String PREFIX = "defaultrouter.shortestpath"; + + @ConfigurationEntry( + type = "String", + description = { + "The routing algorithm to be used. Valid values:", + "'DIJKSTRA': Routes are computed using Dijkstra's algorithm.", + "'BELLMAN_FORD': Routes are computed using the Bellman-Ford algorithm.", + "'FLOYD_WARSHALL': Routes are computed using the Floyd-Warshall algorithm."}, + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START + ) + Algorithm algorithm(); + + @ConfigurationEntry( + type = "Comma-separated list of strings", + description = { + "The types of route evaluators/cost factors to be used.", + "Results of multiple evaluators are added up. Valid values:", + "'DISTANCE': A route's cost equals the sum of the lengths of its paths.", + "'TRAVELTIME': A route's cost equals the vehicle's expected travel time.", + "'EXPLICIT_PROPERTIES': A route's cost equals the sum of the explicitly given costs " + + "extracted from path properties.", + "'HOPS': A route's cost equals the number of paths it consists of.", + "'BOUNDING_BOX': A route's cost equals 0 if the vehicle's bounding box does not protrude " + + "beyond _any_ bounding boxes of points along the route. Otherwise, a route's cost " + + "is considered infinitely high, resulting in the route to be effectively discarded." + }, + changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START + ) + List<String> edgeEvaluators(); + + /** + * The available algorithms. + */ + enum Algorithm { + /** + * The Dijkstra algorithm. + */ + DIJKSTRA(false), + /** + * The Bellman-Ford algorithm. + */ + BELLMAN_FORD(true), + /** + * The Floyd-Warshall algorithm. + */ + FLOYD_WARSHALL(false); + + private final boolean handlingNegativeCosts; + + Algorithm(boolean handlingNegativeCosts) { + this.handlingNegativeCosts = handlingNegativeCosts; + } + + public boolean isHandlingNegativeCosts() { + return handlingNegativeCosts; + } + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/ShortestPathPointRouter.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/ShortestPathPointRouter.java new file mode 100644 index 0000000..02d08e5 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/routing/jgrapht/ShortestPathPointRouter.java @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.jgrapht.GraphPath; +import org.jgrapht.alg.interfaces.ShortestPathAlgorithm; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.Route; +import org.opentcs.strategies.basic.routing.PointRouter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Computes routes between points using a JGraphT-based shortest path algorithm. + * <p> + * <em>Note that this implementation does not integrate static routes.</em> + * </p> + */ +public class ShortestPathPointRouter + implements + PointRouter { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ShortestPathPointRouter.class); + + private final ShortestPathAlgorithm<String, Edge> algo; + + private final Map<String, Point> points = new HashMap<>(); + + public ShortestPathPointRouter( + ShortestPathAlgorithm<String, Edge> algo, + Collection<Point> points + ) { + this.algo = requireNonNull(algo, "algo"); + requireNonNull(points, "points"); + + for (Point point : points) { + this.points.put(point.getName(), point); + } + + } + + @Override + public List<Route.Step> getRouteSteps(Point srcPoint, Point destPoint) { + requireNonNull(srcPoint, "srcPoint"); + requireNonNull(destPoint, "destPoint"); + + long timeBefore = System.currentTimeMillis(); + if (Objects.equals(srcPoint.getName(), destPoint.getName())) { + return new ArrayList<>(); + } + + GraphPath<String, Edge> graphPath = algo.getPath(srcPoint.getName(), destPoint.getName()); + if (graphPath == null) { + return null; + } + + List<Route.Step> result = translateToSteps(graphPath); + + LOG.debug( + "Looking up route from {} to {} took {} milliseconds.", + srcPoint.getName(), + destPoint.getName(), + System.currentTimeMillis() - timeBefore + ); + + return result; + } + + @Override + public long getCosts( + TCSObjectReference<Point> srcPointRef, + TCSObjectReference<Point> destPointRef + ) { + requireNonNull(srcPointRef, "srcPointRef"); + requireNonNull(destPointRef, "destPointRef"); + + if (Objects.equals(srcPointRef.getName(), destPointRef.getName())) { + return 0; + } + + GraphPath<String, Edge> graphPath = algo.getPath( + srcPointRef.getName(), + destPointRef.getName() + ); + if (graphPath == null) { + return INFINITE_COSTS; + } + + return (long) graphPath.getWeight(); + } + + private List<Route.Step> translateToSteps(GraphPath<String, Edge> graphPath) { + List<Edge> edges = graphPath.getEdgeList(); + List<Route.Step> result = new ArrayList<>(edges.size()); + + int routeIndex = 0; + for (Edge edge : edges) { + Point sourcePoint = points.get(graphPath.getGraph().getEdgeSource(edge)); + Point destPoint = points.get(graphPath.getGraph().getEdgeTarget(edge)); + + result.add( + new Route.Step( + edge.getPath(), + sourcePoint, + destPoint, + orientation(edge, sourcePoint), + routeIndex + ) + ); + routeIndex++; + } + + return result; + } + + private Vehicle.Orientation orientation(Edge edge, Point graphSourcePoint) { + return Objects.equals(edge.getPath().getSourcePoint(), graphSourcePoint.getReference()) + ? Vehicle.Orientation.FORWARD + : Vehicle.Orientation.BACKWARD; + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/AllocationAdvisor.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/AllocationAdvisor.java new file mode 100644 index 0000000..f6c941f --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/AllocationAdvisor.java @@ -0,0 +1,152 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.List; +import java.util.Set; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.components.kernel.Scheduler.Client; +import org.opentcs.data.model.TCSResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A module implementation that forwards method calls to all submodules. + */ +public class AllocationAdvisor + implements + Scheduler.Module { + + /** + * This class' logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(AllocationAdvisor.class); + /** + * The submodules. + */ + private final Set<Scheduler.Module> modules; + /** + * This instance's initialized flag. + */ + private boolean initialized; + + /** + * Creates a new instance. + * + * @param modules The submodules. + */ + @Inject + public AllocationAdvisor(Set<Scheduler.Module> modules) { + this.modules = requireNonNull(modules, "modules"); + } + + @Override + public void initialize() { + if (isInitialized()) { + LOG.debug("Already initialized, doing nothing."); + return; + } + + for (Scheduler.Module module : modules) { + module.initialize(); + } + + initialized = true; + } + + @Override + public void terminate() { + if (!isInitialized()) { + LOG.debug("Not initialized, doing nothing."); + return; + } + + for (Scheduler.Module module : modules) { + module.terminate(); + } + + initialized = false; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void setAllocationState( + @Nonnull + Scheduler.Client client, + @Nonnull + Set<TCSResource<?>> alloc, + @Nonnull + List<Set<TCSResource<?>>> remainingClaim + ) { + requireNonNull(client, "client"); + requireNonNull(alloc, "alloc"); + requireNonNull(remainingClaim, "remainingClaim"); + + for (Scheduler.Module module : modules) { + module.setAllocationState(client, alloc, remainingClaim); + } + } + + @Override + public boolean mayAllocate(Scheduler.Client client, Set<TCSResource<?>> resources) { + boolean result = true; + for (Scheduler.Module module : modules) { + result = result && module.mayAllocate(client, resources); + } + return result; + } + + @Override + public void prepareAllocation( + Scheduler.Client client, + Set<TCSResource<?>> resources + ) { + requireNonNull(client, "client"); + requireNonNull(resources, "resources"); + + for (Scheduler.Module module : modules) { + LOG.debug( + "Module {}: Preparing allocation for resources {} for client {}.", + module, + resources, + client + ); + module.prepareAllocation(client, resources); + } + } + + @Override + public boolean hasPreparedAllocation( + Scheduler.Client client, + Set<TCSResource<?>> resources + ) { + boolean result = true; + for (Scheduler.Module module : modules) { + result = result && module.hasPreparedAllocation(client, resources); + } + return result; + } + + @Override + public void allocationReleased(Client client, Set<TCSResource<?>> resources) { + requireNonNull(resources, "resources"); + + for (Scheduler.Module module : modules) { + LOG.debug( + "Module {}: Allocation released for resources {} for client {}.", + module, + resources, + client + ); + module.allocationReleased(client, resources); + } + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/AllocatorCommand.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/AllocatorCommand.java new file mode 100644 index 0000000..3e91e43 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/AllocatorCommand.java @@ -0,0 +1,220 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling; + +import static java.util.Objects.requireNonNull; + +import java.util.Set; +import org.opentcs.components.kernel.Scheduler.Client; +import org.opentcs.data.model.TCSResource; + +/** + * A command for the scheduler's allocation task. + */ +abstract class AllocatorCommand + implements + Comparable<AllocatorCommand> { + + /** + * The command's priority (lesser values represent higher priority). + */ + private final int priority; + /** + * The point of time at which the command was created. + */ + private final long creationTime; + /** + * The scheduler client the command is associated with. + */ + private final Client client; + + /** + * Creates a new instance. + * + * @param priority The command's priority (lesser values represent higher priority). + * @param client The scheduler client the command is associated with. + */ + private AllocatorCommand(int priority, Client client) { + this.priority = priority; + this.client = requireNonNull(client, "client"); + this.creationTime = System.currentTimeMillis(); + } + + @Override + public int compareTo(AllocatorCommand o) { + // Natural ordering of commands by (1) priority, (2) age and (3) client ID. + if (priority < o.priority) { + return -1; + } + else if (priority > o.priority) { + return 1; + } + else if (this.creationTime < o.creationTime) { + return -1; + } + else if (this.creationTime > o.creationTime) { + return 1; + } + else { + return client.getId().compareTo(o.client.getId()); + } + } + + /** + * Returns the scheduler client this command is associated with. + * + * @return The scheduler client. + */ + public Client getClient() { + return client; + } + + /** + * Indicates resources being released by a client. + */ + public static class AllocationsReleased + extends + AllocatorCommand { + + /** + * The resources being released. + */ + private final Set<TCSResource<?>> resources; + + /** + * Creates a new instance. + * + * @param client The scheduler client this command is associated with. + * @param resources The resources being released. + */ + AllocationsReleased(Client client, Set<TCSResource<?>> resources) { + super(2, client); + this.resources = requireNonNull(resources, "resources"); + } + + /** + * Returns the resources being released. + * + * @return The resources being released. + */ + public Set<TCSResource<?>> getResources() { + return resources; + } + + @Override + public String toString() { + return "AllocationsReleased{" + + "client=" + getClient() + + ", resources=" + resources + + '}'; + } + } + + /** + * Indicates the receiving task should retry to grant deferred allocations. + */ + public static class RetryAllocates + extends + AllocatorCommand { + + /** + * Creates a new instance. + * + * @param client The scheduler client this command is associated with. + */ + RetryAllocates(Client client) { + super(3, client); + } + + @Override + public String toString() { + return "RetryAllocates{" + + "client=" + getClient() + + '}'; + } + } + + /** + * Indicates the receiving task should check if a set of resources is prepared for client + * allocation. + */ + public static class CheckAllocationsPrepared + extends + AllocatorCommand { + + /** + * The resources to be checked. + */ + private final Set<TCSResource<?>> resources; + + /** + * Creates a new instance. + * + * @param client The scheduler client this command is associated with. + * @param resources The resources to be checked. + */ + CheckAllocationsPrepared(Client client, Set<TCSResource<?>> resources) { + super(4, client); + this.resources = requireNonNull(resources, "resources"); + } + + /** + * Returns the resources to be checked. + * + * @return The resources to be checked. + */ + public Set<TCSResource<?>> getResources() { + return resources; + } + + @Override + public String toString() { + return "CheckAllocationsPrepared{" + + "client=" + getClient() + + ", resources=" + resources + + '}'; + } + } + + /** + * Indicates the receiving task should try to allocate a set of resources for a client. + */ + public static class Allocate + extends + AllocatorCommand { + + /** + * The resources to be allocated. + */ + private final Set<TCSResource<?>> resources; + + /** + * Creates a new instance. + * + * @param client The scheduler client this command is associated with. + * @param resources The resources to be allocated. + */ + Allocate(Client client, Set<TCSResource<?>> resources) { + super(5, client); + this.resources = requireNonNull(resources, "resources"); + } + + /** + * Returns the resources to be allocated. + * + * @return The resources to be allocated. + */ + public Set<TCSResource<?>> getResources() { + return resources; + } + + @Override + public String toString() { + return "Allocate{" + + "client=" + getClient() + + ", resources=" + resources + + '}'; + } + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/AllocatorTask.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/AllocatorTask.java new file mode 100644 index 0000000..999d19e --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/AllocatorTask.java @@ -0,0 +1,232 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.components.kernel.Scheduler.Client; +import org.opentcs.customizations.kernel.GlobalSyncObject; +import org.opentcs.data.model.TCSResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handles regular resource allocations. + */ +class AllocatorTask + implements + Runnable { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(AllocatorTask.class); + /** + * The reservation pool. + */ + private final ReservationPool reservationPool; + /** + * Takes care of (sub)modules. + */ + private final Scheduler.Module allocationAdvisor; + /** + * Allocations deferred because they couldn't be granted, yet. + */ + private final Queue<AllocatorCommand.Allocate> deferredAllocations; + /** + * Executes tasks. + */ + private final ScheduledExecutorService kernelExecutor; + /** + * A global object to be used for synchronization within the kernel. + */ + private final Object globalSyncObject; + /** + * Describes the actual task. + */ + private final AllocatorCommand command; + + /** + * Creates a new instance. + */ + AllocatorTask( + @Nonnull + ReservationPool reservationPool, + @Nonnull + Queue<AllocatorCommand.Allocate> deferredAllocations, + @Nonnull + Scheduler.Module allocationAdvisor, + @Nonnull + ScheduledExecutorService kernelExecutor, + @Nonnull + @GlobalSyncObject + Object globalSyncObject, + @Nonnull + AllocatorCommand command + ) { + this.reservationPool = requireNonNull(reservationPool, "reservationPool"); + this.deferredAllocations = requireNonNull(deferredAllocations, "deferredAllocations"); + this.allocationAdvisor = requireNonNull(allocationAdvisor, "allocationAdvisor"); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject"); + this.command = requireNonNull(command, "command"); + } + + @Override + public void run() { + LOG.debug("Processing AllocatorCommand: {}", command); + + if (command instanceof AllocatorCommand.Allocate) { + processAllocate((AllocatorCommand.Allocate) command); + } + else if (command instanceof AllocatorCommand.RetryAllocates) { + scheduleRetryWaitingAllocations(); + } + else if (command instanceof AllocatorCommand.CheckAllocationsPrepared) { + checkAllocationsPrepared((AllocatorCommand.CheckAllocationsPrepared) command); + } + else if (command instanceof AllocatorCommand.AllocationsReleased) { + allocationsReleased((AllocatorCommand.AllocationsReleased) command); + } + else { + LOG.warn("Unhandled AllocatorCommand implementation {}, ignored.", command.getClass()); + } + } + + private void processAllocate(AllocatorCommand.Allocate command) { + if (!tryAllocate(command)) { + LOG.debug("{}: Resources unavailable, deferring allocation...", command.getClient().getId()); + deferredAllocations.add(command); + return; + } + + checkAllocationsPrepared(command.getClient(), command.getResources()); + } + + private void checkAllocationsPrepared(AllocatorCommand.CheckAllocationsPrepared command) { + checkAllocationsPrepared(command.getClient(), command.getResources()); + } + + private void checkAllocationsPrepared(Client client, Set<TCSResource<?>> resources) { + if (!allocationAdvisor.hasPreparedAllocation(client, resources)) { + LOG.debug( + "{}: Preparation of resources not yet done.", + client.getId() + ); + // XXX remember the resources a client is waiting for preparation done? + return; + } + + LOG.debug( + "Preparation of resources '{}' successful, calling back client '{}'...", + resources, + client.getId() + ); + if (!client.allocationSuccessful(resources)) { + LOG.warn( + "{}: Client didn't want allocated resources ({}), unallocating them...", + client.getId(), + resources + ); + undoAllocate(client, resources); + // See if others want the resources this one didn't, then. + scheduleRetryWaitingAllocations(); + } + // Notify modules about the changes in claimed/allocated resources for this client. + allocationAdvisor.setAllocationState( + client, + reservationPool.allocatedResources(client), + reservationPool.getClaim(client) + ); + } + + /** + * Allocates the given set of resources, if possible. + * + * @param command Describes the requested allocation. + * @return <code>true</code> if, and only if, the given resources were allocated. + */ + private boolean tryAllocate(AllocatorCommand.Allocate command) { + Scheduler.Client client = command.getClient(); + Set<TCSResource<?>> resources = command.getResources(); + + synchronized (globalSyncObject) { + if (!reservationPool.isNextInClaim(client, resources)) { + LOG.error( + "{}: Not allocating resources that are not next claimed resources: {}", + client.getId(), + resources + ); + return false; + } + + LOG.debug("{}: Checking resource availability: {}...", client.getId(), resources); + if (!reservationPool.resourcesAvailableForUser(resources, client)) { + LOG.debug("{}: Resources unavailable.", client.getId()); + return false; + } + + LOG.debug("{}: Checking if resources may be allocated...", client.getId()); + if (!allocationAdvisor.mayAllocate(client, resources)) { + LOG.debug("{}: Resources may not be allocated.", client.getId()); + return false; + } + + LOG.debug("{}: Preparing resources for allocation...", client.getId()); + allocationAdvisor.prepareAllocation(client, resources); + + LOG.debug("{}: All resources available, allocating...", client.getId()); + // Allocate resources. + for (TCSResource<?> curRes : command.getResources()) { + reservationPool.getReservationEntry(curRes).allocate(client); + } + + LOG.debug("{}: Removing resources claim: {}...", client.getId(), resources); + reservationPool.unclaim(client, resources); + + return true; + } + } + + private void allocationsReleased(AllocatorCommand.AllocationsReleased command) { + allocationAdvisor.allocationReleased(command.getClient(), command.getResources()); + } + + /** + * Unallocates the given set of resources. + * <p> + * Note that this does <em>not</em> return any previously claimed resources to the client! + * </p> + * + * @param command Describes the allocated resources. + */ + private void undoAllocate(Client client, Set<TCSResource<?>> resources) { + synchronized (globalSyncObject) { + reservationPool.free(client, resources); + } + } + + /** + * Moves all waiting allocations back into the incoming queue so they can be rechecked. + */ + private void scheduleRetryWaitingAllocations() { + for (AllocatorCommand.Allocate allocate : deferredAllocations) { + kernelExecutor.submit( + new AllocatorTask( + reservationPool, + deferredAllocations, + allocationAdvisor, + kernelExecutor, + globalSyncObject, + allocate + ) + ); + } + deferredAllocations.clear(); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/DefaultScheduler.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/DefaultScheduler.java new file mode 100644 index 0000000..96d2a8e --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/DefaultScheduler.java @@ -0,0 +1,428 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.util.Assertions.checkArgument; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.stream.Collectors; +import org.opentcs.components.kernel.ResourceAllocationException; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.customizations.ApplicationEventBus; +import org.opentcs.customizations.kernel.GlobalSyncObject; +import org.opentcs.customizations.kernel.KernelExecutor; +import org.opentcs.data.TCSObjectEvent; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.TCSResource; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.scheduling.AllocatorCommand.Allocate; +import org.opentcs.strategies.basic.scheduling.AllocatorCommand.AllocationsReleased; +import org.opentcs.strategies.basic.scheduling.AllocatorCommand.CheckAllocationsPrepared; +import org.opentcs.strategies.basic.scheduling.AllocatorCommand.RetryAllocates; +import org.opentcs.util.event.EventBus; +import org.opentcs.util.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implements a basic simple scheduler strategy for resources used by vehicles, preventing + * collisions. + */ +public class DefaultScheduler + implements + Scheduler, + EventHandler { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(DefaultScheduler.class); + /** + * Takes care of modules. + */ + private final Module allocationAdvisor; + /** + * The reservation pool. + */ + private final ReservationPool reservationPool; + /** + * Allocations deferred because they couldn't be granted, yet. + */ + private final Queue<AllocatorCommand.Allocate> deferredAllocations = new LinkedBlockingQueue<>(); + /** + * Executes scheduling tasks. + */ + private final ScheduledExecutorService kernelExecutor; + /** + * The kernel's event bus. + */ + private final EventBus eventBus; + /** + * A global object to be used for synchronization within the kernel. + */ + private final Object globalSyncObject; + /** + * Allocations that are scheduled for execution on the kernel executor. + */ + private final Map<Client, List<Future<?>>> allocateFutures = new HashMap<>(); + /** + * Indicates whether this component is enabled. + */ + private boolean initialized; + + /** + * Creates a new BasicScheduler instance. + * + * @param allocationAdvisor Takes care of modules. + * @param reservationPool The reservation pool to be used. + * @param kernelExecutor Executes scheduling tasks. + * @param eventBus The kernel's event bus. + * @param globalSyncObject The kernel threads' global synchronization object. + */ + @Inject + public DefaultScheduler( + AllocationAdvisor allocationAdvisor, + ReservationPool reservationPool, + @KernelExecutor + ScheduledExecutorService kernelExecutor, + @ApplicationEventBus + EventBus eventBus, + @GlobalSyncObject + Object globalSyncObject + ) { + this.allocationAdvisor = requireNonNull(allocationAdvisor, "allocationAdvisor"); + this.reservationPool = requireNonNull(reservationPool, "reservationPool"); + this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor"); + this.eventBus = requireNonNull(eventBus, "eventBus"); + this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + reservationPool.clear(); + allocationAdvisor.initialize(); + + eventBus.subscribe(this); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + eventBus.unsubscribe(this); + + allocationAdvisor.terminate(); + + initialized = false; + } + + @Override + public void claim(Client client, List<Set<TCSResource<?>>> resources) { + requireNonNull(client, "client"); + requireNonNull(resources, "resources"); + + synchronized (globalSyncObject) { + reservationPool.setClaim(client, resources); + + allocationAdvisor.setAllocationState( + client, + reservationPool.allocatedResources(client), + resources + ); + } + } + + @Override + public void allocate(Client client, Set<TCSResource<?>> resources) { + requireNonNull(client, "client"); + requireNonNull(resources, "resources"); + + synchronized (globalSyncObject) { + checkArgument( + reservationPool.isNextInClaim(client, resources), + "Not the next claimed resources: %s", + resources + ); + + Future<?> allocateFuture = kernelExecutor.submit( + new AllocatorTask( + reservationPool, + deferredAllocations, + allocationAdvisor, + kernelExecutor, + globalSyncObject, + new Allocate(client, resources) + ) + ); + + // Remember the allocate future in case we need to cancel it. + addAllocateFuture(client, allocateFuture); + + // Clean up the collection of allocate futures and remove futures that have already been + // completed. This could also be done in other places, but doing it for every new allocation + // should be sufficient. + removeCompletedAllocateFutures(client); + } + } + + @Override + public boolean mayAllocateNow(Client client, Set<TCSResource<?>> resources) { + requireNonNull(client, "client"); + requireNonNull(resources, "resources"); + + synchronized (globalSyncObject) { + return reservationPool.resourcesAvailableForUser(resources, client); + } + } + + @Override + public void allocateNow(Client client, Set<TCSResource<?>> resources) + throws ResourceAllocationException { + requireNonNull(client, "client"); + requireNonNull(resources, "resources"); + + synchronized (globalSyncObject) { + if (mayAllocateNow(client, resources)) { + LOG.debug("{}: Allocating immediately: {}", client.getId(), resources); + for (TCSResource<?> curResource : resources) { + reservationPool.getReservationEntry(curResource).allocate(client); + } + } + else { + throw new ResourceAllocationException( + String.format( + "%s: Requested resources not available for allocation: %s", + client.getId(), + resources + ) + ); + } + } + } + + @Override + public void free(Client client, Set<TCSResource<?>> resources) { + requireNonNull(client, "client"); + requireNonNull(resources, "resources"); + + synchronized (globalSyncObject) { + LOG.debug("{}: Releasing resources: {}", client.getId(), resources); + reservationPool.free(client, resources); + + // Check which resources are now completely free + Set<TCSResource<?>> completelyFreeResources = resources.stream() + .filter(resource -> reservationPool.getReservationEntry(resource).isFree()) + .collect(Collectors.toCollection(HashSet::new)); + new AllocatorTask( + reservationPool, + deferredAllocations, + allocationAdvisor, + kernelExecutor, + globalSyncObject, + new AllocationsReleased(client, completelyFreeResources) + ).run(); + } + kernelExecutor.submit( + new AllocatorTask( + reservationPool, + deferredAllocations, + allocationAdvisor, + kernelExecutor, + globalSyncObject, + new RetryAllocates(client) + ) + ); + } + + @Override + public void freeAll(Client client) { + requireNonNull(client, "client"); + + synchronized (globalSyncObject) { + Set<TCSResource<?>> freedResources = reservationPool.allocatedResources(client); + + LOG.debug("{}: Releasing all resources...", client.getId()); + reservationPool.freeAll(client); + clearPendingAllocations(client); + + new AllocatorTask( + reservationPool, + deferredAllocations, + allocationAdvisor, + kernelExecutor, + globalSyncObject, + new AllocationsReleased(client, freedResources) + ).run(); + } + kernelExecutor.submit( + new AllocatorTask( + reservationPool, + deferredAllocations, + allocationAdvisor, + kernelExecutor, + globalSyncObject, + new RetryAllocates(client) + ) + ); + } + + @Override + public void clearPendingAllocations(Client client) { + requireNonNull(client, "client"); + synchronized (globalSyncObject) { + LOG.debug("{}: Clearing pending allocation requests...", client.getId()); + deferredAllocations.removeIf(allocate -> client.equals(allocate.getClient())); + cancelPendingAllocateFutures(client); + } + } + + @Override + public void reschedule() { + new AllocatorTask( + reservationPool, + deferredAllocations, + allocationAdvisor, + kernelExecutor, + globalSyncObject, + new RetryAllocates(new DummyClient()) + ).run(); + } + + @Override + public Map<String, Set<TCSResource<?>>> getAllocations() { + synchronized (globalSyncObject) { + return reservationPool.getAllocations(); + } + } + + @Override + public void preparationSuccessful( + @Nonnull + Module module, + @Nonnull + Client client, + @Nonnull + Set<TCSResource<?>> resources + ) { + requireNonNull(module, "module"); + requireNonNull(client, "client"); + requireNonNull(resources, "resources"); + + new AllocatorTask( + reservationPool, + deferredAllocations, + allocationAdvisor, + kernelExecutor, + globalSyncObject, + new CheckAllocationsPrepared(client, resources) + ).run(); + } + + @Override + public void onEvent(Object event) { + if (!(event instanceof TCSObjectEvent)) { + return; + } + + TCSObjectEvent tcsObjectEvent = (TCSObjectEvent) event; + if (tcsObjectEvent.getType() != TCSObjectEvent.Type.OBJECT_MODIFIED + || !(tcsObjectEvent.getCurrentObjectState() instanceof Vehicle)) { + return; + } + + // If the vehicle was unpaused, trigger a scheduling run in case the vehicle is waiting for + // resources. + if (((Vehicle) tcsObjectEvent.getPreviousObjectState()).isPaused() + && !((Vehicle) tcsObjectEvent.getCurrentObjectState()).isPaused()) { + reschedule(); + } + } + + private void addAllocateFuture(Client client, Future<?> allocateFuture) { + if (!allocateFutures.containsKey(client)) { + allocateFutures.put(client, new ArrayList<>()); + } + + allocateFutures.get(client).add(allocateFuture); + } + + private void removeCompletedAllocateFutures(Client client) { + if (!allocateFutures.containsKey(client)) { + return; + } + + allocateFutures.get(client).removeAll( + allocateFutures.get(client).stream() + .filter(future -> future.isDone()) + .collect(Collectors.toList()) + ); + } + + private void cancelPendingAllocateFutures(Client client) { + if (!allocateFutures.containsKey(client)) { + return; + } + + allocateFutures.get(client).stream() + .filter(future -> !future.isDone()) + .forEach(future -> future.cancel(false)); + } + + /** + * A dummy client for cases in which we need to provide a client but do not have a real one. + */ + private static class DummyClient + implements + Scheduler.Client { + + /** + * Creates a new instance. + */ + DummyClient() { + } + + @Override + public String getId() { + return "DefaultScheduler-DummyClient"; + } + + @Override + public TCSObjectReference<Vehicle> getRelatedVehicle() { + return null; + } + + @Override + public boolean allocationSuccessful(Set<TCSResource<?>> resources) { + return false; + } + + @Override + public void allocationFailed(Set<TCSResource<?>> resources) { + } + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/DummyScheduler.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/DummyScheduler.java new file mode 100644 index 0000000..60ebeeb --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/DummyScheduler.java @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling; + +import static java.util.Objects.requireNonNull; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.data.model.TCSResource; + +/** + * A <code>Scheduler</code> implementation that does not really do any resource management - all + * allocations are granted immediately without checking. + */ +public class DummyScheduler + implements + Scheduler { + + /** + * Executes our <code>CallbackTask</code>s. + */ + private ExecutorService callbackExecutor; + /** + * Indicates whether this component is enabled. + */ + private boolean initialized; + + /** + * Creates a new DummyScheduler. + */ + public DummyScheduler() { + } + + @Override + public void initialize() { + callbackExecutor = Executors.newSingleThreadExecutor(); + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + callbackExecutor.shutdown(); + initialized = false; + } + + @Override + public void claim(Client client, List<Set<TCSResource<?>>> resourceSequence) { + } + + @Override + public void allocate(Client resourceUser, Set<TCSResource<?>> resources) { + requireNonNull(resourceUser, "resourceUser"); + requireNonNull(resources, "resources"); + // Just schedule the callback for successful allocation. + callbackExecutor.execute(new CallbackTask(resourceUser, resources)); + // Don't do anything else - this is a dummy, after all. + } + + @Override + public void free(Client resourceUser, Set<TCSResource<?>> resources) { + } + + @Override + public void freeAll(Client client) { + } + + @Override + public boolean mayAllocateNow(Client resourceUser, Set<TCSResource<?>> resources) { + return true; + } + + @Override + public void allocateNow(Client resourceUser, Set<TCSResource<?>> resources) { + } + + @Override + public void clearPendingAllocations(Client client) { + } + + @Override + public void reschedule() { + } + + @Override + public void preparationSuccessful(Module module, Client client, Set<TCSResource<?>> resources) { + } + + @Override + public Map<String, Set<TCSResource<?>>> getAllocations() { + return new HashMap<>(); + } + + /** + * A task that merely calls back <code>ResourceUser</code>s. + */ + private static class CallbackTask + implements + Runnable { + + /** + * The client to call back. + */ + private final Client client; + /** + * The resources the client is asking for. + */ + private final Set<TCSResource<?>> resources; + + /** + * Creates a new CallbackTask. + * + * @param newClient The client to call back. + * @param newResources The resources the client is asking for. + */ + CallbackTask(Client newClient, Set<TCSResource<?>> newResources) { + client = newClient; + resources = newResources; + } + + @Override + public void run() { + client.allocationSuccessful(resources); + } + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/ReservationEntry.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/ReservationEntry.java new file mode 100644 index 0000000..d2724ee --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/ReservationEntry.java @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling; + +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; + +import org.opentcs.components.kernel.Scheduler.Client; +import org.opentcs.data.model.TCSResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Contains reservation information for a resource - a reference to the + * <code>ResourceUser</code> currently holding the resource and a counter + * for how many times the <code>ResouceUser</code> has allocated the resource. + */ +public class ReservationEntry { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ReservationEntry.class); + /** + * Instance of resource that vehicle may claim for exclusive usage. + */ + private final TCSResource<?> resource; + /** + * The client for which the resource is currently reserved. + */ + private Client client; + /** + * The reservation counter. + * With every allocation the counter will be incremented, with every call to <code>free()</code> + * it will be decremented. + */ + private int counter; + + /** + * Creates a new instance. + * + * @param reqResource The resource. + */ + public ReservationEntry(final TCSResource<?> reqResource) { + this.resource = requireNonNull(reqResource, "reqResource"); + } + + /** + * Returns the resource. + * + * @return The resource. + */ + public TCSResource<?> getResource() { + return resource; + } + + /** + * Returns the client currently allocating the resource. + * + * @return The client currently allocating the resource, or <code>null</code>, if the resource + * isn't currently allocated. + */ + public Client getClient() { + return client; + } + + /** + * Reserves the resource for the given client. + * Increments the reservation counter for the resource if the user has already allocated this + * resource before. + * + * @param client The allocating client. + */ + void allocate(Client client) { + if (this.client == null) { + LOG.debug("Allocating resource {} for client {}", resource, client.getId()); + this.client = client; + } + else if (this.client != client) { + // The resource is already allocated by someone else - may not happen. + throw new IllegalStateException( + "'" + client + "' tried to allocate resource allocated by " + + this.client + ); + } + else { + LOG.debug( + "Incrementing allocation counter for resource {}; client: {}", + resource, + client.getId() + ); + } + counter++; + } + + /** + * Deallocates the resource once, i.e. decrements the allocation counter. + * If the counter is decremented to zero, the resource is freed and the reference to the client + * is set to <code>null</code>. + */ + void free() { + checkState(counter > 0, "counter is already less than 1"); + counter--; + if (counter == 0) { + client = null; + } + } + + /** + * Deallocates the resource completely, i.e. set the allocation counter to zero and the client + * to <code>null</code>. + */ + void freeCompletely() { + counter = 0; + client = null; + } + + /** + * Checks if the resource is currently not allocated by anyone. + * + * @return <code>true</code> if, and only if, the resource is not currently + * allocated by anyone. + */ + boolean isFree() { + return (client == null) && (counter == 0); + } + + /** + * Checks if the resource is currently allocated by the given client. + * + * @param client The client. + * @return <code>true</code> if, and only if, the resource is currently allocated by the given + * client. + */ + boolean isAllocatedBy(Client client) { + return this.client == client; + } + + @Override + public String toString() { + return "ReservationEntry{" + + "resource=" + resource + + ", client=" + client + + ", counter=" + counter + + '}'; + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/ReservationPool.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/ReservationPool.java new file mode 100644 index 0000000..4c75287 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/ReservationPool.java @@ -0,0 +1,296 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Queue; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.data.model.TCSResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + */ +public class ReservationPool { + + /** + * This class's Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(ReservationPool.class); + /** + * All claims. + */ + private final Map<Scheduler.Client, Queue<Set<TCSResource<?>>>> claimsByClient + = new HashMap<>(); + /** + * <code>ReservationEntry</code> instances for each <code>TCSResource</code>. + */ + private final Map<TCSResource<?>, ReservationEntry> reservations = new HashMap<>(); + + /** + * Creates a new instance. + */ + @Inject + public ReservationPool() { + } + + /** + * Returns a reservation entry for the given resource. + * + * @param resource The resource for which to return the reservation entry. + * @return The reservation entry for the given resource. + */ + @Nonnull + public ReservationEntry getReservationEntry(TCSResource<?> resource) { + requireNonNull(resource, "resource"); + + ReservationEntry entry = reservations.get(resource); + if (entry == null) { + entry = new ReservationEntry(resource); + reservations.put(resource, entry); + } + return entry; + } + + /** + * Returns the sequence of resource sets claimed by the given client. + * + * @param client The client. + * @return The sequence of resource sets claimed by the given client. + */ + @Nonnull + public List<Set<TCSResource<?>>> getClaim( + @Nonnull + Scheduler.Client client + ) { + requireNonNull(client, "client"); + + return claimsByClient.getOrDefault(client, new ArrayDeque<>()).stream() + .map(resourceSet -> Set.copyOf(resourceSet)) + .collect(Collectors.toList()); + } + + /** + * Sets the sequence of claimed resource sets for the given client. + * + * @param client The client. + * @param resources The sequence of claimed resources. + */ + public void setClaim( + @Nonnull + Scheduler.Client client, + @Nonnull + List<Set<TCSResource<?>>> resources + ) { + requireNonNull(client, "client"); + requireNonNull(resources, "resources"); + + claimsByClient.put(client, new ArrayDeque<>(resources)); + } + + /** + * Removes the given resource set from the head of the sequence of claimed resource sets for the + * given client. + * + * @param client The client. + * @param resources The resource set to be removed from the head of the client's claim sequence. + * @throws IllegalArgumentException If the given resource set is not the head of the client's + * claim sequence. + */ + public void unclaim( + @Nonnull + Scheduler.Client client, + @Nonnull + Set<TCSResource<?>> resources + ) + throws IllegalArgumentException { + requireNonNull(client, "client"); + requireNonNull(resources, "resources"); + + if (!claimsByClient.containsKey(client) || claimsByClient.get(client).isEmpty()) { + return; + } + + if (!isNextInClaim(client, resources)) { + throw new IllegalArgumentException( + String.format( + "Resources to unclaim and head of claimed resource don't match: %s != %s", + resources, + claimsByClient.get(client).peek() + ) + ); + } + + claimsByClient.get(client).remove(); + } + + /** + * Checks whether the given resource set is at the head of the given client's claim sequence. + * + * @param client The client. + * @param resources The resources to be checked. + * @return <code>true</code> if, and only if, the given resource set is at the head of the given + * client's claim sequence. + */ + public boolean isNextInClaim( + @Nonnull + Scheduler.Client client, + @Nonnull + Set<TCSResource<?>> resources + ) { + requireNonNull(client, "client"); + requireNonNull(resources, "resources"); + + if (!claimsByClient.containsKey(client) || claimsByClient.get(client).isEmpty()) { + return false; + } + + if (!Objects.equals(resources, claimsByClient.get(client).peek())) { + return false; + } + + return true; + } + + /** + * Returns all resources allocated by the given client. + * + * @param client The client for which to return all allocated resources. + * @return All resources allocated by the given client. + */ + @Nonnull + public Set<TCSResource<?>> allocatedResources( + @Nonnull + Scheduler.Client client + ) { + requireNonNull(client, "client"); + + return reservations.entrySet().stream() + .filter(entry -> entry.getValue().isAllocatedBy(client)) + .map(entry -> entry.getKey()) + .collect(Collectors.toSet()); + } + + /** + * Checks if all resources in the given set of resources are be available for the given client. + * + * @param resources The set of resources to be checked. + * @param client The client for which to check. + * @return <code>true</code> if, and only if, all resources in the given set + * are available for the given client. + */ + public boolean resourcesAvailableForUser( + @Nonnull + Set<TCSResource<?>> resources, + @Nonnull + Scheduler.Client client + ) { + requireNonNull(resources, "resources"); + requireNonNull(client, "client"); + + for (TCSResource<?> curResource : resources) { + // Check if the resource is available. + ReservationEntry entry = getReservationEntry(curResource); + if (!entry.isFree() && !entry.isAllocatedBy(client)) { + LOG.debug( + "{}: Resource {} unavailable, reserved by {}", + client.getId(), + curResource.getName(), + entry.getClient().getId() + ); + return false; + } + } + return true; + } + + public void free( + @Nonnull + Scheduler.Client client, + @Nonnull + Set<TCSResource<?>> resources + ) { + requireNonNull(client, "client"); + requireNonNull(resources, "resources"); + + LOG.debug("{}: Releasing resources: {}", client.getId(), resources); + for (TCSResource<?> curResource : getFreeableResources(resources, client)) { + getReservationEntry(curResource).free(); + } + } + + public void freeAll( + @Nonnull + Scheduler.Client client + ) { + requireNonNull(client, "client"); + + reservations.values().stream() + .filter(reservationEntry -> reservationEntry.isAllocatedBy(client)) + .forEach(reservationEntry -> reservationEntry.freeCompletely()); + } + + @Nonnull + public Map<String, Set<TCSResource<?>>> getAllocations() { + final Map<String, Set<TCSResource<?>>> result = new HashMap<>(); + for (Map.Entry<TCSResource<?>, ReservationEntry> curEntry : reservations.entrySet()) { + final TCSResource<?> curResource = curEntry.getKey(); + final Scheduler.Client curUser = curEntry.getValue().getClient(); + if (curUser != null) { + Set<TCSResource<?>> userResources = result.get(curUser.getId()); + if (userResources == null) { + userResources = new HashSet<>(); + } + userResources.add(curResource); + result.put(curUser.getId(), userResources); + } + } + return result; + } + + public void clear() { + claimsByClient.clear(); + reservations.clear(); + } + + /** + * Returns a set of resources that is a subset of the given set of resources and is reserved/could + * be released by the given client. + * + * @param resources The set of resources to be filtered for resources that could be released. + * @param client The client that should be able to release the returned resources. + * @return A set of resources that is a subset of the given set of resources and is reserved/could + * be released by the given client. + */ + @Nonnull + private Set<TCSResource<?>> getFreeableResources( + @Nonnull + Set<TCSResource<?>> resources, + @Nonnull + Scheduler.Client client + ) { + // Make sure we're freeing only resources that are allocated by us. + final Set<TCSResource<?>> freeableResources = new HashSet<>(); + for (TCSResource<?> curRes : resources) { + ReservationEntry entry = getReservationEntry(curRes); + if (!entry.isAllocatedBy(client)) { + LOG.warn("{}: Freed resource not reserved: {}, entry: {}", client.getId(), curRes, entry); + } + else { + freeableResources.add(curRes); + } + } + return freeableResources; + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/PausedVehicleModule.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/PausedVehicleModule.java new file mode 100644 index 0000000..e36d8ef --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/PausedVehicleModule.java @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling.modules; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.List; +import java.util.Set; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.customizations.kernel.GlobalSyncObject; +import org.opentcs.data.model.TCSResource; +import org.opentcs.data.model.Vehicle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Allows allocations for vehicles that are not paused only. + * <p> + * Note that this module assumes that a client's {@link Scheduler.Client#getId()} returns the name + * of a vehicle. + * </p> + */ +public class PausedVehicleModule + implements + Scheduler.Module { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PausedVehicleModule.class); + /** + * The object service. + */ + private final TCSObjectService objectService; + /** + * A global object to be used for synchronization within the kernel. + */ + private final Object globalSyncObject; + /** + * Whether this module is initialized. + */ + private boolean initialized; + + @Inject + public PausedVehicleModule( + @Nonnull + TCSObjectService objectService, + @Nonnull + @GlobalSyncObject + Object globalSyncObject + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + initialized = false; + } + + @Override + public void setAllocationState( + Scheduler.Client client, + Set<TCSResource<?>> alloc, + List<Set<TCSResource<?>>> remainingClaim + ) { + } + + @Override + public boolean mayAllocate( + Scheduler.Client client, + Set<TCSResource<?>> resources + ) { + requireNonNull(client, "client"); + requireNonNull(resources, "resources"); + + synchronized (globalSyncObject) { + Vehicle vehicle = objectService.fetchObject(Vehicle.class, client.getId()); + + if (vehicle == null) { + LOG.debug("Client '{}' is not a vehicle; not interfering with allocation.", client.getId()); + return true; + } + if (!vehicle.isPaused()) { + return true; + } + + LOG.debug("Not allowing allocation for paused vehicle '{}'.", client.getId()); + return false; + } + } + + @Override + public void prepareAllocation( + Scheduler.Client client, + Set<TCSResource<?>> resources + ) { + } + + @Override + public boolean hasPreparedAllocation( + Scheduler.Client client, + Set<TCSResource<?>> resources + ) { + return true; + } + + @Override + public void allocationReleased( + Scheduler.Client client, + Set<TCSResource<?>> resources + ) { + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/SameDirectionBlockModule.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/SameDirectionBlockModule.java new file mode 100644 index 0000000..603966b --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/SameDirectionBlockModule.java @@ -0,0 +1,391 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling.modules; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.components.kernel.Scheduler.PROPKEY_BLOCK_ENTRY_DIRECTION; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Queue; +import java.util.Set; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.components.kernel.services.InternalPlantModelService; +import org.opentcs.customizations.kernel.GlobalSyncObject; +import org.opentcs.data.model.Block; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.TCSResource; +import org.opentcs.strategies.basic.scheduling.ReservationPool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Checks if the resources a client may allocate are part of a + * {@link Block.Type#SAME_DIRECTION_ONLY} block and whether the client is allowed to drive along + * the block in the requested direction. + */ +public class SameDirectionBlockModule + implements + Scheduler.Module { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(SameDirectionBlockModule.class); + /** + * The reservation pool. + */ + private final ReservationPool reservationPool; + /** + * The plant model service. + */ + private final InternalPlantModelService plantModelService; + /** + * The permissions for all {@link Block.Type#SAME_DIRECTION_ONLY} blocks in a plant model. + */ + private final Map<Block, BlockPermission> permissions = new HashMap<>(); + /** + * A global object to be used for synchronization within the kernel. + */ + private final Object globalSyncObject; + /** + * Whether this module is initialized. + */ + private boolean initialized; + + @Inject + public SameDirectionBlockModule( + @Nonnull + ReservationPool reservationPool, + @Nonnull + InternalPlantModelService plantModelService, + @GlobalSyncObject + Object globalSyncObject + ) { + this.reservationPool = requireNonNull(reservationPool, "reservationPool"); + this.plantModelService = requireNonNull(plantModelService, "plantModelService"); + this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + Set<Block> blocks = plantModelService.fetchObjects(Block.class); + for (Block block : blocks) { + if (block.getType() == Block.Type.SAME_DIRECTION_ONLY) { + permissions.put(block, new BlockPermission(block)); + } + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + permissions.clear(); + + initialized = false; + } + + @Override + public void setAllocationState( + Scheduler.Client client, + Set<TCSResource<?>> alloc, + List<Set<TCSResource<?>>> remainingClaim + ) { + } + + @Override + public boolean mayAllocate(Scheduler.Client client, Set<TCSResource<?>> resources) { + requireNonNull(client, "client"); + requireNonNull(resources, "resources"); + + synchronized (globalSyncObject) { + // Other modules may prevented the last allocation, discard any previous requests. + discardPreviousRequests(); + + Set<Block> blocks = filterBlocksContainingResources( + resources, + Block.Type.SAME_DIRECTION_ONLY + ); + if (blocks.isEmpty()) { + LOG.debug("{}: No blocks to be checked, allocation allowed.", client.getId()); + return true; + } + + Path path = selectPath(resources); + if (path == null) { + // If there's no path in the requested resources the vehicle won't move and already has + // permission to be in the block(s). + LOG.debug("{}: No path in resources, allocation allowed.", client.getId()); + return true; + } + + LOG.debug("{}: Checking resource availability: {}", client.getId(), resources); + if (!checkBlockEntryPermissions( + client, + blocks, + path.getProperties().getOrDefault(PROPKEY_BLOCK_ENTRY_DIRECTION, path.getName()) + )) { + LOG.debug("{}: Resources unavailable.", client.getId()); + return false; + } + + LOG.debug("{}: Resources available, allocation allowed.", client.getId()); + return true; + } + } + + @Override + public void prepareAllocation(Scheduler.Client client, Set<TCSResource<?>> resources) { + permissions.values().forEach(permission -> permission.permitPendingRequests()); + } + + @Override + public boolean hasPreparedAllocation(Scheduler.Client client, Set<TCSResource<?>> resources) { + return permissions.values().stream().noneMatch(BlockPermission::hasPendingRequests); + } + + @Override + public void allocationReleased(Scheduler.Client client, Set<TCSResource<?>> resources) { + requireNonNull(client, "client"); + requireNonNull(resources, "resources"); + + synchronized (globalSyncObject) { + for (Map.Entry<Block, BlockPermission> entry : permissions.entrySet()) { + Block block = entry.getKey(); + BlockPermission permission = entry.getValue(); + + if (!permission.isPermissionGranted(client)) { + continue; + } + + if (blockResourcesAllocatedByClient(block, client)) { + continue; + } + + // The client released resources and does no longer hold any resources of this block. + // We don't need permissions any more. + permission.removePermissionFor(client); + } + } + } + + private void discardPreviousRequests() { + LOG.debug("Discarding all pending requests..."); + permissions.values().forEach(permission -> permission.clearPendingRequests()); + } + + private Set<Block> filterBlocksContainingResources( + Set<TCSResource<?>> resources, + Block.Type type + ) { + Set<Block> result = new HashSet<>(); + Set<Block> blocks = plantModelService.fetchObjects( + Block.class, + block -> block.getType() == type + ); + for (TCSResource<?> resource : resources) { + for (Block block : blocks) { + if (block.getMembers().contains(resource.getReference())) { + result.add(block); + } + } + } + return result; + } + + @Nullable + private Path selectPath(Set<TCSResource<?>> resources) { + for (TCSResource<?> resource : resources) { + if (resource instanceof Path) { + return ((Path) resource); + } + } + + return null; + } + + private boolean checkBlockEntryPermissions( + Scheduler.Client client, + Set<Block> blocks, + String entryDirection + ) { + LOG.debug( + "{}: Checking entry permissions for blocks '{}' with entry direction '{}'.", + client.getId(), + entryDirection + ); + boolean entryPermissible = true; + for (Block block : blocks) { + entryPermissible &= permissions.get(block).enqueueRequest(client, entryDirection); + } + + return entryPermissible; + } + + private boolean blockResourcesAllocatedByClient(Block block, Scheduler.Client client) { + Set<Block> clientBlocks + = filterBlocksContainingResources( + reservationPool.allocatedResources(client), + Block.Type.SAME_DIRECTION_ONLY + ); + return clientBlocks.contains(block); + } + + /** + * Manages the clients that are permitted to drive along a block by considering the direction + * clients request to enter the block. + */ + private class BlockPermission { + + /** + * The block to manage permissions for. + */ + private final Block block; + /** + * The clients permitted to drive along the block. + */ + private final Set<Scheduler.Client> clients = new HashSet<>(); + /** + * The direction vehicles are allowed to enter the block. + */ + @Nullable + private String entryDirection; + /** + * The queue of pending permission requests. + */ + private final Queue<PermissionRequest> pendingRequests = new ArrayDeque<>(); + + BlockPermission(Block block) { + this.block = requireNonNull(block, "block"); + } + + public void permitPendingRequests() { + while (hasPendingRequests()) { + PermissionRequest request = pendingRequests.poll(); + + if (clientAlreadyInBlock(request.getClient())) { + LOG.debug( + "Permission for block {} already granted to {}.", + block.getName(), + request.getClient().getId() + ); + } + else if (entryPermissible(request.getEntryDirection())) { + clients.add(request.getClient()); + this.entryDirection = request.getEntryDirection(); + LOG.debug( + "Permission for block {} granted to {} (entryDirection={}).", + block.getName(), + request.getClient().getId(), + request.getEntryDirection() + ); + } + } + } + + public boolean enqueueRequest(Scheduler.Client client, String entryDirection) { + if (clientAlreadyInBlock(client) + || entryPermissible(entryDirection)) { + LOG.debug( + "Enqueuing permission request for block {} to {} with entry direction '{}'.", + block.getName(), + client.getId(), + entryDirection + ); + pendingRequests.add(new PermissionRequest(client, entryDirection)); + return true; + } + + LOG.debug( + "Client {} not permissible to block {} with entry direction '{}' (!= '{}').", + client.getId(), + block.getName(), + entryDirection, + this.entryDirection + ); + return false; + } + + public void clearPendingRequests() { + pendingRequests.clear(); + } + + public void removePermissionFor(Scheduler.Client client) { + clients.remove(client); + + if (clients.isEmpty()) { + entryDirection = null; + } + } + + public boolean isPermissionGranted(Scheduler.Client client) { + return clients.contains(client); + } + + private boolean hasPendingRequests() { + return !pendingRequests.isEmpty(); + } + + private boolean clientAlreadyInBlock(Scheduler.Client client) { + return isPermissionGranted(client); + } + + private boolean entryPermissible(String entryDirection) { + return this.entryDirection == null + || Objects.equals(this.entryDirection, entryDirection); + } + } + + private class PermissionRequest { + + /** + * The requesting client. + */ + private final Scheduler.Client client; + /** + * The entry direction permission is requested for. + */ + private final String entryDirection; + + /** + * Creates a new instance. + * + * @param client The requesting client. + * @param entryDirection The entry direction permission is requested for. + * @param blocks The blocks the client requests permission for. + */ + PermissionRequest(Scheduler.Client client, String entryDirection) { + this.client = requireNonNull(client, "client"); + this.entryDirection = requireNonNull(entryDirection, "entryDirection"); + } + + public Scheduler.Client getClient() { + return client; + } + + public String getEntryDirection() { + return entryDirection; + } + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/SingleVehicleBlockModule.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/SingleVehicleBlockModule.java new file mode 100644 index 0000000..f552371 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/SingleVehicleBlockModule.java @@ -0,0 +1,206 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling.modules; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.components.kernel.services.InternalPlantModelService; +import org.opentcs.customizations.kernel.GlobalSyncObject; +import org.opentcs.data.model.Block; +import org.opentcs.data.model.TCSResource; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.strategies.basic.scheduling.ReservationPool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Checks if the resources a client may allocate are part of a + * {@link Block.Type#SINGLE_VEHICLE_ONLY} block and whether the expanded resources are all available + * to the client. + */ +public class SingleVehicleBlockModule + implements + Scheduler.Module { + + /** + * This class's logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(SingleVehicleBlockModule.class); + /** + * The reservation pool. + */ + private final ReservationPool reservationPool; + /** + * The plant model service. + */ + private final InternalPlantModelService plantModelService; + /** + * A global object to be used for synchronization within the kernel. + */ + private final Object globalSyncObject; + /** + * Whether this module is initialized. + */ + private boolean initialized; + + @Inject + public SingleVehicleBlockModule( + @Nonnull + ReservationPool reservationPool, + @Nonnull + InternalPlantModelService plantModelService, + @Nonnull + @GlobalSyncObject + Object globalSyncObject + ) { + this.reservationPool = requireNonNull(reservationPool, "reservationPool"); + this.plantModelService = requireNonNull(plantModelService, "plantModelService"); + this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + initialized = false; + } + + @Override + public void setAllocationState( + Scheduler.Client client, + Set<TCSResource<?>> alloc, + List<Set<TCSResource<?>>> remainingClaim + ) { + } + + @Override + public boolean mayAllocate( + Scheduler.Client client, + Set<TCSResource<?>> resources + ) { + requireNonNull(client, "client"); + requireNonNull(resources, "resources"); + + synchronized (globalSyncObject) { + Set<Block> blocks = filterBlocksContainingResources( + resources, + Block.Type.SINGLE_VEHICLE_ONLY + ); + + if (blocks.isEmpty()) { + LOG.debug("{}: No blocks to be checked, allocation allowed.", client.getId()); + return true; + } + + Set<TCSResource<?>> resourcesExpanded = expandResources(resources); + resourcesExpanded = filterRelevantResources(resourcesExpanded, blocks); + + LOG.debug("{}: Checking resource availability: {}", client.getId(), resources); + if (!reservationPool.resourcesAvailableForUser(resourcesExpanded, client)) { + LOG.debug("{}: Resources unavailable.", client.getId()); + return false; + } + + LOG.debug("{}: Resources available, allocation allowed.", client.getId()); + return true; + } + } + + @Override + public void prepareAllocation( + Scheduler.Client client, + Set<TCSResource<?>> resources + ) { + } + + @Override + public boolean hasPreparedAllocation( + Scheduler.Client client, + Set<TCSResource<?>> resources + ) { + return true; + } + + @Override + public void allocationReleased( + Scheduler.Client client, + Set<TCSResource<?>> resources + ) { + } + + private Set<Block> filterBlocksContainingResources( + Set<TCSResource<?>> resources, + Block.Type type + ) { + Set<Block> result = new HashSet<>(); + Set<Block> blocks = plantModelService.fetchObjects( + Block.class, + block -> block.getType() == type + ); + for (TCSResource<?> resource : resources) { + for (Block block : blocks) { + if (block.getMembers().contains(resource.getReference())) { + result.add(block); + } + } + } + return result; + } + + private Set<TCSResource<?>> filterRelevantResources( + Set<TCSResource<?>> resources, + Set<Block> blocks + ) { + Set<TCSResourceReference<?>> blockResources = blocks.stream() + .flatMap(block -> block.getMembers().stream()) + .collect(Collectors.toSet()); + + return resources.stream() + .filter(resource -> blockResources.contains(resource.getReference())) + .collect(Collectors.toSet()); + } + + /** + * Returns the given set of resources after expansion (by resolution of blocks, for instance) by + * the kernel. + * + * @param resources The set of resources to be expanded. + * @return The given set of resources after expansion (by resolution of + * blocks, for instance) by the kernel. + */ + private Set<TCSResource<?>> expandResources(Set<TCSResource<?>> resources) { + requireNonNull(resources, "resources"); + // Build a set of references + Set<TCSResourceReference<?>> refs = resources.stream() + .map((resource) -> resource.getReference()) + .collect(Collectors.toSet()); + // Let the kernel expand the resources for us. + Set<TCSResource<?>> result = plantModelService.expandResources(refs); + LOG.debug("Set {} expanded to {}", resources, result); + return result; + } + +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/AreaAllocationModule.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/AreaAllocationModule.java new file mode 100644 index 0000000..6ea9cb6 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/AreaAllocationModule.java @@ -0,0 +1,165 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling.modules.areaAllocation; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.List; +import java.util.Set; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.TCSResource; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.scheduling.ReservationPool; + +/** + * Allows resource allocation only if the area related to the respective resource is free. + */ +public class AreaAllocationModule + implements + Scheduler.Module { + + private final TCSObjectService objectService; + private final ReservationPool reservationPool; + private final AreaAllocator areaAllocator; + private boolean initialized; + + /** + * Creates a new instance. + * + * @param objectService The object service to use. + * @param reservationPool Keeps track of allocated resources. + * @param areaAllocator Keeps track of allocated areas. + */ + @Inject + public AreaAllocationModule( + @Nonnull + TCSObjectService objectService, + @Nonnull + ReservationPool reservationPool, + @Nonnull + AreaAllocator areaAllocator + ) { + this.objectService = requireNonNull(objectService, "objectService"); + this.reservationPool = requireNonNull(reservationPool, "reservationPool"); + this.areaAllocator = requireNonNull(areaAllocator, "areaAllocator"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + areaAllocator.initialize(); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + areaAllocator.terminate(); + + initialized = false; + } + + @Override + public void setAllocationState( + @Nonnull + Scheduler.Client client, + @Nonnull + Set<TCSResource<?>> alloc, + @Nonnull + List<Set<TCSResource<?>>> remainingClaim + ) { + allocationChanged(client); + } + + @Override + public boolean mayAllocate( + @Nonnull + Scheduler.Client client, + @Nonnull + Set<TCSResource<?>> resources + ) { + requireNonNull(client, "client"); + requireNonNull(resources, "resources"); + + if (client.getRelatedVehicle() == null) { + return true; + } + + Vehicle vehicle = objectService.fetchObject(Vehicle.class, client.getRelatedVehicle()); + if (vehicle.getEnvelopeKey() == null) { + return true; + } + + return areaAllocator.mayAllocateAreas( + client.getRelatedVehicle(), + vehicle.getEnvelopeKey(), + resources + ); + } + + @Override + public void prepareAllocation( + @Nonnull + Scheduler.Client client, + @Nonnull + Set<TCSResource<?>> resources + ) { + } + + @Override + public boolean hasPreparedAllocation( + @Nonnull + Scheduler.Client client, + @Nonnull + Set<TCSResource<?>> resources + ) { + return true; + } + + @Override + public void allocationReleased( + @Nonnull + Scheduler.Client client, + @Nonnull + Set<TCSResource<?>> resources + ) { + allocationChanged(client); + } + + private void allocationChanged( + @Nonnull + Scheduler.Client client + ) { + requireNonNull(client, "client"); + + if (client.getRelatedVehicle() == null) { + return; + } + + Vehicle vehicle = objectService.fetchObject(Vehicle.class, client.getRelatedVehicle()); + if (vehicle.getEnvelopeKey() == null) { + return; + } + + areaAllocator.updateAllocatedAreas( + client.getRelatedVehicle(), + vehicle.getEnvelopeKey(), + reservationPool.allocatedResources(client) + ); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/AreaAllocations.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/AreaAllocations.java new file mode 100644 index 0000000..0138de5 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/AreaAllocations.java @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling.modules.areaAllocation; + +import jakarta.inject.Inject; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.locationtech.jts.geom.GeometryCollection; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Vehicle; + +/** + * A container for keeping track of areas allocated by vehicles. + */ +public class AreaAllocations { + + private final Map<TCSObjectReference<Vehicle>, GeometryCollection> allocatedAreasByVehicles + = new HashMap<>(); + + @Inject + public AreaAllocations() { + } + + /** + * Clears the area allocations for all vehicles. + */ + public void clearAreaAllocations() { + allocatedAreasByVehicles.clear(); + } + + /** + * Sets the allocation for the given vehicle to the given allocated areas, discarding any previous + * area allocation. + * + * @param vehicleRef The vehicle reference. + * @param allocatedAreas The allocated areas to set as the vehicle's current area allocation. + */ + public void setAreaAllocation( + TCSObjectReference<Vehicle> vehicleRef, + GeometryCollection allocatedAreas + ) { + allocatedAreasByVehicles.put(vehicleRef, allocatedAreas); + } + + /** + * Clears the area allocation for the given vehicle. + * + * @param vehicleRef The vehicle reference. + */ + public void clearAreaAllocation(TCSObjectReference<Vehicle> vehicleRef) { + allocatedAreasByVehicles.remove(vehicleRef); + } + + /** + * Checks if the given vehicle is allowed to allocate the given ares. + * + * @param vehicleRef The vehicle reference. + * @param requestedAreas The requested areas (to be allocated). + * @return {@code true}, if the vehicle is allowed to allocate the given areas, otherwise + * {@code false} (i.e. in case some of the reuqested areas are already allocated by other + * vehicles). + */ + public boolean isAreaAllocationAllowed( + TCSObjectReference<Vehicle> vehicleRef, + GeometryCollection requestedAreas + ) { + return allocatedAreasByVehicles.entrySet().stream() + // Only check areas allocated by vehicles other than the given vehicle. + .filter(entry -> !Objects.equals(entry.getKey(), vehicleRef)) + .noneMatch(entry -> entry.getValue().intersects(requestedAreas)); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/AreaAllocator.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/AreaAllocator.java new file mode 100644 index 0000000..d41dbd8 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/AreaAllocator.java @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling.modules.areaAllocation; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.Set; +import org.opentcs.components.Lifecycle; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.TCSResource; +import org.opentcs.data.model.Vehicle; + +/** + * Responsible for managing allocated areas. + */ +public class AreaAllocator + implements + Lifecycle { + + private final AreaProvider areaProvider; + private final AreaAllocations areaAllocations; + private boolean initialized; + + /** + * Creates a new instance. + * + * @param areaProvider Provides areas related to resources. + * @param areaAllocations Keeps track of areas allocated by vehicles. + */ + @Inject + public AreaAllocator(AreaProvider areaProvider, AreaAllocations areaAllocations) { + this.areaProvider = requireNonNull(areaProvider, "areaProvider"); + this.areaAllocations = requireNonNull(areaAllocations, "areaAllocations"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + areaProvider.initialize(); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + areaProvider.terminate(); + areaAllocations.clearAreaAllocations(); + + initialized = false; + } + + /** + * Checks if the given vehicle is allowed to allocate the areas related to the given envelope key + * and the given set of resources. + * + * @param vehicleRef The vehicle reference. + * @param envelopeKey The envelope key. + * @param resources The set of resources. + * @return {@code true}, if the areas realted to the given envelope key and the given set of + * resources are not allocated by any vehicle other than the given one, otherwise {@code false}. + */ + public boolean mayAllocateAreas( + @Nonnull + TCSObjectReference<Vehicle> vehicleRef, + @Nonnull + String envelopeKey, + @Nonnull + Set<TCSResource<?>> resources + ) { + requireNonNull(vehicleRef, "vehicleRef"); + requireNonNull(envelopeKey, "envelopeKey"); + requireNonNull(resources, "resources"); + + if (resources.isEmpty()) { + return true; + } + + return areaAllocations.isAreaAllocationAllowed( + vehicleRef, + areaProvider.getAreas(envelopeKey, resources) + ); + } + + /** + * Updates the given vehicle's allocated areas to the areas related to the given envelope key + * and the given set of resources. + * + * @param vehicleRef The vehicle reference. + * @param envelopeKey The envelope key. + * @param resources The set of resources. + */ + public void updateAllocatedAreas( + @Nonnull + TCSObjectReference<Vehicle> vehicleRef, + @Nonnull + String envelopeKey, + @Nonnull + Set<TCSResource<?>> resources + ) { + requireNonNull(vehicleRef, "vehicleRef"); + requireNonNull(envelopeKey, "envelopeKey"); + requireNonNull(resources, "resources"); + + if (resources.isEmpty()) { + areaAllocations.clearAreaAllocation(vehicleRef); + return; + } + + areaAllocations.setAreaAllocation(vehicleRef, areaProvider.getAreas(envelopeKey, resources)); + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/AreaProvider.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/AreaProvider.java new file mode 100644 index 0000000..ad3338d --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/AreaProvider.java @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling.modules.areaAllocation; + +import jakarta.annotation.Nonnull; +import java.util.Set; +import org.locationtech.jts.geom.GeometryCollection; +import org.opentcs.components.Lifecycle; +import org.opentcs.data.model.TCSResource; + +/** + * Provides areas related to resources. + */ +public interface AreaProvider + extends + Lifecycle { + + /** + * Provides the areas related to the given envelope key and the given set of resources as a + * {@link GeometryCollection}. + * + * @param envelopeKey The envelope key. + * @param resources The set of resources. + * @return The areas related to the given envelope key and the given set of resources. + */ + GeometryCollection getAreas( + @Nonnull + String envelopeKey, + @Nonnull + Set<TCSResource<?>> resources + ); +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/CachingAreaProvider.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/CachingAreaProvider.java new file mode 100644 index 0000000..35578cc --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/CachingAreaProvider.java @@ -0,0 +1,191 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling.modules.areaAllocation; + +import static java.util.Objects.requireNonNull; +import static org.opentcs.strategies.basic.scheduling.modules.areaAllocation.CustomGeometryFactory.EMPTY_GEOMETRY; + +import jakarta.annotation.Nonnull; +import jakarta.inject.Inject; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Envelope; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResource; + +/** + * An {@link AreaProvider} implementation that, upon initialization, computes and caches the areas + * for the {@link Envelope}s defined at all {@link Point}s and {@link Path}s. + */ +public class CachingAreaProvider + implements + AreaProvider { + + private final TCSObjectService objectService; + private final CustomGeometryFactory geometryFactory = new CustomGeometryFactory(); + private final Map<CacheKey, Geometry> cache = new HashMap<>(); + private boolean initialized; + + /** + * Creates a new instance. + * + * @param objectService The object service to use. + */ + @Inject + public CachingAreaProvider(TCSObjectService objectService) { + this.objectService = requireNonNull(objectService, "objectService"); + } + + @Override + public void initialize() { + if (isInitialized()) { + return; + } + + populateCache(); + + initialized = true; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void terminate() { + if (!isInitialized()) { + return; + } + + cache.clear(); + + initialized = false; + } + + @Override + public GeometryCollection getAreas( + @Nonnull + String envelopeKey, + @Nonnull + Set<TCSResource<?>> resources + ) { + requireNonNull(envelopeKey, "envelopeKey"); + requireNonNull(resources, "resources"); + + Geometry[] computedAreas = resources.stream() + .map(resource -> lookupArea(envelopeKey, resource)) + .filter(geometry -> geometry != EMPTY_GEOMETRY) + .toArray(Geometry[]::new); + + return geometryFactory.createGeometryCollection(computedAreas); + } + + private void populateCache() { + Set<Point> points = objectService.fetchObjects( + Point.class, + point -> !point.getVehicleEnvelopes().isEmpty() + ); + for (Point point : points) { + for (Map.Entry<String, Envelope> entry : point.getVehicleEnvelopes().entrySet()) { + String envelopeKey = entry.getKey(); + computeArea(envelopeKey, point) + .ifPresent(geometry -> cache.put(new CacheKey(envelopeKey, point), geometry)); + } + } + + Set<Path> paths = objectService.fetchObjects( + Path.class, + path -> !path.getVehicleEnvelopes().isEmpty() + ); + for (Path path : paths) { + for (Map.Entry<String, Envelope> entry : path.getVehicleEnvelopes().entrySet()) { + String envelopeKey = entry.getKey(); + computeArea(envelopeKey, path) + .ifPresent(geometry -> cache.put(new CacheKey(envelopeKey, path), geometry)); + } + } + } + + private Optional<Geometry> computeArea(String envelopeKey, TCSResource<?> resource) { + Map<String, Envelope> vehicleEnvelopes = extractVehicleEnvelopes(resource); + + if (!vehicleEnvelopes.containsKey(envelopeKey)) { + return Optional.empty(); + } + + Envelope envelope = vehicleEnvelopes.get(envelopeKey); + Coordinate[] coordinates = envelope.getVertices().stream() + .map(vertex -> new Coordinate(vertex.getX(), vertex.getY())) + .toArray(Coordinate[]::new); + + return Optional.of(geometryFactory.createPolygonOrEmptyGeometry(coordinates)); + } + + private Map<String, Envelope> extractVehicleEnvelopes(TCSResource<?> resource) { + if (resource instanceof Point) { + return ((Point) resource).getVehicleEnvelopes(); + } + else if (resource instanceof Path) { + return ((Path) resource).getVehicleEnvelopes(); + } + else { + return Map.of(); + } + } + + private Geometry lookupArea(String envelopeKey, TCSResource<?> resource) { + return cache.getOrDefault(new CacheKey(envelopeKey, resource), EMPTY_GEOMETRY); + } + + /** + * Combines the envelope key and the resource (for which there is a corresponding area) to be used + * as the cache's key. + */ + private static class CacheKey { + + private final String envelopeKey; + private final TCSResource<?> resource; + + /** + * Creates a new instance. + * + * @param envelopeKey The envelope key. + * @param resource The resource. + */ + CacheKey(String envelopeKey, TCSResource<?> resource) { + this.envelopeKey = requireNonNull(envelopeKey, "envelopeKey"); + this.resource = requireNonNull(resource, "resource"); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 37 * hash + Objects.hashCode(this.envelopeKey); + hash = 37 * hash + Objects.hashCode(this.resource); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof CacheKey)) { + return false; + } + + CacheKey other = (CacheKey) obj; + return Objects.equals(this.envelopeKey, other.envelopeKey) + && Objects.equals(this.resource, other.resource); + } + } +} diff --git a/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/CustomGeometryFactory.java b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/CustomGeometryFactory.java new file mode 100644 index 0000000..4071db3 --- /dev/null +++ b/opentcs-strategies-default/src/main/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/CustomGeometryFactory.java @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling.modules.areaAllocation; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Nonnull; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; +import org.opentcs.data.model.Envelope; +import org.opentcs.data.model.Path; + +/** + * A {@link GeometryFactory} extended by custom methods for creating geometries. + */ +public class CustomGeometryFactory + extends + GeometryFactory { + + /** + * A constant for an empty {@link Geometry}. + */ + public static final Geometry EMPTY_GEOMETRY = new GeometryFactory().createGeometryCollection(); + + /** + * Creates a new instance. + */ + public CustomGeometryFactory() { + } + + /** + * Creates a {@link Geometry} with the given coordinates. + * <p> + * Based on the number of given coordinates, this method returns + * <ul> + * <li>an {@link CustomGeometryFactory#EMPTY_GEOMETRY}, for 0, 1, 2 or 3 coordinates, since we + * need at least 4 coordinates for a valid polygon.</li> + * <li>a {@link Polygon}, for 4 or more coordinates.</li> + * </ul> + * <p> + * Background: In the context of {@link AreaAllocationModule}, {@link Geometry}s are created for + * {@link Envelope}s defined at {@link Point}s and {@link Path}s. These envelopes can consist + * of one or more vertices. This method is supposed to create valid geometries regardless of the + * number of vertices (which is otherwise not possible using a single method of the JTS library). + * + * @param coordinates The coordinates to create a {@link Geometry} with. + * @return A {@link Geometry} + */ + public Geometry createPolygonOrEmptyGeometry( + @Nonnull + Coordinate... coordinates + ) { + requireNonNull(coordinates, "coordinates"); + + switch (coordinates.length) { + case 0: + case 1: + case 2: + case 3: + return EMPTY_GEOMETRY; + default: + return this.createPolygon(coordinates); + } + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/AssignmentCandidateTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/AssignmentCandidateTest.java new file mode 100644 index 0000000..0ad9b7a --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/AssignmentCandidateTest.java @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.comparesEqualTo; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; + +/** + * Unit tests for {@link AssignmentCandidate}. + */ +class AssignmentCandidateTest { + + private Vehicle vehicle; + private TransportOrder transportOrder; + private List<DriveOrder> driveOrders; + + @BeforeEach + void setUp() { + vehicle = new Vehicle("vehicle1"); + Point point = new Point("point"); + DriveOrder.Destination destination = new DriveOrder.Destination(point.getReference()); + DriveOrder driveOrder = new DriveOrder(destination); + driveOrders = List.of(driveOrder); + transportOrder = new TransportOrder("transportOrder1", driveOrders); + } + + @Test + void exceptionWhenDriveOrdersEmpty() { + driveOrders = List.of(); + + Exception exception = assertThrows( + IllegalArgumentException.class, () -> { + AssignmentCandidate assignmentCandidate = new AssignmentCandidate( + vehicle, + transportOrder, + driveOrders + ); + } + ); + + assertThat(exception.getMessage(), containsString("driveOrders is empty")); + } + + @Test + void exceptionWhenRouteEmpty() { + Exception exception = assertThrows( + IllegalArgumentException.class, () -> { + AssignmentCandidate assignmentCandidate = new AssignmentCandidate( + vehicle, + transportOrder, + driveOrders + ); + } + ); + + assertThat(exception.getMessage(), containsString("a drive order's route is null")); + } + + @Test + void calculatesCompleteRoutingCosts() { + Point point1 = new Point("point1"); + Point point2 = new Point("point2"); + Route.Step step1 = new Route.Step( + null, null, point1, Vehicle.Orientation.FORWARD, 0, true, null + ); + Route.Step step2 = new Route.Step( + null, null, point2, Vehicle.Orientation.FORWARD, 0, true, null + ); + Route route1 = new Route(List.of(step1), 1234); + Route route2 = new Route(List.of(step2), 5678); + DriveOrder driveOrder1 = new DriveOrder(new DriveOrder.Destination(point1.getReference())); + DriveOrder driveOrder2 = new DriveOrder(new DriveOrder.Destination(point2.getReference())); + driveOrder1 = driveOrder1.withRoute(route1); + driveOrder2 = driveOrder2.withRoute(route2); + driveOrders = List.of(driveOrder1, driveOrder2); + + AssignmentCandidate assignmentCandidate = new AssignmentCandidate( + vehicle, + transportOrder, + driveOrders + ); + + assertThat(assignmentCandidate.getCompleteRoutingCosts(), comparesEqualTo(6912L)); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/CompositeTransportOrderSelectionFilterTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/CompositeTransportOrderSelectionFilterTest.java new file mode 100644 index 0000000..e711b21 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/CompositeTransportOrderSelectionFilterTest.java @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.selection.TransportOrderSelectionFilter; +import org.opentcs.strategies.basic.dispatching.selection.orders.CompositeTransportOrderSelectionFilter; + +/** + * Unit tests for {@link CompositeTransportOrderSelectionFilter}. + */ +class CompositeTransportOrderSelectionFilterTest { + + private static final String NAME_TRANSPORT = "Transport"; + private static final String NAME_ORDER = "Order"; + private static final String NAME_TRANSPORT_ORDER = "TransportOrder"; + private static final String NAME_RANDOM = "SomeRandomName"; + + /** + * The class to test. + */ + private CompositeTransportOrderSelectionFilter transportOrderSelectionFilter; + + private List<TransportOrder> transportOrders; + + CompositeTransportOrderSelectionFilterTest() { + } + + @BeforeEach + void setUp() { + transportOrders = new ArrayList<>(); + transportOrders.add(createTransportOrder(NAME_TRANSPORT)); + transportOrders.add(createTransportOrder(NAME_ORDER)); + transportOrders.add(createTransportOrder(NAME_TRANSPORT_ORDER)); + transportOrders.add(createTransportOrder(NAME_RANDOM)); + } + + @Test + void shouldFilterNoTransportOrders() { + Set<TransportOrderSelectionFilter> filters + = new HashSet<>( + Arrays.asList( + new RefuseAllFilter(), + new FilterIfTransportOrderNameContainsTransport(), + new FilterIfTransportOrderNameContainsOrder() + ) + ); + transportOrderSelectionFilter = new CompositeTransportOrderSelectionFilter(filters); + + long remainingTransportOrders = transportOrders.stream() + .filter(order -> transportOrderSelectionFilter.apply(order).isEmpty()) + .count(); + + assertEquals(0, remainingTransportOrders); + } + + @Test + void shouldFilterTransportOrdersContainingTransport() { + Set<TransportOrderSelectionFilter> filters + = new HashSet<>(Arrays.asList(new FilterIfTransportOrderNameContainsTransport())); + transportOrderSelectionFilter = new CompositeTransportOrderSelectionFilter(filters); + + long remainingTransportOrders = transportOrders.stream() + .filter(order -> transportOrderSelectionFilter.apply(order).isEmpty()) + .count(); + + assertEquals(2, remainingTransportOrders); + } + + @Test + void shouldFilterTransportOrdersContainingOrder() { + Set<TransportOrderSelectionFilter> filters + = new HashSet<>(Arrays.asList(new FilterIfTransportOrderNameContainsOrder())); + transportOrderSelectionFilter = new CompositeTransportOrderSelectionFilter(filters); + + long remainingTransportOrders = transportOrders.stream() + .filter(order -> transportOrderSelectionFilter.apply(order).isEmpty()) + .count(); + + assertEquals(2, remainingTransportOrders); + } + + @Test + void shouldFilterTransportOrdersContainingTransportOrOrder() { + Set<TransportOrderSelectionFilter> filters + = new HashSet<>( + Arrays.asList( + new FilterIfTransportOrderNameContainsTransport(), + new FilterIfTransportOrderNameContainsOrder() + ) + ); + transportOrderSelectionFilter = new CompositeTransportOrderSelectionFilter(filters); + + List<TransportOrder> remainingTransportOrders = transportOrders.stream() + .filter(order -> !transportOrderSelectionFilter.apply(order).isEmpty()) + .collect(Collectors.toList()); + + assertEquals(3, remainingTransportOrders.size()); + } + + private TransportOrder createTransportOrder(String name) { + return new TransportOrder(name, new ArrayList<>()); + } + + private class RefuseAllFilter + implements + TransportOrderSelectionFilter { + + @Override + public Collection<String> apply(TransportOrder t) { + return Arrays.asList("just no"); + } + } + + private class FilterIfTransportOrderNameContainsTransport + implements + TransportOrderSelectionFilter { + + @Override + public Collection<String> apply(TransportOrder t) { + return t.getName().contains("Transport") + ? new ArrayList<>() + : Arrays.asList("order name does not contain 'Transport'"); + } + } + + private class FilterIfTransportOrderNameContainsOrder + implements + TransportOrderSelectionFilter { + + @Override + public Collection<String> apply(TransportOrder t) { + return t.getName().contains("Order") + ? new ArrayList<>() + : Arrays.asList("order name does not contain 'Order'"); + } + } + +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/TransportOrderAssignmentCheckerTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/TransportOrderAssignmentCheckerTest.java new file mode 100644 index 0000000..b1746d6 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/TransportOrderAssignmentCheckerTest.java @@ -0,0 +1,188 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.opentcs.components.kernel.dipatching.TransportOrderAssignmentVeto; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; + +/** + */ +class TransportOrderAssignmentCheckerTest { + + private TCSObjectService objectService; + private OrderReservationPool orderReservationPool; + private TransportOrderAssignmentChecker checker; + private Vehicle vehicle; + private TransportOrder transportOrder; + + @BeforeEach + void setUp() { + objectService = mock(TCSObjectService.class); + orderReservationPool = new OrderReservationPool(); + checker = new TransportOrderAssignmentChecker(objectService, orderReservationPool); + + vehicle = new Vehicle("some-vehicle") + .withProcState(Vehicle.ProcState.IDLE) + .withState(Vehicle.State.IDLE) + .withIntegrationLevel(Vehicle.IntegrationLevel.TO_BE_UTILIZED) + .withCurrentPosition(new Point("some-point").getReference()) + .withOrderSequence(null); + transportOrder = new TransportOrder("some-order", new ArrayList<>()) + .withState(TransportOrder.State.DISPATCHABLE) + .withWrappingSequence(null) + .withIntendedVehicle(vehicle.getReference()); + } + + @ParameterizedTest + @EnumSource( + value = TransportOrder.State.class, + names = {"RAW", "ACTIVE", "BEING_PROCESSED", "WITHDRAWN", "FINISHED", "FAILED", "UNROUTABLE"} + ) + void onlyAcceptDispatchableOrders(TransportOrder.State orderState) { + transportOrder = transportOrder.withState(orderState); + + assertThat( + checker.checkTransportOrderAssignment(transportOrder), + is(TransportOrderAssignmentVeto.TRANSPORT_ORDER_STATE_INVALID) + ); + } + + @Test + void onlyAcceptOrdersWithoutWrappingSequence() { + transportOrder + = transportOrder.withWrappingSequence(new OrderSequence("some-seq").getReference()); + + assertThat( + checker.checkTransportOrderAssignment(transportOrder), + is(TransportOrderAssignmentVeto.TRANSPORT_ORDER_PART_OF_ORDER_SEQUENCE) + ); + } + + @Test + void onlyAcceptOrdersWithIntendedVehicle() { + transportOrder = transportOrder.withIntendedVehicle(null); + + assertThat( + checker.checkTransportOrderAssignment(transportOrder), + is(TransportOrderAssignmentVeto.TRANSPORT_ORDER_INTENDED_VEHICLE_NOT_SET) + ); + } + + @ParameterizedTest + @EnumSource( + value = Vehicle.ProcState.class, + names = {"AWAITING_ORDER", "PROCESSING_ORDER"} + ) + void onlyAcceptIntendedVehicleWithValidProcState(Vehicle.ProcState procState) { + vehicle = vehicle.withProcState(procState); + + when(objectService.fetchObject(Vehicle.class, vehicle.getReference())) + .thenReturn(vehicle); + + assertThat( + checker.checkTransportOrderAssignment(transportOrder), + is(TransportOrderAssignmentVeto.VEHICLE_PROCESSING_STATE_INVALID) + ); + } + + @ParameterizedTest + @EnumSource( + value = Vehicle.State.class, + names = {"UNKNOWN", "UNAVAILABLE", "ERROR", "EXECUTING"} + ) + void onlyAcceptIntendedVehicleWithValidState(Vehicle.State state) { + vehicle = vehicle.withState(state); + + when(objectService.fetchObject(Vehicle.class, vehicle.getReference())) + .thenReturn(vehicle); + + assertThat( + checker.checkTransportOrderAssignment(transportOrder), + is(TransportOrderAssignmentVeto.VEHICLE_STATE_INVALID) + ); + } + + @ParameterizedTest + @EnumSource( + value = Vehicle.IntegrationLevel.class, + names = {"TO_BE_IGNORED", "TO_BE_NOTICED", "TO_BE_RESPECTED"} + ) + void onlyAcceptIntendedVehicleWithValidIntegrationLevel(Vehicle.IntegrationLevel level) { + vehicle = vehicle.withIntegrationLevel(level); + + when(objectService.fetchObject(Vehicle.class, vehicle.getReference())) + .thenReturn(vehicle); + + assertThat( + checker.checkTransportOrderAssignment(transportOrder), + is(TransportOrderAssignmentVeto.VEHICLE_INTEGRATION_LEVEL_INVALID) + ); + } + + @Test + void onlyAcceptIntendedVehicleWithKnownPosition() { + vehicle = vehicle.withCurrentPosition(null); + + when(objectService.fetchObject(Vehicle.class, vehicle.getReference())) + .thenReturn(vehicle); + + assertThat( + checker.checkTransportOrderAssignment(transportOrder), + is(TransportOrderAssignmentVeto.VEHICLE_CURRENT_POSITION_UNKNOWN) + ); + } + + @Test + void onlyAcceptIntendedVehicleWithoutOrderSequence() { + vehicle = vehicle.withOrderSequence(new OrderSequence("some-seq").getReference()); + + when(objectService.fetchObject(Vehicle.class, vehicle.getReference())) + .thenReturn(vehicle); + + assertThat( + checker.checkTransportOrderAssignment(transportOrder), + is(TransportOrderAssignmentVeto.VEHICLE_PROCESSING_ORDER_SEQUENCE) + ); + } + + @Test + void onlyAcceptIntendedVehicleWithoutReservation() { + orderReservationPool.addReservation( + new TransportOrder("some-other-order", List.of()).getReference(), + vehicle.getReference() + ); + + when(objectService.fetchObject(Vehicle.class, vehicle.getReference())) + .thenReturn(vehicle); + + assertThat( + checker.checkTransportOrderAssignment(transportOrder), + is(TransportOrderAssignmentVeto.GENERIC_VETO) + ); + } + + @Test + void acceptValidOrderAndVehicle() { + // No changes to vehicle, transport order or reservation pool here. + when(objectService.fetchObject(Vehicle.class, vehicle.getReference())) + .thenReturn(vehicle); + + assertThat(checker.checkTransportOrderAssignment(transportOrder), + is(TransportOrderAssignmentVeto.NO_VETO)); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/DispatchingStatusMarkerTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/DispatchingStatusMarkerTest.java new file mode 100644 index 0000000..ff13eee --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/DispatchingStatusMarkerTest.java @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.opentcs.data.order.TransportOrderHistoryCodes.ORDER_DISPATCHING_DEFERRED; +import static org.opentcs.data.order.TransportOrderHistoryCodes.ORDER_DISPATCHING_RESUMED; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.ObjectHistory; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.phase.OrderFilterResult; + +/** + * Tests for {@link DispatchingStatusMarker}. + */ +class DispatchingStatusMarkerTest { + + private DispatchingStatusMarker dispatchingStatusMarker; + + @BeforeEach + void setUp() { + dispatchingStatusMarker = new DispatchingStatusMarker(mock(TCSObjectService.class)); + } + + @Test + void evaluateOrderAsDeferred() { + TransportOrder order = new TransportOrder("order", List.of()) + .withHistoryEntry( + new ObjectHistory.Entry(ORDER_DISPATCHING_DEFERRED, List.of("some-reason")) + ); + + assertTrue(dispatchingStatusMarker.isOrderMarkedAsDeferred(order)); + } + + @ParameterizedTest + @ValueSource(strings = {ORDER_DISPATCHING_RESUMED, "some-unrelated-history-event-code"}) + void evaluateOrderAsNotDeferred(String eventCode) { + TransportOrder order = new TransportOrder("order", List.of()) + .withHistoryEntry(new ObjectHistory.Entry(eventCode, List.of("some-reason"))); + + assertFalse(dispatchingStatusMarker.isOrderMarkedAsDeferred(order)); + } + + @Test + void evaluateOrderDeferralReasonsAsChangedForNotDeferredOrder() { + TransportOrder order = new TransportOrder("order", List.of()); + OrderFilterResult orderFilterResult = new OrderFilterResult(order, List.of("some-new-reason")); + + assertTrue(dispatchingStatusMarker.haveDeferralReasonsForOrderChanged(orderFilterResult)); + } + + @Test + void evaluateOrderDeferralReasonsAsChangedForDeferredOrder() { + TransportOrder order = new TransportOrder("order", List.of()) + .withHistoryEntry( + new ObjectHistory.Entry(ORDER_DISPATCHING_DEFERRED, List.of("some-reason")) + ); + OrderFilterResult orderFilterResult = new OrderFilterResult(order, List.of("some-new-reason")); + + assertTrue(dispatchingStatusMarker.haveDeferralReasonsForOrderChanged(orderFilterResult)); + } + + @Test + void evaluateOrderDeferralReasonsAsChangedForResumedOrder() { + TransportOrder order = new TransportOrder("order", List.of()) + .withHistoryEntry( + new ObjectHistory.Entry(ORDER_DISPATCHING_DEFERRED, List.of("some-reason")) + ) + .withHistoryEntry(new ObjectHistory.Entry(ORDER_DISPATCHING_RESUMED, List.of())); + OrderFilterResult orderFilterResult = new OrderFilterResult(order, List.of("some-reason")); + + assertTrue(dispatchingStatusMarker.haveDeferralReasonsForOrderChanged(orderFilterResult)); + } + + @Test + void evaluateOrderDeferralReasonsAsUnchangedForNotDeferredOrder() { + TransportOrder order = new TransportOrder("order", List.of()); + OrderFilterResult orderFilterResult = new OrderFilterResult(order, List.of()); + + assertFalse(dispatchingStatusMarker.haveDeferralReasonsForOrderChanged(orderFilterResult)); + } + + @Test + void evaluateOrderDeferralReasonsAsUnchangedForDeferredOrder() { + TransportOrder order = new TransportOrder("order", List.of()) + .withHistoryEntry( + new ObjectHistory.Entry(ORDER_DISPATCHING_DEFERRED, List.of("some-reason")) + ); + OrderFilterResult orderFilterResult = new OrderFilterResult(order, List.of("some-reason")); + + assertFalse(dispatchingStatusMarker.haveDeferralReasonsForOrderChanged(orderFilterResult)); + } + + @Test + void evaluateOrderDeferralReasonsAsUnchangedForResumedOrder() { + TransportOrder order = new TransportOrder("order", List.of()) + .withHistoryEntry( + new ObjectHistory.Entry(ORDER_DISPATCHING_DEFERRED, List.of("some-reason")) + ) + .withHistoryEntry(new ObjectHistory.Entry(ORDER_DISPATCHING_RESUMED, List.of())); + OrderFilterResult orderFilterResult = new OrderFilterResult(order, List.of()); + + assertFalse(dispatchingStatusMarker.haveDeferralReasonsForOrderChanged(orderFilterResult)); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByCompleteRoutingCostsTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByCompleteRoutingCostsTest.java new file mode 100644 index 0000000..3b943aa --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByCompleteRoutingCostsTest.java @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment.priorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorByCompleteRoutingCosts; + +/** + */ +class CandidateComparatorByCompleteRoutingCostsTest { + + private CandidateComparatorByCompleteRoutingCosts comparator; + + @BeforeEach + void setUp() { + comparator = new CandidateComparatorByCompleteRoutingCosts(); + } + + @Test + void sortCheapCandidatesUp() { + AssignmentCandidate candidate1 = candidateWithRoutingCost(10); + AssignmentCandidate candidate2 = candidateWithRoutingCost(50); + AssignmentCandidate candidate3 = candidateWithRoutingCost(30); + + List<AssignmentCandidate> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate1))); + assertThat(list.get(1), is(theInstance(candidate3))); + assertThat(list.get(2), is(theInstance(candidate2))); + + } + + private AssignmentCandidate candidateWithRoutingCost(long cost) { + TransportOrder trasportOrder = new TransportOrder("TOrder1", new ArrayList<>()); + Route.Step dummyStep = new Route.Step( + null, + null, + new Point("Point1"), + Vehicle.Orientation.FORWARD, 1 + ); + Route route = new Route(Arrays.asList(dummyStep), cost); + List<DriveOrder> driveOrders = List.of( + new DriveOrder(new DriveOrder.Destination(new Point("Point2").getReference())) + .withRoute(route) + ); + + return new AssignmentCandidate(new Vehicle("Vehicle1"), trasportOrder, driveOrders); + } + +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByDeadlineTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByDeadlineTest.java new file mode 100644 index 0000000..694c404 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByDeadlineTest.java @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment.priorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.Route.Step; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorByDeadline; + +/** + * Unit tests for {@link CandidateComparatorByDeadline}. + */ +class CandidateComparatorByDeadlineTest { + + private CandidateComparatorByDeadline comparator; + + @BeforeEach + void setUp() { + comparator = new CandidateComparatorByDeadline(); + } + + @Test + void sortEarlyDeadlinesUp() { + Instant deadline = Instant.now(); + AssignmentCandidate candidate1 = candidateWithDeadline(deadline.plusMillis(50)); + AssignmentCandidate candidate2 = candidateWithDeadline(deadline.plusMillis(200)); + AssignmentCandidate candidate3 = candidateWithDeadline(deadline.plusMillis(100)); + + List<AssignmentCandidate> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate1))); + assertThat(list.get(1), is(theInstance(candidate3))); + assertThat(list.get(2), is(theInstance(candidate2))); + } + + private AssignmentCandidate candidateWithDeadline(Instant time) { + TransportOrder deadlinedOrder = new TransportOrder("Some order", new ArrayList<>()) + .withDeadline(time); + Step dummyStep + = new Route.Step(null, null, new Point("Point1"), Vehicle.Orientation.FORWARD, 1); + Route route = new Route(Arrays.asList(dummyStep), 10); + List<DriveOrder> driveOrders = List.of( + new DriveOrder(new DriveOrder.Destination(new Point("Point2").getReference())) + .withRoute(route) + ); + + return new AssignmentCandidate( + new Vehicle("Vehicle1"), + deadlinedOrder, + driveOrders + ); + } + +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByEnergyLevelTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByEnergyLevelTest.java new file mode 100644 index 0000000..5f1d901 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByEnergyLevelTest.java @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment.priorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorByEnergyLevel; + +/** + */ +class CandidateComparatorByEnergyLevelTest { + + private CandidateComparatorByEnergyLevel comparator; + + @BeforeEach + void setUp() { + comparator = new CandidateComparatorByEnergyLevel(); + } + + @Test + void sortHighEnergyCandidatesUp() { + AssignmentCandidate candidate1 = candidateWithVehicleEnergyLevel(10); + AssignmentCandidate candidate2 = candidateWithVehicleEnergyLevel(50); + AssignmentCandidate candidate3 = candidateWithVehicleEnergyLevel(30); + + List<AssignmentCandidate> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate2))); + assertThat(list.get(1), is(theInstance(candidate3))); + assertThat(list.get(2), is(theInstance(candidate1))); + + } + + private AssignmentCandidate candidateWithVehicleEnergyLevel(int vehicleEnergy) { + TransportOrder plainOrder = new TransportOrder("TOrder1", new ArrayList<>()); + Route.Step dummyStep = new Route.Step( + null, + null, + new Point("Point-001"), + Vehicle.Orientation.FORWARD, 1 + ); + Route route = new Route(Arrays.asList(dummyStep), 10); + List<DriveOrder> driveOrders = List.of( + new DriveOrder(new DriveOrder.Destination(new Point("Point-001").getReference())) + .withRoute(route) + ); + + return new AssignmentCandidate( + new Vehicle("EnergyVehicle").withEnergyLevel(vehicleEnergy), + plainOrder, + driveOrders + ); + } + +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByInitialRoutingCostsTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByInitialRoutingCostsTest.java new file mode 100644 index 0000000..fbe6ee1 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByInitialRoutingCostsTest.java @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment.priorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorByInitialRoutingCosts; + +/** + * Unit tests for {@link CandidateComparatorByInitialRoutingCosts}. + */ +class CandidateComparatorByInitialRoutingCostsTest { + + private CandidateComparatorByInitialRoutingCosts comparator; + + @BeforeEach + void setUp() { + comparator = new CandidateComparatorByInitialRoutingCosts(); + } + + @Test + void sortCheapCandidatesUp() { + AssignmentCandidate candidate1 = candidateWithInitialRoutingCost(10); + AssignmentCandidate candidate2 = candidateWithInitialRoutingCost(50); + AssignmentCandidate candidate3 = candidateWithInitialRoutingCost(30); + + List<AssignmentCandidate> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate1))); + assertThat(list.get(1), is(theInstance(candidate3))); + assertThat(list.get(2), is(theInstance(candidate2))); + + } + + private AssignmentCandidate candidateWithInitialRoutingCost(long initialRoutingCost) { + TransportOrder trasportOrder = new TransportOrder("TOrder1", new ArrayList<>()); + Route.Step dummyStep + = new Route.Step(null, null, new Point("Point1"), Vehicle.Orientation.FORWARD, 1); + Route route = new Route(Arrays.asList(dummyStep), initialRoutingCost); + List<DriveOrder> driveOrders = List.of( + new DriveOrder(new DriveOrder.Destination(new Point("Point2").getReference())) + .withRoute(route) + ); + + return new AssignmentCandidate(new Vehicle("Vehicle1"), trasportOrder, driveOrders); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByOrderAgeTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByOrderAgeTest.java new file mode 100644 index 0000000..5696c23 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByOrderAgeTest.java @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment.priorization; + +import static java.time.Instant.ofEpochMilli; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorByOrderAge; + +/** + */ +class CandidateComparatorByOrderAgeTest { + + private CandidateComparatorByOrderAge comparator; + + @BeforeEach + void setUp() { + comparator = new CandidateComparatorByOrderAge(); + } + + @Test + void sortOldTransportOrdersUp() { + + AssignmentCandidate candidate1 = candidateWithOrderCreationTime(ofEpochMilli(10)); + AssignmentCandidate candidate2 = candidateWithOrderCreationTime(ofEpochMilli(50)); + AssignmentCandidate candidate3 = candidateWithOrderCreationTime(ofEpochMilli(30)); + + List<AssignmentCandidate> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate1))); + assertThat(list.get(1), is(theInstance(candidate3))); + assertThat(list.get(2), is(theInstance(candidate2))); + + } + + private AssignmentCandidate candidateWithOrderCreationTime(Instant creationTime) { + TransportOrder trasportOrder = new TransportOrder("TOrder1", new ArrayList<>()) + .withCreationTime(creationTime); + Route.Step dummyStep + = new Route.Step(null, null, new Point("Point1"), Vehicle.Orientation.FORWARD, 1); + Route route = new Route(Arrays.asList(dummyStep), 10); + List<DriveOrder> driveOrders = List.of( + new DriveOrder(new DriveOrder.Destination(new Point("Point2").getReference())) + .withRoute(route) + ); + + return new AssignmentCandidate(new Vehicle("Vehicle1"), trasportOrder, driveOrders); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByOrderNameTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByOrderNameTest.java new file mode 100644 index 0000000..027063e --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByOrderNameTest.java @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment.priorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorByOrderName; + +/** + */ +class CandidateComparatorByOrderNameTest { + + private CandidateComparatorByOrderName comparator; + + @BeforeEach + void setUp() { + comparator = new CandidateComparatorByOrderName(); + } + + @Test + void sortCandidatesAlphabeticallyByOrderName() { + AssignmentCandidate candidate1 = candidateWithOrderName("AA"); + AssignmentCandidate candidate2 = candidateWithOrderName("C"); + AssignmentCandidate candidate3 = candidateWithOrderName("AB"); + + List<AssignmentCandidate> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate1))); + assertThat(list.get(1), is(theInstance(candidate3))); + assertThat(list.get(2), is(theInstance(candidate2))); + } + + private AssignmentCandidate candidateWithOrderName(String name) { + TransportOrder trasportOrder = new TransportOrder(name, new ArrayList<>()); + Route.Step dummyStep + = new Route.Step(null, null, new Point("Point1"), Vehicle.Orientation.FORWARD, 1); + Route route = new Route(Arrays.asList(dummyStep), 10); + List<DriveOrder> driveOrders = List.of( + new DriveOrder(new DriveOrder.Destination(new Point("Point2").getReference())) + .withRoute(route) + ); + + return new AssignmentCandidate(new Vehicle("Vehicle1"), trasportOrder, driveOrders); + } + +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByVehicleNameTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByVehicleNameTest.java new file mode 100644 index 0000000..b48346b --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorByVehicleNameTest.java @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment.priorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorByVehicleName; + +/** + */ +class CandidateComparatorByVehicleNameTest { + + private CandidateComparatorByVehicleName comparator; + + @BeforeEach + void setUp() { + comparator = new CandidateComparatorByVehicleName(); + } + + @Test + void sortVehicleNamesAlphabetically() { + AssignmentCandidate candidate1 = candidateWithVehicleName("AA"); + AssignmentCandidate candidate2 = candidateWithVehicleName("C"); + AssignmentCandidate candidate3 = candidateWithVehicleName("AB"); + + List<AssignmentCandidate> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate1))); + assertThat(list.get(1), is(theInstance(candidate3))); + assertThat(list.get(2), is(theInstance(candidate2))); + } + + private AssignmentCandidate candidateWithVehicleName(String vehicleName) { + TransportOrder trasportOrder = new TransportOrder("TOrder1", new ArrayList<>()); + Route.Step dummyStep + = new Route.Step(null, null, new Point("Point1"), Vehicle.Orientation.FORWARD, 1); + Route route = new Route(Arrays.asList(dummyStep), 10); + List<DriveOrder> driveOrders = List.of( + new DriveOrder(new DriveOrder.Destination(new Point("Point2").getReference())) + .withRoute(route) + ); + + return new AssignmentCandidate(new Vehicle(vehicleName), trasportOrder, driveOrders); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorDeadlineAtRiskFirstTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorDeadlineAtRiskFirstTest.java new file mode 100644 index 0000000..ba531ac --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorDeadlineAtRiskFirstTest.java @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment.priorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorDeadlineAtRiskFirst; +import org.opentcs.strategies.basic.dispatching.priorization.transportorder.TransportOrderComparatorDeadlineAtRiskFirst; + +/** + * Unit tests for {@link CandidateComparatorDeadlineAtRiskFirst}. + */ +class CandidateComparatorDeadlineAtRiskFirstTest { + + private CandidateComparatorDeadlineAtRiskFirst comparator; + + @BeforeEach + void setUp() { + DefaultDispatcherConfiguration configuration + = Mockito.mock(DefaultDispatcherConfiguration.class); + Mockito.when(configuration.deadlineAtRiskPeriod()).thenReturn(Long.valueOf(60 * 60 * 1000)); + + this.comparator = new CandidateComparatorDeadlineAtRiskFirst( + new TransportOrderComparatorDeadlineAtRiskFirst(configuration) + ); + } + + @Test + void sortCriticalDeadlinesUp() { + Instant deadline = Instant.now(); + AssignmentCandidate candidate1 = candidateWithDeadline(deadline.plus(270, ChronoUnit.MINUTES)); + AssignmentCandidate candidate2 = candidateWithDeadline(deadline.plus(30, ChronoUnit.MINUTES)); + AssignmentCandidate candidate3 = candidateWithDeadline(deadline.plus(180, ChronoUnit.MINUTES)); + + List<AssignmentCandidate> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate2))); + } + + private AssignmentCandidate candidateWithDeadline(Instant time) { + TransportOrder deadlinedOrder + = new TransportOrder("Some order", new ArrayList<>()).withDeadline(time); + Route.Step dummyStep = new Route.Step( + null, + null, + new Point("Point1"), + Vehicle.Orientation.FORWARD, + 1 + ); + Route route = new Route(Arrays.asList(dummyStep), 10); + List<DriveOrder> driveOrders = Arrays.asList( + new DriveOrder(new DriveOrder.Destination(new Point("Point2").getReference())) + .withRoute(route) + ); + + return new AssignmentCandidate( + new Vehicle("Vehicle1"), + deadlinedOrder, + driveOrders + ); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorIdleFirstTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorIdleFirstTest.java new file mode 100644 index 0000000..652a8dd --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CandidateComparatorIdleFirstTest.java @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment.priorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorIdleFirst; + +/** + */ +class CandidateComparatorIdleFirstTest { + + private CandidateComparatorIdleFirst comparator; + + @BeforeEach + void setUp() { + comparator = new CandidateComparatorIdleFirst(); + } + + @Test + void sortVehiclesIdleFirst() { + AssignmentCandidate candidate1 = candidateWithVehicleState(Vehicle.State.CHARGING); + AssignmentCandidate candidate2 = candidateWithVehicleState(Vehicle.State.IDLE); + AssignmentCandidate candidate3 = candidateWithVehicleState(Vehicle.State.CHARGING); + + List<AssignmentCandidate> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate2))); + assertThat(list.subList(1, 3), contains(candidate1, candidate3)); + } + + private AssignmentCandidate candidateWithVehicleState(Vehicle.State vehicleState) { + TransportOrder trasportOrder = new TransportOrder("TOrder1", new ArrayList<>()); + Route.Step dummyStep + = new Route.Step(null, null, new Point("Point1"), Vehicle.Orientation.FORWARD, 1); + Route route = new Route(Arrays.asList(dummyStep), 10); + List<DriveOrder> driveOrders = List.of( + new DriveOrder(new DriveOrder.Destination(new Point("Point2").getReference())) + .withRoute(route) + ); + + return new AssignmentCandidate( + new Vehicle("Vehicle1").withState(vehicleState), + trasportOrder, + driveOrders + ); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CompositeOrderCandidateComparatorTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CompositeOrderCandidateComparatorTest.java new file mode 100644 index 0000000..311e4b7 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CompositeOrderCandidateComparatorTest.java @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment.priorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration; +import org.opentcs.strategies.basic.dispatching.priorization.CompositeOrderCandidateComparator; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorByDeadline; + +/** + */ +class CompositeOrderCandidateComparatorTest { + + private CompositeOrderCandidateComparator comparator; + private DefaultDispatcherConfiguration configuration; + private Map<String, Comparator<AssignmentCandidate>> availableComparators; + + @BeforeEach + void setUp() { + configuration = Mockito.mock(DefaultDispatcherConfiguration.class); + availableComparators = new HashMap<>(); + + } + + @Test + void sortAlphabeticallyForOtherwiseEqualInstances() { + + Mockito.when(configuration.orderCandidatePriorities()) + .thenReturn(List.of()); + comparator = new CompositeOrderCandidateComparator(configuration, availableComparators); + + AssignmentCandidate candidate1 = candidateWithName("AA"); + AssignmentCandidate candidate2 = candidateWithName("CC"); + AssignmentCandidate candidate3 = candidateWithName("AB"); + + List<AssignmentCandidate> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate1))); + assertThat(list.get(1), is(theInstance(candidate3))); + assertThat(list.get(2), is(theInstance(candidate2))); + } + + @Test + void sortsByAgeAndName() { + Mockito.when(configuration.orderCandidatePriorities()) + .thenReturn(List.of()); + comparator = new CompositeOrderCandidateComparator(configuration, availableComparators); + + AssignmentCandidate candidate1 = candidateWithNameAndCreationtime("AA", 2); + AssignmentCandidate candidate2 = candidateWithNameAndCreationtime("CC", 1); + AssignmentCandidate candidate3 = candidateWithNameAndCreationtime("BB", 1); + + List<AssignmentCandidate> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate3))); + assertThat(list.get(1), is(theInstance(candidate2))); + assertThat(list.get(2), is(theInstance(candidate1))); + } + + @Test + void sortsByAgeAndNameAndInitialRoutingCost() { + String deadlineKey = "BY_DEADLINE"; + Mockito.when(configuration.orderCandidatePriorities()) + .thenReturn(List.of(deadlineKey)); + availableComparators.put( + deadlineKey, + new CandidateComparatorByDeadline() + ); + comparator = new CompositeOrderCandidateComparator(configuration, availableComparators); + + AssignmentCandidate candidate1 = candidateWithNameCreationtimeAndDeadline("AA", 3, 20); + AssignmentCandidate candidate2 = candidateWithNameCreationtimeAndDeadline("CC", 2, 20); + AssignmentCandidate candidate3 = candidateWithNameCreationtimeAndDeadline("BB", 1, 60); + AssignmentCandidate candidate4 = candidateWithNameCreationtimeAndDeadline("DD", 1, 60); + + List<AssignmentCandidate> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + list.add(candidate4); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate2))); + assertThat(list.get(1), is(theInstance(candidate1))); + assertThat(list.get(2), is(theInstance(candidate3))); + assertThat(list.get(3), is(theInstance(candidate4))); + } + + private AssignmentCandidate candidateWithName(String ordername) { + TransportOrder trasportOrder = new TransportOrder(ordername, new ArrayList<>()); + return new AssignmentCandidate(new Vehicle("Vehicle1"), trasportOrder, buildPlainDriveOrders()); + } + + private AssignmentCandidate candidateWithNameAndCreationtime( + String ordername, + long creationTime + ) { + TransportOrder trasportOrder = new TransportOrder(ordername, new ArrayList<>()) + .withCreationTime(Instant.ofEpochMilli(creationTime)); + return new AssignmentCandidate(new Vehicle("Vehicle1"), trasportOrder, buildPlainDriveOrders()); + } + + private AssignmentCandidate candidateWithNameCreationtimeAndDeadline( + String ordername, + long creationTime, + long deadline + ) { + TransportOrder trasportOrder = new TransportOrder(ordername, new ArrayList<>()) + .withCreationTime(Instant.ofEpochMilli(creationTime)) + .withDeadline(Instant.ofEpochMilli(deadline)); + return new AssignmentCandidate( + new Vehicle("Vehicle1"), + trasportOrder, + buildPlainDriveOrders() + ); + } + + private List<DriveOrder> buildPlainDriveOrders() { + Route.Step dummyStep + = new Route.Step(null, null, new Point("Point1"), Vehicle.Orientation.FORWARD, 1); + Route route = new Route(Arrays.asList(dummyStep), 10); + List<DriveOrder> driveOrders = List.of( + new DriveOrder(new DriveOrder.Destination(new Point("Point2").getReference())) + .withRoute(route) + ); + return driveOrders; + } + +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CompositeOrderComparatorTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CompositeOrderComparatorTest.java new file mode 100644 index 0000000..8b10612 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CompositeOrderComparatorTest.java @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment.priorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration; +import org.opentcs.strategies.basic.dispatching.priorization.CompositeOrderComparator; +import org.opentcs.strategies.basic.dispatching.priorization.transportorder.TransportOrderComparatorByDeadline; + +/** + * Unit tests for {@link CompositeOrderComparator}. + */ +class CompositeOrderComparatorTest { + + private CompositeOrderComparator comparator; + private DefaultDispatcherConfiguration configuration; + private Map<String, Comparator<TransportOrder>> availableComparators; + + @BeforeEach + void setUp() { + configuration = Mockito.mock(DefaultDispatcherConfiguration.class); + availableComparators = new HashMap<>(); + } + + @Test + void sortNamesUpForOtherwiseEqualInstances() { + + Mockito.when(configuration.orderPriorities()) + .thenReturn(List.of()); + comparator = new CompositeOrderComparator(configuration, availableComparators); + + TransportOrder candidate1 = new TransportOrder("AA", new ArrayList<>()); + TransportOrder candidate2 = new TransportOrder("CC", new ArrayList<>()); + TransportOrder candidate3 = new TransportOrder("AB", new ArrayList<>()); + + List<TransportOrder> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate1))); + assertThat(list.get(1), is(theInstance(candidate3))); + assertThat(list.get(2), is(theInstance(candidate2))); + } + + @Test + void sortsByAgeAndName() { + Mockito.when(configuration.orderPriorities()) + .thenReturn(List.of()); + comparator = new CompositeOrderComparator(configuration, availableComparators); + + Instant creationTime = Instant.now(); + TransportOrder candidate1 = candidateWithNameAndCreationtime( + "AA", + creationTime.minusSeconds(1) + ); + TransportOrder candidate2 = candidateWithNameAndCreationtime( + "CC", + creationTime.minusSeconds(2) + ); + TransportOrder candidate3 = candidateWithNameAndCreationtime( + "BB", + creationTime.minusSeconds(2) + ); + + List<TransportOrder> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate3))); + assertThat(list.get(1), is(theInstance(candidate2))); + assertThat(list.get(2), is(theInstance(candidate1))); + } + + @Test + void sortsByAgeAndNameAndDeadline() { + String deadlineKey = "BY_DEADLINE"; + Mockito.when(configuration.orderPriorities()) + .thenReturn(List.of(deadlineKey)); + availableComparators.put( + deadlineKey, + new TransportOrderComparatorByDeadline() + ); + + comparator = new CompositeOrderComparator(configuration, availableComparators); + + Instant currentTime = Instant.now(); + TransportOrder candidate1 + = candidateWithNameCreationtimeAndDeadline( + "AA", + currentTime.minusSeconds(2),//Creation + currentTime.plusSeconds(2) + );//Deadline + TransportOrder candidate2 + = candidateWithNameCreationtimeAndDeadline( + "CC", + currentTime.minusSeconds(2),//Creation + currentTime.plusSeconds(1) + );//Deadline + TransportOrder candidate3 + = candidateWithNameCreationtimeAndDeadline( + "BB", + currentTime.minusSeconds(2),//Creation + currentTime.plusSeconds(2) + );//Deadline + TransportOrder candidate4 + = candidateWithNameCreationtimeAndDeadline( + "DD", + currentTime.minusSeconds(1),//Creation + currentTime.plusSeconds(5) + );//Deadline + + List<TransportOrder> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + list.add(candidate4); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate2))); + assertThat(list.get(1), is(theInstance(candidate1))); + assertThat(list.get(2), is(theInstance(candidate3))); + assertThat(list.get(3), is(theInstance(candidate4))); + } + + private TransportOrder candidateWithNameAndCreationtime( + String ordername, + Instant creationTime + ) { + return new TransportOrder(ordername, new ArrayList<>()) + .withCreationTime(creationTime); + } + + private TransportOrder candidateWithNameCreationtimeAndDeadline( + String ordername, + Instant creationTime, + Instant deadline + ) { + return new TransportOrder(ordername, new ArrayList<>()) + .withCreationTime(creationTime) + .withDeadline(deadline); + } + +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CompositeVehicleCandidateComparatorTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CompositeVehicleCandidateComparatorTest.java new file mode 100644 index 0000000..a989289 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CompositeVehicleCandidateComparatorTest.java @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment.priorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.AssignmentCandidate; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration; +import org.opentcs.strategies.basic.dispatching.priorization.CompositeVehicleCandidateComparator; +import org.opentcs.strategies.basic.dispatching.priorization.candidate.CandidateComparatorByInitialRoutingCosts; + +/** + */ +class CompositeVehicleCandidateComparatorTest { + + private CompositeVehicleCandidateComparator comparator; + private DefaultDispatcherConfiguration configuration; + private Map<String, Comparator<AssignmentCandidate>> availableComparators; + + @BeforeEach + void setUp() { + configuration = Mockito.mock(DefaultDispatcherConfiguration.class); + availableComparators = new HashMap<>(); + } + + @Test + void sortNamesUpForOtherwiseEqualInstances() { + + Mockito.when(configuration.vehicleCandidatePriorities()) + .thenReturn(List.of()); + comparator = new CompositeVehicleCandidateComparator(configuration, availableComparators); + + AssignmentCandidate candidate1 = candidateWithName("AA"); + AssignmentCandidate candidate2 = candidateWithName("CC"); + AssignmentCandidate candidate3 = candidateWithName("AB"); + + List<AssignmentCandidate> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate1))); + assertThat(list.get(1), is(theInstance(candidate3))); + assertThat(list.get(2), is(theInstance(candidate2))); + } + + @Test + void sortsByNameAndEnergylevel() { + Mockito.when(configuration.vehicleCandidatePriorities()) + .thenReturn(List.of()); + comparator = new CompositeVehicleCandidateComparator(configuration, availableComparators); + + AssignmentCandidate candidate1 = candidateWithNameEnergylevel("AA", 1); + AssignmentCandidate candidate2 = candidateWithNameEnergylevel("CC", 2); + AssignmentCandidate candidate3 = candidateWithNameEnergylevel("BB", 2); + + List<AssignmentCandidate> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate3))); + assertThat(list.get(1), is(theInstance(candidate2))); + assertThat(list.get(2), is(theInstance(candidate1))); + } + + @Test + void sortsByNameAndRoutingCostAndEnergyLevel() { + String initRoutingCostKey = "BY_INITIAL_ROUTING_COSTS"; + Mockito.when(configuration.vehicleCandidatePriorities()) + .thenReturn(List.of(initRoutingCostKey)); + availableComparators.put( + initRoutingCostKey, + new CandidateComparatorByInitialRoutingCosts() + ); + + comparator = new CompositeVehicleCandidateComparator(configuration, availableComparators); + + AssignmentCandidate candidate1 = candidateWithNameEnergylevelInitialRoutingCosts("AA", 3, 60); + AssignmentCandidate candidate2 = candidateWithNameEnergylevelInitialRoutingCosts("CC", 2, 60); + AssignmentCandidate candidate3 = candidateWithNameEnergylevelInitialRoutingCosts("BB", 1, 20); + AssignmentCandidate candidate4 = candidateWithNameEnergylevelInitialRoutingCosts("DD", 1, 20); + + List<AssignmentCandidate> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + list.add(candidate4); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate3))); + assertThat(list.get(1), is(theInstance(candidate4))); + assertThat(list.get(2), is(theInstance(candidate1))); + assertThat(list.get(3), is(theInstance(candidate2))); + } + + private AssignmentCandidate candidateWithName(String name) { + TransportOrder trasportOrder = new TransportOrder("TOrder-1", new ArrayList<>()); + return new AssignmentCandidate( + new Vehicle(name), + trasportOrder, + buildDriveOrders(10) + ); + } + + private AssignmentCandidate candidateWithNameEnergylevel( + String name, + int energyLevel + ) { + TransportOrder trasportOrder = new TransportOrder("TOrder-1", new ArrayList<>()); + return new AssignmentCandidate( + new Vehicle(name).withEnergyLevel(energyLevel), + trasportOrder, + buildDriveOrders(10) + ); + } + + private AssignmentCandidate candidateWithNameEnergylevelInitialRoutingCosts( + String name, + int energyLevel, + long routingCost + ) { + TransportOrder trasportOrder = new TransportOrder("TOrder-1", new ArrayList<>()); + return new AssignmentCandidate( + new Vehicle(name).withEnergyLevel(energyLevel), + trasportOrder, + buildDriveOrders(routingCost) + ); + } + + private List<DriveOrder> buildDriveOrders(long routingCost) { + Route.Step dummyStep + = new Route.Step(null, null, new Point("Point1"), Vehicle.Orientation.FORWARD, 1); + Route route = new Route(Arrays.asList(dummyStep), routingCost); + List<DriveOrder> driveOrders = List.of( + new DriveOrder(new DriveOrder.Destination(new Point("Point2").getReference())) + .withRoute(route) + ); + return driveOrders; + } + +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CompositeVehicleComparatorTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CompositeVehicleComparatorTest.java new file mode 100644 index 0000000..94cf812 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/CompositeVehicleComparatorTest.java @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment.priorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration; +import org.opentcs.strategies.basic.dispatching.priorization.CompositeVehicleComparator; +import org.opentcs.strategies.basic.dispatching.priorization.vehicle.VehicleComparatorIdleFirst; + +/** + */ +class CompositeVehicleComparatorTest { + + private CompositeVehicleComparator comparator; + private DefaultDispatcherConfiguration configuration; + private Map<String, Comparator<Vehicle>> availableComparators; + + @BeforeEach + void setUp() { + configuration = Mockito.mock(DefaultDispatcherConfiguration.class); + availableComparators = new HashMap<>(); + + } + + @Test + void sortNamesUpForOtherwiseEqualInstances() { + Mockito.when(configuration.vehiclePriorities()) + .thenReturn(List.of()); + comparator = new CompositeVehicleComparator(configuration, availableComparators); + + Vehicle candidate1 = new Vehicle("AA"); + Vehicle candidate2 = new Vehicle("CC"); + Vehicle candidate3 = new Vehicle("AB"); + + List<Vehicle> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate1))); + assertThat(list.get(1), is(theInstance(candidate3))); + assertThat(list.get(2), is(theInstance(candidate2))); + } + + @Test + void sortsByNameAndEnergylevel() { + Mockito.when(configuration.vehiclePriorities()) + .thenReturn(List.of()); + comparator = new CompositeVehicleComparator(configuration, availableComparators); + + Vehicle candidate1 = new Vehicle("AA").withEnergyLevel(1); + Vehicle candidate2 = new Vehicle("CC").withEnergyLevel(2); + Vehicle candidate3 = new Vehicle("BB").withEnergyLevel(2); + + List<Vehicle> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate3))); + assertThat(list.get(1), is(theInstance(candidate2))); + assertThat(list.get(2), is(theInstance(candidate1))); + } + + @Test + void sortsByNameEnergylevelRoutingCost() { + + Mockito.when(configuration.vehiclePriorities()) + .thenReturn(List.of("IDLE_FIRST")); + availableComparators.put( + "IDLE_FIRST", + new VehicleComparatorIdleFirst() + ); + + comparator = new CompositeVehicleComparator(configuration, availableComparators); + + Vehicle candidate1 = new Vehicle("AA").withEnergyLevel(30).withState(Vehicle.State.EXECUTING); + Vehicle candidate2 = new Vehicle("BB").withEnergyLevel(30).withState(Vehicle.State.IDLE); + Vehicle candidate3 = new Vehicle("CC").withEnergyLevel(60).withState(Vehicle.State.IDLE); + Vehicle candidate4 = new Vehicle("DD").withEnergyLevel(60).withState(Vehicle.State.IDLE); + + List<Vehicle> list = new ArrayList<>(); + list.add(candidate1); + list.add(candidate2); + list.add(candidate3); + list.add(candidate4); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(candidate3))); + assertThat(list.get(1), is(theInstance(candidate4))); + assertThat(list.get(2), is(theInstance(candidate2))); + assertThat(list.get(3), is(theInstance(candidate1))); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/TransportOrderComparatorByAgeTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/TransportOrderComparatorByAgeTest.java new file mode 100644 index 0000000..b49f083 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/TransportOrderComparatorByAgeTest.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment.priorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.priorization.transportorder.TransportOrderComparatorByAge; + +/** + */ +class TransportOrderComparatorByAgeTest { + + private TransportOrderComparatorByAge comparator; + + @BeforeEach + void setUp() { + comparator = new TransportOrderComparatorByAge(); + } + + @Test + void sortOlderOrdersUp() { + Instant creationTime = Instant.now(); + TransportOrder plainOrder = new TransportOrder("Some order", new ArrayList<>()); + TransportOrder order1 = plainOrder.withCreationTime(creationTime); + TransportOrder order2 = plainOrder.withCreationTime(creationTime.plus(2, ChronoUnit.HOURS)); + TransportOrder order3 = plainOrder.withCreationTime(creationTime.plus(1, ChronoUnit.HOURS)); + + List<TransportOrder> list = new ArrayList<>(); + list.add(order1); + list.add(order2); + list.add(order3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(order1))); + assertThat(list.get(1), is(theInstance(order3))); + assertThat(list.get(2), is(theInstance(order2))); + } + +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/TransportOrderComparatorByDeadlineTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/TransportOrderComparatorByDeadlineTest.java new file mode 100644 index 0000000..70a0c25 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/TransportOrderComparatorByDeadlineTest.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment.priorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.priorization.transportorder.TransportOrderComparatorByDeadline; + +/** + */ +class TransportOrderComparatorByDeadlineTest { + + private TransportOrderComparatorByDeadline comparator; + + @BeforeEach + void setUp() { + comparator = new TransportOrderComparatorByDeadline(); + } + + @Test + void sortEarlyDeadlinesUp() { + Instant creationTime = Instant.now(); + TransportOrder plainOrder = new TransportOrder("Some order ", new ArrayList<>()); + TransportOrder order1 = plainOrder.withDeadline(creationTime); + TransportOrder order2 = plainOrder.withDeadline(creationTime.plus(2, ChronoUnit.HOURS)); + TransportOrder order3 = plainOrder.withDeadline(creationTime.plus(1, ChronoUnit.HOURS)); + + List<TransportOrder> list = new ArrayList<>(); + list.add(order1); + list.add(order2); + list.add(order3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(order1))); + assertThat(list.get(1), is(theInstance(order3))); + assertThat(list.get(2), is(theInstance(order2))); + } + +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/TransportOrderComparatorByNameTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/TransportOrderComparatorByNameTest.java new file mode 100644 index 0000000..4075d1e --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/TransportOrderComparatorByNameTest.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment.priorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.priorization.transportorder.TransportOrderComparatorByName; + +/** + */ +class TransportOrderComparatorByNameTest { + + private TransportOrderComparatorByName comparator; + + @BeforeEach + void setUp() { + comparator = new TransportOrderComparatorByName(); + } + + @Test + void sortsAlphabeticallyByName() { + TransportOrder order1 = new TransportOrder("AA", List.of()); + TransportOrder order2 = new TransportOrder("CC", List.of()); + TransportOrder order3 = new TransportOrder("AB", List.of()); + + List<TransportOrder> list = new ArrayList<>(); + list.add(order1); + list.add(order2); + list.add(order3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(order1))); + assertThat(list.get(1), is(theInstance(order3))); + assertThat(list.get(2), is(theInstance(order2))); + } + +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/TransportOrderComparatorDeadlineAtRiskFirstTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/TransportOrderComparatorDeadlineAtRiskFirstTest.java new file mode 100644 index 0000000..3578300 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/TransportOrderComparatorDeadlineAtRiskFirstTest.java @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment.priorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration; +import org.opentcs.strategies.basic.dispatching.priorization.transportorder.TransportOrderComparatorDeadlineAtRiskFirst; + +/** + */ +class TransportOrderComparatorDeadlineAtRiskFirstTest { + + private TransportOrderComparatorDeadlineAtRiskFirst comparator; + + private DefaultDispatcherConfiguration configuration; + + @BeforeEach + void setUp() { + configuration = Mockito.mock(DefaultDispatcherConfiguration.class); + when(configuration.deadlineAtRiskPeriod()).thenReturn(Long.valueOf(60 * 60 * 1000)); + + comparator = new TransportOrderComparatorDeadlineAtRiskFirst(configuration); + } + + @Test + void sortCriticalDeadlinesUp() { + Instant deadline = Instant.now(); + TransportOrder plainOrder = new TransportOrder("Some order ", new ArrayList<>()); + TransportOrder order1 = plainOrder.withDeadline(deadline.plus(150, ChronoUnit.MINUTES)); + TransportOrder order2 = plainOrder.withDeadline(deadline.plus(5, ChronoUnit.MINUTES)); + TransportOrder order3 = plainOrder.withDeadline(deadline.plus(170, ChronoUnit.MINUTES)); + + List<TransportOrder> list = new ArrayList<>(); + list.add(order1); + list.add(order2); + list.add(order3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(order2))); + } + +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/VehicleComparatorByEnergyLevelTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/VehicleComparatorByEnergyLevelTest.java new file mode 100644 index 0000000..4512286 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/VehicleComparatorByEnergyLevelTest.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment.priorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.dispatching.priorization.vehicle.VehicleComparatorByEnergyLevel; + +/** + */ +class VehicleComparatorByEnergyLevelTest { + + private VehicleComparatorByEnergyLevel comparator; + + @BeforeEach + void setUp() { + comparator = new VehicleComparatorByEnergyLevel(); + } + + @Test + void sortHighEnergyLevelsUp() { + Vehicle vehicle1 = new Vehicle("Vehicle1").withEnergyLevel(99); + Vehicle vehicle2 = vehicle1.withEnergyLevel(50); + Vehicle vehicle3 = vehicle1.withEnergyLevel(98); + + List<Vehicle> list = new ArrayList<>(); + list.add(vehicle1); + list.add(vehicle2); + list.add(vehicle3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(vehicle1))); + assertThat(list.get(1), is(theInstance(vehicle3))); + assertThat(list.get(2), is(theInstance(vehicle2))); + } + +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/VehicleComparatorByNameTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/VehicleComparatorByNameTest.java new file mode 100644 index 0000000..51f0ee0 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/VehicleComparatorByNameTest.java @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment.priorization; + + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.dispatching.priorization.vehicle.VehicleComparatorByName; + +/** + */ +class VehicleComparatorByNameTest { + private VehicleComparatorByName comparator; + + @BeforeEach + void setUp() { + comparator = new VehicleComparatorByName(); + } + + + @Test + void sortsAlphabeticallyByName() { + Vehicle vehicle1 = new Vehicle("AA"); + Vehicle vehicle2 = new Vehicle("CC"); + Vehicle vehicle3 = new Vehicle("AB"); + + List<Vehicle> list = new ArrayList<>(); + list.add(vehicle1); + list.add(vehicle2); + list.add(vehicle3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(vehicle1))); + assertThat(list.get(1), is(theInstance(vehicle3))); + assertThat(list.get(2), is(theInstance(vehicle2))); + } + +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/VehicleComparatorIdleFirstTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/VehicleComparatorIdleFirstTest.java new file mode 100644 index 0000000..f593860 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/assignment/priorization/VehicleComparatorIdleFirstTest.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.assignment.priorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.theInstance; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.dispatching.priorization.vehicle.VehicleComparatorIdleFirst; + +/** + */ +class VehicleComparatorIdleFirstTest { + + private VehicleComparatorIdleFirst comparator; + + @BeforeEach + void setUp() { + comparator = new VehicleComparatorIdleFirst(); + } + + @Test + void sortIdleVehiclesUp() { + Vehicle vehicle1 = new Vehicle("Vehicle1").withState(Vehicle.State.CHARGING); + Vehicle vehicle2 = vehicle1.withState(Vehicle.State.IDLE); + Vehicle vehicle3 = vehicle1.withState(Vehicle.State.CHARGING); + + List<Vehicle> list = new ArrayList<>(); + list.add(vehicle1); + list.add(vehicle2); + list.add(vehicle3); + + Collections.sort(list, comparator); + + assertThat(list.get(0), is(theInstance(vehicle2))); + } + +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/parking/AbstractParkingPositionSupplierTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/parking/AbstractParkingPositionSupplierTest.java new file mode 100644 index 0000000..bfa94d2 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/parking/AbstractParkingPositionSupplierTest.java @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.parking; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.InternalPlantModelService; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResource; +import org.opentcs.data.model.Vehicle; + +/** + * Tests for {@link AbstractParkingPositionSupplier}. + */ +class AbstractParkingPositionSupplierTest { + + private InternalPlantModelService plantModelService; + private Router router; + private AbstractParkingPositionSupplierImpl supplier; + + AbstractParkingPositionSupplierTest() { + } + + @BeforeEach + void setUp() { + plantModelService = mock(InternalPlantModelService.class); + router = mock(Router.class); + supplier = new AbstractParkingPositionSupplierImpl(plantModelService, router); + + } + + @Test + void returnsEmptyAllParkingPositionsOccupied() { + Point point1 = new Point("vehicle's current position"); + Point point2 = new Point("parking position occupied by another vehicle") + .withType(Point.Type.PARK_POSITION) + .withOccupyingVehicle(new Vehicle("another vehicle").getReference()); + Point point3 = new Point("parking position occupied by yet another vehicle") + .withType(Point.Type.PARK_POSITION) + .withOccupyingVehicle(new Vehicle("yet another vehicle").getReference()); + Vehicle vehicle = new Vehicle("vehicle") + .withCurrentPosition(point1.getReference()); + + when(router.getTargetedPoints()).thenReturn(new HashSet<>()); + when(plantModelService.fetchObjects(eq(Point.class), any())).thenReturn(setOf(point2, point3)); + when(plantModelService.expandResources(Collections.singleton(point2.getReference()))) + .thenReturn(Collections.singleton(point2)); + when(plantModelService.expandResources(Collections.singleton(point3.getReference()))) + .thenReturn(Collections.singleton(point3)); + + Set<Point> result = supplier.findUsableParkingPositions(vehicle); + assertTrue(result.isEmpty()); + } + + @Test + void returnsUnoccupiedParkingPositions() { + Point point1 = new Point("vehicle's current position"); + Point point2 = new Point("unoccupied parking position") + .withType(Point.Type.PARK_POSITION); + Point point3 = new Point("another unoccupied parking position") + .withType(Point.Type.PARK_POSITION); + Vehicle vehicle = new Vehicle("vehicle") + .withCurrentPosition(point1.getReference()); + + when(router.getTargetedPoints()).thenReturn(new HashSet<>()); + when(plantModelService.fetchObjects(eq(Point.class), any())).thenReturn(setOf(point2, point3)); + when(plantModelService.expandResources(Collections.singleton(point2.getReference()))) + .thenReturn(Collections.singleton(point2)); + when(plantModelService.expandResources(Collections.singleton(point3.getReference()))) + .thenReturn(Collections.singleton(point3)); + + Set<Point> result = supplier.findUsableParkingPositions(vehicle); + assertFalse(result.isEmpty()); + assertEquals(setOf(point2, point3), result); + } + + @Test + void returnsExpandedPoints() { + Point[] points = new Point[5]; + Path[] paths = new Path[points.length]; + for (int i = 0; i < points.length; i++) { + points[i] = new Point("Point" + i); + paths[i] = new Path( + "Path" + i, + new Point("some point").getReference(), + new Point("some other point").getReference() + ); + } + + Set<TCSResource<?>> blockMembers = setOf( + points[1], points[2], points[3], + paths[0], paths[4] + ); + + when(plantModelService.fetchObjects(Point.class)).thenReturn(setOf(points)); + when(plantModelService.expandResources(Collections.singleton(points[2].getReference()))) + .thenReturn(blockMembers); + + Set<Point> result = supplier.expandPoints(points[2]); + assertFalse(result.isEmpty()); + assertEquals(setOf(points[1], points[2], points[3]), result); + } + + @SuppressWarnings("unchecked") + private <T> Set<T> setOf(T... resources) { + return new HashSet<>(Arrays.asList(resources)); + } + + class AbstractParkingPositionSupplierImpl + extends + AbstractParkingPositionSupplier { + + AbstractParkingPositionSupplierImpl( + InternalPlantModelService plantModelService, + Router router + ) { + super(plantModelService, router); + } + + @Override + public Optional<Point> findParkingPosition(Vehicle vehicle) { + throw new UnsupportedOperationException("Outside of this test's scope."); + } + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/parking/DefaultParkingPositionSupplierTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/parking/DefaultParkingPositionSupplierTest.java new file mode 100644 index 0000000..a3ff5d4 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/parking/DefaultParkingPositionSupplierTest.java @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.parking; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.Dispatcher; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.InternalPlantModelService; +import org.opentcs.data.model.Block; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; + +/** + */ +class DefaultParkingPositionSupplierTest { + + private InternalPlantModelService plantModelService; + private Router router; + private Vehicle vehicle; + private DefaultParkingPositionSupplier supplier; + + @BeforeEach + void setUp() { + plantModelService = mock(InternalPlantModelService.class); + router = mock(Router.class); + vehicle = new Vehicle("vehicle"); + supplier = new DefaultParkingPositionSupplier(plantModelService, router); + } + + @AfterEach + void tearDown() { + supplier.terminate(); + } + + @Test + void returnsEmptyForUnknownVehiclePosition() { + vehicle = vehicle.withCurrentPosition(null); + supplier.initialize(); + Optional<Point> result = supplier.findParkingPosition(vehicle); + assertThat(result.isPresent(), is(false)); + } + + @Test + void returnsEmptyForUnknownAssignedParkingPosition() { + vehicle = vehicle + .withCurrentPosition(new Point("dummyPoint").getReference()) + .withProperty(Dispatcher.PROPKEY_ASSIGNED_PARKING_POSITION, "someUnknownPoint"); + supplier.initialize(); + Optional<Point> result = supplier.findParkingPosition(vehicle); + assertThat(result.isPresent(), is(false)); + } + + @Test + void returnsEmptyForNoParkingPositionsFromKernel() { + when(plantModelService.fetchObjects(Point.class)).thenReturn(new HashSet<>()); + when(plantModelService.fetchObjects(Block.class)).thenReturn(new HashSet<>()); + vehicle = vehicle.withCurrentPosition(new Point("dummyPoint").getReference()); + supplier.initialize(); + Optional<Point> result = supplier.findParkingPosition(vehicle); + assertThat(result.isPresent(), is(false)); + } + + @Test + void returnsClosestParkingPosition() { + Point point1 = new Point("vehicle's current position"); + Point point2 = new Point("parking position") + .withType(Point.Type.PARK_POSITION); + Point point3 = new Point("another parking position closer to the vehicle") + .withType(Point.Type.PARK_POSITION); + vehicle = new Vehicle("vehicle").withCurrentPosition(point1.getReference()); + + when(router.getTargetedPoints()).thenReturn(new HashSet<>()); + when(plantModelService.fetchObject(Point.class, point1.getReference())).thenReturn(point1); + when(plantModelService.fetchObjects(eq(Point.class), any())).thenReturn(setOf(point2, point3)); + when(router.getCosts(vehicle, point1, point2, Set.of())).thenReturn(10L); + when(router.getCosts(vehicle, point1, point3, Set.of())).thenReturn(1L); + + Optional<Point> result = supplier.findParkingPosition(vehicle); + assertTrue(result.isPresent()); + assertEquals(point3, result.get()); + } + + @SuppressWarnings("unchecked") + private <T> Set<T> setOf(T... resources) { + return new HashSet<>(Arrays.asList(resources)); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/parking/ParkingPositionPriorityComparatorTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/parking/ParkingPositionPriorityComparatorTest.java new file mode 100644 index 0000000..ec9ec83 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/parking/ParkingPositionPriorityComparatorTest.java @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.parking; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThan; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.Dispatcher; +import org.opentcs.data.model.Point; + +/** + */ +class ParkingPositionPriorityComparatorTest { + + private ParkingPositionPriorityComparator comparator; + + @BeforeEach + void setUp() { + comparator = new ParkingPositionPriorityComparator(new ParkingPositionToPriorityFunction()); + } + + @Test + void prefersPrioritizedParkingPositions() { + Point pointWithoutPrio + = new Point("Point without prio") + .withType(Point.Type.PARK_POSITION); + Point pointWithPrio + = new Point("Point with prio") + .withType(Point.Type.PARK_POSITION) + .withProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY, "1"); + + // Let's try it the one way, ... + assertThat( + comparator.compare(pointWithPrio, pointWithoutPrio), + lessThan(0) + ); + // ...and the other way. + assertThat( + comparator.compare(pointWithoutPrio, pointWithPrio), + greaterThan(0) + ); + } + + @Test + void prefersSmallerPriorityIntegers() { + Point pointWithLowerPrioValue + = new Point("Point with lower prio value") + .withType(Point.Type.PARK_POSITION) + .withProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY, "23"); + Point pointWithHigherPrioValue + = new Point("Point with higher prio value") + .withType(Point.Type.PARK_POSITION) + .withProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY, "142"); + + // Let's try it the one way, ... + assertThat( + comparator.compare(pointWithLowerPrioValue, pointWithHigherPrioValue), + lessThan(0) + ); + // ...and the other way. + assertThat( + comparator.compare(pointWithHigherPrioValue, pointWithLowerPrioValue), + greaterThan(0) + ); + } + + @Test + void treatsSamePrioritiesEqually() { + Point point1 + = new Point("Point 1, with prio 4") + .withType(Point.Type.PARK_POSITION) + .withProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY, "4"); + Point point2 + = new Point("Point 2, also with prio 4") + .withType(Point.Type.PARK_POSITION) + .withProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY, "4"); + + // Let's try it the one way, ... + assertThat( + comparator.compare(point1, point2), + is(0) + ); + // ...and the other way. + assertThat( + comparator.compare(point2, point1), + is(0) + ); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/parking/ParkingPositionToPriorityFunctionTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/parking/ParkingPositionToPriorityFunctionTest.java new file mode 100644 index 0000000..212e1e8 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/parking/ParkingPositionToPriorityFunctionTest.java @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.parking; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.Dispatcher; +import org.opentcs.data.model.Point; + +/** + */ +class ParkingPositionToPriorityFunctionTest { + + private ParkingPositionToPriorityFunction priorityFunction; + + @BeforeEach + void setUp() { + priorityFunction = new ParkingPositionToPriorityFunction(); + } + + @Test + void returnsNullForNonParkingPosition() { + Point point = new Point("Some point") + .withType(Point.Type.HALT_POSITION) + .withProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY, "1"); + + assertThat(priorityFunction.apply(point), is(nullValue())); + } + + @Test + void returnsNullForParkingPositionWithoutPriorityProperty() { + Point point = new Point("Some point").withType(Point.Type.PARK_POSITION); + + assertThat(priorityFunction.apply(point), is(nullValue())); + } + + @Test + void returnsNullForParkingPositionWithNonDecimalProperty() { + Point point = new Point("Some point") + .withType(Point.Type.PARK_POSITION) + .withProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY, "abc"); + + assertThat(priorityFunction.apply(point), is(nullValue())); + } + + @Test + void returnsPriorityForParkingPositionWithDecimalProperty() { + Point point = new Point("Some point") + .withType(Point.Type.PARK_POSITION) + .withProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY, "23"); + + assertThat(priorityFunction.apply(point), is(23)); + } + +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/parking/PrioritizedParkingPositionSupplierTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/parking/PrioritizedParkingPositionSupplierTest.java new file mode 100644 index 0000000..d8def0a --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/parking/PrioritizedParkingPositionSupplierTest.java @@ -0,0 +1,177 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.parking; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.Dispatcher; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.InternalPlantModelService; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; + +/** + * Tests for {@link PrioritizedParkingPositionSupplier}. + */ +class PrioritizedParkingPositionSupplierTest { + + private PrioritizedParkingPositionSupplier supplier; + private InternalPlantModelService plantModelService; + private Router router; + private ParkingPositionToPriorityFunction priorityFunction; + + @BeforeEach + void setUp() { + plantModelService = mock(InternalPlantModelService.class); + router = mock(Router.class); + priorityFunction = new ParkingPositionToPriorityFunction(); + supplier = new PrioritizedParkingPositionSupplier(plantModelService, router, priorityFunction); + } + + @Test + void returnsPrioritizedParkingPosition() { + Point point1 = new Point("vehicle's current position"); + Point point2 = new Point("parking position without priority") + .withType(Point.Type.PARK_POSITION); + Point point3 = new Point("parking position with priority") + .withType(Point.Type.PARK_POSITION) + .withProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY, "1"); + Vehicle vehicle = new Vehicle("vehicle") + .withCurrentPosition(point1.getReference()); + + when(router.getTargetedPoints()).thenReturn(new HashSet<>()); + when(plantModelService.fetchObject(Point.class, point1.getReference())).thenReturn(point1); + when(plantModelService.fetchObjects(eq(Point.class), any())).thenReturn(setOf(point2, point3)); + when(router.getCosts(vehicle, point1, point3, Set.of())).thenReturn(1L); + + Optional<Point> result = supplier.findParkingPosition(vehicle); + assertTrue(result.isPresent()); + assertEquals(point3, result.get()); + } + + @Test + void returnsClosestPrioritizedParkingPositionForPositionsWithSamePriority() { + Point point1 = new Point("vehicle's current position"); + Point point2 = new Point("parking position with some priority") + .withType(Point.Type.PARK_POSITION) + .withProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY, "4"); + Point point3 = new Point("parking position with the same priority but closer to the vehicle") + .withType(Point.Type.PARK_POSITION) + .withProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY, "4"); + Vehicle vehicle = new Vehicle("vehicle") + .withCurrentPosition(point1.getReference()); + + when(router.getTargetedPoints()).thenReturn(new HashSet<>()); + when(plantModelService.fetchObject(Point.class, point1.getReference())).thenReturn(point1); + when(plantModelService.fetchObjects(eq(Point.class), any())).thenReturn(setOf(point2, point3)); + when(router.getCosts(vehicle, point1, point2, Set.of())).thenReturn(10L); + when(router.getCosts(vehicle, point1, point3, Set.of())).thenReturn(1L); + + Optional<Point> result = supplier.findParkingPosition(vehicle); + assertTrue(result.isPresent()); + assertEquals(point3, result.get()); + } + + @Test + void returnsHigherPrioritizedParkingPositionThanCurrentlyOccupying() { + Point point1 = new Point("vehicle's current parking position with some priority") + .withType(Point.Type.PARK_POSITION) + .withProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY, "17"); + Point point2 = new Point("parking position with lower priority") + .withType(Point.Type.PARK_POSITION) + .withProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY, "20"); + Point point3 = new Point("parking position with higher priority") + .withType(Point.Type.PARK_POSITION) + .withProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY, "12"); + Vehicle vehicle = new Vehicle("vehicle") + .withCurrentPosition(point1.getReference()); + + when(router.getTargetedPoints()).thenReturn(new HashSet<>()); + when(plantModelService.fetchObject(Point.class, point1.getReference())).thenReturn(point1); + when(plantModelService.fetchObjects(eq(Point.class), any())).thenReturn(setOf(point2, point3)); + when(router.getCosts(vehicle, point1, point3, Set.of())).thenReturn(1L); + + Optional<Point> result = supplier.findParkingPosition(vehicle); + assertTrue(result.isPresent()); + assertEquals(point3, result.get()); + } + + @Test + void returnsEmptyForOnlyPositionsAvailableWithSamePriorityAsCurrentlyOccupying() { + Point point1 = new Point("vehicle's current parking position with some priority") + .withType(Point.Type.PARK_POSITION) + .withProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY, "13"); + Point point2 = new Point("parking position with the same priority") + .withType(Point.Type.PARK_POSITION) + .withProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY, "13"); + Point point3 = new Point("another parking position with the same priority") + .withType(Point.Type.PARK_POSITION) + .withProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY, "13"); + Vehicle vehicle = new Vehicle("vehicle") + .withCurrentPosition(point1.getReference()); + + when(router.getTargetedPoints()).thenReturn(new HashSet<>()); + when(plantModelService.fetchObject(Point.class, point1.getReference())).thenReturn(point1); + when(plantModelService.fetchObjects(eq(Point.class), any())).thenReturn(setOf(point2, point3)); + + Optional<Point> result = supplier.findParkingPosition(vehicle); + assertFalse(result.isPresent()); + } + + @Test + void returnsEmptyForNoHigherPrioritizedParkingPositionsAvailable() { + Point point1 = new Point("vehicle's current parking position with some priority") + .withType(Point.Type.PARK_POSITION) + .withProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY, "13"); + Point point2 = new Point("parking position with lower priority") + .withType(Point.Type.PARK_POSITION) + .withProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY, "24"); + Point point3 = new Point("another parking position with lower priority") + .withType(Point.Type.PARK_POSITION) + .withProperty(Dispatcher.PROPKEY_PARKING_POSITION_PRIORITY, "45"); + Vehicle vehicle = new Vehicle("vehicle") + .withCurrentPosition(point1.getReference()); + + when(router.getTargetedPoints()).thenReturn(new HashSet<>()); + when(plantModelService.fetchObject(Point.class, point1.getReference())).thenReturn(point1); + when(plantModelService.fetchObjects(eq(Point.class), any())).thenReturn(setOf(point2, point3)); + + Optional<Point> result = supplier.findParkingPosition(vehicle); + assertFalse(result.isPresent()); + } + + @Test + void returnsEmptyForNoPrioritizedParkingPositionAvailable() { + Point point1 = new Point("vehicle's current position"); + Point point2 = new Point("parking position with without priority") + .withType(Point.Type.PARK_POSITION); + Point point3 = new Point("another parking position without priority") + .withType(Point.Type.PARK_POSITION); + Vehicle vehicle = new Vehicle("vehicle") + .withCurrentPosition(point1.getReference()); + + when(router.getTargetedPoints()).thenReturn(new HashSet<>()); + when(plantModelService.fetchObject(Point.class, point1.getReference())).thenReturn(point1); + when(plantModelService.fetchObjects(eq(Point.class), any())).thenReturn(setOf(point2, point3)); + + Optional<Point> result = supplier.findParkingPosition(vehicle); + assertFalse(result.isPresent()); + } + + @SuppressWarnings("unchecked") + private <T> Set<T> setOf(T... resources) { + return new HashSet<>(Arrays.asList(resources)); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/recharging/DefaultRechargePositionSupplierTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/recharging/DefaultRechargePositionSupplierTest.java new file mode 100644 index 0000000..6273068 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/phase/recharging/DefaultRechargePositionSupplierTest.java @@ -0,0 +1,247 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.phase.recharging; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.oneOf; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.Dispatcher; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.services.InternalPlantModelService; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder.Destination; + +/** + * Tests for {@link DefaultRechargePositionSupplier}. + */ +class DefaultRechargePositionSupplierTest { + + private Point currentPosition; + private Vehicle vehicle; + private LocationType rechargeLocType; + private Location rechargeLoc1; + private Location rechargeLoc2; + private Location rechargeLoc3; + private Point locationAccessPoint; + + private InternalPlantModelService plantModelService; + private Router router; + private DefaultRechargePositionSupplier rechargePosSupplier; + + @BeforeEach + void setUp() { + currentPosition = new Point("current-position"); + vehicle = new Vehicle("some-vehicle") + .withCurrentPosition(currentPosition.getReference()) + .withRechargeOperation("Do some recharging"); + + rechargeLocType = new LocationType("some-recharge-loc-type") + .withAllowedOperations(List.of(vehicle.getRechargeOperation())); + + rechargeLoc1 = new Location("recharge-loc-1", rechargeLocType.getReference()); + rechargeLoc2 = new Location("recharge-loc-2", rechargeLocType.getReference()); + rechargeLoc3 = new Location("recharge-loc-3", rechargeLocType.getReference()); + + locationAccessPoint = new Point("location-access-point"); + + Location.Link link1 + = new Location.Link(rechargeLoc1.getReference(), locationAccessPoint.getReference()); + Location.Link link2 + = new Location.Link(rechargeLoc2.getReference(), locationAccessPoint.getReference()); + Location.Link link3 + = new Location.Link(rechargeLoc3.getReference(), locationAccessPoint.getReference()); + + rechargeLoc1 = rechargeLoc1.withAttachedLinks(Set.of(link1)); + rechargeLoc2 = rechargeLoc2.withAttachedLinks(Set.of(link2)); + rechargeLoc3 = rechargeLoc3.withAttachedLinks(Set.of(link3)); + + locationAccessPoint = locationAccessPoint.withAttachedLinks(Set.of(link1, link2, link3)); + + plantModelService = mock(InternalPlantModelService.class); + router = mock(Router.class); + rechargePosSupplier = new DefaultRechargePositionSupplier(plantModelService, router); + + when(plantModelService.fetchObject(Point.class, currentPosition.getReference())) + .thenReturn(currentPosition); + when(plantModelService.fetchObject(Point.class, locationAccessPoint.getReference())) + .thenReturn(locationAccessPoint); + when(plantModelService.fetchObject(LocationType.class, rechargeLocType.getReference())) + .thenReturn(rechargeLocType); + when(plantModelService.fetchObjects(Location.class)) + .thenReturn(Set.of(rechargeLoc1, rechargeLoc2, rechargeLoc3)); + when(plantModelService.expandResources(Set.of(locationAccessPoint.getReference()))) + .thenReturn(Set.of(locationAccessPoint)); + + rechargePosSupplier.initialize(); + } + + @AfterEach + void tearDown() { + rechargePosSupplier.terminate(); + } + + @Test + void returnEmptyListForUnknownVehiclePosition() { + assertThat( + rechargePosSupplier.findRechargeSequence(vehicle.withCurrentPosition(null)), + is(empty()) + ); + } + + @Test + void returnEmptyListForUnknownRechargeOperation() { + assertThat( + rechargePosSupplier.findRechargeSequence( + vehicle.withRechargeOperation("some-unknown-recharge-operation") + ), + is(empty()) + ); + } + + @Test + void returnEmptyListForNonexistentAssignedRechargeLocation() { + assertThat( + rechargePosSupplier.findRechargeSequence( + vehicle.withProperty( + Dispatcher.PROPKEY_ASSIGNED_RECHARGE_LOCATION, + "some-unknown-location" + ) + ), + is(empty()) + ); + } + + @Test + void returnAnyForNonexistentPreferredRechargeLocation() { + List<Destination> result = rechargePosSupplier.findRechargeSequence( + vehicle.withProperty( + Dispatcher.PROPKEY_PREFERRED_RECHARGE_LOCATION, + "some-unknown-location" + ) + ); + + assertThat(result, hasSize(1)); + assertThat( + result.get(0).getDestination(), + is( + oneOf( + rechargeLoc1.getReference(), + rechargeLoc2.getReference(), + rechargeLoc2.getReference() + ) + ) + ); + } + + @Test + void returnAnyIfVehicleHasNoAssignedOrPreferredLocation() { + List<Destination> result = rechargePosSupplier.findRechargeSequence(vehicle); + + assertThat(result, hasSize(1)); + assertThat( + result.get(0).getDestination(), + is( + oneOf( + rechargeLoc1.getReference(), + rechargeLoc2.getReference(), + rechargeLoc2.getReference() + ) + ) + ); + } + + @Test + void returnAssignedRechargeLocationIfSet() { + List<Destination> result; + + result = rechargePosSupplier.findRechargeSequence( + vehicle.withProperty(Dispatcher.PROPKEY_ASSIGNED_RECHARGE_LOCATION, rechargeLoc1.getName()) + ); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getDestination(), is(rechargeLoc1.getReference())); + + result = rechargePosSupplier.findRechargeSequence( + vehicle.withProperty(Dispatcher.PROPKEY_ASSIGNED_RECHARGE_LOCATION, rechargeLoc2.getName()) + ); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getDestination(), is(rechargeLoc2.getReference())); + + result = rechargePosSupplier.findRechargeSequence( + vehicle.withProperty(Dispatcher.PROPKEY_ASSIGNED_RECHARGE_LOCATION, rechargeLoc3.getName()) + ); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getDestination(), is(rechargeLoc3.getReference())); + } + + @Test + void returnPreferredRechargeLocationIfSet() { + List<Destination> result; + + result = rechargePosSupplier.findRechargeSequence( + vehicle.withProperty(Dispatcher.PROPKEY_PREFERRED_RECHARGE_LOCATION, rechargeLoc1.getName()) + ); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getDestination(), is(rechargeLoc1.getReference())); + + result = rechargePosSupplier.findRechargeSequence( + vehicle.withProperty(Dispatcher.PROPKEY_PREFERRED_RECHARGE_LOCATION, rechargeLoc2.getName()) + ); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getDestination(), is(rechargeLoc2.getReference())); + + result = rechargePosSupplier.findRechargeSequence( + vehicle.withProperty(Dispatcher.PROPKEY_PREFERRED_RECHARGE_LOCATION, rechargeLoc3.getName()) + ); + + assertThat(result, hasSize(1)); + assertThat(result.get(0).getDestination(), is(rechargeLoc3.getReference())); + } + + @Test + void givePrecedenceToAssignedOverPreferredIfBothSet() { + vehicle = vehicle.withProperty( + Dispatcher.PROPKEY_PREFERRED_RECHARGE_LOCATION, + rechargeLoc2.getName() + ); + + List<Destination> result; + + result = rechargePosSupplier.findRechargeSequence( + vehicle.withProperty(Dispatcher.PROPKEY_ASSIGNED_RECHARGE_LOCATION, rechargeLoc1.getName()) + ); + + assertThat(result, hasSize(1)); + assertThat( + result.get(0).getDestination(), + is(rechargeLoc1.getReference()) + ); + + result = rechargePosSupplier.findRechargeSequence( + vehicle.withProperty(Dispatcher.PROPKEY_ASSIGNED_RECHARGE_LOCATION, rechargeLoc3.getName()) + ); + + assertThat(result, hasSize(1)); + assertThat( + result.get(0).getDestination(), + is(rechargeLoc3.getReference()) + ); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/rerouting/RegularDriveOrderMergerTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/rerouting/RegularDriveOrderMergerTest.java new file mode 100644 index 0000000..81213f6 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/rerouting/RegularDriveOrderMergerTest.java @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.rerouting; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.Router; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.Route; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.routing.ResourceAvoidanceExtractor; + +/** + * Test cases for {@link RegularDriveOrderMerger}. + */ +class RegularDriveOrderMergerTest { + + /** + * Class under test. + */ + private RegularDriveOrderMerger driveOrderMerger; + /** + * Test dependencies. + */ + private Router router; + + @BeforeEach + void setUp() { + router = mock(Router.class); + ResourceAvoidanceExtractor resourceAvoidanceExtractor = mock(); + driveOrderMerger = new RegularDriveOrderMerger( + router, + resourceAvoidanceExtractor + ); + + given(resourceAvoidanceExtractor.extractResourcesToAvoid(any(TransportOrder.class))) + .willReturn(ResourceAvoidanceExtractor.ResourcesToAvoid.EMPTY); + } + + @Test + void shouldMergeDriveOrders() { + // Arrange + DriveOrder orderA = createDriveOrder(10, "A", "B", "C", "D", "E", "F", "G"); + DriveOrder orderB = createDriveOrder(10, "D", "H", "I", "J"); + when(router.getCosts(any(Vehicle.class), any(Point.class), any(Point.class), anySet())) + .thenReturn(20L); + Route expected = createDriveOrder(20, "A", "B", "C", "D", "H", "I", "J").getRoute(); + + // Act + Route actual = driveOrderMerger.mergeDriveOrders( + orderA, + orderB, + new TransportOrder("t1", List.of()), + TransportOrder.ROUTE_STEP_INDEX_DEFAULT, + new Vehicle("Vehicle") + ).getRoute(); + + // Assert + assertStepsEqualsIgnoringReroutingType(expected, actual); + } + + private DriveOrder createDriveOrder(long routeCosts, String startPoint, String... pointNames) { + List<Point> points = new ArrayList<>(); + for (String pointName : pointNames) { + points.add(new Point(pointName)); + } + DriveOrder.Destination dest + = new DriveOrder.Destination(points.get(points.size() - 1).getReference()); + return new DriveOrder(dest).withRoute(createRoute(new Point(startPoint), points, routeCosts)); + } + + private Route createRoute(Point startPoint, List<Point> points, long costs) { + List<Route.Step> routeSteps = new ArrayList<>(); + Point srcPoint = startPoint; + Point destPoint = points.get(0); + Path path = new Path( + srcPoint.getName() + " --- " + destPoint.getName(), + srcPoint.getReference(), + destPoint.getReference() + ); + routeSteps.add(new Route.Step(path, srcPoint, destPoint, Vehicle.Orientation.FORWARD, 0)); + + for (int i = 1; i < points.size(); i++) { + srcPoint = points.get(i - 1); + destPoint = points.get(i); + path = new Path( + srcPoint.getName() + " --- " + destPoint.getName(), + srcPoint.getReference(), + destPoint.getReference() + ); + routeSteps.add(new Route.Step(path, srcPoint, destPoint, Vehicle.Orientation.FORWARD, i)); + } + return new Route(routeSteps, costs); + } + + private void assertStepsEqualsIgnoringReroutingType(Route routeA, Route routeB) { + assertThat(routeA.getSteps().size(), is(routeB.getSteps().size())); + for (int i = 0; i < routeA.getSteps().size(); i++) { + Route.Step stepA = routeA.getSteps().get(i); + Route.Step stepB = routeB.getSteps().get(i); + assertTrue( + Objects.equals(stepA.getPath(), stepB.getPath()) + && Objects.equals(stepA.getSourcePoint(), stepB.getSourcePoint()) + && Objects.equals(stepA.getDestinationPoint(), stepB.getDestinationPoint()) + && Objects.equals(stepA.getVehicleOrientation(), stepB.getVehicleOrientation()) + && stepA.getRouteIndex() == stepB.getRouteIndex() + && stepA.isExecutionAllowed() == stepB.isExecutionAllowed() + ); + } + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/selection/orders/ContainsLockedTargetLocationsTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/selection/orders/ContainsLockedTargetLocationsTest.java new file mode 100644 index 0000000..12f40b0 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/selection/orders/ContainsLockedTargetLocationsTest.java @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection.orders; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.TCSObject; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Point; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.TransportOrder; + +/** + * Defines test cases for {@link ContainsLockedTargetLocations}. + */ +class ContainsLockedTargetLocationsTest { + + /** + * The class to test. + */ + private ContainsLockedTargetLocations filter; + /** + * The object service to use. + */ + private TCSObjectService objectService; + /** + * The local object pool to be used by the object service. + */ + private Map<TCSObjectReference<?>, TCSObject<?>> localObjectPool; + + @BeforeEach + void setUp() { + localObjectPool = new HashMap<>(); + objectService = mock(TCSObjectService.class); + when(objectService.fetchObject(any(), ArgumentMatchers.<TCSObjectReference<?>>any())) + .thenAnswer( + invocation -> localObjectPool.get((TCSObjectReference<?>) invocation.getArgument(1)) + ); + filter = new ContainsLockedTargetLocations(objectService); + } + + @Test + void shouldFilterTransportOrderWithLockedLocation() { + Collection<String> result = filter.apply(transportOrderWithLockedLocation()); + assertFalse(result.isEmpty()); + } + + @Test + void shouldIgnoreTransportOrderWithUnlockedLocation() { + Collection<String> result = filter.apply(transportOrderWithoutLockedLocation()); + assertTrue(result.isEmpty()); + } + + @Test + void shouldIgnoreTransportOrderWithPointDestination() { + Collection<String> result = filter.apply(transportOrderWithPointDestination()); + assertTrue(result.isEmpty()); + } + + private TransportOrder transportOrderWithLockedLocation() { + List<DriveOrder> driveOrders = new ArrayList<>(); + LocationType locationType = new LocationType("LocationType-1"); + + Location location = new Location("Location-1", locationType.getReference()); + localObjectPool.put(location.getReference(), location); + DriveOrder.Destination destination = new DriveOrder.Destination(location.getReference()); + driveOrders.add(new DriveOrder(destination)); + + location = new Location("Location-2", locationType.getReference()).withLocked(true); + localObjectPool.put(location.getReference(), location); + destination = new DriveOrder.Destination(location.getReference()); + driveOrders.add(new DriveOrder(destination)); + + return new TransportOrder("TransportOrder-1", driveOrders); + } + + private TransportOrder transportOrderWithoutLockedLocation() { + List<DriveOrder> driveOrders = new ArrayList<>(); + LocationType locationType = new LocationType("LocationType-1"); + + Location location = new Location("Location-1", locationType.getReference()); + localObjectPool.put(location.getReference(), location); + DriveOrder.Destination destination = new DriveOrder.Destination(location.getReference()); + driveOrders.add(new DriveOrder(destination)); + + location = new Location("Location-2", locationType.getReference()); + localObjectPool.put(location.getReference(), location); + destination = new DriveOrder.Destination(location.getReference()); + driveOrders.add(new DriveOrder(destination)); + + return new TransportOrder("TransportOrder-1", driveOrders); + } + + private TransportOrder transportOrderWithPointDestination() { + List<DriveOrder> driveOrders = new ArrayList<>(); + + Point point = new Point("Point-1"); + localObjectPool.put(point.getReference(), point); + DriveOrder.Destination destination = new DriveOrder.Destination(point.getReference()); + driveOrders.add(new DriveOrder(destination)); + + return new TransportOrder("TransportOrder-1", driveOrders); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsAvailableForAnyOrderTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsAvailableForAnyOrderTest.java new file mode 100644 index 0000000..a6b4787 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsAvailableForAnyOrderTest.java @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection.vehicles; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderSequence; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.dispatching.DefaultDispatcherConfiguration; +import org.opentcs.strategies.basic.dispatching.OrderReservationPool; + +/** + * Test for {@link IsAvailableForAnyOrder}. + */ +class IsAvailableForAnyOrderTest { + + private IsAvailableForAnyOrder isAvailableForAnyOrder; + private Vehicle vehicleAvailableForAnyOrder; + private TransportOrder transportOrder; + private OrderReservationPool orderReservationPool; + private List<TCSObjectReference<TransportOrder>> reservationsList; + + @BeforeEach + void setUp() { + TCSObjectService objectService = mock(); + DefaultDispatcherConfiguration configuration = mock(); + orderReservationPool = mock(); + + isAvailableForAnyOrder = new IsAvailableForAnyOrder( + objectService, + orderReservationPool, + configuration + ); + + transportOrder = new TransportOrder("T1", List.of()) + .withDispensable(false); + + vehicleAvailableForAnyOrder = new Vehicle("V1") + .withIntegrationLevel(Vehicle.IntegrationLevel.TO_BE_UTILIZED) + .withCurrentPosition(new Point("p1").getReference()) + .withState(Vehicle.State.IDLE) + .withProcState(Vehicle.ProcState.IDLE); + + given(objectService.fetchObject(TransportOrder.class, transportOrder.getReference())) + .willReturn(transportOrder); + + reservationsList = new ArrayList<>(); + given(orderReservationPool.findReservations(any())) + .willReturn(reservationsList); + } + + @Test + void checkVehicleIsAvailable() { + Vehicle vehicle = vehicleAvailableForAnyOrder; + + assertTrue(isAvailableForAnyOrder.test(vehicle)); + } + + @Test + void checkVehicleIsPaused() { + Vehicle vehicle = vehicleAvailableForAnyOrder.withPaused(true); + + assertFalse(isAvailableForAnyOrder.test(vehicle)); + } + + @ParameterizedTest + @EnumSource( + value = Vehicle.IntegrationLevel.class, + names = {"TO_BE_IGNORED", "TO_BE_NOTICED", "TO_BE_RESPECTED"} + ) + void checkVehicleIsNotFullyIntegrated(Vehicle.IntegrationLevel integrationLevel) { + Vehicle vehicle = vehicleAvailableForAnyOrder.withIntegrationLevel(integrationLevel); + + assertFalse(isAvailableForAnyOrder.test(vehicle)); + } + + @Test + void checkVehicleHasNoPosition() { + Vehicle vehicle = vehicleAvailableForAnyOrder.withCurrentPosition(null); + + assertFalse(isAvailableForAnyOrder.test(vehicle)); + } + + @Test + void checkVehicleHasOrderSequence() { + Vehicle vehicle = vehicleAvailableForAnyOrder + .withOrderSequence(new OrderSequence("OS").getReference()); + + assertFalse(isAvailableForAnyOrder.test(vehicle)); + } + + @Test + void checkVehicleNeedsMoreCharging() { + Vehicle vehicle = vehicleAvailableForAnyOrder + .withEnergyLevel(10) + .withState(Vehicle.State.CHARGING); + + assertFalse(isAvailableForAnyOrder.test(vehicle)); + } + + @Test + void checkVehicleProcessesOrderThatIsNotDispensable() { + Vehicle vehicle = vehicleAvailableForAnyOrder + .withProcState(Vehicle.ProcState.PROCESSING_ORDER) + .withTransportOrder(transportOrder.getReference()); + + assertFalse(isAvailableForAnyOrder.test(vehicle)); + } + + @Test + void checkVehicleHasOrderReservation() { + Vehicle vehicle = vehicleAvailableForAnyOrder; + + reservationsList.add(transportOrder.getReference()); + + assertFalse(isAvailableForAnyOrder.test(vehicle)); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsIdleAndDegradedTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsIdleAndDegradedTest.java new file mode 100644 index 0000000..fd3a62a --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsIdleAndDegradedTest.java @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection.vehicles; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; + +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderConstants; +import org.opentcs.data.order.OrderSequence; + +/** + * Test for {@link IsIdleAndDegraded}. + */ +class IsIdleAndDegradedTest { + + private Vehicle idleAndDegradedVehicle; + private IsIdleAndDegraded isIdleAndDegraded; + + @BeforeEach + void setUp() { + isIdleAndDegraded = new IsIdleAndDegraded(); + idleAndDegradedVehicle = new Vehicle("V1") + .withIntegrationLevel(Vehicle.IntegrationLevel.TO_BE_UTILIZED) + .withState(Vehicle.State.IDLE) + .withProcState(Vehicle.ProcState.IDLE) + .withCurrentPosition(new Point("p1").getReference()) + .withEnergyLevel(10) + .withAllowedOrderTypes(Set.of(OrderConstants.TYPE_ANY)); + } + + @ParameterizedTest + @ValueSource(strings = {OrderConstants.TYPE_ANY, OrderConstants.TYPE_CHARGE}) + void checkVehicleIsIdleAndDegraded(String type) { + Vehicle vehicle = idleAndDegradedVehicle + .withAllowedOrderTypes(Set.of(type)); + assertThat(isIdleAndDegraded.apply(vehicle), hasSize(0)); + } + + @ParameterizedTest + @EnumSource( + value = Vehicle.IntegrationLevel.class, + names = {"TO_BE_IGNORED", "TO_BE_NOTICED", "TO_BE_RESPECTED"} + ) + void checkVehicleIsNotFullyIntegrated(Vehicle.IntegrationLevel integrationLevel) { + Vehicle vehicle = idleAndDegradedVehicle.withIntegrationLevel(integrationLevel); + + assertThat(isIdleAndDegraded.apply(vehicle), hasSize(1)); + } + + @Test + void checkVehicleHasNoPosition() { + Vehicle vehicle = idleAndDegradedVehicle.withCurrentPosition(null); + + assertThat(isIdleAndDegraded.apply(vehicle), hasSize(1)); + } + + @ParameterizedTest + @EnumSource( + value = Vehicle.State.class, + names = {"UNKNOWN", "UNAVAILABLE", "ERROR", "EXECUTING", "CHARGING"} + ) + void checkVehicleHasIncorrectState(Vehicle.State state) { + Vehicle vehicle = idleAndDegradedVehicle.withState(state); + + assertThat(isIdleAndDegraded.apply(vehicle), hasSize(1)); + } + + @ParameterizedTest + @EnumSource( + value = Vehicle.ProcState.class, + names = {"AWAITING_ORDER", "PROCESSING_ORDER"} + ) + void checkVehicleHasIncorrectProcState(Vehicle.ProcState procState) { + Vehicle vehicle = idleAndDegradedVehicle.withProcState(procState); + + assertThat(isIdleAndDegraded.apply(vehicle), hasSize(1)); + } + + @Test + void checkVehicleHasOrderSequence() { + Vehicle vehicle = idleAndDegradedVehicle + .withOrderSequence(new OrderSequence("OS").getReference()); + + assertThat(isIdleAndDegraded.apply(vehicle), hasSize(1)); + } + + @Test + void checkEnergyLevelIsNotDegraded() { + Vehicle vehicle = idleAndDegradedVehicle + .withEnergyLevel(100); + + assertThat(isIdleAndDegraded.apply(vehicle), hasSize(1)); + } + + @Test + void checkVehicleIsNotAllowedToCharge() { + Vehicle vehicle = idleAndDegradedVehicle + .withAllowedOrderTypes(Set.of(OrderConstants.TYPE_PARK)); + + assertThat(isIdleAndDegraded.apply(vehicle), hasSize(1)); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsParkableTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsParkableTest.java new file mode 100644 index 0000000..b98faad --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsParkableTest.java @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection.vehicles; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderConstants; +import org.opentcs.data.order.OrderSequence; + +/** + * Test for {@link IsParkable}. + */ +class IsParkableTest { + + private TCSObjectService objectService; + private IsParkable isParkable; + private Vehicle parkableVehicle; + private Point p1; + + @BeforeEach + void setUp() { + objectService = mock(); + isParkable = new IsParkable(objectService); + p1 = new Point("p1"); + parkableVehicle = new Vehicle("V1") + .withIntegrationLevel(Vehicle.IntegrationLevel.TO_BE_UTILIZED) + .withState(Vehicle.State.IDLE) + .withProcState(Vehicle.ProcState.IDLE) + .withCurrentPosition(p1.getReference()) + .withAllowedOrderTypes(Set.of(OrderConstants.TYPE_ANY)); + + given(objectService.fetchObject(Point.class, p1.getReference())) + .willReturn(p1); + } + + @ParameterizedTest + @ValueSource(strings = {OrderConstants.TYPE_ANY, OrderConstants.TYPE_PARK}) + void checkVehicleIsParkable(String type) { + Vehicle vehicle = parkableVehicle + .withAllowedOrderTypes(Set.of(type)); + assertThat(isParkable.apply(vehicle), hasSize(0)); + } + + @ParameterizedTest + @EnumSource( + value = Vehicle.IntegrationLevel.class, + names = {"TO_BE_IGNORED", "TO_BE_NOTICED", "TO_BE_RESPECTED"} + ) + void checkVehicleIsNotFullyIntegrated(Vehicle.IntegrationLevel integrationLevel) { + Vehicle vehicle = parkableVehicle.withIntegrationLevel(integrationLevel); + + assertThat(isParkable.apply(vehicle), hasSize(1)); + } + + @Test + void checkVehicleHasParkingPosition() { + p1 = p1.withType(Point.Type.PARK_POSITION); + Vehicle vehicle = parkableVehicle.withCurrentPosition(p1.getReference()); + + given(objectService.fetchObject(Point.class, p1.getReference())) + .willReturn(p1); + + assertThat(isParkable.apply(vehicle), hasSize(1)); + } + + @ParameterizedTest + @EnumSource( + value = Vehicle.State.class, + names = {"UNKNOWN", "UNAVAILABLE", "ERROR", "EXECUTING", "CHARGING"} + ) + void checkVehicleHasIncorrectState(Vehicle.State state) { + Vehicle vehicle = parkableVehicle.withState(state); + + assertThat(isParkable.apply(vehicle), hasSize(1)); + } + + @ParameterizedTest + @EnumSource( + value = Vehicle.ProcState.class, + names = {"AWAITING_ORDER", "PROCESSING_ORDER"} + ) + void checkVehicleHasIncorrectProcState(Vehicle.ProcState procState) { + Vehicle vehicle = parkableVehicle.withProcState(procState); + + assertThat(isParkable.apply(vehicle), hasSize(1)); + } + + @Test + void checkVehicleHasOrderSequence() { + Vehicle vehicle = parkableVehicle + .withOrderSequence(new OrderSequence("OS").getReference()); + + assertThat(isParkable.apply(vehicle), hasSize(1)); + } + + @Test + void checkVehicleIsNotAllowedToPark() { + Vehicle vehicle = parkableVehicle + .withAllowedOrderTypes(Set.of(OrderConstants.TYPE_CHARGE)); + + assertThat(isParkable.apply(vehicle), hasSize(1)); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsReparkableTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsReparkableTest.java new file mode 100644 index 0000000..cd72c94 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/dispatching/selection/vehicles/IsReparkableTest.java @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.dispatching.selection.vehicles; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.OrderConstants; +import org.opentcs.data.order.OrderSequence; + +/** + * Test for {@link IsRereparkable}. + */ +class IsReparkableTest { + + private TCSObjectService objectService; + private IsReparkable isReparkable; + private Vehicle reparkableVehicle; + private Point p1; + + @BeforeEach + void setUp() { + objectService = mock(); + isReparkable = new IsReparkable(objectService); + p1 = new Point("p1").withType(Point.Type.PARK_POSITION); + reparkableVehicle = new Vehicle("V1") + .withIntegrationLevel(Vehicle.IntegrationLevel.TO_BE_UTILIZED) + .withState(Vehicle.State.IDLE) + .withProcState(Vehicle.ProcState.IDLE) + .withCurrentPosition(p1.getReference()) + .withAllowedOrderTypes(Set.of(OrderConstants.TYPE_ANY)); + + given(objectService.fetchObject(Point.class, p1.getReference())) + .willReturn(p1); + } + + @ParameterizedTest + @ValueSource(strings = {OrderConstants.TYPE_ANY, OrderConstants.TYPE_PARK}) + void checkVehicleIsReparkable(String type) { + Vehicle vehicle = reparkableVehicle + .withAllowedOrderTypes(Set.of(type)); + assertThat(isReparkable.apply(vehicle), hasSize(0)); + } + + @ParameterizedTest + @EnumSource( + value = Vehicle.IntegrationLevel.class, + names = {"TO_BE_IGNORED", "TO_BE_NOTICED", "TO_BE_RESPECTED"} + ) + void checkVehicleIsNotFullyIntegrated(Vehicle.IntegrationLevel integrationLevel) { + Vehicle vehicle = reparkableVehicle.withIntegrationLevel(integrationLevel); + + assertThat(isReparkable.apply(vehicle), hasSize(1)); + } + + @Test + void checkVehicleHasParkingPosition() { + p1 = p1.withType(Point.Type.HALT_POSITION); + Vehicle vehicle = reparkableVehicle.withCurrentPosition(p1.getReference()); + + given(objectService.fetchObject(Point.class, p1.getReference())) + .willReturn(p1); + + assertThat(isReparkable.apply(vehicle), hasSize(1)); + } + + @ParameterizedTest + @EnumSource( + value = Vehicle.State.class, + names = {"UNKNOWN", "UNAVAILABLE", "ERROR", "EXECUTING", "CHARGING"} + ) + void checkVehicleHasIncorrectState(Vehicle.State state) { + Vehicle vehicle = reparkableVehicle.withState(state); + + assertThat(isReparkable.apply(vehicle), hasSize(1)); + } + + @ParameterizedTest + @EnumSource( + value = Vehicle.ProcState.class, + names = {"AWAITING_ORDER", "PROCESSING_ORDER"} + ) + void checkVehicleHasIncorrectProcState(Vehicle.ProcState procState) { + Vehicle vehicle = reparkableVehicle.withProcState(procState); + + assertThat(isReparkable.apply(vehicle), hasSize(1)); + } + + @Test + void checkVehicleHasOrderSequence() { + Vehicle vehicle = reparkableVehicle + .withOrderSequence(new OrderSequence("OS").getReference()); + + assertThat(isReparkable.apply(vehicle), hasSize(1)); + } + + @Test + void checkVehicleIsNotAllowedToPark() { + Vehicle vehicle = reparkableVehicle + .withAllowedOrderTypes(Set.of(OrderConstants.TYPE_CHARGE)); + + assertThat(isReparkable.apply(vehicle), hasSize(1)); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/DefaultRouterTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/DefaultRouterTest.java new file mode 100644 index 0000000..5a849fc --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/DefaultRouterTest.java @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.opentcs.strategies.basic.routing.PointRouter.INFINITE_COSTS; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.routing.GroupMapper; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.DriveOrder; +import org.opentcs.data.order.DriveOrder.Destination; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.routing.jgrapht.PointRouterProvider; + +/** + * Tests for {@link DefaultRouter}. + */ +class DefaultRouterTest { + + private DefaultRouter defaultRouter; + private TCSObjectService objectService; + private PointRouterProvider pointRouterProvider; + private GroupMapper routingGroupMapper; + private DefaultRouterConfiguration configuration; + private TransportOrder order; + private PointRouter pointRouter; + + @BeforeEach + public void setUp() { + objectService = mock(); + pointRouterProvider = mock(); + routingGroupMapper = mock(); + configuration = mock(); + defaultRouter = new DefaultRouter( + objectService, + pointRouterProvider, + routingGroupMapper, + configuration + ); + + pointRouter = mock(); + Point point1 = new Point("P1").withType(Point.Type.HALT_POSITION); + Point point2 = new Point("P2").withType(Point.Type.HALT_POSITION); + order = new TransportOrder( + "t1", + List.of( + new DriveOrder( + new Destination(point1.getReference()) + .withOperation(Destination.OP_MOVE) + ), + new DriveOrder( + new Destination(point2.getReference()) + .withOperation(Destination.OP_MOVE) + ) + ) + ); + + when(objectService.fetchObject(Point.class, "P1")).thenReturn(point1); + when(objectService.fetchObject(Point.class, "P2")).thenReturn(point2); + } + + @Test + void checkRoutabilityNotRoutable() { + when(pointRouterProvider.getPointRoutersByVehicleGroup()) + .thenReturn(Map.of("some-group", pointRouter)); + when(pointRouter.getCosts(any(Point.class), any(Point.class))).thenReturn(INFINITE_COSTS); + + assertThat(defaultRouter.checkRoutability(order), is(empty())); + } + + @Test + void checkRoutabilityIsRoutable() { + Vehicle vehicle = new Vehicle("some-vehicle"); + when(pointRouterProvider.getPointRoutersByVehicleGroup()) + .thenReturn(Map.of("some-group", pointRouter)); + when(pointRouter.getCosts(any(Point.class), any(Point.class))).thenReturn(50L); + when(objectService.fetchObjects(Vehicle.class)).thenReturn(Set.of(vehicle)); + when(routingGroupMapper.apply(vehicle)).thenReturn("some-group"); + + assertThat(defaultRouter.checkRoutability(order), contains(vehicle)); + } + + @Test + void checkGeneralRoutabilityNotRoutable() { + when(pointRouterProvider.getGeneralPointRouter(order)).thenReturn(pointRouter); + when(pointRouter.getCosts(any(Point.class), any(Point.class))).thenReturn(INFINITE_COSTS); + + assertThat(defaultRouter.checkGeneralRoutability(order), is(false)); + } + + @Test + void checkGeneralRoutabilityIsRoutable() { + when(pointRouterProvider.getGeneralPointRouter(order)).thenReturn(pointRouter); + when(pointRouter.getCosts(any(Point.class), any(Point.class))).thenReturn(50L); + + assertThat(defaultRouter.checkGeneralRoutability(order), is(true)); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/ResourceAvoidanceExtractorTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/ResourceAvoidanceExtractorTest.java new file mode 100644 index 0000000..601111a --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/ResourceAvoidanceExtractorTest.java @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.ObjectPropConstants; +import org.opentcs.data.model.Location; +import org.opentcs.data.model.Location.Link; +import org.opentcs.data.model.LocationType; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.routing.ResourceAvoidanceExtractor.ResourcesToAvoid; + +/** + * Tests for {@link ResourceAvoidanceExtractor}. + */ +class ResourceAvoidanceExtractorTest { + + private Point pointA; + private Point pointB; + private Point pointC; + private Path pathAB; + private Path pathBC; + private Location locationC; + + private TCSObjectService objectService; + private ResourceAvoidanceExtractor extractor; + + @BeforeEach + void setUp() { + pointA = new Point("Point-A"); + pointB = new Point("Point-B"); + pointC = new Point("Point-C"); + pathAB = new Path("Path-AB", pointA.getReference(), pointB.getReference()); + pathBC = new Path("Path-BC", pointB.getReference(), pointC.getReference()); + locationC = new Location("Location-C", new LocationType("").getReference()); + locationC = locationC.withAttachedLinks( + Set.of(new Link(locationC.getReference(), pointC.getReference())) + ); + + objectService = mock(); + when(objectService.fetchObject(Point.class, "Point-A")).thenReturn(pointA); + when(objectService.fetchObject(Point.class, "Point-B")).thenReturn(pointB); + when(objectService.fetchObject(Point.class, pointC.getReference())).thenReturn(pointC); + when(objectService.fetchObject(Path.class, "Path-AB")).thenReturn(pathAB); + when(objectService.fetchObject(Path.class, "Path-BC")).thenReturn(pathBC); + when(objectService.fetchObject(Location.class, "Location-C")).thenReturn(locationC); + + when(objectService.fetchObject(Point.class, pointA.getReference())).thenReturn(pointA); + when(objectService.fetchObject(Point.class, pointB.getReference())).thenReturn(pointB); + when(objectService.fetchObject(Path.class, pathAB.getReference())).thenReturn(pathAB); + when(objectService.fetchObject(Path.class, pathBC.getReference())).thenReturn(pathBC); + when(objectService.fetchObject(Location.class, locationC.getReference())).thenReturn(locationC); + + extractor = new ResourceAvoidanceExtractor(objectService); + } + + @Test + void shouldReturnEmptyResultForUnknownResourceName() { + TransportOrder order = new TransportOrder("some-order", List.of()) + .withProperty(ObjectPropConstants.TRANSPORT_ORDER_RESOURCES_TO_AVOID, "unknown-resource"); + + ResourcesToAvoid result = extractor.extractResourcesToAvoid(order); + + assertThat(result.getPoints()).isEmpty(); + assertThat(result.getPaths()).isEmpty(); + assertTrue(result.isEmpty()); + } + + @Test + void shouldReturnEmptyResultForEmptySet() { + ResourcesToAvoid result = extractor.extractResourcesToAvoid(Set.of()); + + assertThat(result.getPoints()).isEmpty(); + assertThat(result.getPaths()).isEmpty(); + assertTrue(result.isEmpty()); + } + + @Test + void shouldExtractResourcesTransportOrder() { + TransportOrder order = new TransportOrder("some-order", List.of()) + .withProperty( + ObjectPropConstants.TRANSPORT_ORDER_RESOURCES_TO_AVOID, + "Point-A,Path-AB,Location-C" + ); + + ResourcesToAvoid result = extractor.extractResourcesToAvoid(order); + + assertThat(result.getPoints()) + .hasSize(2) + .contains(pointA, pointC); + assertThat(result.getPaths()) + .hasSize(1) + .contains(pathAB); + assertFalse(result.isEmpty()); + } + + @Test + void shouldExtractResourcesSetOfResources() { + ResourcesToAvoid result = extractor.extractResourcesToAvoid( + Set.of( + pointA.getReference(), + pathAB.getReference(), + locationC.getReference() + ) + ); + + assertThat(result.getPoints()) + .hasSize(2) + .contains(pointA, pointC); + assertThat(result.getPaths()) + .hasSize(1) + .contains(pathAB); + assertFalse(result.isEmpty()); + } + + @Test + void shouldNotIgnoreLeadingAndTrailingWhitespace() { + TransportOrder order = new TransportOrder("some-order", List.of()) + .withProperty( + ObjectPropConstants.TRANSPORT_ORDER_RESOURCES_TO_AVOID, + "Point-A ,Point-B, Path-AB,Path-BC" + ); + + ResourcesToAvoid result = extractor.extractResourcesToAvoid(order); + + assertThat(result.getPoints()) + .hasSize(1) + .contains(pointB); + assertThat(result.getPaths()) + .hasSize(1) + .contains(pathBC); + assertFalse(result.isEmpty()); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/edgeevaluator/BoundingBoxProtrusionCheckTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/edgeevaluator/BoundingBoxProtrusionCheckTest.java new file mode 100644 index 0000000..1d07f8e --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/edgeevaluator/BoundingBoxProtrusionCheckTest.java @@ -0,0 +1,173 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.edgeevaluator; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.BoundingBox; +import org.opentcs.data.model.Couple; +import org.opentcs.strategies.basic.routing.edgeevaluator.BoundingBoxProtrusionCheck.BoundingBoxProtrusion; + +/** + * Tests for {@link BoundingBoxProtrusionCheck}. + */ +class BoundingBoxProtrusionCheckTest { + + private BoundingBoxProtrusionCheck boundingBoxProtrusionCheck; + + @BeforeEach + void setUp() { + boundingBoxProtrusionCheck = new BoundingBoxProtrusionCheck(); + } + + @Test + void detectProtrusionFromTheFront() { + BoundingBox outer = new BoundingBox(1000, 500, 1000) + .withReferenceOffset(new Couple(200, 0)); + BoundingBox inner = new BoundingBox(600, 300, 200) + .withReferenceOffset(new Couple(-100, 0)); + + BoundingBoxProtrusion result = boundingBoxProtrusionCheck.checkProtrusion(inner, outer); + + assertTrue(result.protrudesFront()); + assertFalse(result.protrudesBack()); + assertFalse(result.protrudesLeft()); + assertFalse(result.protrudesRight()); + assertFalse(result.protrudesTop()); + } + + @Test + void detectProtrusionFromTheBack() { + BoundingBox outer = new BoundingBox(1000, 500, 1000) + .withReferenceOffset(new Couple(-200, 0)); + BoundingBox inner = new BoundingBox(600, 300, 200) + .withReferenceOffset(new Couple(100, 0)); + + BoundingBoxProtrusion result = boundingBoxProtrusionCheck.checkProtrusion(inner, outer); + + assertFalse(result.protrudesFront()); + assertTrue(result.protrudesBack()); + assertFalse(result.protrudesLeft()); + assertFalse(result.protrudesRight()); + assertFalse(result.protrudesTop()); + } + + @Test + void detectProtrusionFromTheLeft() { + BoundingBox outer = new BoundingBox(1000, 500, 1000) + .withReferenceOffset(new Couple(0, 200)); + BoundingBox inner = new BoundingBox(600, 300, 200) + .withReferenceOffset(new Couple(0, -100)); + + BoundingBoxProtrusion result = boundingBoxProtrusionCheck.checkProtrusion(inner, outer); + + assertFalse(result.protrudesFront()); + assertFalse(result.protrudesBack()); + assertTrue(result.protrudesLeft()); + assertFalse(result.protrudesRight()); + assertFalse(result.protrudesTop()); + } + + @Test + void detectProtrusionFromTheRight() { + BoundingBox outer = new BoundingBox(1000, 500, 1000) + .withReferenceOffset(new Couple(0, -200)); + BoundingBox inner = new BoundingBox(600, 300, 200) + .withReferenceOffset(new Couple(0, 100)); + + BoundingBoxProtrusion result = boundingBoxProtrusionCheck.checkProtrusion(inner, outer); + + assertFalse(result.protrudesFront()); + assertFalse(result.protrudesBack()); + assertFalse(result.protrudesLeft()); + assertTrue(result.protrudesRight()); + assertFalse(result.protrudesTop()); + } + + @Test + void detectProtrusionFromTheTop() { + BoundingBox outer = new BoundingBox(1000, 500, 1000) + .withReferenceOffset(new Couple(0, 0)); + BoundingBox inner = new BoundingBox(600, 300, 1100) + .withReferenceOffset(new Couple(0, 0)); + + BoundingBoxProtrusion result = boundingBoxProtrusionCheck.checkProtrusion(inner, outer); + + assertFalse(result.protrudesFront()); + assertFalse(result.protrudesBack()); + assertFalse(result.protrudesLeft()); + assertFalse(result.protrudesRight()); + assertTrue(result.protrudesTop()); + } + + @Test + void noProtrusionWhenInnerIsFlushWithTheFrontOfOuter() { + BoundingBox outer = new BoundingBox(1000, 500, 1000) + .withReferenceOffset(new Couple(200, 0)); + BoundingBox inner = new BoundingBox(600, 300, 200) + .withReferenceOffset(new Couple(0, 0)); + + BoundingBoxProtrusion result = boundingBoxProtrusionCheck.checkProtrusion(inner, outer); + + assertFalse(result.protrudesAnywhere()); + } + + @Test + void noProtrusionWhenInnerIsFlushWithTheBackOfOuter() { + BoundingBox outer = new BoundingBox(1000, 500, 1000) + .withReferenceOffset(new Couple(-200, 0)); + BoundingBox inner = new BoundingBox(600, 300, 200) + .withReferenceOffset(new Couple(0, 0)); + + BoundingBoxProtrusion result = boundingBoxProtrusionCheck.checkProtrusion(inner, outer); + + assertFalse(result.protrudesAnywhere()); + } + + @Test + void noProtrusionWhenInnerIsFlushWithTheLeftOfOuter() { + BoundingBox outer = new BoundingBox(1000, 500, 1000) + .withReferenceOffset(new Couple(0, 200)); + BoundingBox inner = new BoundingBox(600, 300, 200) + .withReferenceOffset(new Couple(0, 100)); + + BoundingBoxProtrusion result = boundingBoxProtrusionCheck.checkProtrusion(inner, outer); + + assertFalse(result.protrudesAnywhere()); + } + + @Test + void noProtrusionWhenInnerIsFlushWithTheRightOfOuter() { + BoundingBox outer = new BoundingBox(1000, 500, 1000) + .withReferenceOffset(new Couple(0, -200)); + BoundingBox inner = new BoundingBox(600, 300, 200) + .withReferenceOffset(new Couple(0, -100)); + + BoundingBoxProtrusion result = boundingBoxProtrusionCheck.checkProtrusion(inner, outer); + + assertFalse(result.protrudesAnywhere()); + } + + @Test + void noProtrusionWhenInnerIsEqualToOuter() { + BoundingBox outer = new BoundingBox(1000, 1000, 1000); + BoundingBox inner = new BoundingBox(1000, 1000, 1000); + + BoundingBoxProtrusion result = boundingBoxProtrusionCheck.checkProtrusion(inner, outer); + + assertFalse(result.protrudesAnywhere()); + } + + @Test + void noProtrusionWhenInnerIsSmallerThanOuter() { + BoundingBox outer = new BoundingBox(1000, 1000, 1000); + BoundingBox inner = new BoundingBox(500, 500, 500); + + BoundingBoxProtrusion result = boundingBoxProtrusionCheck.checkProtrusion(inner, outer); + + assertFalse(result.protrudesAnywhere()); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorBoundingBoxTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorBoundingBoxTest.java new file mode 100644 index 0000000..c1c1b15 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorBoundingBoxTest.java @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.edgeevaluator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.BoundingBox; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; + +/** + * Tests for {@link EdgeEvaluatorBoundingBox}. + */ +class EdgeEvaluatorBoundingBoxTest { + + private TCSObjectService objectService; + private BoundingBoxProtrusionCheck protrusionCheck; + private EdgeEvaluatorBoundingBox edgeEvaluator; + + @BeforeEach + void setUp() { + objectService = mock(); + protrusionCheck = new BoundingBoxProtrusionCheck(); + edgeEvaluator = new EdgeEvaluatorBoundingBox(objectService, protrusionCheck); + } + + @Test + void excludeForwardEdge() { + Point srcPoint = new Point("1").withMaxVehicleBoundingBox(new BoundingBox(5, 5, 5)); + Point destPoint = new Point("2").withMaxVehicleBoundingBox(new BoundingBox(1, 1, 1)); + Path path = new Path("1 -- 2", srcPoint.getReference(), destPoint.getReference()); + Edge edge = new Edge(path, false); + Vehicle vehicle = new Vehicle("vehicle").withBoundingBox(new BoundingBox(3, 3, 3)); + when(objectService.fetchObject(Point.class, "2")).thenReturn(destPoint); + + double result = edgeEvaluator.computeWeight(edge, vehicle); + + assertThat(result).isEqualTo(Double.POSITIVE_INFINITY); + } + + @Test + void includeForwardEdge() { + Point srcPoint = new Point("1").withMaxVehicleBoundingBox(new BoundingBox(5, 5, 5)); + Point destPoint = new Point("2").withMaxVehicleBoundingBox(new BoundingBox(5, 5, 5)); + Path path = new Path("1 -- 2", srcPoint.getReference(), destPoint.getReference()); + Edge edge = new Edge(path, false); + Vehicle vehicle = new Vehicle("vehicle").withBoundingBox(new BoundingBox(3, 3, 3)); + when(objectService.fetchObject(Point.class, "2")).thenReturn(destPoint); + + double result = edgeEvaluator.computeWeight(edge, vehicle); + + assertThat(result).isZero(); + } + + @Test + void ignoreBoundingBoxProtrusionAtSourceVertexWithForwardEdge() { + Point srcPoint = new Point("1").withMaxVehicleBoundingBox(new BoundingBox(1, 1, 1)); + Point destPoint = new Point("2").withMaxVehicleBoundingBox(new BoundingBox(5, 5, 5)); + Path path = new Path("1 -- 2", srcPoint.getReference(), destPoint.getReference()); + Edge edge = new Edge(path, false); + Vehicle vehicle = new Vehicle("vehicle").withBoundingBox(new BoundingBox(3, 3, 3)); + when(objectService.fetchObject(Point.class, "2")).thenReturn(destPoint); + + double result = edgeEvaluator.computeWeight(edge, vehicle); + + assertThat(result).isZero(); + } + + @Test + void excludeReverseEdge() { + Point srcPoint = new Point("1").withMaxVehicleBoundingBox(new BoundingBox(1, 1, 1)); + Point destPoint = new Point("2").withMaxVehicleBoundingBox(new BoundingBox(5, 5, 5)); + Path path = new Path("1 -- 2", srcPoint.getReference(), destPoint.getReference()); + Edge edge = new Edge(path, true); + Vehicle vehicle = new Vehicle("vehicle").withBoundingBox(new BoundingBox(3, 3, 3)); + when(objectService.fetchObject(Point.class, "1")).thenReturn(srcPoint); + + double result = edgeEvaluator.computeWeight(edge, vehicle); + + assertThat(result).isEqualTo(Double.POSITIVE_INFINITY); + } + + @Test + void includeReverseEdge() { + Point srcPoint = new Point("1").withMaxVehicleBoundingBox(new BoundingBox(5, 5, 5)); + Point destPoint = new Point("2").withMaxVehicleBoundingBox(new BoundingBox(5, 5, 5)); + Path path = new Path("1 -- 2", srcPoint.getReference(), destPoint.getReference()); + Edge edge = new Edge(path, true); + Vehicle vehicle = new Vehicle("vehicle").withBoundingBox(new BoundingBox(3, 3, 3)); + when(objectService.fetchObject(Point.class, "1")).thenReturn(srcPoint); + + double result = edgeEvaluator.computeWeight(edge, vehicle); + + assertThat(result).isZero(); + } + + @Test + void ignoreBoundingBoxProtrusionAtSourceVertexWithReverseEdge() { + Point srcPoint = new Point("1").withMaxVehicleBoundingBox(new BoundingBox(5, 5, 5)); + Point destPoint = new Point("2").withMaxVehicleBoundingBox(new BoundingBox(1, 1, 1)); + Path path = new Path("1 -- 2", srcPoint.getReference(), destPoint.getReference()); + Edge edge = new Edge(path, true); + Vehicle vehicle = new Vehicle("vehicle").withBoundingBox(new BoundingBox(3, 3, 3)); + when(objectService.fetchObject(Point.class, "1")).thenReturn(srcPoint); + + double result = edgeEvaluator.computeWeight(edge, vehicle); + + assertThat(result).isZero(); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorCompositeTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorCompositeTest.java new file mode 100644 index 0000000..6e213b6 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorCompositeTest.java @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.edgeevaluator; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.components.kernel.routing.EdgeEvaluator; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.routing.jgrapht.ShortestPathConfiguration; + +/** + */ +class EdgeEvaluatorCompositeTest { + + private static final String EVALUATOR_MOCK = "EVALUATOR_MOCK"; + private static final String EVALUATOR_1 = "EVALUATOR_1"; + private static final String EVALUATOR_2 = "EVALUATOR_2"; + private static final String EVALUATOR_3 = "EVALUATOR_3"; + + private Edge edge; + private Vehicle vehicle; + private EdgeEvaluator evaluatorMock; + private ShortestPathConfiguration configuration; + private final Map<String, EdgeEvaluator> evaluators = new HashMap<>(); + + @BeforeEach + void setUp() { + Point srcPoint = new Point("srcPoint"); + Point dstPoint = new Point("dstPoint"); + + edge = new Edge( + new Path("pathName", srcPoint.getReference(), dstPoint.getReference()), + true + ); + vehicle = new Vehicle("someVehicle"); + + configuration = mock(ShortestPathConfiguration.class); + + evaluatorMock = mock(EdgeEvaluator.class); + + evaluators.put(EVALUATOR_MOCK, evaluatorMock); + evaluators.put(EVALUATOR_1, new FixedValueEdgeEvaluator(1.0)); + evaluators.put(EVALUATOR_2, new FixedValueEdgeEvaluator(0.9)); + evaluators.put(EVALUATOR_3, new FixedValueEdgeEvaluator(Double.POSITIVE_INFINITY)); + } + + @Test + void notifyOnGraphComputation() { + when(configuration.edgeEvaluators()).thenReturn(List.of(EVALUATOR_MOCK)); + EdgeEvaluatorComposite edgeEvaluator = new EdgeEvaluatorComposite(configuration, evaluators); + + verify(evaluatorMock, never()).onGraphComputationStart(vehicle); + verify(evaluatorMock, never()).onGraphComputationEnd(vehicle); + + edgeEvaluator.onGraphComputationStart(vehicle); + verify(evaluatorMock).onGraphComputationStart(vehicle); + verify(evaluatorMock, never()).onGraphComputationEnd(vehicle); + + edgeEvaluator.onGraphComputationEnd(vehicle); + verify(evaluatorMock).onGraphComputationStart(vehicle); + verify(evaluatorMock).onGraphComputationEnd(vehicle); + } + + @Test + void computeZeroWithoutComponents() { + EdgeEvaluatorComposite edgeEvaluator = new EdgeEvaluatorComposite(configuration, evaluators); + + verifyNoInteractions(evaluatorMock); + assertThat(edgeEvaluator.computeWeight(edge, vehicle), is(0.0)); + } + + @Test + void computeSumOfOneComponent() { + when(configuration.edgeEvaluators()).thenReturn(List.of(EVALUATOR_1)); + EdgeEvaluatorComposite edgeEvaluator = new EdgeEvaluatorComposite(configuration, evaluators); + + verifyNoInteractions(evaluatorMock); + assertThat(edgeEvaluator.computeWeight(edge, vehicle), is(1.0)); + } + + @Test + void computeSumOfTwoComponents() { + when(configuration.edgeEvaluators()).thenReturn(List.of(EVALUATOR_1, EVALUATOR_2)); + EdgeEvaluatorComposite edgeEvaluator = new EdgeEvaluatorComposite(configuration, evaluators); + + verifyNoInteractions(evaluatorMock); + assertThat(edgeEvaluator.computeWeight(edge, vehicle), is(1.9)); + } + + @Test + void computeInfinityIfAnyComponentReturnsInfinity() { + when(configuration.edgeEvaluators()).thenReturn(List.of(EVALUATOR_1, EVALUATOR_2, EVALUATOR_3)); + EdgeEvaluatorComposite edgeEvaluator = new EdgeEvaluatorComposite(configuration, evaluators); + + verifyNoInteractions(evaluatorMock); + assertThat(edgeEvaluator.computeWeight(edge, vehicle), is(Double.POSITIVE_INFINITY)); + } + + private static class FixedValueEdgeEvaluator + implements + EdgeEvaluator { + + private final double value; + + FixedValueEdgeEvaluator(double value) { + this.value = value; + } + + @Override + public void onGraphComputationStart(Vehicle vehicle) { + } + + @Override + public void onGraphComputationEnd(Vehicle vehicle) { + } + + @Override + public double computeWeight(Edge edge, Vehicle vehicle) { + return value; + } + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorExplicitPropertiesTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorExplicitPropertiesTest.java new file mode 100644 index 0000000..3b642e0 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorExplicitPropertiesTest.java @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.edgeevaluator; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; + +/** + */ +class EdgeEvaluatorExplicitPropertiesTest { + + private EdgeEvaluatorExplicitProperties edgeEvaluator; + + private ExplicitPropertiesConfiguration configuration; + + @BeforeEach + void setUp() { + configuration = mock(ExplicitPropertiesConfiguration.class); + edgeEvaluator = new EdgeEvaluatorExplicitProperties(configuration); + } + + @Test + void extractCorrectProperties() { + Edge edge = new Edge( + new Path( + "pathName", + new Point("srcPoint").getReference(), + new Point("dstPoint").getReference() + ) + .withProperty(Router.PROPKEY_ROUTING_COST_FORWARD + "XYZ", "1234") + .withProperty(Router.PROPKEY_ROUTING_COST_REVERSE + "XYZ", "5678"), + false + ); + Vehicle vehicle = new Vehicle("someVehicle") + .withProperty(Router.PROPKEY_ROUTING_GROUP, "XYZ"); + + assertThat(edgeEvaluator.computeWeight(edge, vehicle), is(1234.0)); + + Edge reverseEdge = new Edge(edge.getPath(), true); + + assertThat(edgeEvaluator.computeWeight(reverseEdge, vehicle), is(5678.0)); + } + + @Test + void useConfiguredDefaultValue() { + when(configuration.defaultValue()).thenReturn("123.456"); + + Edge edge = new Edge( + new Path("pathName", new Point("srcPoint").getReference(), + new Point("dstPoint").getReference()), + false + ); + Vehicle vehicle = new Vehicle("someVehicle") + .withProperty(Router.PROPKEY_ROUTING_GROUP, "XYZ"); + + assertThat(edgeEvaluator.computeWeight(edge, vehicle), is(123.456)); + + Edge reverseEdge = new Edge(edge.getPath(), true); + + assertThat(edgeEvaluator.computeWeight(reverseEdge, vehicle), is(123.456)); + } + + @Test + void handleInvalidDefaultValue() { + when(configuration.defaultValue()).thenReturn("some invalid value"); + + Edge edge = new Edge( + new Path("pathName", new Point("srcPoint").getReference(), + new Point("dstPoint").getReference()), + false + ); + Vehicle vehicle = new Vehicle("someVehicle") + .withProperty(Router.PROPKEY_ROUTING_GROUP, "XYZ"); + + assertThat(edgeEvaluator.computeWeight(edge, vehicle), is(Double.POSITIVE_INFINITY)); + + Edge reverseEdge = new Edge(edge.getPath(), true); + + assertThat(edgeEvaluator.computeWeight(reverseEdge, vehicle), is(Double.POSITIVE_INFINITY)); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorTravelTimeTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorTravelTimeTest.java new file mode 100644 index 0000000..07222c3 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/edgeevaluator/EdgeEvaluatorTravelTimeTest.java @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.edgeevaluator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentcs.strategies.basic.routing.PointRouter.INFINITE_COSTS; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; + +/** + */ +class EdgeEvaluatorTravelTimeTest { + + private EdgeEvaluatorTravelTime edgeEvaluator; + + @BeforeEach + void setUp() { + edgeEvaluator = new EdgeEvaluatorTravelTime(); + } + + @Test + void computeTravelTime() { + Edge edge = new Edge( + new Path( + "pathName", + new Point("srcPoint").getReference(), + new Point("dstPoint").getReference() + ) + .withLength(10000) + .withMaxVelocity(1000) + .withMaxReverseVelocity(500), + false + ); + Vehicle vehicle = new Vehicle("someVehicle"); + + // Length is 10 meters, maximum velocity is 1 m/s. -> The weight should be 10 (seconds). + assertEquals(10.0, edgeEvaluator.computeWeight(edge, vehicle), 0.0); + + Edge reverseEdge = new Edge(edge.getPath(), true); + + // Length is 10 meters, maximum velocity is 0.5 m/s. -> The weight should be 20 (seconds). + assertEquals(20.0, edgeEvaluator.computeWeight(reverseEdge, vehicle), 0.0); + } + + @Test + void infiniteCostsForUntraversablePaths() { + Edge edge = new Edge( + new Path( + "pathName", + new Point("srcPoint").getReference(), + new Point("dstPoint").getReference() + ) + .withLength(10000) + .withMaxVelocity(0) + .withMaxReverseVelocity(0), + false + ); + Vehicle vehicle = new Vehicle("someVehicle"); + + // Expect the weight/costs to be infinite. + assertEquals(INFINITE_COSTS, edgeEvaluator.computeWeight(edge, vehicle), 0.0); + + Edge reverseEdge = new Edge(edge.getPath(), true); + + // Expect the weight/costs to be infinite. + assertEquals(INFINITE_COSTS, edgeEvaluator.computeWeight(reverseEdge, vehicle), 0.0); + } + +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/DefaultModelGraphMapperTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/DefaultModelGraphMapperTest.java new file mode 100644 index 0000000..d44d5c0 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/DefaultModelGraphMapperTest.java @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Map; +import java.util.Set; +import org.jgrapht.Graph; +import org.jgrapht.graph.DirectedWeightedMultigraph; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.components.kernel.routing.EdgeEvaluator; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.routing.edgeevaluator.EdgeEvaluatorComposite; + +/** + * Tests for {@link DefaultModelGraphMapper}. + */ +class DefaultModelGraphMapperTest { + + private Set<Point> points; + private Path pathAB; + private Path pathBC; + private Set<Path> paths; + + private Vehicle vehicle; + + private MapperComponentsFactory mapperComponentsFactory; + private PointVertexMapper pointVertexMapper; + private PathEdgeMapper pathEdgeMapper; + private DefaultModelGraphMapper mapper; + + @BeforeEach + void setUp() { + Point pointA = new Point("A"); + Point pointB = new Point("B"); + Point pointC = new Point("C"); + points = Set.of(pointA, pointB, pointC); + pathAB = new Path("A-->B", pointA.getReference(), pointB.getReference()); + pathBC = new Path("B-->C", pointB.getReference(), pointC.getReference()); + paths = Set.of(pathAB, pathBC); + vehicle = new Vehicle("someVehicle"); + + mapperComponentsFactory = mock(MapperComponentsFactory.class); + pointVertexMapper = mock(PointVertexMapper.class); + when(mapperComponentsFactory.createPointVertexMapper()) + .thenReturn(pointVertexMapper); + pathEdgeMapper = mock(PathEdgeMapper.class); + when(mapperComponentsFactory.createPathEdgeMapper(any(EdgeEvaluator.class), anyBoolean())) + .thenReturn(pathEdgeMapper); + mapper = new DefaultModelGraphMapper( + mock(EdgeEvaluatorComposite.class), + mapperComponentsFactory + ); + + } + + @Test + void translateModelToGraph() { + when(pointVertexMapper.translatePoints(any())).thenReturn(Set.of("A", "B", "C")); + Edge edgeAB = new Edge(pathAB, false); + Edge edgeBC = new Edge(pathBC, false); + when(pathEdgeMapper.translatePaths(any(), any())) + .thenReturn(Map.of(edgeAB, 42.0, edgeBC, 29.0)); + + Graph<String, Edge> result = mapper.translateModel(points, paths, vehicle); + + assertThat(result.vertexSet()) + .hasSize(3) + .contains("A", "B", "C"); + assertThat(result.edgeSet()) + .hasSize(2) + .contains(edgeAB, edgeBC); + assertThat(result.getEdgeWeight(edgeAB)).isEqualTo(42.0); + assertThat(result.getEdgeWeight(edgeBC)).isEqualTo(29.0); + verify(pointVertexMapper).translatePoints(points); + verify(pathEdgeMapper).translatePaths(paths, vehicle); + } + + @Test + void updateGraphWithChangedPaths() { + // Build the input graph with one path/edge that is expected to be updated. + Graph<String, Edge> originalGraph = new DirectedWeightedMultigraph<>(Edge.class); + originalGraph.addVertex("A"); + originalGraph.addVertex("B"); + originalGraph.addVertex("C"); + Edge edgeAB = new Edge(pathAB, false); + originalGraph.addEdge("A", "B", edgeAB); + originalGraph.setEdgeWeight(edgeAB, 42.0); + Edge edgeBC = new Edge(pathBC, false); + originalGraph.addEdge("B", "C", edgeBC); + originalGraph.setEdgeWeight(edgeBC, 29.0); + + when(pathEdgeMapper.translatePaths(any(), any())).thenReturn(Map.of(edgeAB, 79.0)); + Set<Path> changedPaths = Set.of(pathAB); + + Graph<String, Edge> result = mapper.updateGraph(changedPaths, vehicle, originalGraph); + + // Assert that the output graph contains the same vertices and edges but the weight of one of + // the edges was updated. + assertThat(result.vertexSet()) + .hasSize(3) + .contains("A", "B", "C"); + assertThat(result.edgeSet()) + .hasSize(2) + .contains(edgeAB, edgeBC); + assertThat(result.getEdgeWeight(edgeAB)).isEqualTo(79.0); + assertThat(result.getEdgeWeight(edgeBC)).isEqualTo(29.0); + verify(pathEdgeMapper).translatePaths(changedPaths, vehicle); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/GraphProviderTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/GraphProviderTest.java new file mode 100644 index 0000000..cd2393b --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/GraphProviderTest.java @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.util.Set; +import org.jgrapht.graph.DirectedWeightedMultigraph; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.components.kernel.routing.GroupMapper; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.routing.jgrapht.GraphProvider.GraphResult; + +/** + * Tests for {@link GraphProvider}. + */ +class GraphProviderTest { + + private DefaultModelGraphMapper defaultModelGraphMapper; + private GroupMapper groupMapper; + private GraphMutator graphMutator; + private GraphProvider graphProvider; + + @BeforeEach + void setUp() { + defaultModelGraphMapper = mock(); + groupMapper = mock(); + graphMutator = mock(); + graphProvider = new GraphProvider( + mock(TCSObjectService.class), + mock(GeneralModelGraphMapper.class), + defaultModelGraphMapper, + groupMapper, + graphMutator + ); + } + + @Test + void deriveGraphWithSameKeyOnlyOnce() { + Vehicle vehicle = new Vehicle("some-vehicle"); + when(groupMapper.apply(vehicle)).thenReturn("some-group"); + when(defaultModelGraphMapper.translateModel(anyCollection(), anyCollection(), eq(vehicle))) + .thenReturn(new DirectedWeightedMultigraph<>(Edge.class)); + when(graphMutator.deriveGraph(anySet(), anySet(), any(GraphResult.class))) + .thenReturn( + new GraphResult( + vehicle, + Set.of(), + Set.of(), + Set.of(), + Set.of(), + new DirectedWeightedMultigraph<>(Edge.class) + ) + ); + + graphProvider.getDerivedGraphResult(vehicle, Set.of(), Set.of()); + verify(graphMutator).deriveGraph(anySet(), anySet(), any(GraphResult.class)); + + graphProvider.getDerivedGraphResult(vehicle, Set.of(), Set.of()); + verifyNoMoreInteractions(graphMutator); + } + + @Test + void deriveGraphWithSameKeyAgainAfterInvalidation() { + Vehicle vehicle = new Vehicle("some-vehicle"); + when(groupMapper.apply(vehicle)).thenReturn("some-group"); + when(defaultModelGraphMapper.translateModel(anyCollection(), anyCollection(), eq(vehicle))) + .thenReturn(new DirectedWeightedMultigraph<>(Edge.class)); + when(graphMutator.deriveGraph(anySet(), anySet(), any(GraphResult.class))) + .thenReturn( + new GraphResult( + vehicle, + Set.of(), + Set.of(), + Set.of(), + Set.of(), + new DirectedWeightedMultigraph<>(Edge.class) + ) + ); + + graphProvider.getDerivedGraphResult(vehicle, Set.of(), Set.of()); + graphProvider.invalidate(); + graphProvider.getDerivedGraphResult(vehicle, Set.of(), Set.of()); + verify(graphMutator, times(2)).deriveGraph(anySet(), anySet(), any(GraphResult.class)); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/HashedResourceSetTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/HashedResourceSetTest.java new file mode 100644 index 0000000..21e91b8 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/HashedResourceSetTest.java @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Objects; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Point; + +/** + * Tests for {@link HashedResourceSet}. + */ +class HashedResourceSetTest { + + private HashedResourceSet<Point> set; + + @BeforeEach + void setUp() { + set = new HashedResourceSet<>(points -> 13); + } + + @Test + void isInitiallyEmpty() { + assertTrue(set.isEmpty()); + assertThat(set.getResources()).isEmpty(); + assertThat(set.getHash()).isEqualTo(13); + } + + @Test + void setsResourcesAndHash() { + set.overrideResources(Set.of(new Point("1"), new Point("2"), new Point("3"))); + + assertFalse(set.isEmpty()); + assertThat(set.getResources()).hasSize(3); + assertThat(set.getHash()).isEqualTo(13); + } + + @Test + void updatesResources() { + Point originalPoint = new Point("1"); + Point updatedPoint = originalPoint.withProperty("some-key", "some-value"); + + set.overrideResources(Set.of(originalPoint, new Point("2"), new Point("3"))); + assertThat(set.getResources()).hasSize(3); + assertThat(set.getHash()).isEqualTo(13); + assertThat(set.getResources()).anyMatch( + point -> Objects.equals(point.getName(), "1") && point.getProperties().isEmpty() + ); + + set.updateResources(Set.of(updatedPoint)); + assertThat(set.getResources()).hasSize(3); + assertThat(set.getHash()).isEqualTo(13); + assertThat(set.getResources()).anyMatch( + point -> Objects.equals(point.getName(), "1") + && Objects.equals(point.getProperty("some-key"), "some-value") + ); + } + + @Test + void clearsResources() { + set.updateResources(Set.of(new Point("1"), new Point("2"), new Point("3"))); + assertFalse(set.isEmpty()); + + set.clear(); + assertTrue(set.isEmpty()); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/PathEdgeMapperTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/PathEdgeMapperTest.java new file mode 100644 index 0000000..865162b --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/PathEdgeMapperTest.java @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.components.kernel.routing.EdgeEvaluator; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.Vehicle; + +/** + * Tests for {@link PathEdgeMapper}. + */ +class PathEdgeMapperTest { + + private Point pointA; + private Point pointB; + private Point pointC; + + private Path pathAB; + private Path pathBC; + + private Vehicle vehicle; + + private PathEdgeMapper mapper; + private EdgeEvaluator edgeEvaluator; + private ShortestPathConfiguration configuration; + + @BeforeEach + void setUp() { + pointA = new Point("A"); + pointB = new Point("B"); + pointC = new Point("C"); + + pathAB = new Path("A-->B", pointA.getReference(), pointB.getReference()) + .withMaxVelocity(1000) + .withMaxReverseVelocity(0); + pathBC = new Path("B<->C", pointB.getReference(), pointC.getReference()) + .withMaxVelocity(1000) + .withMaxReverseVelocity(1000); + + vehicle = new Vehicle("someVehicle"); + + edgeEvaluator = mock(); + when(edgeEvaluator.computeWeight(any(), any())).thenReturn(42.0); + configuration = mock(); + when(configuration.algorithm()).thenReturn(ShortestPathConfiguration.Algorithm.DIJKSTRA); + mapper = new PathEdgeMapper(edgeEvaluator, true, configuration); + } + + @Test + void translateEmptyPathCollectionToEmptyMap() { + Map<Edge, Double> result = mapper.translatePaths(new HashSet<>(), vehicle); + + assertThat(result).isEmpty(); + verify(edgeEvaluator).onGraphComputationStart(vehicle); + verify(edgeEvaluator).onGraphComputationEnd(vehicle); + } + + @Test + void translateUnidirectionalPathToOneEdge() { + Map<Edge, Double> result = mapper.translatePaths(Set.of(pathAB), vehicle); + + assertThat(result).hasSize(1); + assertThat(result) + .extractingFromEntries( + entry -> entry.getKey().getPath(), + entry -> entry.getKey().isTravellingReverse(), + entry -> entry.getValue() + ) + .contains(tuple(pathAB, false, 42.0)); + verify(edgeEvaluator).onGraphComputationStart(vehicle); + verify(edgeEvaluator).onGraphComputationEnd(vehicle); + } + + @Test + void translateBidirectionalPathToTwoEdges() { + Map<Edge, Double> result = mapper.translatePaths(Set.of(pathBC), vehicle); + + assertThat(result).hasSize(2); + assertThat(result) + .extractingFromEntries( + entry -> entry.getKey().getPath(), + entry -> entry.getKey().isTravellingReverse(), + entry -> entry.getValue() + ) + .contains( + tuple(pathBC, false, 42.0), + tuple(pathBC, true, 42.0) + ); + verify(edgeEvaluator).onGraphComputationStart(vehicle); + verify(edgeEvaluator).onGraphComputationEnd(vehicle); + } + + @Test + void translateOneUnidirectionalAndOneBidirectionalPathsToThreeEdges() { + Map<Edge, Double> result = mapper.translatePaths(Set.of(pathAB, pathBC), vehicle); + + assertThat(result).hasSize(3); + assertThat(result) + .extractingFromEntries( + entry -> entry.getKey().getPath(), + entry -> entry.getKey().isTravellingReverse(), + entry -> entry.getValue() + ) + .contains( + tuple(pathAB, false, 42.0), + tuple(pathBC, false, 42.0), + tuple(pathBC, true, 42.0) + ); + verify(edgeEvaluator).onGraphComputationStart(vehicle); + verify(edgeEvaluator).onGraphComputationEnd(vehicle); + } + + @Test + void excludeLockedPaths() { + Map<Edge, Double> result = mapper.translatePaths(Set.of(pathAB.withLocked(true)), vehicle); + + assertThat(result).isEmpty(); + verify(edgeEvaluator).onGraphComputationStart(vehicle); + verify(edgeEvaluator).onGraphComputationEnd(vehicle); + } + + @Test + void includeLockedPaths() { + mapper = new PathEdgeMapper(edgeEvaluator, false, configuration); + + Map<Edge, Double> result = mapper.translatePaths(Set.of(pathAB.withLocked(true)), vehicle); + + assertThat(result).hasSize(1); + assertThat(result) + .extractingFromEntries( + entry -> entry.getKey().getPath(), + entry -> entry.getKey().isTravellingReverse(), + entry -> entry.getValue() + ) + .contains(tuple(pathAB, false, 42.0)); + verify(edgeEvaluator).onGraphComputationStart(vehicle); + verify(edgeEvaluator).onGraphComputationEnd(vehicle); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/PointRouterProviderTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/PointRouterProviderTest.java new file mode 100644 index 0000000..7a6a31e --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/PointRouterProviderTest.java @@ -0,0 +1,202 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.invocation.InvocationOnMock; +import org.opentcs.components.kernel.Router; +import org.opentcs.components.kernel.routing.GroupMapper; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.TCSObject; +import org.opentcs.data.model.Vehicle; +import org.opentcs.data.order.TransportOrder; +import org.opentcs.strategies.basic.routing.DefaultRoutingGroupMapper; +import org.opentcs.strategies.basic.routing.PointRouter; +import org.opentcs.strategies.basic.routing.PointRouterFactory; +import org.opentcs.strategies.basic.routing.ResourceAvoidanceExtractor; +import org.opentcs.strategies.basic.routing.ResourceAvoidanceExtractor.ResourcesToAvoid; + +/** + * Tests for {@link PointRouterProvider}. + */ +public class PointRouterProviderTest { + + /** + * The vehicles which are returned when asking the kernel for vehicles. + */ + private final Set<Vehicle> vehicles = new HashSet<>(); + + private TCSObjectService objectService; + private ResourceAvoidanceExtractor resourceAvoidanceExtractor; + private GroupMapper routingGroupMapper; + private PointRouterFactory pointRouterFactory; + private GraphProvider graphProvider; + private PointRouterProvider pointRouterProvider; + + @BeforeEach + public void setUp() { + objectService = mock(); + when(objectService.fetchObjects(Vehicle.class)).thenReturn(vehicles); + when(objectService.fetchObject(eq(Vehicle.class), anyString())) + .then( + o -> vehicles.stream() + .filter(t -> filterByName(o, t)) + .findFirst().orElse(null) + ); + resourceAvoidanceExtractor = mock(); + when(resourceAvoidanceExtractor.extractResourcesToAvoid(nullable(TransportOrder.class))) + .thenReturn(ResourcesToAvoid.EMPTY); + when(resourceAvoidanceExtractor.extractResourcesToAvoid(anySet())) + .thenReturn(ResourcesToAvoid.EMPTY); + routingGroupMapper = new DefaultRoutingGroupMapper(); + pointRouterFactory = mock(); + when(pointRouterFactory.createPointRouter(any(Vehicle.class), anySet(), anySet())) + .thenReturn(mock(PointRouter.class)); + graphProvider = mock(); + + pointRouterProvider = new PointRouterProvider( + objectService, + resourceAvoidanceExtractor, + routingGroupMapper, + pointRouterFactory, + graphProvider + ); + } + + @Test + void shouldProvidePointRouterForDefaultRoutingGroup() { + pointRouterProvider.getPointRouterForVehicle( + createVehicle("Vehicle-000", -1), + (TransportOrder) null + ); + pointRouterProvider.getPointRouterForVehicle( + createVehicle("Vehicle-001", -1), + (TransportOrder) null + ); + pointRouterProvider.getPointRouterForVehicle( + createVehicle("Vehicle-002", -1), + (TransportOrder) null + ); + pointRouterProvider.getPointRouterForVehicle( + createVehicle("Vehicle-003", -1), + Set.of() + ); + + verify(pointRouterFactory, times(1)).createPointRouter(any(Vehicle.class), anySet(), anySet()); + } + + @Test + void shouldProvidePointRouterForDefinedRoutingGroup() { + pointRouterProvider.getPointRouterForVehicle( + createVehicle("Vehicle-000", 1), + (TransportOrder) null + ); + pointRouterProvider.getPointRouterForVehicle( + createVehicle("Vehicle-001", 1), + (TransportOrder) null + ); + pointRouterProvider.getPointRouterForVehicle( + createVehicle("Vehicle-002", 1), + (TransportOrder) null + ); + pointRouterProvider.getPointRouterForVehicle( + createVehicle("Vehicle-003", 1), + Set.of() + ); + + verify(pointRouterFactory, times(1)).createPointRouter(any(Vehicle.class), anySet(), anySet()); + } + + @Test + void shouldProvidePointRouterForDefaultAndDefinedRoutingGroups() { + pointRouterProvider.getPointRouterForVehicle( + createVehicle("Vehicle-000", 1), + (TransportOrder) null + ); + pointRouterProvider.getPointRouterForVehicle( + createVehicle("Vehicle-001", 1), + (TransportOrder) null + ); + pointRouterProvider.getPointRouterForVehicle( + createVehicle("Vehicle-002", -1), + (TransportOrder) null + ); + pointRouterProvider.getPointRouterForVehicle( + createVehicle("Vehicle-003", 1), + Set.of() + ); + + verify(pointRouterFactory, times(2)).createPointRouter(any(Vehicle.class), anySet(), anySet()); + } + + @Test + void shouldProvideIndividualPointRouters() { + for (int x = 0; x < 15; x++) { + pointRouterProvider.getPointRouterForVehicle( + createVehicle("Vehicle-0" + x, x), (TransportOrder) null + ); + } + + verify(pointRouterFactory, times(15)).createPointRouter(any(Vehicle.class), anySet(), anySet()); + } + + /** + * Creates a vehicle with a unique id, the given name and the given routing group. + * If the routing group is negative no property will be added. + * The vehicle will be added to the kernel objects. + * + * @param name The name of the vehicle + * @param routingGroup The routing group of the vehicle + * @return The vehicle + */ + private Vehicle createVehicle(String name, int routingGroup) { + Vehicle vehicle = new Vehicle(name); + if (routingGroup >= 0) { + vehicle = vehicle.withProperty(Router.PROPKEY_ROUTING_GROUP, String.valueOf(routingGroup)); + } + vehicles.add(vehicle); + + return vehicle; + } + + /** + * Stream filter to check if the second argument of the invocation is equal to the object's name. + * + * @param mock The method call on the mock + * @param object The kernel object to compare if the call wants this object + * @return <code>true</code> if the objects name is equal to the second invocation argument + */ + private boolean filterByName(InvocationOnMock mock, TCSObject<?> object) { + Object arg1 = mock.getArguments()[1]; + //Here one can add different types for the getTCSObject method like TCSObjectReference or + //TCSResourceReference + if (arg1 instanceof String) { + return filterByName((String) arg1, object); + } + return false; + } + + /** + * Stream filter to check if object has the given name. + * + * @param name The object name to check + * @param object The kernel object to compare with the given name + * @return <code>true</code> if the objects name is equal to the given name + */ + private boolean filterByName(String name, TCSObject<?> object) { + return name.equals(object.getName()); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/PointVertexMapperTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/PointVertexMapperTest.java new file mode 100644 index 0000000..a92b415 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/PointVertexMapperTest.java @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.data.model.Point; + +/** + * Tests for {@link PointVertexMapper}. + */ +class PointVertexMapperTest { + + private Point pointA; + private Point pointB; + private Point pointC; + private Point pointD; + + private PointVertexMapper mapper; + + @BeforeEach + void setUp() { + pointA = new Point("A"); + pointB = new Point("B"); + pointC = new Point("C"); + pointD = new Point("D"); + + mapper = new PointVertexMapper(); + } + + @Test + void translateEmptyPointCollectionToEmptySet() { + Set<String> result = mapper.translatePoints(new HashSet<>()); + + assertThat(result).isEmpty(); + } + + @Test + void translatePointsToPointNames() { + Set<String> result + = mapper.translatePoints(new HashSet<>(Arrays.asList(pointA, pointB, pointC, pointD))); + + assertThat(result).hasSize(4); + assertThat(result).contains("A", "B", "C", "D"); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/ShortestPathPointRouterTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/ShortestPathPointRouterTest.java new file mode 100644 index 0000000..81a4461 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/routing/jgrapht/ShortestPathPointRouterTest.java @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.routing.jgrapht; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import org.jgrapht.Graph; +import org.jgrapht.alg.shortestpath.DijkstraShortestPath; +import org.jgrapht.graph.DirectedWeightedMultigraph; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.routing.Edge; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.order.Route.Step; +import org.opentcs.strategies.basic.routing.PointRouter; + +/** + */ +class ShortestPathPointRouterTest { + + private Point pointA; + private Point pointB; + private Point pointC; + + private Path pathAC; + + private Edge edgeAC; + + private ShortestPathPointRouter pointRouter; + + @BeforeEach + void setUp() { + pointA = new Point("A"); + pointB = new Point("B"); + pointC = new Point("C"); + + pathAC = new Path("A-->C", pointA.getReference(), pointC.getReference()); + + edgeAC = new Edge(pathAC, false); + + Graph<String, Edge> graph = new DirectedWeightedMultigraph<>(Edge.class); + + graph.addVertex(pointA.getName()); + graph.addVertex(pointB.getName()); + graph.addVertex(pointC.getName()); + + graph.addEdge(pointA.getName(), pointC.getName(), edgeAC); + graph.setEdgeWeight(edgeAC, 1234); + + pointRouter = new ShortestPathPointRouter( + new DijkstraShortestPath<>(graph), + new HashSet<>(Arrays.asList(pointA, pointB, pointC)) + ); + } + + @Test + void returnZeroCostsIfDestinationIsSource() { + assertEquals(0, pointRouter.getCosts(pointA.getReference(), pointA.getReference())); + } + + @Test + void returnEmptyRouteIfDestinationIsSource() { + List<Step> steps = pointRouter.getRouteSteps(pointA, pointA); + assertNotNull(steps); + assertThat(steps, is(empty())); + } + + @Test + void returnInfiniteCostsIfNoRouteExists() { + assertEquals( + PointRouter.INFINITE_COSTS, + pointRouter.getCosts(pointA.getReference(), pointB.getReference()) + ); + } + + @Test + void returnNullIfNoRouteExists() { + assertNull(pointRouter.getRouteSteps(pointA, pointB)); + } + + @Test + void returnGraphPathCostsForExistingRoute() { + assertEquals( + 1234, + pointRouter.getCosts(pointA.getReference(), pointC.getReference()) + ); + } + + @Test + void returnGraphPathStepsForExistingRoute() { + List<Step> steps = pointRouter.getRouteSteps(pointA, pointC); + assertNotNull(steps); + assertThat(steps, is(not(empty()))); + } + +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/scheduling/ReservationPoolTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/scheduling/ReservationPoolTest.java new file mode 100644 index 0000000..1b25b3a --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/scheduling/ReservationPoolTest.java @@ -0,0 +1,151 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.collection.IsMapWithSize.anEmptyMap; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResource; +import org.opentcs.data.model.Vehicle; + +/** + * Unit tests for {@link ReservationPool}. + */ +class ReservationPoolTest { + + private Scheduler.Client client; + private ReservationPool reservationPool; + + @BeforeEach + void setUp() { + client = new TestClient(); + reservationPool = new ReservationPool(); + } + + @Test + void claimIsEmptyInitially() { + assertThat(reservationPool.getClaim(client), is(empty())); + } + + @Test + void claimIsEmptyAfterClear() { + Set<TCSResource<?>> resources = Set.of(new Point("point1"), new Point("point2")); + List<Set<TCSResource<?>>> claim = List.of(resources); + + reservationPool.setClaim(client, claim); + reservationPool.clear(); + + assertThat(reservationPool.getClaim(client), is(empty())); + } + + @Test + void disallowUnclaimingResourcesNotPreviouslyClaimed() { + Set<TCSResource<?>> resources = Set.of(new Point("point1"), new Point("point2")); + List<Set<TCSResource<?>>> claim = List.of(resources); + + reservationPool.setClaim(client, claim); + + assertThatIllegalArgumentException().isThrownBy( + () -> reservationPool.unclaim(client, Set.of()) + ); + + reservationPool.unclaim(client, resources); + } + + @Test + void returnClaimedResources() { + Set<TCSResource<?>> resources = Set.of(new Point("point1"), new Point("point2")); + List<Set<TCSResource<?>>> claim = List.of(resources); + + reservationPool.setClaim(client, claim); + + List<Set<TCSResource<?>>> claimedResources = reservationPool.getClaim(client); + + assertThat(claimedResources, hasSize(1)); + assertThat(claimedResources.get(0), hasItems(new Point("point1"), new Point("point2"))); + } + + @Test + void confirmNextClaim() { + Set<TCSResource<?>> resources = Set.of(new Point("point1"), new Point("point2")); + List<Set<TCSResource<?>>> claim = List.of(resources); + + reservationPool.setClaim(client, claim); + + assertThat( + reservationPool.isNextInClaim(client, Set.of(new Point("point1"), new Point("point2"))), + is(true) + ); + } + + @Test + void allocatedResourcesIsEmptyInitially() { + assertThat(reservationPool.allocatedResources(client), is(empty())); + assertThat(reservationPool.getAllocations(), is(anEmptyMap())); + } + + @Test + void allocatedResourcesIsEmptyAfterClear() { + reservationPool.getReservationEntry(new Point("point1")).allocate(client); + reservationPool.clear(); + + assertThat(reservationPool.allocatedResources(client), is(empty())); + } + + @Test + void reflectAllocatedResources() { + reservationPool.getReservationEntry(new Point("point1")).allocate(client); + + assertThat(reservationPool.allocatedResources(client), hasSize(1)); + assertThat(reservationPool.getAllocations(), is(aMapWithSize(1))); + } + + @Test + void allocatedResourcesIsEmptyAfterFreeAll() { + reservationPool.getReservationEntry(new Point("point1")).allocate(client); + reservationPool.freeAll(client); + + assertThat(reservationPool.allocatedResources(client), is(empty())); + assertThat(reservationPool.getAllocations(), is(anEmptyMap())); + } + + /** + * A dummy client for cases in which we need to provide a client but do not have a real one. + */ + private static class TestClient + implements + Scheduler.Client { + + @Override + public String getId() { + return getClass().getName(); + } + + @Override + public TCSObjectReference<Vehicle> getRelatedVehicle() { + return null; + } + + @Override + public boolean allocationSuccessful(Set<TCSResource<?>> resources) { + return false; + } + + @Override + public void allocationFailed(Set<TCSResource<?>> resources) { + } + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/scheduling/modules/PausedVehicleModuleTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/scheduling/modules/PausedVehicleModuleTest.java new file mode 100644 index 0000000..5ac758c --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/scheduling/modules/PausedVehicleModuleTest.java @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling.modules; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.TCSResource; +import org.opentcs.data.model.Vehicle; + +/** + * Unit tests for {@link PausedVehicleModule}. + */ +class PausedVehicleModuleTest { + + /** + * The module to test. + */ + private PausedVehicleModule module; + + private TCSObjectService objectService; + + @BeforeEach + void setUp() { + objectService = mock(TCSObjectService.class); + module = new PausedVehicleModule(objectService, new Object()); + } + + @Test + void allowAllocationForUnpausedVehicle() { + Vehicle vehicle = new Vehicle("some-vehicle").withPaused(false); + Scheduler.Client client = new SampleClient(vehicle.getName()); + + when(objectService.fetchObject(eq(Vehicle.class), any(String.class))).thenReturn(vehicle); + + assertTrue(module.mayAllocate(client, Set.of())); + } + + @Test + void refuseAllocationForPausedVehicle() { + Vehicle vehicle = new Vehicle("some-vehicle").withPaused(true); + Scheduler.Client client = new SampleClient(vehicle.getName()); + + when(objectService.fetchObject(eq(Vehicle.class), any(String.class))).thenReturn(vehicle); + + assertFalse(module.mayAllocate(client, Set.of())); + } + + private class SampleClient + implements + Scheduler.Client { + + private final String id; + + SampleClient(String id) { + this.id = id; + } + + @Override + public String getId() { + return id; + } + + @Override + public TCSObjectReference<Vehicle> getRelatedVehicle() { + return null; + } + + @Override + public boolean allocationSuccessful( + Set<TCSResource<?>> resources + ) { + return true; + } + + @Override + public void allocationFailed( + Set<TCSResource<?>> resources + ) { + } + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/scheduling/modules/SingleVehicleBlockModuleTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/scheduling/modules/SingleVehicleBlockModuleTest.java new file mode 100644 index 0000000..f547c71 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/scheduling/modules/SingleVehicleBlockModuleTest.java @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling.modules; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentcs.components.kernel.Scheduler; +import org.opentcs.components.kernel.services.InternalPlantModelService; +import org.opentcs.data.TCSObjectReference; +import org.opentcs.data.model.Block; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; +import org.opentcs.data.model.TCSResource; +import org.opentcs.data.model.TCSResourceReference; +import org.opentcs.data.model.Vehicle; +import org.opentcs.strategies.basic.scheduling.ReservationPool; + +/** + * Unit tests for {@link SingleVehicleBlockModule}. + */ +class SingleVehicleBlockModuleTest { + + /** + * The module to test. + */ + private SingleVehicleBlockModule module; + + private ReservationPool reservationPool; + + private InternalPlantModelService plantModelService; + + @BeforeEach + void setUp() { + reservationPool = mock(ReservationPool.class); + plantModelService = mock(InternalPlantModelService.class); + module = new SingleVehicleBlockModule(reservationPool, plantModelService, new Object()); + } + + @Test + void shouldAllowAllocatioForNonBlocks() { + Scheduler.Client client = new SampleClient(); + ModelData model = new ModelData(); + + when(plantModelService.fetchObjects(eq(Block.class), any())).thenReturn(new HashSet<>()); + assertTrue(module.mayAllocate(client, model.resourcesToAllocate)); + } + + @Test + void shouldAllowAllocationForUnoccupiedBlock() { + Scheduler.Client client = new SampleClient(); + ModelData model = new ModelData(); + + when(plantModelService.fetchObjects(eq(Block.class), any())) + .thenReturn(new HashSet<>(Arrays.asList(model.getBlock()))); + when(plantModelService.expandResources(any())).thenReturn(model.getBlockResources()); + when(reservationPool.resourcesAvailableForUser(model.getBlockResources(), client)) + .thenReturn(true); + assertTrue(module.mayAllocate(client, model.getResourcesToAllocate())); + } + + @Test + void shouldDenyAllocationForOccupiedBlock() { + Scheduler.Client client = new SampleClient(); + ModelData model = new ModelData(); + + when(plantModelService.fetchObjects(eq(Block.class), any())) + .thenReturn(new HashSet<>(Arrays.asList(model.getBlock()))); + when(plantModelService.expandResources(any())).thenReturn(model.getBlockResources()); + when(reservationPool.resourcesAvailableForUser(model.getBlockResources(), client)) + .thenReturn(false); + assertFalse(module.mayAllocate(client, model.getResourcesToAllocate())); + } + + private class ModelData { + + private final Set<TCSResource<?>> blockResources = new HashSet<>(); + private final Set<TCSResource<?>> resourcesToAllocate = new HashSet<>(); + private final Block block; + + ModelData() { + Point pointA = new Point("A"); + Point pointB = new Point("B"); + Point pointC = new Point("C"); + Path pathAB = new Path("A-B", pointA.getReference(), pointB.getReference()); + Path pathBC = new Path("B-C", pointB.getReference(), pointC.getReference()); + + Set<TCSResourceReference<?>> resourceRefs = new HashSet<>(); + resourceRefs.add(pointB.getReference()); + resourceRefs.add(pointC.getReference()); + resourceRefs.add(pathAB.getReference()); + resourceRefs.add(pathBC.getReference()); + block = new Block("Block") + .withMembers(resourceRefs) + .withType(Block.Type.SINGLE_VEHICLE_ONLY); + + blockResources.add(pathAB); + blockResources.add(pointB); + blockResources.add(pathBC); + blockResources.add(pointC); + + resourcesToAllocate.add(pathAB); + resourcesToAllocate.add(pointB); + } + + Set<TCSResource<?>> getBlockResources() { + return blockResources; + } + + Set<TCSResource<?>> getResourcesToAllocate() { + return resourcesToAllocate; + } + + Block getBlock() { + return block; + } + } + + private class SampleClient + implements + Scheduler.Client { + + @Override + public String getId() { + return "SampleClient"; + } + + @Override + public TCSObjectReference<Vehicle> getRelatedVehicle() { + return null; + } + + @Override + public boolean allocationSuccessful( + Set<TCSResource<?>> resources + ) { + return true; + } + + @Override + public void allocationFailed( + Set<TCSResource<?>> resources + ) { + } + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/AreaAllocationsTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/AreaAllocationsTest.java new file mode 100644 index 0000000..b6c805a --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/AreaAllocationsTest.java @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling.modules.areaAllocation; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.GeometryFactory; +import org.opentcs.data.model.Vehicle; + +/** + * Tests for {@link AreaAllocations}. + */ +class AreaAllocationsTest { + + private AreaAllocations areaAllocations; + private Vehicle vehicle = new Vehicle("some-vehicle"); + + @BeforeEach + void setUp() { + areaAllocations = new AreaAllocations(); + areaAllocations.clearAreaAllocations(); + } + + @Test + void allowAreaAllocationWhenNoOtherAllocationsPresent() { + GeometryCollection requestedArea = createCollectionWithOneGeometry( + new Coordinate(0, 0), + new Coordinate(0, 10), + new Coordinate(10, 10), + new Coordinate(10, 0), + new Coordinate(0, 0) + ); + + assertTrue(areaAllocations.isAreaAllocationAllowed(vehicle.getReference(), requestedArea)); + } + + @Test + void prohibitAreaAllocationWhenAreaIsAllocatedByAnotherVehicle() { + // Arrange + GeometryCollection requestedArea = createCollectionWithOneGeometry( + new Coordinate(0, 0), + new Coordinate(0, 10), + new Coordinate(10, 10), + new Coordinate(10, 0), + new Coordinate(0, 0) + ); + Vehicle vehicle2 = new Vehicle("some-other-vehicle"); + // Allocate area for another vehicle. + assertTrue(areaAllocations.isAreaAllocationAllowed(vehicle2.getReference(), requestedArea)); + areaAllocations.setAreaAllocation(vehicle2.getReference(), requestedArea); + + // Act & Assert + assertFalse(areaAllocations.isAreaAllocationAllowed(vehicle.getReference(), requestedArea)); + } + + @Test + void prohibitAreaAllocationWhenAreaIsIntersectingAreaAllocatedByAnotherVehicle() { + // Arrange + GeometryCollection allocatedArea = createCollectionWithOneGeometry( + new Coordinate(0, 0), + new Coordinate(0, 10), + new Coordinate(10, 10), + new Coordinate(10, 0), + new Coordinate(0, 0) + ); + GeometryCollection requestedArea = createCollectionWithOneGeometry( + new Coordinate(5, 0), + new Coordinate(5, 10), + new Coordinate(15, 10), + new Coordinate(15, 0), + new Coordinate(5, 0) + ); + Vehicle vehicle2 = new Vehicle("some-other-vehicle"); + areaAllocations.setAreaAllocation(vehicle2.getReference(), allocatedArea); + + // Act & Assert + assertFalse(areaAllocations.isAreaAllocationAllowed(vehicle.getReference(), requestedArea)); + } + + @Test + void allowAreaAllocationWhenAreaIsIntersectingAreaAllocatedBySameVehicle() { + // Arrange + GeometryCollection allocatedArea = createCollectionWithOneGeometry( + new Coordinate(0, 0), + new Coordinate(0, 10), + new Coordinate(10, 10), + new Coordinate(10, 0), + new Coordinate(0, 0) + ); + GeometryCollection requestedArea = createCollectionWithOneGeometry( + new Coordinate(5, 0), + new Coordinate(5, 10), + new Coordinate(15, 10), + new Coordinate(15, 0), + new Coordinate(5, 0) + ); + areaAllocations.setAreaAllocation(vehicle.getReference(), allocatedArea); + + // Act & Assert + assertTrue(areaAllocations.isAreaAllocationAllowed(vehicle.getReference(), requestedArea)); + } + + private GeometryCollection createCollectionWithOneGeometry(Coordinate... coordinates) { + GeometryFactory geometryFactory = new GeometryFactory(); + return geometryFactory.createGeometryCollection( + new Geometry[]{geometryFactory.createPolygon(coordinates)} + ); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/CachingAreaProviderTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/CachingAreaProviderTest.java new file mode 100644 index 0000000..69405e2 --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/CachingAreaProviderTest.java @@ -0,0 +1,188 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling.modules.areaAllocation; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.GeometryCollection; +import org.opentcs.components.kernel.services.TCSObjectService; +import org.opentcs.data.model.Couple; +import org.opentcs.data.model.Envelope; +import org.opentcs.data.model.Path; +import org.opentcs.data.model.Point; + +/** + * Tests for {@link CachingAreaProvider}. + */ +class CachingAreaProviderTest { + + private CachingAreaProvider areaProvider; + private TCSObjectService objectService; + private Point point1; + private Point point2; + private Point point3; + private Path path1; + private Path path2; + + @BeforeEach + void setUp() { + objectService = mock(); + areaProvider = new CachingAreaProvider(objectService); + + point1 = new Point("point1"); + point2 = new Point("point2"); + point3 = new Point("point3"); + path1 = new Path("path1", point1.getReference(), point2.getReference()); + path2 = new Path("path2", point2.getReference(), point3.getReference()); + } + + @Test + void providesEmptyGeometryCollectionWhenCacheIsEmpty() { + // Arrange + areaProvider.initialize(); + + // Act & Assert: When not providing a resource set + GeometryCollection result = areaProvider.getAreas("some-envelope-key", Set.of()); + assertThat(result.getNumGeometries(), is(0)); + assertTrue(result.isEmpty()); + + // Act & Assert: When providing a resource set + result = areaProvider.getAreas("some-envelope-key", Set.of(point1, point2, path1)); + assertThat(result.getNumGeometries(), is(0)); + assertTrue(result.isEmpty()); + } + + @Test + void providesNonEmptyGeometryCollectionForKnownEnvelopeKey() { + // Arrange + point2 = point2.withVehicleEnvelopes( + Map.of( + "some-envelope-key", + new Envelope( + List.of( + new Couple(100, 0), + new Couple(100, 10), + new Couple(110, 10), + new Couple(110, 0), + new Couple(100, 0) + ) + ) + ) + ); + point3 = point3.withVehicleEnvelopes( + Map.of( + "some-envelope-key", + new Envelope( + List.of( + new Couple(110, 0), + new Couple(110, 10), + new Couple(120, 10), + new Couple(120, 0), + new Couple(110, 0) + ) + ) + ) + ); + path1 = path1.withVehicleEnvelopes( + Map.of( + "some-envelope-key", + new Envelope( + List.of( + new Couple(0, 0), + new Couple(0, 10), + new Couple(110, 10), + new Couple(110, 0), + new Couple(0, 0) + ) + ) + ) + ); + when(objectService.fetchObjects(eq(Point.class), any())) + .thenReturn(Set.of(point1, point2, point3)); + when(objectService.fetchObjects(eq(Path.class), any())) + .thenReturn(Set.of(path1, path2)); + areaProvider.initialize(); + + // Act & Assert: Three resources with envelopes + GeometryCollection result = areaProvider.getAreas( + "some-envelope-key", Set.of(point2, path1, point3, path2) + ); + assertThat(result.getNumGeometries(), is(3)); + + // Act & Assert: Only one resources with envelopes + result = areaProvider.getAreas( + "some-envelope-key", Set.of(point3, path2) + ); + assertThat(result.getNumGeometries(), is(1)); + } + + @Test + void providesEmptyGeometryCollectionForUnknownEnvelopeKey() { + // Arrange + point2 = point2.withVehicleEnvelopes( + Map.of( + "some-envelope-key", + new Envelope( + List.of( + new Couple(100, 0), + new Couple(100, 10), + new Couple(110, 10), + new Couple(110, 0), + new Couple(100, 0) + ) + ) + ) + ); + point3 = point3.withVehicleEnvelopes( + Map.of( + "some-envelope-key", + new Envelope( + List.of( + new Couple(110, 0), + new Couple(110, 10), + new Couple(120, 10), + new Couple(120, 0), + new Couple(110, 0) + ) + ) + ) + ); + path1 = path1.withVehicleEnvelopes( + Map.of( + "some-envelope-key", + new Envelope( + List.of( + new Couple(0, 0), + new Couple(0, 10), + new Couple(110, 10), + new Couple(110, 0), + new Couple(0, 0) + ) + ) + ) + ); + when(objectService.fetchObjects(eq(Point.class), any())) + .thenReturn(Set.of(point1, point2, point3)); + when(objectService.fetchObjects(eq(Path.class), any())) + .thenReturn(Set.of(path1, path2)); + areaProvider.initialize(); + + // Act & Assert + GeometryCollection result = areaProvider.getAreas( + "some-unknown-envelope-key", Set.of(point2, path1, point3, path2) + ); + assertThat(result.getNumGeometries(), is(0)); + assertTrue(result.isEmpty()); + } +} diff --git a/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/CustomGeometryFactoryTest.java b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/CustomGeometryFactoryTest.java new file mode 100644 index 0000000..56cdc1d --- /dev/null +++ b/opentcs-strategies-default/src/test/java/org/opentcs/strategies/basic/scheduling/modules/areaAllocation/CustomGeometryFactoryTest.java @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT +package org.opentcs.strategies.basic.scheduling.modules.areaAllocation; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.isA; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.Polygon; + +/** + * Tests for {@link CustomGeometryFactory}. + */ +class CustomGeometryFactoryTest { + + private CustomGeometryFactory factory; + + @BeforeEach + void setUp() { + factory = new CustomGeometryFactory(); + } + + @Test + void emptyGeometryIsAValidGeometry() { + assertTrue(CustomGeometryFactory.EMPTY_GEOMETRY.isValid()); + } + + @Test + void providesEmptyGeometryWhenCreatingPolygonWithZeroCoordinates() { + Geometry result = factory.createPolygonOrEmptyGeometry(new Coordinate[]{}); + assertThat(result, is(CustomGeometryFactory.EMPTY_GEOMETRY)); + } + + @Test + void providesEmptyGeometryWhenCreatingPolygonWithOneCoordinate() { + Geometry result = factory.createPolygonOrEmptyGeometry( + new Coordinate[]{ + new Coordinate(0, 0) + } + ); + assertThat(result, is(CustomGeometryFactory.EMPTY_GEOMETRY)); + } + + @Test + void providesEmptyGeometryWhenCreatingPolygonWithTwoCoordinates() { + Geometry result = factory.createPolygonOrEmptyGeometry( + new Coordinate[]{ + new Coordinate(0, 0), + new Coordinate(0, 0) + } + ); + assertThat(result, is(CustomGeometryFactory.EMPTY_GEOMETRY)); + } + + @Test + void providesEmptyGeometryWhenCreatingPolygonWithThreeCoordinates() { + Geometry result = factory.createPolygonOrEmptyGeometry( + new Coordinate[]{ + new Coordinate(0, 0), + new Coordinate(0, 10), + new Coordinate(0, 0) + } + ); + assertThat(result, is(CustomGeometryFactory.EMPTY_GEOMETRY)); + } + + @Test + void providesValidPolygonWhenCreatingPolygonWithSufficientAmountOfCoordinates() { + Geometry result = factory.createPolygonOrEmptyGeometry( + new Coordinate[]{ + new Coordinate(0, 0), + new Coordinate(0, 10), + new Coordinate(5, 5), + new Coordinate(0, 0) + } + ); + + assertThat(result, isA(Polygon.class)); + assertTrue(result.isValid()); + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..157eaf7 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: The openTCS Authors +// SPDX-License-Identifier: MIT + +rootProject.name = 'opentcs' + +include 'opentcs-api-base' +include 'opentcs-api-injection' +include 'opentcs-impl-configuration-gestalt' +include 'opentcs-common' +include 'opentcs-commadapter-loopback' +include 'opentcs-strategies-default' +include 'opentcs-kernel-extension-http-services' +include 'opentcs-kernel-extension-rmi-services' +include 'opentcs-kernel' +include 'opentcs-kernelcontrolcenter' +include 'opentcs-plantoverview-base' +include 'opentcs-plantoverview-common' +include 'opentcs-modeleditor' +include 'opentcs-operationsdesk' +include 'opentcs-peripheralcommadapter-loopback' +include 'opentcs-plantoverview-panel-loadgenerator' +include 'opentcs-plantoverview-panel-resourceallocation' +include 'opentcs-plantoverview-themes-default' +include 'opentcs-documentation' diff --git a/src/main/dist/LICENSE.assets.txt b/src/main/dist/LICENSE.assets.txt new file mode 100644 index 0000000..49ce653 --- /dev/null +++ b/src/main/dist/LICENSE.assets.txt @@ -0,0 +1,317 @@ +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. diff --git a/src/main/dist/LICENSE.txt b/src/main/dist/LICENSE.txt new file mode 100644 index 0000000..f6a08c1 --- /dev/null +++ b/src/main/dist/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) The openTCS Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.