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:
@@ -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}>
|
||||||
</button>
|
Показать все ({hiddenCount} скрыто)
|
||||||
|
</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,45 +13,59 @@ 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
|
: undefined;
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const toggleLayer = (key: Tgu2DMapLayerKey) => {
|
const toggleLayer = (key: Tgu2DMapLayerKey) => {
|
||||||
setLayers((current) => ({ ...current, [key]: !current[key] }));
|
setLayers((current) => ({ ...current, [key]: !current[key] }));
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user