routes: объём маршрута (volumeMb) — расчёт, хранение в БД и отображение
Объём режима = скорость × длительность, Мбайт: съёмка 120 Мбайт/с (12 ГБ / 100 с), сброс — канал 1200 Мбит/с ÷ 8. Единый источник — ModeDataRates в pcp-types-lib. Хранится колонкой volume_mb (Flyway V10 mission-planing, V9 complex-mission, бэкфилл из duration), заполняется на всех путях построения через @PrePersist/@PreUpdate. DTO отдают вычисляемое read-only поле (та же формула). Фронт pcp-tgu-ops-ui показывает объём в редакторе (инспектор, построенный маршрут) и в сводке комплексного плана.
This commit is contained in:
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package space.nstart.pcp.pcp_types_lib.dto.satellite.mission
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Скорости накопления/сброса данных для оценки объёма режима.
|
||||||
|
*
|
||||||
|
* Объём (`volumeMb`) считается единообразно во всех сервисах в **мегабайтах (Мбайт)** как
|
||||||
|
* `скорость × длительность`, поэтому константы живут в общей библиотеке типов — один источник
|
||||||
|
* правды для текущего плана (pcp-ui-service), редактора и комплексного плана (pcp-tgu-ops-ui).
|
||||||
|
*
|
||||||
|
* Внимание на единицы: съёмка задана в Мбайт/с, а скорость канала сброса — в Мбит/с,
|
||||||
|
* поэтому при расчёте объёма сброса скорость приводится к Мбайт делением на 8.
|
||||||
|
*/
|
||||||
|
object ModeDataRates {
|
||||||
|
/**
|
||||||
|
* Накопление при съёмке — единое для всех КА, **Мбайт/с**.
|
||||||
|
* Из расчёта 100 с маршрута = 12 ГБ (1 ГБ = 1000 Мбайт): 12000 Мбайт / 100 с = 120 Мбайт/с.
|
||||||
|
*/
|
||||||
|
const val SURVEY_MBYTE_PER_SEC: Double = 120.0
|
||||||
|
|
||||||
|
/** Сброс — постоянная скорость канала, **Мбит/с**. */
|
||||||
|
const val DROP_MBIT_PER_SEC: Double = 1200.0
|
||||||
|
|
||||||
|
/** Бит в байте — приведение скорости канала (Мбит/с) к объёму памяти (Мбайт). */
|
||||||
|
private const val BITS_PER_BYTE: Double = 8.0
|
||||||
|
|
||||||
|
/** Объём маршрута съёмки, Мбайт, по его длительности (секунды). */
|
||||||
|
fun surveyVolumeMb(durationSeconds: Double): Double = durationSeconds * SURVEY_MBYTE_PER_SEC
|
||||||
|
|
||||||
|
/** Объём сброса, Мбайт, по длительности сеанса (секунды): скорость канала Мбит/с → Мбайт. */
|
||||||
|
fun dropVolumeMb(durationSeconds: Double): Double = durationSeconds * DROP_MBIT_PER_SEC / BITS_PER_BYTE
|
||||||
|
}
|
||||||
+13
-2
@@ -1,5 +1,6 @@
|
|||||||
package space.nstart.pcp.pcp_types_lib.dto.satellite.mission
|
package space.nstart.pcp.pcp_types_lib.dto.satellite.mission
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.fasterxml.jackson.annotation.JsonSubTypes
|
import com.fasterxml.jackson.annotation.JsonSubTypes
|
||||||
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||||
import io.swagger.v3.oas.annotations.media.Schema
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
@@ -35,6 +36,10 @@ sealed interface ModeResponseDTO {
|
|||||||
|
|
||||||
@get:Schema(description = "Тип режима", allowableValues = ["SURVEY", "DROP"])
|
@get:Schema(description = "Тип режима", allowableValues = ["SURVEY", "DROP"])
|
||||||
val type: String
|
val type: String
|
||||||
|
|
||||||
|
@get:Schema(description = "Объём режима, Мбайт (накопление съёмки 120 Мбайт/с, сброс — канал 1200 Мбит/с)")
|
||||||
|
@get:JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
|
val volumeMb: Double
|
||||||
}
|
}
|
||||||
|
|
||||||
@Schema(description = "Режим съемки")
|
@Schema(description = "Режим съемки")
|
||||||
@@ -54,7 +59,10 @@ data class SurveyModeResponseDTO(
|
|||||||
val roll: Double = 0.0,
|
val roll: Double = 0.0,
|
||||||
@get:Schema(description = "Идентификаторы забронированных слотов")
|
@get:Schema(description = "Идентификаторы забронированных слотов")
|
||||||
val bookedSlotIds: List<Long> = emptyList()
|
val bookedSlotIds: List<Long> = emptyList()
|
||||||
) : ModeResponseDTO
|
) : ModeResponseDTO {
|
||||||
|
@get:JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
|
override val volumeMb: Double get() = ModeDataRates.surveyVolumeMb(duration)
|
||||||
|
}
|
||||||
|
|
||||||
@Schema(description = "Режим сброса")
|
@Schema(description = "Режим сброса")
|
||||||
data class DropModeResponseDTO(
|
data class DropModeResponseDTO(
|
||||||
@@ -66,4 +74,7 @@ data class DropModeResponseDTO(
|
|||||||
val station: String? = "",
|
val station: String? = "",
|
||||||
val duration: Double = 0.0,
|
val duration: Double = 0.0,
|
||||||
val surveys: List<Long> = emptyList()
|
val surveys: List<Long> = emptyList()
|
||||||
) : ModeResponseDTO
|
) : ModeResponseDTO {
|
||||||
|
@get:JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
|
override val volumeMb: Double get() = ModeDataRates.dropVolumeMb(duration)
|
||||||
|
}
|
||||||
|
|||||||
+7
-1
@@ -1,5 +1,6 @@
|
|||||||
package space.nstart.pcp.pcp_types_lib.dto.satellite.mission
|
package space.nstart.pcp.pcp_types_lib.dto.satellite.mission
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
data class SatelliteModeResponseDTO(
|
data class SatelliteModeResponseDTO(
|
||||||
@@ -19,4 +20,9 @@ data class SatelliteModeResponseDTO(
|
|||||||
val cellNums: List<Long> = emptyList(),
|
val cellNums: List<Long> = emptyList(),
|
||||||
val bookedSlotIds: List<Long> = emptyList(),
|
val bookedSlotIds: List<Long> = emptyList(),
|
||||||
val snapshotId: Long? = null
|
val snapshotId: Long? = null
|
||||||
)
|
) {
|
||||||
|
/** Объём режима, Мбайт: сброс — канал 1200 Мбит/с (÷8), иначе — накопление съёмки 120 Мбайт/с. */
|
||||||
|
@get:JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
|
val volumeMb: Double
|
||||||
|
get() = if (type == "DROP") ModeDataRates.dropVolumeMb(duration) else ModeDataRates.surveyVolumeMb(duration)
|
||||||
|
}
|
||||||
|
|||||||
+6
-1
@@ -1,5 +1,6 @@
|
|||||||
package space.nstart.pcp.pcp_types_lib.dto.satellite.mission
|
package space.nstart.pcp.pcp_types_lib.dto.satellite.mission
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import io.swagger.v3.oas.annotations.media.Schema
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
import jakarta.validation.constraints.DecimalMin
|
import jakarta.validation.constraints.DecimalMin
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@@ -74,4 +75,8 @@ data class SurveyRouteBuildResponseDTO(
|
|||||||
val contourWkt: String,
|
val contourWkt: String,
|
||||||
@field:Schema(description = "Идентификатор сохранённого режима съёмки в БД (null — маршрут не сохранялся)")
|
@field:Schema(description = "Идентификатор сохранённого режима съёмки в БД (null — маршрут не сохранялся)")
|
||||||
val surveyModeId: Long? = null
|
val surveyModeId: Long? = null
|
||||||
)
|
) {
|
||||||
|
@get:Schema(description = "Объём маршрута, Мбайт (накопление съёмки 120 Мбайт/с × длительность)")
|
||||||
|
@get:JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
|
val volumeMb: Double get() = ModeDataRates.surveyVolumeMb(duration)
|
||||||
|
}
|
||||||
|
|||||||
+23
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.module.kotlin.readValue
|
|||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertInstanceOf
|
import org.junit.jupiter.api.Assertions.assertInstanceOf
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class ModeResponseDTOJacksonTest {
|
class ModeResponseDTOJacksonTest {
|
||||||
|
|
||||||
@@ -50,4 +51,26 @@ class ModeResponseDTOJacksonTest {
|
|||||||
assertEquals("SURVEY", result[0].type)
|
assertEquals("SURVEY", result[0].type)
|
||||||
assertEquals("DROP", result[1].type)
|
assertEquals("DROP", result[1].type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `serializes computed volume and tolerates it on read-back`() {
|
||||||
|
// Сервис-источник теперь сериализует volumeMb; потребитель (pcp-ui-service)
|
||||||
|
// десериализует тот же JSON обратно — read-only поле не должно ломать чтение.
|
||||||
|
val modes: List<ModeResponseDTO> = listOf(
|
||||||
|
SurveyModeResponseDTO(id = 1, planId = 10, duration = 100.0),
|
||||||
|
DropModeResponseDTO(id = 2, planId = 10, duration = 5.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
val json = mapper.writeValueAsString(modes)
|
||||||
|
val tree = mapper.readTree(json)
|
||||||
|
|
||||||
|
// Съёмка: 100 с × 120 Мбайт/с = 12000 Мбайт. Сброс: 5 с × 1200 Мбит/с ÷ 8 = 750 Мбайт.
|
||||||
|
assertEquals(12000.0, tree[0]["volumeMb"].asDouble())
|
||||||
|
assertEquals(750.0, tree[1]["volumeMb"].asDouble())
|
||||||
|
|
||||||
|
val roundTrip: List<ModeResponseDTO> = mapper.readValue(json)
|
||||||
|
assertEquals(12000.0, roundTrip[0].volumeMb)
|
||||||
|
assertEquals(750.0, roundTrip[1].volumeMb)
|
||||||
|
assertTrue(roundTrip[0] is SurveyModeResponseDTO)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -12,8 +12,11 @@ import jakarta.persistence.Id
|
|||||||
import jakarta.persistence.OneToMany
|
import jakarta.persistence.OneToMany
|
||||||
import jakarta.persistence.JoinColumn
|
import jakarta.persistence.JoinColumn
|
||||||
import jakarta.persistence.ManyToOne
|
import jakarta.persistence.ManyToOne
|
||||||
|
import jakarta.persistence.PrePersist
|
||||||
|
import jakarta.persistence.PreUpdate
|
||||||
import jakarta.persistence.Table
|
import jakarta.persistence.Table
|
||||||
import jakarta.persistence.Index
|
import jakarta.persistence.Index
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.ModeDataRates
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@@ -92,4 +95,14 @@ class SatelliteModeEntity(
|
|||||||
fetch = FetchType.LAZY
|
fetch = FetchType.LAZY
|
||||||
)
|
)
|
||||||
var cells: MutableList<SatelliteModeCellEntity> = mutableListOf()
|
var cells: MutableList<SatelliteModeCellEntity> = mutableListOf()
|
||||||
)
|
) {
|
||||||
|
/** Объём маршрута, Мбайт. Денормализация: считается из duration при построении (satellite_modes — только SURVEY). */
|
||||||
|
@Column(name = "volume_mb", nullable = false)
|
||||||
|
var volumeMb: Double = ModeDataRates.surveyVolumeMb(duration)
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
@PreUpdate
|
||||||
|
fun computeVolumeMb() {
|
||||||
|
volumeMb = ModeDataRates.surveyVolumeMb(duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
-- Объём маршрута съёмки как атрибут, Мбайт. Считается при построении из длительности
|
||||||
|
-- (см. ModeDataRates в pcp-types-lib): накопление 120 Мбайт/с. satellite_modes — только SURVEY.
|
||||||
|
ALTER TABLE satellite_modes ADD COLUMN volume_mb DOUBLE PRECISION NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- Бэкфилл существующих маршрутов из длительности.
|
||||||
|
UPDATE satellite_modes SET volume_mb = duration * 120;
|
||||||
+14
-1
@@ -3,7 +3,10 @@ package space.nstart.pcp.pcp_mission_planing_service.entity
|
|||||||
import jakarta.persistence.Column
|
import jakarta.persistence.Column
|
||||||
import jakarta.persistence.DiscriminatorValue
|
import jakarta.persistence.DiscriminatorValue
|
||||||
import jakarta.persistence.Entity
|
import jakarta.persistence.Entity
|
||||||
|
import jakarta.persistence.PrePersist
|
||||||
|
import jakarta.persistence.PreUpdate
|
||||||
import jakarta.persistence.Table
|
import jakarta.persistence.Table
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.ModeDataRates
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@@ -20,4 +23,14 @@ class DropModeEntity(
|
|||||||
|
|
||||||
@Column(name = "duration", nullable = false)
|
@Column(name = "duration", nullable = false)
|
||||||
var duration: Double = 0.0
|
var duration: Double = 0.0
|
||||||
) : ModeEntity(id, plan, timeStart, revolution)
|
) : ModeEntity(id, plan, timeStart, revolution) {
|
||||||
|
/** Объём сброса, Мбайт. Денормализация: считается из duration (канал 1200 Мбит/с ÷ 8) при сохранении. */
|
||||||
|
@Column(name = "volume_mb", nullable = false)
|
||||||
|
var volumeMb: Double = ModeDataRates.dropVolumeMb(duration)
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
@PreUpdate
|
||||||
|
fun computeVolumeMb() {
|
||||||
|
volumeMb = ModeDataRates.dropVolumeMb(duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+14
-1
@@ -5,8 +5,11 @@ import jakarta.persistence.DiscriminatorValue
|
|||||||
import jakarta.persistence.Entity
|
import jakarta.persistence.Entity
|
||||||
import jakarta.persistence.EnumType
|
import jakarta.persistence.EnumType
|
||||||
import jakarta.persistence.Enumerated
|
import jakarta.persistence.Enumerated
|
||||||
|
import jakarta.persistence.PrePersist
|
||||||
|
import jakarta.persistence.PreUpdate
|
||||||
import jakarta.persistence.Table
|
import jakarta.persistence.Table
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.routes.SurveyModeStatus
|
import space.nstart.pcp.pcp_types_lib.dto.routes.SurveyModeStatus
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.ModeDataRates
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@@ -46,4 +49,14 @@ class SurveyModeEntity(
|
|||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
@Column(name = "source", nullable = false, length = 20)
|
@Column(name = "source", nullable = false, length = 20)
|
||||||
var source: SurveyModeSource = SurveyModeSource.SLOT
|
var source: SurveyModeSource = SurveyModeSource.SLOT
|
||||||
) : ModeEntity(id, plan, timeStart, revolution)
|
) : ModeEntity(id, plan, timeStart, revolution) {
|
||||||
|
/** Объём маршрута съёмки, Мбайт. Денормализация: считается из duration при построении/сохранении. */
|
||||||
|
@Column(name = "volume_mb", nullable = false)
|
||||||
|
var volumeMb: Double = ModeDataRates.surveyVolumeMb(duration)
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
@PreUpdate
|
||||||
|
fun computeVolumeMb() {
|
||||||
|
volumeMb = ModeDataRates.surveyVolumeMb(duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
-- Объём данных как атрибут маршрута, Мбайт. Считается при построении из длительности
|
||||||
|
-- (см. ModeDataRates в pcp-types-lib): съёмка 120 Мбайт/с; сброс — канал 1200 Мбит/с ÷ 8 = 150 Мбайт/с.
|
||||||
|
ALTER TABLE survey_modes ADD COLUMN volume_mb DOUBLE PRECISION NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE drop_modes ADD COLUMN volume_mb DOUBLE PRECISION NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- Бэкфилл существующих маршрутов из длительности.
|
||||||
|
UPDATE survey_modes SET volume_mb = duration * 120;
|
||||||
|
UPDATE drop_modes SET volume_mb = duration * 1200.0 / 8;
|
||||||
@@ -99,6 +99,8 @@ export type ComplexPlanMode = {
|
|||||||
cellNum: number;
|
cellNum: number;
|
||||||
bookedSlotIds: number[];
|
bookedSlotIds: number[];
|
||||||
snapshotId: number | null;
|
snapshotId: number | null;
|
||||||
|
/** Объём режима, МБ (скорость накопления/сброса × длительность). Считает бэк (pcp-types-lib). */
|
||||||
|
volumeMb?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export type SurveyRoute = {
|
|||||||
longitude: number;
|
longitude: number;
|
||||||
/** Контур полосы съёмки, WKT POLYGON (lon lat, ...). */
|
/** Контур полосы съёмки, WKT POLYGON (lon lat, ...). */
|
||||||
contourWkt: string;
|
contourWkt: string;
|
||||||
|
/** Объём маршрута, МБ (скорость накопления × длительность). Считает бэк (pcp-types-lib). */
|
||||||
|
volumeMb?: number;
|
||||||
/** Идентификатор сохранённого режима съёмки в БД (есть только после сохранения). */
|
/** Идентификатор сохранённого режима съёмки в БД (есть только после сохранения). */
|
||||||
surveyModeId?: number;
|
surveyModeId?: number;
|
||||||
};
|
};
|
||||||
@@ -96,6 +98,8 @@ export type DropMode = {
|
|||||||
station: string | null;
|
station: string | null;
|
||||||
duration: number;
|
duration: number;
|
||||||
surveys: number[];
|
surveys: number[];
|
||||||
|
/** Объём сброса, Мбайт (канал 1200 Мбит/с ÷ 8 × длительность). Считает бэк (pcp-types-lib). */
|
||||||
|
volumeMb?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import type { Tgu2DMapLayersState } from "../tgu-map-2d/model/mapLayerTypes";
|
|||||||
import type { MapPolygon, Tgu2DMapScene } from "../tgu-map-2d/model/mapTypes";
|
import type { MapPolygon, Tgu2DMapScene } from "../tgu-map-2d/model/mapTypes";
|
||||||
import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
|
import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
|
||||||
import { formatLocalDateTime, parseLocal, toLocalInputValue, toLocalIso } from "../../utils/localTime";
|
import { formatLocalDateTime, parseLocal, toLocalInputValue, toLocalIso } from "../../utils/localTime";
|
||||||
|
import { formatVolume } from "../../utils/dataVolume";
|
||||||
|
|
||||||
type TguComplexPlanTabProps = {
|
type TguComplexPlanTabProps = {
|
||||||
range: TimelineRange;
|
range: TimelineRange;
|
||||||
@@ -203,6 +204,8 @@ export function TguComplexPlanTab({ range, platforms }: TguComplexPlanTabProps)
|
|||||||
|
|
||||||
const displayRun = selectedRun;
|
const displayRun = selectedRun;
|
||||||
const contoursCount = modePolygons.length;
|
const contoursCount = modePolygons.length;
|
||||||
|
// Суммарный объём режимов run’а (МБ) — съёмка по скорости накопления, сброс по скорости канала.
|
||||||
|
const totalVolumeMb = modes.reduce((sum, mode) => sum + (mode.volumeMb ?? 0), 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="tgu-map-workspace">
|
<main className="tgu-map-workspace">
|
||||||
@@ -253,7 +256,7 @@ export function TguComplexPlanTab({ range, platforms }: TguComplexPlanTabProps)
|
|||||||
<div className="tgu-map-toolbar__spacer" style={{ flex: 1 }} />
|
<div className="tgu-map-toolbar__spacer" style={{ flex: 1 }} />
|
||||||
{displayRun && (
|
{displayRun && (
|
||||||
<div className="tgu-map-toolbar__subtitle">
|
<div className="tgu-map-toolbar__subtitle">
|
||||||
run #{displayRun.runId} · режимов {displayRun.calculatedModesCount} · охват {formatPercent(displayRun.coveredAreaPercent)}
|
run #{displayRun.runId} · режимов {displayRun.calculatedModesCount} · охват {formatPercent(displayRun.coveredAreaPercent)} · объём {formatVolume(totalVolumeMb)}
|
||||||
{displayRun.snapshotIds.length > 0 ? ` · snapshot ${displayRun.snapshotIds.join(", ")}` : ""} · на карте {contoursCount} контуров
|
{displayRun.snapshotIds.length > 0 ? ` · snapshot ${displayRun.snapshotIds.join(", ")}` : ""} · на карте {contoursCount} контуров
|
||||||
{showRequests ? ` · заявок ${requestItems.length}` : ""}
|
{showRequests ? ` · заявок ${requestItems.length}` : ""}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { formatDate, formatDateTime, formatDateTimeSeconds, formatDuration, formatTime, kindColor, kindLabel } from "./editorPresentation";
|
import { formatDate, formatDateTime, formatDateTimeSeconds, formatDuration, formatTime, formatVolume, kindColor, kindLabel } from "./editorPresentation";
|
||||||
import { statusStyle } from "../tgu-planning/tguStatus";
|
import { statusStyle } from "../tgu-planning/tguStatus";
|
||||||
import { formatLocalDateTime, parseLocal, toLocalIso } from "../../utils/localTime";
|
import { formatLocalDateTime, parseLocal, toLocalIso } from "../../utils/localTime";
|
||||||
import { Time24Field } from "../../components/timeFields";
|
import { Time24Field } from "../../components/timeFields";
|
||||||
@@ -172,6 +172,8 @@ export function WorkInspector({
|
|||||||
<dd className="mono">{formatDateTimeSeconds(work.endMs)}</dd>
|
<dd className="mono">{formatDateTimeSeconds(work.endMs)}</dd>
|
||||||
<dt>Длительность</dt>
|
<dt>Длительность</dt>
|
||||||
<dd className="mono">{formatDuration(work.endMs - work.startMs)}</dd>
|
<dd className="mono">{formatDuration(work.endMs - work.startMs)}</dd>
|
||||||
|
<dt>Объём</dt>
|
||||||
|
<dd className="mono">{formatVolume(work.volumeMb)}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -427,7 +429,7 @@ export function ShootPanel({
|
|||||||
<div className="tgu-alert tgu-alert--danger tgu-alert--compact">{routeError}</div>
|
<div className="tgu-alert tgu-alert--danger tgu-alert--compact">{routeError}</div>
|
||||||
) : route ? (
|
) : route ? (
|
||||||
<div className="tgu-editor-panel__hint">
|
<div className="tgu-editor-panel__hint">
|
||||||
Маршрут построен · виток {route.revolution} · крен {route.roll.toFixed(1)}° · захват {route.captureAngle.toFixed(1)}°
|
Маршрут построен · виток {route.revolution} · крен {route.roll.toFixed(1)}° · захват {route.captureAngle.toFixed(1)}° · объём {formatVolume(route.volumeMb)}
|
||||||
<br />
|
<br />
|
||||||
центр {route.lat.toFixed(2)}, {route.longitude.toFixed(2)} · контур показан на карте
|
центр {route.lat.toFixed(2)}, {route.longitude.toFixed(2)} · контур показан на карте
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -741,7 +741,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
|||||||
rollDeg: savedRoute?.roll ?? routeRoll,
|
rollDeg: savedRoute?.roll ?? routeRoll,
|
||||||
captureAngleDeg: savedRoute?.captureAngle ?? DEFAULT_CAPTURE_ANGLE_DEG,
|
captureAngleDeg: savedRoute?.captureAngle ?? DEFAULT_CAPTURE_ANGLE_DEG,
|
||||||
centerLat: savedRoute?.lat,
|
centerLat: savedRoute?.lat,
|
||||||
centerLon: savedRoute?.longitude
|
centerLon: savedRoute?.longitude,
|
||||||
|
volumeMb: savedRoute?.volumeMb
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
setEditingWorkId(undefined);
|
setEditingWorkId(undefined);
|
||||||
@@ -788,6 +789,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
|||||||
setDropSaving(true);
|
setDropSaving(true);
|
||||||
setDropError(undefined);
|
setDropError(undefined);
|
||||||
let savedDropId: number | undefined;
|
let savedDropId: number | undefined;
|
||||||
|
let savedDropVolumeMb: number | undefined;
|
||||||
try {
|
try {
|
||||||
// Ленивое создание черновика на первую правку; адресуем сброс в DRAFT, а не в живой план.
|
// Ленивое создание черновика на первую правку; адресуем сброс в DRAFT, а не в живой план.
|
||||||
const draftId = await ensureDraft();
|
const draftId = await ensureDraft();
|
||||||
@@ -806,6 +808,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
|||||||
planEnd: toLocalIso(selectedPlan.endMs)
|
planEnd: toLocalIso(selectedPlan.endMs)
|
||||||
});
|
});
|
||||||
savedDropId = savedDrop.id;
|
savedDropId = savedDrop.id;
|
||||||
|
savedDropVolumeMb = savedDrop.volumeMb;
|
||||||
// Перечитываем включения DRAFT (источник истины) и ревалидируем.
|
// Перечитываем включения DRAFT (источник истины) и ревалидируем.
|
||||||
void refreshDraftModes(draftId);
|
void refreshDraftModes(draftId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -824,7 +827,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
|||||||
endMs: zoneStopMsVal,
|
endMs: zoneStopMsVal,
|
||||||
label: `Сброс · ${stationNm}`,
|
label: `Сброс · ${stationNm}`,
|
||||||
station: { id: String(selectedZone.stationId), name: stationNm },
|
station: { id: String(selectedZone.stationId), name: stationNm },
|
||||||
planModeId: savedDropId
|
planModeId: savedDropId,
|
||||||
|
volumeMb: savedDropVolumeMb
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
setEditingWorkId(undefined);
|
setEditingWorkId(undefined);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export type PlanSurveyMode = {
|
|||||||
duration: number;
|
duration: number;
|
||||||
contourWkt: string | null;
|
contourWkt: string | null;
|
||||||
roll: number;
|
roll: number;
|
||||||
|
/** Объём маршрута, МБ (скорость накопления × длительность). Считает бэк (pcp-types-lib). */
|
||||||
|
volumeMb?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PlanDropMode = {
|
export type PlanDropMode = {
|
||||||
@@ -21,6 +23,8 @@ export type PlanDropMode = {
|
|||||||
station: string | null;
|
station: string | null;
|
||||||
duration: number;
|
duration: number;
|
||||||
surveys: number[];
|
surveys: number[];
|
||||||
|
/** Объём сброса, Мбайт (канал 1200 Мбит/с ÷ 8 × длительность). Считает бэк (pcp-types-lib). */
|
||||||
|
volumeMb?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PlanMode = PlanSurveyMode | PlanDropMode;
|
export type PlanMode = PlanSurveyMode | PlanDropMode;
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export type AddShootInput = {
|
|||||||
captureAngleDeg?: number;
|
captureAngleDeg?: number;
|
||||||
centerLat?: number;
|
centerLat?: number;
|
||||||
centerLon?: number;
|
centerLon?: number;
|
||||||
|
/** Объём маршрута, МБ (из ответа бэка). */
|
||||||
|
volumeMb?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AddDownlinkInput = {
|
export type AddDownlinkInput = {
|
||||||
@@ -39,6 +41,8 @@ export type AddDownlinkInput = {
|
|||||||
station: EditorGroundStation;
|
station: EditorGroundStation;
|
||||||
/** id сохранённого режима сброса в БД черновика (если сброс персистнут). */
|
/** id сохранённого режима сброса в БД черновика (если сброс персистнут). */
|
||||||
planModeId?: number;
|
planModeId?: number;
|
||||||
|
/** Объём сброса, МБ (из ответа бэка). */
|
||||||
|
volumeMb?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function seedDraft(plan?: TguPlanUi, spacecraftId?: string): Draft {
|
export function seedDraft(plan?: TguPlanUi, spacecraftId?: string): Draft {
|
||||||
@@ -80,6 +84,7 @@ export function planModesToWorks(modes: PlanMode[]): EditorWork[] {
|
|||||||
rollDeg: mode.roll,
|
rollDeg: mode.roll,
|
||||||
centerLat: mode.lat,
|
centerLat: mode.lat,
|
||||||
centerLon: mode.longitude,
|
centerLon: mode.longitude,
|
||||||
|
volumeMb: mode.volumeMb,
|
||||||
// Центр контура — он же опорная точка для проверки видимости.
|
// Центр контура — он же опорная точка для проверки видимости.
|
||||||
targetLat: mode.lat,
|
targetLat: mode.lat,
|
||||||
targetLon: mode.longitude
|
targetLon: mode.longitude
|
||||||
@@ -96,7 +101,8 @@ export function planModesToWorks(modes: PlanMode[]): EditorWork[] {
|
|||||||
planModeId: mode.id,
|
planModeId: mode.id,
|
||||||
revolution: mode.revolution,
|
revolution: mode.revolution,
|
||||||
stationId: mode.station ?? undefined,
|
stationId: mode.station ?? undefined,
|
||||||
surveyIds: mode.surveys
|
surveyIds: mode.surveys,
|
||||||
|
volumeMb: mode.volumeMb
|
||||||
}];
|
}];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -160,6 +166,7 @@ export function addShoot(draft: Draft, input: AddShootInput): Draft {
|
|||||||
captureAngleDeg: input.captureAngleDeg,
|
captureAngleDeg: input.captureAngleDeg,
|
||||||
centerLat: input.centerLat,
|
centerLat: input.centerLat,
|
||||||
centerLon: input.centerLon,
|
centerLon: input.centerLon,
|
||||||
|
volumeMb: input.volumeMb,
|
||||||
origin: "added",
|
origin: "added",
|
||||||
isNew: true
|
isNew: true
|
||||||
};
|
};
|
||||||
@@ -176,6 +183,7 @@ export function addDownlink(draft: Draft, input: AddDownlinkInput): Draft {
|
|||||||
endMs: input.endMs,
|
endMs: input.endMs,
|
||||||
stationId: input.station.id,
|
stationId: input.station.id,
|
||||||
planModeId: input.planModeId,
|
planModeId: input.planModeId,
|
||||||
|
volumeMb: input.volumeMb,
|
||||||
origin: "added",
|
origin: "added",
|
||||||
isNew: true
|
isNew: true
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import type { EditorWorkKind } from "./model/editorTypes";
|
import type { EditorWorkKind } from "./model/editorTypes";
|
||||||
|
|
||||||
|
// Объём данных — общий форматтер (используется и в комплексном плане).
|
||||||
|
export { formatVolume } from "../../utils/dataVolume";
|
||||||
|
|
||||||
export const EDITOR_MONTHS = ["янв", "фев", "мар", "апр", "май", "июн", "июл", "авг", "сен", "окт", "ноя", "дек"];
|
export const EDITOR_MONTHS = ["янв", "фев", "мар", "апр", "май", "июн", "июл", "авг", "сен", "окт", "ноя", "дек"];
|
||||||
|
|
||||||
export const DEFAULT_EDITOR_MODES = [
|
export const DEFAULT_EDITOR_MODES = [
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export type EditorWorkBase = {
|
|||||||
endMs: number;
|
endMs: number;
|
||||||
origin: EditorWorkOrigin;
|
origin: EditorWorkOrigin;
|
||||||
isNew?: boolean;
|
isNew?: boolean;
|
||||||
|
/** Объём включения, МБ (считает бэк: скорость × длительность). Есть у включений плана и сохранённых правок. */
|
||||||
|
volumeMb?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EditorShootWork = EditorWorkBase & {
|
export type EditorShootWork = EditorWorkBase & {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Форматирование объёма данных режима (съёмка/сброс). Бэк отдаёт объём в мегабайтах
|
||||||
|
* (см. ModeDataRates в pcp-types-lib: накопление съёмки 120 Мбайт/с, сброс — канал 1200 Мбит/с ÷ 8).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** До 1000 — в МБ, дальше — в ГБ (1 ГБ = 1000 МБ). Тут МБ = мегабайт. «—», если значения нет. */
|
||||||
|
export function formatVolume(mb?: number | null): string {
|
||||||
|
if (mb == null || !Number.isFinite(mb) || mb <= 0) {
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
if (mb >= 1000) {
|
||||||
|
const gb = mb / 1000;
|
||||||
|
return `${gb >= 100 ? Math.round(gb) : gb.toFixed(1)} ГБ`;
|
||||||
|
}
|
||||||
|
return `${Math.round(mb)} МБ`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user