feat(map): add reference layers and stabilize workspace controls
This commit is contained in:
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user