refactor(tgu-ui): единый инспектор объектов карты (MapObjectInspector) на вкладках «Карта» и «Комплексный план»

Обобщил ComplexInfoPanel в общий MapObjectInspector (tgu-map-2d): карточки для
всех типов объектов — заявка, включение комплексного плана, ячейка тепловой карты,
маршрут planWorks, станция, маркер КА, линия; список с раскрытием при наложении.

Вкладка «Карта» переведена на enableObjectChooser + onObjectsAtPoint + правый
инспектор — заменила разрозненные Tgu2DMapRequestsPanel/Tgu2DMapStationPanel (удалены)
и объектную часть нижней плашки (Tgu2DMapDetails ужат до контекста плана). Вкладка
«Комплексный план» использует тот же инспектор.

RequestAtPoint переехал в tgu-requests/RequestDetailsPanel (нужен вкладке «Заявки»).
This commit is contained in:
Дмитрий Соловьев
2026-06-10 15:58:55 +03:00
parent bdbc874f41
commit 7202f7cb02
9 changed files with 164 additions and 270 deletions
@@ -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}
@@ -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";