feat(globe): port to CeliumJS
This commit is contained in:
parent
ec03425067
commit
eb7698e034
51 changed files with 3521 additions and 1992 deletions
166
src/lib/map/cesium-scene.ts
Normal file
166
src/lib/map/cesium-scene.ts
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import {
|
||||
Cartesian3,
|
||||
Color,
|
||||
ConstantPositionProperty,
|
||||
Entity,
|
||||
HorizontalOrigin,
|
||||
PolylineDashMaterialProperty,
|
||||
VerticalOrigin,
|
||||
type Viewer,
|
||||
} from 'cesium';
|
||||
import type {
|
||||
CircleOptions,
|
||||
LineOptions,
|
||||
MapLayer,
|
||||
Marker,
|
||||
MarkerOptions,
|
||||
Scene,
|
||||
} from './core';
|
||||
|
||||
/**
|
||||
* Cesium implementation of the Scene contract.
|
||||
*
|
||||
* Every position here goes through `Cartesian3.fromDegrees(lng, lat)`, which
|
||||
* converts geographic degrees straight to earth-centred coordinates. Nothing
|
||||
* passes through a Mercator tiler, so latitudes above 85.051129° — where the
|
||||
* MapLibre implementation silently clamped every vertex — render correctly.
|
||||
* That is the whole reason this file exists.
|
||||
*/
|
||||
|
||||
/** '#rrggbb' + opacity -> Cesium Color. Falls back to black on an unparseable string. */
|
||||
function toColor(css: string | undefined, opacity = 1): Color {
|
||||
const parsed = Color.fromCssColorString(css ?? '#000');
|
||||
// fromCssColorString returns undefined for garbage rather than throwing.
|
||||
return (parsed ?? Color.BLACK).withAlpha(opacity);
|
||||
}
|
||||
|
||||
export class CesiumScene implements Scene {
|
||||
private entities = new Map<string, Entity>();
|
||||
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
private viewer: Viewer,
|
||||
) {}
|
||||
|
||||
private scopeId(id: string): string {
|
||||
return `${this.name}__${id}`;
|
||||
}
|
||||
|
||||
private add(id: string, entity: Entity): Entity {
|
||||
this.remove(id);
|
||||
const added = this.viewer.entities.add(entity);
|
||||
this.entities.set(id, added);
|
||||
return added;
|
||||
}
|
||||
|
||||
addLine(id: string, options: LineOptions): MapLayer {
|
||||
// LatLngTuple is [lat, lng] or [lat, lng, alt]; Cesium wants lng first.
|
||||
// Swapping these is the classic bug here.
|
||||
// `c.length === 3` is what narrows the LatLngTuple union for TypeScript;
|
||||
// a `> 2` comparison does not.
|
||||
const heights: number[] = options.coords.map((c) => (c.length === 3 ? c[2] : 0));
|
||||
// Only lift the line into 3D when it actually has altitude. A track whose
|
||||
// every vertex sits at ground level (a bounding-box ring, or telemetry
|
||||
// before launch) must stay draped: at height 0 a polyline is coplanar
|
||||
// with the ellipsoid, z-fights it, and disappears entirely.
|
||||
const use3d = Math.max(...heights) > 1;
|
||||
const positions = use3d
|
||||
? Cartesian3.fromDegreesArrayHeights(
|
||||
options.coords.flatMap((c, i) => [c[1], c[0], heights[i]]),
|
||||
)
|
||||
: Cartesian3.fromDegreesArray(options.coords.flatMap((c) => [c[1], c[0]]));
|
||||
const color = toColor(options.color, options.opacity ?? 1);
|
||||
this.add(
|
||||
id,
|
||||
new Entity({
|
||||
id: this.scopeId(id),
|
||||
polyline: {
|
||||
positions,
|
||||
width: options.width ?? 3,
|
||||
material: options.dashArray
|
||||
? new PolylineDashMaterialProperty({
|
||||
color,
|
||||
dashLength: options.dashArray[0] + options.dashArray[1],
|
||||
})
|
||||
: color,
|
||||
// arcType is left at its default, GEODESIC.
|
||||
// Draped only for ground-level geometry (see use3d above); a
|
||||
// real flight is drawn at its own altitude.
|
||||
clampToGround: !use3d,
|
||||
},
|
||||
}),
|
||||
);
|
||||
return { id, remove: () => this.remove(id) };
|
||||
}
|
||||
|
||||
addCircle(id: string, options: CircleOptions): MapLayer {
|
||||
// radiusPx is screen-space, matching the MapLibre circle-layer semantics
|
||||
// the callers were written against; PointGraphics.pixelSize is the direct
|
||||
// equivalent (diameter, hence the doubling).
|
||||
this.add(
|
||||
id,
|
||||
new Entity({
|
||||
id: this.scopeId(id),
|
||||
position: Cartesian3.fromDegrees(options.center[0], options.center[1]),
|
||||
point: {
|
||||
pixelSize: (options.radiusPx ?? 5) * 2,
|
||||
color: toColor(options.color, options.opacity ?? 1),
|
||||
outlineColor: toColor(options.strokeColor, 1),
|
||||
outlineWidth: options.strokeWidth ?? 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
return { id, remove: () => this.remove(id) };
|
||||
}
|
||||
|
||||
addMarker(id: string, options: MarkerOptions): Marker {
|
||||
const alt = options.altitude ?? 0;
|
||||
const entity = this.add(
|
||||
id,
|
||||
new Entity({
|
||||
id: this.scopeId(id),
|
||||
position: Cartesian3.fromDegrees(options.lngLat[0], options.lngLat[1], alt),
|
||||
...(options.iconUrl
|
||||
? {
|
||||
billboard: {
|
||||
image: options.iconUrl,
|
||||
width: options.iconSize?.[0],
|
||||
height: options.iconSize?.[1],
|
||||
horizontalOrigin: HorizontalOrigin.CENTER,
|
||||
verticalOrigin: VerticalOrigin.BOTTOM,
|
||||
},
|
||||
}
|
||||
: { point: { pixelSize: 10, color: Color.CRIMSON } }),
|
||||
// Shown by Cesium's own selection UI; the MapLibre build used a
|
||||
// hover popup, which has no direct Cesium equivalent.
|
||||
...(options.popupHtml ? { description: options.popupHtml } : {}),
|
||||
}),
|
||||
);
|
||||
return {
|
||||
setLngLat: (pos) => {
|
||||
// LngLatTuple has no altitude, so a moved marker keeps the one it
|
||||
// was created with.
|
||||
entity.position = new ConstantPositionProperty(
|
||||
Cartesian3.fromDegrees(pos[0], pos[1], alt),
|
||||
);
|
||||
},
|
||||
remove: () => this.remove(id),
|
||||
};
|
||||
}
|
||||
|
||||
remove(id: string): void {
|
||||
const e = this.entities.get(id);
|
||||
if (!e) return;
|
||||
this.viewer.entities.remove(e);
|
||||
this.entities.delete(id);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
for (const e of this.entities.values()) this.viewer.entities.remove(e);
|
||||
this.entities.clear();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue