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
@@ -3,6 +3,7 @@ import { drawBaseMap } from "./canvas/drawBaseMap";
import { drawPlanWorks } from "./canvas/drawPlanWorks";
import { drawStations } from "./canvas/drawStations";
import { drawSwath } from "./canvas/drawSwath";
import { drawTiles } from "./canvas/drawTiles";
import { drawTracks } from "./canvas/drawTracks";
import { createMapProjection } from "./geometry/mapProjection";
import { createPolygonSpatialIndex, hitTestPolygons, queryPolygonsByBBox } from "./geometry/spatialIndex";
@@ -64,6 +65,8 @@ export function Tgu2DMapView({
const hoverRef = useRef<MapPolygon | undefined>(undefined);
const [size, setSize] = useState<MapSize>({ width: 1000, height: 560 });
const [view, setView] = useState<MapViewState>(INITIAL_VIEW);
const [tileVersion, setTileVersion] = useState(0);
const handleTileLoad = useCallback(() => setTileVersion((v) => v + 1), []);
const [tooltip, setTooltip] = useState<TooltipState>();
const selectedPolygonRef = useRef<MapPolygon | undefined>(undefined);
const activePolygons = useMemo(
@@ -125,7 +128,8 @@ export function Tgu2DMapView({
context.setTransform(dpr, 0, 0, dpr, 0, 0);
context.clearRect(0, 0, size.width, size.height);
drawBaseMap(context, projection, size);
drawTiles(context, view, projection, size, handleTileLoad);
drawBaseMap(context, projection);
if (layers.swath) {
drawSwath(context, visiblePolygons.filter((polygon) => polygon.layer === "swath"), projection, size);
@@ -142,7 +146,7 @@ export function Tgu2DMapView({
});
return () => window.cancelAnimationFrame(frameId);
}, [layers, projection, redrawToken, scene.lines, scene.markers, size, visiblePolygons]);
}, [handleTileLoad, layers, projection, redrawToken, scene.lines, scene.markers, size, tileVersion, view, visiblePolygons]);
const screenPointFromEvent = useCallback((event: React.PointerEvent | React.MouseEvent): ScreenPoint => {
const bounds = wrapperRef.current?.getBoundingClientRect();
@@ -307,7 +311,7 @@ export function Tgu2DMapView({
<span>{tooltip.subtitle}</span>
</div>
)}
<div className="tgu-2d-map__attribution">Схематичная основа · Web Mercator</div>
<div className="tgu-2d-map__attribution">© <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener noreferrer">OpenStreetMap</a> contributors · © <a href="https://carto.com/attributions" target="_blank" rel="noopener noreferrer">CARTO</a></div>
<div className="tgu-2d-map__zoom">
<button type="button" onClick={() => setView((current) => ({ ...current, zoom: Math.min(8, current.zoom + 0.5) }))}>+</button>
<button type="button" onClick={() => setView((current) => ({ ...current, zoom: Math.max(0.8, current.zoom - 0.5) }))}>-</button>