Files
NODEDC_DESIGN_GUIDELINE/apps/catalog/src/mapCameraPresets.ts
T

124 lines
6.0 KiB
TypeScript

/**
* Geometric-error bands observed in the current Cesium OSM Buildings 96188
* hierarchy. They are ordered from the global root towards maximum detail.
* The provider may publish a newer hierarchy later, so these values are a
* measured runtime profile rather than a permanent Cesium API contract.
*/
export const OSM_BUILDINGS_GEOMETRIC_ERROR_LEVELS = [
77_067.33978,
38_533.66989,
19_266.834945,
9_633.417472,
4_816.708736,
2_408.354368,
1_204.177184,
602.088592,
301.044296,
150.522148,
75.261074,
37.630537,
18.815269,
9.407634,
4.703817,
2.351909,
] as const;
export const CAMERA_SURVEY_CROSS_TRACK_FRACTION = 0.69;
export type CameraSurveyPreset = {
id: string;
profile: number;
label: string;
heightAboveGroundMeters: number;
pitchMetersPerTurn: number;
speedMetersPerSecond: number;
targetRadiusMeters: number;
};
/**
* Ten operational scale profiles cover a regional survey from street to
* macro scale. They deliberately do not claim a one-to-one provider LOD:
* Cesium selects imagery, terrain and 3D Tiles through different SSE rules,
* viewport geometry, pixel ratio, availability and tile bounding volumes.
*
* Height and turn pitch double together. With a 60 degree horizontal field
* of view and a near-nadir camera, pitch = 0.8 * height keeps about 31%
* cross-track overlap. Runtime additionally clamps pitch to the live viewport,
* so a narrow inspector cannot open gaps between turns.
*
* Speed is a ceiling, not a promise: the renderer pauses whenever current
* Imagery, Terrain or OSM Buildings requests are unsettled. A 25 km target
* radius makes every profile a finite city-scale pass instead of silently
* continuing to the 250 km geodesic safety boundary.
*/
export const CAMERA_SURVEY_PRESETS = [
{ id: "osm-detail-1", profile: 1, label: "Профиль 1 · 80 м", heightAboveGroundMeters: 80, pitchMetersPerTurn: 64, speedMetersPerSecond: 256, targetRadiusMeters: 25_000 },
{ id: "osm-detail-2", profile: 2, label: "Профиль 2 · 160 м", heightAboveGroundMeters: 160, pitchMetersPerTurn: 128, speedMetersPerSecond: 512, targetRadiusMeters: 25_000 },
{ id: "osm-detail-3", profile: 3, label: "Профиль 3 · 320 м", heightAboveGroundMeters: 320, pitchMetersPerTurn: 256, speedMetersPerSecond: 1_024, targetRadiusMeters: 25_000 },
{ id: "osm-detail-4", profile: 4, label: "Профиль 4 · 640 м", heightAboveGroundMeters: 640, pitchMetersPerTurn: 512, speedMetersPerSecond: 2_048, targetRadiusMeters: 25_000 },
{ id: "osm-detail-5", profile: 5, label: "Профиль 5 · 1,28 км", heightAboveGroundMeters: 1_280, pitchMetersPerTurn: 1_024, speedMetersPerSecond: 4_096, targetRadiusMeters: 25_000 },
{ id: "osm-detail-6", profile: 6, label: "Профиль 6 · 2,56 км", heightAboveGroundMeters: 2_560, pitchMetersPerTurn: 2_048, speedMetersPerSecond: 5_000, targetRadiusMeters: 25_000 },
{ id: "osm-detail-7", profile: 7, label: "Профиль 7 · 5,12 км", heightAboveGroundMeters: 5_120, pitchMetersPerTurn: 4_096, speedMetersPerSecond: 5_000, targetRadiusMeters: 25_000 },
{ id: "osm-detail-8", profile: 8, label: "Профиль 8 · 10,24 км", heightAboveGroundMeters: 10_240, pitchMetersPerTurn: 8_192, speedMetersPerSecond: 5_000, targetRadiusMeters: 25_000 },
{ id: "osm-detail-9", profile: 9, label: "Профиль 9 · 20,48 км", heightAboveGroundMeters: 20_480, pitchMetersPerTurn: 16_384, speedMetersPerSecond: 5_000, targetRadiusMeters: 25_000 },
{ id: "osm-detail-10", profile: 10, label: "Профиль 10 · 40,96 км", heightAboveGroundMeters: 40_960, pitchMetersPerTurn: 32_768, speedMetersPerSecond: 5_000, targetRadiusMeters: 25_000 },
] as const satisfies readonly CameraSurveyPreset[];
export type CameraSurveyPresetId = (typeof CAMERA_SURVEY_PRESETS)[number]["id"];
export type CameraSurveySelection = CameraSurveyPresetId | "custom";
export const DEFAULT_CAMERA_SURVEY_PRESET = CAMERA_SURVEY_PRESETS[0];
export const OSM_BUILDINGS_OBSERVED_BAND_COUNT = OSM_BUILDINGS_GEOMETRIC_ERROR_LEVELS.length;
export function findCameraSurveyPreset(id: CameraSurveyPresetId) {
return CAMERA_SURVEY_PRESETS.find((preset) => preset.id === id);
}
export function cameraSurveyCrossTrackWidth(heightAboveGroundMeters: number, horizontalFieldOfViewDegrees = 60) {
return 2 * heightAboveGroundMeters * Math.tan(horizontalFieldOfViewDegrees * Math.PI / 360);
}
export function cameraSurveyTurnOverlap(preset: CameraSurveyPreset, horizontalFieldOfViewDegrees = 60) {
const width = cameraSurveyCrossTrackWidth(preset.heightAboveGroundMeters, horizontalFieldOfViewDegrees);
return 1 - preset.pitchMetersPerTurn / width;
}
export function cameraSurveyPitchForViewport(
heightAboveGroundMeters: number,
requestedPitchMeters: number,
horizontalFieldOfViewRadians: number,
) {
const crossTrackWidthMeters = 2 * heightAboveGroundMeters * Math.tan(horizontalFieldOfViewRadians / 2);
return Math.min(requestedPitchMeters, crossTrackWidthMeters * CAMERA_SURVEY_CROSS_TRACK_FRACTION);
}
export function cameraSurveySpiralDistance(targetRadiusMeters: number, pitchMetersPerTurn: number) {
const angle = (Math.PI * 2 * targetRadiusMeters) / pitchMetersPerTurn;
const radialGrowth = pitchMetersPerTurn / (Math.PI * 2);
return (radialGrowth / 2) * (
angle * Math.sqrt(1 + angle * angle)
+ Math.asinh(angle)
);
}
export function cameraSurveySampleDistances(
startDistanceMeters: number,
spacingMeters: number,
targetDistanceMeters: number,
maximumSamples: number,
) {
if (startDistanceMeters > targetDistanceMeters || maximumSamples <= 0) return [];
const distances: number[] = [];
for (let index = 0; index < maximumSamples; index += 1) {
const distanceMeters = Math.min(startDistanceMeters + index * spacingMeters, targetDistanceMeters);
distances.push(distanceMeters);
if (distanceMeters >= targetDistanceMeters) break;
}
return distances;
}
export function cameraSurveyTravelSeconds(preset: CameraSurveyPreset) {
return cameraSurveySpiralDistance(preset.targetRadiusMeters, preset.pitchMetersPerTurn)
/ preset.speedMetersPerSecond;
}