feat(map): adopt DC Default scene
This commit is contained in:
@@ -0,0 +1,681 @@
|
||||
import {
|
||||
Checker,
|
||||
ColorField,
|
||||
ControlRow,
|
||||
Inspector,
|
||||
RangeControl,
|
||||
StatusBadge,
|
||||
} from "@nodedc/ui-react";
|
||||
|
||||
import type {
|
||||
MapInspectorSection,
|
||||
MapViewDocument,
|
||||
} from "../../core/map/mapView";
|
||||
import type { useMapGatewayHealth } from "../../core/map/useMapGatewayHealth";
|
||||
|
||||
export const editableMapInspectorSections = new Set<MapInspectorSection>([
|
||||
"base-terrain",
|
||||
"atmosphere-light",
|
||||
"buildings",
|
||||
"grid-lod",
|
||||
"camera",
|
||||
"tile-cache",
|
||||
]);
|
||||
|
||||
export function MapSettingsInspector({
|
||||
draft,
|
||||
updateDraft,
|
||||
gatewayHealth,
|
||||
}: {
|
||||
draft: MapViewDocument;
|
||||
updateDraft: (
|
||||
update: (current: MapViewDocument) => MapViewDocument,
|
||||
) => void;
|
||||
gatewayHealth: ReturnType<typeof useMapGatewayHealth>;
|
||||
}) {
|
||||
return (
|
||||
<Inspector
|
||||
singleOpen
|
||||
sections={createInspectorSections(draft, updateDraft, gatewayHealth)}
|
||||
openSections={draft.view.inspectorOpenSections.filter(
|
||||
(section) => editableMapInspectorSections.has(section),
|
||||
)}
|
||||
activeId={draft.view.inspectorOpenSections[0]}
|
||||
onOpenSectionsChange={(sections) => {
|
||||
updateDraft((current) => {
|
||||
current.view.inspectorOpenSections = sections.filter(
|
||||
(section): section is MapInspectorSection =>
|
||||
editableMapInspectorSections.has(section as MapInspectorSection),
|
||||
).slice(0, 1);
|
||||
return current;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function createInspectorSections(
|
||||
draft: MapViewDocument,
|
||||
updateDraft: (
|
||||
update: (current: MapViewDocument) => MapViewDocument,
|
||||
) => void,
|
||||
gatewayHealth: ReturnType<typeof useMapGatewayHealth>,
|
||||
) {
|
||||
const view = draft.view;
|
||||
const settings = view.visualSettings;
|
||||
return [
|
||||
{
|
||||
id: "base-terrain",
|
||||
label: "Подложка и terrain",
|
||||
description: "provider-neutral surface",
|
||||
content: (
|
||||
<div className="mission-map__inspector-controls">
|
||||
<ControlRow label="Подложка">
|
||||
<strong>Cesium World Imagery</strong>
|
||||
</ControlRow>
|
||||
<Checker
|
||||
label="Спутниковая подложка"
|
||||
checked={view.layerVisibility.imagery}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.layerVisibility.imagery = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<Checker
|
||||
label="Terrain"
|
||||
checked={view.layerVisibility.terrain}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.layerVisibility.terrain = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Вертикальное преувеличение рельефа"
|
||||
value={settings.terrainExaggeration * 100}
|
||||
min={25}
|
||||
max={300}
|
||||
step={1}
|
||||
formatValue={(value) => `${(value / 100).toFixed(2)}×`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.terrainExaggeration = value / 100;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<Checker
|
||||
label="Монохромная поверхность"
|
||||
checked={settings.monochromeEnabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.visualSettings.monochromeEnabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<ControlRow label="Цвет монохрома">
|
||||
<ColorField
|
||||
label="Цвет монохромной поверхности"
|
||||
value={settings.monochromeColor}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.monochromeColor = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</ControlRow>
|
||||
<RangeControl
|
||||
label="Яркость"
|
||||
value={settings.imageryBrightness}
|
||||
min={0}
|
||||
max={200}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.imageryBrightness = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Контраст"
|
||||
value={settings.imageryContrast}
|
||||
min={0}
|
||||
max={200}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.imageryContrast = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Насыщенность"
|
||||
value={settings.imagerySaturation}
|
||||
min={0}
|
||||
max={200}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.imagerySaturation = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Гамма"
|
||||
value={settings.imageryGamma}
|
||||
min={0}
|
||||
max={300}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.imageryGamma = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Оттенок"
|
||||
value={settings.imageryHue}
|
||||
min={-180}
|
||||
max={180}
|
||||
formatValue={(value) => `${value}°`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.imageryHue = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Прозрачность imagery"
|
||||
value={settings.imageryAlpha}
|
||||
min={0}
|
||||
max={100}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.imageryAlpha = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<ControlRow label="Цвет планеты">
|
||||
<ColorField
|
||||
label="Цвет terrain без imagery"
|
||||
value={settings.globeColor}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.globeColor = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</ControlRow>
|
||||
<ControlRow label="Фон сцены">
|
||||
<ColorField
|
||||
label="Цвет фона сцены"
|
||||
value={settings.backgroundColor}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.backgroundColor = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</ControlRow>
|
||||
<RangeControl
|
||||
label="Высота карты"
|
||||
value={view.mapHeight}
|
||||
min={420}
|
||||
max={1080}
|
||||
step={1}
|
||||
formatValue={(value) => `${value} px`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.mapHeight = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "atmosphere-light",
|
||||
label: "Атмосфера и освещение",
|
||||
description: "scene / color correction",
|
||||
content: (
|
||||
<div className="mission-map__inspector-controls">
|
||||
<Checker
|
||||
label="Показывать атмосферу"
|
||||
checked={settings.atmosphereEnabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.visualSettings.atmosphereEnabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Атмосфера: оттенок"
|
||||
value={settings.atmosphereHue}
|
||||
min={-100}
|
||||
max={100}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.atmosphereHue = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Атмосфера: насыщенность"
|
||||
value={settings.atmosphereSaturation}
|
||||
min={-100}
|
||||
max={100}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.atmosphereSaturation = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Атмосфера: яркость"
|
||||
value={settings.atmosphereBrightness}
|
||||
min={-100}
|
||||
max={100}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.atmosphereBrightness = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<Checker
|
||||
label="Туман"
|
||||
checked={settings.fogEnabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.visualSettings.fogEnabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Плотность тумана"
|
||||
value={settings.fogDensity}
|
||||
min={0}
|
||||
max={100}
|
||||
formatValue={(value) => `${(value / 10_000).toFixed(4)}`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.fogDensity = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<Checker
|
||||
label="Солнечное освещение"
|
||||
checked={settings.sunEnabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.visualSettings.sunEnabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Час солнца"
|
||||
value={settings.sunHour}
|
||||
min={0}
|
||||
max={24}
|
||||
formatValue={(value) => `${value}:00 UTC`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.sunHour = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Интенсивность света"
|
||||
value={settings.sunIntensity}
|
||||
min={0}
|
||||
max={200}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.sunIntensity = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<Checker
|
||||
label="Тени"
|
||||
checked={settings.shadowsEnabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.visualSettings.shadowsEnabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "buildings",
|
||||
label: "3D здания",
|
||||
description: "3D Tiles / detail",
|
||||
content: (
|
||||
<div className="mission-map__inspector-controls">
|
||||
<Checker
|
||||
label="Показывать 3D здания"
|
||||
checked={view.layerVisibility.buildings}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.layerVisibility.buildings = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<ControlRow label="Цвет">
|
||||
<ColorField
|
||||
label="Цвет зданий"
|
||||
value={settings.buildingsColor}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.buildingsColor = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</ControlRow>
|
||||
<RangeControl
|
||||
label="Прозрачность"
|
||||
value={Math.round(settings.buildingsOpacity * 100)}
|
||||
min={0}
|
||||
max={100}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.buildingsOpacity = value / 100;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Детализация"
|
||||
value={settings.buildingsMaximumScreenSpaceError}
|
||||
min={4}
|
||||
max={32}
|
||||
formatValue={(value) => `SSE ${value}`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.buildingsMaximumScreenSpaceError = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "grid-lod",
|
||||
label: "Сетка и LOD",
|
||||
description: "first adapter control",
|
||||
content: (
|
||||
<div className="mission-map__inspector-controls">
|
||||
<Checker
|
||||
label="3D-сетка"
|
||||
checked={view.layerVisibility.grid}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.layerVisibility.grid = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<Checker
|
||||
label="LOD по высоте камеры"
|
||||
checked={settings.gridLodEnabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridLodEnabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Высота над поверхностью"
|
||||
value={settings.gridHeightMeters}
|
||||
min={0}
|
||||
max={1000}
|
||||
formatValue={(value) => `${value} м`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridHeightMeters = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="LOD 1: до высоты"
|
||||
value={settings.gridLod1MaxHeightKm}
|
||||
min={1}
|
||||
max={50}
|
||||
formatValue={(value) => `${value} км`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridLod1MaxHeightKm = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="LOD 1: шаг"
|
||||
value={settings.gridLod1StepKm}
|
||||
min={1}
|
||||
max={10}
|
||||
formatValue={(value) => `${value} км`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridLod1StepKm = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="LOD 2: до высоты"
|
||||
value={settings.gridLod2MaxHeightKm}
|
||||
min={10}
|
||||
max={200}
|
||||
formatValue={(value) => `${value} км`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridLod2MaxHeightKm = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="LOD 2: шаг"
|
||||
value={settings.gridLod2StepKm}
|
||||
min={1}
|
||||
max={25}
|
||||
formatValue={(value) => `${value} км`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridLod2StepKm = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="LOD 3: шаг"
|
||||
value={settings.gridLod3StepKm}
|
||||
min={5}
|
||||
max={100}
|
||||
formatValue={(value) => `${value} км`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridLod3StepKm = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Радиус сетки"
|
||||
value={settings.gridRadiusKm}
|
||||
min={5}
|
||||
max={150}
|
||||
formatValue={(value) => `${value} км`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridRadiusKm = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<ControlRow label="Цвет линий">
|
||||
<ColorField
|
||||
label="Цвет линий сетки"
|
||||
value={settings.gridColor}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridColor = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</ControlRow>
|
||||
<RangeControl
|
||||
label="Толщина линий"
|
||||
value={settings.gridLineWidth}
|
||||
min={1}
|
||||
max={8}
|
||||
formatValue={(value) => `${value} px`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridLineWidth = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Прозрачность сетки"
|
||||
value={settings.gridOpacity}
|
||||
min={0}
|
||||
max={100}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridOpacity = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<Checker
|
||||
label="Точки в пересечениях"
|
||||
checked={settings.gridDotsEnabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridDotsEnabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Размер точки"
|
||||
value={settings.gridDotsSize}
|
||||
min={2}
|
||||
max={28}
|
||||
formatValue={(value) => `${value} px`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridDotsSize = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<ControlRow label="Цвет точек">
|
||||
<ColorField
|
||||
label="Цвет точек сетки"
|
||||
value={settings.gridDotsColor}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridDotsColor = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</ControlRow>
|
||||
<RangeControl
|
||||
label="Прозрачность точек"
|
||||
value={settings.gridDotsOpacity}
|
||||
min={0}
|
||||
max={100}
|
||||
formatValue={(value) => `${value}%`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.gridDotsOpacity = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "camera",
|
||||
label: "Анимация камеры",
|
||||
description: "geodesic spiral survey",
|
||||
content: (
|
||||
<div className="mission-map__inspector-controls">
|
||||
<Checker
|
||||
label="Разрешить анимацию камеры"
|
||||
checked={settings.cameraAnimationEnabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.visualSettings.cameraAnimationEnabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<ControlRow label="Сохранённая позиция">
|
||||
<StatusBadge tone={view.camera ? "success" : "neutral"}>
|
||||
{view.camera ? "Есть" : "Не задана"}
|
||||
</StatusBadge>
|
||||
</ControlRow>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "tile-cache",
|
||||
label: "TileCache",
|
||||
description: "Platform Map Gateway",
|
||||
content: (
|
||||
<div
|
||||
className="mission-map__inspector-controls"
|
||||
data-health-code={gatewayHealth.code ?? undefined}
|
||||
>
|
||||
<Checker
|
||||
label="Использовать общий TileCache"
|
||||
checked={view.cacheIntent.enabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.cacheIntent.enabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<Checker
|
||||
label="Не перезаписывать существующие тайлы"
|
||||
checked={view.cacheIntent.noOverwrite}
|
||||
disabled={!view.cacheIntent.enabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.cacheIntent.noOverwrite = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<CacheHealth health={gatewayHealth} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function CacheHealth({
|
||||
health,
|
||||
}: {
|
||||
health: ReturnType<typeof useMapGatewayHealth>;
|
||||
}) {
|
||||
if (health.state === "loading" || health.state === "idle") {
|
||||
return (
|
||||
<ControlRow label="Состояние">
|
||||
<StatusBadge tone="neutral">Проверяем</StatusBadge>
|
||||
</ControlRow>
|
||||
);
|
||||
}
|
||||
if (!health.snapshot) {
|
||||
return (
|
||||
<ControlRow label="Состояние">
|
||||
<StatusBadge tone="danger">Недоступен</StatusBadge>
|
||||
</ControlRow>
|
||||
);
|
||||
}
|
||||
const { cache } = health.snapshot;
|
||||
return (
|
||||
<dl className="mission-map__cache-health">
|
||||
<div>
|
||||
<dt>Хранилище</dt>
|
||||
<dd>
|
||||
<StatusBadge tone={cache.persistent ? "success" : "warning"}>
|
||||
{cache.persistent ? "Постоянное" : "Временное"}
|
||||
</StatusBadge>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Объекты</dt>
|
||||
<dd>{cache.entries.toLocaleString("ru-RU")}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Объём</dt>
|
||||
<dd>
|
||||
{formatBytes(cache.bytes)}
|
||||
{cache.maxBytes === null ? "" : ` / ${formatBytes(cache.maxBytes)}`}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Заполнение</dt>
|
||||
<dd>
|
||||
<StatusBadge
|
||||
tone={
|
||||
cache.atCapacity || health.state === "stale"
|
||||
? "warning"
|
||||
: "success"
|
||||
}
|
||||
>
|
||||
{cache.atCapacity
|
||||
? "Лимит"
|
||||
: health.state === "stale"
|
||||
? "Устарело"
|
||||
: "Норма"}
|
||||
</StatusBadge>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
|
||||
function formatBytes(value: number): string {
|
||||
if (value < 1024) return `${value} Б`;
|
||||
const units = ["КБ", "МБ", "ГБ", "ТБ"];
|
||||
let amount = value / 1024;
|
||||
let unit = units[0];
|
||||
for (let index = 1; index < units.length && amount >= 1024; index += 1) {
|
||||
amount /= 1024;
|
||||
unit = units[index];
|
||||
}
|
||||
return `${amount.toFixed(amount >= 10 ? 1 : 2)} ${unit}`;
|
||||
}
|
||||
@@ -19,10 +19,44 @@ export interface MapCamera {
|
||||
|
||||
export interface MapVisualSettings {
|
||||
atmosphereEnabled: boolean;
|
||||
lightingEnabled: boolean;
|
||||
atmosphereHue: number;
|
||||
atmosphereSaturation: number;
|
||||
atmosphereBrightness: number;
|
||||
fogEnabled: boolean;
|
||||
fogDensity: number;
|
||||
sunEnabled: boolean;
|
||||
sunHour: number;
|
||||
sunIntensity: number;
|
||||
shadowsEnabled: boolean;
|
||||
monochromeEnabled: boolean;
|
||||
monochromeColor: string;
|
||||
imageryBrightness: number;
|
||||
imageryContrast: number;
|
||||
imagerySaturation: number;
|
||||
imageryGamma: number;
|
||||
imageryHue: number;
|
||||
imageryAlpha: number;
|
||||
globeColor: string;
|
||||
backgroundColor: string;
|
||||
terrainExaggeration: number;
|
||||
buildingsColor: string;
|
||||
buildingsOpacity: number;
|
||||
buildingsMaximumScreenSpaceError: number;
|
||||
gridLodEnabled: boolean;
|
||||
gridHeightMeters: number;
|
||||
gridLod1MaxHeightKm: number;
|
||||
gridLod1StepKm: number;
|
||||
gridLod2MaxHeightKm: number;
|
||||
gridLod2StepKm: number;
|
||||
gridLod3StepKm: number;
|
||||
gridRadiusKm: number;
|
||||
gridLineWidth: number;
|
||||
gridColor: string;
|
||||
gridOpacity: number;
|
||||
gridDotsEnabled: boolean;
|
||||
gridDotsSize: number;
|
||||
gridDotsColor: string;
|
||||
gridDotsOpacity: number;
|
||||
cameraAnimationEnabled: boolean;
|
||||
}
|
||||
|
||||
@@ -69,16 +103,57 @@ export function defaultMapViewDocument(): MapViewDocument {
|
||||
return {
|
||||
revision: 0,
|
||||
view: {
|
||||
camera: null,
|
||||
camera: {
|
||||
longitude: 37.618423,
|
||||
latitude: 55.751244,
|
||||
height: 40_000,
|
||||
heading: 0,
|
||||
pitch: -51.56620156177409,
|
||||
roll: 0,
|
||||
},
|
||||
visualSettings: {
|
||||
atmosphereEnabled: true,
|
||||
lightingEnabled: true,
|
||||
atmosphereEnabled: false,
|
||||
atmosphereHue: 0,
|
||||
atmosphereSaturation: 0,
|
||||
atmosphereBrightness: 0,
|
||||
fogEnabled: true,
|
||||
fogDensity: 2,
|
||||
sunEnabled: true,
|
||||
sunHour: 12,
|
||||
sunIntensity: 200,
|
||||
shadowsEnabled: true,
|
||||
monochromeEnabled: false,
|
||||
monochromeColor: "#15151b",
|
||||
imageryBrightness: 118,
|
||||
imageryContrast: 102,
|
||||
imagerySaturation: 0,
|
||||
imageryGamma: 57,
|
||||
imageryHue: 13,
|
||||
imageryAlpha: 27,
|
||||
globeColor: "#15151b",
|
||||
backgroundColor: "#08090d",
|
||||
terrainExaggeration: 1,
|
||||
buildingsMaximumScreenSpaceError: 16,
|
||||
buildingsColor: "#a27aff",
|
||||
buildingsOpacity: 1,
|
||||
buildingsMaximumScreenSpaceError: 4,
|
||||
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,
|
||||
cameraAnimationEnabled: true,
|
||||
},
|
||||
mapHeight: 720,
|
||||
mapHeight: 694,
|
||||
inspectorOpenSections: [],
|
||||
cacheIntent: {
|
||||
enabled: true,
|
||||
@@ -89,7 +164,7 @@ export function defaultMapViewDocument(): MapViewDocument {
|
||||
imagery: true,
|
||||
terrain: true,
|
||||
buildings: true,
|
||||
grid: false,
|
||||
grid: true,
|
||||
targets: true,
|
||||
},
|
||||
},
|
||||
@@ -99,7 +174,7 @@ export function defaultMapViewDocument(): MapViewDocument {
|
||||
export function decodeMapViewDocument(value: unknown): MapViewDocument {
|
||||
const document = requireRecord(value, "map view");
|
||||
requireExactKeys(document, ["schema_version", "revision", "view"], "map view");
|
||||
if (document.schema_version !== "missioncore.map-view/v1") {
|
||||
if (document.schema_version !== "missioncore.map-view/v2") {
|
||||
throw new Error("Версия сохранённого состояния карты не поддерживается.");
|
||||
}
|
||||
return {
|
||||
@@ -124,11 +199,45 @@ export function encodeMapViewPut(document: MapViewDocument): unknown {
|
||||
: null,
|
||||
visual_settings: {
|
||||
atmosphere_enabled: document.view.visualSettings.atmosphereEnabled,
|
||||
lighting_enabled: document.view.visualSettings.lightingEnabled,
|
||||
atmosphere_hue: document.view.visualSettings.atmosphereHue,
|
||||
atmosphere_saturation: document.view.visualSettings.atmosphereSaturation,
|
||||
atmosphere_brightness: document.view.visualSettings.atmosphereBrightness,
|
||||
fog_enabled: document.view.visualSettings.fogEnabled,
|
||||
fog_density: document.view.visualSettings.fogDensity,
|
||||
sun_enabled: document.view.visualSettings.sunEnabled,
|
||||
sun_hour: document.view.visualSettings.sunHour,
|
||||
sun_intensity: document.view.visualSettings.sunIntensity,
|
||||
shadows_enabled: document.view.visualSettings.shadowsEnabled,
|
||||
monochrome_enabled: document.view.visualSettings.monochromeEnabled,
|
||||
monochrome_color: document.view.visualSettings.monochromeColor,
|
||||
imagery_brightness: document.view.visualSettings.imageryBrightness,
|
||||
imagery_contrast: document.view.visualSettings.imageryContrast,
|
||||
imagery_saturation: document.view.visualSettings.imagerySaturation,
|
||||
imagery_gamma: document.view.visualSettings.imageryGamma,
|
||||
imagery_hue: document.view.visualSettings.imageryHue,
|
||||
imagery_alpha: document.view.visualSettings.imageryAlpha,
|
||||
globe_color: document.view.visualSettings.globeColor,
|
||||
background_color: document.view.visualSettings.backgroundColor,
|
||||
terrain_exaggeration: document.view.visualSettings.terrainExaggeration,
|
||||
buildings_color: document.view.visualSettings.buildingsColor,
|
||||
buildings_opacity: document.view.visualSettings.buildingsOpacity,
|
||||
buildings_maximum_screen_space_error:
|
||||
document.view.visualSettings.buildingsMaximumScreenSpaceError,
|
||||
grid_lod_enabled: document.view.visualSettings.gridLodEnabled,
|
||||
grid_height_meters: document.view.visualSettings.gridHeightMeters,
|
||||
grid_lod_1_max_height_km: document.view.visualSettings.gridLod1MaxHeightKm,
|
||||
grid_lod_1_step_km: document.view.visualSettings.gridLod1StepKm,
|
||||
grid_lod_2_max_height_km: document.view.visualSettings.gridLod2MaxHeightKm,
|
||||
grid_lod_2_step_km: document.view.visualSettings.gridLod2StepKm,
|
||||
grid_lod_3_step_km: document.view.visualSettings.gridLod3StepKm,
|
||||
grid_radius_km: document.view.visualSettings.gridRadiusKm,
|
||||
grid_line_width: document.view.visualSettings.gridLineWidth,
|
||||
grid_color: document.view.visualSettings.gridColor,
|
||||
grid_opacity: document.view.visualSettings.gridOpacity,
|
||||
grid_dots_enabled: document.view.visualSettings.gridDotsEnabled,
|
||||
grid_dots_size: document.view.visualSettings.gridDotsSize,
|
||||
grid_dots_color: document.view.visualSettings.gridDotsColor,
|
||||
grid_dots_opacity: document.view.visualSettings.gridDotsOpacity,
|
||||
camera_animation_enabled: document.view.visualSettings.cameraAnimationEnabled,
|
||||
},
|
||||
map_height: document.view.mapHeight,
|
||||
@@ -228,29 +337,175 @@ function decodeVisualSettings(value: unknown): MapVisualSettings {
|
||||
settings,
|
||||
[
|
||||
"atmosphere_enabled",
|
||||
"lighting_enabled",
|
||||
"atmosphere_hue",
|
||||
"atmosphere_saturation",
|
||||
"atmosphere_brightness",
|
||||
"fog_enabled",
|
||||
"fog_density",
|
||||
"sun_enabled",
|
||||
"sun_hour",
|
||||
"sun_intensity",
|
||||
"shadows_enabled",
|
||||
"monochrome_enabled",
|
||||
"monochrome_color",
|
||||
"imagery_brightness",
|
||||
"imagery_contrast",
|
||||
"imagery_saturation",
|
||||
"imagery_gamma",
|
||||
"imagery_hue",
|
||||
"imagery_alpha",
|
||||
"globe_color",
|
||||
"background_color",
|
||||
"terrain_exaggeration",
|
||||
"buildings_color",
|
||||
"buildings_opacity",
|
||||
"buildings_maximum_screen_space_error",
|
||||
"grid_lod_enabled",
|
||||
"grid_height_meters",
|
||||
"grid_lod_1_max_height_km",
|
||||
"grid_lod_1_step_km",
|
||||
"grid_lod_2_max_height_km",
|
||||
"grid_lod_2_step_km",
|
||||
"grid_lod_3_step_km",
|
||||
"grid_radius_km",
|
||||
"grid_line_width",
|
||||
"grid_color",
|
||||
"grid_opacity",
|
||||
"grid_dots_enabled",
|
||||
"grid_dots_size",
|
||||
"grid_dots_color",
|
||||
"grid_dots_opacity",
|
||||
"camera_animation_enabled",
|
||||
],
|
||||
"map view.view.visual_settings",
|
||||
);
|
||||
return {
|
||||
atmosphereEnabled: requireBoolean(settings.atmosphere_enabled, "atmosphere_enabled"),
|
||||
lightingEnabled: requireBoolean(settings.lighting_enabled, "lighting_enabled"),
|
||||
atmosphereHue: requireNumber(settings.atmosphere_hue, "atmosphere_hue", -100, 100),
|
||||
atmosphereSaturation: requireNumber(
|
||||
settings.atmosphere_saturation,
|
||||
"atmosphere_saturation",
|
||||
-100,
|
||||
100,
|
||||
),
|
||||
atmosphereBrightness: requireNumber(
|
||||
settings.atmosphere_brightness,
|
||||
"atmosphere_brightness",
|
||||
-100,
|
||||
100,
|
||||
),
|
||||
fogEnabled: requireBoolean(settings.fog_enabled, "fog_enabled"),
|
||||
fogDensity: requireNumber(settings.fog_density, "fog_density", 0, 100),
|
||||
sunEnabled: requireBoolean(settings.sun_enabled, "sun_enabled"),
|
||||
sunHour: requireNumber(settings.sun_hour, "sun_hour", 0, 24),
|
||||
sunIntensity: requireNumber(settings.sun_intensity, "sun_intensity", 0, 200),
|
||||
shadowsEnabled: requireBoolean(settings.shadows_enabled, "shadows_enabled"),
|
||||
monochromeEnabled: requireBoolean(settings.monochrome_enabled, "monochrome_enabled"),
|
||||
monochromeColor: requireHexColor(settings.monochrome_color, "monochrome_color"),
|
||||
imageryBrightness: requireNumber(
|
||||
settings.imagery_brightness,
|
||||
"imagery_brightness",
|
||||
0,
|
||||
200,
|
||||
),
|
||||
imageryContrast: requireNumber(
|
||||
settings.imagery_contrast,
|
||||
"imagery_contrast",
|
||||
0,
|
||||
200,
|
||||
),
|
||||
imagerySaturation: requireNumber(
|
||||
settings.imagery_saturation,
|
||||
"imagery_saturation",
|
||||
0,
|
||||
200,
|
||||
),
|
||||
imageryGamma: requireNumber(settings.imagery_gamma, "imagery_gamma", 0, 300),
|
||||
imageryHue: requireNumber(settings.imagery_hue, "imagery_hue", -180, 180),
|
||||
imageryAlpha: requireNumber(settings.imagery_alpha, "imagery_alpha", 0, 100),
|
||||
globeColor: requireHexColor(settings.globe_color, "globe_color"),
|
||||
backgroundColor: requireHexColor(settings.background_color, "background_color"),
|
||||
terrainExaggeration: requireNumber(
|
||||
settings.terrain_exaggeration,
|
||||
"terrain_exaggeration",
|
||||
0.1,
|
||||
20,
|
||||
0.25,
|
||||
3,
|
||||
),
|
||||
buildingsColor: requireHexColor(settings.buildings_color, "buildings_color"),
|
||||
buildingsOpacity: requireNumber(
|
||||
settings.buildings_opacity,
|
||||
"buildings_opacity",
|
||||
0,
|
||||
1,
|
||||
),
|
||||
buildingsMaximumScreenSpaceError: requireNumber(
|
||||
settings.buildings_maximum_screen_space_error,
|
||||
"buildings_maximum_screen_space_error",
|
||||
4,
|
||||
32,
|
||||
),
|
||||
gridLodEnabled: requireBoolean(settings.grid_lod_enabled, "grid_lod_enabled"),
|
||||
gridHeightMeters: requireNumber(
|
||||
settings.grid_height_meters,
|
||||
"grid_height_meters",
|
||||
0,
|
||||
1000,
|
||||
),
|
||||
gridLod1MaxHeightKm: requireNumber(
|
||||
settings.grid_lod_1_max_height_km,
|
||||
"grid_lod_1_max_height_km",
|
||||
1,
|
||||
64,
|
||||
50,
|
||||
),
|
||||
gridLod1StepKm: requireNumber(
|
||||
settings.grid_lod_1_step_km,
|
||||
"grid_lod_1_step_km",
|
||||
1,
|
||||
10,
|
||||
),
|
||||
gridLod2MaxHeightKm: requireNumber(
|
||||
settings.grid_lod_2_max_height_km,
|
||||
"grid_lod_2_max_height_km",
|
||||
10,
|
||||
200,
|
||||
),
|
||||
gridLod2StepKm: requireNumber(
|
||||
settings.grid_lod_2_step_km,
|
||||
"grid_lod_2_step_km",
|
||||
1,
|
||||
25,
|
||||
),
|
||||
gridLod3StepKm: requireNumber(
|
||||
settings.grid_lod_3_step_km,
|
||||
"grid_lod_3_step_km",
|
||||
5,
|
||||
100,
|
||||
),
|
||||
gridRadiusKm: requireNumber(settings.grid_radius_km, "grid_radius_km", 5, 150),
|
||||
gridLineWidth: requireNumber(
|
||||
settings.grid_line_width,
|
||||
"grid_line_width",
|
||||
1,
|
||||
8,
|
||||
),
|
||||
gridColor: requireHexColor(settings.grid_color, "grid_color"),
|
||||
gridOpacity: requireNumber(settings.grid_opacity, "grid_opacity", 0, 100),
|
||||
gridDotsEnabled: requireBoolean(
|
||||
settings.grid_dots_enabled,
|
||||
"grid_dots_enabled",
|
||||
),
|
||||
gridDotsSize: requireNumber(
|
||||
settings.grid_dots_size,
|
||||
"grid_dots_size",
|
||||
2,
|
||||
28,
|
||||
),
|
||||
gridDotsColor: requireHexColor(settings.grid_dots_color, "grid_dots_color"),
|
||||
gridDotsOpacity: requireNumber(
|
||||
settings.grid_dots_opacity,
|
||||
"grid_dots_opacity",
|
||||
0,
|
||||
100,
|
||||
),
|
||||
cameraAnimationEnabled: requireBoolean(
|
||||
settings.camera_animation_enabled,
|
||||
@@ -317,6 +572,16 @@ function requireBoolean(value: unknown, path: string): boolean {
|
||||
return value;
|
||||
}
|
||||
|
||||
function requireHexColor(value: unknown, path: string): string {
|
||||
if (
|
||||
typeof value !== "string" ||
|
||||
!/^#[0-9a-f]{6}$/i.test(value)
|
||||
) {
|
||||
throw new Error(`${path} должен быть цветом #RRGGBB.`);
|
||||
}
|
||||
return value.toLowerCase();
|
||||
}
|
||||
|
||||
function requireNumber(
|
||||
value: unknown,
|
||||
path: string,
|
||||
|
||||
@@ -16,36 +16,24 @@ import { mapPageTemplate } from "@nodedc/page-patterns";
|
||||
import {
|
||||
Button,
|
||||
Checker,
|
||||
ControlRow,
|
||||
Icon,
|
||||
IconButton,
|
||||
Inspector,
|
||||
MapGlassSurface,
|
||||
RangeControl,
|
||||
StatusBadge,
|
||||
Window,
|
||||
WindowFooterActions,
|
||||
} from "@nodedc/ui-react";
|
||||
|
||||
import { MapSettingsInspector } from "../../components/map/MapSettingsInspector";
|
||||
import {
|
||||
cloneMapViewDocument,
|
||||
defaultMapViewDocument,
|
||||
type MapInspectorSection,
|
||||
type MapViewDocument,
|
||||
} from "../../core/map/mapView";
|
||||
import { useMapGatewayHealth } from "../../core/map/useMapGatewayHealth";
|
||||
import { useMapView } from "../../core/map/useMapView";
|
||||
import type { WorkspaceDefinition } from "../../productModel";
|
||||
|
||||
const editableInspectorSections = new Set<MapInspectorSection>([
|
||||
"base-terrain",
|
||||
"atmosphere-light",
|
||||
"buildings",
|
||||
"grid-lod",
|
||||
"camera",
|
||||
"tile-cache",
|
||||
]);
|
||||
|
||||
export function WorldMapWorkspace({
|
||||
definition,
|
||||
}: {
|
||||
@@ -174,12 +162,57 @@ export function WorldMapWorkspace({
|
||||
layers={draft.view.layerVisibility}
|
||||
settings={{
|
||||
atmosphere_enabled: draft.view.visualSettings.atmosphereEnabled,
|
||||
lighting_enabled: draft.view.visualSettings.lightingEnabled,
|
||||
atmosphere_hue: draft.view.visualSettings.atmosphereHue,
|
||||
atmosphere_saturation:
|
||||
draft.view.visualSettings.atmosphereSaturation,
|
||||
atmosphere_brightness:
|
||||
draft.view.visualSettings.atmosphereBrightness,
|
||||
fog_enabled: draft.view.visualSettings.fogEnabled,
|
||||
fog_density: draft.view.visualSettings.fogDensity,
|
||||
sun_enabled: draft.view.visualSettings.sunEnabled,
|
||||
sun_hour: draft.view.visualSettings.sunHour,
|
||||
sun_intensity: draft.view.visualSettings.sunIntensity,
|
||||
shadows_enabled: draft.view.visualSettings.shadowsEnabled,
|
||||
monochrome_enabled: draft.view.visualSettings.monochromeEnabled,
|
||||
terrain_exaggeration: draft.view.visualSettings.terrainExaggeration,
|
||||
monochrome_color: draft.view.visualSettings.monochromeColor,
|
||||
imagery_brightness: draft.view.visualSettings.imageryBrightness,
|
||||
imagery_contrast: draft.view.visualSettings.imageryContrast,
|
||||
imagery_saturation: draft.view.visualSettings.imagerySaturation,
|
||||
imagery_gamma: draft.view.visualSettings.imageryGamma,
|
||||
imagery_hue: draft.view.visualSettings.imageryHue,
|
||||
imagery_alpha: draft.view.visualSettings.imageryAlpha,
|
||||
globe_color: draft.view.visualSettings.globeColor,
|
||||
background_color: draft.view.visualSettings.backgroundColor,
|
||||
terrain_exaggeration:
|
||||
draft.view.visualSettings.terrainExaggeration,
|
||||
buildings_color: draft.view.visualSettings.buildingsColor,
|
||||
buildings_opacity: draft.view.visualSettings.buildingsOpacity,
|
||||
buildings_maximum_screen_space_error:
|
||||
draft.view.visualSettings.buildingsMaximumScreenSpaceError,
|
||||
camera_animation_enabled: draft.view.visualSettings.cameraAnimationEnabled,
|
||||
grid_lod_enabled: draft.view.visualSettings.gridLodEnabled,
|
||||
grid_height_meters: draft.view.visualSettings.gridHeightMeters,
|
||||
grid_lod_1_max_height_km:
|
||||
draft.view.visualSettings.gridLod1MaxHeightKm,
|
||||
grid_lod_1_step_km:
|
||||
draft.view.visualSettings.gridLod1StepKm,
|
||||
grid_lod_2_max_height_km:
|
||||
draft.view.visualSettings.gridLod2MaxHeightKm,
|
||||
grid_lod_2_step_km:
|
||||
draft.view.visualSettings.gridLod2StepKm,
|
||||
grid_lod_3_step_km:
|
||||
draft.view.visualSettings.gridLod3StepKm,
|
||||
grid_radius_km: draft.view.visualSettings.gridRadiusKm,
|
||||
grid_line_width: draft.view.visualSettings.gridLineWidth,
|
||||
grid_color: draft.view.visualSettings.gridColor,
|
||||
grid_opacity: draft.view.visualSettings.gridOpacity,
|
||||
grid_dots_enabled:
|
||||
draft.view.visualSettings.gridDotsEnabled,
|
||||
grid_dots_size: draft.view.visualSettings.gridDotsSize,
|
||||
grid_dots_color: draft.view.visualSettings.gridDotsColor,
|
||||
grid_dots_opacity:
|
||||
draft.view.visualSettings.gridDotsOpacity,
|
||||
camera_animation_enabled:
|
||||
draft.view.visualSettings.cameraAnimationEnabled,
|
||||
}}
|
||||
cacheIntent={{
|
||||
enabled: draft.view.cacheIntent.enabled,
|
||||
@@ -338,22 +371,10 @@ export function WorldMapWorkspace({
|
||||
</WindowFooterActions>
|
||||
)}
|
||||
>
|
||||
<Inspector
|
||||
singleOpen
|
||||
sections={createInspectorSections(draft, updateDraft, gatewayHealth)}
|
||||
openSections={draft.view.inspectorOpenSections.filter(
|
||||
(section) => editableInspectorSections.has(section),
|
||||
)}
|
||||
activeId={draft.view.inspectorOpenSections[0]}
|
||||
onOpenSectionsChange={(sections) => {
|
||||
updateDraft((current) => {
|
||||
current.view.inspectorOpenSections = sections.filter(
|
||||
(section): section is MapInspectorSection =>
|
||||
editableInspectorSections.has(section as MapInspectorSection),
|
||||
).slice(0, 1);
|
||||
return current;
|
||||
});
|
||||
}}
|
||||
<MapSettingsInspector
|
||||
draft={draft}
|
||||
updateDraft={updateDraft}
|
||||
gatewayHealth={gatewayHealth}
|
||||
/>
|
||||
</Window>
|
||||
</div>
|
||||
@@ -380,249 +401,6 @@ function LayerControl({
|
||||
);
|
||||
}
|
||||
|
||||
function createInspectorSections(
|
||||
draft: MapViewDocument,
|
||||
updateDraft: (update: (current: MapViewDocument) => MapViewDocument) => void,
|
||||
gatewayHealth: ReturnType<typeof useMapGatewayHealth>,
|
||||
) {
|
||||
const view = draft.view;
|
||||
return [
|
||||
{
|
||||
id: "base-terrain",
|
||||
label: "Подложка и terrain",
|
||||
description: "provider-neutral surface",
|
||||
content: (
|
||||
<div className="mission-map__inspector-controls">
|
||||
<Checker
|
||||
label="Спутниковая подложка"
|
||||
checked={view.layerVisibility.imagery}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.layerVisibility.imagery = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<Checker
|
||||
label="Рельеф"
|
||||
checked={view.layerVisibility.terrain}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.layerVisibility.terrain = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<Checker
|
||||
label="Монохромная подложка"
|
||||
checked={view.visualSettings.monochromeEnabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.visualSettings.monochromeEnabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Высота карты"
|
||||
value={view.mapHeight}
|
||||
min={420}
|
||||
max={1080}
|
||||
step={20}
|
||||
formatValue={(value) => `${value} px`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.mapHeight = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "atmosphere-light",
|
||||
label: "Атмосфера и освещение",
|
||||
description: "scene / color correction",
|
||||
content: (
|
||||
<div className="mission-map__inspector-controls">
|
||||
<Checker
|
||||
label="Атмосфера"
|
||||
checked={view.visualSettings.atmosphereEnabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.visualSettings.atmosphereEnabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<Checker
|
||||
label="Освещение глобуса"
|
||||
checked={view.visualSettings.lightingEnabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.visualSettings.lightingEnabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "buildings",
|
||||
label: "3D здания",
|
||||
description: "3D Tiles / detail",
|
||||
content: (
|
||||
<div className="mission-map__inspector-controls">
|
||||
<Checker
|
||||
label="Показывать 3D-здания"
|
||||
checked={view.layerVisibility.buildings}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.layerVisibility.buildings = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Детализация"
|
||||
value={view.visualSettings.buildingsMaximumScreenSpaceError}
|
||||
min={1}
|
||||
max={64}
|
||||
step={1}
|
||||
formatValue={(value) => `${value} SSE`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.buildingsMaximumScreenSpaceError = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "grid-lod",
|
||||
label: "Сетка и LOD",
|
||||
description: "first adapter control",
|
||||
content: (
|
||||
<div className="mission-map__inspector-controls">
|
||||
<Checker
|
||||
label="Планетарная сетка"
|
||||
checked={view.layerVisibility.grid}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.layerVisibility.grid = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<RangeControl
|
||||
label="Масштаб рельефа"
|
||||
value={view.visualSettings.terrainExaggeration}
|
||||
min={0.1}
|
||||
max={8}
|
||||
step={0.1}
|
||||
formatValue={(value) => `${value.toFixed(1)}×`}
|
||||
onChange={(value) => updateDraft((current) => {
|
||||
current.view.visualSettings.terrainExaggeration = value;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "camera",
|
||||
label: "Анимация камеры",
|
||||
description: "geodesic spiral survey",
|
||||
content: (
|
||||
<div className="mission-map__inspector-controls">
|
||||
<Checker
|
||||
label="Разрешить анимацию камеры"
|
||||
checked={view.visualSettings.cameraAnimationEnabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.visualSettings.cameraAnimationEnabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<ControlRow label="Сохранённая позиция">
|
||||
<StatusBadge tone={view.camera ? "success" : "neutral"}>
|
||||
{view.camera ? "Есть" : "Не задана"}
|
||||
</StatusBadge>
|
||||
</ControlRow>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "tile-cache",
|
||||
label: "TileCache",
|
||||
description: "Platform Map Gateway",
|
||||
content: (
|
||||
<div
|
||||
className="mission-map__inspector-controls"
|
||||
data-health-code={gatewayHealth.code ?? undefined}
|
||||
>
|
||||
<Checker
|
||||
label="Использовать общий TileCache"
|
||||
checked={view.cacheIntent.enabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.cacheIntent.enabled = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<Checker
|
||||
label="Не перезаписывать существующие тайлы"
|
||||
checked={view.cacheIntent.noOverwrite}
|
||||
disabled={!view.cacheIntent.enabled}
|
||||
onChange={(checked) => updateDraft((current) => {
|
||||
current.view.cacheIntent.noOverwrite = checked;
|
||||
return current;
|
||||
})}
|
||||
/>
|
||||
<CacheHealth health={gatewayHealth} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function CacheHealth({
|
||||
health,
|
||||
}: {
|
||||
health: ReturnType<typeof useMapGatewayHealth>;
|
||||
}) {
|
||||
if (health.state === "loading" || health.state === "idle") {
|
||||
return (
|
||||
<ControlRow label="Состояние">
|
||||
<StatusBadge tone="neutral">Проверяем</StatusBadge>
|
||||
</ControlRow>
|
||||
);
|
||||
}
|
||||
if (!health.snapshot) {
|
||||
return (
|
||||
<ControlRow label="Состояние">
|
||||
<StatusBadge tone="danger">Недоступен</StatusBadge>
|
||||
</ControlRow>
|
||||
);
|
||||
}
|
||||
const { cache } = health.snapshot;
|
||||
return (
|
||||
<dl className="mission-map__cache-health">
|
||||
<div>
|
||||
<dt>Хранилище</dt>
|
||||
<dd>
|
||||
<StatusBadge tone={cache.persistent ? "success" : "warning"}>
|
||||
{cache.persistent ? "Постоянное" : "Временное"}
|
||||
</StatusBadge>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Объекты</dt>
|
||||
<dd>{cache.entries.toLocaleString("ru-RU")}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Объём</dt>
|
||||
<dd>
|
||||
{formatBytes(cache.bytes)}
|
||||
{cache.maxBytes === null ? "" : ` / ${formatBytes(cache.maxBytes)}`}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Заполнение</dt>
|
||||
<dd>
|
||||
<StatusBadge tone={cache.atCapacity ? "warning" : health.state === "stale" ? "warning" : "success"}>
|
||||
{cache.atCapacity ? "Лимит" : health.state === "stale" ? "Устарело" : "Норма"}
|
||||
</StatusBadge>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
|
||||
function runtimePresentation(state: MapRuntimeState): {
|
||||
label: string;
|
||||
tone: "neutral" | "success" | "warning" | "danger";
|
||||
@@ -650,15 +428,3 @@ function providerPresentation(
|
||||
if (provider.phase === "error") return { label: "Недоступен", tone: "danger" };
|
||||
return { label: "Загрузка", tone: "neutral" };
|
||||
}
|
||||
|
||||
function formatBytes(value: number): string {
|
||||
if (value < 1024) return `${value} Б`;
|
||||
const units = ["КБ", "МБ", "ГБ", "ТБ"];
|
||||
let amount = value / 1024;
|
||||
let unit = units[0];
|
||||
for (let index = 1; index < units.length && amount >= 1024; index += 1) {
|
||||
amount /= 1024;
|
||||
unit = units[index];
|
||||
}
|
||||
return `${amount.toFixed(amount >= 10 ? 1 : 2)} ${unit}`;
|
||||
}
|
||||
|
||||
@@ -22,19 +22,60 @@ after(async () => {
|
||||
|
||||
function serverDocument(overrides = {}) {
|
||||
return {
|
||||
schema_version: "missioncore.map-view/v1",
|
||||
schema_version: "missioncore.map-view/v2",
|
||||
revision: 0,
|
||||
view: {
|
||||
camera: null,
|
||||
camera: {
|
||||
longitude: 37.618423,
|
||||
latitude: 55.751244,
|
||||
height: 40000,
|
||||
heading: 0,
|
||||
pitch: -51.56620156177409,
|
||||
roll: 0,
|
||||
},
|
||||
visual_settings: {
|
||||
atmosphere_enabled: true,
|
||||
lighting_enabled: true,
|
||||
atmosphere_enabled: false,
|
||||
atmosphere_hue: 0,
|
||||
atmosphere_saturation: 0,
|
||||
atmosphere_brightness: 0,
|
||||
fog_enabled: true,
|
||||
fog_density: 2,
|
||||
sun_enabled: true,
|
||||
sun_hour: 12,
|
||||
sun_intensity: 200,
|
||||
shadows_enabled: true,
|
||||
monochrome_enabled: false,
|
||||
monochrome_color: "#15151b",
|
||||
imagery_brightness: 118,
|
||||
imagery_contrast: 102,
|
||||
imagery_saturation: 0,
|
||||
imagery_gamma: 57,
|
||||
imagery_hue: 13,
|
||||
imagery_alpha: 27,
|
||||
globe_color: "#15151b",
|
||||
background_color: "#08090d",
|
||||
terrain_exaggeration: 1,
|
||||
buildings_maximum_screen_space_error: 16,
|
||||
buildings_color: "#a27aff",
|
||||
buildings_opacity: 1,
|
||||
buildings_maximum_screen_space_error: 4,
|
||||
grid_lod_enabled: true,
|
||||
grid_height_meters: 500,
|
||||
grid_lod_1_max_height_km: 10,
|
||||
grid_lod_1_step_km: 1,
|
||||
grid_lod_2_max_height_km: 50,
|
||||
grid_lod_2_step_km: 5,
|
||||
grid_lod_3_step_km: 25,
|
||||
grid_radius_km: 40,
|
||||
grid_line_width: 4,
|
||||
grid_color: "#f5f5f5",
|
||||
grid_opacity: 12,
|
||||
grid_dots_enabled: true,
|
||||
grid_dots_size: 7,
|
||||
grid_dots_color: "#ffffff",
|
||||
grid_dots_opacity: 58,
|
||||
camera_animation_enabled: true,
|
||||
},
|
||||
map_height: 720,
|
||||
map_height: 694,
|
||||
inspector_open_sections: [],
|
||||
cache_intent: {
|
||||
enabled: true,
|
||||
@@ -45,7 +86,7 @@ function serverDocument(overrides = {}) {
|
||||
imagery: true,
|
||||
terrain: true,
|
||||
buildings: true,
|
||||
grid: false,
|
||||
grid: true,
|
||||
targets: true,
|
||||
},
|
||||
},
|
||||
@@ -53,9 +94,12 @@ function serverDocument(overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
test("map view starts without a fabricated global camera or subject", () => {
|
||||
test("map view starts from the canonical Foundry camera without a fabricated subject", () => {
|
||||
const document = mapView.defaultMapViewDocument();
|
||||
assert.equal(document.view.camera, null);
|
||||
assert.equal(document.view.camera.longitude, 37.618423);
|
||||
assert.equal(document.view.camera.latitude, 55.751244);
|
||||
assert.equal(document.view.visualSettings.imagerySaturation, 0);
|
||||
assert.equal(document.view.visualSettings.gridDotsOpacity, 58);
|
||||
assert.equal(document.view.selectedSubjectId, null);
|
||||
assert.equal(document.view.cacheIntent.enabled, true);
|
||||
assert.equal(document.view.cacheIntent.noOverwrite, true);
|
||||
|
||||
@@ -194,8 +194,22 @@ the existing Gateway is reachable and reports a configured state.
|
||||
|
||||
## Map state contract
|
||||
|
||||
Add a versioned Mission Core map document outside operator environment media,
|
||||
for example `missioncore.map-view/v1`, with optimistic revision:
|
||||
Mission Core stores a versioned map document outside operator environment
|
||||
media. The implemented contract is `missioncore.map-view/v2`, with optimistic
|
||||
revision:
|
||||
|
||||
- its default camera and complete visual state are the canonical Foundry
|
||||
`DC Default` profile;
|
||||
- black-and-white presentation is produced by zero imagery saturation while
|
||||
imagery remains active;
|
||||
- atmosphere, fog, sun, shadows, terrain, buildings and the elevated LOD grid
|
||||
are persisted as provider-neutral values;
|
||||
- saved `missioncore.map-view/v1` documents are upgraded in memory and become
|
||||
v2 on the next explicit save;
|
||||
- provider attribution remains registered in the renderer. The local
|
||||
experiment may hide only the visual credit overlay through
|
||||
`MISSIONCORE_MAP_SANDBOX_HIDE_CREDITS=1`; production defaults to visible
|
||||
credits and must not enable that sandbox flag.
|
||||
|
||||
- camera: longitude, latitude, height, heading, pitch and roll;
|
||||
- provider-neutral visual settings compatible with Map Page;
|
||||
|
||||
@@ -13,6 +13,7 @@ from fastapi import APIRouter, HTTPException, Path, Query, Request
|
||||
from fastapi.responses import JSONResponse, Response, StreamingResponse
|
||||
|
||||
MAP_GATEWAY_URL_ENV: Final = "MISSIONCORE_MAP_GATEWAY_INTERNAL_URL"
|
||||
MAP_SANDBOX_HIDE_CREDITS_ENV: Final = "MISSIONCORE_MAP_SANDBOX_HIDE_CREDITS"
|
||||
MAP_SCHEMA_VERSION: Final = "missioncore.map-gateway/v1"
|
||||
MAP_RUNTIME_SCHEMA_VERSION: Final = "missioncore.map-runtime/v1"
|
||||
MAP_PAGE_VERSION: Final = "0.1.0"
|
||||
@@ -149,6 +150,11 @@ class MapGatewayProxy:
|
||||
"terrain": 1,
|
||||
"buildings": 96188,
|
||||
},
|
||||
"sandbox": {
|
||||
"hide_credit_overlay": _environment_flag(
|
||||
MAP_SANDBOX_HIDE_CREDITS_ENV
|
||||
)
|
||||
},
|
||||
},
|
||||
headers={"Cache-Control": "no-store"},
|
||||
)
|
||||
@@ -309,6 +315,15 @@ def _validate_internal_url(raw_url: str) -> str:
|
||||
return raw_url.rstrip("/")
|
||||
|
||||
|
||||
def _environment_flag(name: str) -> bool:
|
||||
return os.environ.get(name, "").strip().lower() in {
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
"on",
|
||||
}
|
||||
|
||||
|
||||
def _validate_cache_target(target_url: str) -> None:
|
||||
if len(target_url) > MAX_CACHE_TARGET_LENGTH:
|
||||
raise HTTPException(status_code=422, detail="Map resource URL is too long.")
|
||||
|
||||
+128
-15
@@ -10,7 +10,7 @@ from typing import Literal
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
MAP_VIEW_SCHEMA_VERSION: Literal["missioncore.map-view/v1"] = "missioncore.map-view/v1"
|
||||
MAP_VIEW_SCHEMA_VERSION: Literal["missioncore.map-view/v2"] = "missioncore.map-view/v2"
|
||||
MapInspectorSection = Literal[
|
||||
"base-terrain",
|
||||
"atmosphere-light",
|
||||
@@ -37,15 +37,49 @@ class MapCamera(StrictMapModel):
|
||||
|
||||
|
||||
class MapVisualSettings(StrictMapModel):
|
||||
atmosphere_enabled: bool = True
|
||||
lighting_enabled: bool = True
|
||||
atmosphere_enabled: bool = False
|
||||
atmosphere_hue: float = Field(default=0.0, ge=-100.0, le=100.0)
|
||||
atmosphere_saturation: float = Field(default=0.0, ge=-100.0, le=100.0)
|
||||
atmosphere_brightness: float = Field(default=0.0, ge=-100.0, le=100.0)
|
||||
fog_enabled: bool = True
|
||||
fog_density: float = Field(default=2.0, ge=0.0, le=100.0)
|
||||
sun_enabled: bool = True
|
||||
sun_hour: float = Field(default=12.0, ge=0.0, le=24.0)
|
||||
sun_intensity: float = Field(default=200.0, ge=0.0, le=200.0)
|
||||
shadows_enabled: bool = True
|
||||
monochrome_enabled: bool = False
|
||||
terrain_exaggeration: float = Field(default=1.0, ge=0.1, le=20.0)
|
||||
monochrome_color: str = Field(default="#15151b", pattern=r"^#[0-9A-Fa-f]{6}$")
|
||||
imagery_brightness: float = Field(default=118.0, ge=0.0, le=200.0)
|
||||
imagery_contrast: float = Field(default=102.0, ge=0.0, le=200.0)
|
||||
imagery_saturation: float = Field(default=0.0, ge=0.0, le=200.0)
|
||||
imagery_gamma: float = Field(default=57.0, ge=0.0, le=300.0)
|
||||
imagery_hue: float = Field(default=13.0, ge=-180.0, le=180.0)
|
||||
imagery_alpha: float = Field(default=27.0, ge=0.0, le=100.0)
|
||||
globe_color: str = Field(default="#15151b", pattern=r"^#[0-9A-Fa-f]{6}$")
|
||||
background_color: str = Field(default="#08090d", pattern=r"^#[0-9A-Fa-f]{6}$")
|
||||
terrain_exaggeration: float = Field(default=1.0, ge=0.25, le=3.0)
|
||||
buildings_color: str = Field(default="#a27aff", pattern=r"^#[0-9A-Fa-f]{6}$")
|
||||
buildings_opacity: float = Field(default=1.0, ge=0.0, le=1.0)
|
||||
buildings_maximum_screen_space_error: float = Field(
|
||||
default=16.0,
|
||||
ge=1.0,
|
||||
le=64.0,
|
||||
default=4.0,
|
||||
ge=4.0,
|
||||
le=32.0,
|
||||
)
|
||||
grid_lod_enabled: bool = True
|
||||
grid_height_meters: float = Field(default=500.0, ge=0.0, le=1000.0)
|
||||
grid_lod_1_max_height_km: float = Field(default=10.0, ge=1.0, le=50.0)
|
||||
grid_lod_1_step_km: float = Field(default=1.0, ge=1.0, le=10.0)
|
||||
grid_lod_2_max_height_km: float = Field(default=50.0, ge=10.0, le=200.0)
|
||||
grid_lod_2_step_km: float = Field(default=5.0, ge=1.0, le=25.0)
|
||||
grid_lod_3_step_km: float = Field(default=25.0, ge=5.0, le=100.0)
|
||||
grid_radius_km: float = Field(default=40.0, ge=5.0, le=150.0)
|
||||
grid_line_width: float = Field(default=4.0, ge=1.0, le=8.0)
|
||||
grid_color: str = Field(default="#f5f5f5", pattern=r"^#[0-9A-Fa-f]{6}$")
|
||||
grid_opacity: float = Field(default=12.0, ge=0.0, le=100.0)
|
||||
grid_dots_enabled: bool = True
|
||||
grid_dots_size: float = Field(default=7.0, ge=2.0, le=28.0)
|
||||
grid_dots_color: str = Field(default="#ffffff", pattern=r"^#[0-9A-Fa-f]{6}$")
|
||||
grid_dots_opacity: float = Field(default=58.0, ge=0.0, le=100.0)
|
||||
camera_animation_enabled: bool = True
|
||||
|
||||
|
||||
@@ -53,7 +87,7 @@ class MapLayerVisibility(StrictMapModel):
|
||||
imagery: bool = True
|
||||
terrain: bool = True
|
||||
buildings: bool = True
|
||||
grid: bool = False
|
||||
grid: bool = True
|
||||
targets: bool = True
|
||||
|
||||
|
||||
@@ -63,9 +97,18 @@ class MapCacheIntent(StrictMapModel):
|
||||
|
||||
|
||||
class MapViewContent(StrictMapModel):
|
||||
camera: MapCamera | None = None
|
||||
camera: MapCamera | None = Field(
|
||||
default_factory=lambda: MapCamera(
|
||||
longitude=37.618423,
|
||||
latitude=55.751244,
|
||||
height=40_000.0,
|
||||
heading=0.0,
|
||||
pitch=-51.56620156177409,
|
||||
roll=0.0,
|
||||
)
|
||||
)
|
||||
visual_settings: MapVisualSettings = Field(default_factory=MapVisualSettings)
|
||||
map_height: int = Field(default=720, ge=420, le=2160)
|
||||
map_height: int = Field(default=694, ge=420, le=2160)
|
||||
inspector_open_sections: list[MapInspectorSection] = Field(
|
||||
default_factory=list,
|
||||
max_length=1,
|
||||
@@ -91,7 +134,43 @@ class MapViewPut(StrictMapModel):
|
||||
|
||||
|
||||
class MapViewDocument(MapViewPut):
|
||||
schema_version: Literal["missioncore.map-view/v1"] = MAP_VIEW_SCHEMA_VERSION
|
||||
schema_version: Literal["missioncore.map-view/v2"] = MAP_VIEW_SCHEMA_VERSION
|
||||
|
||||
|
||||
class LegacyMapVisualSettings(StrictMapModel):
|
||||
atmosphere_enabled: bool = True
|
||||
lighting_enabled: bool = True
|
||||
monochrome_enabled: bool = False
|
||||
terrain_exaggeration: float = Field(default=1.0, ge=0.1, le=20.0)
|
||||
buildings_maximum_screen_space_error: float = Field(
|
||||
default=16.0,
|
||||
ge=1.0,
|
||||
le=64.0,
|
||||
)
|
||||
camera_animation_enabled: bool = True
|
||||
|
||||
|
||||
class LegacyMapViewContent(StrictMapModel):
|
||||
camera: MapCamera | None = None
|
||||
visual_settings: LegacyMapVisualSettings = Field(default_factory=LegacyMapVisualSettings)
|
||||
map_height: int = Field(default=720, ge=420, le=2160)
|
||||
inspector_open_sections: list[MapInspectorSection] = Field(
|
||||
default_factory=list,
|
||||
max_length=1,
|
||||
)
|
||||
cache_intent: MapCacheIntent = Field(default_factory=MapCacheIntent)
|
||||
selected_subject_id: str | None = Field(
|
||||
default=None,
|
||||
max_length=160,
|
||||
pattern=r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$",
|
||||
)
|
||||
layer_visibility: MapLayerVisibility = Field(default_factory=MapLayerVisibility)
|
||||
|
||||
|
||||
class LegacyMapViewDocument(StrictMapModel):
|
||||
schema_version: Literal["missioncore.map-view/v1"]
|
||||
revision: int = Field(ge=0)
|
||||
view: LegacyMapViewContent
|
||||
|
||||
|
||||
def default_map_view() -> MapViewDocument:
|
||||
@@ -101,6 +180,36 @@ def default_map_view() -> MapViewDocument:
|
||||
)
|
||||
|
||||
|
||||
def upgrade_legacy_map_view(document: LegacyMapViewDocument) -> MapViewDocument:
|
||||
legacy_settings = document.view.visual_settings
|
||||
settings = MapVisualSettings(
|
||||
atmosphere_enabled=legacy_settings.atmosphere_enabled,
|
||||
sun_enabled=legacy_settings.lighting_enabled,
|
||||
monochrome_enabled=legacy_settings.monochrome_enabled,
|
||||
terrain_exaggeration=min(
|
||||
3.0,
|
||||
max(0.25, legacy_settings.terrain_exaggeration),
|
||||
),
|
||||
buildings_maximum_screen_space_error=min(
|
||||
32.0,
|
||||
max(4.0, legacy_settings.buildings_maximum_screen_space_error),
|
||||
),
|
||||
camera_animation_enabled=legacy_settings.camera_animation_enabled,
|
||||
)
|
||||
return MapViewDocument(
|
||||
revision=document.revision,
|
||||
view=MapViewContent(
|
||||
camera=document.view.camera,
|
||||
visual_settings=settings,
|
||||
map_height=document.view.map_height,
|
||||
inspector_open_sections=document.view.inspector_open_sections,
|
||||
cache_intent=document.view.cache_intent,
|
||||
selected_subject_id=document.view.selected_subject_id,
|
||||
layer_visibility=document.view.layer_visibility,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class MapViewStore:
|
||||
def __init__(self, root: Path) -> None:
|
||||
self.root = root.expanduser().resolve()
|
||||
@@ -138,10 +247,14 @@ class MapViewStore:
|
||||
if not self.document_path.is_file():
|
||||
return default_map_view()
|
||||
try:
|
||||
return MapViewDocument.model_validate_json(
|
||||
self.document_path.read_text(encoding="utf-8")
|
||||
)
|
||||
except (OSError, ValueError) as exc:
|
||||
payload = json.loads(self.document_path.read_text(encoding="utf-8"))
|
||||
if (
|
||||
isinstance(payload, dict)
|
||||
and payload.get("schema_version") == "missioncore.map-view/v1"
|
||||
):
|
||||
return upgrade_legacy_map_view(LegacyMapViewDocument.model_validate(payload))
|
||||
return MapViewDocument.model_validate(payload)
|
||||
except (OSError, ValueError, TypeError) as exc:
|
||||
raise RuntimeError("map view is corrupt") from exc
|
||||
|
||||
|
||||
|
||||
+19
-1
@@ -13,6 +13,7 @@ from fastapi.routing import APIRoute
|
||||
|
||||
from k1link.web.map_api import (
|
||||
MAP_GATEWAY_URL_ENV,
|
||||
MAP_SANDBOX_HIDE_CREDITS_ENV,
|
||||
MapGatewayConfiguration,
|
||||
MapGatewayProxy,
|
||||
build_map_router,
|
||||
@@ -108,7 +109,10 @@ def test_configuration_is_fail_closed_and_rejects_non_origin_values(
|
||||
MapGatewayConfiguration.from_environment()
|
||||
|
||||
|
||||
def test_runtime_config_exposes_only_same_origin_contract_and_hides_internal_origin() -> None:
|
||||
def test_runtime_config_exposes_only_same_origin_contract_and_hides_internal_origin(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.delenv(MAP_SANDBOX_HIDE_CREDITS_ENV, raising=False)
|
||||
service, requests = _service(
|
||||
lambda request: httpx.Response(
|
||||
200,
|
||||
@@ -133,11 +137,25 @@ def test_runtime_config_exposes_only_same_origin_contract_and_hides_internal_ori
|
||||
"terrain": 1,
|
||||
"buildings": 96188,
|
||||
}
|
||||
assert document["sandbox"] == {"hide_credit_overlay": False}
|
||||
assert response.headers["cache-control"] == "no-store"
|
||||
assert not requests
|
||||
assert "map-gateway.internal" not in _response_body(response).decode()
|
||||
|
||||
|
||||
def test_runtime_config_hides_credits_only_with_explicit_sandbox_flag(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
service, _ = _service(lambda request: httpx.Response(200))
|
||||
monkeypatch.setenv(MAP_SANDBOX_HIDE_CREDITS_ENV, "true")
|
||||
|
||||
response = service.runtime_configuration()
|
||||
|
||||
assert isinstance(response, JSONResponse)
|
||||
document = json.loads(_response_body(response))
|
||||
assert document["sandbox"] == {"hide_credit_overlay": True}
|
||||
|
||||
|
||||
def test_runtime_config_is_fail_closed_when_gateway_is_not_configured() -> None:
|
||||
service = MapGatewayProxy(MapGatewayConfiguration(None))
|
||||
response = service.runtime_configuration()
|
||||
|
||||
@@ -15,15 +15,23 @@ from k1link.web.map_view_api import (
|
||||
)
|
||||
|
||||
|
||||
def test_default_map_view_is_honestly_unlocated_and_cache_safe() -> None:
|
||||
def test_default_map_view_matches_the_canonical_foundry_scene_and_cache_policy() -> None:
|
||||
document = default_map_view()
|
||||
|
||||
assert document.schema_version == "missioncore.map-view/v1"
|
||||
assert document.schema_version == "missioncore.map-view/v2"
|
||||
assert document.revision == 0
|
||||
assert document.view.camera is None
|
||||
assert document.view.camera is not None
|
||||
assert document.view.camera.longitude == pytest.approx(37.618423)
|
||||
assert document.view.camera.latitude == pytest.approx(55.751244)
|
||||
assert document.view.camera.height == pytest.approx(40_000.0)
|
||||
assert document.view.selected_subject_id is None
|
||||
assert document.view.layer_visibility.imagery is True
|
||||
assert document.view.layer_visibility.grid is True
|
||||
assert document.view.layer_visibility.targets is True
|
||||
assert document.view.visual_settings.imagery_saturation == 0
|
||||
assert document.view.visual_settings.imagery_alpha == 27
|
||||
assert document.view.visual_settings.imagery_gamma == 57
|
||||
assert document.view.visual_settings.background_color == "#08090d"
|
||||
assert document.view.cache_intent.enabled is True
|
||||
assert document.view.cache_intent.no_overwrite is True
|
||||
|
||||
@@ -98,3 +106,47 @@ def test_persisted_document_contains_no_runtime_or_credential_material(
|
||||
assert "gateway" not in serialized
|
||||
assert "provider" not in serialized
|
||||
assert "cesium" not in serialized
|
||||
|
||||
|
||||
def test_v1_map_view_is_upgraded_without_rewriting_the_source_file(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
store = MapViewStore(tmp_path / "map-view")
|
||||
store.root.mkdir(parents=True)
|
||||
legacy = {
|
||||
"schema_version": "missioncore.map-view/v1",
|
||||
"revision": 7,
|
||||
"view": {
|
||||
"camera": None,
|
||||
"visual_settings": {
|
||||
"atmosphere_enabled": True,
|
||||
"lighting_enabled": False,
|
||||
"monochrome_enabled": True,
|
||||
"terrain_exaggeration": 2,
|
||||
"buildings_maximum_screen_space_error": 12,
|
||||
"camera_animation_enabled": False,
|
||||
},
|
||||
"map_height": 720,
|
||||
"inspector_open_sections": [],
|
||||
"cache_intent": {"enabled": True, "no_overwrite": True},
|
||||
"selected_subject_id": None,
|
||||
"layer_visibility": {
|
||||
"imagery": True,
|
||||
"terrain": True,
|
||||
"buildings": True,
|
||||
"grid": False,
|
||||
"targets": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
store.document_path.write_text(json.dumps(legacy), encoding="utf-8")
|
||||
|
||||
upgraded = store.read()
|
||||
|
||||
assert upgraded.schema_version == "missioncore.map-view/v2"
|
||||
assert upgraded.revision == 7
|
||||
assert upgraded.view.camera is None
|
||||
assert upgraded.view.visual_settings.atmosphere_enabled is True
|
||||
assert upgraded.view.visual_settings.sun_enabled is False
|
||||
assert upgraded.view.visual_settings.monochrome_enabled is True
|
||||
assert json.loads(store.document_path.read_text(encoding="utf-8")) == legacy
|
||||
|
||||
Reference in New Issue
Block a user