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
+191 -21
View File
@@ -520,21 +520,183 @@ const MAP_PAGE_SETTING_KEYS = new Set([
"buildingsOpacity", "buildingsDetail", "imageryBrightness", "imageryContrast",
"imagerySaturation", "gridVisible", "gridLodEnabled", "gridHeightMeters",
"grid3dEnabled", "gridGraticuleEnabled", "gridCenterMode", "gridCenterLatitude",
"gridCenterLongitude", "gridTileSizeKm", "gridAutoDisableHeightKm", "gridRebuildOnMoveEnd",
"gridCenterLongitude", "gridTileSizeKm", "gridAutoDisableHeightKm", "gridRebuildOnMoveEnd", "gridLegacyMode",
"gridMax3dViewAngleDegrees",
"gridLod1MaxHeightKm", "gridLod1StepKm", "gridLod2MaxHeightKm", "gridLod2StepKm",
"gridLod3MaxHeightKm", "gridLod3StepKm", "gridLod4MaxHeightKm", "gridLod4StepKm",
"gridLod5StepKm", "gridLod1Mode", "gridLod2Mode", "gridLod3Mode", "gridLod4Mode",
"gridLod5MaxHeightKm", "gridLod5StepKm", "gridLod1Mode", "gridLod2Mode", "gridLod3Mode", "gridLod4Mode",
"gridLod5Mode", "gridRadiusKm", "gridLineWidth", "gridLineDiameterMeters", "gridColor", "gridOpacity",
"gridDotsEnabled", "gridDotsSize", "gridDotsDiameterMeters", "gridDotsColor", "gridDotsOpacity",
"gridCrossesEnabled", "gridCrossesLengthMeters", "gridCrossesWidthMeters",
"gridCrossesColor", "gridCrossesOpacity",
"gridCrossesColor", "gridCrossesOpacity", "gridLodProfiles",
]);
const GRID_LOD_PROFILE_KEYS = new Set([
"maxHeightKm", "stepKm", "mode", "heightMeters", "max3dViewAngleDegrees", "tileSizeKm", "radiusKm",
"lineDiameterMeters", "lineColor", "lineOpacity", "dotsEnabled", "dotsDiameterMeters",
"dotsColor", "dotsOpacity", "crossesEnabled", "crossesLengthMeters", "crossesWidthMeters",
"crossesColor", "crossesOpacity", "graticuleStepDegrees", "graticuleLineWidthPx", "graticuleColor",
"graticuleOpacity",
]);
const GRID_LOD_PROFILE_INPUT_KEYS = new Set([...GRID_LOD_PROFILE_KEYS, "lineWidthPx"]);
const MAX_LOCAL_GRID_INDEX = 512;
const LEGACY_GRID_PROFILE_SETTING_KEYS = new Set([
"gridHeightMeters", "gridTileSizeKm", "gridMax3dViewAngleDegrees", "gridRadiusKm",
"gridLineWidth", "gridLineDiameterMeters", "gridColor", "gridOpacity",
"gridDotsEnabled", "gridDotsSize", "gridDotsDiameterMeters", "gridDotsColor", "gridDotsOpacity",
"gridCrossesEnabled", "gridCrossesLengthMeters", "gridCrossesWidthMeters", "gridCrossesColor", "gridCrossesOpacity",
...Array.from({ length: 5 }, (_unused, index) => index + 1).flatMap((number) => [
`gridLod${number}MaxHeightKm`, `gridLod${number}StepKm`, `gridLod${number}Mode`,
]),
]);
// Exact effective values of the tracked Engine MMAP/MOSCOWMAP Cesium grid.
// These are persisted by Foundry as presentation policy; the renderer remains
// a provider adapter and does not own or silently retune the sector definition.
const DEFAULT_GRID_LOD_PROFILES = Object.freeze([
Object.freeze({
maxHeightKm: 10, stepKm: 1, mode: "3d", heightMeters: 300, max3dViewAngleDegrees: 30,
tileSizeKm: 10, radiusKm: 50, lineDiameterMeters: 4, lineColor: "#f5f5f5", lineOpacity: 12,
dotsEnabled: true, dotsDiameterMeters: 7, dotsColor: "#ffffff", dotsOpacity: 58,
crossesEnabled: false, crossesLengthMeters: 60, crossesWidthMeters: 10, crossesColor: "#9c9c9c", crossesOpacity: 46,
graticuleStepDegrees: 0.25, graticuleLineWidthPx: 1, graticuleColor: "#f5f5f5", graticuleOpacity: 12,
}),
Object.freeze({
maxHeightKm: 50, stepKm: 5, mode: "3d", heightMeters: 500, max3dViewAngleDegrees: 30,
tileSizeKm: 10, radiusKm: 1_000, lineDiameterMeters: 10, lineColor: "#f8fbfc", lineOpacity: 12,
dotsEnabled: true, dotsDiameterMeters: 80, dotsColor: "#ffffff", dotsOpacity: 22,
crossesEnabled: false, crossesLengthMeters: 60, crossesWidthMeters: 10, crossesColor: "#9c9c9c", crossesOpacity: 46,
graticuleStepDegrees: 0.5, graticuleLineWidthPx: 1, graticuleColor: "#f8fbfc", graticuleOpacity: 12,
}),
Object.freeze({
maxHeightKm: 200, stepKm: 25, mode: "3d", heightMeters: 500, max3dViewAngleDegrees: 30,
tileSizeKm: 25, radiusKm: 1_000, lineDiameterMeters: 7, lineColor: "#fafafa", lineOpacity: 12,
dotsEnabled: true, dotsDiameterMeters: 10, dotsColor: "#ffffff", dotsOpacity: 58,
crossesEnabled: false, crossesLengthMeters: 60, crossesWidthMeters: 10, crossesColor: "#9c9c9c", crossesOpacity: 46,
graticuleStepDegrees: 1, graticuleLineWidthPx: 1, graticuleColor: "#fafafa", graticuleOpacity: 12,
}),
Object.freeze({
maxHeightKm: 800, stepKm: 50, mode: "graticule", heightMeters: 500, max3dViewAngleDegrees: 30,
tileSizeKm: 10, radiusKm: 1_000, lineDiameterMeters: 10, lineColor: "#fcfdfd", lineOpacity: 12,
dotsEnabled: true, dotsDiameterMeters: 10, dotsColor: "#fcfdfd", dotsOpacity: 58,
crossesEnabled: false, crossesLengthMeters: 60, crossesWidthMeters: 10, crossesColor: "#9c9c9c", crossesOpacity: 46,
graticuleStepDegrees: 2, graticuleLineWidthPx: 1, graticuleColor: "#fcfdfd", graticuleOpacity: 12,
}),
Object.freeze({
maxHeightKm: 3_000, stepKm: 50, mode: "graticule", heightMeters: 500, max3dViewAngleDegrees: 30,
tileSizeKm: 10, radiusKm: 100, lineDiameterMeters: 7, lineColor: "#9c9c9c", lineOpacity: 12,
dotsEnabled: true, dotsDiameterMeters: 10, dotsColor: "#9c9c9c", dotsOpacity: 58,
crossesEnabled: false, crossesLengthMeters: 60, crossesWidthMeters: 10, crossesColor: "#9c9c9c", crossesOpacity: 46,
graticuleStepDegrees: 2, graticuleLineWidthPx: 1, graticuleColor: "#9c9c9c", graticuleOpacity: 8,
}),
]);
function promoteLegacyGridLodProfiles(settings) {
return DEFAULT_GRID_LOD_PROFILES.map((fallback, index) => {
const number = index + 1;
return {
...fallback,
maxHeightKm: settings[`gridLod${number}MaxHeightKm`] ?? fallback.maxHeightKm,
stepKm: settings[`gridLod${number}StepKm`] ?? fallback.stepKm,
mode: settings[`gridLod${number}Mode`] ?? fallback.mode,
heightMeters: settings.gridHeightMeters ?? fallback.heightMeters,
max3dViewAngleDegrees: settings.gridMax3dViewAngleDegrees ?? fallback.max3dViewAngleDegrees,
tileSizeKm: settings.gridTileSizeKm ?? fallback.tileSizeKm,
radiusKm: settings.gridRadiusKm ?? fallback.radiusKm,
lineDiameterMeters: settings.gridLineDiameterMeters ?? fallback.lineDiameterMeters,
lineColor: settings.gridColor ?? fallback.lineColor,
lineOpacity: settings.gridOpacity ?? fallback.lineOpacity,
dotsEnabled: settings.gridDotsEnabled ?? fallback.dotsEnabled,
dotsDiameterMeters: settings.gridDotsDiameterMeters ?? fallback.dotsDiameterMeters,
dotsColor: settings.gridDotsColor ?? fallback.dotsColor,
dotsOpacity: settings.gridDotsOpacity ?? fallback.dotsOpacity,
crossesEnabled: settings.gridCrossesEnabled ?? fallback.crossesEnabled,
crossesLengthMeters: settings.gridCrossesLengthMeters ?? fallback.crossesLengthMeters,
crossesWidthMeters: settings.gridCrossesWidthMeters ?? fallback.crossesWidthMeters,
crossesColor: settings.gridCrossesColor ?? fallback.crossesColor,
crossesOpacity: settings.gridCrossesOpacity ?? fallback.crossesOpacity,
graticuleLineWidthPx: settings.gridLineWidth ?? fallback.graticuleLineWidthPx,
graticuleColor: settings.gridColor ?? fallback.graticuleColor,
graticuleOpacity: settings.gridOpacity ?? fallback.graticuleOpacity,
};
});
}
function validateGridLodProfiles(value) {
if (!Array.isArray(value) || value.length !== 5) throw applicationError("invalid_map_grid_lod_profiles");
let previousMaxHeightKm = Number.NEGATIVE_INFINITY;
return value.map((profile, index) => {
if (!isObject(profile) || Object.keys(profile).some((key) => !GRID_LOD_PROFILE_INPUT_KEYS.has(key))) {
throw applicationError(`invalid_map_grid_lod_profile_${index + 1}`);
}
const legacyProfile = Object.hasOwn(profile, "lineWidthPx");
const normalized = legacyProfile ? {
...profile,
graticuleStepDegrees: profile.graticuleStepDegrees ?? DEFAULT_GRID_LOD_PROFILES[index].graticuleStepDegrees,
graticuleLineWidthPx: profile.graticuleLineWidthPx ?? profile.lineWidthPx,
graticuleColor: profile.graticuleColor ?? profile.lineColor,
graticuleOpacity: profile.graticuleOpacity ?? profile.lineOpacity,
} : { ...profile };
delete normalized.lineWidthPx;
if (GRID_LOD_PROFILE_KEYS.size !== Object.keys(normalized).length
|| [...GRID_LOD_PROFILE_KEYS].some((key) => !Object.hasOwn(normalized, key))) {
throw applicationError(`invalid_map_grid_lod_profile_${index + 1}`);
}
const code = (key) => `invalid_map_grid_lod_profile_${index + 1}_${key}`;
const maxHeightKm = requireNumber(normalized.maxHeightKm, 0.1, 100_000, code("maxHeightKm"));
if (maxHeightKm <= previousMaxHeightKm) {
throw applicationError(`invalid_map_grid_lod_profile_${index + 1}_maxHeightKm_order`);
}
previousMaxHeightKm = maxHeightKm;
if (!['3d', 'graticule'].includes(normalized.mode)) throw applicationError(code("mode"));
const stepKm = requireNumber(normalized.stepKm, 0.1, 5_000, code("stepKm"));
const radiusKm = requireNumber(normalized.radiusKm, 1, 100_000, code("radiusKm"));
if (radiusKm / stepKm > MAX_LOCAL_GRID_INDEX) {
throw applicationError(`invalid_map_grid_lod_profile_${index + 1}_workload`);
}
return {
maxHeightKm,
stepKm,
mode: normalized.mode,
heightMeters: requireNumber(normalized.heightMeters, 0, 5_000, code("heightMeters")),
max3dViewAngleDegrees: requireNumber(normalized.max3dViewAngleDegrees, 30, 170, code("max3dViewAngleDegrees")),
tileSizeKm: requireNumber(normalized.tileSizeKm, 1, 50, code("tileSizeKm")),
radiusKm,
lineDiameterMeters: requireNumber(normalized.lineDiameterMeters, 1, 100, code("lineDiameterMeters")),
lineColor: requireHex(normalized.lineColor, code("lineColor")),
lineOpacity: requireNumber(normalized.lineOpacity, 0, 100, code("lineOpacity")),
dotsEnabled: requireBoolean(normalized.dotsEnabled, code("dotsEnabled")),
dotsDiameterMeters: requireNumber(normalized.dotsDiameterMeters, 1, 1_000, code("dotsDiameterMeters")),
dotsColor: requireHex(normalized.dotsColor, code("dotsColor")),
dotsOpacity: requireNumber(normalized.dotsOpacity, 0, 100, code("dotsOpacity")),
crossesEnabled: requireBoolean(normalized.crossesEnabled, code("crossesEnabled")),
crossesLengthMeters: requireNumber(normalized.crossesLengthMeters, 1, 5_000, code("crossesLengthMeters")),
crossesWidthMeters: requireNumber(normalized.crossesWidthMeters, 1, 500, code("crossesWidthMeters")),
crossesColor: requireHex(normalized.crossesColor, code("crossesColor")),
crossesOpacity: requireNumber(normalized.crossesOpacity, 0, 100, code("crossesOpacity")),
graticuleStepDegrees: requireNumber(normalized.graticuleStepDegrees, 0.1, 180, code("graticuleStepDegrees")),
graticuleLineWidthPx: requireNumber(normalized.graticuleLineWidthPx, 1, 3, code("graticuleLineWidthPx")),
graticuleColor: requireHex(normalized.graticuleColor, code("graticuleColor")),
graticuleOpacity: requireNumber(normalized.graticuleOpacity, 0, 100, code("graticuleOpacity")),
};
});
}
function validateMapPageSettingsPatch(value) {
if (!isObject(value) || Object.keys(value).length === 0) throw applicationError("invalid_map_page_settings_patch");
if (Object.keys(value).some((key) => !MAP_PAGE_SETTING_KEYS.has(key))) throw applicationError("invalid_map_page_settings_patch_field");
return value;
if (value.gridCenterMode !== undefined && !["camera", "fixed"].includes(value.gridCenterMode)) {
throw applicationError("invalid_map_page_setting_gridCenterMode");
}
if (value.gridLodProfiles === undefined && Object.keys(value).some((key) => LEGACY_GRID_PROFILE_SETTING_KEYS.has(key))) {
throw applicationError("map_grid_lod_profiles_required");
}
return {
...value,
...(value.gridCenterMode !== undefined ? { gridCenterMode: "fixed" } : {}),
...(value.gridLodProfiles !== undefined ? { gridLodProfiles: validateGridLodProfiles(value.gridLodProfiles) } : {}),
};
}
function validateMapInspectorOpenSections(value) {
@@ -565,7 +727,7 @@ function validateMapPageLayout(value) {
// Layouts written before this field existed are safely upgraded to the
// no-overwrite default during read/save; they are not rejected as damaged.
if (settings.cacheNoOverwrite !== undefined) requireBoolean(settings.cacheNoOverwrite, "invalid_map_page_setting_cacheNoOverwrite");
for (const key of ["grid3dEnabled", "gridGraticuleEnabled", "gridRebuildOnMoveEnd", "gridCrossesEnabled"]) {
for (const key of ["grid3dEnabled", "gridGraticuleEnabled", "gridRebuildOnMoveEnd", "gridLegacyMode", "gridCrossesEnabled"]) {
if (settings[key] !== undefined) requireBoolean(settings[key], `invalid_map_page_setting_${key}`);
}
requireString(settings.imagerySource, "invalid_map_page_imagery_source", 64);
@@ -575,9 +737,11 @@ function validateMapPageLayout(value) {
for (const key of ["monochromeColor", "globeColor", "backgroundColor", "buildingsColor", "gridColor", "gridDotsColor"]) {
requireHex(settings[key], `invalid_map_page_setting_${key}`);
}
for (const key of ["gridCenterLatitude", "gridCenterLongitude", "gridTileSizeKm", "gridAutoDisableHeightKm", "gridMax3dViewAngleDegrees", "gridLod3MaxHeightKm", "gridLod4MaxHeightKm", "gridLod4StepKm", "gridLod5StepKm", "gridLineDiameterMeters", "gridDotsDiameterMeters", "gridCrossesLengthMeters", "gridCrossesWidthMeters", "gridCrossesOpacity"]) {
for (const key of ["gridTileSizeKm", "gridAutoDisableHeightKm", "gridMax3dViewAngleDegrees", "gridLod3MaxHeightKm", "gridLod4MaxHeightKm", "gridLod4StepKm", "gridLod5MaxHeightKm", "gridLod5StepKm", "gridLineDiameterMeters", "gridDotsDiameterMeters", "gridCrossesLengthMeters", "gridCrossesWidthMeters", "gridCrossesOpacity"]) {
if (settings[key] !== undefined) requireNumber(settings[key], -100000, 100000, `invalid_map_page_setting_${key}`);
}
if (settings.gridCenterLatitude !== undefined) requireNumber(settings.gridCenterLatitude, -90, 90, "invalid_map_page_setting_gridCenterLatitude");
if (settings.gridCenterLongitude !== undefined) requireNumber(settings.gridCenterLongitude, -180, 180, "invalid_map_page_setting_gridCenterLongitude");
if (settings.gridCrossesColor !== undefined) requireHex(settings.gridCrossesColor, "invalid_map_page_setting_gridCrossesColor");
if (settings.gridCenterMode !== undefined && !["camera", "fixed"].includes(settings.gridCenterMode)) {
throw applicationError("invalid_map_page_setting_gridCenterMode");
@@ -587,6 +751,9 @@ function validateMapPageLayout(value) {
throw applicationError(`invalid_map_page_setting_${key}`);
}
}
const gridLodProfiles = validateGridLodProfiles(
settings.gridLodProfiles ?? promoteLegacyGridLodProfiles(settings),
);
const camera = value.camera;
for (const key of ["longitude", "latitude", "height", "heading", "pitch", "roll"]) {
requireNumber(camera[key], -1_000_000_000, 1_000_000_000, `invalid_map_page_camera_${key}`);
@@ -652,7 +819,7 @@ function validateMapPageLayout(value) {
return {
schemaVersion: 1,
pageId: "map",
settings: { ...settings, cacheNoOverwrite: settings.cacheNoOverwrite ?? true },
settings: { ...settings, cacheNoOverwrite: settings.cacheNoOverwrite ?? true, gridCenterMode: "fixed", gridLodProfiles },
mapHeight: requireInteger(value.mapHeight, 360, 5000, "invalid_map_page_height"),
camera: {
longitude: camera.longitude,
@@ -736,12 +903,13 @@ function defaultMapPageLayout() {
gridLodEnabled: true,
grid3dEnabled: true,
gridGraticuleEnabled: true,
gridCenterMode: "camera",
gridCenterMode: "fixed",
gridCenterLatitude: 55.7558,
gridCenterLongitude: 37.6173,
gridTileSizeKm: 10,
gridAutoDisableHeightKm: 10000,
gridRebuildOnMoveEnd: true,
gridLegacyMode: false,
gridMax3dViewAngleDegrees: 30,
gridHeightMeters: 500,
gridLod1MaxHeightKm: 10,
@@ -750,29 +918,31 @@ function defaultMapPageLayout() {
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: 3000,
gridLod5StepKm: 50,
gridLod5Mode: "graticule",
gridRadiusKm: 1000,
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),
},
mapHeight: 470,
camera: {