Kinda works, improve intercept

This commit is contained in:
Martin Asprusten 2026-04-01 00:03:49 +02:00
parent 8e43c1f51b
commit a14f1268ab
No known key found for this signature in database
13 changed files with 1278 additions and 105 deletions

View File

@ -39,7 +39,7 @@ export const Eve: Body = {
rotationPeriod: 80500, rotationPeriod: 80500,
sphereOfInfluence: 85109365, sphereOfInfluence: 85109365,
closestSafeDistance: 790000, closestSafeDistance: 790000,
initialMeridianLongitude: 0 initialMeridianLongitude: 0.00040726270866286995
}; };
export const Gilly: Body = { export const Gilly: Body = {
@ -50,7 +50,7 @@ export const Gilly: Body = {
rotationPeriod: 28255, rotationPeriod: 28255,
sphereOfInfluence: 126123.27, sphereOfInfluence: 126123.27,
closestSafeDistance: 19400, closestSafeDistance: 19400,
initialMeridianLongitude: 0.0859373 initialMeridianLongitude: 0.08784209395031439
}; };
export const Kerbin: Body = { export const Kerbin: Body = {

View File

@ -133,6 +133,27 @@ export function addVector(vectorOne: number[][], vectorTwo: number[][]): number[
return result; return result;
} }
export function addMatrix(matrixOne: number[][], matrixTwo: number[][]): number[][] {
if (!checkIfValidMatrix(matrixOne) || !checkIfValidMatrix(matrixTwo)) {
throw new TypeError("Two valid matrices are required");
}
if (matrixOne.length != matrixTwo.length || matrixOne[0].length != matrixTwo[0].length) {
throw new TypeError("The two matrices need to have the same dimensions");
}
var result = [];
for (var i = 0; i < matrixOne.length; i++) {
let row = [];
for (var j = 0; j < matrixOne[0].length; j++) {
row.push(matrixOne[i][j] + matrixTwo[i][j]);
}
result.push(row);
}
return result;
}
export function subtractVector(vectorOne: number[][], vectorTwo: number[][]): number[][] { export function subtractVector(vectorOne: number[][], vectorTwo: number[][]): number[][] {
if (!checkIfValidMatrix(vectorOne) || !checkIfValidMatrix(vectorTwo)) { if (!checkIfValidMatrix(vectorOne) || !checkIfValidMatrix(vectorTwo)) {
throw new TypeError("Two valid matrices are required"); throw new TypeError("Two valid matrices are required");

View File

@ -1,5 +1,6 @@
import type { InterpolationParameters } from "../gui/interpolate";
import type { Body } from "./constants"; import type { Body } from "./constants";
import { addVector, getVectorMagnitude, multiplyMatrixWithScalar, normalizeVector, subtractVector, vectorCrossProduct, vectorDotProduct } from "./mathematics"; import { addMatrix, addVector, getVectorMagnitude, matrixMultiply, multiplyMatrixWithScalar, normalizeVector, subtractVector, vectorCrossProduct, vectorDotProduct } from "./mathematics";
export interface Orbit { export interface Orbit {
semiLatusRectum: number, semiLatusRectum: number,
@ -32,12 +33,42 @@ export interface Manoeuvre {
export interface Transfer { export interface Transfer {
transferOrbit: Orbit, transferOrbit: Orbit,
transferOrbitTrueAnomalyAtManoeuvreOne: number,
transferOrbitTrueanomalyAtManoeuvreTwo: number,
firstManoeuvre: Manoeuvre, firstManoeuvre: Manoeuvre,
secondManoeuvre: Manoeuvre, secondManoeuvre: Manoeuvre,
farthestPointDistance: number, farthestPointDistance: number,
closestPointDistance: number, closestPointDistance: number,
} }
export interface Landing {
transferOrbit: Orbit,
transferManoeuvre: Manoeuvre,
brakingDeltaV: number,
brakingAltitude: number,
brakingTimestamp: number,
totalDeltaV: number
}
export interface ShipParameters {
mass: number,
thrust: number,
specificImpulse: number
};
export interface LandingParameters {
targetLatitude: number;
targetLongitude: number;
targetAltitude: number;
minimumAngle: number;
};
export const DefaultShipParameters: ShipParameters = {
mass: 1000,
thrust: 100,
specificImpulse: 100
};
export class LambertSolutions { export class LambertSolutions {
// Pre-calculate some values to make finding an orbit based on the parameter gamma faster // Pre-calculate some values to make finding an orbit based on the parameter gamma faster
// Naming these in a good way is very hard. To see where they came from, look at // Naming these in a good way is very hard. To see where they came from, look at
@ -161,6 +192,11 @@ export class LambertSolutions {
transferGoalTrueAnomaly -= 2 * Math.PI; transferGoalTrueAnomaly -= 2 * Math.PI;
} }
if (transferStartTrueAnomaly < 2 * Math.PI && transferGoalTrueAnomaly > 2 * Math.PI) {
transferStartTrueAnomaly -= 2 * Math.PI;
transferGoalTrueAnomaly -= 2 * Math.PI;
}
let closestPoint; let closestPoint;
if (transferStartTrueAnomaly < 0 && transferGoalTrueAnomaly >= 0) { if (transferStartTrueAnomaly < 0 && transferGoalTrueAnomaly >= 0) {
closestPoint = semiLatusRectum / (1 + eccentricity); closestPoint = semiLatusRectum / (1 + eccentricity);
@ -220,6 +256,8 @@ export class LambertSolutions {
return { return {
transferOrbit: transferOrbit, transferOrbit: transferOrbit,
transferOrbitTrueAnomalyAtManoeuvreOne: transferStartTrueAnomaly,
transferOrbitTrueanomalyAtManoeuvreTwo: transferGoalTrueAnomaly,
firstManoeuvre: startManoeuvre, firstManoeuvre: startManoeuvre,
secondManoeuvre: goalManoeuvre, secondManoeuvre: goalManoeuvre,
closestPointDistance: closestPoint, closestPointDistance: closestPoint,
@ -308,14 +346,15 @@ export function getEccentricAndTrueAnomalyFromMeanAnomaly(meanAnomaly: number, e
keplerEquationDerivative = (guess: number) => eccentricity * Math.cosh(guess) - 1; keplerEquationDerivative = (guess: number) => eccentricity * Math.cosh(guess) - 1;
} }
while (Math.abs(keplerEquation(eccentricAnomaly)) > 0.000001) { while (Math.abs(keplerEquation(eccentricAnomaly)) > 0.0000000001) {
eccentricAnomaly = eccentricAnomaly - keplerEquation(eccentricAnomaly) / keplerEquationDerivative(eccentricAnomaly); eccentricAnomaly = eccentricAnomaly - keplerEquation(eccentricAnomaly) / keplerEquationDerivative(eccentricAnomaly);
} }
if (eccentricity < 1) { if (eccentricity < 1) {
trueAnomaly = 2*Math.atan(Math.sqrt((1 + eccentricity) / (1 - eccentricity)) * Math.tan(eccentricAnomaly / 2)); let beta = eccentricity / (1 + Math.sqrt(1 - eccentricity**2));
trueAnomaly = eccentricAnomaly + 2 * Math.atan(beta * Math.sin(eccentricAnomaly) / (1 - beta * Math.cos(eccentricAnomaly)));
} else { } else {
trueAnomaly = 2*Math.atan(Math.sqrt((eccentricity + 1) / (eccentricity - 1)) * Math.tanh(eccentricAnomaly / 2)); trueAnomaly = Math.atan2(Math.sqrt(eccentricity**2 - 1) * Math.sinh(eccentricAnomaly), (eccentricity - Math.cosh(eccentricAnomaly)));
} }
} }
@ -378,7 +417,7 @@ export function getOrbitalCoordinatesFromAltitude(altitude: number, headingInwar
meanAnomaly = eccentricAnomaly; meanAnomaly = eccentricAnomaly;
} else if (orbit.eccentricity < 1) { } else if (orbit.eccentricity < 1) {
eccentricAnomaly = Math.atan2(Math.sqrt(1 - orbit.eccentricity**2)*Math.sin(trueAnomaly), orbit.eccentricity + Math.cos(trueAnomaly)); eccentricAnomaly = Math.atan2(Math.sqrt(1 - orbit.eccentricity**2)*Math.sin(trueAnomaly), orbit.eccentricity + Math.cos(trueAnomaly));
meanAnomaly = eccentricAnomaly - eccentricAnomaly * Math.sin(eccentricAnomaly); meanAnomaly = eccentricAnomaly - orbit.eccentricity * Math.sin(eccentricAnomaly);
} else { } else {
eccentricAnomaly = 2 * Math.atanh(Math.sqrt((orbit.eccentricity - 1) / (orbit.eccentricity + 1)) * Math.tan(trueAnomaly / 2)); eccentricAnomaly = 2 * Math.atanh(Math.sqrt((orbit.eccentricity - 1) / (orbit.eccentricity + 1)) * Math.tan(trueAnomaly / 2));
meanAnomaly = orbit.eccentricity * Math.sinh(eccentricAnomaly) - eccentricAnomaly; meanAnomaly = orbit.eccentricity * Math.sinh(eccentricAnomaly) - eccentricAnomaly;
@ -625,6 +664,7 @@ export function findCheapestLambertSolution(lambertSolutions: LambertSolutions):
} }
export type ProgressCallbackFunction = (transfersChecked: number, totalNumberOfTransfers: number, currentBestDeltaV: number | null, currentBestTransfer: Transfer | null) => void; export type ProgressCallbackFunction = (transfersChecked: number, totalNumberOfTransfers: number, currentBestDeltaV: number | null, currentBestTransfer: Transfer | null) => void;
export type LandingProgressCallbackFunction = (pathsChecked: number, totalNumberOfPaths: number, currentBestDeltaV: number | null, currentBestLanding: Landing | null) => void;
export function findCheapestTransfer(startingSituation: OrbitalCoordinates, targetOrbit: Orbit, body: Body, progressCallback?: ProgressCallbackFunction): Transfer | null { export function findCheapestTransfer(startingSituation: OrbitalCoordinates, targetOrbit: Orbit, body: Body, progressCallback?: ProgressCallbackFunction): Transfer | null {
// First, create a set of starting true anomalies // First, create a set of starting true anomalies
@ -647,7 +687,7 @@ export function findCheapestTransfer(startingSituation: OrbitalCoordinates, targ
} else { } else {
let finalAnomaly = Math.abs(Math.acos((startingSituation.orbit.semiLatusRectum - body.sphereOfInfluence) / (body.sphereOfInfluence * startingSituation.orbit.eccentricity))); let finalAnomaly = Math.abs(Math.acos((startingSituation.orbit.semiLatusRectum - body.sphereOfInfluence) / (body.sphereOfInfluence * startingSituation.orbit.eccentricity)));
let step = (finalAnomaly - startingSituation.trueAnomaly) / 100; let step = (finalAnomaly - startingSituation.trueAnomaly) / 100;
for (var i = 1; i < 101; i++) { for (var i = 0; i < 101; i++) {
startingTrueAnomalies.push(startingSituation.trueAnomaly + i * step); startingTrueAnomalies.push(startingSituation.trueAnomaly + i * step);
} }
} }
@ -712,70 +752,56 @@ export function findCheapestTransfer(startingSituation: OrbitalCoordinates, targ
return bestTransfer; return bestTransfer;
} }
export function findLambertSolutionsWithCorrectTime(lambertSolutions: LambertSolutions, expectedTime: number): Transfer | null{ export function findLambertSolutionsWithCorrectTime(lambertSolutions: LambertSolutions, expectedTime: number): Transfer | null {
const step = lambertSolutions.parabolaGamma - lambertSolutions.extremalGamma; // Do a secant method search. Start near the parabola
let scale = Math.abs(lambertSolutions.parabolaGamma - lambertSolutions.extremalGamma);
let x0 = lambertSolutions.parabolaGamma;
let x1 = lambertSolutions.parabolaGamma + scale / 100;
const getGamma = (steps: number) => { let t0 = lambertSolutions.getTransfer(x0);
return lambertSolutions.extremalGamma + steps*step; let t1 = lambertSolutions.getTransfer(x1);
}
let lowerSteps = 0; let f0 = t0.secondManoeuvre.time - expectedTime;
let lowerTransfer = lambertSolutions.getTransfer(getGamma(lowerSteps)); let f1 = t1.secondManoeuvre.time - expectedTime;
let lowerTime = 0;
let upperSteps = 1; let f2 = 100;
let upperTransfer = lambertSolutions.getTransfer(getGamma(upperSteps)); let t2 = null;
let upperTime = upperTransfer.secondManoeuvre.time;
let foundWindow = false; let counter = 0;
// Find window that contains transfer
for (let windowAttempts = 0; windowAttempts < 100; windowAttempts++) { while (Math.abs(f2) > 0.5 && counter < 100) {
if (upperTime < 0 || expectedTime < upperTime) { counter++;
foundWindow = true; let divisor = 1;
let step = f1 * (x1 - x0) / (f1 - f0);
let manoeuvreTime = -1;
let x2 = 0;
let innerCounter = 0;
while ((isNaN(manoeuvreTime) || manoeuvreTime < 0) && innerCounter < 10) {
innerCounter++;
x2 = x1 - step / (2**divisor);
t2 = lambertSolutions.getTransfer(x2);
manoeuvreTime = t2.secondManoeuvre.time;
divisor += 1;
}
if (innerCounter == 10) {
break; break;
} }
lowerSteps = upperSteps; f2 = manoeuvreTime - expectedTime;
lowerTransfer = upperTransfer;
lowerTime = upperTime;
upperSteps += 1; x0 = x1;
upperTransfer = lambertSolutions.getTransfer(getGamma(upperSteps)); x1 = x2;
upperTime = upperTransfer.secondManoeuvre.time; f0 = f1;
f1 = f2;
} }
if (!foundWindow) { if (Math.abs(f2) <= 0.5) {
return t2;
} else {
return null; return null;
} }
const acceptablyClose = 0.5;
const numberOfAttempts = 200;
for (let attempts = 0; attempts < numberOfAttempts; attempts++) {
if (Math.abs(lowerTime - expectedTime) < acceptablyClose) {
return lowerTransfer;
}
if (Math.abs(upperTime - expectedTime) < acceptablyClose) {
return upperTransfer;
}
let middleSteps = (upperSteps + lowerSteps) / 2;
let middleTransfer = lambertSolutions.getTransfer(getGamma(middleSteps));
let middleTime = middleTransfer.secondManoeuvre.time;
if (middleTime > 0 && middleTime < expectedTime) {
lowerSteps = middleSteps;
lowerTransfer = middleTransfer;
lowerTime = middleTime;
} else {
upperSteps = middleSteps;
upperTransfer = middleTransfer;
upperTime = middleTime;
}
}
return null;
} }
export function findCheapestIntercept(startingSituation: OrbitalCoordinates, targetSituation: OrbitalCoordinates, body: Body, extraTrueAnomaly: number, progressCallback?: ProgressCallbackFunction): Transfer | null { export function findCheapestIntercept(startingSituation: OrbitalCoordinates, targetSituation: OrbitalCoordinates, body: Body, extraTrueAnomaly: number, progressCallback?: ProgressCallbackFunction): Transfer | null {
@ -823,12 +849,14 @@ export function findCheapestIntercept(startingSituation: OrbitalCoordinates, tar
if (!startingOrbitStable) { if (!startingOrbitStable) {
maxStartTrueAnomaly = Math.abs(Math.acos((startingSituation.orbit.semiLatusRectum - body.sphereOfInfluence) / (body.sphereOfInfluence * startingSituation.orbit.eccentricity))); maxStartTrueAnomaly = Math.abs(Math.acos((startingSituation.orbit.semiLatusRectum - body.sphereOfInfluence) / (body.sphereOfInfluence * startingSituation.orbit.eccentricity)));
maxStartTime = getTimeBetweenTrueAnomalies(startingSituation.trueAnomaly, maxStartTrueAnomaly, startingSituation.orbit, body); maxStartTime = getTimeBetweenTrueAnomalies(startingSituation.trueAnomaly, maxStartTrueAnomaly, startingSituation.orbit, body);
maxEndTime = 2*maxStartTime;
} }
if (!interceptOrbitStable) { if (!interceptOrbitStable) {
maxEndTrueAnomaly = Math.abs(Math.acos((targetSituation.orbit.semiLatusRectum - body.sphereOfInfluence) / (body.sphereOfInfluence * targetSituation.orbit.eccentricity))); maxEndTrueAnomaly = Math.abs(Math.acos((targetSituation.orbit.semiLatusRectum - body.sphereOfInfluence) / (body.sphereOfInfluence * targetSituation.orbit.eccentricity)));
maxEndTime = getTimeBetweenTrueAnomalies(targetSituation.trueAnomaly, maxEndTrueAnomaly, targetSituation.orbit, body); maxEndTime = getTimeBetweenTrueAnomalies(targetSituation.trueAnomaly, maxEndTrueAnomaly, targetSituation.orbit, body);
} else {
// Set the max end time to the max start time plus two full orbits of the target
maxEndTime = maxStartTime + 4 * Math.PI * Math.sqrt((targetSituation.orbit.semiLatusRectum / (1 - targetSituation.orbit.eccentricity**2))**3 / body.gravitationalParameter);
} }
} }
@ -899,4 +927,487 @@ export function findCheapestIntercept(startingSituation: OrbitalCoordinates, tar
}); });
return bestTransfer; return bestTransfer;
}
export function findCheapestLanding(startingCoordinates: OrbitalCoordinates, startTime: number, ship: ShipParameters, landingParameters: LandingParameters, body: Body, progressCallback?: LandingProgressCallbackFunction): Landing | null {
// Starting points are a list of true anomalies with corresponding times
let startingPoints: [number, number][] = [];
let minimumStartingTrueAnomaly = startingCoordinates.trueAnomaly;
let maximumStartingTrueAnomaly = startingCoordinates.trueAnomaly + 2 * Math.PI;
let boundedAbove = false;
// Check if we're on a collision course with the planet
let minimumAltitude = startingCoordinates.orbit.semiLatusRectum / (1 + startingCoordinates.orbit.eccentricity);
// If we're colliding, we need to do our manoeuvre quickly
if (minimumAltitude < body.closestSafeDistance) {
let criticalAnomaly = Math.acos((startingCoordinates.orbit.semiLatusRectum - body.closestSafeDistance) / (body.closestSafeDistance * startingCoordinates.orbit.eccentricity));
// We might be in a weird orbit where we're on the way away from the planet, in which case we shouldn't bound our starting
if (minimumStartingTrueAnomaly < -criticalAnomaly) {
boundedAbove = true;
maximumStartingTrueAnomaly = -criticalAnomaly;
}
}
// If we're on a parabolic or hyperbolic orbit, or one that will leave the planet's sphere of influence, set an upper bound for the anomaly
if (!boundedAbove) {
let goesOutsideSphereOfIncluence = false;
if (startingCoordinates.orbit.eccentricity < 1) {
let maximumAltitude = startingCoordinates.orbit.semiLatusRectum / (1 - startingCoordinates.orbit.eccentricity);
if (maximumAltitude > body.sphereOfInfluence) {
goesOutsideSphereOfIncluence = true;
}
} else {
goesOutsideSphereOfIncluence = true;
}
if (goesOutsideSphereOfIncluence) {
maximumStartingTrueAnomaly = Math.acos((startingCoordinates.orbit.semiLatusRectum - body.sphereOfInfluence) / (body.sphereOfInfluence * startingCoordinates.orbit.eccentricity));
}
}
// Our set of starting points is decided
let stepLength = (maximumStartingTrueAnomaly - minimumStartingTrueAnomaly) / 100.0;
for (let i = 0; i < 100; i++) {
let testAnomaly = minimumStartingTrueAnomaly + i * stepLength;
let timeUntilAnomaly = getTimeBetweenTrueAnomalies(minimumStartingTrueAnomaly, testAnomaly, startingCoordinates.orbit, body);
startingPoints.push([testAnomaly, timeUntilAnomaly + startTime]);
};
let bestLanding: Landing | null = null;
let bestLandingDeltaV: number | null = null;
// We need to simulate the ship braking, in order to refine the manoeuvre
const simulateBraking = (transferTrueAnomaly: number, transferOrbit: Orbit, body: Body, ship: ShipParameters) => {
let localVectors = getLocalVectors(transferTrueAnomaly, transferOrbit);
let speed = getSpeed(transferTrueAnomaly, transferOrbit, body);
let velocity = multiplyMatrixWithScalar(speed, localVectors.prograde);
let radius = transferOrbit.semiLatusRectum / (1 + transferOrbit.eccentricity * Math.cos(transferTrueAnomaly));
let localX = radius * Math.cos(transferTrueAnomaly);
let localY = radius * Math.sin(transferTrueAnomaly);
let startingPosition = addVector(
multiplyMatrixWithScalar(localX, transferOrbit.coordinateAxes[0]),
multiplyMatrixWithScalar(localY, transferOrbit.coordinateAxes[1])
);
let position = startingPosition;
let mass = ship.mass;
let massUse = ship.thrust / ship.specificImpulse;
let timeSteps = 0;
let lengthOfStep = 0.01;
let totalDeltaV = 0;
while (true) {
let radius = getVectorMagnitude(position);
let downwardsVector = multiplyMatrixWithScalar(-1 / radius, position);
let gravityAcceleration = body.gravitationalParameter / radius**2;
let gravityVector = multiplyMatrixWithScalar(gravityAcceleration, downwardsVector);
let latitude = Math.atan2(position[2][0], Math.sqrt(position[0][0]**2 + position[1][0]**2));
let longitude = Math.atan2(position[1][0], position[0][0]);
let surfaceSpeed = body.radius * Math.cos(latitude) * 2 * Math.PI / body.rotationPeriod;
let surfaceVelocity = [
[-surfaceSpeed * Math.sin(longitude)],
[surfaceSpeed * Math.cos(longitude)],
[0]
];
let excessVelocity = subtractVector(velocity, surfaceVelocity);
let excessSpeed = getVectorMagnitude(excessVelocity);
let engineAcceleration = ship.thrust / mass;
if (excessSpeed < lengthOfStep * engineAcceleration) {
break;
}
let engineAccelerationVector = multiplyMatrixWithScalar(-engineAcceleration, normalizeVector(excessVelocity));
let totalAcceleration = addVector(engineAccelerationVector, gravityVector);
position = addVector(position, addVector(multiplyMatrixWithScalar(lengthOfStep, velocity), multiplyMatrixWithScalar(lengthOfStep**2 / 2, totalAcceleration)));
velocity = addVector(velocity, multiplyMatrixWithScalar(lengthOfStep, totalAcceleration));
mass -= massUse * lengthOfStep;
totalDeltaV += engineAcceleration * lengthOfStep;
if (mass < 0) {
return null;
}
timeSteps += 1;
}
return {
timeSpent: timeSteps * lengthOfStep,
finalPosition: position,
deltaVSpent: totalDeltaV
};
};
const createFakeOrbit = (position: number[][]): Orbit => {
let radius = getVectorMagnitude(position);
let longitude = Math.atan2(position[1][0], position[0][0]);
let latitude = Math.atan2(position[2][0], Math.sqrt(position[0][0]**2 + position[1][0]**2));
if (latitude > 0) {
return getOrbit(radius, radius, latitude, longitude - Math.PI / 2, Math.PI / 2);
} else {
return getOrbit(radius, radius, -latitude, longitude + Math.PI / 2, -Math.PI / 2);
}
}
let freefallMultiplier = Math.PI / (2 * Math.sqrt(2 * body.gravitationalParameter));
startingPoints.forEach(([trueAnomaly, startTime], startingIndex) => {
// Test 100 different landing times at each true anomaly. We'll set the upper bound for the transfer time to be one and a
// half the time it takes to free fall from the current position
let currentRadius = startingCoordinates.orbit.semiLatusRectum / (1 + startingCoordinates.orbit.eccentricity * Math.cos(trueAnomaly));
let freeFallTime = freefallMultiplier * currentRadius ** (3 / 2);
for (let i = 1; i < 101; i++) {
let targetTime = 1.5 * freeFallTime * i / 100;
let rotatedTargetLongitude = landingParameters.targetLongitude + body.initialMeridianLongitude + (targetTime + startTime) * 2 * Math.PI / body.rotationPeriod;
let targetX = (landingParameters.targetAltitude + body.radius) * Math.cos(landingParameters.targetLatitude) * Math.cos(rotatedTargetLongitude);
let targetY = (landingParameters.targetAltitude + body.radius) * Math.cos(landingParameters.targetLatitude) * Math.sin(rotatedTargetLongitude);
let targetZ = (landingParameters.targetAltitude + body.radius) * Math.sin(landingParameters.targetLatitude);
let target = [[targetX], [targetY], [targetZ]];
// Make up an orbit that goes through this point, so we can use it with the Lambert solver I made earlier that always
// assumes you want to go between two orbits
let fakeTargetOrbit = createFakeOrbit(target);
[true, false].forEach(backwards => {
let lambertSolutions = new LambertSolutions(startingCoordinates.orbit, trueAnomaly, fakeTargetOrbit, 0, body, backwards);
let possibleTransfer = findLambertSolutionsWithCorrectTime(lambertSolutions, targetTime);
if (possibleTransfer) {
// Throw out any transfers that go below the surface and come up on our rendezvous
if (possibleTransfer.closestPointDistance < landingParameters.targetAltitude + body.radius - 1) {
return;
}
// Check if the transfer comes in steep enough
let localVectors = getLocalVectors(possibleTransfer.transferOrbitTrueanomalyAtManoeuvreTwo, possibleTransfer.transferOrbit);
let targetDownwardsVector = multiplyMatrixWithScalar(-1, normalizeVector(target));
let maximumAngle = Math.PI / 2 - landingParameters.minimumAngle;
let angle = Math.acos(vectorDotProduct(localVectors.prograde, targetDownwardsVector));
if (angle > maximumAngle) {
return;
}
// Do some iterative searching to find a transfer that lands in the middle
let foundGoodLanding = false;
let previousTargetPosition = target;
let previousTargetTime = targetTime;
let landingCounter = 0;
while (!foundGoodLanding && landingCounter < 100) {
landingCounter++;
let massAfterTransfer = ship.mass / (Math.exp(possibleTransfer.firstManoeuvre.totalDeltaV / (9.81 * ship.specificImpulse)));
let testShip: ShipParameters = {
thrust: ship.thrust,
mass: massAfterTransfer,
specificImpulse: ship.specificImpulse
};
let simulationResult = simulateBraking(possibleTransfer.transferOrbitTrueanomalyAtManoeuvreTwo, possibleTransfer.transferOrbit, body, testShip);
if (simulationResult == null) {
return;
}
let difference = subtractVector(simulationResult.finalPosition, target);
let timeDifference = possibleTransfer.secondManoeuvre.time + simulationResult.timeSpent - targetTime;
let differenceMagnitude = getVectorMagnitude(difference);
if (timeDifference > 100 || differenceMagnitude > 10) {
// We're too far away, try another maneuver
let newTarget = subtractVector(previousTargetPosition, difference);
let newTime = previousTargetTime - timeDifference;
let newTargetOrbit = createFakeOrbit(newTarget);
previousTargetPosition = newTarget;
previousTargetTime = newTime;
lambertSolutions = new LambertSolutions(startingCoordinates.orbit, trueAnomaly, newTargetOrbit, 0, body, backwards);
possibleTransfer = findLambertSolutionsWithCorrectTime(lambertSolutions, newTime);
if (!possibleTransfer) {
return;
}
} else {
foundGoodLanding = true;
possibleTransfer.firstManoeuvre.time += startTime;
possibleTransfer.secondManoeuvre.time += startTime;
let totalDeltaV = possibleTransfer.firstManoeuvre.totalDeltaV + simulationResult.deltaVSpent;
if (bestLandingDeltaV == null || totalDeltaV < bestLandingDeltaV) {
let secondManoeuvreAltitude = possibleTransfer.transferOrbit.semiLatusRectum / (1 + possibleTransfer.transferOrbit.eccentricity * Math.cos(possibleTransfer.transferOrbitTrueanomalyAtManoeuvreTwo)) - body.radius;
let landing: Landing = {
transferOrbit: possibleTransfer.transferOrbit,
transferManoeuvre: possibleTransfer.firstManoeuvre,
brakingDeltaV: simulationResult.deltaVSpent,
brakingAltitude: secondManoeuvreAltitude,
brakingTimestamp: possibleTransfer.secondManoeuvre.time,
totalDeltaV: totalDeltaV
};
bestLanding = landing;
bestLandingDeltaV = totalDeltaV;
}
}
};
}
});
if (progressCallback) {
progressCallback(startingIndex * 100 + i, 100*100, bestLandingDeltaV, bestLanding);
}
}
});
return bestLanding;
}
export function findOrbitThroughInterpolation(ownCoordinates: OrbitalCoordinates, interpolationParameters: InterpolationParameters, body: Body): [OrbitalCoordinates, number] | null {
let targetPeriapsis = interpolationParameters.targetPeriapsis;
let targetEccentricity;
let targetSemiLatusRectum;
if (interpolationParameters.orbitChoice == "apoapsis") {
let targetApoapsis = interpolationParameters.targetApoapsis;
const semiMajor = (targetPeriapsis + targetApoapsis) / 2;
const linearEccentricity = semiMajor - targetPeriapsis;
targetEccentricity = linearEccentricity / semiMajor;
targetSemiLatusRectum = semiMajor * (1 - targetEccentricity**2);
} else {
let semiMajorInverse = 2 / interpolationParameters.targetAltitude - interpolationParameters.targetSpeed**2 / body.gravitationalParameter;
// If this is zero, we have a parabolic trajectory
if (Math.abs(semiMajorInverse) < 0.0001) {
targetEccentricity = 1;
targetSemiLatusRectum = 2 * targetPeriapsis;
} else {
let semiMajor = 1 / semiMajorInverse;
// If this is positive, we have an elliptical orbit
if (semiMajor > 0) {
const linearEccentricity = semiMajor - targetPeriapsis;
targetEccentricity = linearEccentricity / semiMajor;
targetSemiLatusRectum = semiMajor * (1 - targetEccentricity**2);
} else {
const linearEccentricity = semiMajor + targetPeriapsis;
targetEccentricity = linearEccentricity / semiMajor;
targetSemiLatusRectum = semiMajor * (targetEccentricity**2 - 1);
}
}
}
// Now, we have a pretty good description of the orbit, we only need to find the plane it lies in
// Given the first measurement of distance to planet, distance to target, and target to planet, there exists a circle
// that the target could be in
let ownShipHeadedInwards = interpolationParameters.secondOwnAltitude < interpolationParameters.firstOwnAltitude;
let targetHeadedInwards = interpolationParameters.secondTargetAltitude < interpolationParameters.firstTargetAltitude;
// Define some helper functions for later
const getRandomPerpendicularVector = (vector: number[][]) => {
let perpendicularX = Math.random();
let perpendicularY = Math.random();
let perpendicularZ = Math.random();
let normalVector: number[][];
if (Math.abs(vector[0][0]) > 0.0001) {
normalVector = [
[(-vector[1][0] * perpendicularY - vector[2][0]*perpendicularZ) / vector[0][0]],
[perpendicularY],
[perpendicularZ]
];
} else if (Math.abs(vector[1][0]) > 0.0001) {
normalVector = [
[perpendicularX],
[(-vector[0][0] / perpendicularX - vector[2][0] / perpendicularZ) / vector[1][0]],
[perpendicularZ]
];
} else {
normalVector = [
[perpendicularX],
[perpendicularY],
[(-vector[0][0] * perpendicularX - vector[1][0] * perpendicularY) / vector[2][0]]
]
};
return normalizeVector(normalVector);
};
const getTrueAnomalyFromAltitude = (altitude: number, semiLatusRectum: number, eccentricity: number, headingInwards: boolean) => {
let trueAnomaly = Math.acos((semiLatusRectum - altitude) / (altitude * eccentricity));
if (headingInwards) {
trueAnomaly = -trueAnomaly;
}
return trueAnomaly;
}
const getPossibleTargetLocationFunction = (ownOrbit: Orbit, ownAltitude: number, targetAltitude: number, distanceToTarget: number): ((angle: number) => number[][]) => {
let ownTrueAnomaly = getTrueAnomalyFromAltitude(ownAltitude, ownOrbit.semiLatusRectum, ownOrbit.eccentricity, ownShipHeadedInwards);
let ownLocalX = interpolationParameters.firstOwnAltitude * Math.cos(ownTrueAnomaly);
let ownLocalY = interpolationParameters.firstOwnAltitude * Math.sin(ownTrueAnomaly);
let ownDirection = normalizeVector(
addVector(
multiplyMatrixWithScalar(ownLocalX, ownOrbit.coordinateAxes[0]),
multiplyMatrixWithScalar(ownLocalY, ownOrbit.coordinateAxes[1])
)
);
let perpendicularOne = getRandomPerpendicularVector(ownDirection);
let perpendicularTwo = normalizeVector(vectorCrossProduct(ownDirection, perpendicularOne));
// Find phase angle to target
let phaseAngle = Math.acos((ownAltitude**2 + targetAltitude**2 - distanceToTarget**2) / (2 * ownAltitude * targetAltitude));
let distanceAlongDirection = targetAltitude * Math.cos(phaseAngle);
let distanceAlongNormal = targetAltitude * Math.sin(phaseAngle);
let root = multiplyMatrixWithScalar(distanceAlongDirection, ownDirection);
return function(angle: number) {
let addition = addVector(
multiplyMatrixWithScalar(distanceAlongNormal * Math.cos(angle), perpendicularOne),
multiplyMatrixWithScalar(distanceAlongNormal * Math.sin(angle), perpendicularTwo)
);
return addVector(root, addition);
}
}
let firstTargetFunction = getPossibleTargetLocationFunction(ownCoordinates.orbit, interpolationParameters.firstOwnAltitude, interpolationParameters.firstTargetAltitude, interpolationParameters.firstDistance);
let secondTargetFunction = getPossibleTargetLocationFunction(ownCoordinates.orbit, interpolationParameters.secondOwnAltitude, interpolationParameters.secondTargetAltitude, interpolationParameters.secondDistance);
// Since we know a lot about the target orbit, we can figure out how far it is supposed to be between the two points
let firstTargetTrueAnomaly = getTrueAnomalyFromAltitude(interpolationParameters.firstTargetAltitude, targetSemiLatusRectum, targetEccentricity, targetHeadedInwards);
let secondTargetTrueAnomaly = getTrueAnomalyFromAltitude(interpolationParameters.secondTargetAltitude, targetSemiLatusRectum, targetEccentricity, targetHeadedInwards);
// Use cosine rule
let targetMovement = Math.sqrt(interpolationParameters.firstTargetAltitude**2 + interpolationParameters.secondTargetAltitude**2 - 2 * interpolationParameters.firstTargetAltitude * interpolationParameters.secondTargetAltitude * Math.cos(secondTargetTrueAnomaly - firstTargetTrueAnomaly));
// Now, we need to do multivariate optimization to find the two angles to give to the functions that make the distance between
// two target positions as equal to the target movement as possible
const functionToOptimize = (angleOne: number, angleTwo: number) => {
let firstTargetPosition = firstTargetFunction(angleOne);
let secondTargetPosition = secondTargetFunction(angleTwo);
return Math.abs(getVectorMagnitude(subtractVector(secondTargetPosition, firstTargetPosition)) - targetMovement);
}
let bestAngleOne = 0;
let bestAngleTwo = 0;
let bestValue = 1e99;
for (var i = 0; i < 100; i++) {
for (var j = 0; j < 100; j++) {
let angleOne = 2 * Math.PI * i / 100;
let angleTwo = 2 * Math.PI * j / 100;
let value = functionToOptimize(angleOne, angleTwo);
if (value < bestValue) {
bestAngleOne = angleOne;
bestAngleTwo = angleTwo;
bestValue = value;
}
}
}
// Do some gradient descent on the best values found so far
while (bestValue > 0.5) {
let estimateDfDx = (functionToOptimize(bestAngleOne+0.0000001, bestAngleTwo) - bestValue) / 0.0000001;
let estimateDfDy = (functionToOptimize(bestAngleOne, bestAngleTwo+0.0000001) - bestValue) / 0.0000001;
let slopeNormal = vectorCrossProduct(
[[1], [0], [estimateDfDx]],
[[0], [1], [estimateDfDy]]
);
let direction = normalizeVector([[slopeNormal[0][0]], [slopeNormal[1][0]], [0]]);
let estimateDfDv = (functionToOptimize(bestAngleOne + direction[0][0]*0.0001, bestAngleTwo + direction[1][0]*0.0001) - bestValue) / 0.0001;
let bestValueImprovement = bestValue;
let bestAngleOneUpdate = bestAngleOne;
let bestAngleTwoUpdate = bestAngleTwo;
let divisor = 0;
let deadEnd = false;
while (bestValueImprovement >= bestValue) {
bestAngleOneUpdate = bestAngleOne - bestValue * direction[0][0] / (estimateDfDv * 2**divisor);
bestAngleTwoUpdate = bestAngleTwo - bestValue * direction[1][0] / (estimateDfDv * 2**divisor);
bestValueImprovement = functionToOptimize(bestAngleOneUpdate, bestAngleTwoUpdate);
divisor += 1;
if (divisor >= 32) {
deadEnd = true;
break;
}
}
if (deadEnd) {
break;
}
bestAngleOne = bestAngleOneUpdate;
bestAngleTwo = bestAngleTwoUpdate;
bestValue = bestValueImprovement;
}
let bestFirstPosition = firstTargetFunction(bestAngleOne);
let bestSecondPosition = secondTargetFunction(bestAngleTwo);
if (bestFirstPosition == null || bestSecondPosition == null) {
return null;
}
// We can now find the vector that defines the plane the target is in
let normalVector = normalizeVector(vectorCrossProduct(bestFirstPosition, bestSecondPosition));
let firstTargetPositionDirection = normalizeVector(bestFirstPosition);
// Rotate the position vector about the normal vector by the true anomaly to find the vector pointing to the periapsis
let identityMatrix = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
let crossProductMatrix = [
[0, -normalVector[2][0], normalVector[1][0]],
[normalVector[2][0], 0, -normalVector[0][0]],
[-normalVector[1][0], normalVector[0][0], 0]
];
let outerProductMatrix = [
[normalVector[0][0]**2, normalVector[0][0]*normalVector[1][0], normalVector[0][0]*normalVector[2][0]],
[normalVector[0][0]*normalVector[1][0], normalVector[1][0]**2, normalVector[1][0]*normalVector[2][0]],
[normalVector[0][0]*normalVector[2][0], normalVector[1][0]*normalVector[2][0], normalVector[2][0]**2]
];
let rotationMatrix = addMatrix(
multiplyMatrixWithScalar(Math.cos(-firstTargetTrueAnomaly), identityMatrix),
addMatrix(
multiplyMatrixWithScalar(Math.sin(-firstTargetTrueAnomaly), crossProductMatrix),
multiplyMatrixWithScalar(1 - Math.cos(-firstTargetTrueAnomaly), outerProductMatrix)
)
);
let targetOrbitXVector = matrixMultiply(rotationMatrix, firstTargetPositionDirection);
let targetOrbitYVector = vectorCrossProduct(normalVector, targetOrbitXVector);
let targetOrbit: Orbit = {
semiLatusRectum: targetSemiLatusRectum,
eccentricity: targetEccentricity,
coordinateAxes: [targetOrbitXVector, targetOrbitYVector, normalVector]
};
let timePassed = getTimeBetweenTrueAnomalies(ownCoordinates.trueAnomaly, getTrueAnomalyFromAltitude(interpolationParameters.secondOwnAltitude, ownCoordinates.orbit.semiLatusRectum, ownCoordinates.orbit.eccentricity, ownShipHeadedInwards), ownCoordinates.orbit, body);
return [getOrbitalCoordinatesFromAltitude(interpolationParameters.secondTargetAltitude, targetHeadedInwards, targetOrbit, body), timePassed];
} }

View File

@ -1,5 +1,5 @@
import type { Body } from "../calculations/constants"; import type { Body } from "../calculations/constants";
import { getOrbit, getOrbitalCoordinates, getOrbitalCoordinatesFromAltitude, getOrbitFromEccentricity, type Orbit, type OrbitalCoordinates } from "../calculations/orbit-calculations"; import { getOrbit, getOrbitalCoordinates, getOrbitalCoordinatesFromAltitude, getOrbitFromEccentricity, type LandingParameters, type Orbit, type OrbitalCoordinates } from "../calculations/orbit-calculations";
export interface OrbitalParameters { export interface OrbitalParameters {
periapsis: number; periapsis: number;
@ -33,20 +33,26 @@ export const DefaultOrbitalParameters: OrbitalParameters = {
currentTimeAtReading: 0 currentTimeAtReading: 0
} }
export function encodeOrbitalParameters(orbitalParameters: OrbitalParameters): string { export const DefaultLandingParameters: LandingParameters = {
return JSON.stringify(orbitalParameters); targetLatitude: 0,
targetLongitude: 0,
targetAltitude: 0,
minimumAngle: Math.PI / 4
};
export function encodeJsonObject<Type>(object: Type): string {
return JSON.stringify(object);
} }
export function decodeOrbitalParameters(jsonString: string): OrbitalParameters { export function getDecoderFunction<Type>(defaultValue: Type): (jsonString: string) => Type {
let parsedObject = JSON.parse(jsonString); return function(jsonString: string): Type {
var orbitalParameters: OrbitalParameters; let parsedObject = JSON.parse(jsonString);
if (parsedObject as OrbitalParameters === undefined) { if (parsedObject as Type === undefined) {
orbitalParameters = DefaultOrbitalParameters; return defaultValue;
} else { } else {
orbitalParameters = parsedObject; return parsedObject;
}
} }
return orbitalParameters;
} }
export function createLabel(forId: string, labelText: string): HTMLLabelElement { export function createLabel(forId: string, labelText: string): HTMLLabelElement {

View File

@ -1,10 +1,20 @@
import { createLabel, createNumberInput, getCoordinatesFromParameters, type OrbitalParameters } from "./common"; import { createLabel, createNumberInput, createRadioButton, getCoordinatesFromParameters, type OrbitalParameters } from "./common";
import { OrbitalParametersGui } from "./orbit"; import { OrbitalParametersGui } from "./orbit";
import { type Body } from "../calculations/constants"; import { type Body } from "../calculations/constants";
import type { FindBestInterceptMessage, ProgressMessage } from "./worker"; import type { FindBestInterceptMessage, ProgressMessage } from "./worker";
import type { ChangingStorageValue } from "../storage"; import type { ChangingStorageValue } from "../storage";
import { ManoeuvresGui } from "./manoeuvres"; import { ManoeuvresGui } from "./manoeuvres";
import { extrapolateTrajectory, type OrbitalCoordinates } from "../calculations/orbit-calculations"; import { extrapolateTrajectory, findOrbitThroughInterpolation, type OrbitalCoordinates } from "../calculations/orbit-calculations";
import { InterpolateOrbitGui, type InterpolationParameters } from "./interpolate";
export type TargetOrbitChoice = "KnownOrbit" | "InterpolateOrbit";
export function decodeTargetOrbitChoice(s: string): TargetOrbitChoice {
if (s == "KnownOrbit" || s == "InterpolateOrbit") {
return s;
}
return "KnownOrbit";
}
export class InterceptTargetGui { export class InterceptTargetGui {
currentTime: ChangingStorageValue<number>; currentTime: ChangingStorageValue<number>;
@ -12,33 +22,72 @@ export class InterceptTargetGui {
startingOrbitalParameters: ChangingStorageValue<OrbitalParameters>; startingOrbitalParameters: ChangingStorageValue<OrbitalParameters>;
targetOrbitalParameters: ChangingStorageValue<OrbitalParameters>; targetOrbitalParameters: ChangingStorageValue<OrbitalParameters>;
additionalTrueAnomaly: ChangingStorageValue<number>; additionalTrueAnomaly: ChangingStorageValue<number>;
targetOrbitChoice: ChangingStorageValue<TargetOrbitChoice>;
interpolationParameters: ChangingStorageValue<InterpolationParameters>;
parentDiv: HTMLDivElement; parentDiv: HTMLDivElement;
orbitGui: OrbitalParametersGui; orbitGui: OrbitalParametersGui;
interpolateGui: InterpolateOrbitGui;
manoeuvresGui: ManoeuvresGui; manoeuvresGui: ManoeuvresGui;
sourceId: string; sourceId: string;
worker: Worker | null; worker: Worker | null;
constructor(currentTime: ChangingStorageValue<number>, body: ChangingStorageValue<Body>, startingOrbitalParameters: ChangingStorageValue<OrbitalParameters>, targetOrbitalParameters: ChangingStorageValue<OrbitalParameters>, additionalTrueAnomaly: ChangingStorageValue<number>) { constructor(currentTime: ChangingStorageValue<number>, body: ChangingStorageValue<Body>, startingOrbitalParameters: ChangingStorageValue<OrbitalParameters>, targetOrbitalParameters: ChangingStorageValue<OrbitalParameters>, additionalTrueAnomaly: ChangingStorageValue<number>, targetOrbitChoice: ChangingStorageValue<TargetOrbitChoice>, interpolationParameters: ChangingStorageValue<InterpolationParameters>) {
this.currentTime = currentTime; this.currentTime = currentTime;
this.body = body; this.body = body;
this.startingOrbitalParameters = startingOrbitalParameters; this.startingOrbitalParameters = startingOrbitalParameters;
this.targetOrbitalParameters = targetOrbitalParameters; this.targetOrbitalParameters = targetOrbitalParameters;
this.additionalTrueAnomaly = additionalTrueAnomaly; this.additionalTrueAnomaly = additionalTrueAnomaly;
this.targetOrbitChoice = targetOrbitChoice;
this.interpolationParameters = interpolationParameters;
this.orbitGui = new OrbitalParametersGui(targetOrbitalParameters, "orbitWithPosition"); this.orbitGui = new OrbitalParametersGui(targetOrbitalParameters, "orbitWithPosition");
this.interpolateGui = new InterpolateOrbitGui(this.interpolationParameters);
this.manoeuvresGui = new ManoeuvresGui(true); this.manoeuvresGui = new ManoeuvresGui(true);
this.worker = null; this.worker = null;
this.parentDiv = document.createElement("div"); this.parentDiv = document.createElement("div");
let choiceHeader = document.createElement("h3")
choiceHeader.appendChild(document.createTextNode("Choose target orbit type"));
this.parentDiv.appendChild(choiceHeader);
let targetChoiceId = crypto.randomUUID();
let knownOrbitId = crypto.randomUUID();
let knownOrbitButton = createRadioButton(targetChoiceId, knownOrbitId);
let knownOrbitLabel = createLabel(knownOrbitId, "Known orbit");
this.parentDiv.appendChild(knownOrbitButton);
this.parentDiv.appendChild(knownOrbitLabel);
let interpolateOrbitId = crypto.randomUUID();
let interpolateOrbitButton = createRadioButton(targetChoiceId, interpolateOrbitId);
let interpolateOrbitLabel = createLabel(interpolateOrbitId, "Interpolate orbit");
this.parentDiv.appendChild(interpolateOrbitButton);
this.parentDiv.appendChild(interpolateOrbitLabel);
let orbitContainer = document.createElement("div");
this.parentDiv.appendChild(orbitContainer);
let knownOrbitContainer = document.createElement("div");
let parametersHeader = document.createElement("h3"); let parametersHeader = document.createElement("h3");
parametersHeader.appendChild(document.createTextNode("Target orbit:")); parametersHeader.appendChild(document.createTextNode("Target orbit:"));
this.parentDiv.appendChild(parametersHeader); knownOrbitContainer.appendChild(parametersHeader);
knownOrbitContainer.appendChild(this.orbitGui.parentDiv);
this.parentDiv.appendChild(this.orbitGui.parentDiv); let interpolateOrbitContainer = document.createElement("div");
let interpolateOrbitHeader = document.createElement("h3");
interpolateOrbitHeader.appendChild(document.createTextNode("Interpolate orbit:"));
interpolateOrbitContainer.appendChild(interpolateOrbitHeader);
interpolateOrbitContainer.appendChild(this.interpolateGui.parentDiv);
let offsetHeader = document.createElement("h3");
offsetHeader.appendChild(document.createTextNode("Offset:"));
this.parentDiv.appendChild(offsetHeader);
let additionalTrueAnomalyContainer = document.createElement("div"); let additionalTrueAnomalyContainer = document.createElement("div");
additionalTrueAnomalyContainer.classList.add("orbitalParameter"); additionalTrueAnomalyContainer.classList.add("orbitalParameter");
@ -49,7 +98,6 @@ export class InterceptTargetGui {
let additionalTrueAnomalyInput = createNumberInput(additionalTrueAnomalyId, -360, 360); let additionalTrueAnomalyInput = createNumberInput(additionalTrueAnomalyId, -360, 360);
[additionalTrueanomalyLabel, additionalTrueAnomalyInput].forEach(child => additionalTrueAnomalyContainer.appendChild(child)); [additionalTrueanomalyLabel, additionalTrueAnomalyInput].forEach(child => additionalTrueAnomalyContainer.appendChild(child));
let searchButton = document.createElement("button"); let searchButton = document.createElement("button");
searchButton.appendChild(document.createTextNode("Search for cheapest intercept")); searchButton.appendChild(document.createTextNode("Search for cheapest intercept"));
this.parentDiv.appendChild(searchButton); this.parentDiv.appendChild(searchButton);
@ -72,25 +120,52 @@ export class InterceptTargetGui {
let body = this.body.getCurrentValue(); let body = this.body.getCurrentValue();
let startingOrbitalParameters = this.startingOrbitalParameters.getCurrentValue(); let startingOrbitalParameters = this.startingOrbitalParameters.getCurrentValue();
let targetOrbitalParameters = this.targetOrbitalParameters.getCurrentValue(); let targetOrbitalParameters = this.targetOrbitalParameters.getCurrentValue();
let additionalTrueAnomaly = this.additionalTrueAnomaly.getCurrentValue();
let targetOrbitChoice = this.targetOrbitChoice.getCurrentValue();
let interpolationParameters = this.interpolationParameters.getCurrentValue();
interpolationParameters = structuredClone(interpolationParameters);
interpolationParameters.firstOwnAltitude += body.radius;
interpolationParameters.firstTargetAltitude += body.radius;
interpolationParameters.secondOwnAltitude += body.radius;
interpolationParameters.secondTargetAltitude += body.radius;
interpolationParameters.targetAltitude += body.radius;
interpolationParameters.targetPeriapsis += body.radius;
interpolationParameters.targetApoapsis += body.radius;
let startingCoordinates: OrbitalCoordinates | null = getCoordinatesFromParameters(startingOrbitalParameters, body); let startingCoordinates: OrbitalCoordinates | null = getCoordinatesFromParameters(startingOrbitalParameters, body);
let targetCoordinates: OrbitalCoordinates | null = getCoordinatesFromParameters(targetOrbitalParameters, body); let startTime: number | null = null;
let startTime = Math.max(currentTime, startingOrbitalParameters.currentTimeAtReading, targetOrbitalParameters.currentTimeAtReading); let targetStartTime = 0;
if (startTime > startingOrbitalParameters.currentTimeAtReading) { let targetCoordinates: OrbitalCoordinates | null = null;
let addedTime = startTime - startingOrbitalParameters.currentTimeAtReading; if (targetOrbitChoice == "InterpolateOrbit") {
startingCoordinates = extrapolateTrajectory(addedTime, startingCoordinates, body); let result = findOrbitThroughInterpolation(startingCoordinates, interpolationParameters, body);
if (result) {
targetCoordinates = result[0];
targetStartTime = startingOrbitalParameters.currentTimeAtReading + result[1];
}
} else if (targetOrbitChoice = "KnownOrbit") {
targetCoordinates = getCoordinatesFromParameters(targetOrbitalParameters, body);
targetStartTime = targetOrbitalParameters.currentTimeAtReading;
} }
if (startTime > targetOrbitalParameters.currentTimeAtReading) {
let addedTime = startTime - targetOrbitalParameters.currentTimeAtReading;
targetCoordinates = extrapolateTrajectory(addedTime, targetCoordinates, body);
}
let additionalTrueAnomaly = this.additionalTrueAnomaly.getCurrentValue();
if (startingCoordinates && targetCoordinates) { if (startingCoordinates && targetCoordinates) {
startTime = Math.max(currentTime, startingOrbitalParameters.currentTimeAtReading, targetStartTime);
if (startTime > startingOrbitalParameters.currentTimeAtReading) {
let addedTime = startTime - startingOrbitalParameters.currentTimeAtReading;
startingCoordinates = extrapolateTrajectory(addedTime, startingCoordinates, body);
}
if (startTime > targetStartTime) {
let addedTime = startTime - targetStartTime;
targetCoordinates = extrapolateTrajectory(addedTime, targetCoordinates, body);
}
}
if (startingCoordinates && targetCoordinates && startTime) {
this.worker = new Worker(new URL('./worker.ts', import.meta.url), {type: 'module'}); this.worker = new Worker(new URL('./worker.ts', import.meta.url), {type: 'module'});
this.worker.addEventListener("message", event => { this.worker.addEventListener("message", event => {
let transferResponse = event.data as ProgressMessage; let transferResponse = event.data as ProgressMessage;
@ -101,8 +176,8 @@ export class InterceptTargetGui {
} }
if (transferResponse.bestTransfer) { if (transferResponse.bestTransfer) {
transferResponse.bestTransfer.firstManoeuvre.time += currentTime; transferResponse.bestTransfer.firstManoeuvre.time += startTime;
transferResponse.bestTransfer.secondManoeuvre.time += currentTime; transferResponse.bestTransfer.secondManoeuvre.time += startTime;
} }
this.manoeuvresGui.displayProgress(transferResponse); this.manoeuvresGui.displayProgress(transferResponse);
@ -143,5 +218,19 @@ export class InterceptTargetGui {
let additionalTrueAnomaly = parseFloat(additionalTrueAnomalyInput.value) * Math.PI / 180.0; let additionalTrueAnomaly = parseFloat(additionalTrueAnomalyInput.value) * Math.PI / 180.0;
this.additionalTrueAnomaly.set(additionalTrueAnomaly, this.sourceId); this.additionalTrueAnomaly.set(additionalTrueAnomaly, this.sourceId);
}); });
knownOrbitButton.addEventListener("change", () => targetOrbitChoice.set("KnownOrbit", this.sourceId));
interpolateOrbitButton.addEventListener("change", () => targetOrbitChoice.set("InterpolateOrbit", this.sourceId));
targetOrbitChoice.listenToValue((_, value) => {
orbitContainer.innerHTML = "";
if (value == "KnownOrbit") {
knownOrbitButton.setAttribute("checked", "");
orbitContainer.appendChild(knownOrbitContainer);
} else if (value == "InterpolateOrbit") {
interpolateOrbitButton.setAttribute("checked", "");
orbitContainer.appendChild(interpolateOrbitContainer);
}
});
} }
} }

171
src/gui/interpolate.ts Normal file
View File

@ -0,0 +1,171 @@
import type { ChangingStorageValue } from "../storage";
import { createLabel, createNumberInput, createRadioButton } from "./common";
export interface InterpolationParameters {
targetPeriapsis: number,
orbitChoice: "apoapsis" | "speedAndAltitude",
targetApoapsis: number,
targetSpeed: number,
targetAltitude: number,
firstOwnAltitude: number,
firstTargetAltitude: number,
firstDistance: number,
secondOwnAltitude: number,
secondTargetAltitude: number,
secondDistance: number,
}
export const DefaultInterpolationParameters: InterpolationParameters = {
targetPeriapsis: 100000,
orbitChoice: "apoapsis",
targetApoapsis: 200000,
targetSpeed: 0,
targetAltitude: 0,
firstOwnAltitude: 0,
firstTargetAltitude: 0,
firstDistance: 0,
secondOwnAltitude: 0,
secondTargetAltitude: 0,
secondDistance: 0
}
export class InterpolateOrbitGui {
parentDiv: HTMLDivElement;
interpolationParameters: ChangingStorageValue<InterpolationParameters>
sourceId: string;
constructor(interpolationParameters: ChangingStorageValue<InterpolationParameters>) {
this.parentDiv = document.createElement("div");
this.interpolationParameters = interpolationParameters;
let targetOrbitHeader = document.createElement("h4");
targetOrbitHeader.appendChild(document.createTextNode("Describe target orbit:"));
this.parentDiv.appendChild(targetOrbitHeader);
let addToParent = (elements: HTMLElement[]) => {
let container = document.createElement("div");
container.classList.add("orbitalParameter");
elements.forEach(child => container.appendChild(child));
this.parentDiv.appendChild(container);
}
let createInputWithLabel = (label: string, minimum: number, maximum?: number, size?: number) => {
let inputId = crypto.randomUUID();
let labelElement = createLabel(inputId, label);
let numberInput = createNumberInput(inputId, minimum, maximum, size);
addToParent([labelElement, numberInput]);
return numberInput;
}
let periapsisInput = createInputWithLabel("Target peripasis:", 0);
let descriptorChoiceId = crypto.randomUUID();
let apoapsisButtonId = crypto.randomUUID();
let apoapsisButton = createRadioButton(descriptorChoiceId, apoapsisButtonId);
let apoapsisButtonLabel = createLabel(apoapsisButtonId, "Use apoapsis");
let speedAndAltitudeId = crypto.randomUUID();
let speedAndAltitudeButton = createRadioButton(descriptorChoiceId, speedAndAltitudeId);
let speedAndAltitudeLabel = createLabel(speedAndAltitudeId, "Use speed and altitude");
addToParent([apoapsisButton, apoapsisButtonLabel, speedAndAltitudeButton, speedAndAltitudeLabel]);
let apoapsisInput = createInputWithLabel("Target apopasis:", 0);
let speedInput = createInputWithLabel("Target speed:", 0);
let altitudeInput = createInputWithLabel("Target altitude:", 0);
let instantOneHeader = document.createElement("h4");
instantOneHeader.appendChild(document.createTextNode("Measurement one:"));
this.parentDiv.appendChild(instantOneHeader);
let firstOwnAltitudeInput = createInputWithLabel("Own altitude:", 0);
let firstTargetAltitudeInput = createInputWithLabel("Target altitude:", 0);
let firstDistanceInput = createInputWithLabel("Distance to target:", 0);
let instantTwoHeader = document.createElement("h4");
instantTwoHeader.appendChild(document.createTextNode("Measurement two:"));
this.parentDiv.appendChild(instantTwoHeader);
let secondOwnAltitudeInput = createInputWithLabel("Own altitude:", 0);
let secondTargetAltitudeInput = createInputWithLabel("Target altitude:", 0);
let secondDistanceInput = createInputWithLabel("Distance to target:", 0);
this.sourceId = crypto.randomUUID();
const onChange = () => {
let periapsis = parseFloat(periapsisInput.value);
let apoapsis = parseFloat(apoapsisInput.value);
let speed = parseFloat(speedInput.value);
let altitude = parseFloat(altitudeInput.value);
let firstTargetAltitude = parseFloat(firstTargetAltitudeInput.value);
let firstOwnAltitude = parseFloat(firstOwnAltitudeInput.value);
let firstDistance = parseFloat(firstDistanceInput.value);
let secondTargetAltitude = parseFloat(secondTargetAltitudeInput.value);
let secondOwnAltitude = parseFloat(secondOwnAltitudeInput.value);
let secondDistance = parseFloat(secondDistanceInput.value);
let orbitChoice: "apoapsis" | "speedAndAltitude" = "apoapsis";
if (speedAndAltitudeButton.checked) {
orbitChoice = "speedAndAltitude"
};
interpolationParameters.set({
targetPeriapsis: periapsis,
orbitChoice: orbitChoice,
targetApoapsis: apoapsis,
targetSpeed: speed,
targetAltitude: altitude,
firstTargetAltitude: firstTargetAltitude,
firstOwnAltitude: firstOwnAltitude,
firstDistance: firstDistance,
secondTargetAltitude: secondTargetAltitude,
secondOwnAltitude: secondOwnAltitude,
secondDistance: secondDistance,
}, this.sourceId)
};
[
periapsisInput,
apoapsisButton,
speedAndAltitudeButton,
apoapsisInput,
speedInput,
altitudeInput,
firstOwnAltitudeInput,
firstTargetAltitudeInput,
firstDistanceInput,
secondOwnAltitudeInput,
secondTargetAltitudeInput,
secondDistanceInput
].forEach(input => input.addEventListener("change", onChange));
interpolationParameters.listenToValue((source, value) => {
if (value.orbitChoice == "apoapsis") {
apoapsisInput.removeAttribute("disabled");
speedInput.setAttribute("disabled", "");
altitudeInput.setAttribute("disabled", "");
apoapsisButton.setAttribute("checked", "");
} else if (value.orbitChoice == "speedAndAltitude") {
apoapsisInput.setAttribute("disabled", "");
speedInput.removeAttribute("disabled");
altitudeInput.removeAttribute("disabled");
speedAndAltitudeButton.setAttribute("checked", "");
}
if (source == this.sourceId) {
return;
}
periapsisInput.value = value.targetPeriapsis.toString();
apoapsisInput.value = value.targetApoapsis.toString();
speedInput.value = value.targetSpeed.toString();
altitudeInput.value = value.targetAltitude.toString();
firstOwnAltitudeInput.value = value.firstOwnAltitude.toString();
firstTargetAltitudeInput.value = value.firstTargetAltitude.toString();
firstDistanceInput.value = value.firstDistance.toString();
secondOwnAltitudeInput.value = value.secondOwnAltitude.toString();
secondTargetAltitudeInput.value = value.secondTargetAltitude.toString();
secondDistanceInput.value = value.secondDistance.toString();
});
}
}

177
src/gui/landing.ts Normal file
View File

@ -0,0 +1,177 @@
import type { Body } from "../calculations/constants";
import { extrapolateTrajectory, findCheapestLanding, getOrbitalCoordinates, type OrbitalCoordinates, type ShipParameters } from "../calculations/orbit-calculations";
import type { ChangingStorageValue } from "../storage";
import { createDisabledInput, createLabel, getCoordinatesFromParameters, getOrbitFromParameters, type OrbitalParameters } from "./common";
import { type LandingParameters } from "../calculations/orbit-calculations";
import { LandingParametersGui } from "./landingparameters";
import { ShipGui } from "./ship";
import type { FindBestLandingMessage, LandingProgressMessage } from "./worker";
export class LandingGui {
parentDiv: HTMLDivElement;
currentTime: ChangingStorageValue<number>;
body: ChangingStorageValue<Body>;
orbitalParameters: ChangingStorageValue<OrbitalParameters>;
shipParameters: ChangingStorageValue<ShipParameters>;
landingParameters: ChangingStorageValue<LandingParameters>;
shipGui: ShipGui;
landingParametersGui: LandingParametersGui;
worker: Worker | null = null;
constructor(currentTime: ChangingStorageValue<number>, body: ChangingStorageValue<Body>, orbitalParameters: ChangingStorageValue<OrbitalParameters>, shipParameters: ChangingStorageValue<ShipParameters>, landingParameters: ChangingStorageValue<LandingParameters>) {
this.parentDiv = document.createElement("div");
this.currentTime = currentTime;
this.body = body;
this.orbitalParameters = orbitalParameters;
this.shipParameters = shipParameters;
this.landingParameters = landingParameters;
this.shipGui = new ShipGui(shipParameters);
this.landingParametersGui = new LandingParametersGui(this.landingParameters);
let shipHeader = document.createElement("h3");
shipHeader.appendChild(document.createTextNode("Ship attributes:"));
this.parentDiv.appendChild(shipHeader);
this.parentDiv.appendChild(this.shipGui.parentDiv);
let landingHeader = document.createElement("h3");
landingHeader.appendChild(document.createTextNode("Landing parameters"));
this.parentDiv.appendChild(landingHeader);
this.parentDiv.appendChild(this.landingParametersGui.parentDiv);
let findLandingButton = document.createElement("button");
findLandingButton.appendChild(document.createTextNode("Search for landing"));
this.parentDiv.appendChild(findLandingButton);
let landingSolutionHeader = document.createElement("h3");
landingSolutionHeader.appendChild(document.createTextNode("Landing:"));
this.parentDiv.appendChild(landingSolutionHeader);
let searchExplanation = document.createElement("p");
this.parentDiv.appendChild(searchExplanation);
const addToParent = (elements: HTMLElement[]) => {
let containerDiv = document.createElement("div");
containerDiv.classList.add("orbitalParameter");
this.parentDiv.appendChild(containerDiv);
elements.forEach(child => containerDiv.appendChild(child));
}
let manoeuvreHeader = document.createElement("h4");
manoeuvreHeader.appendChild(document.createTextNode("Manoeuvre:"));
this.parentDiv.appendChild(manoeuvreHeader);
let manoeuvreTimeId = crypto.randomUUID();
let manoeuvreTimeLabel = createLabel(manoeuvreTimeId, "Time:");
let manoeuvreTimeInput = createDisabledInput(manoeuvreTimeId);
addToParent([manoeuvreTimeLabel, manoeuvreTimeInput]);
let progradeId = crypto.randomUUID();
let progradeLabel = createLabel(progradeId, "Prograde delta-v:");
let progradeInput = createDisabledInput(progradeId);
addToParent([progradeLabel, progradeInput]);
let normalId = crypto.randomUUID();
let normalLabel = createLabel(normalId, "Normal delta-v:");
let normalInput = createDisabledInput(normalId);
addToParent([normalLabel, normalInput]);
let radialId = crypto.randomUUID();
let radialLabel = createLabel(radialId, "Radial delta-v:");
let radialInput = createDisabledInput(radialId);
addToParent([radialLabel, radialInput]);
let totalId = crypto.randomUUID();
let totalLabel = createLabel(totalId, "Total delta-v:");
let totalInput = createDisabledInput(totalId);
addToParent([totalLabel, totalInput]);
let brakingHeader = document.createElement("h4");
brakingHeader.appendChild(document.createTextNode("Braking:"));
this.parentDiv.appendChild(brakingHeader);
let brakingTimeId = crypto.randomUUID();
let brakingTimeLabel = createLabel(brakingTimeId, "Time to start braking:");
let brakingTimeInput = createDisabledInput(brakingTimeId);
addToParent([brakingTimeLabel, brakingTimeInput]);
let brakingAltitudeId = crypto.randomUUID();
let brakingAltitudeLabel = createLabel(brakingAltitudeId, "Altitude to start braking:");
let brakingAltitudeInput = createDisabledInput(brakingAltitudeId);
addToParent([brakingAltitudeLabel, brakingAltitudeInput]);
let brakingDeltaVId = crypto.randomUUID();
let brakingDeltaVLabel = createLabel(brakingDeltaVId, "Delta-v required:");
let brakingDeltaVInput = createDisabledInput(brakingDeltaVId);
addToParent([brakingDeltaVLabel, brakingDeltaVInput]);
findLandingButton.addEventListener("click", () => {
if (this.worker !== null) {
this.worker.terminate();
this.worker = null;
findLandingButton.innerHTML = "Search for landing";
} else {
findLandingButton.innerHTML = "Cancel search";
let currentTime = this.currentTime.getCurrentValue();
let orbitalParameters = this.orbitalParameters.getCurrentValue();
let shipParameters = this.shipParameters.getCurrentValue();
let landingParameters = this.landingParameters.getCurrentValue();
let body = this.body.getCurrentValue();
let coordinates: OrbitalCoordinates | null = getCoordinatesFromParameters(orbitalParameters, body);
let startTime = Math.max(currentTime, orbitalParameters.currentTimeAtReading);
if (startTime > orbitalParameters.currentTimeAtReading) {
let addedTime = startTime - orbitalParameters.currentTimeAtReading;
coordinates = extrapolateTrajectory(addedTime, coordinates, body);
}
if (coordinates) {
this.worker = new Worker(new URL('./worker.ts', import.meta.url), {type: 'module'});
this.worker.addEventListener("message", e => {
let landingResponse = e.data as LandingProgressMessage;
if (landingResponse) {
searchExplanation.innerHTML = `Search is ${landingResponse.percentDone}% finished`;
if (landingResponse.bestDeltaV != null && landingResponse.bestLanding != null) {
searchExplanation.appendChild(document.createElement("br"));
searchExplanation.appendChild(document.createTextNode(`Current best landing costs ${landingResponse.bestDeltaV.toFixed(3)} m/s`));
manoeuvreTimeInput.value = landingResponse.bestLanding.transferManoeuvre.time.toFixed(0);
progradeInput.value = landingResponse.bestLanding.transferManoeuvre.progradeDeltaV.toFixed(3);
normalInput.value = landingResponse.bestLanding.transferManoeuvre.normalDeltaV.toFixed(3);
radialInput.value = landingResponse.bestLanding.transferManoeuvre.radialDeltaV.toFixed(3);
totalInput.value = landingResponse.bestLanding.transferManoeuvre.totalDeltaV.toFixed(3);
brakingTimeInput.value = landingResponse.bestLanding.brakingTimestamp.toFixed(0);
brakingAltitudeInput.value = landingResponse.bestLanding.brakingAltitude.toFixed(0);
brakingDeltaVInput.value = landingResponse.bestLanding.brakingDeltaV.toFixed(3);
};
if (landingResponse.finished) {
this.worker?.terminate();
this.worker = null;
findLandingButton.innerHTML = "Search for landing";
}
}
});
let message: FindBestLandingMessage = {
type: "FindBestLandingMessage",
startingCoordinates: coordinates,
startTime: startTime,
shipParameters: shipParameters,
landingParameters: landingParameters,
body: body
};
this.worker.postMessage(message);
}
}
});
}
}

View File

@ -0,0 +1,71 @@
import type { ChangingStorageValue } from "../storage";
import { createLabel, createNumberInput } from "./common";
import { type LandingParameters } from "../calculations/orbit-calculations";
export class LandingParametersGui {
parentDiv: HTMLElement;
landingParameters: ChangingStorageValue<LandingParameters>;
sourceId: string;
constructor(landingParameters: ChangingStorageValue<LandingParameters>) {
this.parentDiv = document.createElement("div");
this.landingParameters = landingParameters;
let addToParent = (elements: HTMLElement[]) => {
let containerDiv = document.createElement("div");
containerDiv.classList.add("orbitalParameter");
this.parentDiv.appendChild(containerDiv);
elements.forEach(child => containerDiv.appendChild(child));
}
let targetLatitudeId = crypto.randomUUID();
let targetLatitudeLabel = createLabel(targetLatitudeId, "Target latitude:");
let targetLatitudeInput = createNumberInput(targetLatitudeId, -90, 90);
addToParent([targetLatitudeLabel, targetLatitudeInput]);
let targetLongitudeId = crypto.randomUUID();
let targetLongitudeLabel = createLabel(targetLongitudeId, "Target longitude:");
let targetLongitudeInput = createNumberInput(targetLongitudeId, -360, 360);
addToParent([targetLongitudeLabel, targetLongitudeInput]);
let targetAltitudeId = crypto.randomUUID();
let targetAltitudeLabel = createLabel(targetAltitudeId, "Target altitude:");
let targetAltitudeInput = createNumberInput(targetAltitudeId, 0);
addToParent([targetAltitudeLabel, targetAltitudeInput]);
let minimumAngleId = crypto.randomUUID();
let minimumAngleLabel = createLabel(minimumAngleId, "Minimum incoming angle:");
let minimumAngleInput = createNumberInput(minimumAngleId, 0, 90);
addToParent([minimumAngleLabel, minimumAngleInput]);
this.sourceId = crypto.randomUUID();
const onChange = () => {
let targetLatitude = parseFloat(targetLatitudeInput.value) * Math.PI / 180.0;
let targetLongitude = parseFloat(targetLongitudeInput.value) * Math.PI / 180.0;
let targetAltitude = parseFloat(targetAltitudeInput.value);
let minimumAngle = parseFloat(minimumAngleInput.value) * Math.PI / 180.0;
landingParameters.set({
targetLatitude: targetLatitude,
targetLongitude: targetLongitude,
targetAltitude: targetAltitude,
minimumAngle: minimumAngle
}, this.sourceId);
};
[targetLatitudeInput, targetLongitudeInput, targetAltitudeInput, minimumAngleInput].forEach(child => child.addEventListener("change", onChange));
landingParameters.listenToValue((source, value) => {
if (source == this.sourceId) {
return;
}
targetLatitudeInput.value = (value.targetLatitude * 180 / Math.PI).toString();
targetLongitudeInput.value = (value.targetLongitude * 180 / Math.PI).toString();
targetAltitudeInput.value = value.targetAltitude.toString();
minimumAngleInput.value = (value.minimumAngle * 180 / Math.PI).toString();
});
}
}

65
src/gui/ship.ts Normal file
View File

@ -0,0 +1,65 @@
import type { ShipParameters } from "../calculations/orbit-calculations";
import type { ChangingStorageValue } from "../storage";
import { createLabel, createNumberInput } from "./common";
export class ShipGui {
parentDiv: HTMLElement;
shipParameters: ChangingStorageValue<ShipParameters>;
sourceId: string;
constructor(shipParameters: ChangingStorageValue<ShipParameters>) {
this.parentDiv = document.createElement("div");
this.shipParameters = shipParameters;
let addToParent = (elements: HTMLElement[], parent: HTMLElement) => {
let containerDiv = document.createElement("div");
containerDiv.classList.add("orbitalParameter");
elements.forEach(child => containerDiv.appendChild(child));
parent.appendChild(containerDiv);
}
let shipMassId = crypto.randomUUID();
let shipMassLabel = createLabel(shipMassId, "Mass of ship (kg):")
let shipMassInput = createNumberInput(shipMassId, 0);
addToParent([shipMassLabel, shipMassInput], this.parentDiv);
let shipThrustId = crypto.randomUUID();
let shipThrustLabel = createLabel(shipThrustId, "Thrust of ship (N):");
let shipThrustInput = createNumberInput(shipThrustId, 0);
addToParent([shipThrustLabel, shipThrustInput], this.parentDiv);
let specificImpulseId = crypto.randomUUID();
let specificImpulseLabel = createLabel(specificImpulseId, "Specific impulse of ship (m/s):");
let specificImpulseInput = createNumberInput(specificImpulseId, 0);
addToParent([specificImpulseLabel, specificImpulseInput], this.parentDiv);
this.sourceId = crypto.randomUUID();
const onChange = () => {
let shipMass = parseFloat(shipMassInput.value);
let shipThrust = parseFloat(shipThrustInput.value);
let shipSpecificImpulse = parseFloat(specificImpulseInput.value);
this.shipParameters.set({
mass: shipMass,
thrust: shipThrust,
specificImpulse: shipSpecificImpulse
}, this.sourceId);
};
[shipMassInput, shipThrustInput, specificImpulseInput].forEach(child => child.addEventListener("change", onChange));
shipParameters.listenToValue((source, value) => {
if (source == this.sourceId) {
return;
}
shipMassInput.value = value.mass.toString();
shipThrustInput.value = value.thrust.toString();
specificImpulseInput.value = value.specificImpulse.toString();
});
}
}

View File

@ -82,6 +82,8 @@ export class SimplePlaneChangeGui {
let bestDeltaV = Math.min(planeChange.firstManoeuvre.totalDeltaV, planeChange.secondManoeuvre.totalDeltaV); let bestDeltaV = Math.min(planeChange.firstManoeuvre.totalDeltaV, planeChange.secondManoeuvre.totalDeltaV);
let bestTransfer: Transfer = { let bestTransfer: Transfer = {
transferOrbit: {semiLatusRectum: 0, eccentricity: 0, coordinateAxes: [[], [], []]}, transferOrbit: {semiLatusRectum: 0, eccentricity: 0, coordinateAxes: [[], [], []]},
transferOrbitTrueAnomalyAtManoeuvreOne: 0,
transferOrbitTrueanomalyAtManoeuvreTwo: 0,
firstManoeuvre: planeChange.firstManoeuvre, firstManoeuvre: planeChange.firstManoeuvre,
secondManoeuvre: planeChange.secondManoeuvre, secondManoeuvre: planeChange.secondManoeuvre,
closestPointDistance: 0, closestPointDistance: 0,

View File

@ -79,8 +79,8 @@ export class TargetOrbitGui {
} }
if (transferResponse.bestTransfer) { if (transferResponse.bestTransfer) {
transferResponse.bestTransfer.firstManoeuvre.time += currentTime; transferResponse.bestTransfer.firstManoeuvre.time += startingTime;
transferResponse.bestTransfer.secondManoeuvre.time += currentTime; transferResponse.bestTransfer.secondManoeuvre.time += startingTime;
} }
this.manoeuvresGui.displayProgress(transferResponse); this.manoeuvresGui.displayProgress(transferResponse);

View File

@ -1,5 +1,6 @@
import type { Body } from "../calculations/constants"; import type { Body } from "../calculations/constants";
import { findCheapestIntercept, findCheapestTransfer, type Orbit, type OrbitalCoordinates, type Transfer } from "../calculations/orbit-calculations"; import { findCheapestIntercept, findCheapestLanding, findCheapestTransfer, type Orbit, type OrbitalCoordinates, type ShipParameters, type Transfer } from "../calculations/orbit-calculations";
import type { Landing, LandingParameters, LandingProgressCallbackFunction, Manoeuvre } from "../calculations/orbit-calculations";
const ctx: Worker = self as any; const ctx: Worker = self as any;
@ -18,6 +19,15 @@ export interface FindBestInterceptMessage {
body: Body; body: Body;
} }
export interface FindBestLandingMessage {
"type": "FindBestLandingMessage",
startTime: number,
startingCoordinates: OrbitalCoordinates,
shipParameters: ShipParameters,
landingParameters: LandingParameters,
body: Body
}
export interface ProgressMessage { export interface ProgressMessage {
type: "ProgressMessage" type: "ProgressMessage"
finished: boolean, finished: boolean,
@ -26,6 +36,14 @@ export interface ProgressMessage {
bestTransfer: Transfer | null bestTransfer: Transfer | null
} }
export interface LandingProgressMessage {
type: "LandingProgressMessage",
finished: boolean,
percentDone: number,
bestDeltaV: number | null,
bestLanding: Landing | null
}
ctx.addEventListener("message", event => { ctx.addEventListener("message", event => {
const progressCallback = (numberChecked: number, totalNumber: number, bestDeltaV: number | null, bestTransfer: Transfer | null) => { const progressCallback = (numberChecked: number, totalNumber: number, bestDeltaV: number | null, bestTransfer: Transfer | null) => {
if (numberChecked % 100 == 0) { if (numberChecked % 100 == 0) {
@ -42,9 +60,23 @@ ctx.addEventListener("message", event => {
} }
}; };
let message = event.data as FindBestTransferMessage | FindBestInterceptMessage; const landingProgressCallback: LandingProgressCallbackFunction = (pathsChecked: number, totalNumberOfPaths: number, currentBestDeltaV: number | null, currentBestLanding: Landing | null) => {
if (pathsChecked % 100 == 0) {
let percentDone = pathsChecked * 100 / totalNumberOfPaths;
let message: LandingProgressMessage = {
type: "LandingProgressMessage",
finished: false,
percentDone: percentDone,
bestDeltaV: currentBestDeltaV,
bestLanding: currentBestLanding
};
ctx.postMessage(message);
}
}
let message = event.data as FindBestTransferMessage | FindBestInterceptMessage | FindBestLandingMessage;
if (message.type == "FindBestTransfer") { if (message.type == "FindBestTransfer") {
console.log("Finding best transfer");
let bestTransfer = findCheapestTransfer(message.startingSituation, message.targetOrbit, message.body, progressCallback); let bestTransfer = findCheapestTransfer(message.startingSituation, message.targetOrbit, message.body, progressCallback);
let bestDeltaV = null; let bestDeltaV = null;
if (bestTransfer) { if (bestTransfer) {
@ -62,7 +94,6 @@ ctx.addEventListener("message", event => {
} }
if (message.type == "FindBestIntercept") { if (message.type == "FindBestIntercept") {
console.log("Finding best intercept");
let bestIntercept = findCheapestIntercept(message.startingSituation, message.targetSituation, message.body, message.additionalTrueAnomaly, progressCallback); let bestIntercept = findCheapestIntercept(message.startingSituation, message.targetSituation, message.body, message.additionalTrueAnomaly, progressCallback);
let bestDeltaV = null; let bestDeltaV = null;
if (bestIntercept) { if (bestIntercept) {
@ -79,4 +110,17 @@ ctx.addEventListener("message", event => {
ctx.postMessage(finishedMessage); ctx.postMessage(finishedMessage);
} }
if (message.type == "FindBestLandingMessage") {
let bestLanding = findCheapestLanding(message.startingCoordinates, message.startTime, message.shipParameters, message.landingParameters, message.body, landingProgressCallback);
let finishedMessage: LandingProgressMessage = {
type: "LandingProgressMessage",
finished: true,
percentDone: 100,
bestDeltaV: bestLanding?.totalDeltaV ?? null,
bestLanding: bestLanding
};
ctx.postMessage(finishedMessage);
}
}); });

View File

@ -1,6 +1,9 @@
import { getPlanetByName, Kerbol } from "./calculations/constants"; import { getPlanetByName, Kerbol } from "./calculations/constants";
import { createLabel, createRadioButton, decodeOrbitalParameters, DefaultOrbitalParameters, encodeOrbitalParameters } from "./gui/common"; import { DefaultShipParameters } from "./calculations/orbit-calculations";
import { InterceptTargetGui } from "./gui/intercept"; import { createLabel, createRadioButton, DefaultLandingParameters, DefaultOrbitalParameters, encodeJsonObject, getDecoderFunction } from "./gui/common";
import { decodeTargetOrbitChoice, InterceptTargetGui } from "./gui/intercept";
import { DefaultInterpolationParameters } from "./gui/interpolate";
import { LandingGui } from "./gui/landing";
import { OrbitalParametersGui } from "./gui/orbit"; import { OrbitalParametersGui } from "./gui/orbit";
import { PlanetGui } from "./gui/planet"; import { PlanetGui } from "./gui/planet";
import { SimplePlaneChangeGui } from "./gui/simpleplanechange"; import { SimplePlaneChangeGui } from "./gui/simpleplanechange";
@ -8,7 +11,7 @@ import { TargetOrbitGui } from "./gui/targetorbit";
import { TimeGui } from "./gui/time"; import { TimeGui } from "./gui/time";
import { ChangingStorageValue } from "./storage"; import { ChangingStorageValue } from "./storage";
type CalculationType = "planeChange" | "targetOrbit" | "intercept"; type CalculationType = "planeChange" | "targetOrbit" | "intercept" | "landing";
function decodeCalculationType(input: string): CalculationType { function decodeCalculationType(input: string): CalculationType {
let calculationType = input as CalculationType; let calculationType = input as CalculationType;
if (calculationType !== undefined) { if (calculationType !== undefined) {
@ -20,11 +23,15 @@ function decodeCalculationType(input: string): CalculationType {
let currentTime = new ChangingStorageValue(0, "currentTime", parseInt, n => n.toString()); let currentTime = new ChangingStorageValue(0, "currentTime", parseInt, n => n.toString());
let body = new ChangingStorageValue(Kerbol, "planet", s => getPlanetByName(s), p => p.planetName); let body = new ChangingStorageValue(Kerbol, "planet", s => getPlanetByName(s), p => p.planetName);
let orbitalParameters = new ChangingStorageValue(DefaultOrbitalParameters, "orbitalParameters", decodeOrbitalParameters, encodeOrbitalParameters); let orbitalParameters = new ChangingStorageValue(DefaultOrbitalParameters, "orbitalParameters", getDecoderFunction(DefaultOrbitalParameters), encodeJsonObject);
let calculationType = new ChangingStorageValue("planeChange", "calculationType", decodeCalculationType, s => s); let calculationType = new ChangingStorageValue("planeChange", "calculationType", decodeCalculationType, s => s);
let targetOrbitalParameters = new ChangingStorageValue(DefaultOrbitalParameters, "targetOrbitalParameters", decodeOrbitalParameters, encodeOrbitalParameters); let targetOrbitalParameters = new ChangingStorageValue(DefaultOrbitalParameters, "targetOrbitalParameters", getDecoderFunction(DefaultOrbitalParameters), encodeJsonObject);
let circularizeOrbit = new ChangingStorageValue(false, "circularizeOrbit", s => s == "true", b => b ? "true" : "false"); let circularizeOrbit = new ChangingStorageValue(false, "circularizeOrbit", s => s == "true", b => b ? "true" : "false");
let targetOrbitChoice = new ChangingStorageValue("KnownOrbit", "targetOrbitChoice", decodeTargetOrbitChoice, s => s);
let interpolationParameters = new ChangingStorageValue(DefaultInterpolationParameters, "InterpolationParameters", getDecoderFunction(DefaultInterpolationParameters), encodeJsonObject);
let additionalInterceptTrueanomaly = new ChangingStorageValue(0, "additionalTrueAnomaly", parseFloat, s => s.toString()); let additionalInterceptTrueanomaly = new ChangingStorageValue(0, "additionalTrueAnomaly", parseFloat, s => s.toString());
let shipParameters = new ChangingStorageValue(DefaultShipParameters, "shipParameters", getDecoderFunction(DefaultShipParameters), encodeJsonObject);
let landingParameters = new ChangingStorageValue(DefaultLandingParameters, "landingParameters", getDecoderFunction(DefaultLandingParameters), encodeJsonObject);
let dateGui = new TimeGui(currentTime, true); let dateGui = new TimeGui(currentTime, true);
let currentTimeDiv = document.getElementById("currentTimeDiv"); let currentTimeDiv = document.getElementById("currentTimeDiv");
@ -40,7 +47,8 @@ orbitalParametersDiv?.appendChild(orbitalParametersGui.parentDiv);
let simplePlaneChangeGui = new SimplePlaneChangeGui(currentTime, body, orbitalParameters, targetOrbitalParameters, circularizeOrbit); let simplePlaneChangeGui = new SimplePlaneChangeGui(currentTime, body, orbitalParameters, targetOrbitalParameters, circularizeOrbit);
let targetOrbitGui = new TargetOrbitGui(currentTime, body, orbitalParameters, targetOrbitalParameters); let targetOrbitGui = new TargetOrbitGui(currentTime, body, orbitalParameters, targetOrbitalParameters);
let interceptTargetGui = new InterceptTargetGui(currentTime, body, orbitalParameters, targetOrbitalParameters, additionalInterceptTrueanomaly); let interceptTargetGui = new InterceptTargetGui(currentTime, body, orbitalParameters, targetOrbitalParameters, additionalInterceptTrueanomaly, targetOrbitChoice, interpolationParameters);
let landingGui = new LandingGui(currentTime, body, orbitalParameters, shipParameters, landingParameters);
let calculationChoiceDiv = document.getElementById("calculationChoice"); let calculationChoiceDiv = document.getElementById("calculationChoice");
@ -59,6 +67,11 @@ interceptTargetButton.addEventListener("change", () => calculationType.set("inte
let interceptTargetLabel = createLabel("interceptTarget", "Intercept target"); let interceptTargetLabel = createLabel("interceptTarget", "Intercept target");
[interceptTargetButton, interceptTargetLabel].forEach(child => calculationChoiceDiv?.appendChild(child)); [interceptTargetButton, interceptTargetLabel].forEach(child => calculationChoiceDiv?.appendChild(child));
let landingButton = createRadioButton("calculationChoice", "landing");
landingButton.addEventListener("change", () => calculationType.set("landing", "main"));
let landingLabel = createLabel("landing", "Landing");
[landingButton, landingLabel].forEach(child => calculationChoiceDiv?.appendChild(child));
let calculationsDiv = document.getElementById("calculation") as HTMLDivElement; let calculationsDiv = document.getElementById("calculation") as HTMLDivElement;
calculationType.listenToValue((_, value) => { calculationType.listenToValue((_, value) => {
calculationsDiv.innerHTML = ""; calculationsDiv.innerHTML = "";
@ -71,6 +84,9 @@ calculationType.listenToValue((_, value) => {
} else if (value == "intercept") { } else if (value == "intercept") {
interceptTargetButton.setAttribute("checked", ""); interceptTargetButton.setAttribute("checked", "");
calculationsDiv.appendChild(interceptTargetGui.parentDiv); calculationsDiv.appendChild(interceptTargetGui.parentDiv);
} else if (value == "landing") {
landingButton.setAttribute("checked", "");
calculationsDiv.appendChild(landingGui.parentDiv);
} }
}); });