Update TGU 2D map spacecraft visibility controls

Switch the 2D map sidebar from single-spacecraft selection to per-spacecraft visibility toggles, add a show-all action, and update map tab labels and styles for the all-spacecraft view.
This commit is contained in:
Дмитрий Соловьев
2026-05-30 21:13:50 +03:00
parent 39f108e496
commit 3d508b6be9
6 changed files with 72 additions and 54 deletions
@@ -7,8 +7,8 @@ export type Tgu2DMapLayerAvailability = Partial<Record<keyof Tgu2DMapLayersState
export type Tgu2DMapSceneInput = { export type Tgu2DMapSceneInput = {
range: TimelineRange; range: TimelineRange;
selectedPlan?: TguPlanUi; selectedPlan?: TguPlanUi;
selectedSpacecraftId?: string;
platforms: TguPlatformUi[]; platforms: TguPlatformUi[];
hiddenSpacecraftIds?: ReadonlySet<string>;
}; };
const UNAVAILABLE = "Данные слоя недоступны в текущем API."; const UNAVAILABLE = "Данные слоя недоступны в текущем API.";
@@ -5,36 +5,41 @@ import type { Tgu2DMapLayerAvailability } from "./Tgu2DMapLayers";
type Tgu2DMapSidebarProps = { type Tgu2DMapSidebarProps = {
platforms: TguPlatformUi[]; platforms: TguPlatformUi[];
selectedSpacecraftId?: string;
selectedPlanId?: string; selectedPlanId?: string;
hiddenIds: ReadonlySet<string>;
layers: Tgu2DMapLayersState; layers: Tgu2DMapLayersState;
search: string; search: string;
layerAvailability: Tgu2DMapLayerAvailability; layerAvailability: Tgu2DMapLayerAvailability;
onSearchChange: (value: string) => void; onSearchChange: (value: string) => void;
onSelectSpacecraft: (spacecraftId?: string) => void; onToggleSpacecraft: (id: string) => void;
onShowAll: () => void;
onToggleLayer: (key: Tgu2DMapLayerKey) => void; onToggleLayer: (key: Tgu2DMapLayerKey) => void;
}; };
export function Tgu2DMapSidebar({ export function Tgu2DMapSidebar({
platforms, platforms,
selectedSpacecraftId,
selectedPlanId, selectedPlanId,
hiddenIds,
layers, layers,
search, search,
layerAvailability, layerAvailability,
onSearchChange, onSearchChange,
onSelectSpacecraft, onToggleSpacecraft,
onShowAll,
onToggleLayer onToggleLayer
}: Tgu2DMapSidebarProps) { }: Tgu2DMapSidebarProps) {
const filteredPlatforms = filterPlatforms(platforms, search); const filteredPlatforms = filterPlatforms(platforms, search);
const hiddenCount = hiddenIds.size;
return ( return (
<aside className="tgu-map-panel"> <aside className="tgu-map-panel">
<div className="tgu-panel-heading"> <div className="tgu-panel-heading">
<span>Карта 2D</span> <span>Карта 2D</span>
<button className="tgu-link-button" type="button" onClick={() => onSelectSpacecraft(undefined)}> {hiddenCount > 0 && (
Все <button className="tgu-link-button" type="button" onClick={onShowAll}>
Показать все ({hiddenCount} скрыто)
</button> </button>
)}
</div> </div>
<div className="tgu-map-panel__section"> <div className="tgu-map-panel__section">
<div className="tgu-map-panel__label">Выбранный план</div> <div className="tgu-map-panel__label">Выбранный план</div>
@@ -69,14 +74,17 @@ export function Tgu2DMapSidebar({
</div> </div>
<div className="tgu-map-spacecraft-list"> <div className="tgu-map-spacecraft-list">
{filteredPlatforms.map((platform) => { {filteredPlatforms.map((platform) => {
const selected = selectedSpacecraftId === platform.spacecraftId; const visible = !hiddenIds.has(platform.spacecraftId);
return ( return (
<button <label
className={`tgu-map-spacecraft ${selected ? "is-selected" : ""}`} className={`tgu-map-spacecraft ${visible ? "" : "is-hidden"}`}
type="button"
key={platform.spacecraftId} key={platform.spacecraftId}
onClick={() => onSelectSpacecraft(platform.spacecraftId)}
> >
<input
type="checkbox"
checked={visible}
onChange={() => onToggleSpacecraft(platform.spacecraftId)}
/>
<span> <span>
<span className="tgu-map-spacecraft__name">{platformLabel(platform, platform.spacecraftId)}</span> <span className="tgu-map-spacecraft__name">{platformLabel(platform, platform.spacecraftId)}</span>
<span className="tgu-map-spacecraft__meta"> <span className="tgu-map-spacecraft__meta">
@@ -85,7 +93,7 @@ export function Tgu2DMapSidebar({
</span> </span>
</span> </span>
<span className="tgu-spacecraft__count">{platform.planCount}</span> <span className="tgu-spacecraft__count">{platform.planCount}</span>
</button> </label>
); );
})} })}
{filteredPlatforms.length === 0 && <div className="tgu-empty tgu-empty--compact">Нет КА по текущему поиску.</div>} {filteredPlatforms.length === 0 && <div className="tgu-empty tgu-empty--compact">Нет КА по текущему поиску.</div>}
@@ -107,4 +115,3 @@ export function Tgu2DMapSidebar({
</aside> </aside>
); );
} }
@@ -1,4 +1,4 @@
import { useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import type { TguPlanUi, TguPlatformUi, TimelineRange } from "../../model/timelineTypes"; import type { TguPlanUi, TguPlatformUi, TimelineRange } from "../../model/timelineTypes";
import { platformLabel } from "../tgu-planning/tguTimelineMapper"; import { platformLabel } from "../tgu-planning/tguTimelineMapper";
import { DEFAULT_TGU_2D_MAP_LAYERS, type Tgu2DMapLayerKey, type Tgu2DMapLayersState } from "./model/mapLayerTypes"; import { DEFAULT_TGU_2D_MAP_LAYERS, type Tgu2DMapLayerKey, type Tgu2DMapLayersState } from "./model/mapLayerTypes";
@@ -13,41 +13,55 @@ import { getTgu2DMapIntervalState, TGU_2D_MAP_INTERVAL_WARNING } from "./tgu2DMa
type Tgu2DMapTabProps = { type Tgu2DMapTabProps = {
range: TimelineRange; range: TimelineRange;
invalidRange: boolean; invalidRange: boolean;
selectedSpacecraftId?: string;
selectedPlan?: TguPlanUi; selectedPlan?: TguPlanUi;
platforms: TguPlatformUi[]; platforms: TguPlatformUi[];
onSelectSpacecraft: (spacecraftId?: string) => void;
}; };
export function Tgu2DMapTab({ export function Tgu2DMapTab({
range, range,
invalidRange, invalidRange,
selectedSpacecraftId,
selectedPlan, selectedPlan,
platforms, platforms
onSelectSpacecraft
}: Tgu2DMapTabProps) { }: Tgu2DMapTabProps) {
const [layers, setLayers] = useState<Tgu2DMapLayersState>(DEFAULT_TGU_2D_MAP_LAYERS); const [layers, setLayers] = useState<Tgu2DMapLayersState>(DEFAULT_TGU_2D_MAP_LAYERS);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [redrawToken, setRedrawToken] = useState(0); const [redrawToken, setRedrawToken] = useState(0);
const [selectedObject, setSelectedObject] = useState<Tgu2DMapSelection>(); const [selectedObject, setSelectedObject] = useState<Tgu2DMapSelection>();
const satelliteId = selectedPlan?.spacecraftId ?? selectedSpacecraftId; const [hiddenIds, setHiddenIds] = useState<ReadonlySet<string>>(new Set());
const mapInterval = getTgu2DMapIntervalState(range, selectedPlan); const mapInterval = getTgu2DMapIntervalState(range, selectedPlan);
const mapInvalidRange = invalidRange || mapInterval.tooLarge; const mapInvalidRange = invalidRange || mapInterval.tooLarge;
const platform = useMemo(
() => platforms.find((item) => item.spacecraftId === satelliteId), const toggleSpacecraft = useCallback((id: string) => {
[platforms, satelliteId] setHiddenIds((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
}, []);
const showAll = useCallback(() => setHiddenIds(new Set()), []);
const planPlatform = useMemo(
() => selectedPlan ? platforms.find((p) => p.spacecraftId === selectedPlan.spacecraftId) : undefined,
[platforms, selectedPlan]
); );
const satelliteLabel = satelliteId ? platformLabel(platform, satelliteId) : "КА не выбран"; const satelliteLabel = selectedPlan
? platformLabel(planPlatform, selectedPlan.spacecraftId)
: hiddenIds.size > 0
? `Скрыто: ${hiddenIds.size} КА`
: "Все КА";
const intervalLabel = `${formatDate(mapInterval.range.fromMs)} - ${formatDate(mapInterval.range.toMs)}`; const intervalLabel = `${formatDate(mapInterval.range.fromMs)} - ${formatDate(mapInterval.range.toMs)}`;
const scene = useMemo( const scene = useMemo(
() => buildTgu2DMapScene({ range: mapInterval.range, selectedPlan, selectedSpacecraftId: satelliteId, platforms }), () => buildTgu2DMapScene({ range: mapInterval.range, selectedPlan, platforms, hiddenSpacecraftIds: hiddenIds }),
[mapInterval.range, platforms, satelliteId, selectedPlan] [mapInterval.range, platforms, selectedPlan, hiddenIds]
); );
const layerAvailability = useMemo(() => getLayerAvailability(layers, scene), [layers, scene]); const layerAvailability = useMemo(() => getLayerAvailability(layers, scene), [layers, scene]);
const overlayMessage = !satelliteId
? "Выберите КА в sidebar или план на timeline." const overlayMessage = invalidRange
: invalidRange
? "Некорректный интервал: from должен быть меньше to." ? "Некорректный интервал: from должен быть меньше to."
: mapInterval.tooLarge : mapInterval.tooLarge
? TGU_2D_MAP_INTERVAL_WARNING ? TGU_2D_MAP_INTERVAL_WARNING
@@ -70,13 +84,14 @@ export function Tgu2DMapTab({
<div className="tgu-map-content"> <div className="tgu-map-content">
<Tgu2DMapSidebar <Tgu2DMapSidebar
platforms={platforms} platforms={platforms}
selectedSpacecraftId={satelliteId}
selectedPlanId={selectedPlan?.planId} selectedPlanId={selectedPlan?.planId}
hiddenIds={hiddenIds}
layers={layers} layers={layers}
search={search} search={search}
layerAvailability={layerAvailability} layerAvailability={layerAvailability}
onSearchChange={setSearch} onSearchChange={setSearch}
onSelectSpacecraft={onSelectSpacecraft} onToggleSpacecraft={toggleSpacecraft}
onShowAll={showAll}
onToggleLayer={toggleLayer} onToggleLayer={toggleLayer}
/> />
<section className="tgu-map-stage"> <section className="tgu-map-stage">
@@ -128,4 +143,3 @@ function layerLabel(layer: Tgu2DMapLayerKey): string {
return "Маркеры КА"; return "Маркеры КА";
} }
} }
@@ -202,14 +202,8 @@ export function TguPlanningPage() {
<Tgu2DMapTab <Tgu2DMapTab
range={appliedRange} range={appliedRange}
invalidRange={invalidRange} invalidRange={invalidRange}
selectedSpacecraftId={selectedSpacecraftId}
selectedPlan={selectedPlan} selectedPlan={selectedPlan}
platforms={platforms} platforms={platforms}
onSelectSpacecraft={(spacecraftId) => {
setSelectedSpacecraftId(spacecraftId);
setSelectedPlanId(undefined);
setDecisionNotice(undefined);
}}
/> />
) : ( ) : (
<TguEditorTab <TguEditorTab
@@ -47,7 +47,7 @@ export function TguToolbar({
aria-selected={activeTab === "timeline"} aria-selected={activeTab === "timeline"}
onClick={() => onTabChange("timeline")} onClick={() => onTabChange("timeline")}
> >
Timeline Планы
</button> </button>
<button <button
className={activeTab === "map" ? "is-active" : ""} className={activeTab === "map" ? "is-active" : ""}
@@ -720,22 +720,25 @@
.tgu-map-spacecraft { .tgu-map-spacecraft {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: auto minmax(0, 1fr) auto;
gap: 0.55rem; gap: 0.55rem;
align-items: center; align-items: center;
width: 100%; border: 1px solid var(--line-soft);
border: 1px solid transparent;
border-radius: 6px; border-radius: 6px;
padding: 0.5rem 0.55rem; padding: 0.5rem 0.55rem;
background: transparent; background: #111416;
color: var(--text-muted); color: var(--text-muted);
text-align: left; cursor: pointer;
transition: opacity 0.15s;
} }
.tgu-map-spacecraft.is-selected { .tgu-map-spacecraft input {
border-color: rgba(104, 195, 189, 0.46); accent-color: var(--accent);
background: rgba(104, 195, 189, 0.12); flex-shrink: 0;
color: var(--text); }
.tgu-map-spacecraft.is-hidden {
opacity: 0.4;
} }
.tgu-map-spacecraft__name, .tgu-map-spacecraft__name,