refactor(tgu-ui): единый инспектор объектов карты (MapObjectInspector) на вкладках «Карта» и «Комплексный план»
Обобщил ComplexInfoPanel в общий MapObjectInspector (tgu-map-2d): карточки для всех типов объектов — заявка, включение комплексного плана, ячейка тепловой карты, маршрут planWorks, станция, маркер КА, линия; список с раскрытием при наложении. Вкладка «Карта» переведена на enableObjectChooser + onObjectsAtPoint + правый инспектор — заменила разрозненные Tgu2DMapRequestsPanel/Tgu2DMapStationPanel (удалены) и объектную часть нижней плашки (Tgu2DMapDetails ужат до контекста плана). Вкладка «Комплексный план» использует тот же инспектор. RequestAtPoint переехал в tgu-requests/RequestDetailsPanel (нужен вкладке «Заявки»).
This commit is contained in:
@@ -19,7 +19,7 @@ import type { Tgu2DMapLayersState } from "../tgu-map-2d/model/mapLayerTypes";
|
||||
import type { MapPolygon, Tgu2DMapScene } from "../tgu-map-2d/model/mapTypes";
|
||||
import type { Tgu2DMapSelection } from "../tgu-map-2d/model/mapSelectionTypes";
|
||||
import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
|
||||
import { ComplexInfoPanel } from "./ComplexInfoPanel";
|
||||
import { MapObjectInspector } from "../tgu-map-2d/MapObjectInspector";
|
||||
import { formatLocalDateTime, parseLocal, toLocalInputValue, toLocalIso } from "../../utils/localTime";
|
||||
import { formatVolume } from "../../utils/dataVolume";
|
||||
|
||||
@@ -365,7 +365,7 @@ export function TguComplexPlanTab({ range, platforms }: TguComplexPlanTabProps)
|
||||
|
||||
{mapHits.length > 0 && (
|
||||
<aside className="tgu-editor-side-panel">
|
||||
<ComplexInfoPanel
|
||||
<MapObjectInspector
|
||||
hits={mapHits}
|
||||
activeId={activeHitId}
|
||||
onToggle={toggleHit}
|
||||
|
||||
+117
-35
@@ -1,41 +1,50 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { Tgu2DMapSelection } from "../tgu-map-2d/model/mapSelectionTypes";
|
||||
import type { Tgu2DMapSelection } from "./model/mapSelectionTypes";
|
||||
import type { ComplexPlanMode } from "../../api/complexPlanApi";
|
||||
import type { PriorityCell } from "../../api/cellsApi";
|
||||
import type { RequestMapItem } from "../../model/requestTypes";
|
||||
import { statusStyle } from "../tgu-planning/tguStatus";
|
||||
import { formatLocalDateTime, parseLocal } from "../../utils/localTime";
|
||||
import { formatVolume } from "../../utils/dataVolume";
|
||||
|
||||
type ComplexInfoPanelProps = {
|
||||
/**
|
||||
* Единый инспектор объектов карты для всех вкладок (Карта, Комплексный план, Редактор):
|
||||
* показывает данные объекта, выбранного на карте — заявки, маршрута/включения, ячейки тепловой
|
||||
* карты, станции, маркера КА. При наложении объектов (≥2 под точкой клика) даёт список с
|
||||
* раскрытием карточки нужного. Данные-обогащение (modes/requestItems/priorityCells) — опциональны:
|
||||
* чего нет, то берётся из самого выбора (имя, статус).
|
||||
*/
|
||||
type MapObjectInspectorProps = {
|
||||
/** Объекты под точкой клика (1 — карточка сразу, ≥2 — список с раскрытием). */
|
||||
hits: Tgu2DMapSelection[];
|
||||
/** id раскрытого объекта в списке. */
|
||||
activeId?: string;
|
||||
onToggle: (selection: Tgu2DMapSelection) => void;
|
||||
onClose: () => void;
|
||||
/** Режимы текущего прогона — для карточки включения комплексного плана. */
|
||||
modes: ComplexPlanMode[];
|
||||
/** Заявки активного интервала — для карточки заявки (если включён показ заявок). */
|
||||
requestItems: RequestMapItem[];
|
||||
/** Ячейки карты приоритетов — для карточки ячейки тепловой карты. */
|
||||
priorityCells: PriorityCell[];
|
||||
/** Режимы прогона комплексного плана — для карточки включения (layer "complexPlan"). */
|
||||
modes?: ComplexPlanMode[];
|
||||
/** Заявки активного интервала — для карточки заявки (layer "requests"). */
|
||||
requestItems?: RequestMapItem[];
|
||||
/** Ячейки карты приоритетов — для карточки ячейки тепловой карты (layer "heatmap"). */
|
||||
priorityCells?: PriorityCell[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Правый инспектор вкладки «Комплексный план» (по аналогии с редактором): показывает данные
|
||||
* объекта, выбранного на карте — включения комплексного плана, заявки или ячейки тепловой карты.
|
||||
* При наложении объектов даёт список под точкой клика с раскрытием карточки нужного.
|
||||
*/
|
||||
export function ComplexInfoPanel({
|
||||
export function MapObjectInspector({
|
||||
hits,
|
||||
activeId,
|
||||
onToggle,
|
||||
onClose,
|
||||
modes,
|
||||
requestItems,
|
||||
priorityCells,
|
||||
}: ComplexInfoPanelProps) {
|
||||
modes = [],
|
||||
requestItems = [],
|
||||
priorityCells = [],
|
||||
}: MapObjectInspectorProps) {
|
||||
const renderCard = (selection: Tgu2DMapSelection): ReactNode => {
|
||||
if (selection.type === "marker") {
|
||||
return selection.layer === "stations"
|
||||
? <StationCard selection={selection} />
|
||||
: <MarkerCard selection={selection} />;
|
||||
}
|
||||
if (selection.type === "line") return <LineCard selection={selection} />;
|
||||
switch (selection.layer) {
|
||||
case "complexPlan":
|
||||
return <ModeCard selection={selection} modes={modes} />;
|
||||
@@ -43,6 +52,8 @@ export function ComplexInfoPanel({
|
||||
return <RequestCard selection={selection} requestItems={requestItems} />;
|
||||
case "heatmap":
|
||||
return <CellCard selection={selection} priorityCells={priorityCells} />;
|
||||
case "planWorks":
|
||||
return <PlanWorkCard selection={selection} />;
|
||||
default:
|
||||
return <div className="tgu-editor-panel__hint">{selection.name || selection.id}</div>;
|
||||
}
|
||||
@@ -82,8 +93,12 @@ export function ComplexInfoPanel({
|
||||
);
|
||||
}
|
||||
|
||||
function ModeCard({ selection, modes }: { selection: Tgu2DMapSelection; modes: ComplexPlanMode[] }) {
|
||||
const mode = selection.type === "polygon" ? modes.find((m) => m.id === selection.modeId) : undefined;
|
||||
type PolygonSelection = Extract<Tgu2DMapSelection, { type: "polygon" }>;
|
||||
type MarkerSelection = Extract<Tgu2DMapSelection, { type: "marker" }>;
|
||||
type LineSelection = Extract<Tgu2DMapSelection, { type: "line" }>;
|
||||
|
||||
function ModeCard({ selection, modes }: { selection: PolygonSelection; modes: ComplexPlanMode[] }) {
|
||||
const mode = modes.find((m) => m.id === selection.modeId);
|
||||
if (!mode) return <div className="tgu-editor-panel__hint">{selection.name || selection.id}</div>;
|
||||
return (
|
||||
<section className="tgu-editor-panel__section">
|
||||
@@ -114,11 +129,8 @@ function ModeCard({ selection, modes }: { selection: Tgu2DMapSelection; modes: C
|
||||
);
|
||||
}
|
||||
|
||||
function RequestCard({ selection, requestItems }: { selection: Tgu2DMapSelection; requestItems: RequestMapItem[] }) {
|
||||
const item =
|
||||
selection.type === "polygon" && selection.requestId
|
||||
? requestItems.find((r) => r.id === selection.requestId)
|
||||
: undefined;
|
||||
function RequestCard({ selection, requestItems }: { selection: PolygonSelection; requestItems: RequestMapItem[] }) {
|
||||
const item = selection.requestId ? requestItems.find((r) => r.id === selection.requestId) : undefined;
|
||||
if (!item) return <div className="tgu-editor-panel__hint">{selection.name || selection.id}</div>;
|
||||
return (
|
||||
<section className="tgu-editor-panel__section">
|
||||
@@ -139,23 +151,24 @@ function RequestCard({ selection, requestItems }: { selection: Tgu2DMapSelection
|
||||
<dd className="mono">{item.importance}</dd>
|
||||
<dt>Покрытие</dt>
|
||||
<dd className="mono">{item.coveragePercent != null ? `${item.coveragePercent.toFixed(1)} %` : "—"}</dd>
|
||||
<dt>КПП</dt>
|
||||
<dd className="mono">{item.kpp?.join(", ") || "—"}</dd>
|
||||
<dt>Приоритет передачи</dt>
|
||||
<dd>{item.highPriorityTransmit ? "Да" : "Нет"}</dd>
|
||||
</dl>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function CellCard({ selection, priorityCells }: { selection: Tgu2DMapSelection; priorityCells: PriorityCell[] }) {
|
||||
const cell =
|
||||
selection.type === "polygon" && selection.cellNum != null
|
||||
? priorityCells.find((c) => c.cellNum === selection.cellNum)
|
||||
: undefined;
|
||||
const importance = cell?.importance ?? (selection.type === "polygon" ? selection.importance : undefined);
|
||||
function CellCard({ selection, priorityCells }: { selection: PolygonSelection; priorityCells: PriorityCell[] }) {
|
||||
const cell = selection.cellNum != null ? priorityCells.find((c) => c.cellNum === selection.cellNum) : undefined;
|
||||
const importance = cell?.importance ?? selection.importance;
|
||||
return (
|
||||
<section className="tgu-editor-panel__section">
|
||||
<div className="tgu-editor-eyebrow">{selection.name}</div>
|
||||
<dl className="tgu-map-request-details__grid">
|
||||
<dt>№ ячейки</dt>
|
||||
<dd className="mono">{cell?.cellNum ?? (selection.type === "polygon" ? selection.cellNum : undefined) ?? "—"}</dd>
|
||||
<dd className="mono">{cell?.cellNum ?? selection.cellNum ?? "—"}</dd>
|
||||
<dt>Важность</dt>
|
||||
<dd className="mono">{importance != null ? importance.toFixed(4) : "—"}</dd>
|
||||
{cell && (
|
||||
@@ -181,6 +194,75 @@ function CellCard({ selection, priorityCells }: { selection: Tgu2DMapSelection;
|
||||
);
|
||||
}
|
||||
|
||||
function PlanWorkCard({ selection }: { selection: PolygonSelection }) {
|
||||
const style = selection.status ? statusStyle(selection.status) : undefined;
|
||||
return (
|
||||
<section className="tgu-editor-panel__section">
|
||||
<div className="tgu-editor-eyebrow">{selection.name}</div>
|
||||
<dl className="tgu-map-request-details__grid">
|
||||
{selection.spacecraftId && (<><dt>КА</dt><dd className="mono">{selection.spacecraftId}</dd></>)}
|
||||
{selection.planId && (<><dt>План</dt><dd className="mono">{selection.planId}</dd></>)}
|
||||
{selection.surveyType && (<><dt>Тип съёмки</dt><dd>{surveyTypeLabel(selection.surveyType)}</dd></>)}
|
||||
</dl>
|
||||
{style && (
|
||||
<span className="tgu-map-status-pill">
|
||||
<i style={{ backgroundColor: style.color }} />
|
||||
{style.label}
|
||||
</span>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function StationCard({ selection }: { selection: MarkerSelection }) {
|
||||
return (
|
||||
<section className="tgu-editor-panel__section">
|
||||
<div className="tgu-editor-eyebrow">{selection.name}</div>
|
||||
<dl className="tgu-map-request-details__grid">
|
||||
<dt>Номер</dt>
|
||||
<dd className="mono">{selection.stationNumber ?? "—"}</dd>
|
||||
<dt>Широта</dt>
|
||||
<dd className="mono">{selection.lat != null ? `${selection.lat.toFixed(4)}°` : "—"}</dd>
|
||||
<dt>Долгота</dt>
|
||||
<dd className="mono">{selection.lon != null ? `${selection.lon.toFixed(4)}°` : "—"}</dd>
|
||||
<dt>Высота, м</dt>
|
||||
<dd className="mono">{selection.altitude != null ? selection.altitude.toFixed(1) : "—"}</dd>
|
||||
<dt>Угол места мин, °</dt>
|
||||
<dd className="mono">{selection.elevationMin != null ? selection.elevationMin.toFixed(1) : "—"}</dd>
|
||||
<dt>Угол места макс, °</dt>
|
||||
<dd className="mono">{selection.elevationMax != null ? selection.elevationMax.toFixed(1) : "—"}</dd>
|
||||
</dl>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function MarkerCard({ selection }: { selection: MarkerSelection }) {
|
||||
return (
|
||||
<section className="tgu-editor-panel__section">
|
||||
<div className="tgu-editor-eyebrow">{selection.name}</div>
|
||||
<dl className="tgu-map-request-details__grid">
|
||||
{selection.spacecraftId && (<><dt>КА</dt><dd className="mono">{selection.spacecraftId}</dd></>)}
|
||||
<dt>Широта</dt>
|
||||
<dd className="mono">{selection.lat != null ? `${selection.lat.toFixed(4)}°` : "—"}</dd>
|
||||
<dt>Долгота</dt>
|
||||
<dd className="mono">{selection.lon != null ? `${selection.lon.toFixed(4)}°` : "—"}</dd>
|
||||
</dl>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function LineCard({ selection }: { selection: LineSelection }) {
|
||||
return (
|
||||
<section className="tgu-editor-panel__section">
|
||||
<div className="tgu-editor-eyebrow">{selection.name}</div>
|
||||
<dl className="tgu-map-request-details__grid">
|
||||
<dt>Тип</dt>
|
||||
<dd>{selection.kind === "drop" ? "Сброс на НКПОР" : "Трасса КА"}</dd>
|
||||
</dl>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ReqFragmentRow({ requestId, coveragePercent, importance }: { requestId: string; coveragePercent: number; importance: number }) {
|
||||
return (
|
||||
<>
|
||||
@@ -213,10 +295,10 @@ function selectionTitle(selection: Tgu2DMapSelection): string {
|
||||
}
|
||||
}
|
||||
|
||||
/** Класс цветовой метки в списке выбора (есть стили для requests/planWorks/swath/...). */
|
||||
/** Класс цветовой метки в списке выбора (стили в tgu-editor.css). */
|
||||
function swatchKind(selection: Tgu2DMapSelection): string {
|
||||
if (selection.type === "marker") return "station";
|
||||
if (selection.type === "line") return "drop";
|
||||
if (selection.type === "marker") return selection.layer === "stations" ? "station" : "spacecraftMarkers";
|
||||
if (selection.type === "line") return selection.kind === "drop" ? "drop" : "tracks";
|
||||
return selection.layer;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import type { TguPlanUi } from "../../model/timelineTypes";
|
||||
import { platformLabel } from "../tgu-planning/tguTimelineMapper";
|
||||
import { statusStyle } from "../tgu-planning/tguStatus";
|
||||
import type { Tgu2DMapSelection } from "./model/mapSelectionTypes";
|
||||
|
||||
type Tgu2DMapDetailsProps = {
|
||||
selectedPlan?: TguPlanUi;
|
||||
selectedObject?: Tgu2DMapSelection;
|
||||
};
|
||||
|
||||
export function Tgu2DMapDetails({ selectedPlan, selectedObject }: Tgu2DMapDetailsProps) {
|
||||
/**
|
||||
* Нижняя плашка контекста вкладки «Карта»: какой план выбран на «Планах» (КА/статус).
|
||||
* Информация по объектам карты живёт в правом инспекторе (MapObjectInspector), не здесь.
|
||||
*/
|
||||
export function Tgu2DMapDetails({ selectedPlan }: Tgu2DMapDetailsProps) {
|
||||
const status = selectedPlan ? statusStyle(selectedPlan.status) : undefined;
|
||||
|
||||
return (
|
||||
@@ -34,15 +36,6 @@ export function Tgu2DMapDetails({ selectedPlan, selectedObject }: Tgu2DMapDetail
|
||||
) : "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="tgu-map-details__label">Объект карты</div>
|
||||
<div className="tgu-map-details__value mono">{selectedObject?.name || selectedObject?.id || "-"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="tgu-map-details__label">Работы</div>
|
||||
<div className="tgu-map-details__value">Работы плана</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
import type { RequestMapItem } from "../../model/requestTypes";
|
||||
import type { MapPolygon } from "./model/mapTypes";
|
||||
import { formatLocalDateTime, parseLocal } from "../../utils/localTime";
|
||||
|
||||
export type RequestAtPoint = { polygon: MapPolygon; item: RequestMapItem };
|
||||
|
||||
type Props = {
|
||||
requests: RequestAtPoint[];
|
||||
selectedPolygonId?: string;
|
||||
onHover: (polygonId: string | undefined) => void;
|
||||
onSelect: (polygonId: string) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function Tgu2DMapRequestsPanel({ requests, selectedPolygonId, onHover, onSelect, onClose }: Props) {
|
||||
const selectedEntry = selectedPolygonId ? requests.find((r) => r.polygon.id === selectedPolygonId) : undefined;
|
||||
|
||||
return (
|
||||
<aside className="tgu-map-requests-panel">
|
||||
<div className="tgu-panel-heading">
|
||||
<span>Заявки в точке ({requests.length})</span>
|
||||
<button className="tgu-link-button" type="button" onClick={onClose}>
|
||||
Закрыть
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="tgu-map-requests-list">
|
||||
{requests.map(({ polygon, item }) => (
|
||||
<button
|
||||
key={polygon.id}
|
||||
className={`tgu-map-request-row ${polygon.id === selectedPolygonId ? "is-selected" : ""}`}
|
||||
type="button"
|
||||
onMouseEnter={() => onHover(polygon.id)}
|
||||
onMouseLeave={() => onHover(undefined)}
|
||||
onClick={() => onSelect(polygon.id)}
|
||||
>
|
||||
<i className={`tgu-map-request-row__type req-${item.surveyType.toLowerCase()}`} />
|
||||
<span className="tgu-map-request-row__body">
|
||||
<span className="tgu-map-request-row__name">{item.name || item.id}</span>
|
||||
<span className="tgu-map-request-row__meta">
|
||||
{formatDateRange(item.beginDateTime, item.endDateTime)}
|
||||
{" · "}
|
||||
{item.surveyType}
|
||||
{" · "}
|
||||
{item.status}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedEntry && (
|
||||
<div className="tgu-map-request-details">
|
||||
<div className="tgu-map-panel__label">Детали заявки</div>
|
||||
<dl className="tgu-map-request-details__grid">
|
||||
<dt>ID</dt>
|
||||
<dd className="mono">{selectedEntry.item.id}</dd>
|
||||
|
||||
<dt>Название</dt>
|
||||
<dd>{selectedEntry.item.name || "—"}</dd>
|
||||
|
||||
<dt>Статус</dt>
|
||||
<dd>{selectedEntry.item.status}</dd>
|
||||
|
||||
<dt>Тип съёмки</dt>
|
||||
<dd>{selectedEntry.item.surveyType}</dd>
|
||||
|
||||
<dt>Начало</dt>
|
||||
<dd className="mono">{formatDateTime(selectedEntry.item.beginDateTime)}</dd>
|
||||
|
||||
<dt>Конец</dt>
|
||||
<dd className="mono">{formatDateTime(selectedEntry.item.endDateTime)}</dd>
|
||||
|
||||
<dt>Важность</dt>
|
||||
<dd className="mono">{selectedEntry.item.importance}</dd>
|
||||
|
||||
<dt>Покрытие</dt>
|
||||
<dd className="mono">{selectedEntry.item.coveragePercent != null ? `${selectedEntry.item.coveragePercent.toFixed(1)} %` : "—"}</dd>
|
||||
|
||||
<dt>КПП</dt>
|
||||
<dd className="mono">{selectedEntry.item.kpp?.join(", ") || "—"}</dd>
|
||||
|
||||
<dt>Приоритет передачи</dt>
|
||||
<dd>{selectedEntry.item.highPriorityTransmit ? "Да" : "Нет"}</dd>
|
||||
|
||||
<dt>Создано</dt>
|
||||
<dd className="mono">{formatDateTime(selectedEntry.item.createdAt)}</dd>
|
||||
|
||||
<dt>Изменено</dt>
|
||||
<dd className="mono">{formatDateTime(selectedEntry.item.updatedAt)}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
function formatDateTime(value: string): string {
|
||||
return formatLocalDateTime(parseLocal(value));
|
||||
}
|
||||
|
||||
function formatDateRange(begin: string, end: string): string {
|
||||
const b = new Date(parseLocal(begin));
|
||||
const e = new Date(parseLocal(end));
|
||||
const sameDay =
|
||||
b.getFullYear() === e.getFullYear() &&
|
||||
b.getMonth() === e.getMonth() &&
|
||||
b.getDate() === e.getDate();
|
||||
const dateOpts: Intl.DateTimeFormatOptions = { day: "2-digit", month: "2-digit" };
|
||||
const timeOpts: Intl.DateTimeFormatOptions = { hour: "2-digit", minute: "2-digit" };
|
||||
const beginStr = `${b.toLocaleDateString("ru-RU", dateOpts)} ${b.toLocaleTimeString("ru-RU", timeOpts)}`;
|
||||
const endStr = sameDay
|
||||
? b.toLocaleTimeString("ru-RU", timeOpts)
|
||||
: `${e.toLocaleDateString("ru-RU", dateOpts)} ${e.toLocaleTimeString("ru-RU", timeOpts)}`;
|
||||
return `${beginStr} – ${endStr}`;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { Tgu2DMapSelection } from "./model/mapSelectionTypes";
|
||||
|
||||
type StationSelection = Extract<Tgu2DMapSelection, { type: "marker" }> & { layer: "stations" };
|
||||
|
||||
type Props = {
|
||||
selection: StationSelection;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function Tgu2DMapStationPanel({ selection, onClose }: Props) {
|
||||
return (
|
||||
<aside className="tgu-map-requests-panel">
|
||||
<div className="tgu-panel-heading">
|
||||
<span>Станция</span>
|
||||
<button className="tgu-link-button" type="button" onClick={onClose}>
|
||||
Закрыть
|
||||
</button>
|
||||
</div>
|
||||
<div className="tgu-map-request-details" style={{ flex: 1 }}>
|
||||
<div className="tgu-map-panel__label">{selection.name}</div>
|
||||
<dl className="tgu-map-request-details__grid">
|
||||
<dt>Номер</dt>
|
||||
<dd className="mono">{selection.stationNumber ?? "—"}</dd>
|
||||
|
||||
<dt>Широта</dt>
|
||||
<dd className="mono">{selection.lat != null ? `${selection.lat.toFixed(4)}°` : "—"}</dd>
|
||||
|
||||
<dt>Долгота</dt>
|
||||
<dd className="mono">{selection.lon != null ? `${selection.lon.toFixed(4)}°` : "—"}</dd>
|
||||
|
||||
<dt>Высота, м</dt>
|
||||
<dd className="mono">{selection.altitude != null ? selection.altitude.toFixed(1) : "—"}</dd>
|
||||
|
||||
<dt>Угол места мин, °</dt>
|
||||
<dd className="mono">{selection.elevationMin != null ? selection.elevationMin.toFixed(1) : "—"}</dd>
|
||||
|
||||
<dt>Угол места макс, °</dt>
|
||||
<dd className="mono">{selection.elevationMax != null ? selection.elevationMax.toFixed(1) : "—"}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -6,12 +6,10 @@ import type { TguPlanUi, TguPlatformUi, TimelineRange } from "../../model/timeli
|
||||
import { platformLabel } from "../tgu-planning/tguTimelineMapper";
|
||||
import { DEFAULT_TGU_2D_MAP_LAYERS, type Tgu2DMapLayerKey, type Tgu2DMapLayersState } from "./model/mapLayerTypes";
|
||||
import type { Tgu2DMapSelection } from "./model/mapSelectionTypes";
|
||||
import type { MapPolygon } from "./model/mapTypes";
|
||||
import { buildTgu2DMapScene, getLayerAvailability } from "./Tgu2DMapLayers";
|
||||
import { Tgu2DMapDetails } from "./Tgu2DMapDetails";
|
||||
import { Tgu2DMapRequestsPanel, type RequestAtPoint } from "./Tgu2DMapRequestsPanel";
|
||||
import { MapObjectInspector } from "./MapObjectInspector";
|
||||
import { Tgu2DMapSidebar } from "./Tgu2DMapSidebar";
|
||||
import { Tgu2DMapStationPanel } from "./Tgu2DMapStationPanel";
|
||||
import { Tgu2DMapToolbar } from "./Tgu2DMapToolbar";
|
||||
import { Tgu2DMapView } from "./Tgu2DMapView";
|
||||
import { getTgu2DMapIntervalState, TGU_2D_MAP_INTERVAL_WARNING } from "./tgu2DMapInterval";
|
||||
@@ -58,15 +56,13 @@ export function Tgu2DMapTab({
|
||||
const [layers, setLayers] = useState<Tgu2DMapLayersState>(DEFAULT_TGU_2D_MAP_LAYERS);
|
||||
const [search, setSearch] = useState("");
|
||||
const [redrawToken, setRedrawToken] = useState(0);
|
||||
const [selectedObject, setSelectedObject] = useState<Tgu2DMapSelection>();
|
||||
const [hiddenIds, setHiddenIds] = useState<ReadonlySet<string>>(new Set());
|
||||
const [requestsState, setRequestsState] = useState<RequestsState>(() => buildInitialRequestsState(range));
|
||||
const [stations, setStations] = useState<StationDto[]>([]);
|
||||
const [stationsFetchFailed, setStationsFetchFailed] = useState(false);
|
||||
const [requestsAtPoint, setRequestsAtPoint] = useState<RequestAtPoint[]>([]);
|
||||
const [selectedRequestPolygonId, setSelectedRequestPolygonId] = useState<string | undefined>();
|
||||
const [hoveredRequestPolygonId, setHoveredRequestPolygonId] = useState<string | undefined>();
|
||||
const [stationPanel, setStationPanel] = useState<(Tgu2DMapSelection & { type: "marker"; layer: "stations" }) | undefined>();
|
||||
// Инспектор объекта карты справа (единый для всех вкладок): объекты под кликом и раскрытый.
|
||||
const [mapHits, setMapHits] = useState<Tgu2DMapSelection[]>([]);
|
||||
const [activeHitId, setActiveHitId] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
fetchStations()
|
||||
@@ -183,47 +179,24 @@ export function Tgu2DMapTab({
|
||||
setLayers((current) => ({ ...current, [key]: !current[key] }));
|
||||
};
|
||||
|
||||
// Клик по карте: 0/1 объект — карточка сразу; ≥2 (наложение) — список под точкой клика.
|
||||
const handleObjectSelect = useCallback((selection?: Tgu2DMapSelection) => {
|
||||
if (selection?.type === "marker" && selection.layer === "stations") {
|
||||
setStationPanel(selection as Tgu2DMapSelection & { type: "marker"; layer: "stations" });
|
||||
setRequestsAtPoint([]);
|
||||
setSelectedRequestPolygonId(undefined);
|
||||
setHoveredRequestPolygonId(undefined);
|
||||
setSelectedObject(undefined);
|
||||
} else {
|
||||
setStationPanel(undefined);
|
||||
setSelectedObject(selection);
|
||||
}
|
||||
setMapHits(selection ? [selection] : []);
|
||||
setActiveHitId(selection?.id);
|
||||
}, []);
|
||||
|
||||
const handleRequestsAtPoint = useCallback(
|
||||
(polygons: MapPolygon[]) => {
|
||||
if (polygons.length === 0) {
|
||||
setRequestsAtPoint([]);
|
||||
setSelectedRequestPolygonId(undefined);
|
||||
setHoveredRequestPolygonId(undefined);
|
||||
return;
|
||||
}
|
||||
const joined = polygons.flatMap((polygon) => {
|
||||
const item = requestsState.items.find((r) => r.id === polygon.requestId);
|
||||
return item ? [{ polygon, item }] : [];
|
||||
});
|
||||
setRequestsAtPoint(joined);
|
||||
setSelectedRequestPolygonId(undefined);
|
||||
setHoveredRequestPolygonId(undefined);
|
||||
setStationPanel(undefined);
|
||||
},
|
||||
[requestsState.items]
|
||||
);
|
||||
|
||||
const closeRequestsPanel = useCallback(() => {
|
||||
setRequestsAtPoint([]);
|
||||
setSelectedRequestPolygonId(undefined);
|
||||
setHoveredRequestPolygonId(undefined);
|
||||
const handleObjectsAtPoint = useCallback((selections: Tgu2DMapSelection[]) => {
|
||||
setMapHits(selections);
|
||||
setActiveHitId(selections[0]?.id);
|
||||
}, []);
|
||||
|
||||
const closeStationPanel = useCallback(() => {
|
||||
setStationPanel(undefined);
|
||||
const toggleHit = useCallback((selection: Tgu2DMapSelection) => {
|
||||
setActiveHitId((current) => (current === selection.id ? undefined : selection.id));
|
||||
}, []);
|
||||
|
||||
const closeInfoPanel = useCallback(() => {
|
||||
setMapHits([]);
|
||||
setActiveHitId(undefined);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -236,7 +209,7 @@ export function Tgu2DMapTab({
|
||||
onToggleLayer={toggleLayer}
|
||||
/>
|
||||
|
||||
<div className={`tgu-map-content${(requestsAtPoint.length > 0 || stationPanel != null) ? " has-requests-panel" : ""}`}>
|
||||
<div className={`tgu-map-content${mapHits.length > 0 ? " has-side-panel" : ""}`}>
|
||||
<Tgu2DMapSidebar
|
||||
platforms={platforms}
|
||||
selectedPlanId={selectedPlan?.planId}
|
||||
@@ -260,23 +233,22 @@ export function Tgu2DMapTab({
|
||||
scene={mapInvalidRange ? { polygons: [], lines: [], markers: [] } : scene}
|
||||
layers={layers}
|
||||
redrawToken={redrawToken}
|
||||
enableObjectChooser
|
||||
onObjectsAtPoint={handleObjectsAtPoint}
|
||||
onObjectSelect={handleObjectSelect}
|
||||
onRequestsAtPoint={handleRequestsAtPoint}
|
||||
hoveredRequestPolygonId={hoveredRequestPolygonId}
|
||||
selectedRequestPolygonId={selectedRequestPolygonId}
|
||||
selectedObjectId={activeHitId}
|
||||
/>
|
||||
</section>
|
||||
{requestsAtPoint.length > 0 && (
|
||||
<Tgu2DMapRequestsPanel
|
||||
requests={requestsAtPoint}
|
||||
selectedPolygonId={selectedRequestPolygonId}
|
||||
onHover={setHoveredRequestPolygonId}
|
||||
onSelect={setSelectedRequestPolygonId}
|
||||
onClose={closeRequestsPanel}
|
||||
/>
|
||||
)}
|
||||
{stationPanel != null && (
|
||||
<Tgu2DMapStationPanel selection={stationPanel} onClose={closeStationPanel} />
|
||||
{mapHits.length > 0 && (
|
||||
<aside className="tgu-editor-side-panel">
|
||||
<MapObjectInspector
|
||||
hits={mapHits}
|
||||
activeId={activeHitId}
|
||||
onToggle={toggleHit}
|
||||
onClose={closeInfoPanel}
|
||||
requestItems={requestsState.items}
|
||||
/>
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -290,7 +262,7 @@ export function Tgu2DMapTab({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Tgu2DMapDetails selectedPlan={selectedPlan} selectedObject={selectedObject} />
|
||||
<Tgu2DMapDetails selectedPlan={selectedPlan} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { RequestAtPoint } from "../tgu-map-2d/Tgu2DMapRequestsPanel";
|
||||
import type { RequestMapItem } from "../../model/requestTypes";
|
||||
import type { MapPolygon } from "../tgu-map-2d/model/mapTypes";
|
||||
import { formatLocalDateTime, parseLocal } from "../../utils/localTime";
|
||||
|
||||
/** Заявка под точкой клика на карте: полигон-проекция заявки и сама заявка. */
|
||||
export type RequestAtPoint = { polygon: MapPolygon; item: RequestMapItem };
|
||||
|
||||
type Props = {
|
||||
requests: RequestAtPoint[];
|
||||
selectedPolygonId?: string;
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { RequestMapItem } from "../../model/requestTypes";
|
||||
import type { TguPlatformUi, TimelineRange } from "../../model/timelineTypes";
|
||||
import { buildTgu2DMapScene } from "../tgu-map-2d/Tgu2DMapLayers";
|
||||
import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
|
||||
import type { RequestAtPoint } from "../tgu-map-2d/Tgu2DMapRequestsPanel";
|
||||
import type { RequestAtPoint } from "./RequestDetailsPanel";
|
||||
import type { Tgu2DMapSelection } from "../tgu-map-2d/model/mapSelectionTypes";
|
||||
import type { GeoPoint, MapPolygon } from "../tgu-map-2d/model/mapTypes";
|
||||
import { generateUuid } from "../../utils/uuid";
|
||||
|
||||
@@ -1161,6 +1161,8 @@
|
||||
.tgu-editor-pick__swatch--swath { background: #6fb7ff; }
|
||||
.tgu-editor-pick__swatch--complexPlan { background: #c080ff; }
|
||||
.tgu-editor-pick__swatch--heatmap { background: #f5803e; }
|
||||
.tgu-editor-pick__swatch--tracks { background: #8d99a6; }
|
||||
.tgu-editor-pick__swatch--spacecraftMarkers { background: #d5e2e2; }
|
||||
|
||||
.tgu-editor-pick__title {
|
||||
font-size: 0.74rem;
|
||||
|
||||
Reference in New Issue
Block a user