feat(map): add reference layers and stabilize workspace controls

This commit is contained in:
Codex
2026-07-25 13:12:24 +03:00
parent c859ae4db0
commit eaecce9f56
29 changed files with 1593 additions and 247 deletions
+108 -18
View File
@@ -4,6 +4,7 @@ import {
Cartesian3,
BingMapsImageryProvider,
BingMapsStyle,
BillboardGraphics,
Color,
Credit,
Cesium3DTileset,
@@ -29,6 +30,7 @@ import {
ImageryLayer,
JulianDate,
LabelGraphics,
LabelStyle,
Matrix4,
Math as CesiumMath,
PerInstanceColorAppearance,
@@ -64,6 +66,41 @@ const MAX_SPIRAL_SUBSTEPS_PER_FRAME = 300;
const TERRAIN_SAMPLE_TIMEOUT_MS = 12_000;
const SPIRAL_TILE_WAIT_TIMEOUT_MS = 45_000;
const MAX_HGEOZONE_INSTANCES_PER_BATCH = 256;
const elevatedTargetImageCache = new Map<string, string>();
function elevatedTargetImage(
fillColor: Color,
outlineColor: Color,
outlineWidthPx: number,
headSizePx: number,
) {
const safeHeadSize = Math.max(1, headSizePx);
const safeOutlineWidth = Math.max(0, outlineWidthPx);
const imageSize = Math.max(1, Math.ceil(safeHeadSize + safeOutlineWidth * 2));
const key = [
fillColor.toCssColorString(),
outlineColor.toCssColorString(),
safeOutlineWidth,
safeHeadSize,
imageSize,
].join("|");
const cached = elevatedTargetImageCache.get(key);
if (cached) return { image: cached, size: imageSize };
const center = imageSize / 2;
const radius = Math.max(0.5, safeHeadSize / 2);
const svg = [
`<svg xmlns="http://www.w3.org/2000/svg" width="${imageSize}" height="${imageSize}" viewBox="0 0 ${imageSize} ${imageSize}">`,
`<circle cx="${center}" cy="${center}" r="${radius}" fill="${fillColor.toCssColorString()}"`,
safeOutlineWidth > 0
? ` stroke="${outlineColor.toCssColorString()}" stroke-width="${safeOutlineWidth}"/>`
: "/>",
"</svg>",
].join("");
const image = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
elevatedTargetImageCache.set(key, image);
return { image, size: imageSize };
}
type RuntimeConfig = {
cesiumVersion: string;
provider: string;
@@ -622,7 +659,11 @@ function syncRuntimeDataSources(
faultedHGeoZoneGeometryKeys: Set<string>,
) {
const activeBindings = new Map(bindings
.filter((binding) => binding.slotId === "points" || binding.slotId === "zones")
.filter((binding) => (
binding.slotId === "points"
|| binding.slotId === "reference-points"
|| binding.slotId === "zones"
))
.map((binding) => [binding.bindingId, binding]));
for (const [bindingId, dataSource] of dataSources) {
@@ -657,7 +698,7 @@ function syncRuntimeDataSources(
const label = mapRuntimeDisplayLabel(fact, profile);
const baseEntityId = mapRuntimeEntityId(binding.bindingId, fact);
if (fact.geometry.type === "Point" && binding.slotId === "points") {
if (fact.geometry.type === "Point" && (binding.slotId === "points" || binding.slotId === "reference-points")) {
if (profile && profile.target.variant !== "elevated-spike") continue;
const entityId = baseEntityId;
wanted.add(entityId);
@@ -666,19 +707,23 @@ function syncRuntimeDataSources(
entity.name = label;
entity.polygon = undefined;
if (!profile) {
entity.billboard = undefined;
entity.position = new ConstantPositionProperty(Cartesian3.fromDegrees(longitude, latitude, 0));
entity.polyline = undefined;
entity.point = new PointGraphics({
pixelSize: 10,
color,
outlineColor: Color.fromCssColorString("#0c0d12").withAlpha(0.72),
outlineWidth: 2,
outlineColor: Color.TRANSPARENT,
outlineWidth: 0,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
});
entity.label = new LabelGraphics({
text: label,
font: "700 13px Arial",
fillColor: Color.WHITE,
outlineColor: Color.TRANSPARENT,
outlineWidth: 0,
style: LabelStyle.FILL,
showBackground: true,
backgroundColor: Color.BLACK.withAlpha(0.72),
backgroundPadding: new Cartesian2(10, 7),
@@ -714,11 +759,19 @@ function syncRuntimeDataSources(
material: color,
show: showBelowCameraHeight(viewer, target.hideCameraHeightMeters),
});
entity.point = new PointGraphics({
pixelSize: target.headSizePx,
const targetImage = elevatedTargetImage(
color,
outlineColor: Color.fromCssColorString(target.outlineColor).withAlpha(target.outlineOpacity),
outlineWidth: target.outlineWidthPx,
Color.fromCssColorString(target.outlineColor).withAlpha(target.outlineOpacity),
target.outlineWidthPx,
target.headSizePx,
);
entity.point = undefined;
entity.billboard = new BillboardGraphics({
image: targetImage.image,
width: targetImage.size,
height: targetImage.size,
horizontalOrigin: HorizontalOrigin.CENTER,
verticalOrigin: VerticalOrigin.CENTER,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
show: showBelowCameraHeight(viewer, target.hideCameraHeightMeters),
});
@@ -726,9 +779,9 @@ function syncRuntimeDataSources(
text: label,
font: `${profile.label.fontWeight} ${profile.label.sizePx}px Arial`,
fillColor: Color.fromCssColorString(profile.label.color),
outlineColor: Color.fromCssColorString(profile.label.outlineColor),
outlineWidth: profile.label.outlineWidthPx,
style: 2,
outlineColor: Color.TRANSPARENT,
outlineWidth: 0,
style: LabelStyle.FILL,
showBackground: profile.label.backgroundOpacity > 0,
backgroundColor: Color.fromCssColorString(profile.label.backgroundColor).withAlpha(profile.label.backgroundOpacity),
backgroundPadding: new Cartesian2(profile.label.paddingX, profile.label.paddingY),
@@ -759,6 +812,7 @@ function syncRuntimeDataSources(
const entity = dataSource.entities.getById(baseEntityId) ?? dataSource.entities.add({ id: baseEntityId });
entity.name = label;
entity.position = new ConstantPositionProperty(Cartesian3.fromDegrees(labelAnchor[0] / divisor, labelAnchor[1] / divisor, 0));
entity.billboard = undefined;
entity.point = undefined;
entity.polygon = undefined;
entity.polyline = undefined;
@@ -766,9 +820,9 @@ function syncRuntimeDataSources(
text: label,
font: profile ? `${profile.label.fontWeight} ${profile.label.sizePx}px Arial` : "700 13px Arial",
fillColor: profile ? Color.fromCssColorString(profile.label.color) : Color.WHITE,
outlineColor: profile ? Color.fromCssColorString(profile.label.outlineColor) : Color.BLACK,
outlineWidth: profile?.label.outlineWidthPx ?? 1,
style: 2,
outlineColor: Color.TRANSPARENT,
outlineWidth: 0,
style: LabelStyle.FILL,
showBackground: (profile?.label.backgroundOpacity ?? 0.72) > 0,
backgroundColor: profile
? Color.fromCssColorString(profile.label.backgroundColor).withAlpha(profile.label.backgroundOpacity)
@@ -1330,10 +1384,46 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
const viewer = viewerRef.current;
if (!viewer || viewer.isDestroyed()) return false;
const entity = runtimeEntities([entityId])[0];
if (!entity) return false;
void viewer.flyTo(entity, {
duration: 0.45,
offset: new HeadingPitchRange(0, -0.9, 8_000),
const position = entity?.position?.getValue(viewer.clock.currentTime);
if (!position) return false;
// Preserve the observer's current composition exactly as the proven
// legacy MMAP/AIS interaction does: move the camera/viewport frame to the
// selected point without replacing heading, pitch, roll or ground offset.
const camera = viewer.camera;
const canvas = viewer.scene.canvas;
const viewportCenter = camera.pickEllipsoid(
new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2),
viewer.scene.globe.ellipsoid,
);
const cartographic = Cartographic.fromCartesian(position);
const groundTarget = Cartesian3.fromRadians(
cartographic.longitude,
cartographic.latitude,
0,
viewer.scene.globe.ellipsoid,
);
const destination = viewportCenter
? Cartesian3.add(
groundTarget,
Cartesian3.subtract(camera.position, viewportCenter, new Cartesian3()),
new Cartesian3(),
)
: Cartesian3.fromRadians(
cartographic.longitude,
cartographic.latitude,
camera.positionCartographic.height,
viewer.scene.globe.ellipsoid,
);
camera.flyTo({
destination,
orientation: {
heading: camera.heading,
pitch: camera.pitch,
roll: camera.roll,
},
duration: 2.1,
});
return true;
}, [runtimeEntities]);