feat(map): add fixed ENU and WGS84 sector grid

This commit is contained in:
Codex
2026-08-06 10:31:03 +03:00
parent 80e948c018
commit 6f1fcb1eb0
15 changed files with 1806 additions and 362 deletions
+91 -59
View File
@@ -8,6 +8,8 @@ import type {
MapGatewayHealth,
MapPresentation,
MapProviderStatus,
GridLodProfile,
GridSectorSelection,
} from "./CesiumMapRenderer.js";
import { mapRuntimeEntityId, useMapDataProductRuntime } from "./useMapDataProductRuntime.js";
import {
@@ -40,6 +42,8 @@ import {
} from "./mapReferenceStations.js";
import { useMapReferenceRuntime, useMapReferenceSearch } from "./useMapReferenceRuntime.js";
import { buildMapSearchIndex, searchMapSubjects } from "./mapSearch.mjs";
import { DEFAULT_GRID_LOD_PROFILES, gridLodProfile } from "./mapGridPolicy.mjs";
import { MAX_LOCAL_GRID_INDEX } from "./mapSectorGrid.mjs";
const CesiumMapRenderer = lazy(() => import("./CesiumMapRenderer.js").then((module) => ({ default: module.CesiumMapRenderer })));
type PreviewFeatures = { inspector?: boolean; toolbar?: boolean; assistant?: boolean };
@@ -227,12 +231,13 @@ const initialMapSettings: MapPageSettings = {
gridLodEnabled: true,
grid3dEnabled: true,
gridGraticuleEnabled: true,
gridCenterMode: "camera",
gridCenterMode: "fixed",
gridCenterLatitude: 55.7558,
gridCenterLongitude: 37.6173,
gridTileSizeKm: 10,
gridAutoDisableHeightKm: 10_000,
gridRebuildOnMoveEnd: true,
gridLegacyMode: false,
gridMax3dViewAngleDegrees: 30,
gridHeightMeters: 500,
gridLod1MaxHeightKm: 10,
@@ -241,31 +246,47 @@ const initialMapSettings: MapPageSettings = {
gridLod2MaxHeightKm: 50,
gridLod2StepKm: 5,
gridLod2Mode: "3d",
gridLod3MaxHeightKm: 180,
gridLod3MaxHeightKm: 200,
gridLod3StepKm: 25,
gridLod3Mode: "3d",
gridLod4MaxHeightKm: 700,
gridLod4StepKm: 100,
gridLod4MaxHeightKm: 800,
gridLod4StepKm: 50,
gridLod4Mode: "graticule",
gridLod5StepKm: 500,
gridLod5MaxHeightKm: 3_000,
gridLod5StepKm: 50,
gridLod5Mode: "graticule",
gridRadiusKm: 1_000,
gridLineWidth: 4,
gridLineDiameterMeters: 10,
gridColor: "#f5f5f5",
gridRadiusKm: 40,
gridLineWidth: 1,
gridLineDiameterMeters: 7,
gridColor: "#9c9c9c",
gridOpacity: 12,
gridDotsEnabled: true,
gridDotsSize: 7,
gridDotsDiameterMeters: 80,
gridDotsColor: "#ffffff",
gridDotsDiameterMeters: 10,
gridDotsColor: "#9c9c9c",
gridDotsOpacity: 58,
gridCrossesEnabled: false,
gridCrossesLengthMeters: 200,
gridCrossesLengthMeters: 60,
gridCrossesWidthMeters: 10,
gridCrossesColor: "#35cfff",
gridCrossesOpacity: 50,
gridCrossesColor: "#9c9c9c",
gridCrossesOpacity: 46,
gridLodProfiles: structuredClone(DEFAULT_GRID_LOD_PROFILES) as GridLodProfile[],
};
function resolveGridLodProfiles(settings?: Partial<MapPageSettings>): GridLodProfile[] {
// A layout saved by the previous flat contract must not lose the values the
// operator already tuned. Promote its common visual fields and per-band
// height/step/mode values into five authoritative profiles on first read;
// the next ordinary page save persists the canonical array.
const legacySettings: MapPresentation = {
...initialMapSettings,
...settings,
gridLodProfiles: Array.isArray(settings?.gridLodProfiles) ? settings.gridLodProfiles : [],
cacheRefresh: false,
};
return Array.from({ length: 5 }, (_unused, index) => gridLodProfile(legacySettings, index) as GridLodProfile);
}
// A valid, deterministic scene view is available before Cesium emits its
// first move-end event. It makes the page contract immediately saveable;
// the renderer replaces it with the exact live camera as soon as it is ready.
@@ -389,6 +410,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
}>(function MapFixturePreview({ features = { inspector: true, toolbar: true, assistant: false }, expanded = false, initialLayout = null, applicationId, pageId }, ref) {
const workspaceRef = useRef<HTMLDivElement>(null);
const [selectedId, setSelectedId] = useState<string>();
const [selectedGridSector, setSelectedGridSector] = useState<GridSectorSelection | null>(null);
const [subjectCardOpen, setSubjectCardOpen] = useState(false);
const [subjectCardRect, setSubjectCardRect] = useState<WorkspaceWindowRect>(defaultSubjectCardRect);
const [subjectCardMaximized, setSubjectCardMaximized] = useState(false);
@@ -419,7 +441,13 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
// Layouts saved before the cache policy field existed retain the safe
// append-only default when they are opened again.
cacheNoOverwrite: initialLayout?.settings?.cacheNoOverwrite ?? true,
// Camera-relative layouts were decorative and had no stable sector
// identity. Opening one performs a deterministic migration to its stored
// Moscow origin; the current viewport is never promoted to definition.
gridCenterMode: "fixed",
gridLodProfiles: resolveGridLodProfiles(initialLayout?.settings),
}));
const [selectedGridLod, setSelectedGridLod] = useState("0");
const [mapHeight, setMapHeight] = useState(() => initialLayout?.mapHeight ?? (expanded ? 620 : 470));
const [mapCamera, setMapCamera] = useState<MapCameraView>(initialLayout?.camera ?? fallbackMapCamera);
const mapRendererRef = useRef<CesiumMapRendererHandle | null>(null);
@@ -646,6 +674,19 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
[cacheRefresh, mapSettings],
);
const updateMapSettings = (patch: Partial<MapPageSettings>) => setMapSettings((current) => ({ ...current, ...patch }));
const selectedGridLodIndex = Math.max(0, Math.min(4, Number.parseInt(selectedGridLod, 10) || 0));
const activeGridLod = mapSettings.gridLodProfiles[selectedGridLodIndex] ?? DEFAULT_GRID_LOD_PROFILES[selectedGridLodIndex];
const minimumGridLodHeight = selectedGridLodIndex === 0
? 0.1
: mapSettings.gridLodProfiles[selectedGridLodIndex - 1].maxHeightKm + 0.1;
const maximumGridLodHeight = selectedGridLodIndex === mapSettings.gridLodProfiles.length - 1
? 20_000
: Math.max(minimumGridLodHeight, mapSettings.gridLodProfiles[selectedGridLodIndex + 1].maxHeightKm - 0.1);
const updateGridLod = (patch: Partial<GridLodProfile>) => updateMapSettings({
gridLodProfiles: mapSettings.gridLodProfiles.map((profile, index) => (
index === selectedGridLodIndex ? { ...profile, ...patch } : profile
)),
});
const setCacheEnabled = (cacheEnabled: boolean) => {
updateMapSettings({ cacheEnabled });
setRendererRevision((value) => value + 1);
@@ -1267,57 +1308,47 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
description: "first adapter control",
group: "Слои",
content: <>
<small className="catalog-map-inspector__note">Пять LOD сохраняют пространственную сетку вблизи и переходят к гратикуле на дальних высотах. Новый слой подготавливается до удаления предыдущего.</small>
<small className="catalog-map-inspector__note">Фиксированная московская ENU-адресация задаёт неизменные сектора на LOD 13. LOD 45 используют глобальную WGS84-гратику́лу; камера выбирает только LOD и видимую область.</small>
<Checker checked={mapSettings.gridVisible} label="Сетка" onChange={(gridVisible) => updateMapSettings({ gridVisible })} />
<Checker checked={mapSettings.grid3dEnabled} label="3D-сетка" onChange={(grid3dEnabled) => updateMapSettings({ grid3dEnabled })} />
<Checker checked={mapSettings.gridGraticuleEnabled} label="Гратикула" onChange={(gridGraticuleEnabled) => updateMapSettings({ gridGraticuleEnabled })} />
<Checker checked={mapSettings.gridLodEnabled} label="LOD по высоте камеры" onChange={(gridLodEnabled) => updateMapSettings({ gridLodEnabled })} />
<Checker checked={mapSettings.gridRebuildOnMoveEnd} label="Перестраивать после движения" onChange={(gridRebuildOnMoveEnd) => updateMapSettings({ gridRebuildOnMoveEnd })} />
<InspectorSelectField
label="Центр сетки"
value={mapSettings.gridCenterMode}
options={[
{ value: "camera", label: "За камерой", description: "Центр следует за viewport по стабильным тайлам" },
{ value: "fixed", label: "Фиксированный", description: "Используются заданные координаты" },
]}
onChange={(gridCenterMode) => updateMapSettings({ gridCenterMode })}
/>
{mapSettings.gridCenterMode === "fixed" ? <>
<RangeControl label="Центр: широта" value={mapSettings.gridCenterLatitude} min={-89.999} max={89.999} step={0.0001} formatValue={(value) => value.toFixed(4)} onChange={(gridCenterLatitude) => updateMapSettings({ gridCenterLatitude })} />
<RangeControl label="Центр: долгота" value={mapSettings.gridCenterLongitude} min={-180} max={180} step={0.0001} formatValue={(value) => value.toFixed(4)} onChange={(gridCenterLongitude) => updateMapSettings({ gridCenterLongitude })} />
</> : null}
<RangeControl label="Размер тайла" value={mapSettings.gridTileSizeKm} min={1} max={500} step={1} formatValue={(value) => `${value} км`} onChange={(gridTileSizeKm) => updateMapSettings({ gridTileSizeKm })} />
<ControlRow label="Система координат"><strong>Fixed ENU · WGS84</strong></ControlRow>
<RangeControl label="Origin: широта" value={mapSettings.gridCenterLatitude} min={-89.9} max={89.9} step={0.000001} formatValue={(value) => value.toFixed(6)} onChange={(gridCenterLatitude) => updateMapSettings({ gridCenterLatitude })} />
<RangeControl label="Origin: долгота" value={mapSettings.gridCenterLongitude} min={-180} max={180} step={0.000001} formatValue={(value) => value.toFixed(6)} onChange={(gridCenterLongitude) => updateMapSettings({ gridCenterLongitude })} />
<RangeControl label="Автовыключение выше" value={mapSettings.gridAutoDisableHeightKm} min={0} max={50_000} step={100} formatValue={(value) => value === 0 ? "выкл" : `${value} км`} onChange={(gridAutoDisableHeightKm) => updateMapSettings({ gridAutoDisableHeightKm })} />
<RangeControl label="Высота над поверхностью" value={mapSettings.gridHeightMeters} min={0} max={1000} formatValue={(value) => `${value} м`} onChange={(gridHeightMeters) => updateMapSettings({ gridHeightMeters })} />
<RangeControl label="Макс. угол обзора 3D" value={mapSettings.gridMax3dViewAngleDegrees} min={0} max={89} step={1} formatValue={(value) => `${value}°`} onChange={(gridMax3dViewAngleDegrees) => updateMapSettings({ gridMax3dViewAngleDegrees })} />
<InspectorSelectField label="LOD 1: режим" value={mapSettings.gridLod1Mode} options={GRID_MODE_OPTIONS} onChange={(gridLod1Mode) => updateMapSettings({ gridLod1Mode })} />
<RangeControl label="LOD 1: до высоты" value={mapSettings.gridLod1MaxHeightKm} min={1} max={50} formatValue={(value) => `${value} км`} onChange={(gridLod1MaxHeightKm) => updateMapSettings({ gridLod1MaxHeightKm })} />
<RangeControl label="LOD 1: шаг" value={mapSettings.gridLod1StepKm} min={1} max={10} formatValue={(value) => `${value} км`} onChange={(gridLod1StepKm) => updateMapSettings({ gridLod1StepKm })} />
<InspectorSelectField label="LOD 2: режим" value={mapSettings.gridLod2Mode} options={GRID_MODE_OPTIONS} onChange={(gridLod2Mode) => updateMapSettings({ gridLod2Mode })} />
<RangeControl label="LOD 2: до высоты" value={mapSettings.gridLod2MaxHeightKm} min={10} max={200} formatValue={(value) => `${value} км`} onChange={(gridLod2MaxHeightKm) => updateMapSettings({ gridLod2MaxHeightKm })} />
<RangeControl label="LOD 2: шаг" value={mapSettings.gridLod2StepKm} min={1} max={25} formatValue={(value) => `${value} км`} onChange={(gridLod2StepKm) => updateMapSettings({ gridLod2StepKm })} />
<InspectorSelectField label="LOD 3: режим" value={mapSettings.gridLod3Mode} options={GRID_MODE_OPTIONS} onChange={(gridLod3Mode) => updateMapSettings({ gridLod3Mode })} />
<RangeControl label="LOD 3: до высоты" value={mapSettings.gridLod3MaxHeightKm} min={50} max={1_000} step={10} formatValue={(value) => `${value} км`} onChange={(gridLod3MaxHeightKm) => updateMapSettings({ gridLod3MaxHeightKm })} />
<RangeControl label="LOD 3: шаг" value={mapSettings.gridLod3StepKm} min={5} max={100} formatValue={(value) => `${value} км`} onChange={(gridLod3StepKm) => updateMapSettings({ gridLod3StepKm })} />
<InspectorSelectField label="LOD 4: режим" value={mapSettings.gridLod4Mode} options={GRID_MODE_OPTIONS} onChange={(gridLod4Mode) => updateMapSettings({ gridLod4Mode })} />
<RangeControl label="LOD 4: до высоты" value={mapSettings.gridLod4MaxHeightKm} min={100} max={5_000} step={50} formatValue={(value) => `${value} км`} onChange={(gridLod4MaxHeightKm) => updateMapSettings({ gridLod4MaxHeightKm })} />
<RangeControl label="LOD 4: шаг" value={mapSettings.gridLod4StepKm} min={10} max={500} step={5} formatValue={(value) => `${value} км`} onChange={(gridLod4StepKm) => updateMapSettings({ gridLod4StepKm })} />
<InspectorSelectField label="LOD 5: режим" value={mapSettings.gridLod5Mode} options={GRID_MODE_OPTIONS} onChange={(gridLod5Mode) => updateMapSettings({ gridLod5Mode })} />
<RangeControl label="LOD 5: шаг" value={mapSettings.gridLod5StepKm} min={50} max={2_000} step={25} formatValue={(value) => `${value} км`} onChange={(gridLod5StepKm) => updateMapSettings({ gridLod5StepKm })} />
<RangeControl label="Радиус видимости" value={mapSettings.gridRadiusKm} min={5} max={2_000} step={5} formatValue={(value) => `${value} км`} onChange={(gridRadiusKm) => updateMapSettings({ gridRadiusKm })} />
<ControlRow label="Цвет линий"><ColorField label="Цвет линий сетки" value={mapSettings.gridColor} onChange={(gridColor) => updateMapSettings({ gridColor })} /></ControlRow>
<RangeControl label="3D-линии: диаметр" value={mapSettings.gridLineDiameterMeters} min={1} max={100} step={1} formatValue={(value) => `${value} м`} onChange={(gridLineDiameterMeters) => updateMapSettings({ gridLineDiameterMeters })} />
<RangeControl label="Гратикула: толщина" value={mapSettings.gridLineWidth} min={1} max={8} formatValue={(value) => `${value} px`} onChange={(gridLineWidth) => updateMapSettings({ gridLineWidth })} />
<RangeControl label="Прозрачность сетки" value={mapSettings.gridOpacity} min={0} max={100} formatValue={(value) => `${value}%`} onChange={(gridOpacity) => updateMapSettings({ gridOpacity })} />
<Checker checked={mapSettings.gridDotsEnabled} label="Кружки" onChange={(gridDotsEnabled) => updateMapSettings({ gridDotsEnabled })} />
<RangeControl label="Кружки: диаметр" value={mapSettings.gridDotsDiameterMeters} min={2} max={1_000} step={2} formatValue={(value) => `${value} м`} onChange={(gridDotsDiameterMeters) => updateMapSettings({ gridDotsDiameterMeters })} />
<ControlRow label="Кружки: цвет"><ColorField label="Цвет кружков сетки" value={mapSettings.gridDotsColor} onChange={(gridDotsColor) => updateMapSettings({ gridDotsColor })} /></ControlRow>
<RangeControl label="Кружки: прозрачность" value={mapSettings.gridDotsOpacity} min={0} max={100} formatValue={(value) => `${value}%`} onChange={(gridDotsOpacity) => updateMapSettings({ gridDotsOpacity })} />
<Checker checked={mapSettings.gridCrossesEnabled} label="Кресты" onChange={(gridCrossesEnabled) => updateMapSettings({ gridCrossesEnabled })} />
<RangeControl label="Кресты: длина" value={mapSettings.gridCrossesLengthMeters} min={2} max={2_000} step={2} formatValue={(value) => `${value} м`} onChange={(gridCrossesLengthMeters) => updateMapSettings({ gridCrossesLengthMeters })} />
<RangeControl label="Кресты: ширина" value={mapSettings.gridCrossesWidthMeters} min={1} max={100} step={1} formatValue={(value) => `${value} м`} onChange={(gridCrossesWidthMeters) => updateMapSettings({ gridCrossesWidthMeters })} />
<ControlRow label="Кресты: цвет"><ColorField label="Цвет крестов сетки" value={mapSettings.gridCrossesColor} onChange={(gridCrossesColor) => updateMapSettings({ gridCrossesColor })} /></ControlRow>
<RangeControl label="Кресты: прозрачность" value={mapSettings.gridCrossesOpacity} min={0} max={100} formatValue={(value) => `${value}%`} onChange={(gridCrossesOpacity) => updateMapSettings({ gridCrossesOpacity })} />
<div className="catalog-map-grid-lod-tabs">
<SegmentedControl value={selectedGridLod} items={DEFAULT_GRID_LOD_PROFILES.map((_profile, index) => ({ value: String(index), label: `LOD ${index + 1}` }))} label="Уровень детализации сетки" onChange={setSelectedGridLod} />
</div>
<RangeControl label={selectedGridLodIndex === 4 ? "Порог профиля" : о высоты"} value={activeGridLod.maxHeightKm} min={minimumGridLodHeight} max={maximumGridLodHeight} step={0.1} formatValue={(value) => `${value} км`} onChange={(maxHeightKm) => updateGridLod({ maxHeightKm })} />
{selectedGridLodIndex === 4 ? <small className="catalog-map-inspector__note">Последний LOD остаётся активным выше своего порога до общего автовыключения.</small> : null}
<InspectorSelectField label="Режим" value={activeGridLod.mode} options={GRID_MODE_OPTIONS} onChange={(mode) => updateGridLod({ mode })} />
<RangeControl label="Высота WGS84" value={activeGridLod.heightMeters} min={0} max={5_000} step={10} formatValue={(value) => `${value} м`} onChange={(heightMeters) => updateGridLod({ heightMeters })} />
<RangeControl label="Конус видимости 3D" value={activeGridLod.max3dViewAngleDegrees} min={30} max={170} step={1} formatValue={(value) => `${value}°`} onChange={(max3dViewAngleDegrees) => updateGridLod({ max3dViewAngleDegrees })} />
<RangeControl label="Шаг ENU-секторов" value={activeGridLod.stepKm} min={0.1} max={5_000} step={0.1} formatValue={(value) => `${value} км`} onChange={(stepKm) => updateGridLod({ stepKm, radiusKm: Math.min(activeGridLod.radiusKm, stepKm * MAX_LOCAL_GRID_INDEX) })} />
<RangeControl label="Размер тайла ENU" value={activeGridLod.tileSizeKm} min={1} max={50} step={1} formatValue={(value) => `${value} км`} onChange={(tileSizeKm) => updateGridLod({ tileSizeKm })} />
<RangeControl label="Радиус ENU-поля" value={activeGridLod.radiusKm} min={1} max={Math.min(100_000, activeGridLod.stepKm * MAX_LOCAL_GRID_INDEX)} step={1} formatValue={(value) => `${value} км`} onChange={(radiusKm) => updateGridLod({ radiusKm })} />
<RangeControl label="Диаметр 3D-линий" value={activeGridLod.lineDiameterMeters} min={1} max={100} step={1} formatValue={(value) => `${value} м`} onChange={(lineDiameterMeters) => updateGridLod({ lineDiameterMeters })} />
<ControlRow label="Цвет 3D-линий"><ColorField label="Цвет линий ENU-сетки" value={activeGridLod.lineColor} onChange={(lineColor) => updateGridLod({ lineColor })} /></ControlRow>
<RangeControl label="Прозрачность 3D-линий" value={activeGridLod.lineOpacity} min={0} max={100} formatValue={(value) => `${value}%`} onChange={(lineOpacity) => updateGridLod({ lineOpacity })} />
<Checker checked={activeGridLod.dotsEnabled} label="Кружки" onChange={(dotsEnabled) => updateGridLod({ dotsEnabled })} />
<RangeControl label="Кружки: диаметр" value={activeGridLod.dotsDiameterMeters} min={1} max={1_000} step={1} formatValue={(value) => `${value} м`} onChange={(dotsDiameterMeters) => updateGridLod({ dotsDiameterMeters })} />
<ControlRow label="Кружки: цвет"><ColorField label="Цвет кружков сетки" value={activeGridLod.dotsColor} onChange={(dotsColor) => updateGridLod({ dotsColor })} /></ControlRow>
<RangeControl label="Кружки: прозрачность" value={activeGridLod.dotsOpacity} min={0} max={100} formatValue={(value) => `${value}%`} onChange={(dotsOpacity) => updateGridLod({ dotsOpacity })} />
<Checker checked={activeGridLod.crossesEnabled} label="Кресты" onChange={(crossesEnabled) => updateGridLod({ crossesEnabled })} />
<RangeControl label="Кресты: длина" value={activeGridLod.crossesLengthMeters} min={2} max={5_000} step={2} formatValue={(value) => `${value} м`} onChange={(crossesLengthMeters) => updateGridLod({ crossesLengthMeters })} />
<RangeControl label="Кресты: ширина" value={activeGridLod.crossesWidthMeters} min={1} max={500} step={1} formatValue={(value) => `${value} м`} onChange={(crossesWidthMeters) => updateGridLod({ crossesWidthMeters })} />
<ControlRow label="Кресты: цвет"><ColorField label="Цвет крестов сетки" value={activeGridLod.crossesColor} onChange={(crossesColor) => updateGridLod({ crossesColor })} /></ControlRow>
<RangeControl label="Кресты: прозрачность" value={activeGridLod.crossesOpacity} min={0} max={100} formatValue={(value) => `${value}%`} onChange={(crossesOpacity) => updateGridLod({ crossesOpacity })} />
<RangeControl label="Шаг гратикулы" value={activeGridLod.graticuleStepDegrees} min={0.1} max={10} step={0.05} formatValue={(value) => `${value}°`} onChange={(graticuleStepDegrees) => updateGridLod({ graticuleStepDegrees })} />
<RangeControl label="Толщина гратикулы" value={activeGridLod.graticuleLineWidthPx} min={1} max={3} step={1} formatValue={(value) => `${value} px`} onChange={(graticuleLineWidthPx) => updateGridLod({ graticuleLineWidthPx })} />
<ControlRow label="Цвет гратикулы"><ColorField label="Цвет WGS84-гратику́лы" value={activeGridLod.graticuleColor} onChange={(graticuleColor) => updateGridLod({ graticuleColor })} /></ControlRow>
<RangeControl label="Прозрачность гратикулы" value={activeGridLod.graticuleOpacity} min={0} max={100} formatValue={(value) => `${value}%`} onChange={(graticuleOpacity) => updateGridLod({ graticuleOpacity })} />
<ControlRow label="Выбранный сектор"><strong className="catalog-map-grid-sector-id">{selectedGridSector?.id ?? "Нажмите сектор на карте"}</strong></ControlRow>
{selectedGridSector ? <small className="catalog-map-inspector__note">LOD {selectedGridSector.lod} · {selectedGridSector.units === "meters-enu"
? `ENU ${selectedGridSector.bounds.west}${selectedGridSector.bounds.east} м E; ${selectedGridSector.bounds.south}${selectedGridSector.bounds.north} м N`
: `WGS84 ${selectedGridSector.bounds.west}${selectedGridSector.bounds.east}°; ${selectedGridSector.bounds.south}${selectedGridSector.bounds.north}°`}</small> : null}
</>,
},
{
@@ -1447,6 +1478,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
key={rendererRevision}
ref={mapRendererRef}
onSelect={handleSelect}
onGridSectorSelect={setSelectedGridSector}
onGatewayHealth={handleRendererGatewayHealth}
onProviderStatus={setProviderStatus}
onCameraChange={handleCameraChange}