Spaces:
Configuration error
Configuration error
File size: 6,420 Bytes
9f21d0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | import {
ArcType,
Cartesian3,
ClockRange,
Color,
ColorGeometryInstanceAttribute,
CustomDataSource,
GeometryInstance,
JulianDate,
Matrix3,
PolylineColorAppearance,
PolylineGeometry,
SampledPositionProperty,
Transforms,
} from "@cesium/engine";
import type { Viewer } from "@cesium/widgets";
import { PolylineBatch } from "../util/PolylineBatch";
import { EARTH, initialState, type PropagationRequest, type PropagationSample, propagate } from "./propagator";
export interface SimulationSceneRequest extends PropagationRequest {
magneticField: boolean;
}
/**
* Bridges the custom numerical propagator into Satvis's Cesium scene and clock.
* Satvis tracks use Cesium entities too, so custom scenarios behave like native
* visual objects: they orbit, scrub, play, and can be inspected in the same view.
*/
export class SimulationManager {
readonly #source = new CustomDataSource("orbital-simulation");
readonly #orbits: PolylineBatch;
#orbitGeometry: GeometryInstance | undefined;
#run = 0;
constructor(private readonly viewer: Viewer) {
void viewer.dataSources.add(this.#source);
this.#orbits = new PolylineBatch(viewer, "inertial");
}
clear(): void {
this.#source.entities.removeAll();
if (this.#orbitGeometry) {
this.#orbits.remove(this.#orbitGeometry);
this.#orbitGeometry = undefined;
}
}
run(request: SimulationSceneRequest): PropagationSample[] {
const samples = propagate(request);
this.clear();
const runId = ++this.#run;
const start = JulianDate.now();
const positions = samples.map((sample) => inertialToFixed(sample.state, JulianDate.addSeconds(start, sample.elapsedSeconds, new JulianDate())));
const sampledPosition = new SampledPositionProperty();
samples.forEach((sample, index) => {
const position = positions[index];
if (position) sampledPosition.addSample(JulianDate.addSeconds(start, sample.elapsedSeconds, new JulianDate()), position);
});
this.#source.entities.add({
id: `simulation-spacecraft-${runId}`,
name: "Custom propagated spacecraft",
position: sampledPosition,
point: { pixelSize: 10, color: Color.CYAN, outlineColor: Color.WHITE, outlineWidth: 2, disableDepthTestDistance: Number.POSITIVE_INFINITY },
label: { text: "SIMULATION", font: "12px sans-serif", fillColor: Color.CYAN, outlineColor: Color.BLACK, outlineWidth: 2, pixelOffset: new Cartesian3(10, -12, 0), disableDepthTestDistance: Number.POSITIVE_INFINITY },
});
this.addClosedInertialOrbit(request, runId);
if (request.magneticField) this.addMagneticField(runId);
this.viewer.clock.startTime = JulianDate.clone(start);
this.viewer.clock.stopTime = JulianDate.addSeconds(start, request.durationSeconds, new JulianDate());
this.viewer.clock.currentTime = JulianDate.clone(start);
this.viewer.clock.clockRange = ClockRange.LOOP_STOP;
this.viewer.clock.multiplier = Math.max(1, request.durationSeconds / 90);
this.viewer.clock.shouldAnimate = true;
this.viewer.scene.requestRender();
return samples;
}
/**
* The propagated samples above are transformed into Earth-fixed coordinates so
* the spacecraft moves correctly above the rotating globe. They must not be
* used to draw the orbit itself: that would be a finite ground-track segment,
* which is why the old cyan line looked open. The guide below is one osculating
* revolution in the inertial frame, rendered by Satvis's inertial batch.
*/
private addClosedInertialOrbit(request: SimulationSceneRequest, runId: number): void {
const period = osculatingPeriod(request);
const guideSamples = propagate({
...request,
durationSeconds: period,
sampleCount: Math.max(360, Math.ceil(period / 20) + 1),
});
const positions = guideSamples.map((sample) => new Cartesian3(sample.state[0], sample.state[1], sample.state[2]));
const firstPosition = positions[0];
if (!firstPosition) return;
// Numerical perturbations mean the final integration point can differ by a
// few metres. This is a closed one-revolution guide, so join it exactly at
// its epoch rather than drawing an artificial open seam.
positions[positions.length - 1] = Cartesian3.clone(firstPosition);
this.#orbitGeometry = new GeometryInstance({
geometry: new PolylineGeometry({
positions,
width: 2.5,
arcType: ArcType.NONE,
vertexFormat: PolylineColorAppearance.VERTEX_FORMAT,
}),
attributes: { color: ColorGeometryInstanceAttribute.fromColor(Color.CYAN.withAlpha(0.92)) },
id: `simulation-orbit-${runId}`,
});
this.#orbits.add(this.#orbitGeometry);
}
/** A dipole field-line visualization, deliberately separate from force integration. */
private addMagneticField(runId: number): void {
const fieldColor = Color.fromCssColorString("#6ce8ff").withAlpha(0.34);
for (let shell = 1.25; shell <= 3.5; shell += 0.38) {
const limit = Math.acos(Math.sqrt(1 / shell));
const positions: Cartesian3[] = [];
for (let latitude = -limit; latitude <= limit; latitude += 0.025) {
const radius = shell * EARTH.radius * Math.cos(latitude) ** 2;
positions.push(new Cartesian3(radius * Math.cos(latitude), 0, radius * Math.sin(latitude)));
}
this.#source.entities.add({ id: `simulation-magnetic-${runId}-${shell.toFixed(2)}`, name: "Earth magnetic field (visual guide)", polyline: { positions, width: 1, material: fieldColor, clampToGround: false } });
}
}
}
/** The Keplerian period of the initial osculating state, in seconds. */
function osculatingPeriod(request: PropagationRequest): number {
const state = initialState(request);
const radius = Math.hypot(state[0], state[1], state[2]);
const speedSquared = state[3] ** 2 + state[4] ** 2 + state[5] ** 2;
const semimajorAxis = 1 / (2 / radius - speedSquared / EARTH.mu);
return 2 * Math.PI * Math.sqrt(semimajorAxis ** 3 / EARTH.mu);
}
/** Keep the moving sample in the exact frame used by the inertial orbit primitive. */
function inertialToFixed(state: PropagationSample["state"], time: JulianDate): Cartesian3 {
const inertial = new Cartesian3(state[0], state[1], state[2]);
const icrfToFixed = Transforms.computeIcrfToFixedMatrix(time);
return icrfToFixed ? Matrix3.multiplyByVector(icrfToFixed, inertial, new Cartesian3()) : inertial;
}
|