refactor(map): split foundry workspace and cesium runtime
This commit is contained in:
@@ -0,0 +1,428 @@
|
||||
import type { Dispatch, ReactNode, SetStateAction } from "react";
|
||||
import {
|
||||
Button,
|
||||
Checker,
|
||||
ColorField,
|
||||
ControlRow,
|
||||
Icon,
|
||||
InspectorSelectField,
|
||||
RangeControl,
|
||||
} from "@nodedc/ui-react";
|
||||
import type { SelectOption } from "@nodedc/ui-react";
|
||||
import {
|
||||
OSM_BUILDINGS_OBSERVED_BAND_COUNT,
|
||||
cameraSurveySpiralDistance,
|
||||
type CameraSurveySelection,
|
||||
} from "./mapCameraPresets.js";
|
||||
import {
|
||||
isMapReferencePresentationProfile,
|
||||
type MapReferenceLayer,
|
||||
} from "./mapReferenceStations.js";
|
||||
import type { MapPageSettings } from "./mapPageContract.js";
|
||||
import type { MapGatewayHealth, MapProviderStatus } from "./mapRendererContract.js";
|
||||
import type { MapPresentationProfile } from "./mapPresentationProfile.js";
|
||||
import type { MapSelectableEntity } from "./mapWorkspaceModel.mjs";
|
||||
|
||||
export type MapInspectorSection = {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
group: string;
|
||||
icon: ReactNode;
|
||||
content: ReactNode;
|
||||
};
|
||||
|
||||
type UpdateMapSettings = (patch: Partial<MapPageSettings>) => void;
|
||||
type UpdatePresentationProfile = (
|
||||
profileId: string,
|
||||
updater: (profile: MapPresentationProfile) => MapPresentationProfile,
|
||||
) => void;
|
||||
type UpdatePresentationStyle = (
|
||||
profileId: string,
|
||||
styleId: string,
|
||||
patch: Partial<MapPresentationProfile["styles"][number]>,
|
||||
) => void;
|
||||
type GatewayCheckState = "idle" | "checking" | "ready" | "stale" | "error";
|
||||
|
||||
const providerStateLabel: Record<MapProviderStatus["imagery"], string> = {
|
||||
loading: "загружается",
|
||||
ready: "готов",
|
||||
error: "недоступен",
|
||||
"not-configured": "не настроен",
|
||||
};
|
||||
|
||||
const logarithmicControlValue = (value: number) => Math.log10(Math.max(Number.MIN_VALUE, value));
|
||||
const valueFromLogarithmicControl = (value: number) => Math.max(1, Math.round(10 ** value));
|
||||
|
||||
export const formatMetricDistance = (value: number) => value >= 1000
|
||||
? `${(value / 1000).toLocaleString("ru-RU", { maximumFractionDigits: value >= 10_000 ? 0 : 1 })} км`
|
||||
: `${Math.round(value)} м`;
|
||||
|
||||
const formatMetricSpeed = (value: number) => value >= 1000
|
||||
? `${(value / 1000).toLocaleString("ru-RU", { maximumFractionDigits: 1 })} км/с`
|
||||
: `${Math.round(value)} м/с`;
|
||||
|
||||
const formatDuration = (seconds: number) => {
|
||||
if (seconds >= 86_400) return `${(seconds / 86_400).toLocaleString("ru-RU", { maximumFractionDigits: 1 })} сут`;
|
||||
if (seconds >= 3_600) return `${(seconds / 3_600).toLocaleString("ru-RU", { maximumFractionDigits: 1 })} ч`;
|
||||
if (seconds >= 60) return `${Math.round(seconds / 60)} мин`;
|
||||
return `${Math.max(1, Math.round(seconds))} с`;
|
||||
};
|
||||
|
||||
export function buildMapSurfaceInspectorSections({
|
||||
mapSettings,
|
||||
providerStatus,
|
||||
updateMapSettings,
|
||||
}: {
|
||||
mapSettings: MapPageSettings;
|
||||
providerStatus: MapProviderStatus;
|
||||
updateMapSettings: UpdateMapSettings;
|
||||
}): MapInspectorSection[] {
|
||||
return [
|
||||
{
|
||||
id: "map-base",
|
||||
label: "Подложка и terrain",
|
||||
description: "provider-neutral surface",
|
||||
group: "Карта",
|
||||
icon: <Icon name="globe" />,
|
||||
content: <>
|
||||
<ControlRow label="Подложка"><strong>Cesium World Imagery</strong></ControlRow>
|
||||
<small className="catalog-map-inspector__note">Текущий официальный provider. Другие provider-слои появятся только после отдельного asset-контракта Platform.</small>
|
||||
<ControlRow label="Live providers"><span>Imagery: {providerStateLabel[providerStatus.imagery]} · Terrain: {providerStateLabel[providerStatus.terrain]} · 3D: {providerStateLabel[providerStatus.buildings]}</span></ControlRow>
|
||||
{Object.entries(providerStatus.errors).map(([provider, error]) => <small key={provider} className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{provider}: {error}</small>)}
|
||||
<small className="catalog-map-inspector__note">Рельеф — отдельный слой под imagery.</small>
|
||||
<Checker checked={mapSettings.terrainEnabled} label="Terrain" 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: "Карта",
|
||||
icon: <Icon name="activity" />,
|
||||
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: "Карта",
|
||||
icon: <Icon name="building" />,
|
||||
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 })} />
|
||||
</>,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function buildMapPresentationInspectorSections({
|
||||
presentationProfiles,
|
||||
referenceLayers,
|
||||
setReferenceLayers,
|
||||
updatePresentationProfile,
|
||||
updatePresentationStyle,
|
||||
}: {
|
||||
presentationProfiles: MapPresentationProfile[];
|
||||
referenceLayers: MapReferenceLayer[];
|
||||
setReferenceLayers: Dispatch<SetStateAction<MapReferenceLayer[]>>;
|
||||
updatePresentationProfile: UpdatePresentationProfile;
|
||||
updatePresentationStyle: UpdatePresentationStyle;
|
||||
}): MapInspectorSection[] {
|
||||
return [
|
||||
...presentationProfiles.flatMap((profile) => {
|
||||
const referenceProfile = isMapReferencePresentationProfile(profile);
|
||||
const referenceLayer = referenceLayers.find((layer) => layer.presentationProfileId === profile.id);
|
||||
return [
|
||||
{
|
||||
id: `map-target-${profile.id}`,
|
||||
label: referenceProfile ? profile.title : profile.target.variant === "surface-fill" ? "HGeoZone" : "Таргет",
|
||||
description: profile.target.variant === "surface-fill" ? `проекция · ${profile.title}` : profile.title,
|
||||
group: referenceProfile ? "Станции" : profile.target.variant === "surface-fill" ? "Слои" : "Таргеты",
|
||||
icon: <Icon name={referenceProfile ? "globe" : profile.target.variant === "surface-fill" ? "grid" : "target"} />,
|
||||
content: <>
|
||||
<small className="catalog-map-inspector__note">Профиль принадлежит этой странице Application и управляется тем же provider-neutral MCP-контрактом. Исходный API в настройках отсутствует.</small>
|
||||
{referenceLayer ? (
|
||||
<Checker
|
||||
checked={referenceLayer.visible}
|
||||
label={`Показывать слой «${profile.title}»`}
|
||||
onChange={(visible) => setReferenceLayers((current) => current.map((layer) => (
|
||||
layer.id === referenceLayer.id ? { ...layer, visible } : layer
|
||||
)))}
|
||||
/>
|
||||
) : null}
|
||||
{profile.target.variant === "surface-fill" && <>
|
||||
<ControlRow label="Тип слоя"><strong>HGeoZone · ground projection</strong></ControlRow>
|
||||
{profile.styles.map((style) => {
|
||||
const classLabels = profile.classes.filter((item) => item.styleId === style.id).map((item) => item.label);
|
||||
const label = classLabels.length ? classLabels.join(" · ") : style.id;
|
||||
return <div className="catalog-map-inspector__style" key={style.id}>
|
||||
<ControlRow label={`Заливка · ${label}`}><ColorField label={`Цвет заливки: ${label}`} value={style.color} onChange={(color) => updatePresentationStyle(profile.id, style.id, { color })} /></ControlRow>
|
||||
<RangeControl label={`Прозрачность заливки · ${label}`} value={Math.round(style.opacity * 100)} min={0} max={100} step={1} formatValue={(value) => `${value}%`} onChange={(value) => updatePresentationStyle(profile.id, style.id, { opacity: value / 100 })} />
|
||||
</div>;
|
||||
})}
|
||||
<ControlRow label="Граница"><ColorField label="Цвет границы HGeoZone" value={profile.target.outlineColor} onChange={(outlineColor) => updatePresentationProfile(profile.id, (current) => current.target.variant === "surface-fill" ? ({ ...current, target: { ...current.target, outlineColor } }) : current)} /></ControlRow>
|
||||
<RangeControl label="Прозрачность границы" value={Math.round(profile.target.outlineOpacity * 100)} min={0} max={100} step={1} formatValue={(value) => `${value}%`} onChange={(value) => updatePresentationProfile(profile.id, (current) => current.target.variant === "surface-fill" ? ({ ...current, target: { ...current.target, outlineOpacity: value / 100 } }) : current)} />
|
||||
<RangeControl label="Толщина границы" value={profile.target.outlineWidthPx} min={0} max={8} step={0.5} formatValue={(value) => `${value} px`} onChange={(outlineWidthPx) => updatePresentationProfile(profile.id, (current) => current.target.variant === "surface-fill" ? ({ ...current, target: { ...current.target, outlineWidthPx } }) : current)} />
|
||||
</>}
|
||||
{profile.target.variant === "elevated-spike" && <>
|
||||
<RangeControl label="Высота таргета" value={profile.target.stemHeightMeters} min={100} max={10_000} step={50} formatValue={(value) => `${value} м`} onChange={(stemHeightMeters) => updatePresentationProfile(profile.id, (current) => current.target.variant === "elevated-spike" ? ({ ...current, target: { ...current.target, stemHeightMeters } }) : current)} />
|
||||
<RangeControl label="Размер головки" value={profile.target.headSizePx} min={1} max={32} step={1} formatValue={(value) => `${value} px`} onChange={(headSizePx) => updatePresentationProfile(profile.id, (current) => current.target.variant === "elevated-spike" ? ({ ...current, target: { ...current.target, headSizePx } }) : current)} />
|
||||
<RangeControl label="Толщина стержня" value={profile.target.stemWidthPx} min={0.25} max={12} step={0.25} formatValue={(value) => `${value} px`} onChange={(stemWidthPx) => updatePresentationProfile(profile.id, (current) => current.target.variant === "elevated-spike" ? ({ ...current, target: { ...current.target, stemWidthPx } }) : current)} />
|
||||
</>}
|
||||
<InspectorSelectField
|
||||
label="Подпись"
|
||||
value={profile.label.mode}
|
||||
options={[
|
||||
{ value: "subject_id", label: "ID", description: "Стабильный идентификатор сущности" },
|
||||
{ value: "attributes", label: "Имя", description: "Первое доступное display-поле" },
|
||||
{ value: "none", label: "Нет", description: "Не показывать плашку" },
|
||||
]}
|
||||
onChange={(mode) => updatePresentationProfile(profile.id, (current) => ({ ...current, label: { ...current.label, mode } }))}
|
||||
/>
|
||||
<RangeControl label="Размер подписи" value={profile.label.sizePx} min={8} max={32} step={1} formatValue={(value) => `${value} px`} onChange={(sizePx) => updatePresentationProfile(profile.id, (current) => ({ ...current, label: { ...current.label, sizePx } }))} />
|
||||
<RangeControl label="Смещение подписи X" value={profile.label.offsetX} min={-100} max={100} step={1} formatValue={(value) => `${value} px`} onChange={(offsetX) => updatePresentationProfile(profile.id, (current) => ({ ...current, label: { ...current.label, offsetX } }))} />
|
||||
<RangeControl label="Смещение подписи Y" value={profile.label.offsetY} min={-100} max={100} step={1} formatValue={(value) => `${value} px`} onChange={(offsetY) => updatePresentationProfile(profile.id, (current) => ({ ...current, label: { ...current.label, offsetY } }))} />
|
||||
<RangeControl label="Скрывать подпись выше" value={profile.label.hideCameraHeightMeters} min={1_000} max={500_000} step={1_000} formatValue={(value) => `${Math.round(value / 1_000)} км`} onChange={(hideCameraHeightMeters) => updatePresentationProfile(profile.id, (current) => ({ ...current, label: { ...current.label, hideCameraHeightMeters } }))} />
|
||||
<RangeControl label={profile.target.variant === "surface-fill" ? "Скрывать HGeoZone выше" : "Скрывать таргет выше"} value={profile.target.hideCameraHeightMeters} min={1_000} max={500_000} step={1_000} formatValue={(value) => `${Math.round(value / 1_000)} км`} onChange={(hideCameraHeightMeters) => updatePresentationProfile(profile.id, (current) => ({ ...current, target: { ...current.target, hideCameraHeightMeters } }))} />
|
||||
<ControlRow label="Фон плашки"><ColorField label="Цвет фона подписи" value={profile.label.backgroundColor} onChange={(backgroundColor) => updatePresentationProfile(profile.id, (current) => ({ ...current, label: { ...current.label, backgroundColor } }))} /></ControlRow>
|
||||
<RangeControl label="Прозрачность плашки" value={Math.round(profile.label.backgroundOpacity * 100)} min={0} max={100} step={1} formatValue={(value) => `${value}%`} onChange={(value) => updatePresentationProfile(profile.id, (current) => ({ ...current, label: { ...current.label, backgroundOpacity: value / 100 } }))} />
|
||||
</>,
|
||||
},
|
||||
...(profile.target.variant === "surface-fill" || referenceProfile ? [] : [{
|
||||
id: `map-state-classes-${profile.id}`,
|
||||
label: "Классы состояния",
|
||||
description: "нормализованные фасеты онтологии",
|
||||
group: "Таргеты",
|
||||
icon: <Icon name="sliders" />,
|
||||
content: <>
|
||||
<small className="catalog-map-inspector__note">Цвета назначены семантическим классам после нормализации данных. Здесь нет названий provider-статусов и привязки к транспорту.</small>
|
||||
{profile.styles.map((style) => {
|
||||
const classLabels = profile.classes.filter((item) => item.styleId === style.id).map((item) => item.label);
|
||||
const label = classLabels.length ? classLabels.join(" · ") : style.id;
|
||||
return <div className="catalog-map-inspector__style" key={style.id}>
|
||||
<ControlRow label={label}><ColorField label={`Цвет: ${label}`} value={style.color} onChange={(color) => updatePresentationStyle(profile.id, style.id, { color })} /></ControlRow>
|
||||
<RangeControl label={`${label}: прозрачность`} value={Math.round(style.opacity * 100)} min={0} max={100} step={1} formatValue={(value) => `${value}%`} onChange={(value) => updatePresentationStyle(profile.id, style.id, { opacity: value / 100 })} />
|
||||
</div>;
|
||||
})}
|
||||
</>,
|
||||
}]),
|
||||
];
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
export function buildMapRuntimeInspectorSections({
|
||||
animationModeEnabled,
|
||||
setAnimationMode,
|
||||
spiralPresetId,
|
||||
spiralPresetOptions,
|
||||
spiralRunning,
|
||||
selectSpiralPreset,
|
||||
spiralHeightMeters,
|
||||
setSpiralHeightMeters,
|
||||
spiralSpeedMetersPerSecond,
|
||||
setSpiralSpeedMetersPerSecond,
|
||||
spiralPitchMetersPerTurn,
|
||||
setSpiralPitchMetersPerTurn,
|
||||
spiralTargetRadiusMeters,
|
||||
setSpiralTargetRadiusMeters,
|
||||
setSpiralPresetId,
|
||||
spiralCanStart,
|
||||
spiralTileCacheReady,
|
||||
providerStatus,
|
||||
gatewayHealth,
|
||||
gatewayCheckState,
|
||||
toggleSpiralAnimation,
|
||||
spiralMessage,
|
||||
mapSettings,
|
||||
setCacheEnabled,
|
||||
setCacheNoOverwrite,
|
||||
gatewayEndpoint,
|
||||
liveCacheSummary,
|
||||
refreshCurrentViewport,
|
||||
cacheRefresh,
|
||||
verifyGateway,
|
||||
transportDiagnostic,
|
||||
gatewayHealthAge,
|
||||
gatewayCheckError,
|
||||
selected,
|
||||
}: {
|
||||
animationModeEnabled: boolean;
|
||||
setAnimationMode: (enabled: boolean) => void;
|
||||
spiralPresetId: CameraSurveySelection;
|
||||
spiralPresetOptions: Array<SelectOption<CameraSurveySelection>>;
|
||||
spiralRunning: boolean;
|
||||
selectSpiralPreset: (presetId: CameraSurveySelection) => void;
|
||||
spiralHeightMeters: number;
|
||||
setSpiralHeightMeters: Dispatch<SetStateAction<number>>;
|
||||
spiralSpeedMetersPerSecond: number;
|
||||
setSpiralSpeedMetersPerSecond: Dispatch<SetStateAction<number>>;
|
||||
spiralPitchMetersPerTurn: number;
|
||||
setSpiralPitchMetersPerTurn: Dispatch<SetStateAction<number>>;
|
||||
spiralTargetRadiusMeters: number;
|
||||
setSpiralTargetRadiusMeters: Dispatch<SetStateAction<number>>;
|
||||
setSpiralPresetId: Dispatch<SetStateAction<CameraSurveySelection>>;
|
||||
spiralCanStart: boolean;
|
||||
spiralTileCacheReady: boolean;
|
||||
providerStatus: MapProviderStatus;
|
||||
gatewayHealth: MapGatewayHealth | null;
|
||||
gatewayCheckState: GatewayCheckState;
|
||||
toggleSpiralAnimation: () => void;
|
||||
spiralMessage: string | null;
|
||||
mapSettings: MapPageSettings;
|
||||
setCacheEnabled: (enabled: boolean) => void;
|
||||
setCacheNoOverwrite: (enabled: boolean) => void;
|
||||
gatewayEndpoint: string | null;
|
||||
liveCacheSummary: string;
|
||||
refreshCurrentViewport: () => void;
|
||||
cacheRefresh: boolean;
|
||||
verifyGateway: () => void | Promise<void>;
|
||||
transportDiagnostic: string | null;
|
||||
gatewayHealthAge: string | null;
|
||||
gatewayCheckError: string | null;
|
||||
selected: MapSelectableEntity | undefined;
|
||||
}): MapInspectorSection[] {
|
||||
return [
|
||||
{
|
||||
id: "map-camera-animation",
|
||||
label: "Анимация камеры",
|
||||
description: "geodesic spiral survey",
|
||||
group: "Камера",
|
||||
icon: <Icon name="activity" />,
|
||||
content: <>
|
||||
<Checker checked={animationModeEnabled} label="Режим анимации" onChange={setAnimationMode} />
|
||||
{animationModeEnabled ? <>
|
||||
<small className="catalog-map-inspector__note">Стартовая точка берётся из текущей позиции камеры. Камера смотрит почти в надир, а маршрут ждёт текущие tiles перед продолжением. Движение идёт по региональной геодезической спирали WGS84 до выбранного радиуса.</small>
|
||||
<InspectorSelectField
|
||||
label="Профиль покрытия"
|
||||
value={spiralPresetId}
|
||||
options={spiralPresetOptions}
|
||||
disabled={spiralRunning}
|
||||
onChange={selectSpiralPreset}
|
||||
/>
|
||||
<small className="catalog-map-inspector__note">У текущего OSM Buildings подтверждено {OSM_BUILDINGS_OBSERVED_BAND_COUNT} иерархических bands. Десять профилей управляют высотой и покрытием; фактический LOD Cesium выбирает по SSE, viewport и расстоянию.</small>
|
||||
<ControlRow label="Слои прохода"><small>Imagery · Terrain · OSM Buildings</small></ControlRow>
|
||||
<RangeControl
|
||||
label="Высота над землёй"
|
||||
value={logarithmicControlValue(spiralHeightMeters)}
|
||||
min={logarithmicControlValue(10)}
|
||||
max={logarithmicControlValue(100_000)}
|
||||
step={0.01}
|
||||
disabled={spiralRunning}
|
||||
formatValue={(value) => formatMetricDistance(10 ** value)}
|
||||
onChange={(value) => {
|
||||
setSpiralPresetId("custom");
|
||||
setSpiralHeightMeters(valueFromLogarithmicControl(value));
|
||||
}}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Скорость камеры"
|
||||
value={logarithmicControlValue(spiralSpeedMetersPerSecond)}
|
||||
min={logarithmicControlValue(1)}
|
||||
max={logarithmicControlValue(5_000)}
|
||||
step={0.01}
|
||||
disabled={spiralRunning}
|
||||
formatValue={(value) => formatMetricSpeed(10 ** value)}
|
||||
onChange={(value) => {
|
||||
setSpiralPresetId("custom");
|
||||
setSpiralSpeedMetersPerSecond(valueFromLogarithmicControl(value));
|
||||
}}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Шаг спирали"
|
||||
value={logarithmicControlValue(spiralPitchMetersPerTurn)}
|
||||
min={logarithmicControlValue(20)}
|
||||
max={logarithmicControlValue(100_000)}
|
||||
step={0.01}
|
||||
disabled={spiralRunning}
|
||||
formatValue={(value) => formatMetricDistance(10 ** value)}
|
||||
onChange={(value) => {
|
||||
setSpiralPresetId("custom");
|
||||
setSpiralPitchMetersPerTurn(valueFromLogarithmicControl(value));
|
||||
}}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Радиус прохода"
|
||||
value={logarithmicControlValue(spiralTargetRadiusMeters)}
|
||||
min={logarithmicControlValue(1_000)}
|
||||
max={logarithmicControlValue(250_000)}
|
||||
step={0.01}
|
||||
disabled={spiralRunning}
|
||||
formatValue={(value) => formatMetricDistance(10 ** value)}
|
||||
onChange={(value) => {
|
||||
setSpiralPresetId("custom");
|
||||
setSpiralTargetRadiusMeters(valueFromLogarithmicControl(value));
|
||||
}}
|
||||
/>
|
||||
<small className="catalog-map-inspector__note">Расчётное движение без ожидания сети: {formatDuration(cameraSurveySpiralDistance(spiralTargetRadiusMeters, spiralPitchMetersPerTurn) / spiralSpeedMetersPerSecond)}. Tile waits и автоматическое сужение шага под viewport увеличат фактическое время.</small>
|
||||
{!spiralCanStart && !spiralRunning ? <small className="catalog-map-inspector__note" role="status">Подготовка: imagery — {providerStateLabel[providerStatus.imagery]}, terrain — {providerStateLabel[providerStatus.terrain]}, OSM Buildings — {providerStateLabel[providerStatus.buildings]}, TileCache — {spiralTileCacheReady ? "готов" : gatewayHealth?.cache?.atCapacity ? "заполнен" : gatewayCheckState === "checking" ? "проверяется" : "недоступен для записи"}.</small> : null}
|
||||
<Button variant="secondary" shape="pill" onClick={toggleSpiralAnimation} disabled={!spiralRunning && !spiralCanStart}>{spiralRunning ? "Остановить" : "Запустить режим анимации"}</Button>
|
||||
{spiralRunning ? <small className="catalog-map-inspector__note">Камера движется от исходной точки. Выключение режима, уход со страницы или reload остановят сессию.</small> : null}
|
||||
{spiralMessage ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="status">{spiralMessage}</small> : null}
|
||||
</> : null}
|
||||
</>,
|
||||
},
|
||||
{
|
||||
id: "map-cache",
|
||||
label: "TileCache",
|
||||
description: "Platform Map Gateway",
|
||||
group: "Хранение",
|
||||
icon: <Icon name="database" />,
|
||||
content: <>
|
||||
<small className="catalog-map-inspector__note">Общий persistent cache Platform: он не принадлежит приложению, странице или пользователю.</small>
|
||||
<Checker checked={mapSettings.cacheEnabled} label="Кэшировать live-данные" onChange={setCacheEnabled} />
|
||||
<small className="catalog-map-inspector__note">Cache hit отдаётся как есть; новый tile записывается только при miss.</small>
|
||||
<Checker checked={mapSettings.cacheNoOverwrite} disabled={!mapSettings.cacheEnabled} label="Не перезаписывать уже полученное" onChange={setCacheNoOverwrite} />
|
||||
<ControlRow className="catalog-map-inspector__cache-fact" label="Режим"><span>{mapSettings.cacheEnabled ? mapSettings.cacheNoOverwrite ? "Live + Cache · append-only" : "Live + Cache · обновление разрешено" : "Live без persistent cache"}</span></ControlRow>
|
||||
<ControlRow className="catalog-map-inspector__cache-fact" label="Хранилище"><span>Platform Map Gateway</span></ControlRow>
|
||||
<ControlRow className="catalog-map-inspector__cache-fact" label="Подключение"><span>{gatewayEndpoint ?? "runtime profile · не проверено"}</span></ControlRow>
|
||||
<ControlRow className="catalog-map-inspector__cache-fact" label="Записано"><span>{liveCacheSummary}</span></ControlRow>
|
||||
<ControlRow className="catalog-map-inspector__cache-fact" label="Политика"><span>{gatewayHealth?.cache?.writePolicy ?? "append-only · проверяется"}</span></ControlRow>
|
||||
<Button variant="secondary" shape="pill" icon={<Icon name="refresh" />} onClick={refreshCurrentViewport} disabled={!mapSettings.cacheEnabled || cacheRefresh}> {cacheRefresh ? "Обновляем viewport…" : "Обновить текущий viewport"}</Button>
|
||||
<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>
|
||||
{transportDiagnostic ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{transportDiagnostic}</small> : null}
|
||||
{gatewayHealthAge ? <small className="catalog-map-inspector__note">{gatewayHealthAge}</small> : null}
|
||||
<small className="catalog-map-inspector__note">{mapSettings.cacheEnabled ? mapSettings.cacheNoOverwrite ? "Новые miss дописываются; при заполнении объёма Gateway продолжит live-маршрут без удаления прежних tiles." : "Новые запросы этого Application могут явно обновлять уже записанные tiles." : "Real-time: provider остаётся официальным, чтение и запись persistent cache выключены."}</small>
|
||||
{gatewayHealth?.cache?.atCapacity ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">TileCache заполнен: новые tiles показываются live, но не записываются. Существующий cache не удаляется.</small> : null}
|
||||
{gatewayCheckState === "error" || gatewayCheckState === "stale" ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{gatewayCheckError}</small> : null}
|
||||
</>,
|
||||
},
|
||||
{
|
||||
id: "map-selection",
|
||||
label: "Выбранная сущность",
|
||||
description: "selection contract",
|
||||
group: "Данные",
|
||||
icon: <Icon name="target" />,
|
||||
content: <>
|
||||
<ControlRow label="Сущность"><strong>{selected?.title ?? "Нет выбора"}</strong></ControlRow>
|
||||
<ControlRow label="Тип"><span>{selected?.kind ?? "—"}{selected?.status ? ` · ${selected.status}` : ""}</span></ControlRow>
|
||||
</>,
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user