feat(tgu-ops-ui): OSM tiles, editor layout fixes, map resizer

Карта:
- Заменить схематичные контуры континентов на тайлы CartoDB Dark
  (dark_nolabels, CC BY 3.0, без API-ключа) через модульный кеш
  tileLoader.ts + отрисовщик drawTiles.ts на существующей Web Mercator
  проекции — без новых зависимостей
- Сохранить сетку lat/lon 30° поверх тайлов (полезна для орбитального
  анализа), убрать нарисованные контуры и градиент океана
- Обновить атрибуцию: © OpenStreetMap contributors · © CARTO

Вкладка Редактор — раскладка:
- Исправить цепочку высот: workspace переведён с display:grid на
  display:flex + flex-direction:column; layout получает flex:1 — единственный
  надёжный способ пробросить definite height через несколько вложенных
  grid-контейнеров в Chrome
- Обернуть условные notice/applyError в постоянный div.tgu-editor-notices,
  чтобы .tgu-editor-layout всегда был третьим flex-ребёнком (раньше при
  отсутствии уведомлений попадал во вторую auto-строку grid и схлопывался)

Вкладка Редактор — карта:
- Добавить перетаскиваемый разделитель между картой и таймлайном:
  mapRatio state (по умолчанию 50/50), drag-хендлер через document events,
  ограничение 15–80%, визуальный индикатор .tgu-editor-map-resizer
- Ограничить окно наземной трассы 24ч (720 точек, шаг 2 мин) независимо
  от зума таймлайна — при широком диапазоне (7 суток) шаг был 38 мин,
  трасса выглядела как хаотическая сетка через весь глобус

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Дмитрий Соловьев
2026-05-30 16:30:53 +03:00
parent f084775cbb
commit 82eddedc92
6 changed files with 207 additions and 61 deletions
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState, type MutableRefObject } from "react";
import { useEffect, useMemo, useRef, useState, useCallback, type MutableRefObject } from "react";
import { groundTrack, stationPasses, targetPasses } from "../tgu-map-2d/geometry/passes";
import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
import type { Tgu2DMapLayersState } from "../tgu-map-2d/model/mapLayerTypes";
@@ -47,6 +47,8 @@ const EDITOR_MAP_LAYERS: Tgu2DMapLayersState = {
export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, platforms }: TguEditorTabProps) {
const idCounterRef = useRef(0);
const targetCounterRef = useRef(0);
const centerRef = useRef<HTMLElement>(null);
const [mapRatio, setMapRatio] = useState(0.5);
const [history, setHistory] = useState<DraftHistory>(() => ({
draft: seedDraft(selectedPlan, selectedSpacecraftId),
past: [],
@@ -213,6 +215,27 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
});
};
const handleResizerDown = useCallback((e: React.MouseEvent) => {
e.preventDefault();
const startY = e.clientY;
const startRatio = mapRatio;
const onMove = (ev: MouseEvent) => {
const el = centerRef.current;
if (!el) return;
const dy = ev.clientY - startY;
setMapRatio(Math.max(0.15, Math.min(0.80, startRatio + dy / el.clientHeight)));
};
const onUp = () => {
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
};
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
}, [mapRatio]);
if (!selectedSpacecraftId && !selectedPlan) {
return (
<main className="tgu-editor-workspace">
@@ -249,8 +272,10 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
onPan={pan}
/>
{notice && <div className="tgu-alert tgu-alert--success tgu-editor-notice">{notice}</div>}
{applyError && <div className="tgu-alert tgu-alert--danger tgu-editor-notice">{applyError}</div>}
<div className="tgu-editor-notices">
{notice && <div className="tgu-alert tgu-alert--success tgu-editor-notice">{notice}</div>}
{applyError && <div className="tgu-alert tgu-alert--danger tgu-editor-notice">{applyError}</div>}
</div>
<div className="tgu-editor-layout" style={{ gridTemplateColumns: rightMode === "none" ? "15.5rem minmax(0, 1fr) 0" : "15.5rem minmax(0, 1fr) 21rem" }}>
<EditorRail
@@ -273,7 +298,11 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
}}
/>
<section className="tgu-editor-center">
<section
className="tgu-editor-center"
ref={centerRef}
style={{ gridTemplateRows: `${(mapRatio * 100).toFixed(1)}% minmax(0, 1fr)` }}
>
<div className="tgu-editor-map-stage">
{rightMode === "shoot" && <div className="tgu-map-overlay">Кликните точку съёмки на карте.</div>}
<Tgu2DMapView
@@ -296,6 +325,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
}}
onObjectSelect={() => undefined}
/>
<div className="tgu-editor-map-resizer" onMouseDown={handleResizerDown} />
</div>
<EditorTimeline
activeLabel={activeLabel}
@@ -470,13 +500,21 @@ function buildVisibilityTracks(
return tracks;
}
const MAP_TRACK_WINDOW_MS = 24 * 60 * 60 * 1000;
const MAP_TRACK_POINTS = 720; // 2-minute step over 24h → ~45 points per LEO orbit
function buildEditorMapScene(
spacecraftId: string | undefined,
window: EditorTimelineWindow,
stations: EditorGroundStation[]
): Tgu2DMapScene {
// Always show 24h of ground track centred on the timeline window,
// regardless of how wide the timeline is zoomed out.
const center = (window.fromMs + window.toMs) / 2;
const mapRange = { fromMs: center - MAP_TRACK_WINDOW_MS / 2, toMs: center + MAP_TRACK_WINDOW_MS / 2 };
const lines = spacecraftId
? groundTrack({ spacecraftId }, { fromMs: window.fromMs, toMs: window.toMs }, 260).map((points, index) => ({
? groundTrack({ spacecraftId }, mapRange, MAP_TRACK_POINTS).map((points, index) => ({
id: `${spacecraftId}:track:${index}`,
layer: "tracks" as const,
points,