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 {
|
export interface MapVisualSettings {
|
||||||
atmosphereEnabled: boolean;
|
atmosphereEnabled: boolean;
|
||||||
lightingEnabled: boolean;
|
atmosphereHue: number;
|
||||||
|
atmosphereSaturation: number;
|
||||||
|
atmosphereBrightness: number;
|
||||||
|
fogEnabled: boolean;
|
||||||
|
fogDensity: number;
|
||||||
|
sunEnabled: boolean;
|
||||||
|
sunHour: number;
|
||||||
|
sunIntensity: number;
|
||||||
|
shadowsEnabled: boolean;
|
||||||
monochromeEnabled: boolean;
|
monochromeEnabled: boolean;
|
||||||
|
monochromeColor: string;
|
||||||
|
imageryBrightness: number;
|
||||||
|
imageryContrast: number;
|
||||||
|
imagerySaturation: number;
|
||||||
|
imageryGamma: number;
|
||||||
|
imageryHue: number;
|
||||||
|
imageryAlpha: number;
|
||||||
|
globeColor: string;
|
||||||
|
backgroundColor: string;
|
||||||
terrainExaggeration: number;
|
terrainExaggeration: number;
|
||||||
|
buildingsColor: string;
|
||||||
|
buildingsOpacity: number;
|
||||||
buildingsMaximumScreenSpaceError: 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;
|
cameraAnimationEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,16 +103,57 @@ export function defaultMapViewDocument(): MapViewDocument {
|
|||||||
return {
|
return {
|
||||||
revision: 0,
|
revision: 0,
|
||||||
view: {
|
view: {
|
||||||
camera: null,
|
camera: {
|
||||||
|
longitude: 37.618423,
|
||||||
|
latitude: 55.751244,
|
||||||
|
height: 40_000,
|
||||||
|
heading: 0,
|
||||||
|
pitch: -51.56620156177409,
|
||||||
|
roll: 0,
|
||||||
|
},
|
||||||
visualSettings: {
|
visualSettings: {
|
||||||
atmosphereEnabled: true,
|
atmosphereEnabled: false,
|
||||||
lightingEnabled: true,
|
atmosphereHue: 0,
|
||||||
|
atmosphereSaturation: 0,
|
||||||
|
atmosphereBrightness: 0,
|
||||||
|
fogEnabled: true,
|
||||||
|
fogDensity: 2,
|
||||||
|
sunEnabled: true,
|
||||||
|
sunHour: 12,
|
||||||
|
sunIntensity: 200,
|
||||||
|
shadowsEnabled: true,
|
||||||
monochromeEnabled: false,
|
monochromeEnabled: false,
|
||||||
|
monochromeColor: "#15151b",
|
||||||
|
imageryBrightness: 118,
|
||||||
|
imageryContrast: 102,
|
||||||
|
imagerySaturation: 0,
|
||||||
|
imageryGamma: 57,
|
||||||
|
imageryHue: 13,
|
||||||
|
imageryAlpha: 27,
|
||||||
|
globeColor: "#15151b",
|
||||||
|
backgroundColor: "#08090d",
|
||||||
terrainExaggeration: 1,
|
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,
|
cameraAnimationEnabled: true,
|
||||||
},
|
},
|
||||||
mapHeight: 720,
|
mapHeight: 694,
|
||||||
inspectorOpenSections: [],
|
inspectorOpenSections: [],
|
||||||
cacheIntent: {
|
cacheIntent: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -89,7 +164,7 @@ export function defaultMapViewDocument(): MapViewDocument {
|
|||||||
imagery: true,
|
imagery: true,
|
||||||
terrain: true,
|
terrain: true,
|
||||||
buildings: true,
|
buildings: true,
|
||||||
grid: false,
|
grid: true,
|
||||||
targets: true,
|
targets: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -99,7 +174,7 @@ export function defaultMapViewDocument(): MapViewDocument {
|
|||||||
export function decodeMapViewDocument(value: unknown): MapViewDocument {
|
export function decodeMapViewDocument(value: unknown): MapViewDocument {
|
||||||
const document = requireRecord(value, "map view");
|
const document = requireRecord(value, "map view");
|
||||||
requireExactKeys(document, ["schema_version", "revision", "view"], "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("Версия сохранённого состояния карты не поддерживается.");
|
throw new Error("Версия сохранённого состояния карты не поддерживается.");
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -124,11 +199,45 @@ export function encodeMapViewPut(document: MapViewDocument): unknown {
|
|||||||
: null,
|
: null,
|
||||||
visual_settings: {
|
visual_settings: {
|
||||||
atmosphere_enabled: document.view.visualSettings.atmosphereEnabled,
|
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_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,
|
terrain_exaggeration: document.view.visualSettings.terrainExaggeration,
|
||||||
|
buildings_color: document.view.visualSettings.buildingsColor,
|
||||||
|
buildings_opacity: document.view.visualSettings.buildingsOpacity,
|
||||||
buildings_maximum_screen_space_error:
|
buildings_maximum_screen_space_error:
|
||||||
document.view.visualSettings.buildingsMaximumScreenSpaceError,
|
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,
|
camera_animation_enabled: document.view.visualSettings.cameraAnimationEnabled,
|
||||||
},
|
},
|
||||||
map_height: document.view.mapHeight,
|
map_height: document.view.mapHeight,
|
||||||
@@ -228,29 +337,175 @@ function decodeVisualSettings(value: unknown): MapVisualSettings {
|
|||||||
settings,
|
settings,
|
||||||
[
|
[
|
||||||
"atmosphere_enabled",
|
"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_enabled",
|
||||||
|
"monochrome_color",
|
||||||
|
"imagery_brightness",
|
||||||
|
"imagery_contrast",
|
||||||
|
"imagery_saturation",
|
||||||
|
"imagery_gamma",
|
||||||
|
"imagery_hue",
|
||||||
|
"imagery_alpha",
|
||||||
|
"globe_color",
|
||||||
|
"background_color",
|
||||||
"terrain_exaggeration",
|
"terrain_exaggeration",
|
||||||
|
"buildings_color",
|
||||||
|
"buildings_opacity",
|
||||||
"buildings_maximum_screen_space_error",
|
"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",
|
"camera_animation_enabled",
|
||||||
],
|
],
|
||||||
"map view.view.visual_settings",
|
"map view.view.visual_settings",
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
atmosphereEnabled: requireBoolean(settings.atmosphere_enabled, "atmosphere_enabled"),
|
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"),
|
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(
|
terrainExaggeration: requireNumber(
|
||||||
settings.terrain_exaggeration,
|
settings.terrain_exaggeration,
|
||||||
"terrain_exaggeration",
|
"terrain_exaggeration",
|
||||||
0.1,
|
0.25,
|
||||||
20,
|
3,
|
||||||
|
),
|
||||||
|
buildingsColor: requireHexColor(settings.buildings_color, "buildings_color"),
|
||||||
|
buildingsOpacity: requireNumber(
|
||||||
|
settings.buildings_opacity,
|
||||||
|
"buildings_opacity",
|
||||||
|
0,
|
||||||
|
1,
|
||||||
),
|
),
|
||||||
buildingsMaximumScreenSpaceError: requireNumber(
|
buildingsMaximumScreenSpaceError: requireNumber(
|
||||||
settings.buildings_maximum_screen_space_error,
|
settings.buildings_maximum_screen_space_error,
|
||||||
"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,
|
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(
|
cameraAnimationEnabled: requireBoolean(
|
||||||
settings.camera_animation_enabled,
|
settings.camera_animation_enabled,
|
||||||
@@ -317,6 +572,16 @@ function requireBoolean(value: unknown, path: string): boolean {
|
|||||||
return value;
|
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(
|
function requireNumber(
|
||||||
value: unknown,
|
value: unknown,
|
||||||
path: string,
|
path: string,
|
||||||
|
|||||||
@@ -16,36 +16,24 @@ import { mapPageTemplate } from "@nodedc/page-patterns";
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checker,
|
Checker,
|
||||||
ControlRow,
|
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
Inspector,
|
|
||||||
MapGlassSurface,
|
MapGlassSurface,
|
||||||
RangeControl,
|
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
Window,
|
Window,
|
||||||
WindowFooterActions,
|
WindowFooterActions,
|
||||||
} from "@nodedc/ui-react";
|
} from "@nodedc/ui-react";
|
||||||
|
|
||||||
|
import { MapSettingsInspector } from "../../components/map/MapSettingsInspector";
|
||||||
import {
|
import {
|
||||||
cloneMapViewDocument,
|
cloneMapViewDocument,
|
||||||
defaultMapViewDocument,
|
defaultMapViewDocument,
|
||||||
type MapInspectorSection,
|
|
||||||
type MapViewDocument,
|
type MapViewDocument,
|
||||||
} from "../../core/map/mapView";
|
} from "../../core/map/mapView";
|
||||||
import { useMapGatewayHealth } from "../../core/map/useMapGatewayHealth";
|
import { useMapGatewayHealth } from "../../core/map/useMapGatewayHealth";
|
||||||
import { useMapView } from "../../core/map/useMapView";
|
import { useMapView } from "../../core/map/useMapView";
|
||||||
import type { WorkspaceDefinition } from "../../productModel";
|
import type { WorkspaceDefinition } from "../../productModel";
|
||||||
|
|
||||||
const editableInspectorSections = new Set<MapInspectorSection>([
|
|
||||||
"base-terrain",
|
|
||||||
"atmosphere-light",
|
|
||||||
"buildings",
|
|
||||||
"grid-lod",
|
|
||||||
"camera",
|
|
||||||
"tile-cache",
|
|
||||||
]);
|
|
||||||
|
|
||||||
export function WorldMapWorkspace({
|
export function WorldMapWorkspace({
|
||||||
definition,
|
definition,
|
||||||
}: {
|
}: {
|
||||||
@@ -174,12 +162,57 @@ export function WorldMapWorkspace({
|
|||||||
layers={draft.view.layerVisibility}
|
layers={draft.view.layerVisibility}
|
||||||
settings={{
|
settings={{
|
||||||
atmosphere_enabled: draft.view.visualSettings.atmosphereEnabled,
|
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,
|
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:
|
buildings_maximum_screen_space_error:
|
||||||
draft.view.visualSettings.buildingsMaximumScreenSpaceError,
|
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={{
|
cacheIntent={{
|
||||||
enabled: draft.view.cacheIntent.enabled,
|
enabled: draft.view.cacheIntent.enabled,
|
||||||
@@ -338,22 +371,10 @@ export function WorldMapWorkspace({
|
|||||||
</WindowFooterActions>
|
</WindowFooterActions>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Inspector
|
<MapSettingsInspector
|
||||||
singleOpen
|
draft={draft}
|
||||||
sections={createInspectorSections(draft, updateDraft, gatewayHealth)}
|
updateDraft={updateDraft}
|
||||||
openSections={draft.view.inspectorOpenSections.filter(
|
gatewayHealth={gatewayHealth}
|
||||||
(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;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Window>
|
</Window>
|
||||||
</div>
|
</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): {
|
function runtimePresentation(state: MapRuntimeState): {
|
||||||
label: string;
|
label: string;
|
||||||
tone: "neutral" | "success" | "warning" | "danger";
|
tone: "neutral" | "success" | "warning" | "danger";
|
||||||
@@ -650,15 +428,3 @@ function providerPresentation(
|
|||||||
if (provider.phase === "error") return { label: "Недоступен", tone: "danger" };
|
if (provider.phase === "error") return { label: "Недоступен", tone: "danger" };
|
||||||
return { label: "Загрузка", tone: "neutral" };
|
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 = {}) {
|
function serverDocument(overrides = {}) {
|
||||||
return {
|
return {
|
||||||
schema_version: "missioncore.map-view/v1",
|
schema_version: "missioncore.map-view/v2",
|
||||||
revision: 0,
|
revision: 0,
|
||||||
view: {
|
view: {
|
||||||
camera: null,
|
camera: {
|
||||||
|
longitude: 37.618423,
|
||||||
|
latitude: 55.751244,
|
||||||
|
height: 40000,
|
||||||
|
heading: 0,
|
||||||
|
pitch: -51.56620156177409,
|
||||||
|
roll: 0,
|
||||||
|
},
|
||||||
visual_settings: {
|
visual_settings: {
|
||||||
atmosphere_enabled: true,
|
atmosphere_enabled: false,
|
||||||
lighting_enabled: true,
|
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_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,
|
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,
|
camera_animation_enabled: true,
|
||||||
},
|
},
|
||||||
map_height: 720,
|
map_height: 694,
|
||||||
inspector_open_sections: [],
|
inspector_open_sections: [],
|
||||||
cache_intent: {
|
cache_intent: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -45,7 +86,7 @@ function serverDocument(overrides = {}) {
|
|||||||
imagery: true,
|
imagery: true,
|
||||||
terrain: true,
|
terrain: true,
|
||||||
buildings: true,
|
buildings: true,
|
||||||
grid: false,
|
grid: true,
|
||||||
targets: 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();
|
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.selectedSubjectId, null);
|
||||||
assert.equal(document.view.cacheIntent.enabled, true);
|
assert.equal(document.view.cacheIntent.enabled, true);
|
||||||
assert.equal(document.view.cacheIntent.noOverwrite, 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
|
## Map state contract
|
||||||
|
|
||||||
Add a versioned Mission Core map document outside operator environment media,
|
Mission Core stores a versioned map document outside operator environment
|
||||||
for example `missioncore.map-view/v1`, with optimistic revision:
|
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;
|
- camera: longitude, latitude, height, heading, pitch and roll;
|
||||||
- provider-neutral visual settings compatible with Map Page;
|
- 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
|
from fastapi.responses import JSONResponse, Response, StreamingResponse
|
||||||
|
|
||||||
MAP_GATEWAY_URL_ENV: Final = "MISSIONCORE_MAP_GATEWAY_INTERNAL_URL"
|
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_SCHEMA_VERSION: Final = "missioncore.map-gateway/v1"
|
||||||
MAP_RUNTIME_SCHEMA_VERSION: Final = "missioncore.map-runtime/v1"
|
MAP_RUNTIME_SCHEMA_VERSION: Final = "missioncore.map-runtime/v1"
|
||||||
MAP_PAGE_VERSION: Final = "0.1.0"
|
MAP_PAGE_VERSION: Final = "0.1.0"
|
||||||
@@ -149,6 +150,11 @@ class MapGatewayProxy:
|
|||||||
"terrain": 1,
|
"terrain": 1,
|
||||||
"buildings": 96188,
|
"buildings": 96188,
|
||||||
},
|
},
|
||||||
|
"sandbox": {
|
||||||
|
"hide_credit_overlay": _environment_flag(
|
||||||
|
MAP_SANDBOX_HIDE_CREDITS_ENV
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
headers={"Cache-Control": "no-store"},
|
headers={"Cache-Control": "no-store"},
|
||||||
)
|
)
|
||||||
@@ -309,6 +315,15 @@ def _validate_internal_url(raw_url: str) -> str:
|
|||||||
return raw_url.rstrip("/")
|
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:
|
def _validate_cache_target(target_url: str) -> None:
|
||||||
if len(target_url) > MAX_CACHE_TARGET_LENGTH:
|
if len(target_url) > MAX_CACHE_TARGET_LENGTH:
|
||||||
raise HTTPException(status_code=422, detail="Map resource URL is too long.")
|
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 fastapi import APIRouter, HTTPException
|
||||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
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[
|
MapInspectorSection = Literal[
|
||||||
"base-terrain",
|
"base-terrain",
|
||||||
"atmosphere-light",
|
"atmosphere-light",
|
||||||
@@ -37,15 +37,49 @@ class MapCamera(StrictMapModel):
|
|||||||
|
|
||||||
|
|
||||||
class MapVisualSettings(StrictMapModel):
|
class MapVisualSettings(StrictMapModel):
|
||||||
atmosphere_enabled: bool = True
|
atmosphere_enabled: bool = False
|
||||||
lighting_enabled: bool = True
|
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
|
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(
|
buildings_maximum_screen_space_error: float = Field(
|
||||||
default=16.0,
|
default=4.0,
|
||||||
ge=1.0,
|
ge=4.0,
|
||||||
le=64.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
|
camera_animation_enabled: bool = True
|
||||||
|
|
||||||
|
|
||||||
@@ -53,7 +87,7 @@ class MapLayerVisibility(StrictMapModel):
|
|||||||
imagery: bool = True
|
imagery: bool = True
|
||||||
terrain: bool = True
|
terrain: bool = True
|
||||||
buildings: bool = True
|
buildings: bool = True
|
||||||
grid: bool = False
|
grid: bool = True
|
||||||
targets: bool = True
|
targets: bool = True
|
||||||
|
|
||||||
|
|
||||||
@@ -63,9 +97,18 @@ class MapCacheIntent(StrictMapModel):
|
|||||||
|
|
||||||
|
|
||||||
class MapViewContent(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)
|
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(
|
inspector_open_sections: list[MapInspectorSection] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
max_length=1,
|
max_length=1,
|
||||||
@@ -91,7 +134,43 @@ class MapViewPut(StrictMapModel):
|
|||||||
|
|
||||||
|
|
||||||
class MapViewDocument(MapViewPut):
|
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:
|
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:
|
class MapViewStore:
|
||||||
def __init__(self, root: Path) -> None:
|
def __init__(self, root: Path) -> None:
|
||||||
self.root = root.expanduser().resolve()
|
self.root = root.expanduser().resolve()
|
||||||
@@ -138,10 +247,14 @@ class MapViewStore:
|
|||||||
if not self.document_path.is_file():
|
if not self.document_path.is_file():
|
||||||
return default_map_view()
|
return default_map_view()
|
||||||
try:
|
try:
|
||||||
return MapViewDocument.model_validate_json(
|
payload = json.loads(self.document_path.read_text(encoding="utf-8"))
|
||||||
self.document_path.read_text(encoding="utf-8")
|
if (
|
||||||
)
|
isinstance(payload, dict)
|
||||||
except (OSError, ValueError) as exc:
|
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
|
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 (
|
from k1link.web.map_api import (
|
||||||
MAP_GATEWAY_URL_ENV,
|
MAP_GATEWAY_URL_ENV,
|
||||||
|
MAP_SANDBOX_HIDE_CREDITS_ENV,
|
||||||
MapGatewayConfiguration,
|
MapGatewayConfiguration,
|
||||||
MapGatewayProxy,
|
MapGatewayProxy,
|
||||||
build_map_router,
|
build_map_router,
|
||||||
@@ -108,7 +109,10 @@ def test_configuration_is_fail_closed_and_rejects_non_origin_values(
|
|||||||
MapGatewayConfiguration.from_environment()
|
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(
|
service, requests = _service(
|
||||||
lambda request: httpx.Response(
|
lambda request: httpx.Response(
|
||||||
200,
|
200,
|
||||||
@@ -133,11 +137,25 @@ def test_runtime_config_exposes_only_same_origin_contract_and_hides_internal_ori
|
|||||||
"terrain": 1,
|
"terrain": 1,
|
||||||
"buildings": 96188,
|
"buildings": 96188,
|
||||||
}
|
}
|
||||||
|
assert document["sandbox"] == {"hide_credit_overlay": False}
|
||||||
assert response.headers["cache-control"] == "no-store"
|
assert response.headers["cache-control"] == "no-store"
|
||||||
assert not requests
|
assert not requests
|
||||||
assert "map-gateway.internal" not in _response_body(response).decode()
|
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:
|
def test_runtime_config_is_fail_closed_when_gateway_is_not_configured() -> None:
|
||||||
service = MapGatewayProxy(MapGatewayConfiguration(None))
|
service = MapGatewayProxy(MapGatewayConfiguration(None))
|
||||||
response = service.runtime_configuration()
|
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()
|
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.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.selected_subject_id is None
|
||||||
assert document.view.layer_visibility.imagery is True
|
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.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.enabled is True
|
||||||
assert document.view.cache_intent.no_overwrite 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 "gateway" not in serialized
|
||||||
assert "provider" not in serialized
|
assert "provider" not in serialized
|
||||||
assert "cesium" 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