Kinda works, improve intercept
This commit is contained in:
parent
8e43c1f51b
commit
a14f1268ab
@ -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 = {
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -900,3 +928,486 @@ 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];
|
||||||
|
}
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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
171
src/gui/interpolate.ts
Normal 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
177
src/gui/landing.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/gui/landingparameters.ts
Normal file
71
src/gui/landingparameters.ts
Normal 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
65
src/gui/ship.ts
Normal 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
28
src/main.ts
28
src/main.ts
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user