Merge branch 'dev' into route-processing
Conflict resolution: - build.gradle.kts: dev version (keep nstart.cloud repo + jacoco/asm mavenCentral) - settings.gradle.kts: project name from route-processing (observatio-terrae), rest from dev - config addresses (application-local, pcp-mission-planing-service, pcp-dynamic-plan application.yaml): dev versions - menu.html: merged both branches (TGU planning + bookings items) - CatalogControllerTest.kt: merged both mocks (TguPlanningService + BallisticsService) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -124,6 +124,9 @@ default:
|
|||||||
tags:
|
tags:
|
||||||
- nstart
|
- nstart
|
||||||
|
|
||||||
|
variables:
|
||||||
|
INTERNAL_REGISTRY: "$CI_REGISTRY_IMAGE"
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
- build
|
- build
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ variables:
|
|||||||
- libs/pcp-types-lib/**/*
|
- libs/pcp-types-lib/**/*
|
||||||
- build.gradle.kts
|
- build.gradle.kts
|
||||||
- settings.gradle.kts
|
- settings.gradle.kts
|
||||||
|
- gradle.properties
|
||||||
|
- gradle/**/*
|
||||||
- .gitlab/ci/pcp-request-service.yml
|
- .gitlab/ci/pcp-request-service.yml
|
||||||
- helm/pcp-request-service/**/*
|
- helm/pcp-request-service/**/*
|
||||||
- deploy/**/*
|
- deploy/**/*
|
||||||
|
|||||||
+13
-7
@@ -8,7 +8,7 @@ plugins {
|
|||||||
kotlin("plugin.lombok") apply false
|
kotlin("plugin.lombok") apply false
|
||||||
id("org.springframework.boot") apply false
|
id("org.springframework.boot") apply false
|
||||||
id("io.spring.dependency-management") apply false
|
id("io.spring.dependency-management") apply false
|
||||||
id("org.sonarqube") apply false
|
//id("org.sonarqube") apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -16,12 +16,18 @@ allprojects {
|
|||||||
group = "space.nstart.pcp"
|
group = "space.nstart.pcp"
|
||||||
ext.set("kotlinVersion", "2.2.20")
|
ext.set("kotlinVersion", "2.2.20")
|
||||||
repositories {
|
repositories {
|
||||||
// maven {
|
maven {
|
||||||
// url = uri("https://repo.nstart.cloud/repository/maven-proxy/")
|
url = uri("https://repo.nstart.cloud/repository/maven-proxy/")
|
||||||
// }
|
}
|
||||||
mavenCentral()
|
mavenCentral {
|
||||||
mavenLocal()
|
content {
|
||||||
gradlePluginPortal()
|
includeGroup("org.jacoco")
|
||||||
|
includeGroup("org.ow2.asm")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// mavenCentral()
|
||||||
|
// mavenLocal()
|
||||||
|
// gradlePluginPortal()
|
||||||
maven { url = uri("https://jitpack.io") }
|
maven { url = uri("https://jitpack.io") }
|
||||||
|
|
||||||
exclusiveContent {
|
exclusiveContent {
|
||||||
|
|||||||
@@ -3,19 +3,21 @@
|
|||||||
pcp:
|
pcp:
|
||||||
infra:
|
infra:
|
||||||
postgres:
|
postgres:
|
||||||
host: ${PCP_POSTGRES_HOST:192.168.1.8}
|
# host: ${PCP_POSTGRES_HOST:localhost}
|
||||||
port: ${PCP_POSTGRES_PORT:5432}
|
# port: ${PCP_POSTGRES_PORT:5432}
|
||||||
|
host: ${PCP_POSTGRES_HOST:192.168.100.160}
|
||||||
|
port: ${PCP_POSTGRES_PORT:35400}
|
||||||
kafka:
|
kafka:
|
||||||
host: ${PCP_KAFKA_HOST:192.168.1.8}
|
host: ${PCP_KAFKA_HOST:localhost}
|
||||||
port: ${PCP_KAFKA_PORT:29092}
|
port: ${PCP_KAFKA_PORT:29092}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
network:
|
network:
|
||||||
host: ${PCP_NETWORK_HOST:192.168.1.8}
|
host: ${PCP_NETWORK_HOST:localhost}
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
config: 8888
|
config: 38888
|
||||||
admin: 7000
|
admin: 7000
|
||||||
route-processing: 7004
|
route-processing: 7004
|
||||||
request: 7005
|
request: 7005
|
||||||
|
|||||||
@@ -49,9 +49,12 @@ spring:
|
|||||||
default-topic: pcp.tle
|
default-topic: pcp.tle
|
||||||
datasource:
|
datasource:
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_ballistics
|
url: "${SPRING_DATASOURCE_URL}"
|
||||||
username: postgres
|
username: "${SPRING_DATASOURCE_USERNAME}"
|
||||||
password: password
|
password: "${SPRING_DATASOURCE_PASSWORD}"
|
||||||
|
# url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_ballistics
|
||||||
|
# username: postgres
|
||||||
|
# password: password
|
||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: validate
|
ddl-auto: validate
|
||||||
|
|||||||
@@ -96,9 +96,9 @@ spring:
|
|||||||
on-profile: dev
|
on-profile: dev
|
||||||
datasource:
|
datasource:
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_satellites
|
url: "${SPRING_DATASOURCE_URL}"
|
||||||
username: postgres
|
username: "${SPRING_DATASOURCE_USERNAME}"
|
||||||
password: password
|
password: "${SPRING_DATASOURCE_PASSWORD}"
|
||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: validate
|
ddl-auto: validate
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ spring:
|
|||||||
camunda:
|
camunda:
|
||||||
client:
|
client:
|
||||||
mode: self-managed
|
mode: self-managed
|
||||||
grpc-address: http://192.168.1.8:26500
|
grpc-address: http://camunda.k8s.265.nstart.cloud:30901
|
||||||
auth:
|
auth:
|
||||||
method: none
|
method: none
|
||||||
prefer-rest-over-grpc: false
|
prefer-rest-over-grpc: false
|
||||||
@@ -115,9 +115,9 @@ spring:
|
|||||||
missing-topics-fatal: false
|
missing-topics-fatal: false
|
||||||
datasource:
|
datasource:
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_missions
|
url: "${SPRING_DATASOURCE_URL}"
|
||||||
username: postgres
|
username: "${SPRING_DATASOURCE_USERNAME}"
|
||||||
password: password
|
password: "${SPRING_DATASOURCE_PASSWORD}"
|
||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: validate
|
ddl-auto: validate
|
||||||
@@ -136,7 +136,7 @@ spring:
|
|||||||
camunda:
|
camunda:
|
||||||
client:
|
client:
|
||||||
mode: self-managed
|
mode: self-managed
|
||||||
grpc-address: http://192.168.1.8:26500
|
grpc-address: http://camunda.k8s.265.nstart.cloud:30901
|
||||||
auth:
|
auth:
|
||||||
method: none
|
method: none
|
||||||
prefer-rest-over-grpc: false
|
prefer-rest-over-grpc: false
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ spring:
|
|||||||
management-base-url: ${pcp.services.request}
|
management-base-url: ${pcp.services.request}
|
||||||
datasource:
|
datasource:
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_requests
|
url: "${SPRING_DATASOURCE_URL}"
|
||||||
username: postgres
|
username: "${SPRING_DATASOURCE_USERNAME}"
|
||||||
password: password
|
password: "${SPRING_DATASOURCE_PASSWORD}"
|
||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: validate
|
ddl-auto: validate
|
||||||
|
|||||||
@@ -68,9 +68,9 @@ spring:
|
|||||||
on-profile: dev
|
on-profile: dev
|
||||||
datasource:
|
datasource:
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_stations
|
url: "${SPRING_DATASOURCE_URL}"
|
||||||
username: postgres
|
username: "${SPRING_DATASOURCE_USERNAME}"
|
||||||
password: password
|
password: "${SPRING_DATASOURCE_PASSWORD}"
|
||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: validate
|
ddl-auto: validate
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ settings:
|
|||||||
tgu-service: ${pcp.services.tgu}
|
tgu-service: ${pcp.services.tgu}
|
||||||
satellite-catalog-service: ${pcp.services.satellite-catalog}
|
satellite-catalog-service: ${pcp.services.satellite-catalog}
|
||||||
ballistics-service: ${pcp.services.ballistics}
|
ballistics-service: ${pcp.services.ballistics}
|
||||||
|
tle-monitoring-service: ${pcp.services.tle-monitoring}
|
||||||
stations-service: ${pcp.services.stations}
|
stations-service: ${pcp.services.stations}
|
||||||
slots-service: ${pcp.services.slots}
|
slots-service: ${pcp.services.slots}
|
||||||
mission-service: ${pcp.services.mission}
|
mission-service: ${pcp.services.mission}
|
||||||
|
|||||||
@@ -108,9 +108,9 @@ spring:
|
|||||||
default-topic: pcp.tle
|
default-topic: pcp.tle
|
||||||
datasource:
|
datasource:
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_slots
|
url: "${SPRING_DATASOURCE_URL}"
|
||||||
username: postgres
|
username: "${SPRING_DATASOURCE_USERNAME}"
|
||||||
password: password
|
password: "${SPRING_DATASOURCE_PASSWORD}"
|
||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: validate
|
ddl-auto: validate
|
||||||
|
|||||||
Executable
+198
@@ -0,0 +1,198 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Usage:
|
||||||
|
./create-chatgpt-archive.sh [archive.zip]
|
||||||
|
./create-chatgpt-archive.sh --list
|
||||||
|
|
||||||
|
Creates a zip archive with project files useful for code analysis.
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
MAX_FILE_SIZE_BYTES Per-file size limit. Default: 1048576
|
||||||
|
|
||||||
|
The script includes source, configuration, build metadata, CI, Helm and docs.
|
||||||
|
It excludes build outputs, logs, local caches, IDE files, archives, binaries and env files.
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
mode="create"
|
||||||
|
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "--list" ]]; then
|
||||||
|
mode="list"
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( $# > 1 )); then
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
archive_arg="${1:-pcp-chatgpt-context.zip}"
|
||||||
|
max_file_size_bytes="${MAX_FILE_SIZE_BYTES:-1048576}"
|
||||||
|
|
||||||
|
if [[ "$archive_arg" = /* ]]; then
|
||||||
|
archive_path="$archive_arg"
|
||||||
|
else
|
||||||
|
archive_path="$project_root/$archive_arg"
|
||||||
|
fi
|
||||||
|
|
||||||
|
include_paths=(
|
||||||
|
"AGENTS.md"
|
||||||
|
"README.md"
|
||||||
|
"HELP.md"
|
||||||
|
".dockerignore"
|
||||||
|
".gitattributes"
|
||||||
|
".gitignore"
|
||||||
|
".gitlab-ci.yml"
|
||||||
|
"build.gradle.kts"
|
||||||
|
"settings.gradle.kts"
|
||||||
|
"gradle.properties"
|
||||||
|
"gradlew"
|
||||||
|
"gradlew.bat"
|
||||||
|
"create-chatgpt-archive.sh"
|
||||||
|
"changeLogsDynamicSlots.md"
|
||||||
|
".gitlab"
|
||||||
|
"config-repo"
|
||||||
|
"docs"
|
||||||
|
"gradle"
|
||||||
|
"helm"
|
||||||
|
"libs"
|
||||||
|
"schemes"
|
||||||
|
"services"
|
||||||
|
)
|
||||||
|
|
||||||
|
should_skip_path() {
|
||||||
|
local path="$1"
|
||||||
|
|
||||||
|
case "$path" in
|
||||||
|
.git|.git/*|.gradle|.gradle/*|.gradle-local|.gradle-local/*|.idea|.idea/*|.kotlin|.kotlin/*|.vscode|.vscode/*)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
build|build/*|logs|logs/*|out|out/*|bin|bin/*|target|target/*|node_modules|node_modules/*)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*/build|*/build/*|*/logs|*/logs/*|*/out|*/out/*|*/bin|*/bin/*|*/target|*/target/*|*/node_modules|*/node_modules/*)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$path" in
|
||||||
|
.env|.env.*|*/.env|*/.env.*)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*.log|*.tmp|*.bak|*.orig|*.class|*.jar|*.war|*.ear|*.zip|*.tar|*.tgz|*.tar.gz|*.gz|*.7z|*.rar)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*.png|*.jpg|*.jpeg|*.gif|*.webp|*.ico|*.pdf|*.doc|*.docx|*.xls|*.xlsx|*.ppt|*.pptx)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*.keystore|*.jks|*.p12|*.pem|*.key|*.crt)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
is_allowed_file() {
|
||||||
|
local path="$1"
|
||||||
|
|
||||||
|
case "$path" in
|
||||||
|
AGENTS.md|README.md|HELP.md|.dockerignore|.gitattributes|.gitignore|.gitlab-ci.yml)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
build.gradle.kts|settings.gradle.kts|gradle.properties|gradlew|gradlew.bat|create-chatgpt-archive.sh|changeLogsDynamicSlots.md)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
Dockerfile|Makefile|Jenkinsfile)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*.kt|*.kts|*.java|*.groovy|*.xml|*.yaml|*.yml|*.properties|*.json|*.toml|*.md|*.txt)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*.sql|*.graphql|*.graphqls|*.proto|*.bpmn|*.dmn|*.tpl|*.sh)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*.html|*.css|*.scss|*.js|*.jsx|*.ts|*.tsx|*.vue)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest_file="$(mktemp)"
|
||||||
|
trap 'rm -f "$manifest_file"' EXIT
|
||||||
|
|
||||||
|
collect_files() {
|
||||||
|
local root_path
|
||||||
|
local relative_path
|
||||||
|
local file_size
|
||||||
|
|
||||||
|
for root_path in "${include_paths[@]}"; do
|
||||||
|
if [[ ! -e "$project_root/$root_path" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$project_root/$root_path" ]]; then
|
||||||
|
printf '%s\n' "$root_path"
|
||||||
|
else
|
||||||
|
(cd "$project_root" && find "$root_path" -type f -print)
|
||||||
|
fi
|
||||||
|
done | while IFS= read -r relative_path; do
|
||||||
|
relative_path="${relative_path#./}"
|
||||||
|
|
||||||
|
if should_skip_path "$relative_path"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! is_allowed_file "$relative_path"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
file_size="$(stat -c '%s' "$project_root/$relative_path")"
|
||||||
|
if (( file_size > max_file_size_bytes )); then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$relative_path"
|
||||||
|
done | sort -u
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_files > "$manifest_file"
|
||||||
|
|
||||||
|
file_count="$(wc -l < "$manifest_file" | tr -d ' ')"
|
||||||
|
if (( file_count == 0 )); then
|
||||||
|
echo "No files selected for archive." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$mode" == "list" ]]; then
|
||||||
|
cat "$manifest_file"
|
||||||
|
echo
|
||||||
|
echo "Selected files: $file_count" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v zip >/dev/null 2>&1; then
|
||||||
|
echo "zip command is required but was not found." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$archive_path")"
|
||||||
|
rm -f "$archive_path"
|
||||||
|
|
||||||
|
(cd "$project_root" && zip -q -X "$archive_path" -@ < "$manifest_file")
|
||||||
|
|
||||||
|
archive_size="$(du -h "$archive_path" | awk '{print $1}')"
|
||||||
|
echo "Archive created: $archive_path"
|
||||||
|
echo "Selected files: $file_count"
|
||||||
|
echo "Archive size: $archive_size"
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
|
||||||
networkTimeout=60000
|
networkTimeout=300000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -9,3 +9,19 @@ service:
|
|||||||
ingress:
|
ingress:
|
||||||
enabled: true
|
enabled: true
|
||||||
host: pcp-ballistics-service.dev.k8s.264.nstart.cloud
|
host: pcp-ballistics-service.dev.k8s.264.nstart.cloud
|
||||||
|
|
||||||
|
extraEnv:
|
||||||
|
- name: SERVER_PORT
|
||||||
|
value: "8080"
|
||||||
|
- name: SPRING_DATASOURCE_URL
|
||||||
|
value: jdbc:postgresql://pcp-postgres.pcp-dev.svc.altum.local:5432/pcp_ballistics
|
||||||
|
- name: SPRING_DATASOURCE_USERNAME
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: pcp-ballistics-db
|
||||||
|
key: SPRING_DATASOURCE_USERNAME
|
||||||
|
- name: SPRING_DATASOURCE_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: pcp-ballistics-db
|
||||||
|
key: SPRING_DATASOURCE_PASSWORD
|
||||||
@@ -9,3 +9,19 @@ service:
|
|||||||
ingress:
|
ingress:
|
||||||
enabled: true
|
enabled: true
|
||||||
host: pcp-complex-mission-service.dev.k8s.264.nstart.cloud
|
host: pcp-complex-mission-service.dev.k8s.264.nstart.cloud
|
||||||
|
|
||||||
|
extraEnv:
|
||||||
|
- name: SERVER_PORT
|
||||||
|
value: "8080"
|
||||||
|
- name: SPRING_DATASOURCE_URL
|
||||||
|
value: jdbc:postgresql://pcp-postgres.pcp-dev.svc.altum.local:5432/pcp_complex_missions
|
||||||
|
- name: SPRING_DATASOURCE_USERNAME
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: pcp-complex-missions-db
|
||||||
|
key: SPRING_DATASOURCE_USERNAME
|
||||||
|
- name: SPRING_DATASOURCE_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: pcp-complex-missions-db
|
||||||
|
key: SPRING_DATASOURCE_PASSWORD
|
||||||
|
|||||||
@@ -13,3 +13,15 @@ ingress:
|
|||||||
extraEnv:
|
extraEnv:
|
||||||
- name: SERVER_PORT
|
- name: SERVER_PORT
|
||||||
value: "8080"
|
value: "8080"
|
||||||
|
- name: SPRING_DATASOURCE_URL
|
||||||
|
value: jdbc:postgresql://pcp-postgres.pcp-dev.svc.altum.local:5432/pcp_missions
|
||||||
|
- name: SPRING_DATASOURCE_USERNAME
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: pcp-missions-db
|
||||||
|
key: SPRING_DATASOURCE_USERNAME
|
||||||
|
- name: SPRING_DATASOURCE_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: pcp-missions-db
|
||||||
|
key: SPRING_DATASOURCE_PASSWORD
|
||||||
@@ -13,3 +13,15 @@ ingress:
|
|||||||
extraEnv:
|
extraEnv:
|
||||||
- name: SERVER_PORT
|
- name: SERVER_PORT
|
||||||
value: "8080"
|
value: "8080"
|
||||||
|
- name: SPRING_DATASOURCE_URL
|
||||||
|
value: jdbc:postgresql://pcp-postgres.pcp-dev.svc.altum.local:5432/pcp_requests
|
||||||
|
- name: SPRING_DATASOURCE_USERNAME
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: pcp-requests-db
|
||||||
|
key: SPRING_DATASOURCE_USERNAME
|
||||||
|
- name: SPRING_DATASOURCE_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: pcp-requests-db
|
||||||
|
key: SPRING_DATASOURCE_PASSWORD
|
||||||
@@ -13,3 +13,15 @@ ingress:
|
|||||||
extraEnv:
|
extraEnv:
|
||||||
- name: SERVER_PORT
|
- name: SERVER_PORT
|
||||||
value: "8080"
|
value: "8080"
|
||||||
|
- name: SPRING_DATASOURCE_URL
|
||||||
|
value: jdbc:postgresql://pcp-postgres.pcp-dev.svc.altum.local:5432/pcp_stations
|
||||||
|
- name: SPRING_DATASOURCE_USERNAME
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: pcp-stations-db
|
||||||
|
key: SPRING_DATASOURCE_USERNAME
|
||||||
|
- name: SPRING_DATASOURCE_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: pcp-stations-db
|
||||||
|
key: SPRING_DATASOURCE_PASSWORD
|
||||||
@@ -13,3 +13,5 @@ ingress:
|
|||||||
extraEnv:
|
extraEnv:
|
||||||
- name: SERVER_PORT
|
- name: SERVER_PORT
|
||||||
value: "8080"
|
value: "8080"
|
||||||
|
- name: SETTINGS_TLE_MONITORING_SERVICE
|
||||||
|
value: "http://tle-monitoring-service:8080"
|
||||||
|
|||||||
@@ -12,3 +12,5 @@ ingress:
|
|||||||
extraEnv:
|
extraEnv:
|
||||||
- name: SERVER_PORT
|
- name: SERVER_PORT
|
||||||
value: "8080"
|
value: "8080"
|
||||||
|
- name: SETTINGS_TLE_MONITORING_SERVICE
|
||||||
|
value: "http://tle-monitoring-service:8080"
|
||||||
|
|||||||
@@ -23,3 +23,15 @@ extraEnv:
|
|||||||
value: "10"
|
value: "10"
|
||||||
- name: SPRING_FLYWAY_CONNECT_RETRIES_INTERVAL
|
- name: SPRING_FLYWAY_CONNECT_RETRIES_INTERVAL
|
||||||
value: "5s"
|
value: "5s"
|
||||||
|
- name: SPRING_DATASOURCE_URL
|
||||||
|
value: jdbc:postgresql://pcp-postgres.pcp-dev.svc.altum.local:5432/pcp_slots
|
||||||
|
- name: SPRING_DATASOURCE_USERNAME
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: pcp-slots-db
|
||||||
|
key: SPRING_DATASOURCE_USERNAME
|
||||||
|
- name: SPRING_DATASOURCE_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: pcp-slots-db
|
||||||
|
key: SPRING_DATASOURCE_PASSWORD
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ plugins {
|
|||||||
kotlin("plugin.lombok")
|
kotlin("plugin.lombok")
|
||||||
id("org.springframework.boot")
|
id("org.springframework.boot")
|
||||||
id("io.spring.dependency-management")
|
id("io.spring.dependency-management")
|
||||||
id("org.sonarqube")
|
//id("org.sonarqube")
|
||||||
jacoco
|
jacoco
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import ballistics.Ballistics
|
import ballistics.Ballistics
|
||||||
|
import ballistics.types.EarthType
|
||||||
import ballistics.types.InitialConditions
|
import ballistics.types.InitialConditions
|
||||||
import ballistics.types.IntegrationType
|
import ballistics.types.IntegrationType
|
||||||
import ballistics.types.ModDVType
|
import ballistics.types.ModDVType
|
||||||
import ballistics.types.OrbitalPoint
|
import ballistics.types.OrbitalPoint
|
||||||
|
import ballistics.utils.astro.AstronomerJ2000
|
||||||
import ballistics.utils.fromDateTime
|
import ballistics.utils.fromDateTime
|
||||||
import ballistics.utils.math.Vector3D
|
import ballistics.utils.math.Vector3D
|
||||||
import ballistics.utils.toDateTime
|
import ballistics.utils.toDateTime
|
||||||
@@ -15,106 +17,38 @@ fun main() {
|
|||||||
var ts = System.currentTimeMillis()
|
var ts = System.currentTimeMillis()
|
||||||
val r = Ballistics()
|
val r = Ballistics()
|
||||||
|
|
||||||
r.modDVType = ModDVType.BARS // BARS
|
r.modDVType = ModDVType.KONDOR // BARS
|
||||||
r.integrationType = IntegrationType.ADAMS7
|
r.integrationType = IntegrationType.RUNG4
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
val tt2 = LocalDateTime.of(2028, 12, 11, 1, 24, 24, 257000000)
|
||||||
|
|
||||||
|
val op = OrbitalPoint(
|
||||||
|
fromDateTime(tt2),
|
||||||
|
1,
|
||||||
|
Vector3D(
|
||||||
|
2610612.004898679,
|
||||||
|
-3507514.947081291,
|
||||||
|
-5291534.374006776,
|
||||||
|
),
|
||||||
|
Vector3D(
|
||||||
|
-4935.515395115775,
|
||||||
|
3497.047795669036,
|
||||||
|
-4742.501356855924,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
val mnu = mutableListOf<InitialConditions>()
|
val mnu = mutableListOf<InitialConditions>()
|
||||||
mnu.add(
|
mnu.add(
|
||||||
InitialConditions(
|
InitialConditions(
|
||||||
OrbitalPoint(
|
op,
|
||||||
LocalDateTime.of(2021, 12, 3, 0, 5, 45, 0).toEpochSecond(ZoneOffset.UTC).toDouble(),
|
0.06 / 9.8,
|
||||||
1,
|
|
||||||
Vector3D(
|
|
||||||
-5357933.7872,
|
|
||||||
4328646.1787,
|
|
||||||
0.0,
|
|
||||||
),
|
|
||||||
Vector3D(
|
|
||||||
941.8995373,
|
|
||||||
1150.6919822,
|
|
||||||
7544.9920840,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
0.0061,
|
|
||||||
125.0,
|
125.0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
|
||||||
mnu.add(InitialConditions(OrbitalPoint(1681135417.0,
|
|
||||||
1499,
|
|
||||||
6782429.138044466,
|
|
||||||
-1205586.994919729,
|
|
||||||
0.0,
|
|
||||||
-272.89561674108205,
|
|
||||||
-1460.1016031205972,
|
|
||||||
7539.209413654664),
|
|
||||||
0.0,
|
|
||||||
100.0)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
mnu.add(InitialConditions(OrbitalPoint(1681203547.0,
|
|
||||||
1511,
|
|
||||||
2796871.797932365,
|
|
||||||
6295220.245166648,
|
|
||||||
0.0,
|
|
||||||
1351.432690993002,
|
|
||||||
-615.1950685524682,
|
|
||||||
7539.420044409451),
|
|
||||||
0.0,
|
|
||||||
100.0)
|
|
||||||
)
|
|
||||||
|
|
||||||
mnu.add(InitialConditions(OrbitalPoint(1681288710.0,
|
|
||||||
1526,
|
|
||||||
2219522.8066600505,
|
|
||||||
6520960.574145492,
|
|
||||||
0.0,
|
|
||||||
1401.0525909402313,
|
|
||||||
-491.90052480161194,
|
|
||||||
7539.654233369637),
|
|
||||||
0.0,
|
|
||||||
100.0)
|
|
||||||
)
|
|
||||||
|
|
||||||
mnu.add(InitialConditions(OrbitalPoint(1681373873.0,
|
|
||||||
1541,
|
|
||||||
1624190.3979707498,
|
|
||||||
6693858.330353258,
|
|
||||||
0.0,
|
|
||||||
1439.4621272728175,
|
|
||||||
-364.5959436771924,
|
|
||||||
7539.927000039702),
|
|
||||||
0.0,
|
|
||||||
100.0)
|
|
||||||
)
|
|
||||||
|
|
||||||
mnu.add(InitialConditions(OrbitalPoint(1681464714.0,
|
|
||||||
1557,
|
|
||||||
3663882.4989639986,
|
|
||||||
5832354.67692114,
|
|
||||||
0.0,
|
|
||||||
1249.158953470858,
|
|
||||||
-802.9876366520331,
|
|
||||||
7540.375561571017),
|
|
||||||
0.0,
|
|
||||||
100.0)
|
|
||||||
)
|
|
||||||
|
|
||||||
mnu.add(InitialConditions(OrbitalPoint(1681538522.0,
|
|
||||||
1570,
|
|
||||||
-2393483.7360840607,
|
|
||||||
6458206.838182452,
|
|
||||||
0.0,
|
|
||||||
1398.0568057512764,
|
|
||||||
501.055477122656,
|
|
||||||
7540.680958625912),
|
|
||||||
0.0,
|
|
||||||
100.0)
|
|
||||||
)
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
mnu.sortBy { it.point.t }
|
mnu.sortBy { it.point.t }
|
||||||
|
|
||||||
@@ -123,9 +57,14 @@ fun main() {
|
|||||||
|
|
||||||
println("время расчета = ${System.currentTimeMillis() - ts}мс")
|
println("время расчета = ${System.currentTimeMillis() - ts}мс")
|
||||||
|
|
||||||
// for (p in r.points)
|
val astro = AstronomerJ2000(EarthType.PZ90d02)
|
||||||
// println("${p.vit} ${LocalDateTime.ofEpochSecond(p.t.toLong(),0, ZoneOffset.UTC)} ${p.r.x} ${p.r.y} ${p.r.z} ${p.v.x} ${p.v.y} ${p.v.z}")
|
for (p in r.revolutions) {
|
||||||
//
|
val op = astro.grinvToASK(p.vuz)
|
||||||
|
println(
|
||||||
|
"${op.vit} ${toDateTime(op.t)} " +
|
||||||
|
"${op.r.x} ${op.r.y} ${op.r.z} ${op.v.x} ${op.v.y} ${op.v.z}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
ts = System.currentTimeMillis()
|
ts = System.currentTimeMillis()
|
||||||
r.calculateFlightLine(mnu.first().point.t, mnu.first().point.t + 86400.0 * 5)
|
r.calculateFlightLine(mnu.first().point.t, mnu.first().point.t + 86400.0 * 5)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ plugins {
|
|||||||
kotlin("plugin.lombok")
|
kotlin("plugin.lombok")
|
||||||
id("org.springframework.boot")
|
id("org.springframework.boot")
|
||||||
id("io.spring.dependency-management")
|
id("io.spring.dependency-management")
|
||||||
id("org.sonarqube")
|
//id("org.sonarqube")
|
||||||
jacoco
|
jacoco
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package space.nstart.pcp.pcp_types_lib.dto
|
||||||
|
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read model for booked slots pages and integrations.
|
||||||
|
*
|
||||||
|
* The DTO is intentionally stored in pcp-types-lib because it is returned by slots-service
|
||||||
|
* and consumed by ui-service. It contains only booking/slot fields; UI-specific enrichment
|
||||||
|
* such as satellite name/type is performed in ui-service.
|
||||||
|
*/
|
||||||
|
data class BookedSlotDetailsDTO(
|
||||||
|
val bookedSlotId: Long,
|
||||||
|
val satelliteId: Long,
|
||||||
|
val slotNum: Long,
|
||||||
|
val cycle: Long,
|
||||||
|
val timeStart: LocalDateTime,
|
||||||
|
val timeStop: LocalDateTime,
|
||||||
|
val durationSeconds: Long,
|
||||||
|
val roll: Double?,
|
||||||
|
val revolution: Long?,
|
||||||
|
val revolutionSign: String?,
|
||||||
|
val status: String?,
|
||||||
|
val requestIds: List<String> = emptyList(),
|
||||||
|
)
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package space.nstart.pcp.pcp_types_lib.dto.ballistics
|
||||||
|
|
||||||
|
enum class MovementModel {
|
||||||
|
FOTO,
|
||||||
|
KONDOR,
|
||||||
|
METEORM1,
|
||||||
|
METEORM2,
|
||||||
|
BARS,
|
||||||
|
KONDOR_PROGNOZ
|
||||||
|
}
|
||||||
+3
-1
@@ -2,6 +2,8 @@ package space.nstart.pcp.pcp_types_lib.dto.ballistics
|
|||||||
|
|
||||||
class SatelliteICDTO(
|
class SatelliteICDTO(
|
||||||
val satelliteId: Long = -1,
|
val satelliteId: Long = -1,
|
||||||
val ic : InitialConditionsDTO = InitialConditionsDTO()
|
val ic : InitialConditionsDTO = InitialConditionsDTO(),
|
||||||
|
movementModel: MovementModel? = MovementModel.FOTO
|
||||||
) {
|
) {
|
||||||
|
val movementModel: MovementModel = movementModel ?: MovementModel.FOTO
|
||||||
}
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
package space.nstart.pcp.pcp_types_lib.dto.ballistics
|
||||||
|
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
class TlePointRequestDTO(
|
||||||
|
var tle: TLEDTO = TLEDTO(),
|
||||||
|
var time: LocalDateTime = LocalDateTime.now()
|
||||||
|
)
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package space.nstart.pcp.pcp_types_lib.dto.requests.slots
|
||||||
|
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
data class BookedSlotDetailsDTO(
|
||||||
|
val bookedSlotId: Long,
|
||||||
|
val satelliteId: Long,
|
||||||
|
val slotNum: Long,
|
||||||
|
val cycle: Long,
|
||||||
|
val timeStart: LocalDateTime,
|
||||||
|
val timeStop: LocalDateTime,
|
||||||
|
val durationSeconds: Long,
|
||||||
|
val roll: Double?,
|
||||||
|
val revolution: Long?,
|
||||||
|
val revolutionSign: Int?,
|
||||||
|
val status: String?,
|
||||||
|
val requestIds: List<Long> = emptyList()
|
||||||
|
)
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM bellsoft/liberica-openjre-alpine:21.0.5
|
ARG RUNTIME_IMAGE=repo.nstart.cloud/bellsoft/liberica-openjre-alpine:21.0.5
|
||||||
|
FROM ${RUNTIME_IMAGE}
|
||||||
|
|
||||||
ENV JAVA_OPTS=""
|
ENV JAVA_OPTS=""
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ plugins {
|
|||||||
kotlin("plugin.lombok")
|
kotlin("plugin.lombok")
|
||||||
id("org.springframework.boot")
|
id("org.springframework.boot")
|
||||||
id("io.spring.dependency-management")
|
id("io.spring.dependency-management")
|
||||||
id("org.sonarqube")
|
//id("org.sonarqube")
|
||||||
jacoco
|
jacoco
|
||||||
// id("org.graalvm.buildtools.native")
|
// id("org.graalvm.buildtools.native")
|
||||||
}
|
}
|
||||||
@@ -102,11 +102,11 @@ tasks.jacocoTestReport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sonar {
|
//sonar {
|
||||||
properties {
|
// properties {
|
||||||
property("sonar.projectKey", "pcp")
|
// property("sonar.projectKey", "pcp")
|
||||||
property("sonar.login", "sqp_tokenExample")
|
// property("sonar.login", "sqp_tokenExample")
|
||||||
property("sonar.qualitygate.wait", "${property("sonar.qualitygate.wait")}")
|
// property("sonar.qualitygate.wait", "${property("sonar.qualitygate.wait")}")
|
||||||
property("sonar.host.url", "${property("sonar.host.url")}")
|
// property("sonar.host.url", "${property("sonar.host.url")}")
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|||||||
+20
@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.Operation
|
|||||||
import io.swagger.v3.oas.annotations.Parameter
|
import io.swagger.v3.oas.annotations.Parameter
|
||||||
import jakarta.validation.Valid
|
import jakarta.validation.Valid
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
import org.springframework.web.bind.annotation.PostMapping
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
@@ -11,9 +12,11 @@ import org.springframework.web.bind.annotation.RequestBody
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
import org.springframework.web.bind.annotation.RequestParam
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import space.nstart.pcp.pcp_request_service.service.SatelliteIcEventService
|
||||||
import space.nstart.pcp.pcp_request_service.service.SatelliteService
|
import space.nstart.pcp.pcp_request_service.service.SatelliteService
|
||||||
import space.nstart.pcp.pcp_types_lib.configuration.CustomValidationException
|
import space.nstart.pcp.pcp_types_lib.configuration.CustomValidationException
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
|
||||||
@@ -23,6 +26,9 @@ class SatelliteController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private lateinit var satelliteService: SatelliteService
|
private lateinit var satelliteService: SatelliteService
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private lateinit var satelliteIcEventService: SatelliteIcEventService
|
||||||
|
|
||||||
@PostMapping("/{satellite_id}/extract-time")
|
@PostMapping("/{satellite_id}/extract-time")
|
||||||
fun extractTime(
|
fun extractTime(
|
||||||
@PathVariable("satellite_id") satelliteId : Long,
|
@PathVariable("satellite_id") satelliteId : Long,
|
||||||
@@ -30,6 +36,20 @@ class SatelliteController {
|
|||||||
) =
|
) =
|
||||||
satelliteService.exactTime(satelliteId, body)
|
satelliteService.exactTime(satelliteId, body)
|
||||||
|
|
||||||
|
@PostMapping("/receive-rv")
|
||||||
|
fun receiveRv(@RequestBody body: SatelliteICDTO): ResponseEntity<Void> {
|
||||||
|
satelliteIcEventService.handlePlacedIcRv(body)
|
||||||
|
return ResponseEntity.accepted().build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{satellite_id}/initial-conditions/current")
|
||||||
|
fun currentInitialConditions(
|
||||||
|
@PathVariable("satellite_id") satelliteId: Long
|
||||||
|
): ResponseEntity<SatelliteICDTO> =
|
||||||
|
satelliteService.currentInitialConditions(satelliteId)
|
||||||
|
?.let { ResponseEntity.ok(it) }
|
||||||
|
?: ResponseEntity.noContent().build()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
|
|||||||
+4
-3
@@ -1,15 +1,13 @@
|
|||||||
package space.nstart.pcp.pcp_request_service.controller
|
package space.nstart.pcp.pcp_request_service.controller
|
||||||
|
|
||||||
import ballistics.types.OrbitalPoint
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
import org.springframework.web.bind.annotation.RequestBody
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
import space.nstart.pcp.pcp_request_service.service.TLEService
|
import space.nstart.pcp.pcp_request_service.service.TLEService
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TlePointRequestDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRvaRequestDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRvaRequestDTO
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -23,6 +21,9 @@ class TLEController {
|
|||||||
@PostMapping("/parse")
|
@PostMapping("/parse")
|
||||||
fun orbit(@RequestBody tle : TLEDTO) = tleService.parseTLE(tle)
|
fun orbit(@RequestBody tle : TLEDTO) = tleService.parseTLE(tle)
|
||||||
|
|
||||||
|
@PostMapping("/point")
|
||||||
|
fun point(@RequestBody req: TlePointRequestDTO) = tleService.point(req)
|
||||||
|
|
||||||
|
|
||||||
@PostMapping("/rva")
|
@PostMapping("/rva")
|
||||||
fun rva(@RequestBody req : TleRvaRequestDTO) = tleService.rva(req)
|
fun rva(@RequestBody req : TleRvaRequestDTO) = tleService.rva(req)
|
||||||
|
|||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
package space.nstart.pcp.pcp_request_service.entity
|
||||||
|
|
||||||
|
import jakarta.persistence.Column
|
||||||
|
import jakarta.persistence.Entity
|
||||||
|
import jakarta.persistence.EnumType
|
||||||
|
import jakarta.persistence.Enumerated
|
||||||
|
import jakarta.persistence.GeneratedValue
|
||||||
|
import jakarta.persistence.GenerationType
|
||||||
|
import jakarta.persistence.Id
|
||||||
|
import jakarta.persistence.Table
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.MovementModel
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "satellite_initial_conditions")
|
||||||
|
class SatelliteInitialConditionEntity(
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "satellite_initial_condition_id")
|
||||||
|
val id: Long? = null,
|
||||||
|
@Column(name = "satellite_id", nullable = false)
|
||||||
|
val satelliteId: Long = 0,
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "movement_model", nullable = false)
|
||||||
|
val movementModel: MovementModel = MovementModel.FOTO,
|
||||||
|
@Column(name = "orb_time", nullable = false)
|
||||||
|
val orbTime: LocalDateTime = LocalDateTime.now(),
|
||||||
|
@Column(name = "orb_revolution", nullable = false)
|
||||||
|
val orbRevolution: Long = 0,
|
||||||
|
@Column(nullable = false)
|
||||||
|
val x: Double = 0.0,
|
||||||
|
@Column(nullable = false)
|
||||||
|
val y: Double = 0.0,
|
||||||
|
@Column(nullable = false)
|
||||||
|
val z: Double = 0.0,
|
||||||
|
@Column(nullable = false)
|
||||||
|
val vx: Double = 0.0,
|
||||||
|
@Column(nullable = false)
|
||||||
|
val vy: Double = 0.0,
|
||||||
|
@Column(nullable = false)
|
||||||
|
val vz: Double = 0.0,
|
||||||
|
@Column(name = "s_ball", nullable = false)
|
||||||
|
val sBall: Double = 0.0,
|
||||||
|
@Column(nullable = false)
|
||||||
|
val f81: Double = 147.8,
|
||||||
|
@Column(name = "received_at", nullable = false)
|
||||||
|
val receivedAt: LocalDateTime = LocalDateTime.now(),
|
||||||
|
@Column(name = "calculated_at")
|
||||||
|
var calculatedAt: LocalDateTime? = null,
|
||||||
|
@Column(name = "last_calculated", nullable = false)
|
||||||
|
var lastCalculated: Boolean = false
|
||||||
|
) {
|
||||||
|
constructor(request: SatelliteICDTO) : this(
|
||||||
|
satelliteId = request.satelliteId,
|
||||||
|
movementModel = request.movementModel,
|
||||||
|
orbTime = request.ic.orbPoint.time,
|
||||||
|
orbRevolution = request.ic.orbPoint.revolution,
|
||||||
|
x = request.ic.orbPoint.x,
|
||||||
|
y = request.ic.orbPoint.y,
|
||||||
|
z = request.ic.orbPoint.z,
|
||||||
|
vx = request.ic.orbPoint.vx,
|
||||||
|
vy = request.ic.orbPoint.vy,
|
||||||
|
vz = request.ic.orbPoint.vz,
|
||||||
|
sBall = request.ic.sBall,
|
||||||
|
f81 = request.ic.f81
|
||||||
|
)
|
||||||
|
|
||||||
|
fun markLastCalculated(calculatedAt: LocalDateTime = LocalDateTime.now()) {
|
||||||
|
this.calculatedAt = calculatedAt
|
||||||
|
this.lastCalculated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toDto() = SatelliteICDTO(
|
||||||
|
satelliteId = satelliteId,
|
||||||
|
movementModel = movementModel,
|
||||||
|
ic = InitialConditionsDTO(
|
||||||
|
orbPoint = OrbPointDTO(
|
||||||
|
time = orbTime,
|
||||||
|
revolution = orbRevolution,
|
||||||
|
vx = vx,
|
||||||
|
vy = vy,
|
||||||
|
vz = vz,
|
||||||
|
x = x,
|
||||||
|
y = y,
|
||||||
|
z = z
|
||||||
|
),
|
||||||
|
sBall = sBall,
|
||||||
|
f81 = f81
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
+7
@@ -6,6 +6,7 @@ import org.springframework.kafka.annotation.KafkaListener
|
|||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import space.nstart.pcp.pcp_request_service.service.SatelliteIcEventService
|
import space.nstart.pcp.pcp_request_service.service.SatelliteIcEventService
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.MovementModel
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
|
||||||
import tools.jackson.databind.ObjectMapper
|
import tools.jackson.databind.ObjectMapper
|
||||||
import tools.jackson.databind.node.ObjectNode
|
import tools.jackson.databind.node.ObjectNode
|
||||||
@@ -52,6 +53,11 @@ class SatelliteIcRvKafkaListener(
|
|||||||
*/
|
*/
|
||||||
private fun normalizePayload(payloadNode: tools.jackson.databind.JsonNode): tools.jackson.databind.JsonNode {
|
private fun normalizePayload(payloadNode: tools.jackson.databind.JsonNode): tools.jackson.databind.JsonNode {
|
||||||
val normalizedPayload = payloadNode.deepCopy()
|
val normalizedPayload = payloadNode.deepCopy()
|
||||||
|
if (normalizedPayload is ObjectNode &&
|
||||||
|
(normalizedPayload["movementModel"] == null || normalizedPayload["movementModel"].isNull)
|
||||||
|
) {
|
||||||
|
normalizedPayload.put("movementModel", DEFAULT_MOVEMENT_MODEL.name)
|
||||||
|
}
|
||||||
val initialConditionsNode = normalizedPayload["ic"]
|
val initialConditionsNode = normalizedPayload["ic"]
|
||||||
if (initialConditionsNode is ObjectNode) {
|
if (initialConditionsNode is ObjectNode) {
|
||||||
if (initialConditionsNode["sBall"] == null || initialConditionsNode["sBall"].isNull) {
|
if (initialConditionsNode["sBall"] == null || initialConditionsNode["sBall"].isNull) {
|
||||||
@@ -66,5 +72,6 @@ class SatelliteIcRvKafkaListener(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val DEFAULT_INITIAL_CONDITIONS = InitialConditionsDTO()
|
private val DEFAULT_INITIAL_CONDITIONS = InitialConditionsDTO()
|
||||||
|
private val DEFAULT_MOVEMENT_MODEL = MovementModel.FOTO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
package space.nstart.pcp.pcp_request_service.repository
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.data.jpa.repository.Modifying
|
||||||
|
import org.springframework.data.jpa.repository.Query
|
||||||
|
import org.springframework.data.repository.query.Param
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import space.nstart.pcp.pcp_request_service.entity.SatelliteInitialConditionEntity
|
||||||
|
|
||||||
|
interface SatelliteInitialConditionRepository : JpaRepository<SatelliteInitialConditionEntity, Long> {
|
||||||
|
fun findFirstBySatelliteIdAndLastCalculatedTrueOrderByCalculatedAtDescIdDesc(
|
||||||
|
satelliteId: Long
|
||||||
|
): SatelliteInitialConditionEntity?
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Transactional
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
update SatelliteInitialConditionEntity entity
|
||||||
|
set entity.lastCalculated = false
|
||||||
|
where entity.satelliteId = :satelliteId
|
||||||
|
and entity.lastCalculated = true
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun clearLastCalculated(@Param("satelliteId") satelliteId: Long): Int
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
package space.nstart.pcp.pcp_request_service.service
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import space.nstart.pcp.pcp_request_service.entity.SatelliteInitialConditionEntity
|
||||||
|
import space.nstart.pcp.pcp_request_service.repository.SatelliteInitialConditionRepository
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class SatelliteInitialConditionService(
|
||||||
|
private val satelliteInitialConditionRepository: SatelliteInitialConditionRepository
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun saveReceived(request: SatelliteICDTO): SatelliteInitialConditionEntity =
|
||||||
|
satelliteInitialConditionRepository.save(SatelliteInitialConditionEntity(request))
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun markLastCalculated(initialCondition: SatelliteInitialConditionEntity) {
|
||||||
|
satelliteInitialConditionRepository.clearLastCalculated(initialCondition.satelliteId)
|
||||||
|
initialCondition.markLastCalculated()
|
||||||
|
satelliteInitialConditionRepository.save(initialCondition)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
fun current(satelliteId: Long): SatelliteICDTO? =
|
||||||
|
satelliteInitialConditionRepository
|
||||||
|
.findFirstBySatelliteIdAndLastCalculatedTrueOrderByCalculatedAtDescIdDesc(satelliteId)
|
||||||
|
?.toDto()
|
||||||
|
}
|
||||||
+28
-3
@@ -7,6 +7,7 @@ import ballistics.types.EarthType
|
|||||||
import ballistics.types.FleghtLineSector
|
import ballistics.types.FleghtLineSector
|
||||||
import ballistics.types.InitialConditions
|
import ballistics.types.InitialConditions
|
||||||
import ballistics.types.KeplerParams
|
import ballistics.types.KeplerParams
|
||||||
|
import ballistics.types.ModDVType
|
||||||
import ballistics.types.OPKatObj
|
import ballistics.types.OPKatObj
|
||||||
import ballistics.types.OrbitalPoint
|
import ballistics.types.OrbitalPoint
|
||||||
import ballistics.types.PPI
|
import ballistics.types.PPI
|
||||||
@@ -46,6 +47,7 @@ import space.nstart.pcp.pcp_request_service.utils.ContourClipService
|
|||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.MovementModel
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PointViewParamDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PointViewParamDTO
|
||||||
@@ -87,6 +89,10 @@ class SatelliteService {
|
|||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private lateinit var pdcmRepository: PDCMRepository
|
private lateinit var pdcmRepository: PDCMRepository
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private lateinit var satelliteInitialConditionService: SatelliteInitialConditionService
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private lateinit var ascNodeRepository: AscNodeRepository
|
private lateinit var ascNodeRepository: AscNodeRepository
|
||||||
@Autowired
|
@Autowired
|
||||||
@@ -183,9 +189,11 @@ class SatelliteService {
|
|||||||
)
|
)
|
||||||
|
|
||||||
suspend fun resieveRV(rv: SatelliteICDTO) {
|
suspend fun resieveRV(rv: SatelliteICDTO) {
|
||||||
|
val savedInitialCondition = satelliteInitialConditionService.saveReceived(rv)
|
||||||
val bal = Ballistics()
|
val bal = Ballistics()
|
||||||
|
bal.modDVType = rv.movementModel.toModDVType()
|
||||||
|
|
||||||
logger.info("Начало расчета баллистики по RV id = ${rv.satelliteId}")
|
logger.info("Начало расчета баллистики по RV id = ${rv.satelliteId}, model = ${rv.movementModel}")
|
||||||
|
|
||||||
bal.rollMax = (45.0 * PI / 180.0)
|
bal.rollMax = (45.0 * PI / 180.0)
|
||||||
bal.rollMin = (20.0 * PI / 180.0)
|
bal.rollMin = (20.0 * PI / 180.0)
|
||||||
@@ -227,10 +235,25 @@ class SatelliteService {
|
|||||||
|
|
||||||
prepareEarthCovering(bal, rv.satelliteId)
|
prepareEarthCovering(bal, rv.satelliteId)
|
||||||
|
|
||||||
|
satelliteInitialConditionService.markLastCalculated(savedInitialCondition)
|
||||||
|
|
||||||
bal.clear()
|
bal.clear()
|
||||||
logger.info("Расчет для КА ${rv.satelliteId} завершился успешно")
|
logger.info("Расчет для КА ${rv.satelliteId} завершился успешно")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun currentInitialConditions(satelliteId: Long): SatelliteICDTO? =
|
||||||
|
satelliteInitialConditionService.current(satelliteId)
|
||||||
|
|
||||||
|
private fun MovementModel.toModDVType(): ModDVType =
|
||||||
|
when (this) {
|
||||||
|
MovementModel.FOTO -> ModDVType.FOTO
|
||||||
|
MovementModel.KONDOR -> ModDVType.KONDOR
|
||||||
|
MovementModel.METEORM1 -> ModDVType.METEORM1
|
||||||
|
MovementModel.METEORM2 -> ModDVType.METEORM2
|
||||||
|
MovementModel.BARS -> ModDVType.BARS
|
||||||
|
MovementModel.KONDOR_PROGNOZ -> ModDVType.KONDOR_PROGNOZ
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun mergeSectors(sectors: List<FleghtLineSector>): List<FleghtLineSector> {
|
fun mergeSectors(sectors: List<FleghtLineSector>): List<FleghtLineSector> {
|
||||||
if (sectors.isEmpty()) return emptyList()
|
if (sectors.isEmpty()) return emptyList()
|
||||||
@@ -851,8 +874,6 @@ class SatelliteService {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun getAscNodes(id : Long, tn : LocalDateTime?, tk : LocalDateTime?) : List<AscNodeDTO> {
|
fun getAscNodes(id : Long, tn : LocalDateTime?, tk : LocalDateTime?) : List<AscNodeDTO> {
|
||||||
|
|
||||||
return ascNodeRepository.findBySatelliteIdAndTimeBetween(
|
return ascNodeRepository.findBySatelliteIdAndTimeBetween(
|
||||||
@@ -881,3 +902,7 @@ data class SatelliteBallisticsDeletionSummary(
|
|||||||
val earthCoverageDeleted: Int,
|
val earthCoverageDeleted: Int,
|
||||||
val earthCellViewsDeleted: Int
|
val earthCellViewsDeleted: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+34
@@ -1,7 +1,9 @@
|
|||||||
package space.nstart.pcp.pcp_request_service.service
|
package space.nstart.pcp.pcp_request_service.service
|
||||||
|
|
||||||
import ballistics.Ballistics
|
import ballistics.Ballistics
|
||||||
|
import ballistics.orbitalPoints.timeStepper.TLEStepper
|
||||||
import ballistics.types.BallisticsError
|
import ballistics.types.BallisticsError
|
||||||
|
import ballistics.types.EarthType
|
||||||
import ballistics.types.PPI
|
import ballistics.types.PPI
|
||||||
import ballistics.types.TLE
|
import ballistics.types.TLE
|
||||||
import ballistics.types.TLEParams
|
import ballistics.types.TLEParams
|
||||||
@@ -15,6 +17,7 @@ import space.nstart.pcp.pcp_request_service.configuration.CustomValidationExcept
|
|||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RadioVisibilityAreaDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RadioVisibilityAreaDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TlePointRequestDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TargetPositionDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TargetPositionDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRvaRequestDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRvaRequestDTO
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
@@ -47,6 +50,37 @@ class TLEService() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fun point(req: TlePointRequestDTO): OrbPointDTO {
|
||||||
|
val bal = Ballistics()
|
||||||
|
val tleParams = try {
|
||||||
|
bal.getTLEParams(TLE(req.tle.first, req.tle.second), req.tle.header ?: "empty")
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
throw CustomValidationException("Ошибка формата TLE : ${ex.message}")
|
||||||
|
}
|
||||||
|
if (req.time < tleParams.time) {
|
||||||
|
throw CustomValidationException("Время расчета должно быть не раньше эпохи TLE ${tleParams.time}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val point = try {
|
||||||
|
val stepper = TLEStepper(req.tle.first, req.tle.second, EarthType.PZ90d02)
|
||||||
|
stepper.calculate(fromDateTime(req.time))
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
throw CustomErrorException("Ошибка расчета положения по TLE : ${ex.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
return OrbPointDTO(
|
||||||
|
toDateTime(point.t),
|
||||||
|
point.vit.toLong(),
|
||||||
|
point.v.x,
|
||||||
|
point.v.y,
|
||||||
|
point.v.z,
|
||||||
|
point.r.x,
|
||||||
|
point.r.y,
|
||||||
|
point.r.z
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun rva(req : TleRvaRequestDTO) : Iterable<RadioVisibilityAreaDTO> {
|
fun rva(req : TleRvaRequestDTO) : Iterable<RadioVisibilityAreaDTO> {
|
||||||
|
|
||||||
val bal = Ballistics()
|
val bal = Ballistics()
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS satellite_initial_conditions (
|
||||||
|
satellite_initial_condition_id BIGSERIAL PRIMARY KEY,
|
||||||
|
satellite_id BIGINT NOT NULL,
|
||||||
|
movement_model VARCHAR(32) NOT NULL DEFAULT 'FOTO',
|
||||||
|
orb_time TIMESTAMP NOT NULL,
|
||||||
|
orb_revolution BIGINT NOT NULL,
|
||||||
|
x DOUBLE PRECISION NOT NULL,
|
||||||
|
y DOUBLE PRECISION NOT NULL,
|
||||||
|
z DOUBLE PRECISION NOT NULL,
|
||||||
|
vx DOUBLE PRECISION NOT NULL,
|
||||||
|
vy DOUBLE PRECISION NOT NULL,
|
||||||
|
vz DOUBLE PRECISION NOT NULL,
|
||||||
|
s_ball DOUBLE PRECISION NOT NULL,
|
||||||
|
f81 DOUBLE PRECISION NOT NULL,
|
||||||
|
received_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
calculated_at TIMESTAMP,
|
||||||
|
last_calculated BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_satellite_initial_conditions_satellite_id ON satellite_initial_conditions(satellite_id);
|
||||||
|
CREATE INDEX idx_satellite_initial_conditions_last_calculated ON satellite_initial_conditions(satellite_id, last_calculated);
|
||||||
+5
-1
@@ -4,6 +4,7 @@ import org.junit.jupiter.api.Assertions.assertEquals
|
|||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.springframework.beans.factory.ObjectProvider
|
import org.springframework.beans.factory.ObjectProvider
|
||||||
import space.nstart.pcp.pcp_request_service.service.SatelliteIcEventService
|
import space.nstart.pcp.pcp_request_service.service.SatelliteIcEventService
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.MovementModel
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
|
||||||
import tools.jackson.databind.ObjectMapper
|
import tools.jackson.databind.ObjectMapper
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ class SatelliteIcRvKafkaListenerTest {
|
|||||||
assertEquals(56756L, satelliteIcEventService.lastMessage?.satelliteId)
|
assertEquals(56756L, satelliteIcEventService.lastMessage?.satelliteId)
|
||||||
assertEquals(42L, satelliteIcEventService.lastMessage?.ic?.orbPoint?.revolution)
|
assertEquals(42L, satelliteIcEventService.lastMessage?.ic?.orbPoint?.revolution)
|
||||||
assertEquals(140.0, satelliteIcEventService.lastMessage?.ic?.f81)
|
assertEquals(140.0, satelliteIcEventService.lastMessage?.ic?.f81)
|
||||||
|
assertEquals(MovementModel.FOTO, satelliteIcEventService.lastMessage?.movementModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -54,8 +56,9 @@ class SatelliteIcRvKafkaListenerTest {
|
|||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"type": "ICRVPlacedEvent",
|
"type": "ICRVPlacedEvent",
|
||||||
"data": {
|
"data": {
|
||||||
"satelliteId": 56756,
|
"satelliteId": 56756,
|
||||||
|
"movementModel": null,
|
||||||
"ic": {
|
"ic": {
|
||||||
"orbPoint": {
|
"orbPoint": {
|
||||||
"time": "2026-04-16T12:00:00",
|
"time": "2026-04-16T12:00:00",
|
||||||
@@ -77,6 +80,7 @@ class SatelliteIcRvKafkaListenerTest {
|
|||||||
|
|
||||||
assertEquals(0.0, satelliteIcEventService.lastMessage?.ic?.sBall)
|
assertEquals(0.0, satelliteIcEventService.lastMessage?.ic?.sBall)
|
||||||
assertEquals(147.8, satelliteIcEventService.lastMessage?.ic?.f81)
|
assertEquals(147.8, satelliteIcEventService.lastMessage?.ic?.f81)
|
||||||
|
assertEquals(MovementModel.FOTO, satelliteIcEventService.lastMessage?.movementModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RecordingSatelliteIcEventService : SatelliteIcEventService() {
|
private class RecordingSatelliteIcEventService : SatelliteIcEventService() {
|
||||||
|
|||||||
+58
@@ -14,6 +14,11 @@ import org.springframework.kafka.core.DefaultKafkaConsumerFactory
|
|||||||
import org.springframework.kafka.listener.adapter.RecordFilterStrategy
|
import org.springframework.kafka.listener.adapter.RecordFilterStrategy
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import space.nstart.pcp.pcp_request_service.entity.PDCMEntity
|
import space.nstart.pcp.pcp_request_service.entity.PDCMEntity
|
||||||
|
import space.nstart.pcp.pcp_request_service.entity.SatelliteInitialConditionEntity
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.MovementModel
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
@@ -41,6 +46,9 @@ class PDCMRepositoryTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private lateinit var repository: PDCMRepository
|
private lateinit var repository: PDCMRepository
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private lateinit var satelliteInitialConditionRepository: SatelliteInitialConditionRepository
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `find satellite orbit availability returns all satellites with points when time is null`() {
|
fun `find satellite orbit availability returns all satellites with points when time is null`() {
|
||||||
persistPoint(11L, LocalDateTime.of(2026, 4, 14, 10, 0))
|
persistPoint(11L, LocalDateTime.of(2026, 4, 14, 10, 0))
|
||||||
@@ -74,6 +82,34 @@ class PDCMRepositoryTest {
|
|||||||
assertEquals(LocalDateTime.of(2026, 4, 14, 12, 0), actual.single().timeStop)
|
assertEquals(LocalDateTime.of(2026, 4, 14, 12, 0), actual.single().timeStop)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `satellite initial conditions repository returns last calculated condition`() {
|
||||||
|
val oldCondition = satelliteInitialConditionRepository.save(sampleInitialCondition(11L, 1L))
|
||||||
|
oldCondition.markLastCalculated(LocalDateTime.of(2026, 4, 14, 10, 0))
|
||||||
|
satelliteInitialConditionRepository.save(oldCondition)
|
||||||
|
|
||||||
|
val newCondition = satelliteInitialConditionRepository.save(sampleInitialCondition(11L, 2L))
|
||||||
|
satelliteInitialConditionRepository.clearLastCalculated(11L)
|
||||||
|
newCondition.markLastCalculated(LocalDateTime.of(2026, 4, 14, 12, 0))
|
||||||
|
satelliteInitialConditionRepository.save(newCondition)
|
||||||
|
satelliteInitialConditionRepository.save(sampleInitialCondition(11L, 3L))
|
||||||
|
|
||||||
|
entityManager.flush()
|
||||||
|
entityManager.clear()
|
||||||
|
|
||||||
|
val actual = satelliteInitialConditionRepository
|
||||||
|
.findFirstBySatelliteIdAndLastCalculatedTrueOrderByCalculatedAtDescIdDesc(11L)
|
||||||
|
|
||||||
|
assertEquals(2L, actual?.toDto()?.ic?.orbPoint?.revolution)
|
||||||
|
assertEquals(MovementModel.KONDOR, actual?.movementModel)
|
||||||
|
assertEquals(
|
||||||
|
1,
|
||||||
|
satelliteInitialConditionRepository.findAll().count { entity ->
|
||||||
|
entity.satelliteId == 11L && entity.lastCalculated
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun persistPoint(satelliteId: Long, time: LocalDateTime) {
|
private fun persistPoint(satelliteId: Long, time: LocalDateTime) {
|
||||||
entityManager.persist(
|
entityManager.persist(
|
||||||
PDCMEntity(
|
PDCMEntity(
|
||||||
@@ -91,6 +127,28 @@ class PDCMRepositoryTest {
|
|||||||
entityManager.flush()
|
entityManager.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun sampleInitialCondition(satelliteId: Long, revolution: Long) =
|
||||||
|
SatelliteInitialConditionEntity(
|
||||||
|
SatelliteICDTO(
|
||||||
|
satelliteId = satelliteId,
|
||||||
|
movementModel = MovementModel.KONDOR,
|
||||||
|
ic = InitialConditionsDTO(
|
||||||
|
orbPoint = OrbPointDTO(
|
||||||
|
time = LocalDateTime.of(2026, 4, 14, 10, 0).plusHours(revolution),
|
||||||
|
revolution = revolution,
|
||||||
|
vx = 1.0,
|
||||||
|
vy = 2.0,
|
||||||
|
vz = 3.0,
|
||||||
|
x = 4.0,
|
||||||
|
y = 5.0,
|
||||||
|
z = 6.0
|
||||||
|
),
|
||||||
|
sBall = 0.2,
|
||||||
|
f81 = 141.0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@TestConfiguration
|
@TestConfiguration
|
||||||
class KafkaFilterStubConfiguration {
|
class KafkaFilterStubConfiguration {
|
||||||
@Bean("kafkaListenerContainerFactory")
|
@Bean("kafkaListenerContainerFactory")
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM bellsoft/liberica-openjre-alpine:21.0.5
|
ARG RUNTIME_IMAGE=repo.nstart.cloud/bellsoft/liberica-openjre-alpine:21.0.5
|
||||||
|
FROM ${RUNTIME_IMAGE}
|
||||||
|
|
||||||
ENV JAVA_OPTS=""
|
ENV JAVA_OPTS=""
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ plugins {
|
|||||||
kotlin("plugin.lombok")
|
kotlin("plugin.lombok")
|
||||||
id("org.springframework.boot")
|
id("org.springframework.boot")
|
||||||
id("io.spring.dependency-management")
|
id("io.spring.dependency-management")
|
||||||
id("org.sonarqube")
|
//id("org.sonarqube")
|
||||||
jacoco
|
jacoco
|
||||||
// id("org.graalvm.buildtools.native")
|
// id("org.graalvm.buildtools.native")
|
||||||
}
|
}
|
||||||
@@ -101,11 +101,11 @@ tasks.jacocoTestReport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sonar {
|
//sonar {
|
||||||
properties {
|
// properties {
|
||||||
property("sonar.projectKey", "pcp")
|
// property("sonar.projectKey", "pcp")
|
||||||
property("sonar.login", "sqp_tokenExample")
|
// property("sonar.login", "sqp_tokenExample")
|
||||||
property("sonar.qualitygate.wait", "${property("sonar.qualitygate.wait")}")
|
// property("sonar.qualitygate.wait", "${property("sonar.qualitygate.wait")}")
|
||||||
property("sonar.host.url", "${property("sonar.host.url")}")
|
// property("sonar.host.url", "${property("sonar.host.url")}")
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|||||||
-1
@@ -33,7 +33,6 @@ class SatelliteModel(
|
|||||||
val longMission : LongTermMission = LongTermMission(),
|
val longMission : LongTermMission = LongTermMission(),
|
||||||
val bls: BLS = BLS(),
|
val bls: BLS = BLS(),
|
||||||
val scanTLE : Boolean = false
|
val scanTLE : Boolean = false
|
||||||
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun toDTO() = SatelliteInfoDTO(
|
fun toDTO() = SatelliteInfoDTO(
|
||||||
|
|||||||
+136
-30
@@ -18,8 +18,11 @@ import space.nstart.pcp.pcp_types_lib.dto.requests.EarthCellWithRequestsDTO
|
|||||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.OccupiedIntervalDTO
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.OccupiedIntervalDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageTargetDTO
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageTargetDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SurveySlotDTO
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
import kotlin.math.abs
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -36,6 +39,11 @@ class ComplexPlanCalculationService(
|
|||||||
|
|
||||||
private val logger: Logger = LoggerFactory.getLogger(this::class.java)
|
private val logger: Logger = LoggerFactory.getLogger(this::class.java)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val LOG_INTERVAL_SAMPLE_LIMIT = 10
|
||||||
|
private const val ROLL_EPSILON = 0.01
|
||||||
|
}
|
||||||
|
|
||||||
fun process(
|
fun process(
|
||||||
intervalStart: LocalDateTime,
|
intervalStart: LocalDateTime,
|
||||||
intervalEnd: LocalDateTime,
|
intervalEnd: LocalDateTime,
|
||||||
@@ -134,7 +142,7 @@ class ComplexPlanCalculationService(
|
|||||||
|
|
||||||
logger.info("Шаг batch: runId={}, загрузка booked-маршрутов из slots-service", runId)
|
logger.info("Шаг batch: runId={}, загрузка booked-маршрутов из slots-service", runId)
|
||||||
val plannedModesMs = measureTimeMillis {
|
val plannedModesMs = measureTimeMillis {
|
||||||
addPlannedModes(workingSatellites, intervalStart, intervalEnd)
|
addPlannedModes(runId, workingSatellites, intervalStart, intervalEnd)
|
||||||
}
|
}
|
||||||
logger.info(
|
logger.info(
|
||||||
"Stage planned-modes-load: runId={}, satellites={}, durationMs={}",
|
"Stage planned-modes-load: runId={}, satellites={}, durationMs={}",
|
||||||
@@ -161,8 +169,9 @@ class ComplexPlanCalculationService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val targetsChunk = buildTargetsChunk(chunk)
|
val targetsChunk = buildTargetsChunk(chunk)
|
||||||
var coverageByTargetId = emptyMap<Long, List<space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO>>()
|
var coverageByTargetId = emptyMap<Long, List<SlotDTO>>()
|
||||||
val occupiedIntervals = buildOccupiedIntervalsSnapshot(workingSatellites)
|
val occupiedIntervals = buildOccupiedIntervalsSnapshot(workingSatellites)
|
||||||
|
logOccupiedIntervalsSnapshot(runId, chunkIndex + 1, chunkCount, occupiedIntervals)
|
||||||
val coverageFetchMs = measureTimeMillis {
|
val coverageFetchMs = measureTimeMillis {
|
||||||
coverageByTargetId = coverageCalculationClient.coverageModes(
|
coverageByTargetId = coverageCalculationClient.coverageModes(
|
||||||
targets = targetsChunk,
|
targets = targetsChunk,
|
||||||
@@ -189,6 +198,7 @@ class ComplexPlanCalculationService(
|
|||||||
var acceptedForChunk = 0
|
var acceptedForChunk = 0
|
||||||
val chunkProcessMs = measureTimeMillis {
|
val chunkProcessMs = measureTimeMillis {
|
||||||
acceptedForChunk = processChunk(
|
acceptedForChunk = processChunk(
|
||||||
|
runId = runId,
|
||||||
workingSatellites = workingSatellites,
|
workingSatellites = workingSatellites,
|
||||||
cells = chunk,
|
cells = chunk,
|
||||||
coverageByTargetId = coverageByTargetId,
|
coverageByTargetId = coverageByTargetId,
|
||||||
@@ -294,6 +304,7 @@ class ComplexPlanCalculationService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun addPlannedModes(
|
private fun addPlannedModes(
|
||||||
|
runId: Long?,
|
||||||
workingSatellites: List<SatelliteModel>,
|
workingSatellites: List<SatelliteModel>,
|
||||||
intervalStart: LocalDateTime,
|
intervalStart: LocalDateTime,
|
||||||
intervalEnd: LocalDateTime
|
intervalEnd: LocalDateTime
|
||||||
@@ -308,35 +319,35 @@ class ComplexPlanCalculationService(
|
|||||||
)
|
)
|
||||||
|
|
||||||
workingSatellites.forEach { satellite ->
|
workingSatellites.forEach { satellite ->
|
||||||
val plannedForSatellite = plannedModesBySatelliteId[satellite.satelliteId].orEmpty()
|
val plannedForSatellite = plannedModesBySatelliteId[satellite.satelliteId].orEmpty()
|
||||||
plannedModesBySatelliteId[satellite.satelliteId]
|
logBookedSlotsLoaded(runId, satellite.satelliteId, plannedForSatellite)
|
||||||
.orEmpty()
|
plannedForSatellite.forEach { slot ->
|
||||||
.forEach { slot ->
|
surveyPlacementService.mergeSurvey(
|
||||||
surveyPlacementService.mergeSurvey(
|
existingSurveys = satellite.longMission.surveys,
|
||||||
existingSurveys = satellite.longMission.surveys,
|
candidateSurvey = SurveyMode(
|
||||||
candidateSurvey = SurveyMode(
|
revolution = slot.revolution,
|
||||||
revolution = slot.revolution,
|
time = slot.tn,
|
||||||
time = slot.tn,
|
timeStop = slot.tk,
|
||||||
timeStop = slot.tk,
|
roll = slot.roll,
|
||||||
roll = slot.roll,
|
latitude = slot.latitude,
|
||||||
latitude = slot.latitude,
|
longitude = slot.longitude,
|
||||||
longitude = slot.longitude,
|
duration = Duration.between(slot.tn, slot.tk).seconds.toDouble(),
|
||||||
duration = Duration.between(slot.tn, slot.tk).seconds.toDouble(),
|
contourWKT = slot.contour,
|
||||||
contourWKT = slot.contour,
|
cellNum = -1,
|
||||||
cellNum = -1,
|
source = SatelliteModeSource.SLOTS,
|
||||||
source = SatelliteModeSource.SLOTS,
|
bookedSlotIds = slot.slotIds,
|
||||||
bookedSlotIds = slot.slotIds,
|
cellNums = emptyList()
|
||||||
cellNums = emptyList()
|
),
|
||||||
),
|
satellite = satellite
|
||||||
satellite = satellite
|
|
||||||
)
|
|
||||||
}
|
|
||||||
logger.info(
|
|
||||||
"Добавлены booked-маршруты для satelliteId={}: count={}",
|
|
||||||
satellite.satelliteId,
|
|
||||||
plannedForSatellite.size
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
logger.info(
|
||||||
|
"Добавлены booked-маршруты: runId={}, satelliteId={}, count={}",
|
||||||
|
runId,
|
||||||
|
satellite.satelliteId,
|
||||||
|
plannedForSatellite.size
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildCoverageTargets(cells: List<EarthCellWithRequestsDTO>): List<SlotCoverageTargetDTO> =
|
private fun buildCoverageTargets(cells: List<EarthCellWithRequestsDTO>): List<SlotCoverageTargetDTO> =
|
||||||
@@ -367,10 +378,59 @@ class ComplexPlanCalculationService(
|
|||||||
.sortedWith(compareBy<OccupiedIntervalDTO> { it.satelliteId }.thenBy { it.startTime }.thenBy { it.endTime })
|
.sortedWith(compareBy<OccupiedIntervalDTO> { it.satelliteId }.thenBy { it.startTime }.thenBy { it.endTime })
|
||||||
.toList()
|
.toList()
|
||||||
|
|
||||||
|
private fun logBookedSlotsLoaded(runId: Long?, satelliteId: Long, slots: List<SurveySlotDTO>) {
|
||||||
|
if (slots.isEmpty()) {
|
||||||
|
logger.info("Booked slots loaded: runId={}, satelliteId={}, count=0", runId, satelliteId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Booked slots loaded: runId={}, satelliteId={}, count={}, interval=[{} - {}], sample={}",
|
||||||
|
runId,
|
||||||
|
satelliteId,
|
||||||
|
slots.size,
|
||||||
|
slots.minOf { it.tn },
|
||||||
|
slots.maxOf { it.tk },
|
||||||
|
slots.take(LOG_INTERVAL_SAMPLE_LIMIT).map { slot ->
|
||||||
|
"start=${slot.tn},end=${slot.tk},roll=${slot.roll},revolution=${slot.revolution},slotIds=${slot.slotIds}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logOccupiedIntervalsSnapshot(
|
||||||
|
runId: Long?,
|
||||||
|
chunkNumber: Int,
|
||||||
|
chunkCount: Int,
|
||||||
|
occupiedIntervals: List<OccupiedIntervalDTO>
|
||||||
|
) {
|
||||||
|
val bySource = occupiedIntervals.groupingBy { it.source ?: "UNKNOWN" }.eachCount()
|
||||||
|
val slotsBySatellite = occupiedIntervals
|
||||||
|
.filter { it.source == SatelliteModeSource.SLOTS.name }
|
||||||
|
.groupingBy { it.satelliteId }
|
||||||
|
.eachCount()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Occupied intervals snapshot before coverage request: runId={}, chunk={}/{}, total={}, bySource={}, slotsBySatellite={}, slotsSample={}",
|
||||||
|
runId,
|
||||||
|
chunkNumber,
|
||||||
|
chunkCount,
|
||||||
|
occupiedIntervals.size,
|
||||||
|
bySource,
|
||||||
|
slotsBySatellite,
|
||||||
|
occupiedIntervals
|
||||||
|
.filter { it.source == SatelliteModeSource.SLOTS.name }
|
||||||
|
.take(LOG_INTERVAL_SAMPLE_LIMIT)
|
||||||
|
.map { interval ->
|
||||||
|
"satelliteId=${interval.satelliteId},start=${interval.startTime},end=${interval.endTime},roll=${interval.roll},source=${interval.source}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun processChunk(
|
private fun processChunk(
|
||||||
|
runId: Long?,
|
||||||
workingSatellites: List<SatelliteModel>,
|
workingSatellites: List<SatelliteModel>,
|
||||||
cells: List<EarthCellWithRequestsDTO>,
|
cells: List<EarthCellWithRequestsDTO>,
|
||||||
coverageByTargetId: Map<Long, List<space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO>>,
|
coverageByTargetId: Map<Long, List<SlotDTO>>,
|
||||||
intervalStart: LocalDateTime,
|
intervalStart: LocalDateTime,
|
||||||
intervalEnd: LocalDateTime
|
intervalEnd: LocalDateTime
|
||||||
): Int {
|
): Int {
|
||||||
@@ -391,6 +451,13 @@ class ComplexPlanCalculationService(
|
|||||||
val satellite = workingSatellites.find { it.satelliteId == slot.satelliteId }
|
val satellite = workingSatellites.find { it.satelliteId == slot.satelliteId }
|
||||||
?: throw CustomValidationException("KA ${slot.satelliteId} не зарегистророван")
|
?: throw CustomValidationException("KA ${slot.satelliteId} не зарегистророван")
|
||||||
|
|
||||||
|
logComplanCandidateBookedSlotOverlaps(
|
||||||
|
runId = runId,
|
||||||
|
cell = cell,
|
||||||
|
slot = slot,
|
||||||
|
existingSurveys = satellite.longMission.surveys
|
||||||
|
)
|
||||||
|
|
||||||
val decision = surveyPlacementService.tryPlaceSurvey(
|
val decision = surveyPlacementService.tryPlaceSurvey(
|
||||||
existingSurveys = satellite.longMission.surveys,
|
existingSurveys = satellite.longMission.surveys,
|
||||||
candidateSurvey = SurveyMode(
|
candidateSurvey = SurveyMode(
|
||||||
@@ -425,6 +492,45 @@ class ComplexPlanCalculationService(
|
|||||||
return acceptedModes
|
return acceptedModes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun logComplanCandidateBookedSlotOverlaps(
|
||||||
|
runId: Long?,
|
||||||
|
cell: EarthCellWithRequestsDTO,
|
||||||
|
slot: SlotDTO,
|
||||||
|
existingSurveys: List<SurveyMode>
|
||||||
|
) {
|
||||||
|
val overlappingBookedSlots = existingSurveys
|
||||||
|
.filter { survey -> survey.source == SatelliteModeSource.SLOTS }
|
||||||
|
.filter { survey -> intersectsInTime(survey.time, survey.timeStop, slot.tn, slot.tk) }
|
||||||
|
|
||||||
|
if (overlappingBookedSlots.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn(
|
||||||
|
"COMPLAN candidate overlaps booked SLOTS before placement: runId={}, satelliteId={}, targetId={}, cellNum={}, candidateInterval=[{} - {}], candidateRoll={}, candidateRevolution={}, overlaps={}, sameRollOverlaps={}, bookedSlotsSample={}",
|
||||||
|
runId,
|
||||||
|
slot.satelliteId,
|
||||||
|
cell.targetId(),
|
||||||
|
cell.num,
|
||||||
|
slot.tn,
|
||||||
|
slot.tk,
|
||||||
|
slot.roll,
|
||||||
|
slot.revolution,
|
||||||
|
overlappingBookedSlots.size,
|
||||||
|
overlappingBookedSlots.count { survey -> abs(survey.roll - slot.roll) <= ROLL_EPSILON },
|
||||||
|
overlappingBookedSlots.take(LOG_INTERVAL_SAMPLE_LIMIT).map { survey ->
|
||||||
|
"start=${survey.time},end=${survey.timeStop},roll=${survey.roll},source=${survey.source},bookedSlotIds=${survey.bookedSlotIds}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun intersectsInTime(
|
||||||
|
start1: LocalDateTime,
|
||||||
|
stop1: LocalDateTime,
|
||||||
|
start2: LocalDateTime,
|
||||||
|
stop2: LocalDateTime
|
||||||
|
): Boolean = start1 < stop2 && start2 < stop1
|
||||||
|
|
||||||
private fun hasRemainingCapacity(
|
private fun hasRemainingCapacity(
|
||||||
workingSatellites: List<SatelliteModel>,
|
workingSatellites: List<SatelliteModel>,
|
||||||
intervalStart: LocalDateTime,
|
intervalStart: LocalDateTime,
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ class ComplexPlanRunOrchestrationService(
|
|||||||
)
|
)
|
||||||
|
|
||||||
lateinit var createdRun: ComplexPlanRunEntity
|
lateinit var createdRun: ComplexPlanRunEntity
|
||||||
val createRunMs = measureTimeMillis {
|
val createRunMs = measureTimeMillis {
|
||||||
createdRun = complexPlanRunPersistenceService.createRun(
|
createdRun = complexPlanRunPersistenceService.createRun(
|
||||||
ComplexPlanRunCreateDTO(
|
ComplexPlanRunCreateDTO(
|
||||||
intervalStart = request.intervalStart,
|
intervalStart = request.intervalStart,
|
||||||
|
|||||||
+29
-4
@@ -18,6 +18,7 @@ import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageBatchRespon
|
|||||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageTargetDTO
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageTargetDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotStatus
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SurveySlotDTO
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SurveySlotDTO
|
||||||
import tools.jackson.databind.ObjectMapper
|
import tools.jackson.databind.ObjectMapper
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@@ -161,18 +162,42 @@ class CoverageBatchClient(webClientBuilderProvider: ObjectProvider<WebClient.Bui
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val rawCandidateSlots = result.response.sumOf { it.slots.size }
|
||||||
|
val filteredResponse = removeBookedSlotsFromCoverageResponse(result.response)
|
||||||
|
val filteredCandidateSlots = filteredResponse.sumOf { it.slots.size }
|
||||||
|
val removedBookedSlots = rawCandidateSlots - filteredCandidateSlots
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"slots-service coverage batch finish: targets={}, returnedTargets={}, candidateSlots={}, durationMs={}, effectiveCoverageStrategy={}",
|
"slots-service coverage batch finish: targets={}, returnedTargets={}, candidateSlotsRaw={}, candidateSlotsAfterBookedFilter={}, removedBookedSlots={}, durationMs={}, effectiveCoverageStrategy={}",
|
||||||
targets.size,
|
targets.size,
|
||||||
result.response.size,
|
filteredResponse.size,
|
||||||
result.response.sumOf { it.slots.size },
|
rawCandidateSlots,
|
||||||
|
filteredCandidateSlots,
|
||||||
|
removedBookedSlots,
|
||||||
result.durationMs,
|
result.durationMs,
|
||||||
result.coverageStrategy
|
result.coverageStrategy
|
||||||
)
|
)
|
||||||
|
|
||||||
return result.response.associate { it.targetId to it.slots }
|
return filteredResponse.associate { it.targetId to it.slots }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complex plan loads booked slots before asking slots-service for coverage candidates.
|
||||||
|
* slots-service still has to see booked slots internally: it marks them as BOOKED and the
|
||||||
|
* coverage solver subtracts their geometry from the uncovered target area. However these
|
||||||
|
* BOOKED slots are already present in the working complex plan and must not be returned
|
||||||
|
* to chunk processing again as COMPLAN candidates.
|
||||||
|
*/
|
||||||
|
private fun removeBookedSlotsFromCoverageResponse(
|
||||||
|
response: List<SlotCoverageBatchResponseDTO>
|
||||||
|
): List<SlotCoverageBatchResponseDTO> =
|
||||||
|
response.map { targetResponse ->
|
||||||
|
targetResponse.copy(
|
||||||
|
slots = targetResponse.slots.filterNot { slot -> slot.state != SlotStatus.AVAILABLE }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun requestCoverageModes(
|
private fun requestCoverageModes(
|
||||||
targets: List<SlotCoverageTargetDTO>,
|
targets: List<SlotCoverageTargetDTO>,
|
||||||
intervalStart: LocalDateTime,
|
intervalStart: LocalDateTime,
|
||||||
|
|||||||
+14
@@ -27,6 +27,10 @@ class CoverageSchemeCalculationClient(webClientBuilderProvider: ObjectProvider<W
|
|||||||
private val webClientBuilder: WebClient.Builder = webClientBuilderProvider.ifAvailable ?: WebClient.builder()
|
private val webClientBuilder: WebClient.Builder = webClientBuilderProvider.ifAvailable ?: WebClient.builder()
|
||||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val LOG_INTERVAL_SAMPLE_LIMIT = 10
|
||||||
|
}
|
||||||
|
|
||||||
@Value("\${settings.coverage-scheme-service:pcp-coverage-scheme-service}")
|
@Value("\${settings.coverage-scheme-service:pcp-coverage-scheme-service}")
|
||||||
val url = ""
|
val url = ""
|
||||||
|
|
||||||
@@ -55,6 +59,16 @@ class CoverageSchemeCalculationClient(webClientBuilderProvider: ObjectProvider<W
|
|||||||
intervalStart,
|
intervalStart,
|
||||||
intervalEnd
|
intervalEnd
|
||||||
)
|
)
|
||||||
|
if (occupiedIntervals.isNotEmpty()) {
|
||||||
|
logger.warn(
|
||||||
|
"coverage-scheme-service client received occupiedIntervals but does not pass them to coverage-scheme-service request: occupiedIntervals={}, bySource={}, sample={}",
|
||||||
|
occupiedIntervals.size,
|
||||||
|
occupiedIntervals.groupingBy { it.source ?: "UNKNOWN" }.eachCount(),
|
||||||
|
occupiedIntervals.take(LOG_INTERVAL_SAMPLE_LIMIT).map { interval ->
|
||||||
|
"satelliteId=${interval.satelliteId},start=${interval.startTime},end=${interval.endTime},roll=${interval.roll},source=${interval.source}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val result = linkedMapOf<Long, List<SlotDTO>>()
|
val result = linkedMapOf<Long, List<SlotDTO>>()
|
||||||
val durationMs = measureTimeMillis {
|
val durationMs = measureTimeMillis {
|
||||||
|
|||||||
+18
@@ -226,6 +226,9 @@ class SurveyPlacementService {
|
|||||||
if (!canMergeInTime(existing.time, existing.timeStop, mergedSurvey.time, mergedSurvey.timeStop)) {
|
if (!canMergeInTime(existing.time, existing.timeStop, mergedSurvey.time, mergedSurvey.timeStop)) {
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
|
if (shouldSkipComplanInsideSlotsMerge(existing, mergedSurvey)) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
val overlapsInTime = intersectsInTime(
|
val overlapsInTime = intersectsInTime(
|
||||||
existing.time,
|
existing.time,
|
||||||
existing.timeStop,
|
existing.timeStop,
|
||||||
@@ -377,6 +380,21 @@ class SurveyPlacementService {
|
|||||||
return gapSeconds in 0..MERGE_MAX_TIME_GAP_SECONDS
|
return gapSeconds in 0..MERGE_MAX_TIME_GAP_SECONDS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun shouldSkipComplanInsideSlotsMerge(
|
||||||
|
left: SurveyMode,
|
||||||
|
right: SurveyMode
|
||||||
|
): Boolean {
|
||||||
|
val complanSurvey = when {
|
||||||
|
left.source == SatelliteModeSource.COMPLAN && right.source == SatelliteModeSource.SLOTS -> left
|
||||||
|
right.source == SatelliteModeSource.COMPLAN && left.source == SatelliteModeSource.SLOTS -> right
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
|
val slotsSurvey = if (complanSurvey === left) right else left
|
||||||
|
|
||||||
|
return !complanSurvey.time.isBefore(slotsSurvey.time) &&
|
||||||
|
!complanSurvey.timeStop.isAfter(slotsSurvey.timeStop)
|
||||||
|
}
|
||||||
|
|
||||||
private fun intersectsInTime(
|
private fun intersectsInTime(
|
||||||
start1: LocalDateTime,
|
start1: LocalDateTime,
|
||||||
stop1: LocalDateTime,
|
stop1: LocalDateTime,
|
||||||
|
|||||||
+33
@@ -121,6 +121,39 @@ class SurveyPlacementServiceTest {
|
|||||||
assertFalse(surveys.single().contourWKT.startsWith("MULTIPOLYGON"))
|
assertFalse(surveys.single().contourWKT.startsWith("MULTIPOLYGON"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `complan inside booked slot is not merged to mixed`() {
|
||||||
|
val surveys = mutableListOf(
|
||||||
|
survey(
|
||||||
|
start = 0,
|
||||||
|
end = 100,
|
||||||
|
roll = 10.0,
|
||||||
|
source = SatelliteModeSource.SLOTS,
|
||||||
|
contour = "POLYGON ((0 0, 3 0, 3 3, 0 3, 0 0))"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val satellite = satellite()
|
||||||
|
|
||||||
|
val decision = service.tryPlaceSurvey(
|
||||||
|
surveys,
|
||||||
|
survey(
|
||||||
|
start = 0,
|
||||||
|
end = 40,
|
||||||
|
roll = 10.0,
|
||||||
|
source = SatelliteModeSource.COMPLAN,
|
||||||
|
contour = "POLYGON ((1 1, 2 1, 2 2, 1 2, 1 1))"
|
||||||
|
),
|
||||||
|
satellite
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(decision.accepted)
|
||||||
|
assertEquals(2, surveys.size)
|
||||||
|
assertTrue(decision.absorbedSurveys.isEmpty())
|
||||||
|
assertEquals(1, surveys.count { it.source == SatelliteModeSource.SLOTS })
|
||||||
|
assertEquals(1, surveys.count { it.source == SatelliteModeSource.COMPLAN })
|
||||||
|
assertFalse(surveys.any { it.source == SatelliteModeSource.MIXED })
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `different roll conflict resolved by trimming`() {
|
fun `different roll conflict resolved by trimming`() {
|
||||||
val surveys = mutableListOf(survey(start = 51, end = 80, roll = 20.0))
|
val surveys = mutableListOf(survey(start = 51, end = 80, roll = 20.0))
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM bellsoft/liberica-openjre-alpine:21.0.5
|
ARG RUNTIME_IMAGE=repo.nstart.cloud/bellsoft/liberica-openjre-alpine:21.0.5
|
||||||
|
FROM ${RUNTIME_IMAGE}
|
||||||
|
|
||||||
ENV JAVA_OPTS=""
|
ENV JAVA_OPTS=""
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM bellsoft/liberica-openjre-alpine:21.0.5
|
ARG RUNTIME_IMAGE=repo.nstart.cloud/bellsoft/liberica-openjre-alpine:21.0.5
|
||||||
|
FROM ${RUNTIME_IMAGE}
|
||||||
|
|
||||||
ENV JAVA_OPTS=""
|
ENV JAVA_OPTS=""
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ spring:
|
|||||||
import: "configserver:"
|
import: "configserver:"
|
||||||
cloud:
|
cloud:
|
||||||
config:
|
config:
|
||||||
uri: ${CONFIG_SERVER_URI:http://192.168.1.8:8888}
|
uri: ${CONFIG_SERVER_URI:http://localhost:8888}
|
||||||
fail-fast: ${CONFIG_SERVER_FAIL_FAST:true}
|
fail-fast: ${CONFIG_SERVER_FAIL_FAST:true}
|
||||||
profile: ${SPRING_CLOUD_CONFIG_PROFILE:${SPRING_PROFILES_ACTIVE:${spring.profiles.default}}}
|
profile: ${SPRING_CLOUD_CONFIG_PROFILE:${SPRING_PROFILES_ACTIVE:${spring.profiles.default}}}
|
||||||
label: ${SPRING_CLOUD_CONFIG_LABEL:master}
|
label: ${SPRING_CLOUD_CONFIG_LABEL:master}
|
||||||
@@ -15,7 +15,7 @@ spring:
|
|||||||
# max-in-memory-size: 16MB
|
# max-in-memory-size: 16MB
|
||||||
datasource:
|
datasource:
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
url: jdbc:postgresql://192.168.1.8:5432/complan
|
url: jdbc:postgresql://192.168.60.68:5432/complan
|
||||||
username: postgres
|
username: postgres
|
||||||
password: password
|
password: password
|
||||||
jpa:
|
jpa:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM bellsoft/liberica-openjre-alpine:21.0.5
|
ARG RUNTIME_IMAGE=repo.nstart.cloud/bellsoft/liberica-openjre-alpine:21.0.5
|
||||||
|
FROM ${RUNTIME_IMAGE}
|
||||||
|
|
||||||
ENV JAVA_OPTS=""
|
ENV JAVA_OPTS=""
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM bellsoft/liberica-openjre-alpine:21.0.5
|
ARG RUNTIME_IMAGE=repo.nstart.cloud/bellsoft/liberica-openjre-alpine:21.0.5
|
||||||
|
FROM ${RUNTIME_IMAGE}
|
||||||
|
|
||||||
ENV JAVA_OPTS=""
|
ENV JAVA_OPTS=""
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM bellsoft/liberica-openjre-alpine:21.0.5
|
ARG RUNTIME_IMAGE=repo.nstart.cloud/bellsoft/liberica-openjre-alpine:21.0.5
|
||||||
|
FROM ${RUNTIME_IMAGE}
|
||||||
|
|
||||||
ENV JAVA_OPTS=""
|
ENV JAVA_OPTS=""
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ plugins {
|
|||||||
kotlin("plugin.lombok")
|
kotlin("plugin.lombok")
|
||||||
id("org.springframework.boot")
|
id("org.springframework.boot")
|
||||||
id("io.spring.dependency-management")
|
id("io.spring.dependency-management")
|
||||||
id("org.sonarqube")
|
//id("org.sonarqube")
|
||||||
jacoco
|
jacoco
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM bellsoft/liberica-openjre-alpine:21.0.5
|
ARG RUNTIME_IMAGE=repo.nstart.cloud/bellsoft/liberica-openjre-alpine:21.0.5
|
||||||
|
FROM ${RUNTIME_IMAGE}
|
||||||
|
|
||||||
ENV JAVA_OPTS="-jar"
|
ENV JAVA_OPTS="-jar"
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ plugins {
|
|||||||
kotlin("plugin.spring")
|
kotlin("plugin.spring")
|
||||||
id("org.springframework.boot")
|
id("org.springframework.boot")
|
||||||
id("io.spring.dependency-management")
|
id("io.spring.dependency-management")
|
||||||
id("org.sonarqube")
|
//id("org.sonarqube")
|
||||||
jacoco
|
jacoco
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,11 +65,4 @@ tasks.jacocoTestReport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sonar {
|
|
||||||
properties {
|
|
||||||
property("sonar.projectKey", "pcp")
|
|
||||||
property("sonar.login", "sqp_tokenExample")
|
|
||||||
property("sonar.qualitygate.wait", "${property("sonar.qualitygate.wait")}")
|
|
||||||
property("sonar.host.url", "${property("sonar.host.url")}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM bellsoft/liberica-openjre-alpine:21.0.5
|
ARG RUNTIME_IMAGE=repo.nstart.cloud/bellsoft/liberica-openjre-alpine:21.0.5
|
||||||
|
FROM ${RUNTIME_IMAGE}
|
||||||
|
|
||||||
ENV JAVA_OPTS=""
|
ENV JAVA_OPTS=""
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ plugins {
|
|||||||
kotlin("plugin.lombok")
|
kotlin("plugin.lombok")
|
||||||
id("org.springframework.boot")
|
id("org.springframework.boot")
|
||||||
id("io.spring.dependency-management")
|
id("io.spring.dependency-management")
|
||||||
id("org.sonarqube")
|
//id("org.sonarqube")
|
||||||
jacoco
|
jacoco
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,12 +94,3 @@ tasks.jacocoTestReport {
|
|||||||
csv.required = false
|
csv.required = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sonar {
|
|
||||||
properties {
|
|
||||||
property("sonar.projectKey", "pcp")
|
|
||||||
property("sonar.login", "sqp_tokenExample")
|
|
||||||
property("sonar.qualitygate.wait", "${property("sonar.qualitygate.wait")}")
|
|
||||||
property("sonar.host.url", "${property("sonar.host.url")}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM bellsoft/liberica-openjre-alpine:21.0.5
|
ARG RUNTIME_IMAGE=repo.nstart.cloud/bellsoft/liberica-openjre-alpine:21.0.5
|
||||||
|
FROM ${RUNTIME_IMAGE}
|
||||||
|
|
||||||
ENV JAVA_OPTS=""
|
ENV JAVA_OPTS=""
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM bellsoft/liberica-openjre-alpine:21.0.5
|
ARG RUNTIME_IMAGE=repo.nstart.cloud/bellsoft/liberica-openjre-alpine:21.0.5
|
||||||
|
FROM ${RUNTIME_IMAGE}
|
||||||
|
|
||||||
ENV JAVA_OPTS=""
|
ENV JAVA_OPTS=""
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ plugins {
|
|||||||
kotlin("plugin.lombok")
|
kotlin("plugin.lombok")
|
||||||
id("org.springframework.boot")
|
id("org.springframework.boot")
|
||||||
id("io.spring.dependency-management")
|
id("io.spring.dependency-management")
|
||||||
id("org.sonarqube")
|
//id("org.sonarqube")
|
||||||
jacoco
|
jacoco
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
package space.nstart.pcp.slots_service.controller
|
||||||
|
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.BookedSlotDetailsDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSummaryDTO
|
||||||
|
import space.nstart.pcp.slots_service.dto.bookings.BookedSlotViewDTO
|
||||||
|
import space.nstart.pcp.slots_service.dto.bookings.BookingFilterOptionsDTO
|
||||||
|
import space.nstart.pcp.slots_service.dto.bookings.BookingRequestOptionDTO
|
||||||
|
import space.nstart.pcp.slots_service.service.EarthService
|
||||||
|
import space.nstart.pcp.slots_service.service.SatelliteCatalogService
|
||||||
|
import space.nstart.pcp.slots_service.service.SlotService
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/bookings")
|
||||||
|
class BookingsController(
|
||||||
|
private val slotService: SlotService,
|
||||||
|
private val satelliteCatalogService: SatelliteCatalogService,
|
||||||
|
private val earthService: EarthService,
|
||||||
|
) {
|
||||||
|
|
||||||
|
@GetMapping("/options")
|
||||||
|
fun options(): BookingFilterOptionsDTO {
|
||||||
|
val satellites = satelliteCatalogService.allSatellites()
|
||||||
|
val requests = runCatching {
|
||||||
|
earthService.reqs()
|
||||||
|
.collectList()
|
||||||
|
.block()
|
||||||
|
.orEmpty()
|
||||||
|
.mapNotNull { request ->
|
||||||
|
val id = request.requestId?.toString() ?: return@mapNotNull null
|
||||||
|
BookingRequestOptionDTO(id = id, name = request.name)
|
||||||
|
}
|
||||||
|
.sortedWith(compareBy<BookingRequestOptionDTO> { it.name.ifBlank { it.id } }.thenBy { it.id })
|
||||||
|
}.getOrDefault(emptyList())
|
||||||
|
|
||||||
|
return BookingFilterOptionsDTO(
|
||||||
|
satellites = satellites,
|
||||||
|
requests = requests,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
fun bookedSlots(
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) timeStart: LocalDateTime?,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) timeStop: LocalDateTime?,
|
||||||
|
@RequestParam(required = false) requestId: String?,
|
||||||
|
@RequestParam(required = false) satelliteIds: List<Long>?,
|
||||||
|
): List<BookedSlotViewDTO> {
|
||||||
|
val satellitesById = satelliteCatalogService.allSatellites().associateBy { it.id }
|
||||||
|
return slotService.searchBooked(
|
||||||
|
timeStart = timeStart,
|
||||||
|
timeStop = timeStop,
|
||||||
|
requestId = requestId,
|
||||||
|
satelliteIds = satelliteIds,
|
||||||
|
)
|
||||||
|
.map { booking -> booking.toView(satellitesById[booking.satelliteId]) }
|
||||||
|
.sortedWith(
|
||||||
|
compareBy<BookedSlotViewDTO> { it.timeStart ?: LocalDateTime.MAX }
|
||||||
|
.thenBy { it.satelliteId }
|
||||||
|
.thenBy { it.slotNum }
|
||||||
|
.thenBy { it.cycle }
|
||||||
|
.thenBy { it.bookedSlotId }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BookedSlotDetailsDTO.toView(satellite: SatelliteSummaryDTO?): BookedSlotViewDTO = BookedSlotViewDTO(
|
||||||
|
bookedSlotId = bookedSlotId,
|
||||||
|
satelliteId = satelliteId,
|
||||||
|
satelliteCode = satellite?.code,
|
||||||
|
satelliteName = satellite?.name,
|
||||||
|
satelliteTypeCode = satellite?.typeCode,
|
||||||
|
slotNum = slotNum,
|
||||||
|
cycle = cycle,
|
||||||
|
timeStart = timeStart,
|
||||||
|
timeStop = timeStop,
|
||||||
|
durationSeconds = durationSeconds,
|
||||||
|
roll = roll,
|
||||||
|
revolution = revolution,
|
||||||
|
revolutionSign = revolutionSign,
|
||||||
|
status = status,
|
||||||
|
requestIds = requestIds,
|
||||||
|
)
|
||||||
|
}
|
||||||
+9
@@ -18,6 +18,15 @@ class CatalogPageController {
|
|||||||
@GetMapping("/satellites/pdcm")
|
@GetMapping("/satellites/pdcm")
|
||||||
fun satellitesPdcmPage(): String = "satellites-pdcm"
|
fun satellitesPdcmPage(): String = "satellites-pdcm"
|
||||||
|
|
||||||
|
@GetMapping("/tle-comparison")
|
||||||
|
fun tleComparisonPage(): String = "tle-comparison"
|
||||||
|
|
||||||
@GetMapping("/current-plans")
|
@GetMapping("/current-plans")
|
||||||
fun currentPlansPage(): String = "current-plans"
|
fun currentPlansPage(): String = "current-plans"
|
||||||
|
|
||||||
|
@GetMapping("/bookings")
|
||||||
|
fun bookingsPage(): String = "bookings"
|
||||||
|
|
||||||
|
@GetMapping("/tle-analysis")
|
||||||
|
fun tleAnalysisPage(): String = "tle-analysis"
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-1
@@ -1,6 +1,7 @@
|
|||||||
package space.nstart.pcp.slots_service.controller
|
package space.nstart.pcp.slots_service.controller
|
||||||
|
|
||||||
import jakarta.validation.Valid
|
import jakarta.validation.Valid
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping
|
import org.springframework.web.bind.annotation.DeleteMapping
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
@@ -11,7 +12,9 @@ import org.springframework.web.bind.annotation.RequestBody
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
import org.springframework.web.bind.annotation.RequestParam
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import org.springframework.web.server.ResponseStatusException
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCalculationSummaryDTO
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCalculationSummaryDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteCreateDTO
|
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteCreateDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteGroupCreateDTO
|
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteGroupCreateDTO
|
||||||
@@ -20,10 +23,14 @@ import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteObservationProfileD
|
|||||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSlotProfileDTO
|
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSlotProfileDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteUpdateDTO
|
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteUpdateDTO
|
||||||
import space.nstart.pcp.slots_service.service.GroupStateService
|
import space.nstart.pcp.slots_service.service.GroupStateService
|
||||||
|
import space.nstart.pcp.slots_service.service.BallisticsService
|
||||||
import space.nstart.pcp.slots_service.service.SatelliteCatalogService
|
import space.nstart.pcp.slots_service.service.SatelliteCatalogService
|
||||||
import space.nstart.pcp.slots_service.service.SatellitePdcmService
|
import space.nstart.pcp.slots_service.service.SatellitePdcmService
|
||||||
import space.nstart.pcp.slots_service.service.SlotService
|
import space.nstart.pcp.slots_service.service.SlotService
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.format.DateTimeParseException
|
||||||
|
import java.time.format.ResolverStyle
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/catalog")
|
@RequestMapping("/api/catalog")
|
||||||
@@ -31,8 +38,11 @@ class CatalogRestController(
|
|||||||
private val satelliteCatalogService: SatelliteCatalogService,
|
private val satelliteCatalogService: SatelliteCatalogService,
|
||||||
private val slotService: SlotService,
|
private val slotService: SlotService,
|
||||||
private val groupStateService: GroupStateService,
|
private val groupStateService: GroupStateService,
|
||||||
private val satellitePdcmService: SatellitePdcmService
|
private val satellitePdcmService: SatellitePdcmService,
|
||||||
|
private val ballisticsService: BallisticsService
|
||||||
) {
|
) {
|
||||||
|
private val sendInitialConditionsTimeFormatter =
|
||||||
|
DateTimeFormatter.ofPattern("dd.MM.uuuu HH:mm:ss.SSS").withResolverStyle(ResolverStyle.STRICT)
|
||||||
|
|
||||||
@GetMapping("/satellites")
|
@GetMapping("/satellites")
|
||||||
fun allSatellites() = satelliteCatalogService.allSatellites()
|
fun allSatellites() = satelliteCatalogService.allSatellites()
|
||||||
@@ -56,6 +66,56 @@ class CatalogRestController(
|
|||||||
@GetMapping("/satellites/pdcm/{satelliteId}")
|
@GetMapping("/satellites/pdcm/{satelliteId}")
|
||||||
fun satellitePdcm(@PathVariable satelliteId: Long) = satellitePdcmService.satelliteDetails(satelliteId)
|
fun satellitePdcm(@PathVariable satelliteId: Long) = satellitePdcmService.satelliteDetails(satelliteId)
|
||||||
|
|
||||||
|
@PostMapping("/satellites/pdcm/{satelliteId}/send-ic")
|
||||||
|
fun sendSatelliteInitialConditions(
|
||||||
|
@PathVariable satelliteId: Long,
|
||||||
|
@RequestParam(required = false) time: String?
|
||||||
|
): ResponseEntity<Void> {
|
||||||
|
val requestedTime = parseSendInitialConditionsTime(time)
|
||||||
|
slotService.sendSatelliteInitialConditions(satelliteId, requestedTime)
|
||||||
|
return ResponseEntity.accepted().build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/satellites/pdcm/{satelliteId}/initial-conditions")
|
||||||
|
fun setSatelliteInitialConditions(
|
||||||
|
@PathVariable satelliteId: Long,
|
||||||
|
@RequestBody request: SatelliteICDTO
|
||||||
|
): ResponseEntity<Void> {
|
||||||
|
ballisticsService.receiveRv(
|
||||||
|
SatelliteICDTO(
|
||||||
|
satelliteId = satelliteId,
|
||||||
|
ic = request.ic,
|
||||||
|
movementModel = request.movementModel
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return ResponseEntity.accepted().build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/satellites/pdcm/{satelliteId}/initial-conditions")
|
||||||
|
fun currentPdcmInitialConditions(@PathVariable satelliteId: Long): ResponseEntity<Any> {
|
||||||
|
val response = ballisticsService.currentInitialConditions(satelliteId)
|
||||||
|
return if (response == null) {
|
||||||
|
ResponseEntity.noContent().build()
|
||||||
|
} else {
|
||||||
|
ResponseEntity.ok(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseSendInitialConditionsTime(time: String?): LocalDateTime {
|
||||||
|
val normalizedTime = time?.trim()?.takeIf { it.isNotEmpty() }
|
||||||
|
?: return LocalDateTime.now()
|
||||||
|
|
||||||
|
return try {
|
||||||
|
LocalDateTime.parse(normalizedTime, sendInitialConditionsTimeFormatter)
|
||||||
|
} catch (exception: DateTimeParseException) {
|
||||||
|
throw ResponseStatusException(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Параметр time должен быть в формате dd.mm.yyyy hh:MM:ss.zzz",
|
||||||
|
exception
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/satellites")
|
@PostMapping("/satellites")
|
||||||
fun createSatellite(@Valid @RequestBody request: SatelliteCreateDTO) =
|
fun createSatellite(@Valid @RequestBody request: SatelliteCreateDTO) =
|
||||||
satelliteCatalogService.createSatellite(request)
|
satelliteCatalogService.createSatellite(request)
|
||||||
|
|||||||
+9
-1
@@ -11,13 +11,15 @@ import org.springframework.web.bind.annotation.RestController
|
|||||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.MissionRequestDTO
|
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.MissionRequestDTO
|
||||||
import space.nstart.pcp.slots_service.service.MissionService
|
import space.nstart.pcp.slots_service.service.MissionService
|
||||||
import space.nstart.pcp.slots_service.service.SatelliteCatalogService
|
import space.nstart.pcp.slots_service.service.SatelliteCatalogService
|
||||||
|
import space.nstart.pcp.slots_service.service.SlotService
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/current-plans")
|
@RequestMapping("/api/current-plans")
|
||||||
class CurrentPlansController(
|
class CurrentPlansController(
|
||||||
private val satelliteCatalogService: SatelliteCatalogService,
|
private val satelliteCatalogService: SatelliteCatalogService,
|
||||||
private val missionService: MissionService
|
private val missionService: MissionService,
|
||||||
|
private val slotService: SlotService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@GetMapping("/satellites")
|
@GetMapping("/satellites")
|
||||||
@@ -36,6 +38,9 @@ class CurrentPlansController(
|
|||||||
@GetMapping("/missions/{missionId}/modes")
|
@GetMapping("/missions/{missionId}/modes")
|
||||||
fun modes(@PathVariable missionId: UUID) = missionService.modesByMission(missionId)
|
fun modes(@PathVariable missionId: UUID) = missionService.modesByMission(missionId)
|
||||||
|
|
||||||
|
@GetMapping("/booked-slots")
|
||||||
|
fun bookedSlots(@RequestParam ids: List<Long>) = slotService.bookedByIds(ids)
|
||||||
|
|
||||||
@PostMapping("/missions/{missionId}/surveys/calculate")
|
@PostMapping("/missions/{missionId}/surveys/calculate")
|
||||||
fun calculateSurveys(
|
fun calculateSurveys(
|
||||||
@PathVariable missionId: UUID,
|
@PathVariable missionId: UUID,
|
||||||
@@ -44,4 +49,7 @@ class CurrentPlansController(
|
|||||||
|
|
||||||
@PostMapping("/missions/{missionId}/drops/calculate")
|
@PostMapping("/missions/{missionId}/drops/calculate")
|
||||||
fun calculateDrops(@PathVariable missionId: UUID) = missionService.calculateDrops(missionId)
|
fun calculateDrops(@PathVariable missionId: UUID) = missionService.calculateDrops(missionId)
|
||||||
|
|
||||||
|
@PostMapping("/missions/{missionId}/confirm")
|
||||||
|
fun confirmMission(@PathVariable missionId: UUID) = missionService.confirmMission(missionId)
|
||||||
}
|
}
|
||||||
|
|||||||
+33
@@ -57,4 +57,37 @@ class RvaRestController(
|
|||||||
@PathVariable duration: Long,
|
@PathVariable duration: Long,
|
||||||
@RequestBody stations: List<StationDTO>
|
@RequestBody stations: List<StationDTO>
|
||||||
) = slotService.rvaMergeOnRev(satelliteId, duration, stations)
|
) = slotService.rvaMergeOnRev(satelliteId, duration, stations)
|
||||||
|
|
||||||
|
@PostMapping("/average/{satelliteId}/{duration}")
|
||||||
|
@ResponseBody
|
||||||
|
fun rvaAverage(
|
||||||
|
@PathVariable satelliteId: Long,
|
||||||
|
@PathVariable duration: Long,
|
||||||
|
@RequestBody stations: List<StationDTO>
|
||||||
|
): List<RvaAverageDTO> {
|
||||||
|
val days = duration.coerceAtLeast(1).toDouble()
|
||||||
|
val zonesByStation = slotService.rvaCommon(satelliteId, duration, stations)
|
||||||
|
.groupBy { it.stationId }
|
||||||
|
|
||||||
|
return stations
|
||||||
|
.sortedBy { it.number }
|
||||||
|
.map { station ->
|
||||||
|
val zones = zonesByStation[station.number.toLong()].orEmpty()
|
||||||
|
val zoneCount = zones.size
|
||||||
|
val totalDurationSec = zones.sumOf { it.duration }
|
||||||
|
RvaAverageDTO(
|
||||||
|
ppiNumber = station.number.toLong(),
|
||||||
|
ppiLatitude = station.position.lat,
|
||||||
|
averageCount = zoneCount / days,
|
||||||
|
averageDurationSec = if (zoneCount == 0) 0.0 else totalDurationSec / zoneCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class RvaAverageDTO(
|
||||||
|
val ppiNumber: Long,
|
||||||
|
val ppiLatitude: Double,
|
||||||
|
val averageCount: Double,
|
||||||
|
val averageDurationSec: Double
|
||||||
|
)
|
||||||
|
|||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
package space.nstart.pcp.slots_service.controller
|
||||||
|
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import space.nstart.pcp.slots_service.service.TleAnalysisService
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/tle-analysis")
|
||||||
|
class TleAnalysisController(
|
||||||
|
private val tleAnalysisService: TleAnalysisService
|
||||||
|
) {
|
||||||
|
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||||
|
|
||||||
|
@GetMapping("/satellites")
|
||||||
|
fun satellites() =
|
||||||
|
tleAnalysisService.satellites().also { satellites ->
|
||||||
|
logger.info("TLE analysis satellites request completed: satellites={}", satellites.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/satellites/{satelliteId}")
|
||||||
|
fun analyzeSatellite(
|
||||||
|
@PathVariable satelliteId: Long,
|
||||||
|
@RequestParam(defaultValue = "5") tleCount: Int
|
||||||
|
) = tleAnalysisService.analyzeSatellite(satelliteId, tleCount).also { response ->
|
||||||
|
logger.info(
|
||||||
|
"TLE analysis report request completed: satelliteId={}, requestedTleCount={}, rows={}",
|
||||||
|
satelliteId,
|
||||||
|
tleCount,
|
||||||
|
response.rows.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/satellites/{satelliteId}/analysis")
|
||||||
|
fun analyzeSatelliteDetails(
|
||||||
|
@PathVariable satelliteId: Long,
|
||||||
|
@RequestParam(defaultValue = "5") tleCount: Int
|
||||||
|
) = analyzeSatellite(satelliteId, tleCount)
|
||||||
|
}
|
||||||
+183
@@ -0,0 +1,183 @@
|
|||||||
|
package space.nstart.pcp.slots_service.controller
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
|
||||||
|
import space.nstart.pcp.slots_service.configuration.CustomValidationException
|
||||||
|
import space.nstart.pcp.slots_service.dto.tlecomparison.TleCompareRequestDTO
|
||||||
|
import space.nstart.pcp.slots_service.dto.tlecomparison.TleCompareResponseDTO
|
||||||
|
import space.nstart.pcp.slots_service.dto.tlecomparison.TleCompareRowDTO
|
||||||
|
import space.nstart.pcp.slots_service.dto.tlecomparison.TleParsedDTO
|
||||||
|
import space.nstart.pcp.slots_service.dto.tlecomparison.TlePositionDTO
|
||||||
|
import space.nstart.pcp.slots_service.dto.tlecomparison.TlePositionDeltaDTO
|
||||||
|
import space.nstart.pcp.slots_service.dto.tlecomparison.TleTextRequestDTO
|
||||||
|
import space.nstart.pcp.slots_service.service.BallisticsService
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/tle-comparison")
|
||||||
|
class TleComparisonController(
|
||||||
|
private val ballisticsService: BallisticsService
|
||||||
|
) {
|
||||||
|
|
||||||
|
@PostMapping("/parse")
|
||||||
|
fun parse(@RequestBody request: TleTextRequestDTO): TleParsedDTO =
|
||||||
|
ballisticsService.parseTle(parseTleText(request.tle))
|
||||||
|
|
||||||
|
@PostMapping("/compare")
|
||||||
|
fun compare(@RequestBody request: TleCompareRequestDTO): TleCompareResponseDTO {
|
||||||
|
val firstTle = parseTleText(request.first)
|
||||||
|
val secondTle = parseTleText(request.second)
|
||||||
|
val firstParsed = ballisticsService.parseTle(firstTle)
|
||||||
|
val secondParsed = ballisticsService.parseTle(secondTle)
|
||||||
|
val calculationTime = request.time
|
||||||
|
?: throw CustomValidationException("Задайте время сравнения TLE")
|
||||||
|
val firstEpoch = firstParsed.time
|
||||||
|
?: throw CustomValidationException("Не удалось определить эпоху первого TLE")
|
||||||
|
val secondEpoch = secondParsed.time
|
||||||
|
?: throw CustomValidationException("Не удалось определить эпоху второго TLE")
|
||||||
|
val maxEpoch = maxOf(firstEpoch, secondEpoch)
|
||||||
|
if (calculationTime < maxEpoch) {
|
||||||
|
throw CustomValidationException("Время сравнения должно быть не раньше максимальной эпохи TLE: $maxEpoch")
|
||||||
|
}
|
||||||
|
|
||||||
|
val firstPoint = ballisticsService.tlePoint(firstTle, calculationTime).toPosition()
|
||||||
|
val secondPoint = ballisticsService.tlePoint(secondTle, calculationTime).toPosition()
|
||||||
|
val delta = TlePositionDeltaDTO(
|
||||||
|
dx = secondPoint.x - firstPoint.x,
|
||||||
|
dy = secondPoint.y - firstPoint.y,
|
||||||
|
dz = secondPoint.z - firstPoint.z,
|
||||||
|
norm = distance(secondPoint.x - firstPoint.x, secondPoint.y - firstPoint.y, secondPoint.z - firstPoint.z)
|
||||||
|
)
|
||||||
|
|
||||||
|
return TleCompareResponseDTO(
|
||||||
|
time = calculationTime,
|
||||||
|
firstEpoch = firstEpoch,
|
||||||
|
secondEpoch = secondEpoch,
|
||||||
|
first = firstPoint,
|
||||||
|
second = secondPoint,
|
||||||
|
radiusVectorDelta = delta,
|
||||||
|
rows = buildRows(firstPoint, secondPoint, delta)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseTleText(raw: String): TLEDTO {
|
||||||
|
val lines = raw.lines()
|
||||||
|
.map { it.trim() }
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
|
||||||
|
if (lines.isEmpty()) {
|
||||||
|
throw CustomValidationException("Введите TLE")
|
||||||
|
}
|
||||||
|
|
||||||
|
val firstIndex = lines.indexOfFirst { it.startsWith("1 ") }
|
||||||
|
val secondIndex = lines.indexOfFirst { it.startsWith("2 ") }
|
||||||
|
if (firstIndex < 0 || secondIndex < 0) {
|
||||||
|
throw CustomValidationException("TLE должен содержать строки 1 и 2")
|
||||||
|
}
|
||||||
|
|
||||||
|
val first = lines[firstIndex]
|
||||||
|
val second = lines[secondIndex]
|
||||||
|
val header = lines.take(firstIndex)
|
||||||
|
.firstOrNull { !it.startsWith("1 ") && !it.startsWith("2 ") }
|
||||||
|
|
||||||
|
if (first.length < 69 || second.length < 69) {
|
||||||
|
throw CustomValidationException("Строки TLE должны содержать не менее 69 символов")
|
||||||
|
}
|
||||||
|
|
||||||
|
return TLEDTO(
|
||||||
|
header = header,
|
||||||
|
first = first.take(69),
|
||||||
|
second = second.take(69)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildRows(
|
||||||
|
first: TlePositionDTO,
|
||||||
|
second: TlePositionDTO,
|
||||||
|
delta: TlePositionDeltaDTO
|
||||||
|
): List<TleCompareRowDTO> = listOf(
|
||||||
|
timeRow("time", "Время расчета", first.time, second.time),
|
||||||
|
textRow("revolution", "Виток", first.revolution.toString(), second.revolution.toString()),
|
||||||
|
numberRow("x", "X радиус-вектора, м", first.x, second.x),
|
||||||
|
numberRow("y", "Y радиус-вектора, м", first.y, second.y),
|
||||||
|
numberRow("z", "Z радиус-вектора, м", first.z, second.z),
|
||||||
|
numberRow("radiusNorm", "Модуль радиус-вектора, м", first.radiusNorm, second.radiusNorm),
|
||||||
|
numberRow("vx", "Vx, м/с", first.vx, second.vx),
|
||||||
|
numberRow("vy", "Vy, м/с", first.vy, second.vy),
|
||||||
|
numberRow("vz", "Vz, м/с", first.vz, second.vz),
|
||||||
|
numberRow("velocityNorm", "Модуль скорости, м/с", first.velocityNorm, second.velocityNorm),
|
||||||
|
TleCompareRowDTO("dx", "ΔX = X2 - X1, м", first = null, second = null, delta = delta.dx.formatSignedNumber()),
|
||||||
|
TleCompareRowDTO("dy", "ΔY = Y2 - Y1, м", first = null, second = null, delta = delta.dy.formatSignedNumber()),
|
||||||
|
TleCompareRowDTO("dz", "ΔZ = Z2 - Z1, м", first = null, second = null, delta = delta.dz.formatSignedNumber()),
|
||||||
|
TleCompareRowDTO("radiusDeltaNorm", "|Δr|, м", first = null, second = null, delta = delta.norm.formatNumber())
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun OrbPointDTO.toPosition(): TlePositionDTO = TlePositionDTO(
|
||||||
|
time = time,
|
||||||
|
revolution = revolution,
|
||||||
|
x = x,
|
||||||
|
y = y,
|
||||||
|
z = z,
|
||||||
|
vx = vx,
|
||||||
|
vy = vy,
|
||||||
|
vz = vz,
|
||||||
|
radiusNorm = distance(x, y, z),
|
||||||
|
velocityNorm = distance(vx, vy, vz)
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun textRow(code: String, name: String, first: String?, second: String?): TleCompareRowDTO =
|
||||||
|
TleCompareRowDTO(
|
||||||
|
code = code,
|
||||||
|
name = name,
|
||||||
|
first = first,
|
||||||
|
second = second,
|
||||||
|
delta = if (first != null && second != null && first != second) "изменено" else null
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun timeRow(code: String, name: String, first: LocalDateTime?, second: LocalDateTime?): TleCompareRowDTO {
|
||||||
|
val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME
|
||||||
|
val delta = if (first != null && second != null) {
|
||||||
|
val seconds = Duration.between(first, second).seconds
|
||||||
|
formatSecondsDelta(seconds)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
return TleCompareRowDTO(code, name, first?.format(formatter), second?.format(formatter), delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun numberRow(code: String, name: String, first: Double?, second: Double?): TleCompareRowDTO =
|
||||||
|
TleCompareRowDTO(
|
||||||
|
code = code,
|
||||||
|
name = name,
|
||||||
|
first = first?.formatNumber(),
|
||||||
|
second = second?.formatNumber(),
|
||||||
|
delta = if (first != null && second != null) (second - first).formatSignedNumber() else null
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun distance(x: Double, y: Double, z: Double): Double = sqrt(x * x + y * y + z * z)
|
||||||
|
|
||||||
|
private fun Double.formatNumber(): String = String.format(Locale.US, "%.6f", this)
|
||||||
|
|
||||||
|
private fun Double.formatSignedNumber(): String {
|
||||||
|
val sign = if (this > 0) "+" else ""
|
||||||
|
return sign + String.format(Locale.US, "%.6f", this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatSecondsDelta(seconds: Long): String {
|
||||||
|
val sign = if (seconds > 0) "+" else if (seconds < 0) "-" else ""
|
||||||
|
val absolute = abs(seconds)
|
||||||
|
val hours = absolute / 3600
|
||||||
|
val minutes = (absolute % 3600) / 60
|
||||||
|
val secs = absolute % 60
|
||||||
|
return "$sign${hours}ч ${minutes}м ${secs}с"
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
package space.nstart.pcp.slots_service.dto.bookings
|
||||||
|
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSummaryDTO
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI DTO for the bookings page. It intentionally lives in ui-service because the page combines
|
||||||
|
* data from slots-service, request-service and satellite-catalog-service without changing their APIs.
|
||||||
|
*/
|
||||||
|
data class BookedSlotViewDTO(
|
||||||
|
val bookedSlotId: Long,
|
||||||
|
val satelliteId: Long,
|
||||||
|
val satelliteCode: String? = null,
|
||||||
|
val satelliteName: String? = null,
|
||||||
|
val satelliteTypeCode: String? = null,
|
||||||
|
val slotNum: Long,
|
||||||
|
val cycle: Long,
|
||||||
|
val timeStart: LocalDateTime? = null,
|
||||||
|
val timeStop: LocalDateTime? = null,
|
||||||
|
val durationSeconds: Long? = null,
|
||||||
|
val roll: Double? = null,
|
||||||
|
val revolution: Long? = null,
|
||||||
|
val revolutionSign: String? = null,
|
||||||
|
val status: String? = null,
|
||||||
|
val requestIds: List<String> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BookingRequestOptionDTO(
|
||||||
|
val id: String,
|
||||||
|
val name: String = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BookingFilterOptionsDTO(
|
||||||
|
val satellites: List<SatelliteSummaryDTO> = emptyList(),
|
||||||
|
val requests: List<BookingRequestOptionDTO> = emptyList(),
|
||||||
|
)
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
package space.nstart.pcp.slots_service.dto.tleanalysis
|
||||||
|
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
data class TleAnalysisSatelliteDTO(
|
||||||
|
val id: Long,
|
||||||
|
val catalogId: Long? = null,
|
||||||
|
val noradId: Long? = null,
|
||||||
|
val code: String = "",
|
||||||
|
val name: String = "",
|
||||||
|
val typeCode: String = "",
|
||||||
|
val active: Boolean = true,
|
||||||
|
val scanTle: Boolean = false,
|
||||||
|
val tleRecordsCount: Int = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TleAnalysisResponseDTO(
|
||||||
|
val satelliteId: Long,
|
||||||
|
val rows: List<TleAnalysisRowDTO>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TleAnalysisRowDTO(
|
||||||
|
val index: Int,
|
||||||
|
val epoch: LocalDateTime,
|
||||||
|
val revolution: Long,
|
||||||
|
val noradId: Long,
|
||||||
|
val tleHeader: String?,
|
||||||
|
val radiusVector: RadiusVectorDTO,
|
||||||
|
val radiusNorm: Double,
|
||||||
|
val discrepancy: RadiusVectorDiscrepancyDTO?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RadiusVectorDTO(
|
||||||
|
val x: Double,
|
||||||
|
val y: Double,
|
||||||
|
val z: Double
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RadiusVectorDiscrepancyDTO(
|
||||||
|
val previousEpoch: LocalDateTime,
|
||||||
|
val epochDifferenceHours: Double,
|
||||||
|
val dx: Double,
|
||||||
|
val dy: Double,
|
||||||
|
val dz: Double,
|
||||||
|
val norm: Double
|
||||||
|
)
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
package space.nstart.pcp.slots_service.dto.tlecomparison
|
||||||
|
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
class TleTextRequestDTO(
|
||||||
|
var tle: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
class TleCompareRequestDTO(
|
||||||
|
var first: String = "",
|
||||||
|
var second: String = "",
|
||||||
|
var time: LocalDateTime? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TleParsedDTO(
|
||||||
|
val satName: String? = null,
|
||||||
|
val noradId: Long? = null,
|
||||||
|
val revolution: Long? = null,
|
||||||
|
val time: LocalDateTime? = null,
|
||||||
|
val inclination: Double? = null,
|
||||||
|
val perigee: Double? = null,
|
||||||
|
val apogee: Double? = null,
|
||||||
|
val argPerigee: Double? = null,
|
||||||
|
val eccentricity: Double? = null,
|
||||||
|
val major: Double? = null,
|
||||||
|
val minor: Double? = null,
|
||||||
|
val meanAnomaly: Double? = null,
|
||||||
|
val period: Double? = null,
|
||||||
|
val semiMajor: Double? = null,
|
||||||
|
val semiMinor: Double? = null,
|
||||||
|
val ascendingNode: Double? = null,
|
||||||
|
val meanMotion: Double? = null,
|
||||||
|
val meanMotionTLE: Double? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TlePositionDTO(
|
||||||
|
val time: LocalDateTime,
|
||||||
|
val revolution: Long,
|
||||||
|
val x: Double,
|
||||||
|
val y: Double,
|
||||||
|
val z: Double,
|
||||||
|
val vx: Double,
|
||||||
|
val vy: Double,
|
||||||
|
val vz: Double,
|
||||||
|
val radiusNorm: Double,
|
||||||
|
val velocityNorm: Double
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TleCompareRowDTO(
|
||||||
|
val code: String,
|
||||||
|
val name: String,
|
||||||
|
val first: String?,
|
||||||
|
val second: String?,
|
||||||
|
val delta: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TleCompareResponseDTO(
|
||||||
|
val time: LocalDateTime,
|
||||||
|
val firstEpoch: LocalDateTime,
|
||||||
|
val secondEpoch: LocalDateTime,
|
||||||
|
val first: TlePositionDTO,
|
||||||
|
val second: TlePositionDTO,
|
||||||
|
val radiusVectorDelta: TlePositionDeltaDTO,
|
||||||
|
val rows: List<TleCompareRowDTO>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TlePositionDeltaDTO(
|
||||||
|
val dx: Double,
|
||||||
|
val dy: Double,
|
||||||
|
val dz: Double,
|
||||||
|
val norm: Double
|
||||||
|
)
|
||||||
+5
-2
@@ -65,8 +65,11 @@ class RequestRestController {
|
|||||||
|
|
||||||
|
|
||||||
@PostMapping("/station/{station_id}")
|
@PostMapping("/station/{station_id}")
|
||||||
fun sat(@PathVariable("station_id") id : String, @RequestParam view : Boolean) =
|
fun sat(
|
||||||
czmlService.station(id, view)
|
@PathVariable("station_id") id : String,
|
||||||
|
@RequestParam view : Boolean,
|
||||||
|
@RequestParam(required = false) orbitHeightKm : Double?
|
||||||
|
) = czmlService.station(id, view, orbitHeightKm)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+47
@@ -9,7 +9,11 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
|
|||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.FlightLineDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.FlightLineDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TlePointRequestDTO
|
||||||
|
import space.nstart.pcp.slots_service.dto.tlecomparison.TleParsedDTO
|
||||||
import space.nstart.pcp.slots_service.configuration.CustomErrorException
|
import space.nstart.pcp.slots_service.configuration.CustomErrorException
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@@ -68,6 +72,49 @@ class BallisticsService (webClientBuilderProvider: ObjectProvider<WebClient.Buil
|
|||||||
?.firstOrNull()
|
?.firstOrNull()
|
||||||
?: throw CustomErrorException("Не удалось получить положение спутника $id на время $time")
|
?: throw CustomErrorException("Не удалось получить положение спутника $id на время $time")
|
||||||
|
|
||||||
|
fun receiveRv(request: SatelliteICDTO) {
|
||||||
|
webClientBuilder.build()
|
||||||
|
.post()
|
||||||
|
.uri("$url/api/satellites/receive-rv")
|
||||||
|
.bodyValue(request)
|
||||||
|
.retrieve()
|
||||||
|
.toBodilessEntity()
|
||||||
|
.block()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun currentInitialConditions(satelliteId: Long): SatelliteICDTO? =
|
||||||
|
webClientBuilder.build()
|
||||||
|
.get()
|
||||||
|
.uri("$url/api/satellites/$satelliteId/initial-conditions/current")
|
||||||
|
.exchangeToMono { response ->
|
||||||
|
when {
|
||||||
|
response.statusCode().is2xxSuccessful -> response.bodyToMono(SatelliteICDTO::class.java)
|
||||||
|
response.statusCode().value() == 404 || response.statusCode().value() == 204 -> reactor.core.publisher.Mono.empty()
|
||||||
|
else -> response.createException().flatMap { reactor.core.publisher.Mono.error(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.block()
|
||||||
|
|
||||||
|
fun parseTle(tle: TLEDTO): TleParsedDTO =
|
||||||
|
webClientBuilder.build()
|
||||||
|
.post()
|
||||||
|
.uri("$url/api/tle/parse")
|
||||||
|
.bodyValue(tle)
|
||||||
|
.retrieve()
|
||||||
|
.bodyToMono(TleParsedDTO::class.java)
|
||||||
|
.block()
|
||||||
|
?: throw CustomErrorException("Не удалось разобрать TLE")
|
||||||
|
|
||||||
|
fun tlePoint(tle: TLEDTO, time: LocalDateTime): OrbPointDTO =
|
||||||
|
webClientBuilder.build()
|
||||||
|
.post()
|
||||||
|
.uri("$url/api/tle/point")
|
||||||
|
.bodyValue(TlePointRequestDTO(tle = tle, time = time))
|
||||||
|
.retrieve()
|
||||||
|
.bodyToMono(OrbPointDTO::class.java)
|
||||||
|
.block()
|
||||||
|
?: throw CustomErrorException("Не удалось рассчитать положение по TLE на время $time")
|
||||||
|
|
||||||
private fun withTimeInterval(
|
private fun withTimeInterval(
|
||||||
path: String,
|
path: String,
|
||||||
timeStart: LocalDateTime?,
|
timeStart: LocalDateTime?,
|
||||||
|
|||||||
+5
-3
@@ -50,6 +50,7 @@ class CZMLService {
|
|||||||
private val docStationName = "station_"
|
private val docStationName = "station_"
|
||||||
private val entityStationName = "station_"
|
private val entityStationName = "station_"
|
||||||
private val docReqCovName ="req_cov_"
|
private val docReqCovName ="req_cov_"
|
||||||
|
private val DEFAULT_STATION_ORBIT_HEIGHT_KM = 500.0
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private lateinit var stationService: StationService
|
private lateinit var stationService: StationService
|
||||||
@@ -378,9 +379,10 @@ private val docReqCovName ="req_cov_"
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun station(id : String, view : Boolean) : Iterable<CZMLEntity>{
|
fun station(id : String, view : Boolean, orbitHeightKm: Double? = null) : Iterable<CZMLEntity>{
|
||||||
val req = stationService.station(id).block()?:return listOf()
|
val req = stationService.station(id).block()?:return listOf()
|
||||||
val radius = ellipseAx( (500.0 * 1000.0), req.elevationMin * PI / 180)
|
val normalizedOrbitHeightKm = orbitHeightKm?.takeIf { it > 0 } ?: DEFAULT_STATION_ORBIT_HEIGHT_KM
|
||||||
|
val radius = ellipseAx((normalizedOrbitHeightKm * 1000.0), req.elevationMin * PI / 180)
|
||||||
|
|
||||||
val r: MutableList<CZMLEntity> = mutableListOf(CZMLEntity("document", docStationName+id , "1.1"))
|
val r: MutableList<CZMLEntity> = mutableListOf(CZMLEntity("document", docStationName+id , "1.1"))
|
||||||
r.add(
|
r.add(
|
||||||
@@ -401,7 +403,7 @@ private val docReqCovName ="req_cov_"
|
|||||||
extrudedHeight = 1000.0,
|
extrudedHeight = 1000.0,
|
||||||
height = 1000.0
|
height = 1000.0
|
||||||
),
|
),
|
||||||
description = "Станция ${req.name}",
|
description = "Станция ${req.name}<br>Высота орбиты для зоны видимости: ${normalizedOrbitHeightKm} км",
|
||||||
model = CZMLModel(
|
model = CZMLModel(
|
||||||
gltf = "/model/station.glb",
|
gltf = "/model/station.glb",
|
||||||
show = !view
|
show = !view
|
||||||
|
|||||||
+9
@@ -103,6 +103,15 @@ class MissionService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>
|
|||||||
.block()
|
.block()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun confirmMission(missionId: UUID) {
|
||||||
|
webClientBuilder.build()
|
||||||
|
.post()
|
||||||
|
.uri("$url/api/missions/$missionId/confirm")
|
||||||
|
.retrieve()
|
||||||
|
.toBodilessEntity()
|
||||||
|
.block()
|
||||||
|
}
|
||||||
|
|
||||||
fun modesByMission(missionId: UUID): List<ModeResponseDTO> =
|
fun modesByMission(missionId: UUID): List<ModeResponseDTO> =
|
||||||
webClientBuilder.build()
|
webClientBuilder.build()
|
||||||
.get()
|
.get()
|
||||||
|
|||||||
+101
@@ -22,6 +22,7 @@ import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SurveySlotDTO
|
|||||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSlotProfileDTO
|
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSlotProfileDTO
|
||||||
import tools.jackson.databind.ObjectMapper
|
import tools.jackson.databind.ObjectMapper
|
||||||
import space.nstart.pcp.slots_service.configuration.CustomErrorException
|
import space.nstart.pcp.slots_service.configuration.CustomErrorException
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.BookedSlotDetailsDTO
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -148,6 +149,85 @@ class SlotService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fun allBooked(): List<BookedSlotDTO> =
|
||||||
|
webClientBuilder.build()
|
||||||
|
.get()
|
||||||
|
.uri("$url/api/slots/booking")
|
||||||
|
.retrieve()
|
||||||
|
.onStatus({ status -> status.isError }) { response ->
|
||||||
|
response.bodyToMono(String::class.java)
|
||||||
|
.defaultIfEmpty("error")
|
||||||
|
.flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
|
||||||
|
}
|
||||||
|
.bodyToFlux(BookedSlotDTO::class.java)
|
||||||
|
.collectList()
|
||||||
|
.block()
|
||||||
|
?: emptyList()
|
||||||
|
|
||||||
|
fun bookedByIds(bookedSlotIds: List<Long>): List<BookedSlotDetailsDTO> =
|
||||||
|
bookedDetailsByIds(bookedSlotIds)
|
||||||
|
|
||||||
|
fun bookedDetailsByIds(bookedSlotIds: List<Long>): List<BookedSlotDetailsDTO> {
|
||||||
|
val ids = bookedSlotIds.filter { it > 0 }.distinct().toSet()
|
||||||
|
if (ids.isEmpty()) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchBooked(
|
||||||
|
timeStart = null,
|
||||||
|
timeStop = null,
|
||||||
|
requestId = null,
|
||||||
|
satelliteIds = null
|
||||||
|
).filter { it.bookedSlotId in ids }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun searchBooked(
|
||||||
|
timeStart: LocalDateTime?,
|
||||||
|
timeStop: LocalDateTime?,
|
||||||
|
requestId: String?,
|
||||||
|
satelliteIds: List<Long>?
|
||||||
|
): List<BookedSlotDetailsDTO> = webClientBuilder.build()
|
||||||
|
.get()
|
||||||
|
.uri(
|
||||||
|
UriComponentsBuilder
|
||||||
|
.fromUriString("$url/api/slots/booking/search")
|
||||||
|
.queryParamIfPresent("timeStart", java.util.Optional.ofNullable(timeStart))
|
||||||
|
.queryParamIfPresent("timeStop", java.util.Optional.ofNullable(timeStop))
|
||||||
|
.queryParamIfPresent("requestId", java.util.Optional.ofNullable(requestId?.trim()?.takeIf { it.isNotEmpty() }))
|
||||||
|
.apply {
|
||||||
|
satelliteIds.orEmpty().distinct().forEach { queryParam("satelliteIds", it) }
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
.toUriString()
|
||||||
|
)
|
||||||
|
.retrieve()
|
||||||
|
.onStatus({ status -> status.isError }) { response ->
|
||||||
|
response.bodyToMono(String::class.java)
|
||||||
|
.defaultIfEmpty("error")
|
||||||
|
.flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
|
||||||
|
}
|
||||||
|
.bodyToFlux(BookedSlotDetailsDTO::class.java)
|
||||||
|
.collectList()
|
||||||
|
.block()
|
||||||
|
?: emptyList()
|
||||||
|
|
||||||
|
fun bookedByRequest(requestId: String): List<SlotDTO> =
|
||||||
|
webClientBuilder.build()
|
||||||
|
.get()
|
||||||
|
.uri("$url/api/slots/booking/by-request/$requestId")
|
||||||
|
.retrieve()
|
||||||
|
.onStatus({ status -> status.isError }) { response ->
|
||||||
|
response.bodyToMono(String::class.java)
|
||||||
|
.defaultIfEmpty("error")
|
||||||
|
.flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
|
||||||
|
}
|
||||||
|
.bodyToFlux(SlotDTO::class.java)
|
||||||
|
.collectList()
|
||||||
|
.block()
|
||||||
|
?: emptyList()
|
||||||
|
|
||||||
fun bookReq(req: BookingRequestDTO): List<BookedSlotDTO> =
|
fun bookReq(req: BookingRequestDTO): List<BookedSlotDTO> =
|
||||||
webClientBuilder.build()
|
webClientBuilder.build()
|
||||||
.post()
|
.post()
|
||||||
@@ -244,6 +324,27 @@ class SlotService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) {
|
|||||||
}
|
}
|
||||||
.block()
|
.block()
|
||||||
|
|
||||||
|
|
||||||
|
fun sendSatelliteInitialConditions(satelliteId: Long, time: LocalDateTime) {
|
||||||
|
webClientBuilder.build()
|
||||||
|
.post()
|
||||||
|
.uri(
|
||||||
|
UriComponentsBuilder
|
||||||
|
.fromUriString("$url/api/satellite/{satelliteId}/send-ic")
|
||||||
|
.queryParam("time", time)
|
||||||
|
.buildAndExpand(satelliteId)
|
||||||
|
.toUriString()
|
||||||
|
)
|
||||||
|
.retrieve()
|
||||||
|
.onStatus({ status -> status.isError }) { response ->
|
||||||
|
response.bodyToMono(String::class.java)
|
||||||
|
.defaultIfEmpty("error")
|
||||||
|
.flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
|
||||||
|
}
|
||||||
|
.toBodilessEntity()
|
||||||
|
.block()
|
||||||
|
}
|
||||||
|
|
||||||
fun saveSatelliteInitialConditions(
|
fun saveSatelliteInitialConditions(
|
||||||
satelliteId: Long,
|
satelliteId: Long,
|
||||||
request: InitialConditionsDTO,
|
request: InitialConditionsDTO,
|
||||||
|
|||||||
+253
@@ -0,0 +1,253 @@
|
|||||||
|
package space.nstart.pcp.slots_service.service
|
||||||
|
|
||||||
|
import ballistics.Ballistics
|
||||||
|
import ballistics.orbitalPoints.timeStepper.TLEStepper
|
||||||
|
import ballistics.types.EarthType
|
||||||
|
import ballistics.types.TLE
|
||||||
|
import ballistics.utils.fromDateTime
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEExtensionDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSummaryDTO
|
||||||
|
import space.nstart.pcp.slots_service.configuration.CustomErrorException
|
||||||
|
import space.nstart.pcp.slots_service.dto.tleanalysis.RadiusVectorDTO
|
||||||
|
import space.nstart.pcp.slots_service.dto.tleanalysis.RadiusVectorDiscrepancyDTO
|
||||||
|
import space.nstart.pcp.slots_service.dto.tleanalysis.TleAnalysisSatelliteDTO
|
||||||
|
import space.nstart.pcp.slots_service.dto.tleanalysis.TleAnalysisResponseDTO
|
||||||
|
import space.nstart.pcp.slots_service.dto.tleanalysis.TleAnalysisRowDTO
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class TleAnalysisService(
|
||||||
|
private val tleMonitoringService: TleMonitoringService,
|
||||||
|
private val satelliteCatalogService: SatelliteCatalogService
|
||||||
|
) {
|
||||||
|
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||||
|
|
||||||
|
fun satellites(): List<TleAnalysisSatelliteDTO> {
|
||||||
|
logger.info("TLE analysis satellites list stage started")
|
||||||
|
var catalogSatellites = emptyList<SatelliteSummaryDTO>()
|
||||||
|
val catalogLoadMs = measureTimeMillis {
|
||||||
|
catalogSatellites = runCatching { satelliteCatalogService.allSatellites() }
|
||||||
|
.onFailure { exception ->
|
||||||
|
logger.warn("Не удалось загрузить спутники из satellite-catalog-service для анализа TLE: {}", exception.message)
|
||||||
|
}
|
||||||
|
.getOrDefault(emptyList())
|
||||||
|
}
|
||||||
|
var tleRecords = emptyList<TLEExtensionDTO>()
|
||||||
|
val tleLoadMs = measureTimeMillis {
|
||||||
|
tleRecords = runCatching { tleMonitoringService.allTles() }
|
||||||
|
.onFailure { exception ->
|
||||||
|
logger.warn("Не удалось загрузить TLE из tle-monitoring-service для списка анализа TLE: {}", exception.message)
|
||||||
|
}
|
||||||
|
.getOrDefault(emptyList())
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
"TLE analysis satellites list sources loaded: catalogSatellites={}, tleRecords={}, catalogLoadMs={}, tleLoadMs={}",
|
||||||
|
catalogSatellites.size,
|
||||||
|
tleRecords.size,
|
||||||
|
catalogLoadMs,
|
||||||
|
tleLoadMs
|
||||||
|
)
|
||||||
|
|
||||||
|
if (tleRecords.isEmpty()) {
|
||||||
|
val fallbackSatellites = catalogSatellites.mapNotNull { satellite -> satellite.toAnalysisSatelliteFallback() }
|
||||||
|
logger.info(
|
||||||
|
"TLE analysis satellites list built from catalog fallback: satellites={}",
|
||||||
|
fallbackSatellites.size
|
||||||
|
)
|
||||||
|
return fallbackSatellites
|
||||||
|
}
|
||||||
|
|
||||||
|
val catalogByNoradId = catalogSatellites
|
||||||
|
.mapNotNull { satellite -> satellite.noradId?.let { noradId -> noradId to satellite } }
|
||||||
|
.toMap()
|
||||||
|
val catalogById = catalogSatellites.associateBy { it.id }
|
||||||
|
|
||||||
|
val satellites = tleRecords
|
||||||
|
.groupingBy { record -> record.satelliteId }
|
||||||
|
.eachCount()
|
||||||
|
.entries
|
||||||
|
.sortedBy { (satelliteId, _) -> satelliteId }
|
||||||
|
.map { (tleSatelliteId, recordsCount) ->
|
||||||
|
val catalogSatellite = catalogByNoradId[tleSatelliteId] ?: catalogById[tleSatelliteId]
|
||||||
|
catalogSatellite?.toAnalysisSatellite(
|
||||||
|
tleSatelliteId = tleSatelliteId,
|
||||||
|
recordsCount = recordsCount
|
||||||
|
) ?: TleAnalysisSatelliteDTO(
|
||||||
|
id = tleSatelliteId,
|
||||||
|
noradId = tleSatelliteId,
|
||||||
|
name = "КА $tleSatelliteId",
|
||||||
|
tleRecordsCount = recordsCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
"TLE analysis satellites list built from TLE records: satellites={}, matchedCatalogSatellites={}, unmatchedTleSatellites={}",
|
||||||
|
satellites.size,
|
||||||
|
satellites.count { it.catalogId != null },
|
||||||
|
satellites.count { it.catalogId == null }
|
||||||
|
)
|
||||||
|
return satellites
|
||||||
|
}
|
||||||
|
|
||||||
|
fun analyzeSatellite(satelliteId: Long, tleCount: Int = DEFAULT_TLE_ANALYSIS_COUNT): TleAnalysisResponseDTO {
|
||||||
|
val normalizedTleCount = tleCount.coerceAtLeast(1)
|
||||||
|
logger.info(
|
||||||
|
"TLE analysis report stage started: satelliteId={}, requestedTleCount={}, normalizedTleCount={}",
|
||||||
|
satelliteId,
|
||||||
|
tleCount,
|
||||||
|
normalizedTleCount
|
||||||
|
)
|
||||||
|
|
||||||
|
var sourceRecords = emptyList<TLEExtensionDTO>()
|
||||||
|
val sourceLoadMs = measureTimeMillis {
|
||||||
|
sourceRecords = tleMonitoringService.satelliteTles(satelliteId)
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
"TLE analysis report source TLE loaded: satelliteId={}, records={}, durationMs={}",
|
||||||
|
satelliteId,
|
||||||
|
sourceRecords.size,
|
||||||
|
sourceLoadMs
|
||||||
|
)
|
||||||
|
|
||||||
|
val parsedRecords = sourceRecords.mapNotNull { parseRecord(it) }.sortedBy { it.epoch }
|
||||||
|
val skippedRecords = sourceRecords.size - parsedRecords.size
|
||||||
|
if (skippedRecords > 0) {
|
||||||
|
logger.warn(
|
||||||
|
"TLE analysis report skipped invalid TLE records: satelliteId={}, skipped={}, sourceRecords={}",
|
||||||
|
satelliteId,
|
||||||
|
skippedRecords,
|
||||||
|
sourceRecords.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val records = parsedRecords.takeLast(normalizedTleCount)
|
||||||
|
logger.info(
|
||||||
|
"TLE analysis report records selected: satelliteId={}, parsedRecords={}, selectedRecords={}, firstEpoch={}, lastEpoch={}",
|
||||||
|
satelliteId,
|
||||||
|
parsedRecords.size,
|
||||||
|
records.size,
|
||||||
|
records.firstOrNull()?.epoch,
|
||||||
|
records.lastOrNull()?.epoch
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = emptyList<TleAnalysisRowDTO>()
|
||||||
|
val rowsBuildMs = measureTimeMillis {
|
||||||
|
rows = records.mapIndexed { index, record ->
|
||||||
|
// Сравнение пары TLE выполняется на эпоху более поздней записи.
|
||||||
|
val comparisonEpoch = record.epoch
|
||||||
|
val currentPoint = calculatePoint(record.tle, comparisonEpoch)
|
||||||
|
val radiusVector = RadiusVectorDTO(
|
||||||
|
x = currentPoint.r.x,
|
||||||
|
y = currentPoint.r.y,
|
||||||
|
z = currentPoint.r.z
|
||||||
|
)
|
||||||
|
|
||||||
|
val discrepancy = records.getOrNull(index - 1)?.let { previous ->
|
||||||
|
val epochDifferenceHours = Duration.between(previous.epoch, comparisonEpoch).toMillis() / MILLIS_IN_HOUR
|
||||||
|
val previousPointAtCurrentEpoch = calculatePoint(previous.tle, comparisonEpoch)
|
||||||
|
val dx = currentPoint.r.x - previousPointAtCurrentEpoch.r.x
|
||||||
|
val dy = currentPoint.r.y - previousPointAtCurrentEpoch.r.y
|
||||||
|
val dz = currentPoint.r.z - previousPointAtCurrentEpoch.r.z
|
||||||
|
RadiusVectorDiscrepancyDTO(
|
||||||
|
previousEpoch = previous.epoch,
|
||||||
|
epochDifferenceHours = epochDifferenceHours,
|
||||||
|
dx = dx,
|
||||||
|
dy = dy,
|
||||||
|
dz = dz,
|
||||||
|
norm = norm(dx, dy, dz)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TleAnalysisRowDTO(
|
||||||
|
index = index + 1,
|
||||||
|
epoch = record.epoch,
|
||||||
|
revolution = record.revolution,
|
||||||
|
noradId = record.noradId,
|
||||||
|
tleHeader = record.tle.header,
|
||||||
|
radiusVector = radiusVector,
|
||||||
|
radiusNorm = norm(radiusVector.x, radiusVector.y, radiusVector.z),
|
||||||
|
discrepancy = discrepancy
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
"TLE analysis report built: satelliteId={}, rows={}, durationMs={}",
|
||||||
|
satelliteId,
|
||||||
|
rows.size,
|
||||||
|
rowsBuildMs
|
||||||
|
)
|
||||||
|
|
||||||
|
return TleAnalysisResponseDTO(
|
||||||
|
satelliteId = satelliteId,
|
||||||
|
rows = rows
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseRecord(source: TLEExtensionDTO): ParsedTleRecord? = runCatching {
|
||||||
|
val tle = source.tle
|
||||||
|
val params = Ballistics().getTLEParams(TLE(tle.first, tle.second), tle.header ?: "")
|
||||||
|
ParsedTleRecord(
|
||||||
|
tle = tle,
|
||||||
|
epoch = params.time,
|
||||||
|
revolution = source.revolution.takeIf { it > 0 } ?: params.revolution,
|
||||||
|
noradId = params.noradId
|
||||||
|
)
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
|
private fun calculatePoint(tle: TLEDTO, time: LocalDateTime) = runCatching {
|
||||||
|
val stepper = TLEStepper(tle.first, tle.second, EarthType.PZ90d02)
|
||||||
|
stepper.calculate(fromDateTime(time))
|
||||||
|
}.getOrElse { ex ->
|
||||||
|
throw CustomErrorException("Ошибка расчета положения по TLE на время $time: ${ex.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun norm(x: Double, y: Double, z: Double): Double = sqrt(x * x + y * y + z * z)
|
||||||
|
|
||||||
|
private fun SatelliteSummaryDTO.toAnalysisSatellite(
|
||||||
|
tleSatelliteId: Long,
|
||||||
|
recordsCount: Int
|
||||||
|
): TleAnalysisSatelliteDTO =
|
||||||
|
TleAnalysisSatelliteDTO(
|
||||||
|
id = tleSatelliteId,
|
||||||
|
catalogId = id,
|
||||||
|
noradId = noradId ?: tleSatelliteId,
|
||||||
|
code = code,
|
||||||
|
name = name,
|
||||||
|
typeCode = typeCode,
|
||||||
|
active = active,
|
||||||
|
scanTle = scanTle,
|
||||||
|
tleRecordsCount = recordsCount
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun SatelliteSummaryDTO.toAnalysisSatelliteFallback(): TleAnalysisSatelliteDTO? {
|
||||||
|
val analysisId = noradId ?: id.takeIf { it > 0 } ?: return null
|
||||||
|
return TleAnalysisSatelliteDTO(
|
||||||
|
id = analysisId,
|
||||||
|
catalogId = id,
|
||||||
|
noradId = noradId,
|
||||||
|
code = code,
|
||||||
|
name = name,
|
||||||
|
typeCode = typeCode,
|
||||||
|
active = active,
|
||||||
|
scanTle = scanTle,
|
||||||
|
tleRecordsCount = 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class ParsedTleRecord(
|
||||||
|
val tle: TLEDTO,
|
||||||
|
val epoch: LocalDateTime,
|
||||||
|
val revolution: Long,
|
||||||
|
val noradId: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val DEFAULT_TLE_ANALYSIS_COUNT = 5
|
||||||
|
private const val MILLIS_IN_HOUR = 3_600_000.0
|
||||||
|
}
|
||||||
|
}
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
package space.nstart.pcp.slots_service.service
|
||||||
|
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.beans.factory.ObjectProvider
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEExtensionDTO
|
||||||
|
import space.nstart.pcp.slots_service.configuration.CustomErrorException
|
||||||
|
import tools.jackson.databind.ObjectMapper
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class TleMonitoringService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) {
|
||||||
|
private val webClientBuilder: WebClient.Builder = webClientBuilderProvider.ifAvailable ?: WebClient.builder()
|
||||||
|
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||||
|
|
||||||
|
@Value("\${settings.tle-monitoring-service:http://tle-monitoring-service:8080}")
|
||||||
|
private val tleMonitoringUrl: String = ""
|
||||||
|
|
||||||
|
fun allTles(): List<TLEExtensionDTO> {
|
||||||
|
val baseUrl = tleMonitoringUrl.trimEnd('/')
|
||||||
|
logger.info("TLE monitoring client all TLE request started: baseUrl={}", baseUrl)
|
||||||
|
return runCatching {
|
||||||
|
var records = emptyList<TLEExtensionDTO>()
|
||||||
|
val durationMs = measureTimeMillis {
|
||||||
|
records = webClientBuilder.build()
|
||||||
|
.get()
|
||||||
|
.uri("$baseUrl/v1/api/tle")
|
||||||
|
.retrieve()
|
||||||
|
.onStatus({ status -> status.isError }, ::mapError)
|
||||||
|
.bodyToFlux(TLEExtensionDTO::class.java)
|
||||||
|
.collectList()
|
||||||
|
.block()
|
||||||
|
?: emptyList()
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
"TLE monitoring client all TLE request completed: records={}, durationMs={}",
|
||||||
|
records.size,
|
||||||
|
durationMs
|
||||||
|
)
|
||||||
|
records
|
||||||
|
}.getOrElse { exception ->
|
||||||
|
logger.warn("TLE monitoring client all TLE request failed: {}", exception.message)
|
||||||
|
throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun satelliteTles(satelliteId: Long): List<TLEExtensionDTO> {
|
||||||
|
val baseUrl = tleMonitoringUrl.trimEnd('/')
|
||||||
|
logger.info(
|
||||||
|
"TLE monitoring client satellite TLE request started: satelliteId={}, baseUrl={}",
|
||||||
|
satelliteId,
|
||||||
|
baseUrl
|
||||||
|
)
|
||||||
|
return runCatching {
|
||||||
|
var records = emptyList<TLEExtensionDTO>()
|
||||||
|
val durationMs = measureTimeMillis {
|
||||||
|
records = webClientBuilder.build()
|
||||||
|
.get()
|
||||||
|
.uri("$baseUrl/v1/api/tle/satellite/$satelliteId")
|
||||||
|
.retrieve()
|
||||||
|
.onStatus({ status -> status.isError }, ::mapError)
|
||||||
|
.bodyToFlux(TLEExtensionDTO::class.java)
|
||||||
|
.collectList()
|
||||||
|
.block()
|
||||||
|
?: emptyList()
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
"TLE monitoring client satellite TLE request completed: satelliteId={}, records={}, durationMs={}",
|
||||||
|
satelliteId,
|
||||||
|
records.size,
|
||||||
|
durationMs
|
||||||
|
)
|
||||||
|
records
|
||||||
|
}.getOrElse { exception ->
|
||||||
|
logger.warn(
|
||||||
|
"TLE monitoring client satellite TLE request failed: satelliteId={}, error={}",
|
||||||
|
satelliteId,
|
||||||
|
exception.message
|
||||||
|
)
|
||||||
|
throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapError(response: org.springframework.web.reactive.function.client.ClientResponse): Mono<Throwable> =
|
||||||
|
response.bodyToMono(String::class.java)
|
||||||
|
.defaultIfEmpty("error")
|
||||||
|
.flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
|
||||||
|
|
||||||
|
private fun extractErrorMessage(body: String): String {
|
||||||
|
val text = body.trim()
|
||||||
|
if (text.isEmpty()) return "error"
|
||||||
|
|
||||||
|
return runCatching {
|
||||||
|
val node = ObjectMapper().readTree(text)
|
||||||
|
when {
|
||||||
|
node.has("message") -> node.get("message").asText()
|
||||||
|
node.has("error") -> node.get("error").asText()
|
||||||
|
else -> text
|
||||||
|
}
|
||||||
|
}.getOrDefault(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
(function () {
|
||||||
|
const state = {
|
||||||
|
satellites: [],
|
||||||
|
requests: [],
|
||||||
|
selectedSatelliteIds: new Set(),
|
||||||
|
bookings: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const el = (id) => document.getElementById(id);
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
setDefaultInterval();
|
||||||
|
bindEvents();
|
||||||
|
loadOptions().then(() => loadBookings());
|
||||||
|
});
|
||||||
|
|
||||||
|
function bindEvents() {
|
||||||
|
el('bookings-refresh')?.addEventListener('click', () => loadBookings());
|
||||||
|
el('bookings-reset')?.addEventListener('click', resetFilters);
|
||||||
|
el('bookings-satellites-clear')?.addEventListener('click', () => {
|
||||||
|
selectVisibleSatellites();
|
||||||
|
});
|
||||||
|
el('bookings-satellite-search')?.addEventListener('input', renderSatellites);
|
||||||
|
el('bookings-request')?.addEventListener('change', () => loadBookings());
|
||||||
|
el('bookings-time-start')?.addEventListener('change', () => loadBookings());
|
||||||
|
el('bookings-time-stop')?.addEventListener('change', () => loadBookings());
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDefaultInterval() {
|
||||||
|
const now = new Date();
|
||||||
|
now.setMinutes(0, 0, 0);
|
||||||
|
const stop = new Date(now.getTime());
|
||||||
|
stop.setDate(stop.getDate() + 7);
|
||||||
|
if (el('bookings-time-start')) el('bookings-time-start').value = toDateTimeLocal(now);
|
||||||
|
if (el('bookings-time-stop')) el('bookings-time-stop').value = toDateTimeLocal(stop);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFilters() {
|
||||||
|
state.selectedSatelliteIds.clear();
|
||||||
|
if (el('bookings-request')) el('bookings-request').value = '';
|
||||||
|
if (el('bookings-satellite-search')) el('bookings-satellite-search').value = '';
|
||||||
|
setDefaultInterval();
|
||||||
|
renderSatellites();
|
||||||
|
loadBookings();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadOptions() {
|
||||||
|
try {
|
||||||
|
const options = await fetchJson('/api/bookings/options');
|
||||||
|
state.satellites = Array.isArray(options.satellites) ? options.satellites : [];
|
||||||
|
state.requests = Array.isArray(options.requests) ? options.requests : [];
|
||||||
|
renderRequests();
|
||||||
|
renderSatellites();
|
||||||
|
} catch (error) {
|
||||||
|
showAlert(error.message || 'Не удалось загрузить справочники страницы бронирований.', 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBookings() {
|
||||||
|
setTableLoading();
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
const timeStart = el('bookings-time-start')?.value;
|
||||||
|
const timeStop = el('bookings-time-stop')?.value;
|
||||||
|
const requestId = el('bookings-request')?.value;
|
||||||
|
|
||||||
|
if (timeStart) params.set('timeStart', toIsoLocal(timeStart));
|
||||||
|
if (timeStop) params.set('timeStop', toIsoLocal(timeStop));
|
||||||
|
if (requestId) params.set('requestId', requestId);
|
||||||
|
state.selectedSatelliteIds.forEach((id) => params.append('satelliteIds', id));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const items = await fetchJson(`/api/bookings?${params.toString()}`);
|
||||||
|
state.bookings = Array.isArray(items) ? items : [];
|
||||||
|
renderBookings();
|
||||||
|
} catch (error) {
|
||||||
|
state.bookings = [];
|
||||||
|
renderBookings();
|
||||||
|
showAlert(error.message || 'Не удалось загрузить забронированные слоты.', 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleSatellites() {
|
||||||
|
const query = (el('bookings-satellite-search')?.value || '').trim().toLowerCase();
|
||||||
|
return state.satellites.filter((satellite) => satelliteMatches(satellite, query));
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectVisibleSatellites() {
|
||||||
|
const satellites = visibleSatellites();
|
||||||
|
satellites.forEach((satellite) => {
|
||||||
|
const id = Number(satellite.id);
|
||||||
|
if (!Number.isNaN(id)) {
|
||||||
|
state.selectedSatelliteIds.add(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
renderSatellites();
|
||||||
|
loadBookings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRequests() {
|
||||||
|
const select = el('bookings-request');
|
||||||
|
if (!select) return;
|
||||||
|
const current = select.value;
|
||||||
|
select.innerHTML = '<option value="">Все заявки</option>' + state.requests.map((request) => {
|
||||||
|
const title = request.name ? `${request.name} / ${request.id}` : request.id;
|
||||||
|
return `<option value="${escapeHtml(request.id)}">${escapeHtml(title)}</option>`;
|
||||||
|
}).join('');
|
||||||
|
select.value = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSatellites() {
|
||||||
|
const tbody = el('bookings-satellites-body');
|
||||||
|
if (!tbody) return;
|
||||||
|
const satellites = visibleSatellites();
|
||||||
|
|
||||||
|
if (!satellites.length) {
|
||||||
|
tbody.innerHTML = emptyRow(3, 'Спутники не найдены.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = satellites.map((satellite) => {
|
||||||
|
const selected = state.selectedSatelliteIds.has(Number(satellite.id));
|
||||||
|
return `
|
||||||
|
<tr class="${selected ? 'bookings-selected-row' : ''}" data-satellite-id="${escapeHtml(satellite.id)}">
|
||||||
|
<td><input class="form-check-input" type="checkbox" ${selected ? 'checked' : ''} aria-label="Выбрать спутник"></td>
|
||||||
|
<td class="bookings-id">${escapeHtml(satellite.id ?? '—')}</td>
|
||||||
|
<td>
|
||||||
|
<div class="bookings-main-text">${escapeHtml(satellite.name || satellite.code || 'КА')}</div>
|
||||||
|
<div class="bookings-sub-text">${escapeHtml([satellite.code, satellite.typeCode, satellite.noradId ? `NORAD ${satellite.noradId}` : null].filter(Boolean).join(' / ') || '—')}</div>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
tbody.querySelectorAll('tr[data-satellite-id]').forEach((row) => {
|
||||||
|
row.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const id = Number(row.dataset.satelliteId);
|
||||||
|
if (state.selectedSatelliteIds.has(id)) {
|
||||||
|
state.selectedSatelliteIds.delete(id);
|
||||||
|
} else {
|
||||||
|
state.selectedSatelliteIds.add(id);
|
||||||
|
}
|
||||||
|
renderSatellites();
|
||||||
|
loadBookings();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBookings() {
|
||||||
|
const tbody = el('bookings-body');
|
||||||
|
const empty = el('bookings-empty');
|
||||||
|
const wrap = el('bookings-table-wrap');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
if (!state.bookings.length) {
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
empty?.classList.remove('d-none');
|
||||||
|
wrap?.classList.add('d-none');
|
||||||
|
updateSummary();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
empty?.classList.add('d-none');
|
||||||
|
wrap?.classList.remove('d-none');
|
||||||
|
tbody.innerHTML = state.bookings.map((booking) => `
|
||||||
|
<tr>
|
||||||
|
<td class="bookings-id">${escapeHtml(booking.bookedSlotId ?? '—')}</td>
|
||||||
|
<td>
|
||||||
|
<div class="bookings-main-text">${escapeHtml(booking.satelliteName || booking.satelliteCode || `КА ${booking.satelliteId}`)}</div>
|
||||||
|
<div class="bookings-sub-text">ID ${escapeHtml(booking.satelliteId)}${booking.satelliteTypeCode ? ` / ${escapeHtml(booking.satelliteTypeCode)}` : ''}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>Слот ${escapeHtml(booking.slotNum ?? '—')}</div>
|
||||||
|
<div class="bookings-sub-text">Цикл ${escapeHtml(booking.cycle ?? '—')}</div>
|
||||||
|
</td>
|
||||||
|
<td class="bookings-time-cell">
|
||||||
|
<div>${formatDateTime(booking.timeStart)}</div>
|
||||||
|
<div class="bookings-sub-text">${formatDateTime(booking.timeStop)}${booking.durationSeconds != null ? ` / ${formatNumber(booking.durationSeconds)} c` : ''}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>${escapeHtml(booking.revolution ?? '—')}</div>
|
||||||
|
<div class="bookings-sub-text">${escapeHtml(booking.revolutionSign || '—')}</div>
|
||||||
|
</td>
|
||||||
|
<td>${formatNumber(booking.roll)}</td>
|
||||||
|
<td><span class="bookings-badge ${statusClass(booking.status)}">${escapeHtml(booking.status || '—')}</span></td>
|
||||||
|
<td>${formatRequestIds(booking.requestIds)}</td>
|
||||||
|
</tr>`).join('');
|
||||||
|
updateSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSummary() {
|
||||||
|
const summary = el('bookings-summary');
|
||||||
|
if (!summary) return;
|
||||||
|
const requestId = el('bookings-request')?.value;
|
||||||
|
const satellites = state.selectedSatelliteIds.size ? `, спутников: ${state.selectedSatelliteIds.size}` : '';
|
||||||
|
const request = requestId ? `, заявка: ${requestId}` : '';
|
||||||
|
summary.textContent = `Найдено бронирований: ${state.bookings.length}${satellites}${request}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTableLoading() {
|
||||||
|
el('bookings-empty')?.classList.add('d-none');
|
||||||
|
el('bookings-table-wrap')?.classList.remove('d-none');
|
||||||
|
const tbody = el('bookings-body');
|
||||||
|
if (tbody) tbody.innerHTML = emptyRow(8, 'Загрузка бронирований...');
|
||||||
|
const summary = el('bookings-summary');
|
||||||
|
if (summary) summary.textContent = 'Загрузка данных...';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchJson(url, options = {}) {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: { 'Accept': 'application/json', ...(options.headers || {}) },
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
throw new Error(extractError(text) || `HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
if (response.status === 204) return null;
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function satelliteMatches(satellite, query) {
|
||||||
|
if (!query) return true;
|
||||||
|
return [satellite.id, satellite.noradId, satellite.code, satellite.name, satellite.typeCode]
|
||||||
|
.filter((value) => value !== undefined && value !== null)
|
||||||
|
.some((value) => String(value).toLowerCase().includes(query));
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyRow(colspan, text) {
|
||||||
|
return `<tr><td colspan="${colspan}" class="catalog-empty">${escapeHtml(text)}</td></tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRequestIds(ids) {
|
||||||
|
if (!Array.isArray(ids) || !ids.length) return '—';
|
||||||
|
return ids.map((id) => `<span class="bookings-badge">${escapeHtml(id)}</span>`).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusClass(status) {
|
||||||
|
if (!status) return '';
|
||||||
|
return `bookings-status-${String(status).toLowerCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(value) {
|
||||||
|
if (!value) return '—';
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return escapeHtml(value);
|
||||||
|
return date.toLocaleString('ru-RU', {
|
||||||
|
year: '2-digit', month: '2-digit', day: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(value) {
|
||||||
|
if (value === null || value === undefined || value === '') return '—';
|
||||||
|
const number = Number(value);
|
||||||
|
if (Number.isNaN(number)) return escapeHtml(value);
|
||||||
|
return number.toLocaleString('ru-RU', { maximumFractionDigits: 3 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDateTimeLocal(date) {
|
||||||
|
const pad = (value) => String(value).padStart(2, '0');
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toIsoLocal(value) {
|
||||||
|
return value && value.length === 16 ? `${value}:00` : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAlert(message, type = 'info') {
|
||||||
|
const box = el('bookings-alert');
|
||||||
|
if (!box) return;
|
||||||
|
box.innerHTML = `<div class="alert alert-${type} alert-dismissible fade show" role="alert">
|
||||||
|
${escapeHtml(message)}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Закрыть"></button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractError(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
return json.message || json.error || text;
|
||||||
|
} catch (_) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value ?? '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
.bookings-page {
|
||||||
|
height: calc(100vh - 4.25rem);
|
||||||
|
max-height: calc(100vh - 4.25rem);
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-page > .d-flex:first-child,
|
||||||
|
#bookings-alert,
|
||||||
|
.bookings-filter-card {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-layout {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-layout > [class*="col-"] {
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-layout > [class*="col-"] > .catalog-card {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-satellites-card,
|
||||||
|
.bookings-table-card {
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: calc(100vh - 17rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-satellites-card .card-header,
|
||||||
|
.bookings-table-card .card-header {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-satellites-wrap,
|
||||||
|
.bookings-table-card .card-body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-satellites-wrap {
|
||||||
|
height: calc(100vh - 23rem);
|
||||||
|
max-height: calc(100vh - 23rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-table-card .card-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-table-wrap {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
height: calc(100vh - 21rem);
|
||||||
|
max-height: calc(100vh - 21rem);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-check-col {
|
||||||
|
width: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-satellites-table tbody tr {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-selected-row > td {
|
||||||
|
background: rgba(13, 110, 253, 0.12) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-id {
|
||||||
|
font-family: var(--bs-font-monospace);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-main-text {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-sub-text {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid rgba(108, 117, 125, 0.35);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.1rem 0.45rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #495057;
|
||||||
|
background: rgba(248, 249, 250, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-time-cell {
|
||||||
|
min-width: 15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-status-booked {
|
||||||
|
border-color: rgba(13, 110, 253, 0.35);
|
||||||
|
color: #084298;
|
||||||
|
background: rgba(13, 110, 253, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-status-processed,
|
||||||
|
.bookings-status-finished {
|
||||||
|
border-color: rgba(25, 135, 84, 0.4);
|
||||||
|
color: #0f5132;
|
||||||
|
background: rgba(25, 135, 84, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-status-failed,
|
||||||
|
.bookings-status-canceled,
|
||||||
|
.bookings-status-cancelled {
|
||||||
|
border-color: rgba(220, 53, 69, 0.4);
|
||||||
|
color: #842029;
|
||||||
|
background: rgba(220, 53, 69, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1199.98px) {
|
||||||
|
.bookings-page {
|
||||||
|
height: auto;
|
||||||
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
padding-bottom: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-layout,
|
||||||
|
.bookings-layout > [class*="col-"],
|
||||||
|
.bookings-layout > [class*="col-"] > .catalog-card,
|
||||||
|
.bookings-satellites-card,
|
||||||
|
.bookings-table-card,
|
||||||
|
.bookings-satellites-wrap,
|
||||||
|
.bookings-table-wrap,
|
||||||
|
.bookings-table-card .card-body {
|
||||||
|
overflow: visible;
|
||||||
|
height: auto;
|
||||||
|
max-height: none;
|
||||||
|
min-height: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookings-layout > [class*="col-"] {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,16 @@
|
|||||||
min-height: 1.5rem;
|
min-height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.satellite-pdcm-send-time {
|
||||||
|
width: 15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.satellite-pdcm-ic-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.satellite-pdcm-status {
|
.satellite-pdcm-status {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
width: 0.75rem;
|
width: 0.75rem;
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
.tle-analysis-page {
|
||||||
|
height: calc(100vh - 4.75rem);
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-layout {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-layout > [class*="col-"] {
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-sidebar,
|
||||||
|
.tle-analysis-results-card {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-filter-body {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-filter-body .form-control {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-satellites-wrap,
|
||||||
|
.tle-analysis-results-wrap {
|
||||||
|
overflow: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-satellites-wrap {
|
||||||
|
flex: 1 1 0;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-results-wrap {
|
||||||
|
flex: 1 1 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tle-analysis-table-wrap {
|
||||||
|
flex: 1 1 0;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-results-table {
|
||||||
|
min-width: 980px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-satellites-table tbody tr,
|
||||||
|
.tle-analysis-results-table tbody tr {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-satellites-table tbody tr.active {
|
||||||
|
--bs-table-bg: rgba(13, 110, 253, 0.12);
|
||||||
|
--bs-table-hover-bg: rgba(13, 110, 253, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-vector {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-discrepancy {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1199.98px) {
|
||||||
|
.tle-analysis-page {
|
||||||
|
height: auto;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-satellites-wrap,
|
||||||
|
.tle-analysis-results-wrap {
|
||||||
|
max-height: 60vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
.tle-comparison-page {
|
||||||
|
min-height: calc(100vh - 5.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-input-card,
|
||||||
|
.tle-result-card {
|
||||||
|
border: 0;
|
||||||
|
box-shadow: 0 0.5rem 1.25rem rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-input-widget {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-textarea {
|
||||||
|
min-height: 11rem;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
resize: vertical;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-reference {
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.3);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background: rgba(248, 250, 252, 0.82);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-reference-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.55rem 0.75rem;
|
||||||
|
border-bottom: 1px solid rgba(148, 163, 184, 0.25);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-reference-row:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-reference-row span {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-reference-row strong {
|
||||||
|
text-align: right;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-parse-status {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-parse-status.tle-parse-ok {
|
||||||
|
color: #198754 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-parse-status.tle-parse-error {
|
||||||
|
color: #dc3545 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-result-table-wrap {
|
||||||
|
max-height: calc(100vh - 30rem);
|
||||||
|
min-height: 18rem;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-result-table-wrap thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-delta-positive {
|
||||||
|
color: #0d6efd;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-delta-negative {
|
||||||
|
color: #dc3545;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1199.98px) {
|
||||||
|
.tle-result-table-wrap {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
filteredSatellites: [],
|
filteredSatellites: [],
|
||||||
missions: [],
|
missions: [],
|
||||||
currentModes: [],
|
currentModes: [],
|
||||||
|
bookedSlotsById: {},
|
||||||
selectedSatelliteId: null,
|
selectedSatelliteId: null,
|
||||||
selectedMissionId: null,
|
selectedMissionId: null,
|
||||||
selectedMission: null,
|
selectedMission: null,
|
||||||
@@ -168,6 +169,10 @@
|
|||||||
class="btn btn-outline-success btn-sm current-plans-action-btn"
|
class="btn btn-outline-success btn-sm current-plans-action-btn"
|
||||||
data-action="calculate-drops"
|
data-action="calculate-drops"
|
||||||
data-mission-id="${escapeHtml(mission.missionId)}">Расчёт сбросов</button>
|
data-mission-id="${escapeHtml(mission.missionId)}">Расчёт сбросов</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-success btn-sm current-plans-action-btn"
|
||||||
|
data-action="confirm-mission"
|
||||||
|
data-mission-id="${escapeHtml(mission.missionId)}">Утвердить</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
@@ -184,6 +189,8 @@
|
|||||||
calculateSurveys(missionId, button);
|
calculateSurveys(missionId, button);
|
||||||
} else if (button.dataset.action === 'calculate-drops') {
|
} else if (button.dataset.action === 'calculate-drops') {
|
||||||
calculateDrops(missionId, button);
|
calculateDrops(missionId, button);
|
||||||
|
} else if (button.dataset.action === 'confirm-mission') {
|
||||||
|
confirmMission(missionId, button);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -208,6 +215,7 @@
|
|||||||
try {
|
try {
|
||||||
const modes = await fetchJson(`/api/current-plans/missions/${encodeURIComponent(missionId)}/modes`);
|
const modes = await fetchJson(`/api/current-plans/missions/${encodeURIComponent(missionId)}/modes`);
|
||||||
state.currentModes = Array.isArray(modes) ? modes : [];
|
state.currentModes = Array.isArray(modes) ? modes : [];
|
||||||
|
state.bookedSlotsById = await loadBookedSlotsForModes(state.currentModes);
|
||||||
renderModes(state.currentModes);
|
renderModes(state.currentModes);
|
||||||
setExportEnabled(true);
|
setExportEnabled(true);
|
||||||
return state.currentModes;
|
return state.currentModes;
|
||||||
@@ -218,6 +226,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadBookedSlotsForModes(modes) {
|
||||||
|
const bookedSlotIds = [...new Set((modes || []).flatMap((mode) => mode.bookedSlotIds || []))]
|
||||||
|
.filter((slotId) => Number(slotId) > 0);
|
||||||
|
if (!bookedSlotIds.length) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
bookedSlotIds.forEach((slotId) => params.append('ids', slotId));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const bookedSlots = await fetchJson(`/api/current-plans/booked-slots?${params.toString()}`);
|
||||||
|
return (bookedSlots || []).reduce((acc, slot) => {
|
||||||
|
acc[slot.bookedSlotId] = slot;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
} catch (error) {
|
||||||
|
showAlert(error.message || 'Не удалось загрузить детали забронированных слотов.', 'warning');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderModes(modes) {
|
function renderModes(modes) {
|
||||||
const tbody = el('current-plans-modes-body');
|
const tbody = el('current-plans-modes-body');
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
@@ -227,35 +257,158 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = modes.map((mode) => {
|
tbody.innerHTML = modes.flatMap((mode) => {
|
||||||
const type = mode.type || '—';
|
const type = mode.type || '—';
|
||||||
const duration = formatNumber(mode.duration);
|
const duration = formatNumber(mode.duration);
|
||||||
const rowClass = modeRowClass(mode);
|
const rowClass = modeRowClass(mode);
|
||||||
const badgeClass = modeBadgeClass(mode);
|
const badgeClass = modeBadgeClass(mode);
|
||||||
return `
|
const expandable = isExpandableMode(mode);
|
||||||
<tr class="${rowClass}">
|
const expandMarker = expandable ? '<span class="current-plans-expand-marker">▸</span>' : '';
|
||||||
<td><span class="current-plans-badge ${badgeClass}">${escapeHtml(type)}</span></td>
|
const row = `
|
||||||
|
<tr class="${rowClass}${expandable ? ' current-plans-expandable-row' : ''}" data-mode-id="${escapeHtml(mode.id ?? '')}">
|
||||||
|
<td>${expandMarker}<span class="current-plans-badge ${badgeClass}">${escapeHtml(type)}</span></td>
|
||||||
<td>${formatDateTime(mode.timeStart)}</td>
|
<td>${formatDateTime(mode.timeStart)}</td>
|
||||||
<td>${escapeHtml(mode.revolution ?? '—')}</td>
|
<td>${escapeHtml(mode.revolution ?? '—')}</td>
|
||||||
<td>${duration}</td>
|
<td>${duration}</td>
|
||||||
<td class="current-plans-mode-params">${modeParams(mode)}</td>
|
<td class="current-plans-mode-params">${modeParams(mode)}</td>
|
||||||
<td>${modeStatus(mode)}</td>
|
<td>${modeStatus(mode)}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
|
const details = expandable ? modeDetailsRow(mode) : null;
|
||||||
|
return details ? [row, details] : [row];
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
|
tbody.querySelectorAll('.current-plans-expandable-row').forEach((row) => {
|
||||||
|
row.addEventListener('click', () => toggleModeDetails(row.dataset.modeId));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function modeRowClass(mode) {
|
function modeRowClass(mode) {
|
||||||
if (mode.type === 'DROP') return 'current-plans-mode-row-drop';
|
if (mode.type === 'DROP') return 'current-plans-mode-row-drop';
|
||||||
if (mode.source === 'SLOTS') return 'current-plans-mode-row-slots';
|
if (isSlotsLikeSource(mode.source)) return 'current-plans-mode-row-slots';
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function modeBadgeClass(mode) {
|
function modeBadgeClass(mode) {
|
||||||
if (mode.type === 'DROP') return 'current-plans-badge-drop';
|
if (mode.type === 'DROP') return 'current-plans-badge-drop';
|
||||||
if (mode.source === 'SLOTS') return 'current-plans-badge-slots';
|
if (isSlotsLikeSource(mode.source)) return 'current-plans-badge-slots';
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSlotsLikeSource(source) {
|
||||||
|
return source === 'SLOTS' || source === 'MIXED';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExpandableMode(mode) {
|
||||||
|
return (Array.isArray(mode.bookedSlotIds) && mode.bookedSlotIds.length > 0)
|
||||||
|
|| (mode.type === 'DROP' && Array.isArray(mode.surveys) && mode.surveys.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleModeDetails(modeId) {
|
||||||
|
const row = document.querySelector(`#current-plans-modes-body tr[data-mode-id="${cssEscape(modeId)}"]`);
|
||||||
|
const details = document.querySelector(`#current-plans-modes-body tr[data-parent-mode-id="${cssEscape(modeId)}"]`);
|
||||||
|
if (!row || !details) return;
|
||||||
|
|
||||||
|
const collapsed = details.classList.toggle('d-none');
|
||||||
|
row.classList.toggle('current-plans-expanded-row', !collapsed);
|
||||||
|
const marker = row.querySelector('.current-plans-expand-marker');
|
||||||
|
if (marker) marker.textContent = collapsed ? '▸' : '▾';
|
||||||
|
}
|
||||||
|
|
||||||
|
function modeDetailsRow(mode) {
|
||||||
|
const content = mode.type === 'DROP'
|
||||||
|
? dropDetails(mode)
|
||||||
|
: bookedSlotDetails(mode);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr class="current-plans-mode-details-row d-none" data-parent-mode-id="${escapeHtml(mode.id ?? '')}">
|
||||||
|
<td colspan="6">
|
||||||
|
<div class="current-plans-mode-details">${content}</div>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bookedSlotDetails(mode) {
|
||||||
|
const slotIds = Array.isArray(mode.bookedSlotIds) ? mode.bookedSlotIds : [];
|
||||||
|
const rows = slotIds.map((slotId) => {
|
||||||
|
const slot = state.bookedSlotsById[slotId];
|
||||||
|
if (!slot) {
|
||||||
|
return `<tr><td class="current-plans-id">#${escapeHtml(slotId)}</td><td colspan="6">Детали слота не найдены</td></tr>`;
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td class="current-plans-id">#${escapeHtml(slot.bookedSlotId)}</td>
|
||||||
|
<td>${formatDateTime(slot.timeStart)}</td>
|
||||||
|
<td>${formatDuration(slot.durationSeconds)}</td>
|
||||||
|
<td>${escapeHtml(slot.slotNum ?? '—')}</td>
|
||||||
|
<td>${escapeHtml(slot.cycle ?? '—')}</td>
|
||||||
|
<td>${escapeHtml(slot.satelliteId ?? '—')}</td>
|
||||||
|
<td>${escapeHtml(slot.status || '—')}</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="current-plans-detail-title">Забронированные слоты (${slotIds.length})</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm mb-0 current-plans-detail-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Начало</th>
|
||||||
|
<th>Длительность</th>
|
||||||
|
<th>Слот</th>
|
||||||
|
<th>Цикл</th>
|
||||||
|
<th>Спутник</th>
|
||||||
|
<th>Статус</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>${rows}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dropDetails(mode) {
|
||||||
|
const surveyIds = Array.isArray(mode.surveys) ? mode.surveys : [];
|
||||||
|
const surveysById = new Map((state.currentModes || []).map((item) => [Number(item.id), item]));
|
||||||
|
const rows = surveyIds.map((surveyId) => {
|
||||||
|
const survey = surveysById.get(Number(surveyId));
|
||||||
|
if (!survey) {
|
||||||
|
return `<tr><td class="current-plans-id">#${escapeHtml(surveyId)}</td><td colspan="4">Съёмка не найдена в текущем списке режимов</td></tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const booked = Array.isArray(survey.bookedSlotIds) && survey.bookedSlotIds.length
|
||||||
|
? survey.bookedSlotIds.map((slotId) => {
|
||||||
|
const slot = state.bookedSlotsById[slotId];
|
||||||
|
return slot ? `#${slot.bookedSlotId} slot ${slot.slotNum}/cycle ${slot.cycle}` : `#${slotId}`;
|
||||||
|
}).join(', ')
|
||||||
|
: '—';
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td class="current-plans-id">#${escapeHtml(survey.id)}</td>
|
||||||
|
<td>${formatDateTime(survey.timeStart)}</td>
|
||||||
|
<td>${escapeHtml(survey.revolution ?? '—')}</td>
|
||||||
|
<td>${escapeHtml(survey.source || '—')}</td>
|
||||||
|
<td>${escapeHtml(booked)}</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="current-plans-detail-title">Сбрасываемые съёмки (${surveyIds.length})</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm mb-0 current-plans-detail-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Начало</th>
|
||||||
|
<th>Виток</th>
|
||||||
|
<th>Источник</th>
|
||||||
|
<th>Booked slots</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>${rows}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
function modeParams(mode) {
|
function modeParams(mode) {
|
||||||
if (mode.type === 'DROP') {
|
if (mode.type === 'DROP') {
|
||||||
const surveys = Array.isArray(mode.surveys) ? mode.surveys.length : 0;
|
const surveys = Array.isArray(mode.surveys) ? mode.surveys.length : 0;
|
||||||
@@ -276,7 +429,7 @@
|
|||||||
if (mode.type === 'DROP') {
|
if (mode.type === 'DROP') {
|
||||||
return '<span class="current-plans-badge current-plans-badge-drop">DROP</span>';
|
return '<span class="current-plans-badge current-plans-badge-drop">DROP</span>';
|
||||||
}
|
}
|
||||||
const sourceBadgeClass = mode.source === 'SLOTS' ? ' current-plans-badge-slots' : '';
|
const sourceBadgeClass = isSlotsLikeSource(mode.source) ? ' current-plans-badge-slots' : '';
|
||||||
return `
|
return `
|
||||||
<div><span class="current-plans-badge">${escapeHtml(mode.status || '—')}</span></div>
|
<div><span class="current-plans-badge">${escapeHtml(mode.status || '—')}</span></div>
|
||||||
<div><span class="current-plans-badge${sourceBadgeClass}">${escapeHtml(mode.source || '—')}</span></div>`;
|
<div><span class="current-plans-badge${sourceBadgeClass}">${escapeHtml(mode.source || '—')}</span></div>`;
|
||||||
@@ -379,6 +532,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function confirmMission(missionId, button) {
|
||||||
|
setButtonLoading(button, true, 'Утверждение...');
|
||||||
|
try {
|
||||||
|
await fetchJson(`/api/current-plans/missions/${encodeURIComponent(missionId)}/confirm`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Accept': 'application/json'}
|
||||||
|
});
|
||||||
|
showAlert('План утверждён.', 'success');
|
||||||
|
if (state.selectedSatelliteId) {
|
||||||
|
await loadMissions(state.selectedSatelliteId, state.missionsPage, missionId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showAlert(error.message || 'Не удалось утвердить план.', 'danger');
|
||||||
|
} finally {
|
||||||
|
setButtonLoading(button, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function exportSelectedMissionCsv() {
|
function exportSelectedMissionCsv() {
|
||||||
if (!state.selectedMissionId) {
|
if (!state.selectedMissionId) {
|
||||||
showAlert('Выберите миссию для сохранения CSV.', 'warning');
|
showAlert('Выберите миссию для сохранения CSV.', 'warning');
|
||||||
@@ -410,7 +581,7 @@
|
|||||||
mode.type || '',
|
mode.type || '',
|
||||||
mode.id ?? '',
|
mode.id ?? '',
|
||||||
mode.planId ?? '',
|
mode.planId ?? '',
|
||||||
mode.timeStart || '',
|
formatDateTime(mode.timeStart),
|
||||||
mode.revolution ?? '',
|
mode.revolution ?? '',
|
||||||
mode.duration ?? '',
|
mode.duration ?? '',
|
||||||
mode.status || '',
|
mode.status || '',
|
||||||
@@ -469,6 +640,7 @@
|
|||||||
el('current-plans-modes-body').innerHTML = '';
|
el('current-plans-modes-body').innerHTML = '';
|
||||||
el('current-plans-selected-mission').textContent = 'Выберите миссию в верхней таблице.';
|
el('current-plans-selected-mission').textContent = 'Выберите миссию в верхней таблице.';
|
||||||
state.currentModes = [];
|
state.currentModes = [];
|
||||||
|
state.bookedSlotsById = {};
|
||||||
setExportEnabled(false);
|
setExportEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -570,6 +742,12 @@
|
|||||||
|
|
||||||
function formatDateTime(value) {
|
function formatDateTime(value) {
|
||||||
if (!value) return '—';
|
if (!value) return '—';
|
||||||
|
const match = String(value).match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?/);
|
||||||
|
if (match) {
|
||||||
|
const [, year, month, day, hour, minute, second] = match;
|
||||||
|
return `${day}.${month}.${year.slice(2)}, ${hour}:${minute}:${second || '00'}`;
|
||||||
|
}
|
||||||
|
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
if (Number.isNaN(date.getTime())) return escapeHtml(value);
|
if (Number.isNaN(date.getTime())) return escapeHtml(value);
|
||||||
return date.toLocaleString('ru-RU', {
|
return date.toLocaleString('ru-RU', {
|
||||||
@@ -588,6 +766,21 @@
|
|||||||
return number.toLocaleString('ru-RU', {maximumFractionDigits: 2});
|
return number.toLocaleString('ru-RU', {maximumFractionDigits: 2});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDuration(seconds) {
|
||||||
|
const totalSeconds = Number(seconds);
|
||||||
|
if (!Number.isFinite(totalSeconds)) return '—';
|
||||||
|
|
||||||
|
const rounded = Math.max(0, Math.round(totalSeconds));
|
||||||
|
const hours = Math.floor(rounded / 3600);
|
||||||
|
const minutes = Math.floor((rounded % 3600) / 60);
|
||||||
|
const remainingSeconds = rounded % 60;
|
||||||
|
const pad = (number) => String(number).padStart(2, '0');
|
||||||
|
|
||||||
|
return hours > 0
|
||||||
|
? `${hours}:${pad(minutes)}:${pad(remainingSeconds)}`
|
||||||
|
: `${minutes}:${pad(remainingSeconds)}`;
|
||||||
|
}
|
||||||
|
|
||||||
function toDateTimeLocalValue(date) {
|
function toDateTimeLocalValue(date) {
|
||||||
const pad = (number) => String(number).padStart(2, '0');
|
const pad = (number) => String(number).padStart(2, '0');
|
||||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||||
@@ -652,4 +845,11 @@
|
|||||||
.replaceAll('"', '"')
|
.replaceAll('"', '"')
|
||||||
.replaceAll("'", ''');
|
.replaceAll("'", ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cssEscape(value) {
|
||||||
|
if (window.CSS && typeof window.CSS.escape === 'function') {
|
||||||
|
return window.CSS.escape(String(value ?? ''));
|
||||||
|
}
|
||||||
|
return String(value ?? '').replaceAll('"', '\\"');
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -79,7 +79,8 @@
|
|||||||
checkModels();
|
checkModels();
|
||||||
} else{
|
} else{
|
||||||
var sceneMode = (viewer.scene.mode == Cesium.SceneMode.SCENE2D);
|
var sceneMode = (viewer.scene.mode == Cesium.SceneMode.SCENE2D);
|
||||||
var u = '/requests/station/'+req.id+'?view='+sceneMode
|
var u = '/requests/station/'+req.id+'?view='+sceneMode+
|
||||||
|
'&orbitHeightKm='+encodeURIComponent(selectedStationOrbitHeightKm())
|
||||||
fetch(u, {
|
fetch(u, {
|
||||||
method: "post",
|
method: "post",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -115,6 +116,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectedStationOrbitHeightKm() {
|
||||||
|
const input = document.getElementById('stationOrbitHeightKm');
|
||||||
|
const value = Number(input?.value || 500);
|
||||||
|
if (!Number.isFinite(value) || value <= 0) {
|
||||||
|
if (input) input.value = '500';
|
||||||
|
return 500;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function drawSat(show, req, startInput, endInput, runId){
|
function drawSat(show, req, startInput, endInput, runId){
|
||||||
if (req){
|
if (req){
|
||||||
|
|||||||
@@ -30,6 +30,17 @@
|
|||||||
return date.toLocaleString('ru-RU');
|
return date.toLocaleString('ru-RU');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatNumber(value, digits = 3) {
|
||||||
|
const number = Number(value);
|
||||||
|
if (!Number.isFinite(number)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return number.toLocaleString('ru-RU', {
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: digits
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function selectedMode() {
|
function selectedMode() {
|
||||||
return document.querySelector('input[name="rva-mode"]:checked')?.value || 'common';
|
return document.querySelector('input[name="rva-mode"]:checked')?.value || 'common';
|
||||||
}
|
}
|
||||||
@@ -113,7 +124,7 @@
|
|||||||
<td>${row.durationSec}</td>
|
<td>${row.durationSec}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('');
|
`).join('');
|
||||||
} else {
|
} else if (resultState.mode === 'merge-on-rev') {
|
||||||
head.innerHTML = `
|
head.innerHTML = `
|
||||||
<tr>
|
<tr>
|
||||||
<th>Виток</th>
|
<th>Виток</th>
|
||||||
@@ -123,7 +134,24 @@
|
|||||||
body.innerHTML = resultState.rows.map(row => `
|
body.innerHTML = resultState.rows.map(row => `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${row.revolution}</td>
|
<td>${row.revolution}</td>
|
||||||
<td>${row.durationSec}</td>
|
<td>${formatNumber(row.durationSec, 1)}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
} else if (resultState.mode === 'average') {
|
||||||
|
head.innerHTML = `
|
||||||
|
<tr>
|
||||||
|
<th>№ППИ</th>
|
||||||
|
<th>Широта ППИ</th>
|
||||||
|
<th>Сред.кол-во</th>
|
||||||
|
<th>Сред.длит., с</th>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
body.innerHTML = resultState.rows.map(row => `
|
||||||
|
<tr>
|
||||||
|
<td>${row.ppiNumber}</td>
|
||||||
|
<td>${formatNumber(row.ppiLatitude, 6)}</td>
|
||||||
|
<td>${formatNumber(row.averageCount, 3)}</td>
|
||||||
|
<td>${formatNumber(row.averageDurationSec, 1)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
@@ -145,8 +173,9 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = resultState.mode === 'common'
|
let lines;
|
||||||
? [
|
if (resultState.mode === 'common') {
|
||||||
|
lines = [
|
||||||
['stationId', 'revolution', 'onStart', 'onMaximum', 'onStop', 'duration'],
|
['stationId', 'revolution', 'onStart', 'onMaximum', 'onStop', 'duration'],
|
||||||
...resultState.rows.map(row => [
|
...resultState.rows.map(row => [
|
||||||
row.stationId,
|
row.stationId,
|
||||||
@@ -156,23 +185,35 @@
|
|||||||
row.onStop.time,
|
row.onStop.time,
|
||||||
row.duration
|
row.duration
|
||||||
])
|
])
|
||||||
]
|
];
|
||||||
: resultState.mode === 'merge'
|
} else if (resultState.mode === 'merge') {
|
||||||
? [
|
lines = [
|
||||||
['begin', 'end', 'durationSec'],
|
['begin', 'end', 'durationSec'],
|
||||||
...resultState.rows.map(row => [
|
...resultState.rows.map(row => [
|
||||||
row.begin,
|
row.begin,
|
||||||
row.end,
|
row.end,
|
||||||
row.durationSec
|
row.durationSec
|
||||||
])
|
])
|
||||||
]
|
];
|
||||||
: [
|
} else if (resultState.mode === 'merge-on-rev') {
|
||||||
|
lines = [
|
||||||
['revolution', 'durationSec'],
|
['revolution', 'durationSec'],
|
||||||
...resultState.rows.map(row => [
|
...resultState.rows.map(row => [
|
||||||
row.revolution,
|
row.revolution,
|
||||||
row.durationSec
|
row.durationSec
|
||||||
])
|
])
|
||||||
];
|
];
|
||||||
|
} else {
|
||||||
|
lines = [
|
||||||
|
['ppiNumber', 'ppiLatitude', 'averageCount', 'averageDurationSec'],
|
||||||
|
...resultState.rows.map(row => [
|
||||||
|
row.ppiNumber,
|
||||||
|
row.ppiLatitude,
|
||||||
|
row.averageCount,
|
||||||
|
row.averageDurationSec
|
||||||
|
])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const csv = lines.map(line => line.map(csvEscape).join(';')).join('\n');
|
const csv = lines.map(line => line.map(csvEscape).join(';')).join('\n');
|
||||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
selectedSatelliteId: null,
|
selectedSatelliteId: null,
|
||||||
details: null
|
details: null
|
||||||
};
|
};
|
||||||
|
const sendInitialConditionsDateTimePattern = /^(\d{2})\.(\d{2})\.(\d{4}) (\d{2}):(\d{2}):(\d{2})\.(\d{3})$/;
|
||||||
|
|
||||||
function showAlert(message, type = 'danger') {
|
function showAlert(message, type = 'danger') {
|
||||||
const container = document.getElementById('satellite-pdcm-alert');
|
const container = document.getElementById('satellite-pdcm-alert');
|
||||||
@@ -58,6 +59,121 @@
|
|||||||
return value ? value.replace('T', ' ') : '-';
|
return value ? value.replace('T', ' ') : '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pad(value, length = 2) {
|
||||||
|
return String(value).padStart(length, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentDateTimeInputValue() {
|
||||||
|
const now = new Date();
|
||||||
|
return `${pad(now.getDate())}.${pad(now.getMonth() + 1)}.${now.getFullYear()} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTimeInput(value) {
|
||||||
|
if (!value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const match = String(value).match(
|
||||||
|
/^(\d{4})-(\d{2})-(\d{2})[T\s](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?/
|
||||||
|
);
|
||||||
|
if (!match) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, year, month, day, hour, minute, second = '00', millis = '000'] = match;
|
||||||
|
return `${day}.${month}.${year} ${hour}:${minute}:${second}.${millis.slice(0, 3).padEnd(3, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSendInitialConditionsTime() {
|
||||||
|
const input = document.getElementById('satellite-pdcm-send-ic-time');
|
||||||
|
const value = input?.value.trim() || '';
|
||||||
|
const match = value.match(sendInitialConditionsDateTimePattern);
|
||||||
|
if (!match) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Введите время НУ в формате dd.mm.yyyy hh:MM:ss.zzz'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, day, month, year, hour, minute, second, millisecond] = match;
|
||||||
|
const parsed = new Date(
|
||||||
|
Number(year),
|
||||||
|
Number(month) - 1,
|
||||||
|
Number(day),
|
||||||
|
Number(hour),
|
||||||
|
Number(minute),
|
||||||
|
Number(second),
|
||||||
|
Number(millisecond)
|
||||||
|
);
|
||||||
|
const validDate = parsed.getFullYear() === Number(year)
|
||||||
|
&& parsed.getMonth() === Number(month) - 1
|
||||||
|
&& parsed.getDate() === Number(day)
|
||||||
|
&& parsed.getHours() === Number(hour)
|
||||||
|
&& parsed.getMinutes() === Number(minute)
|
||||||
|
&& parsed.getSeconds() === Number(second)
|
||||||
|
&& parsed.getMilliseconds() === Number(millisecond);
|
||||||
|
|
||||||
|
if (!validDate) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Время НУ содержит некорректную дату или время'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
text: value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDateTimeForPayload(value, fieldName) {
|
||||||
|
const text = value?.trim() || '';
|
||||||
|
const match = text.match(sendInitialConditionsDateTimePattern);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`${fieldName}: укажите время в формате dd.mm.yyyy hh:MM:ss.zzz`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, day, month, year, hour, minute, second, millisecond] = match;
|
||||||
|
const parsed = new Date(
|
||||||
|
Number(year),
|
||||||
|
Number(month) - 1,
|
||||||
|
Number(day),
|
||||||
|
Number(hour),
|
||||||
|
Number(minute),
|
||||||
|
Number(second),
|
||||||
|
Number(millisecond)
|
||||||
|
);
|
||||||
|
const validDate = parsed.getFullYear() === Number(year)
|
||||||
|
&& parsed.getMonth() === Number(month) - 1
|
||||||
|
&& parsed.getDate() === Number(day)
|
||||||
|
&& parsed.getHours() === Number(hour)
|
||||||
|
&& parsed.getMinutes() === Number(minute)
|
||||||
|
&& parsed.getSeconds() === Number(second)
|
||||||
|
&& parsed.getMilliseconds() === Number(millisecond);
|
||||||
|
|
||||||
|
if (!validDate) {
|
||||||
|
throw new Error(`${fieldName}: некорректная дата или время`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}T${hour}:${minute}:${second}.${millisecond}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRequiredNumber(inputId, fieldName) {
|
||||||
|
const value = document.getElementById(inputId)?.value;
|
||||||
|
const number = Number(value);
|
||||||
|
if (value === '' || !Number.isFinite(number)) {
|
||||||
|
throw new Error(`${fieldName}: укажите число`);
|
||||||
|
}
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRequiredInteger(inputId, fieldName) {
|
||||||
|
const number = parseRequiredNumber(inputId, fieldName);
|
||||||
|
if (!Number.isInteger(number)) {
|
||||||
|
throw new Error(`${fieldName}: укажите целое число`);
|
||||||
|
}
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
function formatNumber(value) {
|
function formatNumber(value) {
|
||||||
return Number(value ?? 0).toFixed(3);
|
return Number(value ?? 0).toFixed(3);
|
||||||
}
|
}
|
||||||
@@ -71,6 +187,39 @@
|
|||||||
exportButton.disabled = !details || !details.ascNodes || !details.ascNodes.length;
|
exportButton.disabled = !details || !details.ascNodes || !details.ascNodes.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderSendInitialConditionsState(details, loading = false) {
|
||||||
|
const button = document.getElementById('satellite-pdcm-send-ic');
|
||||||
|
if (!button) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const time = parseSendInitialConditionsTime();
|
||||||
|
const enabled = !!details && time.valid && !loading;
|
||||||
|
button.disabled = !enabled;
|
||||||
|
button.textContent = loading ? 'Передача...' : 'Передать НУ в slot-service';
|
||||||
|
if (!details) {
|
||||||
|
button.title = 'Выберите спутник';
|
||||||
|
} else if (!time.valid) {
|
||||||
|
button.title = time.message;
|
||||||
|
} else {
|
||||||
|
button.title = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSetInitialConditionsState(details, loading = false) {
|
||||||
|
const openButton = document.getElementById('satellite-pdcm-open-set-ic');
|
||||||
|
const submitButton = document.getElementById('satellite-pdcm-set-ic-submit');
|
||||||
|
const satelliteSelected = !!state.selectedSatelliteId;
|
||||||
|
if (openButton) {
|
||||||
|
openButton.disabled = !satelliteSelected || loading;
|
||||||
|
openButton.title = satelliteSelected ? '' : 'Выберите спутник';
|
||||||
|
}
|
||||||
|
if (submitButton) {
|
||||||
|
submitButton.disabled = loading;
|
||||||
|
submitButton.textContent = loading ? 'Запуск...' : 'Запустить расчет';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderList() {
|
function renderList() {
|
||||||
const tableBody = document.getElementById('satellite-pdcm-list');
|
const tableBody = document.getElementById('satellite-pdcm-list');
|
||||||
tableBody.innerHTML = '';
|
tableBody.innerHTML = '';
|
||||||
@@ -110,6 +259,8 @@
|
|||||||
if (!details) {
|
if (!details) {
|
||||||
summary.innerHTML = '<div class="catalog-helper">Выберите спутник слева.</div>';
|
summary.innerHTML = '<div class="catalog-helper">Выберите спутник слева.</div>';
|
||||||
renderExportState(null);
|
renderExportState(null);
|
||||||
|
renderSendInitialConditionsState(null);
|
||||||
|
renderSetInitialConditionsState(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +291,8 @@
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
renderExportState(details);
|
renderExportState(details);
|
||||||
|
renderSendInitialConditionsState(details);
|
||||||
|
renderSetInitialConditionsState(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTable(details) {
|
function renderTable(details) {
|
||||||
@@ -270,9 +423,149 @@
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendInitialConditions() {
|
||||||
|
if (!state.selectedSatelliteId || !state.details) {
|
||||||
|
showAlert('Выберите спутник для передачи начальных условий');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const time = parseSendInitialConditionsTime();
|
||||||
|
if (!time.valid) {
|
||||||
|
showAlert(time.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderSendInitialConditionsState(state.details, true);
|
||||||
|
showAlert('');
|
||||||
|
try {
|
||||||
|
await requestJson(`${apiBase}/${state.selectedSatelliteId}/send-ic?time=${encodeURIComponent(time.text)}`, {method: 'POST'});
|
||||||
|
showAlert('Начальные условия спутника переданы в slot-service', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
showAlert(error.message || 'Не удалось передать начальные условия в slot-service');
|
||||||
|
} finally {
|
||||||
|
renderSendInitialConditionsState(state.details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetInitialConditionsForm() {
|
||||||
|
document.getElementById('satellite-pdcm-ic-time').value = currentDateTimeInputValue();
|
||||||
|
document.getElementById('satellite-pdcm-ic-revolution').value = '';
|
||||||
|
document.getElementById('satellite-pdcm-ic-movement-model').value = 'FOTO';
|
||||||
|
document.getElementById('satellite-pdcm-ic-s-ball').value = '0';
|
||||||
|
document.getElementById('satellite-pdcm-ic-f81').value = '147.8';
|
||||||
|
['x', 'y', 'z', 'vx', 'vy', 'vz'].forEach(field => {
|
||||||
|
document.getElementById(`satellite-pdcm-ic-${field}`).value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillInitialConditionsForm(initialConditions) {
|
||||||
|
document.getElementById('satellite-pdcm-ic-time').value =
|
||||||
|
formatDateTimeInput(initialConditions?.ic?.orbPoint?.time) || currentDateTimeInputValue();
|
||||||
|
document.getElementById('satellite-pdcm-ic-revolution').value =
|
||||||
|
initialConditions?.ic?.orbPoint?.revolution ?? '';
|
||||||
|
document.getElementById('satellite-pdcm-ic-movement-model').value =
|
||||||
|
initialConditions?.movementModel || 'FOTO';
|
||||||
|
document.getElementById('satellite-pdcm-ic-s-ball').value =
|
||||||
|
initialConditions?.ic?.sBall ?? '0';
|
||||||
|
document.getElementById('satellite-pdcm-ic-f81').value =
|
||||||
|
initialConditions?.ic?.f81 ?? '147.8';
|
||||||
|
document.getElementById('satellite-pdcm-ic-x').value = initialConditions?.ic?.orbPoint?.x ?? '';
|
||||||
|
document.getElementById('satellite-pdcm-ic-y').value = initialConditions?.ic?.orbPoint?.y ?? '';
|
||||||
|
document.getElementById('satellite-pdcm-ic-z').value = initialConditions?.ic?.orbPoint?.z ?? '';
|
||||||
|
document.getElementById('satellite-pdcm-ic-vx').value = initialConditions?.ic?.orbPoint?.vx ?? '';
|
||||||
|
document.getElementById('satellite-pdcm-ic-vy').value = initialConditions?.ic?.orbPoint?.vy ?? '';
|
||||||
|
document.getElementById('satellite-pdcm-ic-vz').value = initialConditions?.ic?.orbPoint?.vz ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openSetInitialConditionsModal() {
|
||||||
|
if (!state.selectedSatelliteId) {
|
||||||
|
showAlert('Выберите спутник для задания начальных условий');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedSatellite = state.details?.satelliteId === state.selectedSatelliteId
|
||||||
|
? state.details
|
||||||
|
: state.satellites.find(satellite => satellite.satelliteId === state.selectedSatelliteId);
|
||||||
|
document.getElementById('satellite-pdcm-set-ic-target').textContent =
|
||||||
|
selectedSatellite
|
||||||
|
? `${selectedSatellite.name} · ID ${selectedSatellite.satelliteId}`
|
||||||
|
: `ID ${state.selectedSatelliteId}`;
|
||||||
|
resetInitialConditionsForm();
|
||||||
|
|
||||||
|
if (window.bootstrap) {
|
||||||
|
window.bootstrap.Modal.getOrCreateInstance(document.getElementById('satellite-pdcm-set-ic-modal')).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentInitialConditions = await requestJson(`${apiBase}/${state.selectedSatelliteId}/initial-conditions`);
|
||||||
|
if (currentInitialConditions) {
|
||||||
|
fillInitialConditionsForm(currentInitialConditions);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showAlert(error.message || 'Не удалось загрузить актуальные начальные условия');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInitialConditionsPayload() {
|
||||||
|
const movementModel = document.getElementById('satellite-pdcm-ic-movement-model').value || 'FOTO';
|
||||||
|
return {
|
||||||
|
satelliteId: state.selectedSatelliteId,
|
||||||
|
movementModel,
|
||||||
|
ic: {
|
||||||
|
orbPoint: {
|
||||||
|
time: parseDateTimeForPayload(document.getElementById('satellite-pdcm-ic-time').value, 'Time'),
|
||||||
|
revolution: parseRequiredInteger('satellite-pdcm-ic-revolution', 'Revolution'),
|
||||||
|
vx: parseRequiredNumber('satellite-pdcm-ic-vx', 'Vx'),
|
||||||
|
vy: parseRequiredNumber('satellite-pdcm-ic-vy', 'Vy'),
|
||||||
|
vz: parseRequiredNumber('satellite-pdcm-ic-vz', 'Vz'),
|
||||||
|
x: parseRequiredNumber('satellite-pdcm-ic-x', 'X'),
|
||||||
|
y: parseRequiredNumber('satellite-pdcm-ic-y', 'Y'),
|
||||||
|
z: parseRequiredNumber('satellite-pdcm-ic-z', 'Z')
|
||||||
|
},
|
||||||
|
sBall: parseRequiredNumber('satellite-pdcm-ic-s-ball', 'S-ball'),
|
||||||
|
f81: parseRequiredNumber('satellite-pdcm-ic-f81', 'F81')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitInitialConditions(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!state.selectedSatelliteId) {
|
||||||
|
showAlert('Выберите спутник для задания начальных условий');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload;
|
||||||
|
try {
|
||||||
|
payload = buildInitialConditionsPayload();
|
||||||
|
} catch (error) {
|
||||||
|
showAlert(error.message || 'Проверьте параметры начальных условий');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSetInitialConditionsState(state.details, true);
|
||||||
|
showAlert('');
|
||||||
|
try {
|
||||||
|
await requestJson(`${apiBase}/${state.selectedSatelliteId}/initial-conditions`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
const modalElement = document.getElementById('satellite-pdcm-set-ic-modal');
|
||||||
|
if (window.bootstrap && modalElement) {
|
||||||
|
window.bootstrap.Modal.getOrCreateInstance(modalElement).hide();
|
||||||
|
}
|
||||||
|
showAlert('Расчет ПДЦМ по начальным условиям запущен', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
showAlert(error.message || 'Не удалось запустить расчет ПДЦМ по начальным условиям');
|
||||||
|
} finally {
|
||||||
|
renderSetInitialConditionsState(state.details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function selectSatellite(satelliteId) {
|
async function selectSatellite(satelliteId) {
|
||||||
state.selectedSatelliteId = satelliteId;
|
state.selectedSatelliteId = satelliteId;
|
||||||
|
state.details = null;
|
||||||
renderList();
|
renderList();
|
||||||
|
renderSendInitialConditionsState(null);
|
||||||
|
renderSetInitialConditionsState(null);
|
||||||
showAlert('');
|
showAlert('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -284,6 +577,7 @@
|
|||||||
state.details = null;
|
state.details = null;
|
||||||
renderSummary(null);
|
renderSummary(null);
|
||||||
renderTable(null);
|
renderTable(null);
|
||||||
|
renderSetInitialConditionsState(null);
|
||||||
showAlert(error.message || 'Не удалось загрузить ПДЦМ спутника');
|
showAlert(error.message || 'Не удалось загрузить ПДЦМ спутника');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -299,12 +593,14 @@
|
|||||||
if (nextSatelliteId) {
|
if (nextSatelliteId) {
|
||||||
await selectSatellite(nextSatelliteId);
|
await selectSatellite(nextSatelliteId);
|
||||||
} else {
|
} else {
|
||||||
|
state.selectedSatelliteId = null;
|
||||||
state.details = null;
|
state.details = null;
|
||||||
renderSummary(null);
|
renderSummary(null);
|
||||||
renderTable(null);
|
renderTable(null);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
state.satellites = [];
|
state.satellites = [];
|
||||||
|
state.selectedSatelliteId = null;
|
||||||
state.details = null;
|
state.details = null;
|
||||||
renderList();
|
renderList();
|
||||||
renderSummary(null);
|
renderSummary(null);
|
||||||
@@ -317,6 +613,12 @@
|
|||||||
loadSatellites();
|
loadSatellites();
|
||||||
});
|
});
|
||||||
document.getElementById('satellite-pdcm-export').addEventListener('click', exportCurrentCsv);
|
document.getElementById('satellite-pdcm-export').addEventListener('click', exportCurrentCsv);
|
||||||
|
document.getElementById('satellite-pdcm-send-ic').addEventListener('click', sendInitialConditions);
|
||||||
|
document.getElementById('satellite-pdcm-open-set-ic').addEventListener('click', openSetInitialConditionsModal);
|
||||||
|
document.getElementById('satellite-pdcm-set-ic-form').addEventListener('submit', submitInitialConditions);
|
||||||
|
const sendInitialConditionsTimeInput = document.getElementById('satellite-pdcm-send-ic-time');
|
||||||
|
sendInitialConditionsTimeInput.value = currentDateTimeInputValue();
|
||||||
|
sendInitialConditionsTimeInput.addEventListener('input', () => renderSendInitialConditionsState(state.details));
|
||||||
|
|
||||||
loadSatellites();
|
loadSatellites();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -0,0 +1,281 @@
|
|||||||
|
(function () {
|
||||||
|
const state = {
|
||||||
|
satellites: [],
|
||||||
|
selectedSatelliteId: null,
|
||||||
|
analysis: null,
|
||||||
|
loading: false
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
bindEvents();
|
||||||
|
await loadSatellites();
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindEvents() {
|
||||||
|
document.getElementById('tle-analysis-refresh')?.addEventListener('click', async () => {
|
||||||
|
state.selectedSatelliteId = null;
|
||||||
|
state.analysis = null;
|
||||||
|
state.loading = false;
|
||||||
|
renderResults();
|
||||||
|
await loadSatellites();
|
||||||
|
});
|
||||||
|
document.getElementById('tle-analysis-satellite-filter')?.addEventListener('input', renderSatellites);
|
||||||
|
document.getElementById('tle-analysis-count')?.addEventListener('change', async () => {
|
||||||
|
if (state.selectedSatelliteId) {
|
||||||
|
await loadAnalysis(state.selectedSatelliteId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSatellites() {
|
||||||
|
showAlert('Загрузка списка спутников...', 'info');
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/tle-analysis/satellites');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await errorText(response));
|
||||||
|
}
|
||||||
|
state.satellites = normalizeSatelliteResponse(await response.json());
|
||||||
|
renderSatellites();
|
||||||
|
renderResults();
|
||||||
|
clearAlert();
|
||||||
|
} catch (error) {
|
||||||
|
showAlert(`Не удалось загрузить спутники: ${error.message}`, 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAnalysis(satelliteId) {
|
||||||
|
state.loading = true;
|
||||||
|
state.analysis = null;
|
||||||
|
renderResults();
|
||||||
|
showAlert('Формирование анализа TLE...', 'info');
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({ tleCount: String(selectedTleCount()) });
|
||||||
|
const response = await fetch(`/api/tle-analysis/satellites/${encodeURIComponent(satelliteId)}/analysis?${params}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await errorText(response));
|
||||||
|
}
|
||||||
|
state.analysis = await response.json();
|
||||||
|
renderResults();
|
||||||
|
clearAlert();
|
||||||
|
} catch (error) {
|
||||||
|
state.analysis = null;
|
||||||
|
showAlert(`Не удалось сформировать анализ TLE: ${error.message}`, 'danger');
|
||||||
|
} finally {
|
||||||
|
state.loading = false;
|
||||||
|
renderResults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSatellites() {
|
||||||
|
const tbody = document.getElementById('tle-analysis-satellites-body');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
const filter = normalize(document.getElementById('tle-analysis-satellite-filter')?.value || '');
|
||||||
|
const satellites = state.satellites.filter((satellite) => {
|
||||||
|
if (!filter) return true;
|
||||||
|
return normalize([
|
||||||
|
satellite.id,
|
||||||
|
satellite.catalogId,
|
||||||
|
satellite.noradId,
|
||||||
|
satellite.code,
|
||||||
|
satellite.name,
|
||||||
|
satellite.typeCode,
|
||||||
|
satellite.tleRecordsCount
|
||||||
|
].join(' ')).includes(filter);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (satellites.length === 0) {
|
||||||
|
tbody.innerHTML = `<tr><td colspan="3" class="catalog-empty">Спутники не найдены</td></tr>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = satellites.map((satellite) => {
|
||||||
|
const active = Number(satellite.id) === Number(state.selectedSatelliteId) ? 'active' : '';
|
||||||
|
const title = escapeHtml(satellite.name || satellite.code || `КА ${satellite.id}`);
|
||||||
|
const catalogId = satellite.catalogId == null ? satellite.id : satellite.catalogId;
|
||||||
|
const tleCount = Number(satellite.tleRecordsCount || 0);
|
||||||
|
return `
|
||||||
|
<tr class="${active}" data-satellite-id="${escapeHtml(satellite.id)}">
|
||||||
|
<td>${escapeHtml(catalogId)}</td>
|
||||||
|
<td>
|
||||||
|
<div class="catalog-main-text">${title}</div>
|
||||||
|
<div class="catalog-helper">${escapeHtml(satellite.code || '')} ${satellite.typeCode ? ' / ' + escapeHtml(satellite.typeCode) : ''}</div>
|
||||||
|
<div class="catalog-helper">TLE-записей: ${escapeHtml(tleCount)}</div>
|
||||||
|
</td>
|
||||||
|
<td>${satellite.noradId == null ? '—' : escapeHtml(satellite.noradId)}</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
tbody.querySelectorAll('tr[data-satellite-id]').forEach((row) => {
|
||||||
|
row.addEventListener('click', async () => {
|
||||||
|
const satelliteId = Number(row.dataset.satelliteId);
|
||||||
|
state.selectedSatelliteId = satelliteId;
|
||||||
|
state.analysis = null;
|
||||||
|
renderSatellites();
|
||||||
|
renderSelectedSatellite();
|
||||||
|
await loadAnalysis(satelliteId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSelectedSatellite() {
|
||||||
|
const label = document.getElementById('tle-analysis-selected-satellite');
|
||||||
|
if (!label) return;
|
||||||
|
const satellite = state.satellites.find((item) => Number(item.id) === Number(state.selectedSatelliteId));
|
||||||
|
if (!satellite) {
|
||||||
|
label.textContent = 'Выберите спутник слева.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const name = satellite.name || satellite.code || `КА ${satellite.id}`;
|
||||||
|
const catalogId = satellite.catalogId == null ? satellite.id : satellite.catalogId;
|
||||||
|
label.textContent = `КА ${catalogId}: ${name}${satellite.noradId == null ? '' : `, NORAD ${satellite.noradId}`}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResults() {
|
||||||
|
renderSelectedSatellite();
|
||||||
|
const empty = document.getElementById('tle-analysis-empty');
|
||||||
|
const wrap = document.getElementById('tle-analysis-table-wrap');
|
||||||
|
const tbody = document.getElementById('tle-analysis-results-body');
|
||||||
|
const summary = document.getElementById('tle-analysis-summary');
|
||||||
|
if (!empty || !wrap || !tbody || !summary) return;
|
||||||
|
|
||||||
|
if (state.loading) {
|
||||||
|
empty.textContent = 'Формирование анализа TLE...';
|
||||||
|
empty.classList.remove('d-none');
|
||||||
|
wrap.classList.add('d-none');
|
||||||
|
summary.textContent = '';
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = state.analysis?.rows || [];
|
||||||
|
summary.textContent = rows.length ? `Записей TLE: ${rows.length}` : '';
|
||||||
|
|
||||||
|
if (!state.selectedSatelliteId) {
|
||||||
|
empty.textContent = 'Выберите спутник для загрузки TLE.';
|
||||||
|
empty.classList.remove('d-none');
|
||||||
|
wrap.classList.add('d-none');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
empty.textContent = 'Для выбранного спутника TLE не найдены.';
|
||||||
|
empty.classList.remove('d-none');
|
||||||
|
wrap.classList.add('d-none');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
empty.classList.add('d-none');
|
||||||
|
wrap.classList.remove('d-none');
|
||||||
|
tbody.innerHTML = rows.map(renderAnalysisRow).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAnalysisRow(row) {
|
||||||
|
const r = row.radiusVector || {};
|
||||||
|
const discrepancy = row.discrepancy;
|
||||||
|
const diffText = discrepancy
|
||||||
|
? `<div class="tle-analysis-discrepancy">${formatNumber(discrepancy.norm, 3)}</div>
|
||||||
|
<div class="catalog-helper tle-analysis-vector">Δx=${formatNumber(discrepancy.dx, 3)}, Δy=${formatNumber(discrepancy.dy, 3)}, Δz=${formatNumber(discrepancy.dz, 3)}</div>
|
||||||
|
<div class="catalog-helper">к предыдущей эпохе ${escapeHtml(formatDateTime(discrepancy.previousEpoch))}</div>`
|
||||||
|
: '<span class="catalog-helper">не рассчитывается</span>';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${escapeHtml(row.index)}</td>
|
||||||
|
<td>
|
||||||
|
<div>${escapeHtml(formatDateTime(row.epoch))}</div>
|
||||||
|
${row.tleHeader ? `<div class="catalog-helper">${escapeHtml(row.tleHeader)}</div>` : ''}
|
||||||
|
</td>
|
||||||
|
<td>${escapeHtml(row.revolution)}</td>
|
||||||
|
<td>${escapeHtml(row.noradId)}</td>
|
||||||
|
<td class="tle-analysis-vector">
|
||||||
|
x=${formatNumber(r.x, 3)}<br>
|
||||||
|
y=${formatNumber(r.y, 3)}<br>
|
||||||
|
z=${formatNumber(r.z, 3)}
|
||||||
|
</td>
|
||||||
|
<td>${formatNumber(row.radiusNorm, 3)}</td>
|
||||||
|
<td>${discrepancy ? formatNumber(discrepancy.epochDifferenceHours, 3) : '—'}</td>
|
||||||
|
<td>${diffText}</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSatelliteResponse(payload) {
|
||||||
|
if (Array.isArray(payload)) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.items)) {
|
||||||
|
return payload.items;
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.content)) {
|
||||||
|
return payload.content;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedTleCount() {
|
||||||
|
const input = document.getElementById('tle-analysis-count');
|
||||||
|
const value = Number(input?.value || 5);
|
||||||
|
if (!Number.isFinite(value) || value < 1) {
|
||||||
|
if (input) input.value = '5';
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
const count = Math.floor(value);
|
||||||
|
if (input && String(count) !== input.value) {
|
||||||
|
input.value = String(count);
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalize(value) {
|
||||||
|
return String(value || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(value) {
|
||||||
|
if (!value) return '—';
|
||||||
|
return String(value).replace('T', ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(value, fractionDigits) {
|
||||||
|
const number = Number(value);
|
||||||
|
if (!Number.isFinite(number)) return '—';
|
||||||
|
return number.toLocaleString('ru-RU', {
|
||||||
|
minimumFractionDigits: fractionDigits,
|
||||||
|
maximumFractionDigits: fractionDigits
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function errorText(response) {
|
||||||
|
const text = await response.text();
|
||||||
|
if (!text) return `${response.status} ${response.statusText}`;
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
return json.message || json.error || text;
|
||||||
|
} catch (_) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAlert(message, type) {
|
||||||
|
const alert = document.getElementById('tle-analysis-alert');
|
||||||
|
if (!alert) return;
|
||||||
|
alert.innerHTML = `<div class="alert alert-${type} py-2" role="alert">${escapeHtml(message)}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAlert() {
|
||||||
|
const alert = document.getElementById('tle-analysis-alert');
|
||||||
|
if (alert) alert.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value ?? '')
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll("'", ''');
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
(() => {
|
||||||
|
const state = {
|
||||||
|
first: null,
|
||||||
|
second: null,
|
||||||
|
parseTimers: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
bindEvents();
|
||||||
|
updateReference('first', null);
|
||||||
|
updateReference('second', null);
|
||||||
|
updateTimeLimits();
|
||||||
|
});
|
||||||
|
|
||||||
|
function bindEvents() {
|
||||||
|
const first = el('tle-first-text');
|
||||||
|
const second = el('tle-second-text');
|
||||||
|
first?.addEventListener('input', () => scheduleParse('first'));
|
||||||
|
second?.addEventListener('input', () => scheduleParse('second'));
|
||||||
|
el('tle-comparison-time')?.addEventListener('change', validateCalculationTime);
|
||||||
|
el('tle-compare-submit')?.addEventListener('click', compareTle);
|
||||||
|
el('tle-compare-clear')?.addEventListener('click', clearPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleParse(side) {
|
||||||
|
window.clearTimeout(state.parseTimers[side]);
|
||||||
|
const text = getTleText(side);
|
||||||
|
if (!text.trim()) {
|
||||||
|
state[side] = null;
|
||||||
|
updateReference(side, null);
|
||||||
|
updateTimeLimits();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStatus(side, 'Разбор...', 'text-muted');
|
||||||
|
state.parseTimers[side] = window.setTimeout(() => parseTle(side), 350);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function parseTle(side) {
|
||||||
|
try {
|
||||||
|
const parsed = await postJson('/api/tle-comparison/parse', { tle: getTleText(side) });
|
||||||
|
state[side] = parsed;
|
||||||
|
updateReference(side, parsed);
|
||||||
|
updateTimeLimits();
|
||||||
|
setStatus(side, 'Разобрано', 'tle-parse-ok');
|
||||||
|
} catch (error) {
|
||||||
|
state[side] = null;
|
||||||
|
updateReference(side, null);
|
||||||
|
updateTimeLimits();
|
||||||
|
setStatus(side, error.message || 'Ошибка разбора', 'tle-parse-error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function compareTle() {
|
||||||
|
showAlert('');
|
||||||
|
const first = getTleText('first');
|
||||||
|
const second = getTleText('second');
|
||||||
|
const time = getCalculationTime();
|
||||||
|
if (!first.trim() || !second.trim()) {
|
||||||
|
showAlert('Введите оба TLE для сравнения.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!time) {
|
||||||
|
showAlert('Задайте время расчета.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const minTime = maxEpoch();
|
||||||
|
if (minTime && time < minTime) {
|
||||||
|
showAlert(`Время расчета должно быть не раньше максимальной эпохи TLE: ${formatDateTime(minTime)}.`, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCompareLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await postJson('/api/tle-comparison/compare', { first, second, time });
|
||||||
|
renderComparison(response);
|
||||||
|
} catch (error) {
|
||||||
|
showAlert(error.message || 'Не удалось выполнить сравнение TLE.', 'danger');
|
||||||
|
} finally {
|
||||||
|
setCompareLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderComparison(response) {
|
||||||
|
const rows = response?.rows || [];
|
||||||
|
const body = el('tle-comparison-result-body');
|
||||||
|
const empty = el('tle-comparison-empty');
|
||||||
|
const wrap = el('tle-comparison-table-wrap');
|
||||||
|
const meta = el('tle-comparison-result-meta');
|
||||||
|
if (!body || !empty || !wrap) return;
|
||||||
|
|
||||||
|
if (!rows.length) {
|
||||||
|
body.innerHTML = '';
|
||||||
|
empty.classList.remove('d-none');
|
||||||
|
wrap.classList.add('d-none');
|
||||||
|
if (meta) meta.textContent = 'Нет данных для сравнения.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.innerHTML = rows.map((row) => `
|
||||||
|
<tr>
|
||||||
|
<td>${escapeHtml(row.name || row.code || '')}</td>
|
||||||
|
<td>${escapeHtml(emptyToDash(row.first))}</td>
|
||||||
|
<td>${escapeHtml(emptyToDash(row.second))}</td>
|
||||||
|
<td class="${deltaClass(row.delta)}">${escapeHtml(emptyToDash(row.delta))}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
empty.classList.add('d-none');
|
||||||
|
wrap.classList.remove('d-none');
|
||||||
|
if (meta) {
|
||||||
|
const deltaNorm = response.radiusVectorDelta?.norm;
|
||||||
|
const deltaText = Number.isFinite(deltaNorm) ? `${formatNumber(deltaNorm)} м` : '—';
|
||||||
|
meta.textContent = `Время расчета: ${formatDateTime(response.time)}. |Δr| = ${deltaText}. Эпохи: TLE 1 — ${formatDateTime(response.firstEpoch)}, TLE 2 — ${formatDateTime(response.secondEpoch)}.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateReference(side, parsed) {
|
||||||
|
setText(`tle-${side}-epoch`, formatDateTime(parsed?.time) || '—');
|
||||||
|
setText(`tle-${side}-norad`, parsed?.noradId ?? '—');
|
||||||
|
setText(`tle-${side}-revolution`, parsed?.revolution ?? '—');
|
||||||
|
if (!parsed) {
|
||||||
|
const text = getTleText(side);
|
||||||
|
setStatus(side, text.trim() ? 'Ошибка разбора' : 'Нет данных', text.trim() ? 'tle-parse-error' : 'text-muted');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTimeLimits() {
|
||||||
|
const input = el('tle-comparison-time');
|
||||||
|
const helper = el('tle-comparison-time-helper');
|
||||||
|
const minTime = maxEpoch();
|
||||||
|
if (!input) return;
|
||||||
|
if (!minTime) {
|
||||||
|
input.removeAttribute('min');
|
||||||
|
if (helper) helper.textContent = 'Минимальное допустимое время появится после разбора двух TLE.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
input.min = toDateTimeLocalValue(minTime);
|
||||||
|
if (!input.value || input.value < input.min) {
|
||||||
|
input.value = input.min;
|
||||||
|
}
|
||||||
|
if (helper) helper.textContent = `Минимальное допустимое время: ${formatDateTime(minTime)}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCalculationTime() {
|
||||||
|
const input = el('tle-comparison-time');
|
||||||
|
const minTime = maxEpoch();
|
||||||
|
if (!input || !minTime || !input.value) return;
|
||||||
|
const time = getCalculationTime();
|
||||||
|
if (time && time < minTime) {
|
||||||
|
showAlert(`Время расчета должно быть не раньше максимальной эпохи TLE: ${formatDateTime(minTime)}.`, 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPage() {
|
||||||
|
el('tle-first-text').value = '';
|
||||||
|
el('tle-second-text').value = '';
|
||||||
|
const time = el('tle-comparison-time');
|
||||||
|
if (time) time.value = '';
|
||||||
|
state.first = null;
|
||||||
|
state.second = null;
|
||||||
|
updateReference('first', null);
|
||||||
|
updateReference('second', null);
|
||||||
|
updateTimeLimits();
|
||||||
|
showAlert('');
|
||||||
|
renderComparison({ rows: [] });
|
||||||
|
setText('tle-comparison-result-meta', 'Задайте время не раньше максимальной эпохи TLE и нажмите “Сравнить”.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postJson(url, payload) {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
let message = `HTTP ${response.status}`;
|
||||||
|
try {
|
||||||
|
const error = await response.json();
|
||||||
|
message = error.error || Object.values(error)[0] || message;
|
||||||
|
} catch (_) {
|
||||||
|
message = await response.text() || message;
|
||||||
|
}
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function maxEpoch() {
|
||||||
|
if (!state.first?.time || !state.second?.time) return null;
|
||||||
|
return state.first.time >= state.second.time ? state.first.time : state.second.time;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCalculationTime() {
|
||||||
|
const value = el('tle-comparison-time')?.value;
|
||||||
|
return value ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTleText(side) {
|
||||||
|
return el(`tle-${side}-text`)?.value || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(side, text, cssClass) {
|
||||||
|
const node = el(`tle-${side}-status`);
|
||||||
|
if (!node) return;
|
||||||
|
node.className = `tle-parse-status ${cssClass || 'text-muted'}`;
|
||||||
|
node.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCompareLoading(loading) {
|
||||||
|
const button = el('tle-compare-submit');
|
||||||
|
if (!button) return;
|
||||||
|
button.disabled = loading;
|
||||||
|
button.textContent = loading ? 'Расчет...' : 'Рассчитать';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAlert(message, type = 'info') {
|
||||||
|
const container = el('tle-comparison-alert');
|
||||||
|
if (!container) return;
|
||||||
|
container.innerHTML = message
|
||||||
|
? `<div class="alert alert-${type} alert-dismissible fade show" role="alert">
|
||||||
|
${escapeHtml(message)}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Закрыть"></button>
|
||||||
|
</div>`
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDateTimeLocalValue(value) {
|
||||||
|
if (!value) return '';
|
||||||
|
return String(value).slice(0, 19);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(value) {
|
||||||
|
if (!value) return '';
|
||||||
|
return String(value).replace('T', ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(value) {
|
||||||
|
return Number(value).toLocaleString('ru-RU', { maximumFractionDigits: 3 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyToDash(value) {
|
||||||
|
return value === null || value === undefined || value === '' ? '—' : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deltaClass(value) {
|
||||||
|
const text = String(value || '').trim();
|
||||||
|
if (text.startsWith('+')) return 'tle-delta-positive';
|
||||||
|
if (text.startsWith('-')) return 'tle-delta-negative';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setText(id, value) {
|
||||||
|
const node = el(id);
|
||||||
|
if (node) node.textContent = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value ?? '')
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll("'", ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function el(id) {
|
||||||
|
return document.getElementById(id);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org"
|
||||||
|
xmlns:layout="http://www.w3.org/1999/xhtml"
|
||||||
|
layout:decorate="~{layouts/main-layout}">
|
||||||
|
<head>
|
||||||
|
<title>Бронирования</title>
|
||||||
|
<link rel="stylesheet" href="/css/catalog.css"/>
|
||||||
|
<link rel="stylesheet" href="/css/bookings.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<th:block layout:fragment="content">
|
||||||
|
<div id="bookings-page" class="catalog-page bookings-page py-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="mb-1">Бронирования</h2>
|
||||||
|
<div class="catalog-helper">Табличный просмотр забронированных слотов с фильтрацией по времени, заявке и спутникам.</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
|
<button id="bookings-reset" type="button" class="btn btn-outline-secondary">Сбросить</button>
|
||||||
|
<button id="bookings-refresh" type="button" class="btn btn-primary">Обновить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="bookings-alert"></div>
|
||||||
|
|
||||||
|
<div class="card catalog-card bookings-filter-card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3 align-items-end">
|
||||||
|
<div class="col-12 col-md-3">
|
||||||
|
<label for="bookings-time-start" class="form-label">Начало интервала</label>
|
||||||
|
<input id="bookings-time-start" type="datetime-local" class="form-control form-control-sm">
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-3">
|
||||||
|
<label for="bookings-time-stop" class="form-label">Конец интервала</label>
|
||||||
|
<input id="bookings-time-stop" type="datetime-local" class="form-control form-control-sm">
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-3">
|
||||||
|
<label for="bookings-request" class="form-label">Заявка</label>
|
||||||
|
<select id="bookings-request" class="form-select form-select-sm">
|
||||||
|
<option value="">Все заявки</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-3">
|
||||||
|
<label for="bookings-satellite-search" class="form-label">Фильтр спутников</label>
|
||||||
|
<input id="bookings-satellite-search"
|
||||||
|
type="search"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="ID, NORAD, код, имя, тип">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3 bookings-layout">
|
||||||
|
<div class="col-12 col-xl-3">
|
||||||
|
<div class="card catalog-card bookings-satellites-card h-100">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center gap-2">
|
||||||
|
<div>
|
||||||
|
<div class="catalog-section-title">Спутники</div>
|
||||||
|
<div class="catalog-helper mt-1">Выберите один или несколько КА.</div>
|
||||||
|
</div>
|
||||||
|
<button id="bookings-satellites-clear" type="button" class="btn btn-outline-secondary btn-sm">Выбрать все</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0 bookings-satellites-wrap">
|
||||||
|
<table class="table table-hover mb-0 bookings-satellites-table">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th class="bookings-check-col"></th>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Спутник</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="bookings-satellites-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-xl-9">
|
||||||
|
<div class="card catalog-card bookings-table-card h-100">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-start gap-3 flex-wrap">
|
||||||
|
<div>
|
||||||
|
<div class="catalog-section-title">Забронированные слоты</div>
|
||||||
|
<div id="bookings-summary" class="catalog-helper mt-1">Загрузка данных...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div id="bookings-empty" class="catalog-empty d-none">Забронированные слоты не найдены.</div>
|
||||||
|
<div id="bookings-table-wrap" class="table-responsive bookings-table-wrap">
|
||||||
|
<table class="table table-hover mb-0 bookings-table">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Бронь</th>
|
||||||
|
<th>КА</th>
|
||||||
|
<th>Слот / цикл</th>
|
||||||
|
<th>Время</th>
|
||||||
|
<th>Виток</th>
|
||||||
|
<th>Крен</th>
|
||||||
|
<th>Статус</th>
|
||||||
|
<th>Заявки</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="bookings-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/bookings_scripts.js"></script>
|
||||||
|
</th:block>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -46,6 +46,8 @@
|
|||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a class="dropdown-item" href="/satellites">Управление</a></li>
|
<li><a class="dropdown-item" href="/satellites">Управление</a></li>
|
||||||
<li><a class="dropdown-item" href="/satellites/pdcm">ПДЦМ</a></li>
|
<li><a class="dropdown-item" href="/satellites/pdcm">ПДЦМ</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/tle-comparison">Сравнение TLE</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/tle-analysis">Анализ TLE</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
@@ -74,6 +76,9 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/tgu-planning">Планирование ТГУ</a>
|
<a class="nav-link" href="/tgu-planning">Планирование ТГУ</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/bookings">Бронирования</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -292,6 +292,16 @@
|
|||||||
<input class="form-check-input" type="checkbox" id="toggleCoverage">
|
<input class="form-check-input" type="checkbox" id="toggleCoverage">
|
||||||
<label class="form-check-label" for="toggleCoverage">Показать зоны покрытия</label>
|
<label class="form-check-label" for="toggleCoverage">Показать зоны покрытия</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="stationOrbitHeightKm" class="form-label form-label-sm mb-1">Высота орбиты, км</label>
|
||||||
|
<input type="number"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
id="stationOrbitHeightKm"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
value="500"
|
||||||
|
onchange="handleStationOrbitHeightChange()">
|
||||||
|
</div>
|
||||||
<select class="form-select form-select-sm mb-2">
|
<select class="form-select form-select-sm mb-2">
|
||||||
<option selected>Все типы станций</option>
|
<option selected>Все типы станций</option>
|
||||||
<option value="1">Наземные</option>
|
<option value="1">Наземные</option>
|
||||||
@@ -1712,6 +1722,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleStationOrbitHeightChange() {
|
||||||
|
document.querySelectorAll('#stationsTableBody input[type="checkbox"]:checked').forEach(function(checkbox) {
|
||||||
|
checkStation(false, checkbox.value);
|
||||||
|
checkStation(true, checkbox.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Функция для обработки запросов
|
// Функция для обработки запросов
|
||||||
function checkOrbit(show, sat_id) {
|
function checkOrbit(show, sat_id) {
|
||||||
|
|||||||
@@ -56,6 +56,10 @@
|
|||||||
<input class="form-check-input" type="radio" name="rva-mode" id="rva-mode-merge-on-rev" value="merge-on-rev">
|
<input class="form-check-input" type="radio" name="rva-mode" id="rva-mode-merge-on-rev" value="merge-on-rev">
|
||||||
<label class="form-check-label" for="rva-mode-merge-on-rev">Суммарная длительность на витке</label>
|
<label class="form-check-label" for="rva-mode-merge-on-rev">Суммарная длительность на витке</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="rva-mode" id="rva-mode-average" value="average">
|
||||||
|
<label class="form-check-label" for="rva-mode-average">Средняя длительность</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,22 @@
|
|||||||
<h2 class="mb-1">Параметры движения центра масс</h2>
|
<h2 class="mb-1">Параметры движения центра масс</h2>
|
||||||
<div class="catalog-helper">Табличное представление asc-node по выбранному спутнику.</div>
|
<div class="catalog-helper">Табличное представление asc-node по выбранному спутнику.</div>
|
||||||
</div>
|
</div>
|
||||||
<button id="satellite-pdcm-refresh" type="button" class="btn btn-outline-secondary">Обновить</button>
|
<div class="d-flex gap-2 flex-wrap justify-content-end align-items-end satellite-pdcm-send-controls">
|
||||||
|
<div>
|
||||||
|
<label for="satellite-pdcm-send-ic-time" class="form-label catalog-helper mb-1">Время НУ</label>
|
||||||
|
<input
|
||||||
|
id="satellite-pdcm-send-ic-time"
|
||||||
|
type="text"
|
||||||
|
class="form-control satellite-pdcm-send-time"
|
||||||
|
inputmode="numeric"
|
||||||
|
placeholder="dd.mm.yyyy hh:MM:ss.zzz"
|
||||||
|
aria-label="Время НУ"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button id="satellite-pdcm-send-ic" type="button" class="btn btn-outline-primary" disabled>Передать НУ в slot-service</button>
|
||||||
|
<button id="satellite-pdcm-open-set-ic" type="button" class="btn btn-primary" disabled>Задать НУ</button>
|
||||||
|
<button id="satellite-pdcm-refresh" type="button" class="btn btn-outline-secondary">Обновить</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="satellite-pdcm-alert"></div>
|
<div id="satellite-pdcm-alert"></div>
|
||||||
@@ -66,6 +81,84 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="satellite-pdcm-set-ic-modal" tabindex="-1" aria-labelledby="satellite-pdcm-set-ic-title" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||||
|
<form id="satellite-pdcm-set-ic-form" class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="satellite-pdcm-set-ic-title">Задать начальные условия</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="satellite-pdcm-set-ic-target" class="catalog-helper mb-3">Выберите спутник слева.</div>
|
||||||
|
|
||||||
|
<div class="satellite-pdcm-ic-grid">
|
||||||
|
<div>
|
||||||
|
<label for="satellite-pdcm-ic-time" class="form-label">Time</label>
|
||||||
|
<input id="satellite-pdcm-ic-time" type="text" class="form-control" placeholder="dd.mm.yyyy hh:mm:ss.zzz" required/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="satellite-pdcm-ic-revolution" class="form-label">Revolution</label>
|
||||||
|
<input id="satellite-pdcm-ic-revolution" type="number" class="form-control" required/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="satellite-pdcm-ic-movement-model" class="form-label">Модель движения</label>
|
||||||
|
<select id="satellite-pdcm-ic-movement-model" class="form-select" required>
|
||||||
|
<option value="FOTO" selected>FOTO</option>
|
||||||
|
<option value="KONDOR">KONDOR</option>
|
||||||
|
<option value="METEORM1">METEORM1</option>
|
||||||
|
<option value="METEORM2">METEORM2</option>
|
||||||
|
<option value="BARS">BARS</option>
|
||||||
|
<option value="KONDOR_PROGNOZ">KONDOR_PROGNOZ</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="satellite-pdcm-ic-s-ball" class="form-label">S-ball</label>
|
||||||
|
<input id="satellite-pdcm-ic-s-ball" type="number" step="any" class="form-control" value="0" required/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="satellite-pdcm-ic-f81" class="form-label">F81</label>
|
||||||
|
<input id="satellite-pdcm-ic-f81" type="number" step="any" class="form-control" value="147.8" required/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="satellite-pdcm-ic-grid mt-3">
|
||||||
|
<div>
|
||||||
|
<label for="satellite-pdcm-ic-x" class="form-label">X</label>
|
||||||
|
<input id="satellite-pdcm-ic-x" type="number" step="any" class="form-control" required/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="satellite-pdcm-ic-y" class="form-label">Y</label>
|
||||||
|
<input id="satellite-pdcm-ic-y" type="number" step="any" class="form-control" required/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="satellite-pdcm-ic-z" class="form-label">Z</label>
|
||||||
|
<input id="satellite-pdcm-ic-z" type="number" step="any" class="form-control" required/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="satellite-pdcm-ic-grid mt-3">
|
||||||
|
<div>
|
||||||
|
<label for="satellite-pdcm-ic-vx" class="form-label">Vx</label>
|
||||||
|
<input id="satellite-pdcm-ic-vx" type="number" step="any" class="form-control" required/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="satellite-pdcm-ic-vy" class="form-label">Vy</label>
|
||||||
|
<input id="satellite-pdcm-ic-vy" type="number" step="any" class="form-control" required/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="satellite-pdcm-ic-vz" class="form-label">Vz</label>
|
||||||
|
<input id="satellite-pdcm-ic-vz" type="number" step="any" class="form-control" required/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||||
|
<button id="satellite-pdcm-set-ic-submit" type="submit" class="btn btn-primary">Запустить расчет</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/satellite_pdcm_scripts.js"></script>
|
<script src="/satellite_pdcm_scripts.js"></script>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org"
|
||||||
|
xmlns:layout="http://www.w3.org/1999/xhtml"
|
||||||
|
layout:decorate="~{layouts/main-layout}">
|
||||||
|
<head>
|
||||||
|
<title>Анализ TLE</title>
|
||||||
|
<link rel="stylesheet" href="/css/catalog.css"/>
|
||||||
|
<link rel="stylesheet" href="/css/tle-analysis.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<th:block layout:fragment="content">
|
||||||
|
<div id="tle-analysis-page" class="catalog-page tle-analysis-page py-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="mb-1">Анализ TLE</h2>
|
||||||
|
<div class="catalog-helper">Анализ изменения TLE выбранного спутника по разнице рассчитанных радиус-векторов.</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-end gap-2 flex-wrap">
|
||||||
|
<div>
|
||||||
|
<label for="tle-analysis-count" class="form-label mb-1 small text-muted">Количество TLE</label>
|
||||||
|
<input id="tle-analysis-count"
|
||||||
|
type="number"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
value="5">
|
||||||
|
</div>
|
||||||
|
<button id="tle-analysis-refresh" type="button" class="btn btn-outline-secondary">Обновить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tle-analysis-alert"></div>
|
||||||
|
|
||||||
|
<div class="row g-3 tle-analysis-layout">
|
||||||
|
<div class="col-12 col-xl-4">
|
||||||
|
<div class="card catalog-card tle-analysis-sidebar h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="catalog-section-title">Спутники</div>
|
||||||
|
<div class="catalog-helper mt-1">Выберите КА для формирования аналитической таблицы TLE.</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2 tle-analysis-filter-body">
|
||||||
|
<label for="tle-analysis-satellite-filter" class="form-label mb-1 small text-muted">Фильтр</label>
|
||||||
|
<input id="tle-analysis-satellite-filter"
|
||||||
|
type="search"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="ID, NORAD, код, имя, тип">
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0 tle-analysis-satellites-wrap">
|
||||||
|
<table class="table table-hover mb-0 tle-analysis-satellites-table">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Спутник</th>
|
||||||
|
<th>NORAD</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tle-analysis-satellites-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-xl-8">
|
||||||
|
<div class="card catalog-card tle-analysis-results-card h-100">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-start gap-3 flex-wrap">
|
||||||
|
<div>
|
||||||
|
<div class="catalog-section-title">Таблица анализа TLE</div>
|
||||||
|
<div id="tle-analysis-selected-satellite" class="catalog-helper mt-1">Выберите спутник слева.</div>
|
||||||
|
</div>
|
||||||
|
<div id="tle-analysis-summary" class="catalog-helper text-end"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0 tle-analysis-results-wrap">
|
||||||
|
<div id="tle-analysis-empty" class="catalog-empty">Выберите спутник для загрузки TLE.</div>
|
||||||
|
<div id="tle-analysis-table-wrap" class="table-responsive d-none">
|
||||||
|
<table class="table table-hover table-sm mb-0 tle-analysis-results-table">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Эпоха</th>
|
||||||
|
<th>Виток</th>
|
||||||
|
<th>NORAD</th>
|
||||||
|
<th>Радиус-вектор, м</th>
|
||||||
|
<th>|r|, м</th>
|
||||||
|
<th>Разница Времени, ч</th>
|
||||||
|
<th>Расхождение, м</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tle-analysis-results-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="/tle_analysis_scripts.js"></script>
|
||||||
|
</th:block>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org"
|
||||||
|
xmlns:layout="http://www.w3.org/1999/xhtml"
|
||||||
|
layout:decorate="~{layouts/main-layout}">
|
||||||
|
<head>
|
||||||
|
<title>Сравнение TLE</title>
|
||||||
|
<link rel="stylesheet" href="/css/catalog.css"/>
|
||||||
|
<link rel="stylesheet" href="/css/tle-comparison.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<th:block layout:fragment="content">
|
||||||
|
<div id="tle-comparison-page" class="catalog-page tle-comparison-page py-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="mb-1">Сравнение TLE</h2>
|
||||||
|
<div class="catalog-helper">Вставьте два набора TLE, проверьте эпохи и рассчитайте положение спутника на заданный момент времени.</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
|
<button id="tle-compare-submit" type="button" class="btn btn-primary">Рассчитать</button>
|
||||||
|
<button id="tle-compare-clear" type="button" class="btn btn-outline-secondary">Очистить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tle-comparison-alert"></div>
|
||||||
|
|
||||||
|
<div class="card catalog-card tle-input-card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="catalog-section-title">Ввод данных</div>
|
||||||
|
<div class="catalog-helper mt-1">Можно вставить две или три строки TLE. Заголовок определяется автоматически, строки 1 и 2 обязательны. Время расчета должно быть не раньше максимальной эпохи двух TLE.</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-12 col-md-5 col-xl-4">
|
||||||
|
<label for="tle-comparison-time" class="form-label">Время расчета</label>
|
||||||
|
<input id="tle-comparison-time" type="datetime-local" class="form-control" step="1"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-7 col-xl-8 d-flex align-items-end">
|
||||||
|
<div id="tle-comparison-time-helper" class="catalog-helper">Минимальное допустимое время появится после разбора двух TLE.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-12 col-xl-6">
|
||||||
|
<div class="tle-input-widget h-100">
|
||||||
|
<div class="d-flex justify-content-between align-items-center gap-3 mb-2">
|
||||||
|
<label for="tle-first-text" class="form-label mb-0">TLE 1</label>
|
||||||
|
<span id="tle-first-status" class="tle-parse-status text-muted">Нет данных</span>
|
||||||
|
</div>
|
||||||
|
<textarea id="tle-first-text"
|
||||||
|
class="form-control tle-textarea"
|
||||||
|
spellcheck="false"
|
||||||
|
placeholder="ISS (ZARYA) 1 25544U 98067A ... 2 25544 ..."></textarea>
|
||||||
|
<div class="tle-reference mt-3">
|
||||||
|
<div class="tle-reference-row">
|
||||||
|
<span>Эпоха TLE</span>
|
||||||
|
<strong id="tle-first-epoch">—</strong>
|
||||||
|
</div>
|
||||||
|
<div class="tle-reference-row">
|
||||||
|
<span>NORAD</span>
|
||||||
|
<strong id="tle-first-norad">—</strong>
|
||||||
|
</div>
|
||||||
|
<div class="tle-reference-row">
|
||||||
|
<span>Виток</span>
|
||||||
|
<strong id="tle-first-revolution">—</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-xl-6">
|
||||||
|
<div class="tle-input-widget h-100">
|
||||||
|
<div class="d-flex justify-content-between align-items-center gap-3 mb-2">
|
||||||
|
<label for="tle-second-text" class="form-label mb-0">TLE 2</label>
|
||||||
|
<span id="tle-second-status" class="tle-parse-status text-muted">Нет данных</span>
|
||||||
|
</div>
|
||||||
|
<textarea id="tle-second-text"
|
||||||
|
class="form-control tle-textarea"
|
||||||
|
spellcheck="false"
|
||||||
|
placeholder="ISS (ZARYA) 1 25544U 98067A ... 2 25544 ..."></textarea>
|
||||||
|
<div class="tle-reference mt-3">
|
||||||
|
<div class="tle-reference-row">
|
||||||
|
<span>Эпоха TLE</span>
|
||||||
|
<strong id="tle-second-epoch">—</strong>
|
||||||
|
</div>
|
||||||
|
<div class="tle-reference-row">
|
||||||
|
<span>NORAD</span>
|
||||||
|
<strong id="tle-second-norad">—</strong>
|
||||||
|
</div>
|
||||||
|
<div class="tle-reference-row">
|
||||||
|
<span>Виток</span>
|
||||||
|
<strong id="tle-second-revolution">—</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card catalog-card tle-result-card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-start gap-3 flex-wrap">
|
||||||
|
<div>
|
||||||
|
<div class="catalog-section-title">Результаты</div>
|
||||||
|
<div id="tle-comparison-result-meta" class="catalog-helper mt-1">Задайте время не раньше максимальной эпохи TLE и нажмите “Сравнить”.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div id="tle-comparison-empty" class="catalog-empty">Параметры положения и разность радиус-векторов появятся здесь.</div>
|
||||||
|
<div id="tle-comparison-table-wrap" class="table-responsive tle-result-table-wrap d-none">
|
||||||
|
<table class="table table-hover mb-0 align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Параметр</th>
|
||||||
|
<th>TLE 1</th>
|
||||||
|
<th>TLE 2</th>
|
||||||
|
<th>Разница / значение</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tle-comparison-result-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/tle_comparison_scripts.js"></script>
|
||||||
|
</th:block>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user