feat(map): add fixed ENU and WGS84 sector grid
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from "react";
|
||||
import {
|
||||
ArcType,
|
||||
Cartesian2,
|
||||
Cartesian3,
|
||||
BingMapsImageryProvider,
|
||||
@@ -35,15 +36,18 @@ import {
|
||||
Math as CesiumMath,
|
||||
PerInstanceColorAppearance,
|
||||
PointGraphics,
|
||||
PointPrimitiveCollection,
|
||||
PolygonGeometry,
|
||||
PolygonHierarchy,
|
||||
PolylineColorAppearance,
|
||||
PolylineGraphics,
|
||||
PolylineCollection,
|
||||
Resource,
|
||||
sampleTerrainMostDetailed,
|
||||
ScreenSpaceEventHandler,
|
||||
ScreenSpaceEventType,
|
||||
SunLight,
|
||||
Transforms,
|
||||
VerticalOrigin,
|
||||
Viewer,
|
||||
} from "cesium";
|
||||
@@ -65,8 +69,19 @@ import {
|
||||
gridShouldBeVisible,
|
||||
resolveGridMode,
|
||||
selectGridLod,
|
||||
snapGridCenter,
|
||||
} from "./mapGridPolicy.mjs";
|
||||
import {
|
||||
boundedAngularParts,
|
||||
fixedGridOrigin,
|
||||
graticuleGranularity,
|
||||
graticuleLinePlan,
|
||||
graticuleSectorAt,
|
||||
graticuleSectorBounds,
|
||||
localGridPlan,
|
||||
localSectorAt,
|
||||
localSectorBounds,
|
||||
splitLongitudeRange,
|
||||
} from "./mapSectorGrid.mjs";
|
||||
|
||||
const MAX_SPIRAL_SUBSTEPS_PER_FRAME = 300;
|
||||
const TERRAIN_SAMPLE_TIMEOUT_MS = 12_000;
|
||||
@@ -220,12 +235,13 @@ export type MapPresentation = {
|
||||
gridLodEnabled: boolean;
|
||||
grid3dEnabled: boolean;
|
||||
gridGraticuleEnabled: boolean;
|
||||
gridCenterMode: "camera" | "fixed";
|
||||
gridCenterMode: "fixed";
|
||||
gridCenterLatitude: number;
|
||||
gridCenterLongitude: number;
|
||||
gridTileSizeKm: number;
|
||||
gridAutoDisableHeightKm: number;
|
||||
gridRebuildOnMoveEnd: boolean;
|
||||
gridLegacyMode: boolean;
|
||||
gridMax3dViewAngleDegrees: number;
|
||||
gridHeightMeters: number;
|
||||
gridLod1MaxHeightKm: number;
|
||||
@@ -241,6 +257,7 @@ export type MapPresentation = {
|
||||
gridLod4StepKm: number;
|
||||
gridLod4Mode: "3d" | "graticule";
|
||||
gridLod5StepKm: number;
|
||||
gridLod5MaxHeightKm: number;
|
||||
gridLod5Mode: "3d" | "graticule";
|
||||
gridRadiusKm: number;
|
||||
gridLineWidth: number;
|
||||
@@ -257,9 +274,45 @@ export type MapPresentation = {
|
||||
gridCrossesWidthMeters: number;
|
||||
gridCrossesColor: string;
|
||||
gridCrossesOpacity: number;
|
||||
gridLodProfiles: GridLodProfile[];
|
||||
cacheRefresh: boolean;
|
||||
};
|
||||
|
||||
export type GridLodProfile = {
|
||||
maxHeightKm: number;
|
||||
stepKm: number;
|
||||
mode: "3d" | "graticule";
|
||||
heightMeters: number;
|
||||
max3dViewAngleDegrees: number;
|
||||
tileSizeKm: number;
|
||||
radiusKm: number;
|
||||
lineDiameterMeters: number;
|
||||
lineColor: string;
|
||||
lineOpacity: number;
|
||||
dotsEnabled: boolean;
|
||||
dotsDiameterMeters: number;
|
||||
dotsColor: string;
|
||||
dotsOpacity: number;
|
||||
crossesEnabled: boolean;
|
||||
crossesLengthMeters: number;
|
||||
crossesWidthMeters: number;
|
||||
crossesColor: string;
|
||||
crossesOpacity: number;
|
||||
graticuleStepDegrees: number;
|
||||
graticuleLineWidthPx: number;
|
||||
graticuleColor: string;
|
||||
graticuleOpacity: number;
|
||||
};
|
||||
|
||||
export type GridSectorSelection = {
|
||||
id: string;
|
||||
lod: number;
|
||||
mode: "3d" | "graticule";
|
||||
address: { eastIndex: number; northIndex: number } | { longitudeIndex: number; latitudeIndex: number };
|
||||
bounds: { west: number; east: number; south: number; north: number };
|
||||
units: "meters-enu" | "degrees-wgs84";
|
||||
};
|
||||
|
||||
export type MapCameraView = {
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
@@ -903,195 +956,414 @@ function syncRuntimeDataSources(
|
||||
viewer.scene.requestRender();
|
||||
}
|
||||
|
||||
type GridLayerBuild = {
|
||||
dataSource: CustomDataSource | null;
|
||||
key: string;
|
||||
lodIndex: number | null;
|
||||
type GridLodBand = GridLodProfile & { index: number; id: string };
|
||||
type LocalGridAddressing = {
|
||||
mode: "3d";
|
||||
lod: number;
|
||||
stepMeters: number;
|
||||
radiusMeters: number;
|
||||
originLatitude: number;
|
||||
originLongitude: number;
|
||||
inverseEnu: Matrix4;
|
||||
};
|
||||
type GraticuleGridAddressing = {
|
||||
mode: "graticule";
|
||||
lod: number;
|
||||
stepDegrees: number;
|
||||
};
|
||||
type GridAddressing = LocalGridAddressing | GraticuleGridAddressing;
|
||||
type GridResources = {
|
||||
dataSource: CustomDataSource;
|
||||
points: PointPrimitiveCollection | null;
|
||||
crosses: PolylineCollection | null;
|
||||
};
|
||||
type HiddenGridPlan = { mode: "hidden"; key: string; lodIndex: number | null };
|
||||
type LocalGridLayerPlan = {
|
||||
mode: "3d";
|
||||
key: string;
|
||||
lodIndex: number;
|
||||
lod: GridLodBand;
|
||||
origin: { latitude: number; longitude: number };
|
||||
enu: Matrix4;
|
||||
inverseEnu: Matrix4;
|
||||
grid: ReturnType<typeof localGridPlan>;
|
||||
metersPerPixel: number;
|
||||
cameraHeightMeters: number;
|
||||
cameraPosition: Cartesian3;
|
||||
cameraDirection: Cartesian3;
|
||||
};
|
||||
type GraticuleGridLayerPlan = {
|
||||
mode: "graticule";
|
||||
key: string;
|
||||
lodIndex: number;
|
||||
lod: GridLodBand;
|
||||
grid: ReturnType<typeof graticuleLinePlan>;
|
||||
};
|
||||
type GridLayerPlan = HiddenGridPlan | LocalGridLayerPlan | GraticuleGridLayerPlan;
|
||||
|
||||
function buildGridLayer(
|
||||
viewer: Viewer,
|
||||
presentation: MapPresentation,
|
||||
previousLodIndex: number | null,
|
||||
serial: number,
|
||||
): GridLayerBuild {
|
||||
const cameraHeightKm = Math.max(0, Number(viewer.camera.positionCartographic?.height || 0) / 1000);
|
||||
if (!gridShouldBeVisible(presentation, cameraHeightKm)) {
|
||||
return { dataSource: null, key: "hidden", lodIndex: null };
|
||||
function gridMetersPerPixel(viewer: Viewer) {
|
||||
const frustum = viewer.camera.frustum as unknown as { fovy?: number };
|
||||
return Math.max(0.01, (
|
||||
Math.max(1, Number(viewer.camera.positionCartographic?.height || 1))
|
||||
* 2 * Math.tan((Number(frustum.fovy) || Math.PI / 3) / 2)
|
||||
) / Math.max(1, viewer.scene.canvas.clientHeight));
|
||||
}
|
||||
|
||||
function graticuleViewport(viewer: Viewer, stepDegrees: number, cameraHeightKm: number) {
|
||||
const ellipsoid = viewer.scene.globe.ellipsoid;
|
||||
const margin = Math.max(stepDegrees * 2, Math.min(30, cameraHeightKm / 200));
|
||||
const alignLatitude = (value: number, direction: "down" | "up") => clamp(
|
||||
(direction === "down" ? Math.floor(value / stepDegrees) : Math.ceil(value / stepDegrees)) * stepDegrees,
|
||||
-89.9,
|
||||
89.9,
|
||||
);
|
||||
const alignIntervals = (west: number, east: number) => splitLongitudeRange(west - margin, east + margin).map((interval) => ({
|
||||
west: interval.west <= -180 ? -180 : Math.floor(interval.west / stepDegrees) * stepDegrees,
|
||||
east: interval.east >= 180 ? 180 : Math.ceil(interval.east / stepDegrees) * stepDegrees,
|
||||
}));
|
||||
const rectangle = viewer.camera.computeViewRectangle(ellipsoid);
|
||||
if (rectangle) {
|
||||
const south = CesiumMath.toDegrees(rectangle.south);
|
||||
const north = CesiumMath.toDegrees(rectangle.north);
|
||||
const west = CesiumMath.toDegrees(rectangle.west);
|
||||
const east = CesiumMath.toDegrees(rectangle.east);
|
||||
return {
|
||||
south: alignLatitude(south - margin, "down"),
|
||||
north: alignLatitude(north + margin, "up"),
|
||||
longitudeIntervals: alignIntervals(west, east),
|
||||
};
|
||||
}
|
||||
|
||||
const lod = selectGridLod(presentation, cameraHeightKm, previousLodIndex);
|
||||
const pitchDegrees = CesiumMath.toDegrees(viewer.camera.pitch);
|
||||
const viewAngleFromNadir = Math.abs(90 - Math.abs(pitchDegrees));
|
||||
const mode = resolveGridMode(presentation, lod.mode, viewAngleFromNadir);
|
||||
if (mode === "hidden") return { dataSource: null, key: "hidden", lodIndex: lod.index };
|
||||
|
||||
const safeStepKm = clamp(lod.stepKm, 0.25, 5_000);
|
||||
const safeRadiusKm = clamp(presentation.gridRadiusKm, safeStepKm, 2_000);
|
||||
const stepsPerSide = Math.min(40, Math.max(1, Math.floor(safeRadiusKm / safeStepKm)));
|
||||
const cameraPosition = viewer.camera.positionCartographic;
|
||||
const center = snapGridCenter({
|
||||
latitude: cameraPosition ? CesiumMath.toDegrees(cameraPosition.latitude) : 55.751244,
|
||||
longitude: cameraPosition ? CesiumMath.toDegrees(cameraPosition.longitude) : 37.618423,
|
||||
}, presentation);
|
||||
const latitude = center.latitude;
|
||||
const longitude = center.longitude;
|
||||
const key = JSON.stringify([
|
||||
lod.index,
|
||||
mode,
|
||||
latitude,
|
||||
longitude,
|
||||
safeStepKm,
|
||||
safeRadiusKm,
|
||||
presentation.gridHeightMeters,
|
||||
presentation.gridLineWidth,
|
||||
presentation.gridLineDiameterMeters,
|
||||
presentation.gridColor,
|
||||
presentation.gridOpacity,
|
||||
presentation.gridDotsEnabled,
|
||||
presentation.gridDotsDiameterMeters,
|
||||
presentation.gridDotsColor,
|
||||
presentation.gridDotsOpacity,
|
||||
presentation.gridCrossesEnabled,
|
||||
presentation.gridCrossesLengthMeters,
|
||||
presentation.gridCrossesWidthMeters,
|
||||
presentation.gridCrossesColor,
|
||||
presentation.gridCrossesOpacity,
|
||||
]);
|
||||
const dataSource = new CustomDataSource(`nodedc-map-grid:${serial}`);
|
||||
const entities = dataSource.entities;
|
||||
const metersPerLatitudeDegree = 110_574;
|
||||
const metersPerLongitudeDegree = Math.max(1, 111_320 * Math.cos(CesiumMath.toRadians(latitude)));
|
||||
const stepMeters = safeStepKm * 1000;
|
||||
const radiusMeters = stepsPerSide * stepMeters;
|
||||
const deltaLatitude = stepMeters / metersPerLatitudeDegree;
|
||||
const deltaLongitude = stepMeters / metersPerLongitudeDegree;
|
||||
const radiusLatitude = radiusMeters / metersPerLatitudeDegree;
|
||||
const radiusLongitude = radiusMeters / metersPerLongitudeDegree;
|
||||
const lineColor = Color.fromCssColorString(presentation.gridColor).withAlpha(clamp(presentation.gridOpacity / 100, 0, 1));
|
||||
const dotColor = Color.fromCssColorString(presentation.gridDotsColor).withAlpha(clamp(presentation.gridDotsOpacity / 100, 0, 1));
|
||||
const crossColor = Color.fromCssColorString(presentation.gridCrossesColor).withAlpha(clamp(presentation.gridCrossesOpacity / 100, 0, 1));
|
||||
const elevation = Math.max(0, presentation.gridHeightMeters);
|
||||
const projected = mode === "graticule";
|
||||
const geometryHeight = projected ? 0 : elevation;
|
||||
const addGridLine = (id: string, positions: Cartesian3[]) => {
|
||||
if (projected) {
|
||||
entities.add({
|
||||
id,
|
||||
polyline: {
|
||||
positions,
|
||||
width: clamp(presentation.gridLineWidth, 1, 8),
|
||||
material: lineColor,
|
||||
clampToGround: true,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
entities.add({
|
||||
id,
|
||||
corridor: {
|
||||
positions,
|
||||
width: clamp(presentation.gridLineDiameterMeters, 1, 500),
|
||||
material: lineColor,
|
||||
height: geometryHeight,
|
||||
heightReference: HeightReference.NONE,
|
||||
},
|
||||
});
|
||||
const camera = viewer.camera.positionCartographic;
|
||||
const latitude = camera ? CesiumMath.toDegrees(camera.latitude) : 0;
|
||||
const longitude = camera ? CesiumMath.toDegrees(camera.longitude) : 0;
|
||||
const horizon = CesiumMath.toDegrees(Math.acos(clamp(
|
||||
ellipsoid.maximumRadius / (ellipsoid.maximumRadius + Math.max(0, cameraHeightKm * 1_000)),
|
||||
-1,
|
||||
1,
|
||||
)));
|
||||
const range = Math.min(180, Math.max(10, horizon * 2));
|
||||
return {
|
||||
south: alignLatitude(latitude - range - margin, "down"),
|
||||
north: alignLatitude(latitude + range + margin, "up"),
|
||||
longitudeIntervals: alignIntervals(longitude - range - margin, longitude + range + margin),
|
||||
};
|
||||
}
|
||||
|
||||
for (let index = -stepsPerSide; index <= stepsPerSide; index += 1) {
|
||||
const nextLatitude = latitude + index * deltaLatitude;
|
||||
const nextLongitude = longitude + index * deltaLongitude;
|
||||
addGridLine(`${serial}:latitude:${index}`, [
|
||||
Cartesian3.fromDegrees(longitude - radiusLongitude, nextLatitude, geometryHeight),
|
||||
Cartesian3.fromDegrees(longitude + radiusLongitude, nextLatitude, geometryHeight),
|
||||
]);
|
||||
addGridLine(`${serial}:longitude:${index}`, [
|
||||
Cartesian3.fromDegrees(nextLongitude, latitude - radiusLatitude, geometryHeight),
|
||||
Cartesian3.fromDegrees(nextLongitude, latitude + radiusLatitude, geometryHeight),
|
||||
function planGridLayer(viewer: Viewer, presentation: MapPresentation, previousLodIndex: number | null): GridLayerPlan {
|
||||
const cameraHeightKm = Math.max(0, Number(viewer.camera.positionCartographic?.height || 0) / 1_000);
|
||||
if (!gridShouldBeVisible(presentation, cameraHeightKm)) return { mode: "hidden", key: "hidden", lodIndex: null };
|
||||
const lod = selectGridLod(presentation, cameraHeightKm, previousLodIndex) as GridLodBand;
|
||||
const mode = resolveGridMode(presentation, lod.mode);
|
||||
if (mode === "hidden") return { mode: "hidden", key: `hidden:${lod.index}`, lodIndex: lod.index };
|
||||
|
||||
if (mode === "3d") {
|
||||
const origin = fixedGridOrigin(presentation);
|
||||
const anchor = Cartesian3.fromDegrees(origin.longitude, origin.latitude, 0);
|
||||
const enu = Transforms.eastNorthUpToFixedFrame(anchor);
|
||||
const inverseEnu = Matrix4.inverseTransformation(enu, new Matrix4());
|
||||
const stepMeters = clamp(lod.stepKm * 1_000, 100, 5_000_000);
|
||||
const radiusMeters = clamp(lod.radiusKm * 1_000, stepMeters, 100_000_000);
|
||||
const metersPerPixel = gridMetersPerPixel(viewer);
|
||||
const cameraPosition = Cartesian3.clone(viewer.camera.positionWC, new Cartesian3());
|
||||
const cameraDirection = Cartesian3.normalize(viewer.camera.directionWC, new Cartesian3());
|
||||
const cameraLocal = Matrix4.multiplyByPoint(inverseEnu, cameraPosition, new Cartesian3());
|
||||
const cameraPositionBucketMeters = Math.max(stepMeters, lod.tileSizeKm * 500);
|
||||
const cameraPositionKey = [cameraLocal.x, cameraLocal.y]
|
||||
.map((value) => Math.round(value / cameraPositionBucketMeters));
|
||||
const directionKey = [cameraDirection.x, cameraDirection.y, cameraDirection.z]
|
||||
.map((value) => Math.round(value * 50) / 50);
|
||||
const cameraHeightMeters = cameraHeightKm * 1_000;
|
||||
const grid = localGridPlan({ stepMeters, radiusMeters, maximumMarkers: 5_000 });
|
||||
const key = JSON.stringify([
|
||||
"local-enu", lod.index, origin.latitude, origin.longitude, stepMeters, radiusMeters,
|
||||
lod.heightMeters, lod.max3dViewAngleDegrees, lod.tileSizeKm,
|
||||
lod.lineDiameterMeters, lod.lineColor, lod.lineOpacity,
|
||||
lod.dotsEnabled, lod.dotsDiameterMeters, lod.dotsColor, lod.dotsOpacity,
|
||||
lod.crossesEnabled, lod.crossesLengthMeters, lod.crossesWidthMeters, lod.crossesColor, lod.crossesOpacity,
|
||||
grid.markerStride, cameraPositionKey, directionKey, Math.round(cameraHeightKm * 10) / 10,
|
||||
Math.round(metersPerPixel * 10) / 10,
|
||||
]);
|
||||
return {
|
||||
mode, key, lodIndex: lod.index, lod, origin, enu, inverseEnu, grid,
|
||||
metersPerPixel, cameraHeightMeters, cameraPosition, cameraDirection,
|
||||
};
|
||||
}
|
||||
|
||||
const dotStride = Math.max(1, Math.ceil((stepsPerSide * 2 + 1) / 25));
|
||||
for (let row = -stepsPerSide; row <= stepsPerSide; row += dotStride) {
|
||||
for (let column = -stepsPerSide; column <= stepsPerSide; column += dotStride) {
|
||||
const dotLongitude = longitude + column * deltaLongitude;
|
||||
const dotLatitude = latitude + row * deltaLatitude;
|
||||
if (presentation.gridDotsEnabled) {
|
||||
const diameterMeters = clamp(presentation.gridDotsDiameterMeters, 2, 2_000);
|
||||
entities.add({
|
||||
id: `${serial}:circle:${row}:${column}`,
|
||||
position: Cartesian3.fromDegrees(dotLongitude, dotLatitude, geometryHeight),
|
||||
ellipse: {
|
||||
semiMajorAxis: diameterMeters / 2,
|
||||
semiMinorAxis: diameterMeters / 2,
|
||||
material: dotColor,
|
||||
height: geometryHeight,
|
||||
heightReference: projected ? HeightReference.CLAMP_TO_GROUND : HeightReference.NONE,
|
||||
const viewport = graticuleViewport(viewer, lod.graticuleStepDegrees, cameraHeightKm);
|
||||
const grid = graticuleLinePlan({ ...viewport, stepDegrees: lod.graticuleStepDegrees });
|
||||
const key = JSON.stringify([
|
||||
"wgs84-graticule", lod.index, lod.heightMeters, lod.graticuleStepDegrees,
|
||||
lod.graticuleLineWidthPx, lod.graticuleColor, lod.graticuleOpacity,
|
||||
grid.south, grid.north, grid.longitudeIntervals,
|
||||
]);
|
||||
return { mode, key, lodIndex: lod.index, lod, grid };
|
||||
}
|
||||
|
||||
function materializeLocalGrid(plan: LocalGridLayerPlan, serial: number): GridResources {
|
||||
const dataSource = new CustomDataSource(`nodedc-map-grid-local:${serial}`);
|
||||
const points = plan.lod.dotsEnabled ? new PointPrimitiveCollection() : null;
|
||||
const crosses = plan.lod.crossesEnabled ? new PolylineCollection() : null;
|
||||
const lineColor = Color.fromCssColorString(plan.lod.lineColor).withAlpha(clamp(plan.lod.lineOpacity / 100, 0, 1));
|
||||
const dotColor = Color.fromCssColorString(plan.lod.dotsColor).withAlpha(clamp(plan.lod.dotsOpacity / 100, 0, 1));
|
||||
const crossColor = Color.fromCssColorString(plan.lod.crossesColor).withAlpha(clamp(plan.lod.crossesOpacity / 100, 0, 1));
|
||||
const lineWidthPixels = clamp(plan.lod.lineDiameterMeters / plan.metersPerPixel, 1, 8);
|
||||
const dotPixelSize = clamp(plan.lod.dotsDiameterMeters / plan.metersPerPixel, 1, 128);
|
||||
const crossWidthPixels = clamp(plan.lod.crossesWidthMeters / plan.metersPerPixel, 1, 8);
|
||||
const definition = {
|
||||
lod: plan.lod.index + 1,
|
||||
originLatitude: plan.origin.latitude,
|
||||
originLongitude: plan.origin.longitude,
|
||||
stepMeters: plan.grid.stepMeters,
|
||||
};
|
||||
const toWorld = (eastMeters: number, northMeters: number) => {
|
||||
const tangentPoint = Matrix4.multiplyByPoint(plan.enu, new Cartesian3(eastMeters, northMeters, 0), new Cartesian3());
|
||||
const cartographic = Cartographic.fromCartesian(tangentPoint);
|
||||
// Metric addressing stays in the immutable ENU frame. Reprojection only
|
||||
// bends the visual shell to WGS84 at an absolute height; terrain and 3D
|
||||
// Tiles never move the sector boundaries.
|
||||
return Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, Math.max(0, plan.lod.heightMeters));
|
||||
};
|
||||
const useViewCone = Number.isFinite(plan.lod.max3dViewAngleDegrees) && plan.lod.max3dViewAngleDegrees < 170;
|
||||
const viewConeMinimumDot = Math.cos(CesiumMath.toRadians(plan.lod.max3dViewAngleDegrees));
|
||||
const viewConeMinimumDistance = Math.max(
|
||||
plan.grid.stepMeters * 2,
|
||||
Math.min(plan.grid.radiusMeters, plan.cameraHeightMeters * 6),
|
||||
);
|
||||
const cameraLocal = Matrix4.multiplyByPoint(plan.inverseEnu, plan.cameraPosition, new Cartesian3());
|
||||
const cameraDirectionLocal = Matrix4.multiplyByPointAsVector(plan.inverseEnu, plan.cameraDirection, new Cartesian3());
|
||||
Cartesian3.normalize(cameraDirectionLocal, cameraDirectionLocal);
|
||||
const originLatitudeRadians = CesiumMath.toRadians(plan.origin.latitude);
|
||||
const wgs84SemiMajor = 6_378_137;
|
||||
const wgs84EccentricitySquared = 0.00669437999014;
|
||||
const latitudeFactor = 1 - wgs84EccentricitySquared * Math.sin(originLatitudeRadians) ** 2;
|
||||
const eastCurvatureRadius = wgs84SemiMajor / Math.sqrt(latitudeFactor);
|
||||
const northCurvatureRadius = wgs84SemiMajor * (1 - wgs84EccentricitySquared) / latitudeFactor ** 1.5;
|
||||
const approximateShellPoint = (eastMeters: number, northMeters: number) => {
|
||||
const horizontalSquared = eastMeters ** 2 + northMeters ** 2;
|
||||
if (horizontalSquared <= 1e-9) return new Cartesian3(0, 0, plan.lod.heightMeters);
|
||||
const curvatureRadius = horizontalSquared / (
|
||||
eastMeters ** 2 / eastCurvatureRadius + northMeters ** 2 / northCurvatureRadius
|
||||
);
|
||||
// Invert the donor's tangent-point -> geodetic-direction reprojection.
|
||||
// This keeps cone culling close to the WGS84 shell without paying for two
|
||||
// Cartographic conversions on every coarse candidate.
|
||||
const scale = curvatureRadius / Math.sqrt(curvatureRadius ** 2 + horizontalSquared);
|
||||
return new Cartesian3(
|
||||
eastMeters * scale,
|
||||
northMeters * scale,
|
||||
curvatureRadius * scale - curvatureRadius + plan.lod.heightMeters,
|
||||
);
|
||||
};
|
||||
const inViewCone = (eastMeters: number, northMeters: number) => {
|
||||
if (!useViewCone) return true;
|
||||
const shellPoint = approximateShellPoint(eastMeters, northMeters);
|
||||
const direction = Cartesian3.subtract(shellPoint, cameraLocal, new Cartesian3());
|
||||
const distance = Cartesian3.magnitude(direction);
|
||||
if (!Number.isFinite(distance) || distance <= viewConeMinimumDistance) return true;
|
||||
Cartesian3.normalize(direction, direction);
|
||||
const approximateDot = Cartesian3.dot(direction, cameraDirectionLocal);
|
||||
if (Math.abs(approximateDot - viewConeMinimumDot) > 0.15) return approximateDot >= viewConeMinimumDot;
|
||||
// ENU-plane culling is cheap, but the rendered lattice is bent back to
|
||||
// the WGS84 shell. Resolve candidates near the cone boundary in world
|
||||
// space so curvature cannot hide the sector beneath an oblique camera.
|
||||
const worldDirection = Cartesian3.subtract(toWorld(eastMeters, northMeters), plan.cameraPosition, new Cartesian3());
|
||||
Cartesian3.normalize(worldDirection, worldDirection);
|
||||
return Cartesian3.dot(worldDirection, plan.cameraDirection) >= viewConeMinimumDot;
|
||||
};
|
||||
const segmentStepMeters = Math.max(
|
||||
plan.grid.stepMeters,
|
||||
plan.lod.tileSizeKm * 1_000,
|
||||
plan.grid.radiusMeters / 80,
|
||||
);
|
||||
const addSegmentedLine = (axis: "east" | "north", line: (typeof plan.grid.lines)[number]) => {
|
||||
let positions: Cartesian3[] = [];
|
||||
let segmentStart = 0;
|
||||
const flush = () => {
|
||||
if (positions.length >= 2) {
|
||||
const address = axis === "east"
|
||||
? localSectorAt({ eastMeters: line.offsetMeters, northMeters: 0 }, definition)
|
||||
: localSectorAt({ eastMeters: 0, northMeters: line.offsetMeters }, definition);
|
||||
dataSource.entities.add({
|
||||
id: `${address.id}/line-${axis}/part-${segmentStart}`,
|
||||
polyline: {
|
||||
positions,
|
||||
width: lineWidthPixels,
|
||||
material: lineColor,
|
||||
clampToGround: false,
|
||||
arcType: ArcType.NONE,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (presentation.gridCrossesEnabled) {
|
||||
const halfLengthMeters = clamp(presentation.gridCrossesLengthMeters, 2, 5_000) / 2;
|
||||
const halfLatitude = halfLengthMeters / metersPerLatitudeDegree;
|
||||
const halfLongitude = halfLengthMeters / metersPerLongitudeDegree;
|
||||
const corridor = (positions: Cartesian3[]) => ({
|
||||
positions,
|
||||
width: clamp(presentation.gridCrossesWidthMeters, 1, 500),
|
||||
material: crossColor,
|
||||
height: geometryHeight,
|
||||
heightReference: projected ? HeightReference.CLAMP_TO_GROUND : HeightReference.NONE,
|
||||
positions = [];
|
||||
};
|
||||
let part = 0;
|
||||
for (let variable = -line.extentMeters; variable < line.extentMeters; variable += segmentStepMeters) {
|
||||
const next = Math.min(variable + segmentStepMeters, line.extentMeters);
|
||||
const middle = (variable + next) / 2;
|
||||
const middleEast = axis === "east" ? line.offsetMeters : middle;
|
||||
const middleNorth = axis === "east" ? middle : line.offsetMeters;
|
||||
if (!inViewCone(middleEast, middleNorth)) {
|
||||
flush();
|
||||
part += 1;
|
||||
segmentStart = part;
|
||||
continue;
|
||||
}
|
||||
const start = axis === "east" ? toWorld(line.offsetMeters, variable) : toWorld(variable, line.offsetMeters);
|
||||
const end = axis === "east" ? toWorld(line.offsetMeters, next) : toWorld(next, line.offsetMeters);
|
||||
if (positions.length === 0) positions.push(start);
|
||||
positions.push(end);
|
||||
part += 1;
|
||||
}
|
||||
flush();
|
||||
};
|
||||
for (const line of plan.grid.lines) addSegmentedLine("east", line);
|
||||
for (const line of plan.grid.lines) addSegmentedLine("north", line);
|
||||
|
||||
const radiusSquared = plan.grid.radiusMeters ** 2;
|
||||
const crossHalf = plan.lod.crossesLengthMeters / 2;
|
||||
for (let eastIndex = -plan.grid.maximumIndex; eastIndex <= plan.grid.maximumIndex; eastIndex += plan.grid.markerStride) {
|
||||
const eastMeters = eastIndex * plan.grid.stepMeters;
|
||||
for (let northIndex = -plan.grid.maximumIndex; northIndex <= plan.grid.maximumIndex; northIndex += plan.grid.markerStride) {
|
||||
const northMeters = northIndex * plan.grid.stepMeters;
|
||||
if (eastMeters ** 2 + northMeters ** 2 > radiusSquared) continue;
|
||||
if (!inViewCone(eastMeters, northMeters)) continue;
|
||||
const position = toWorld(eastMeters, northMeters);
|
||||
const address = localSectorAt({ eastMeters, northMeters }, definition);
|
||||
if (points) points.add({
|
||||
id: { kind: "nodedc-grid-intersection", sectorId: address.id },
|
||||
position,
|
||||
color: dotColor,
|
||||
pixelSize: Math.max(1, Math.round(dotPixelSize)),
|
||||
});
|
||||
if (crosses && crossHalf > 0) {
|
||||
const eastWest = crosses.add({
|
||||
id: { kind: "nodedc-grid-cross", sectorId: address.id },
|
||||
positions: [toWorld(eastMeters - crossHalf, northMeters), toWorld(eastMeters + crossHalf, northMeters)],
|
||||
width: Math.max(1, Math.round(crossWidthPixels)),
|
||||
});
|
||||
entities.add({
|
||||
id: `${serial}:cross-ns:${row}:${column}`,
|
||||
corridor: corridor([
|
||||
Cartesian3.fromDegrees(dotLongitude, dotLatitude - halfLatitude, geometryHeight),
|
||||
Cartesian3.fromDegrees(dotLongitude, dotLatitude + halfLatitude, geometryHeight),
|
||||
]),
|
||||
});
|
||||
entities.add({
|
||||
id: `${serial}:cross-ew:${row}:${column}`,
|
||||
corridor: corridor([
|
||||
Cartesian3.fromDegrees(dotLongitude - halfLongitude, dotLatitude, geometryHeight),
|
||||
Cartesian3.fromDegrees(dotLongitude + halfLongitude, dotLatitude, geometryHeight),
|
||||
]),
|
||||
const northSouth = crosses.add({
|
||||
id: { kind: "nodedc-grid-cross", sectorId: address.id },
|
||||
positions: [toWorld(eastMeters, northMeters - crossHalf), toWorld(eastMeters, northMeters + crossHalf)],
|
||||
width: Math.max(1, Math.round(crossWidthPixels)),
|
||||
});
|
||||
if (eastWest.material?.uniforms) eastWest.material.uniforms.color = crossColor;
|
||||
if (northSouth.material?.uniforms) northSouth.material.uniforms.color = crossColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { dataSource, key, lodIndex: lod.index };
|
||||
return { dataSource, points, crosses };
|
||||
}
|
||||
|
||||
function materializeGraticule(plan: GraticuleGridLayerPlan, serial: number): GridResources {
|
||||
const dataSource = new CustomDataSource(`nodedc-map-grid-graticule:${serial}`);
|
||||
const color = Color.fromCssColorString(plan.lod.graticuleColor).withAlpha(clamp(plan.lod.graticuleOpacity / 100, 0, 1));
|
||||
const clampToGround = plan.lod.heightMeters <= 0;
|
||||
const height = clampToGround ? 0 : plan.lod.heightMeters;
|
||||
const sampleStep = Math.min(5, Math.max(0.5, plan.grid.stepDegrees));
|
||||
const line = (id: string, positions: Cartesian3[]) => positions.length >= 2 && dataSource.entities.add({
|
||||
id,
|
||||
polyline: {
|
||||
positions,
|
||||
width: plan.lod.graticuleLineWidthPx,
|
||||
material: color,
|
||||
clampToGround,
|
||||
arcType: ArcType.RHUMB,
|
||||
granularity: graticuleGranularity(sampleStep, clampToGround),
|
||||
},
|
||||
});
|
||||
const positionsForParts = (
|
||||
parts: Array<{ start: number; end: number }>,
|
||||
positionAt: (angle: number) => Cartesian3,
|
||||
) => parts.length === 0
|
||||
? []
|
||||
: [positionAt(parts[0].start), ...parts.map(({ end }) => positionAt(end))];
|
||||
for (const { longitude } of plan.grid.meridians) {
|
||||
const parts = boundedAngularParts(plan.grid.south, plan.grid.north);
|
||||
line(
|
||||
`grid/wgs84/l${plan.lod.index + 1}/meridian/${longitude}`,
|
||||
positionsForParts(parts, (latitude) => Cartesian3.fromDegrees(longitude, latitude, height)),
|
||||
);
|
||||
}
|
||||
for (const latitude of plan.grid.parallels) {
|
||||
plan.grid.longitudeIntervals.forEach((interval, index) => {
|
||||
// RHUMB interpolation is correct for parallels, but Cesium rejects an
|
||||
// exactly antipodal equatorial pair. Intermediate vertices keep every
|
||||
// segment below 90° without multiplying the number of Cesium entities.
|
||||
const parts = boundedAngularParts(interval.west, interval.east);
|
||||
line(
|
||||
`grid/wgs84/l${plan.lod.index + 1}/parallel/${latitude}/interval-${index}`,
|
||||
positionsForParts(parts, (longitude) => Cartesian3.fromDegrees(longitude, latitude, height)),
|
||||
);
|
||||
});
|
||||
}
|
||||
return { dataSource, points: null, crosses: null };
|
||||
}
|
||||
|
||||
function materializeGridLayer(plan: Exclude<GridLayerPlan, HiddenGridPlan>, serial: number) {
|
||||
return plan.mode === "3d" ? materializeLocalGrid(plan, serial) : materializeGraticule(plan, serial);
|
||||
}
|
||||
|
||||
class GridLayerController {
|
||||
private current: CustomDataSource | null = null;
|
||||
private pending: CustomDataSource | null = null;
|
||||
private current: { resources: GridResources; addressing: GridAddressing } | null = null;
|
||||
private pending: { resources: GridResources; addressing: GridAddressing } | null = null;
|
||||
private key: string | null = null;
|
||||
private lodIndex: number | null = null;
|
||||
private epoch = 0;
|
||||
private serial = 0;
|
||||
private removeReadyListener: (() => void) | null = null;
|
||||
private fallbackTimer: number | null = null;
|
||||
private retiredResources = new WeakSet<GridResources>();
|
||||
|
||||
constructor(private readonly viewer: Viewer) {}
|
||||
constructor(
|
||||
private readonly viewer: Viewer,
|
||||
private readonly onAddressingChange: () => void,
|
||||
) {}
|
||||
|
||||
private addressingKey(addressing: GridAddressing | undefined) {
|
||||
if (!addressing) return "hidden";
|
||||
return addressing.mode === "3d"
|
||||
? JSON.stringify([addressing.mode, addressing.lod, addressing.originLatitude, addressing.originLongitude, addressing.stepMeters])
|
||||
: JSON.stringify([addressing.mode, addressing.lod, addressing.stepDegrees]);
|
||||
}
|
||||
|
||||
rebuild(presentation: MapPresentation) {
|
||||
const build = buildGridLayer(this.viewer, presentation, this.lodIndex, ++this.serial);
|
||||
if (build.key === this.key) return;
|
||||
this.key = build.key;
|
||||
this.lodIndex = build.lodIndex;
|
||||
// Planning is intentionally cheap. Geometry is materialized only after
|
||||
// key comparison; repeated camera/settings notifications cannot create
|
||||
// and discard thousands of Cesium objects for an unchanged layer.
|
||||
const plan = planGridLayer(this.viewer, presentation, this.lodIndex);
|
||||
if (plan.key === this.key) return;
|
||||
this.key = plan.key;
|
||||
this.lodIndex = plan.lodIndex;
|
||||
const epoch = ++this.epoch;
|
||||
this.cancelPending();
|
||||
|
||||
if (!build.dataSource) {
|
||||
if (this.current) this.viewer.dataSources.remove(this.current, true);
|
||||
if (plan.mode === "hidden") {
|
||||
this.onAddressingChange();
|
||||
if (this.current) this.removeResources(this.current.resources);
|
||||
this.current = null;
|
||||
this.viewer.scene.requestRender();
|
||||
return;
|
||||
}
|
||||
|
||||
const next = build.dataSource;
|
||||
const addressing: GridAddressing = plan.mode === "3d" ? {
|
||||
mode: "3d",
|
||||
lod: plan.lod.index + 1,
|
||||
stepMeters: plan.grid.stepMeters,
|
||||
radiusMeters: plan.grid.radiusMeters,
|
||||
originLatitude: plan.origin.latitude,
|
||||
originLongitude: plan.origin.longitude,
|
||||
inverseEnu: plan.inverseEnu,
|
||||
} : {
|
||||
mode: "graticule",
|
||||
lod: plan.lod.index + 1,
|
||||
stepDegrees: plan.grid.stepDegrees,
|
||||
};
|
||||
const activeAddressing = this.pending?.addressing ?? this.current?.addressing;
|
||||
if (this.addressingKey(activeAddressing) !== this.addressingKey(addressing)) this.onAddressingChange();
|
||||
const resources = materializeGridLayer(plan, ++this.serial);
|
||||
const next = { resources, addressing };
|
||||
this.pending = next;
|
||||
void this.viewer.dataSources.add(next);
|
||||
this.mountResources(resources);
|
||||
let readyFrames = 0;
|
||||
const commit = () => {
|
||||
if (epoch !== this.epoch || this.pending !== next) return;
|
||||
@@ -1102,33 +1374,100 @@ class GridLayerController {
|
||||
const previous = this.current;
|
||||
this.current = next;
|
||||
this.pending = null;
|
||||
if (previous && previous !== next) this.viewer.dataSources.remove(previous, true);
|
||||
if (previous && previous !== next) this.removeResources(previous.resources);
|
||||
this.viewer.scene.requestRender();
|
||||
};
|
||||
this.removeReadyListener = this.viewer.scene.postRender.addEventListener(() => {
|
||||
readyFrames = this.viewer.dataSourceDisplay.ready ? readyFrames + 1 : 0;
|
||||
if (readyFrames >= 2) commit();
|
||||
else this.viewer.scene.requestRender();
|
||||
});
|
||||
// A slow terrain worker must not leave an obsolete buffer mounted forever.
|
||||
// The old layer stays visible during this grace period, so the fallback is
|
||||
// bounded cleanup rather than a visible blank-before-build swap.
|
||||
this.fallbackTimer = window.setTimeout(commit, 2_000);
|
||||
this.viewer.scene.requestRender();
|
||||
}
|
||||
|
||||
pick(worldPosition: Cartesian3): GridSectorSelection | null {
|
||||
const addressing = this.pending?.addressing ?? this.current?.addressing;
|
||||
if (!addressing) return null;
|
||||
if (addressing.mode === "3d") {
|
||||
const ellipsoid = this.viewer.scene.globe.ellipsoid;
|
||||
const surface = ellipsoid.scaleToGeodeticSurface(worldPosition, new Cartesian3()) ?? worldPosition;
|
||||
const normal = ellipsoid.geodeticSurfaceNormal(surface, new Cartesian3());
|
||||
const localSurface = Matrix4.multiplyByPoint(addressing.inverseEnu, surface, new Cartesian3());
|
||||
const localNormal = Matrix4.multiplyByPointAsVector(addressing.inverseEnu, normal, new Cartesian3());
|
||||
// Geometry is addressed on the origin's ENU tangent plane and then
|
||||
// reprojected along the WGS84 normal. Intersect that normal with the
|
||||
// same plane so analytic picking returns the exact source cell even at
|
||||
// the outer edge of a 1,000 km LOD.
|
||||
const normalScale = Math.abs(localNormal.z) > 1e-9 ? -localSurface.z / localNormal.z : 0;
|
||||
const local = new Cartesian3(
|
||||
localSurface.x + localNormal.x * normalScale,
|
||||
localSurface.y + localNormal.y * normalScale,
|
||||
0,
|
||||
);
|
||||
if (local.x ** 2 + local.y ** 2 > addressing.radiusMeters ** 2) return null;
|
||||
const definition = {
|
||||
lod: addressing.lod,
|
||||
originLatitude: addressing.originLatitude,
|
||||
originLongitude: addressing.originLongitude,
|
||||
stepMeters: addressing.stepMeters,
|
||||
};
|
||||
const address = localSectorAt({ eastMeters: local.x, northMeters: local.y }, definition);
|
||||
return {
|
||||
id: address.id,
|
||||
lod: addressing.lod,
|
||||
mode: "3d",
|
||||
address: { eastIndex: address.eastIndex, northIndex: address.northIndex },
|
||||
bounds: localSectorBounds(address, addressing.stepMeters),
|
||||
units: "meters-enu",
|
||||
};
|
||||
}
|
||||
const cartographic = Cartographic.fromCartesian(worldPosition);
|
||||
const address = graticuleSectorAt({
|
||||
longitude: CesiumMath.toDegrees(cartographic.longitude),
|
||||
latitude: CesiumMath.toDegrees(cartographic.latitude),
|
||||
}, { lod: addressing.lod, stepDegrees: addressing.stepDegrees });
|
||||
return {
|
||||
id: address.id,
|
||||
lod: addressing.lod,
|
||||
mode: "graticule",
|
||||
address: { longitudeIndex: address.longitudeIndex, latitudeIndex: address.latitudeIndex },
|
||||
bounds: graticuleSectorBounds(address, addressing.stepDegrees),
|
||||
units: "degrees-wgs84",
|
||||
};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.epoch += 1;
|
||||
this.cancelPending();
|
||||
if (this.current) this.viewer.dataSources.remove(this.current, true);
|
||||
if (this.current) this.removeResources(this.current.resources);
|
||||
this.current = null;
|
||||
}
|
||||
|
||||
private mountResources(resources: GridResources) {
|
||||
const add = this.viewer.dataSources.add(resources.dataSource);
|
||||
void add.then((dataSource) => {
|
||||
if (this.retiredResources.has(resources) && !this.viewer.isDestroyed()) {
|
||||
this.viewer.dataSources.remove(dataSource, true);
|
||||
}
|
||||
}).catch(() => undefined);
|
||||
if (resources.points) this.viewer.scene.primitives.add(resources.points);
|
||||
if (resources.crosses) this.viewer.scene.primitives.add(resources.crosses);
|
||||
}
|
||||
|
||||
private removeResources(resources: GridResources) {
|
||||
this.retiredResources.add(resources);
|
||||
this.viewer.dataSources.remove(resources.dataSource, true);
|
||||
if (resources.points) this.viewer.scene.primitives.remove(resources.points);
|
||||
if (resources.crosses) this.viewer.scene.primitives.remove(resources.crosses);
|
||||
}
|
||||
|
||||
private cancelPending() {
|
||||
this.removeReadyListener?.();
|
||||
this.removeReadyListener = null;
|
||||
if (this.fallbackTimer !== null) window.clearTimeout(this.fallbackTimer);
|
||||
this.fallbackTimer = null;
|
||||
if (this.pending) this.viewer.dataSources.remove(this.pending, true);
|
||||
if (this.pending) this.removeResources(this.pending.resources);
|
||||
this.pending = null;
|
||||
}
|
||||
}
|
||||
@@ -1184,6 +1523,7 @@ function applyPresentation(
|
||||
|
||||
export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
|
||||
onSelect?: (entityId: string) => void;
|
||||
onGridSectorSelect?: (sector: GridSectorSelection | null) => void;
|
||||
onGatewayHealth?: (health: MapGatewayHealth | null) => void;
|
||||
onProviderStatus?: (status: MapProviderStatus) => void;
|
||||
onCameraChange?: (camera: MapCameraView) => void;
|
||||
@@ -1197,6 +1537,7 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
|
||||
presentationFilters?: MapPresentationFilters;
|
||||
}>(function CesiumMapRenderer({
|
||||
onSelect,
|
||||
onGridSectorSelect,
|
||||
onGatewayHealth,
|
||||
onProviderStatus,
|
||||
onCameraChange,
|
||||
@@ -1224,6 +1565,7 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
|
||||
const presentationProfilesRef = useRef(presentationProfiles);
|
||||
const presentationFiltersRef = useRef(presentationFilters);
|
||||
const onSelectRef = useRef(onSelect);
|
||||
const onGridSectorSelectRef = useRef(onGridSectorSelect);
|
||||
const onCameraChangeRef = useRef(onCameraChange);
|
||||
const onCacheRefreshConsumedRef = useRef(onCacheRefreshConsumed);
|
||||
const onReadyChangeRef = useRef(onReadyChange);
|
||||
@@ -1234,6 +1576,10 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
|
||||
onSelectRef.current = onSelect;
|
||||
}, [onSelect]);
|
||||
|
||||
useEffect(() => {
|
||||
onGridSectorSelectRef.current = onGridSectorSelect;
|
||||
}, [onGridSectorSelect]);
|
||||
|
||||
useEffect(() => {
|
||||
onCameraChangeRef.current = onCameraChange;
|
||||
}, [onCameraChange]);
|
||||
@@ -1641,14 +1987,20 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
|
||||
const viewer = viewerRef.current;
|
||||
if (!viewer || viewer.isDestroyed()) return false;
|
||||
const entity = runtimeEntities([entityId])[0];
|
||||
const position = entity?.position?.getValue(viewer.clock.currentTime);
|
||||
if (!position) return false;
|
||||
const cartographic = Cartographic.fromCartesian(position);
|
||||
return focusCoordinates(
|
||||
CesiumMath.toDegrees(cartographic.longitude),
|
||||
CesiumMath.toDegrees(cartographic.latitude),
|
||||
);
|
||||
}, [focusCoordinates, runtimeEntities]);
|
||||
if (!entity) return false;
|
||||
|
||||
// A subject selection is an explicit navigation command. Using the
|
||||
// generic viewport-transfer helper here can produce an imperceptible or
|
||||
// invalid destination when the current screen centre misses the globe
|
||||
// (for example near the horizon). Cesium's entity-aware flight resolves
|
||||
// the time-dynamic position itself and restores the proven operational
|
||||
// map behaviour: centre the selected subject at an inspectable range.
|
||||
void viewer.flyTo(entity, {
|
||||
duration: 0.45,
|
||||
offset: new HeadingPitchRange(0, -0.9, 8_000),
|
||||
});
|
||||
return true;
|
||||
}, [runtimeEntities]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
startSpiralAnimation,
|
||||
@@ -1721,6 +2073,8 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
|
||||
let gridController: GridLayerController | undefined;
|
||||
let removeGridCameraListener: (() => void) | undefined;
|
||||
let removeGridCameraChangedListener: (() => void) | undefined;
|
||||
let gridCameraChangedTimer: number | undefined;
|
||||
let gridResizeTimer: number | undefined;
|
||||
let removeRefreshRenderListener: (() => void) | undefined;
|
||||
let removeRenderErrorListener: (() => void) | undefined;
|
||||
const removeProviderFailureListeners: Array<() => void> = [];
|
||||
@@ -1791,7 +2145,7 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
|
||||
viewer.scene.globe.depthTestAgainstTerrain = true;
|
||||
viewerRef.current = viewer;
|
||||
terrainRef.current = terrain;
|
||||
gridController = new GridLayerController(viewer);
|
||||
gridController = new GridLayerController(viewer, () => onGridSectorSelectRef.current?.(null));
|
||||
const rebuildGrid = () => gridController?.rebuild(presentationRef.current);
|
||||
rebuildGridRef.current = rebuildGrid;
|
||||
removeGridCameraListener = viewer.camera.moveEnd.addEventListener(() => {
|
||||
@@ -1800,7 +2154,11 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
|
||||
onCameraChangeRef.current?.(getCameraView(viewer!));
|
||||
});
|
||||
removeGridCameraChangedListener = viewer.camera.changed.addEventListener(() => {
|
||||
if (!presentationRef.current.gridRebuildOnMoveEnd) rebuildGrid();
|
||||
if (presentationRef.current.gridRebuildOnMoveEnd || gridCameraChangedTimer !== undefined) return;
|
||||
gridCameraChangedTimer = window.setTimeout(() => {
|
||||
gridCameraChangedTimer = undefined;
|
||||
rebuildGrid();
|
||||
}, 150);
|
||||
});
|
||||
|
||||
const providerStatus: MapProviderStatus = {
|
||||
@@ -1973,7 +2331,8 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
|
||||
handler.setInputAction((movement: { position: Cartesian2 }) => {
|
||||
const picked = viewer?.scene.pick(movement.position);
|
||||
const pickedId = picked?.id;
|
||||
if (pickedId instanceof Entity && pickedId.id) {
|
||||
if (pickedId instanceof Entity && pickedId.id && !String(pickedId.id).startsWith("grid/")) {
|
||||
onGridSectorSelectRef.current?.(null);
|
||||
onSelectRef.current?.(pickedId.id);
|
||||
return;
|
||||
}
|
||||
@@ -1982,12 +2341,27 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
|
||||
&& typeof pickedId === "object"
|
||||
&& (pickedId as Partial<HGeoZonePickId>).kind === "nodedc-hgeozone"
|
||||
&& typeof (pickedId as Partial<HGeoZonePickId>).entityId === "string"
|
||||
) onSelectRef.current?.((pickedId as HGeoZonePickId).entityId);
|
||||
) {
|
||||
onGridSectorSelectRef.current?.(null);
|
||||
onSelectRef.current?.((pickedId as HGeoZonePickId).entityId);
|
||||
return;
|
||||
}
|
||||
const ray = viewer?.camera.getPickRay(movement.position);
|
||||
const worldPosition = ray && viewer
|
||||
? viewer.scene.globe.pick(ray, viewer.scene) ?? viewer.camera.pickEllipsoid(movement.position, viewer.scene.globe.ellipsoid)
|
||||
: undefined;
|
||||
onGridSectorSelectRef.current?.(worldPosition ? gridController?.pick(worldPosition) ?? null : null);
|
||||
}, ScreenSpaceEventType.LEFT_CLICK);
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
if (!viewer || viewer.isDestroyed()) return;
|
||||
viewer.resize();
|
||||
viewer.scene.requestRender();
|
||||
if (gridResizeTimer === undefined) {
|
||||
gridResizeTimer = window.setTimeout(() => {
|
||||
gridResizeTimer = undefined;
|
||||
rebuildGrid();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
resizeObserver.observe(containerRef.current);
|
||||
} catch (error) {
|
||||
@@ -2014,6 +2388,8 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
|
||||
resizeObserver?.disconnect();
|
||||
removeGridCameraListener?.();
|
||||
removeGridCameraChangedListener?.();
|
||||
if (gridCameraChangedTimer !== undefined) window.clearTimeout(gridCameraChangedTimer);
|
||||
if (gridResizeTimer !== undefined) window.clearTimeout(gridResizeTimer);
|
||||
removeRefreshRenderListener?.();
|
||||
removeRenderErrorListener?.();
|
||||
for (const removeListener of removeProviderFailureListeners) removeListener();
|
||||
|
||||
Reference in New Issue
Block a user