feat(module-studio): add configurable map page
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
import { forwardRef, lazy, Suspense, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, type CSSProperties, type PointerEvent } from "react";
|
||||
import { Button, Checker, ColorField, ControlRow, GlassSurface, Icon, IconButton, Inspector, RangeControl, Window } from "@nodedc/ui-react";
|
||||
import type { MapCameraView, MapGatewayHealth, MapPresentation } from "./CesiumMapRenderer.js";
|
||||
import sceneFixture from "../../../registry/fixtures/map/map-operational-v0.1.json";
|
||||
|
||||
const CesiumMapRenderer = lazy(() => import("./CesiumMapRenderer.js").then((module) => ({ default: module.CesiumMapRenderer })));
|
||||
|
||||
type PreviewFeatures = { inspector?: boolean; toolbar?: boolean; assistant?: boolean };
|
||||
export type MapPageSettings = Omit<MapPresentation, "cacheRefresh">;
|
||||
type MapRuntimeConfig = { gatewayHealthUrl?: string | null; resourceProxyBase?: string | null };
|
||||
|
||||
export type MapPageLayout = {
|
||||
schemaVersion: 1;
|
||||
pageId: "map";
|
||||
settings: MapPageSettings;
|
||||
mapHeight: number;
|
||||
camera: MapCameraView;
|
||||
savedAt?: string;
|
||||
};
|
||||
|
||||
export type MapFixturePreviewHandle = {
|
||||
getLayout: () => MapPageLayout | null;
|
||||
};
|
||||
|
||||
const initialMapSettings: MapPageSettings = {
|
||||
imagerySource: "cesium-live",
|
||||
imageryVisible: true,
|
||||
cacheEnabled: true,
|
||||
terrainEnabled: true,
|
||||
terrainExaggeration: 1,
|
||||
monochrome: false,
|
||||
monochromeColor: "#15151b",
|
||||
imageryGamma: 100,
|
||||
imageryHue: 0,
|
||||
imageryAlpha: 100,
|
||||
globeColor: "#15151b",
|
||||
backgroundColor: "#08090d",
|
||||
atmosphereEnabled: false,
|
||||
atmosphereHue: 0,
|
||||
atmosphereSaturation: 0,
|
||||
atmosphereBrightness: 0,
|
||||
fogEnabled: true,
|
||||
fogDensity: 2,
|
||||
sunEnabled: true,
|
||||
sunHour: 12,
|
||||
sunIntensity: 200,
|
||||
shadowsEnabled: true,
|
||||
buildingsVisible: true,
|
||||
buildingsColor: "#a27aff",
|
||||
buildingsOpacity: 0.82,
|
||||
buildingsDetail: 16,
|
||||
imageryBrightness: 100,
|
||||
imageryContrast: 100,
|
||||
imagerySaturation: 100,
|
||||
gridVisible: true,
|
||||
gridLodEnabled: true,
|
||||
gridHeightMeters: 500,
|
||||
gridLod1MaxHeightKm: 10,
|
||||
gridLod1StepKm: 1,
|
||||
gridLod2MaxHeightKm: 50,
|
||||
gridLod2StepKm: 5,
|
||||
gridLod3StepKm: 25,
|
||||
gridRadiusKm: 40,
|
||||
gridLineWidth: 4,
|
||||
gridColor: "#f5f5f5",
|
||||
gridOpacity: 12,
|
||||
gridDotsEnabled: true,
|
||||
gridDotsSize: 7,
|
||||
gridDotsColor: "#ffffff",
|
||||
gridDotsOpacity: 58,
|
||||
};
|
||||
|
||||
// A valid, deterministic scene view is available before Cesium emits its
|
||||
// first move-end event. It makes the page contract immediately saveable;
|
||||
// the renderer replaces it with the exact live camera as soon as it is ready.
|
||||
const fallbackMapCamera: MapCameraView = {
|
||||
longitude: sceneFixture.viewport.center[0],
|
||||
latitude: sceneFixture.viewport.center[1],
|
||||
height: sceneFixture.viewport.range,
|
||||
heading: (sceneFixture.viewport.heading * Math.PI) / 180,
|
||||
pitch: (sceneFixture.viewport.pitch * Math.PI) / 180,
|
||||
roll: 0,
|
||||
};
|
||||
|
||||
export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
features?: PreviewFeatures;
|
||||
expanded?: boolean;
|
||||
initialLayout?: MapPageLayout | null;
|
||||
}>(function MapFixturePreview({ features = { inspector: true, toolbar: true, assistant: false }, expanded = false, initialLayout = null }, ref) {
|
||||
const selectable = useMemo(() => [
|
||||
...sceneFixture.scene.movingObjects.map((entity) => ({ id: entity.id, title: entity.label.text, kind: entity.objectType, status: entity.status })),
|
||||
...sceneFixture.scene.stations.map((entity) => ({ id: entity.id, title: entity.label.text, kind: `${entity.stationType} station`, status: undefined })),
|
||||
], []);
|
||||
const [selectedId, setSelectedId] = useState(sceneFixture.selection.entityId ?? selectable[0]?.id);
|
||||
const [inspectorOpen, setInspectorOpen] = useState(false);
|
||||
const [layersOpen, setLayersOpen] = useState(false);
|
||||
const [toolbarOpen, setToolbarOpen] = useState(Boolean(features.toolbar));
|
||||
const [assistantOpen, setAssistantOpen] = useState(false);
|
||||
const [mapSettings, setMapSettings] = useState<MapPageSettings>(() => ({ ...initialMapSettings, ...initialLayout?.settings }));
|
||||
const [mapHeight, setMapHeight] = useState(() => initialLayout?.mapHeight ?? (expanded ? 620 : 470));
|
||||
const [mapCamera, setMapCamera] = useState<MapCameraView>(initialLayout?.camera ?? fallbackMapCamera);
|
||||
// The header Save action can be pressed immediately after Cesium finishes
|
||||
// constructing the scene. Keep the last camera synchronously as well as in
|
||||
// state, so the imperative page-layout contract never waits for React's
|
||||
// render cycle to publish a ready camera.
|
||||
const mapCameraRef = useRef<MapCameraView>(initialLayout?.camera ?? fallbackMapCamera);
|
||||
const [rendererRevision, setRendererRevision] = useState(0);
|
||||
const [gatewayHealth, setGatewayHealth] = useState<MapGatewayHealth | null>(null);
|
||||
const [gatewayEndpoint, setGatewayEndpoint] = useState<string | null>(null);
|
||||
const [gatewayCheckState, setGatewayCheckState] = useState<"idle" | "checking" | "ready" | "error">("idle");
|
||||
const [gatewayCheckError, setGatewayCheckError] = useState<string | null>(null);
|
||||
const selected = selectable.find((entity) => entity.id === selectedId) ?? selectable[0];
|
||||
const presentation: MapPresentation = { ...mapSettings, cacheRefresh: false };
|
||||
const updateMapSettings = (patch: Partial<MapPageSettings>) => setMapSettings((current) => ({ ...current, ...patch }));
|
||||
const setCacheEnabled = (cacheEnabled: boolean) => {
|
||||
updateMapSettings({ cacheEnabled });
|
||||
setRendererRevision((value) => value + 1);
|
||||
};
|
||||
|
||||
const handleCameraChange = useCallback((camera: MapCameraView) => {
|
||||
mapCameraRef.current = camera;
|
||||
setMapCamera(camera);
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getLayout: () => ({
|
||||
schemaVersion: 1,
|
||||
pageId: "map",
|
||||
settings: mapSettings,
|
||||
mapHeight: Math.round(mapHeight),
|
||||
camera: mapCameraRef.current ?? mapCamera,
|
||||
}),
|
||||
}), [mapCamera, mapHeight, mapSettings]);
|
||||
|
||||
const handleSelect = useCallback((entityId: string) => {
|
||||
if (!selectable.some((entity) => entity.id === entityId)) return;
|
||||
setSelectedId(entityId);
|
||||
}, [selectable]);
|
||||
|
||||
const verifyGateway = useCallback(async () => {
|
||||
setGatewayCheckState("checking");
|
||||
setGatewayCheckError(null);
|
||||
try {
|
||||
const runtimeResponse = await fetch("/api/map/runtime-config");
|
||||
const runtime = runtimeResponse.ok ? await runtimeResponse.json() as MapRuntimeConfig : null;
|
||||
if (!runtime?.gatewayHealthUrl) throw new Error("gateway_not_configured");
|
||||
const healthResponse = await fetch(runtime.gatewayHealthUrl);
|
||||
if (!healthResponse.ok) throw new Error("gateway_not_ready");
|
||||
const health = await healthResponse.json() as MapGatewayHealth;
|
||||
if (health.cache?.persistent !== true) throw new Error("persistent_cache_unavailable");
|
||||
setGatewayHealth(health);
|
||||
setGatewayEndpoint(new URL(runtime.gatewayHealthUrl).origin);
|
||||
setGatewayCheckState("ready");
|
||||
} catch (error) {
|
||||
const code = error instanceof Error ? error.message : "gateway_not_ready";
|
||||
setGatewayHealth(null);
|
||||
setGatewayEndpoint(null);
|
||||
setGatewayCheckError(code === "persistent_cache_unavailable"
|
||||
? "Persistent TileCache не подключён: карта не должна продолжать работу с локальной временной папкой."
|
||||
: "Platform Map Gateway недоступен: проверьте runtime profile и persistent volume.");
|
||||
setGatewayCheckState("error");
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inspectorOpen && !layersOpen) return;
|
||||
void verifyGateway();
|
||||
const interval = window.setInterval(() => void verifyGateway(), 5000);
|
||||
return () => window.clearInterval(interval);
|
||||
}, [inspectorOpen, layersOpen, verifyGateway]);
|
||||
|
||||
const liveCacheStatus = gatewayHealth?.cache;
|
||||
const liveCacheSummary = liveCacheStatus
|
||||
? `${liveCacheStatus.entries ?? 0} объектов · ${Math.round((liveCacheStatus.bytes ?? 0) / 1024 / 1024)} MB`
|
||||
: "индекс ещё не получен";
|
||||
|
||||
const startResize = (event: PointerEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
const startY = event.clientY;
|
||||
const startHeight = mapHeight;
|
||||
const onMove = (move: globalThis.PointerEvent) => {
|
||||
const maximum = Math.max(380, Math.round(window.innerHeight * 0.78));
|
||||
setMapHeight(Math.max(360, Math.min(maximum, startHeight + move.clientY - startY)));
|
||||
};
|
||||
const onEnd = () => {
|
||||
window.removeEventListener("pointermove", onMove);
|
||||
window.removeEventListener("pointerup", onEnd);
|
||||
window.removeEventListener("pointercancel", onEnd);
|
||||
};
|
||||
window.addEventListener("pointermove", onMove);
|
||||
window.addEventListener("pointerup", onEnd);
|
||||
window.addEventListener("pointercancel", onEnd);
|
||||
};
|
||||
|
||||
const inspectorSections = [
|
||||
{
|
||||
id: "map-base",
|
||||
label: "Подложка и terrain",
|
||||
description: "provider-neutral surface",
|
||||
group: "Карта",
|
||||
content: <>
|
||||
<ControlRow label="Подложка"><strong>Cesium World Imagery</strong></ControlRow>
|
||||
<small className="catalog-map-inspector__note">Текущий официальный provider. Другие provider-слои появятся только после отдельного asset-контракта Platform.</small>
|
||||
<Checker checked={mapSettings.terrainEnabled} label="Terrain" description="Рельеф — отдельный слой под imagery." onChange={(terrainEnabled) => updateMapSettings({ terrainEnabled })} />
|
||||
<RangeControl label="Вертикальное преувеличение рельефа" value={mapSettings.terrainExaggeration * 100} min={25} max={300} formatValue={(value) => `${(value / 100).toFixed(2)}×`} onChange={(value) => updateMapSettings({ terrainExaggeration: value / 100 })} />
|
||||
<Checker checked={mapSettings.monochrome} label="Монохромная поверхность" onChange={(monochrome) => updateMapSettings({ monochrome })} />
|
||||
<ControlRow label="Цвет монохрома"><ColorField label="Цвет монохромной поверхности" value={mapSettings.monochromeColor} onChange={(monochromeColor) => updateMapSettings({ monochromeColor })} /></ControlRow>
|
||||
<RangeControl label="Яркость" value={mapSettings.imageryBrightness} min={0} max={200} formatValue={(value) => `${value}%`} onChange={(imageryBrightness) => updateMapSettings({ imageryBrightness })} />
|
||||
<RangeControl label="Контраст" value={mapSettings.imageryContrast} min={0} max={200} formatValue={(value) => `${value}%`} onChange={(imageryContrast) => updateMapSettings({ imageryContrast })} />
|
||||
<RangeControl label="Насыщенность" value={mapSettings.imagerySaturation} min={0} max={200} formatValue={(value) => `${value}%`} onChange={(imagerySaturation) => updateMapSettings({ imagerySaturation })} />
|
||||
<RangeControl label="Гамма" value={mapSettings.imageryGamma} min={0} max={300} formatValue={(value) => `${value}%`} onChange={(imageryGamma) => updateMapSettings({ imageryGamma })} />
|
||||
<RangeControl label="Оттенок" value={mapSettings.imageryHue} min={-180} max={180} formatValue={(value) => `${value}°`} onChange={(imageryHue) => updateMapSettings({ imageryHue })} />
|
||||
<RangeControl label="Прозрачность imagery" value={mapSettings.imageryAlpha} min={0} max={100} formatValue={(value) => `${value}%`} onChange={(imageryAlpha) => updateMapSettings({ imageryAlpha })} />
|
||||
<ControlRow label="Цвет планеты"><ColorField label="Цвет terrain без imagery" value={mapSettings.globeColor} onChange={(globeColor) => updateMapSettings({ globeColor })} /></ControlRow>
|
||||
<ControlRow label="Фон сцены"><ColorField label="Цвет фона сцены" value={mapSettings.backgroundColor} onChange={(backgroundColor) => updateMapSettings({ backgroundColor })} /></ControlRow>
|
||||
</>,
|
||||
},
|
||||
{
|
||||
id: "map-atmosphere",
|
||||
label: "Атмосфера и освещение",
|
||||
description: "scene / color correction",
|
||||
group: "Карта",
|
||||
content: <>
|
||||
<Checker checked={mapSettings.atmosphereEnabled} label="Показывать атмосферу" onChange={(atmosphereEnabled) => updateMapSettings({ atmosphereEnabled })} />
|
||||
<RangeControl label="Атмосфера: оттенок" value={mapSettings.atmosphereHue} min={-100} max={100} formatValue={(value) => `${value}%`} onChange={(atmosphereHue) => updateMapSettings({ atmosphereHue })} />
|
||||
<RangeControl label="Атмосфера: насыщенность" value={mapSettings.atmosphereSaturation} min={-100} max={100} formatValue={(value) => `${value}%`} onChange={(atmosphereSaturation) => updateMapSettings({ atmosphereSaturation })} />
|
||||
<RangeControl label="Атмосфера: яркость" value={mapSettings.atmosphereBrightness} min={-100} max={100} formatValue={(value) => `${value}%`} onChange={(atmosphereBrightness) => updateMapSettings({ atmosphereBrightness })} />
|
||||
<Checker checked={mapSettings.fogEnabled} label="Туман" onChange={(fogEnabled) => updateMapSettings({ fogEnabled })} />
|
||||
<RangeControl label="Плотность тумана" value={mapSettings.fogDensity} min={0} max={100} formatValue={(value) => `${(value / 10000).toFixed(4)}`} onChange={(fogDensity) => updateMapSettings({ fogDensity })} />
|
||||
<Checker checked={mapSettings.sunEnabled} label="Солнечное освещение" onChange={(sunEnabled) => updateMapSettings({ sunEnabled })} />
|
||||
<RangeControl label="Час солнца" value={mapSettings.sunHour} min={0} max={24} formatValue={(value) => `${value}:00 UTC`} onChange={(sunHour) => updateMapSettings({ sunHour })} />
|
||||
<RangeControl label="Интенсивность света" value={mapSettings.sunIntensity} min={0} max={200} formatValue={(value) => `${value}%`} onChange={(sunIntensity) => updateMapSettings({ sunIntensity })} />
|
||||
<Checker checked={mapSettings.shadowsEnabled} label="Тени" onChange={(shadowsEnabled) => updateMapSettings({ shadowsEnabled })} />
|
||||
</>,
|
||||
},
|
||||
{
|
||||
id: "map-buildings",
|
||||
label: "3D здания",
|
||||
description: "3D Tiles / detail",
|
||||
group: "Карта",
|
||||
content: <>
|
||||
<Checker checked={mapSettings.buildingsVisible} label="Показывать 3D здания" onChange={(buildingsVisible) => updateMapSettings({ buildingsVisible })} />
|
||||
<ControlRow label="Цвет"><ColorField label="Цвет зданий" value={mapSettings.buildingsColor} onChange={(buildingsColor) => updateMapSettings({ buildingsColor })} /></ControlRow>
|
||||
<RangeControl label="Прозрачность" value={Math.round(mapSettings.buildingsOpacity * 100)} min={0} max={100} formatValue={(value) => `${value}%`} onChange={(value) => updateMapSettings({ buildingsOpacity: value / 100 })} />
|
||||
<RangeControl label="Детализация" value={mapSettings.buildingsDetail} min={4} max={32} formatValue={(value) => `SSE ${value}`} onChange={(buildingsDetail) => updateMapSettings({ buildingsDetail })} />
|
||||
</>,
|
||||
},
|
||||
{
|
||||
id: "map-grid",
|
||||
label: "Сетка и LOD",
|
||||
description: "first adapter control",
|
||||
group: "Слои",
|
||||
content: <>
|
||||
<Checker checked={mapSettings.gridVisible} label="3D-сетка" description="Сетка размещается над поверхностью и меняет шаг по высоте камеры." onChange={(gridVisible) => updateMapSettings({ gridVisible })} />
|
||||
<Checker checked={mapSettings.gridLodEnabled} label="LOD по высоте камеры" onChange={(gridLodEnabled) => updateMapSettings({ gridLodEnabled })} />
|
||||
<RangeControl label="Высота над поверхностью" value={mapSettings.gridHeightMeters} min={0} max={1000} formatValue={(value) => `${value} м`} onChange={(gridHeightMeters) => updateMapSettings({ gridHeightMeters })} />
|
||||
<RangeControl label="LOD 1: до высоты" value={mapSettings.gridLod1MaxHeightKm} min={1} max={50} formatValue={(value) => `${value} км`} onChange={(gridLod1MaxHeightKm) => updateMapSettings({ gridLod1MaxHeightKm })} />
|
||||
<RangeControl label="LOD 1: шаг" value={mapSettings.gridLod1StepKm} min={1} max={10} formatValue={(value) => `${value} км`} onChange={(gridLod1StepKm) => updateMapSettings({ gridLod1StepKm })} />
|
||||
<RangeControl label="LOD 2: до высоты" value={mapSettings.gridLod2MaxHeightKm} min={10} max={200} formatValue={(value) => `${value} км`} onChange={(gridLod2MaxHeightKm) => updateMapSettings({ gridLod2MaxHeightKm })} />
|
||||
<RangeControl label="LOD 2: шаг" value={mapSettings.gridLod2StepKm} min={1} max={25} formatValue={(value) => `${value} км`} onChange={(gridLod2StepKm) => updateMapSettings({ gridLod2StepKm })} />
|
||||
<RangeControl label="LOD 3: шаг" value={mapSettings.gridLod3StepKm} min={5} max={100} formatValue={(value) => `${value} км`} onChange={(gridLod3StepKm) => updateMapSettings({ gridLod3StepKm })} />
|
||||
<RangeControl label="Радиус сетки" value={mapSettings.gridRadiusKm} min={5} max={150} formatValue={(value) => `${value} км`} onChange={(gridRadiusKm) => updateMapSettings({ gridRadiusKm })} />
|
||||
<ControlRow label="Цвет линий"><ColorField label="Цвет линий сетки" value={mapSettings.gridColor} onChange={(gridColor) => updateMapSettings({ gridColor })} /></ControlRow>
|
||||
<RangeControl label="Толщина линий" value={mapSettings.gridLineWidth} min={1} max={8} formatValue={(value) => `${value} px`} onChange={(gridLineWidth) => updateMapSettings({ gridLineWidth })} />
|
||||
<RangeControl label="Прозрачность сетки" value={mapSettings.gridOpacity} min={0} max={100} formatValue={(value) => `${value}%`} onChange={(gridOpacity) => updateMapSettings({ gridOpacity })} />
|
||||
<Checker checked={mapSettings.gridDotsEnabled} label="Точки в пересечениях" onChange={(gridDotsEnabled) => updateMapSettings({ gridDotsEnabled })} />
|
||||
<RangeControl label="Размер точки" value={mapSettings.gridDotsSize} min={2} max={28} formatValue={(value) => `${value} px`} onChange={(gridDotsSize) => updateMapSettings({ gridDotsSize })} />
|
||||
<ControlRow label="Цвет точек"><ColorField label="Цвет точек сетки" value={mapSettings.gridDotsColor} onChange={(gridDotsColor) => updateMapSettings({ gridDotsColor })} /></ControlRow>
|
||||
<RangeControl label="Прозрачность точек" value={mapSettings.gridDotsOpacity} min={0} max={100} formatValue={(value) => `${value}%`} onChange={(gridDotsOpacity) => updateMapSettings({ gridDotsOpacity })} />
|
||||
</>,
|
||||
},
|
||||
{
|
||||
id: "map-cache",
|
||||
label: "TileCache",
|
||||
description: "Platform Map Gateway",
|
||||
group: "Хранение",
|
||||
content: <>
|
||||
<small className="catalog-map-inspector__note">По умолчанию — официальный live-маршрут. Включите запись, чтобы одновременно смотреть карту и пополнять серверный cache.</small>
|
||||
<Checker className="catalog-map-inspector__cache-toggle" checked={mapSettings.cacheEnabled} label="Кэшировать live-данные" onChange={setCacheEnabled} />
|
||||
<ControlRow label="Режим"><strong>{mapSettings.cacheEnabled ? "Live + Cache" : "Live"}</strong></ControlRow>
|
||||
<ControlRow label="Хранилище"><strong>Platform Map Gateway</strong></ControlRow>
|
||||
<ControlRow label="Подключение"><span>{gatewayEndpoint ?? "runtime profile · не проверено"}</span></ControlRow>
|
||||
<ControlRow label="Записано"><strong>{liveCacheSummary}</strong></ControlRow>
|
||||
<Button variant="secondary" shape="pill" icon={<Icon name="refresh" />} onClick={() => void verifyGateway()} disabled={gatewayCheckState === "checking"}>{gatewayCheckState === "checking" ? "Проверяем Gateway…" : "Проверить подключение"}</Button>
|
||||
<small className="catalog-map-inspector__note">Live Cache: {liveCacheSummary} · {gatewayHealth?.cache?.mode ?? "проверяется"}</small>
|
||||
<small className="catalog-map-inspector__note">{mapSettings.cacheEnabled ? "Hybrid: provider остаётся официальным, новые tiles сохраняются в серверный cache." : "Real-time: provider остаётся официальным, чтение и запись persistent cache выключены."}</small>
|
||||
{gatewayCheckState === "error" ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{gatewayCheckError}</small> : null}
|
||||
</>,
|
||||
},
|
||||
{
|
||||
id: "map-selection",
|
||||
label: "Выбранная сущность",
|
||||
description: "selection contract",
|
||||
group: "Данные",
|
||||
content: <>
|
||||
<ControlRow label="Сущность"><strong>{selected?.title ?? "Нет выбора"}</strong></ControlRow>
|
||||
<ControlRow label="Тип"><span>{selected?.kind ?? "—"}{selected?.status ? ` · ${selected.status}` : ""}</span></ControlRow>
|
||||
</>,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`catalog-map-fixture${expanded ? " catalog-map-fixture--expanded" : ""}`}
|
||||
style={{ "--catalog-map-height": `${mapHeight}px` } as CSSProperties}
|
||||
aria-label="Map Page Cesium adapter"
|
||||
>
|
||||
<Suspense fallback={<div className="catalog-map-fixture__loading">Загрузка карты…</div>}>
|
||||
<CesiumMapRenderer key={rendererRevision} onSelect={handleSelect} onGatewayHealth={setGatewayHealth} onCameraChange={handleCameraChange} initialCamera={mapCamera ?? undefined} presentation={presentation} />
|
||||
</Suspense>
|
||||
|
||||
<div className="catalog-map-fixture__actions">
|
||||
<IconButton label="Настройки карты" aria-pressed={inspectorOpen} data-active={inspectorOpen || undefined} onClick={() => setInspectorOpen(true)}><Icon name="settings" /></IconButton>
|
||||
<IconButton label="Слои карты" aria-pressed={layersOpen} data-active={layersOpen || undefined} onClick={() => setLayersOpen((value) => !value)}><Icon name="grid" /></IconButton>
|
||||
{features.toolbar ? <IconButton label="Toolbar" aria-pressed={toolbarOpen} data-active={toolbarOpen || undefined} onClick={() => setToolbarOpen((value) => !value)}><Icon name="panel" /></IconButton> : null}
|
||||
{features.assistant ? <IconButton label="Assistant" aria-pressed={assistantOpen} data-active={assistantOpen || undefined} onClick={() => setAssistantOpen((value) => !value)}><Icon name="apps" /></IconButton> : null}
|
||||
</div>
|
||||
|
||||
{layersOpen ? (
|
||||
<GlassSurface className="catalog-map-fixture__layers" tone="strong" radius="card" padding="sm" aria-label="Настройки слоёв карты">
|
||||
<div className="catalog-map-fixture__layers-head"><strong>Слои карты</strong><IconButton label="Закрыть слои" onClick={() => setLayersOpen(false)}><Icon name="close" /></IconButton></div>
|
||||
<div className="catalog-map-fixture__provider"><strong>Cesium World Imagery</strong><small>официальный live provider</small></div>
|
||||
<Checker checked={mapSettings.terrainEnabled} label="Terrain" onChange={(terrainEnabled) => updateMapSettings({ terrainEnabled })} />
|
||||
<Checker checked={mapSettings.buildingsVisible} label="3D здания" onChange={(buildingsVisible) => updateMapSettings({ buildingsVisible })} />
|
||||
<Checker checked={mapSettings.gridVisible} label="Планетарная сетка" onChange={(gridVisible) => updateMapSettings({ gridVisible })} />
|
||||
<small className="catalog-map-inspector__note">Live Cache: {liveCacheSummary}</small>
|
||||
<Checker className="catalog-map-fixture__cache-toggle" checked={mapSettings.cacheEnabled} label="Кэшировать live-данные" onChange={setCacheEnabled} />
|
||||
</GlassSurface>
|
||||
) : null}
|
||||
|
||||
{toolbarOpen ? (
|
||||
<div className="catalog-map-fixture__toolbar" aria-label="Map toolbar">
|
||||
<IconButton label="Обзор"><Icon name="globe" /></IconButton>
|
||||
<IconButton label="Поиск"><Icon name="search" /></IconButton>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{assistantOpen ? <div className="catalog-map-fixture__assistant"><strong>NODE.DC Assistant</strong><span>Контекст выбранной сущности готов к передаче.</span></div> : null}
|
||||
<button type="button" className="catalog-map-fixture__resize" aria-label="Изменить высоту карты" onPointerDown={startResize}><span /></button>
|
||||
|
||||
<Window
|
||||
open={inspectorOpen && Boolean(features.inspector)}
|
||||
title="Настройки карты"
|
||||
subtitle="MAP / draggable inspector"
|
||||
placement="end"
|
||||
draggable
|
||||
closeOnBackdrop={false}
|
||||
lockBodyScroll={false}
|
||||
trapFocus={false}
|
||||
onClose={() => setInspectorOpen(false)}
|
||||
>
|
||||
<Inspector sections={inspectorSections} defaultOpen={["map-base"]} singleOpen />
|
||||
</Window>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user