From 400bf42b126b9dcefc973de778ab7cee5af8935a Mon Sep 17 00:00:00 2001 From: Martin Asprusten Date: Wed, 25 Mar 2026 21:56:05 +0100 Subject: [PATCH] Before full rewrite of orbit calculations --- index.html | 202 ++++++++++++++++++- package-lock.json | 24 --- package.json | 4 - public/satellite.png | Bin 0 -> 19086 bytes src/calculations/constants.ts | 16 +- src/calculations/mathematics.ts | 208 ++++++++++++++++++++ src/calculations/orbit-calculations.ts | 259 +++++++++++++++++++++++++ src/main.ts | 237 +++++++++++++++++++--- 8 files changed, 890 insertions(+), 60 deletions(-) create mode 100644 public/satellite.png create mode 100644 src/calculations/mathematics.ts diff --git a/index.html b/index.html index 1ac0759..2dfe660 100644 --- a/index.html +++ b/index.html @@ -2,12 +2,210 @@ - + Kerbal calculations -
+

Kerbal calculations

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Current time in game:

+

Time to periapsis:

+
+ +

Planet:

+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +

Orbital parameters:

+ + + + + + + + + + + + + + + + + + + + + +
+ +

What to calculate:

+ + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + +
+
+ +
+

Target plane:

+ + + + + + + + + +
+ +
+ + +

Choose one of the following manoeuvres:

+ + + + + + + + + + + + + + + + + + + + + + + + +
Manoeuvre 1Manoeuvre 2
+
+ +
+

Target orbit:

+ + + + + + + + + + + + + + + + + + + + + +
+ +
+ diff --git a/package-lock.json b/package-lock.json index ac0d838..d48d2bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,11 +7,7 @@ "": { "name": "kerbal-calculations", "version": "0.0.0", - "dependencies": { - "ts-matrix": "^1.4.1" - }, "devDependencies": { - "typescript": "~5.9.3", "vite": "^7.3.1" } }, @@ -1029,26 +1025,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/ts-matrix": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/ts-matrix/-/ts-matrix-1.4.1.tgz", - "integrity": "sha512-huf8zaQF/NackiLsyFiWqX9uyZVGyHCXEmSiWd4/DDuioTlADsO82oA5FOudoKVtZVUDYG4HDzN/jSF2eB7Z7A==", - "license": "SEE LICENSE IN LICENSE.md" - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", diff --git a/package.json b/package.json index 22ed0b9..fab0842 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,6 @@ "preview": "vite preview" }, "devDependencies": { - "typescript": "~5.9.3", "vite": "^7.3.1" - }, - "dependencies": { - "ts-matrix": "^1.4.1" } } diff --git a/public/satellite.png b/public/satellite.png new file mode 100644 index 0000000000000000000000000000000000000000..a9758945b506125ed790db677a6341eda8908081 GIT binary patch literal 19086 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 Z{vTbTrSa|FC;|Wg002ovPDHLkV1fbUYXJZN literal 0 HcmV?d00001 diff --git a/src/calculations/constants.ts b/src/calculations/constants.ts index c46ecde..c910a57 100644 --- a/src/calculations/constants.ts +++ b/src/calculations/constants.ts @@ -1,4 +1,4 @@ -interface Planet { +export interface Planet { radius: number, gravitationalParameter: number, rotationPeriod: number, @@ -6,7 +6,7 @@ interface Planet { initialMeridianLongitude: number } -const Kerbol: Planet = { +export const Kerbol: Planet = { radius: 261600000, gravitationalParameter: 1.1723328e18, rotationPeriod: 432000, @@ -14,7 +14,7 @@ const Kerbol: Planet = { initialMeridianLongitude: 0 }; -const Moho: Planet = { +export const Moho: Planet = { radius: 250000, gravitationalParameter: 1.6860938e11, rotationPeriod: 1210000, @@ -22,7 +22,7 @@ const Moho: Planet = { initialMeridianLongitude: 0 }; -const Eve: Planet = { +export const Eve: Planet = { radius: 700000, gravitationalParameter: 8.1717302e12, rotationPeriod: 80500, @@ -30,7 +30,7 @@ const Eve: Planet = { initialMeridianLongitude: 0 }; -const Gilly: Planet = { +export const Gilly: Planet = { radius: 13000, gravitationalParameter: 8289449.8, rotationPeriod: 28255, @@ -38,7 +38,7 @@ const Gilly: Planet = { initialMeridianLongitude: 0.0859373 }; -const Kerbin: Planet = { +export const Kerbin: Planet = { radius: 600000, gravitationalParameter: 3.5316000e12, rotationPeriod: 21549.425, @@ -46,7 +46,7 @@ const Kerbin: Planet = { initialMeridianLongitude: 1.571261023 }; -const Mun: Planet = { +export const Mun: Planet = { radius: 200000, gravitationalParameter: 6.5138398e10, rotationPeriod: 138984.38, @@ -54,7 +54,7 @@ const Mun: Planet = { initialMeridianLongitude: 4.0145103174219114 }; -const Minmus: Planet = { +export const Minmus: Planet = { radius: 60000, gravitationalParameter: 1.7658000e9, rotationPeriod: 40400, diff --git a/src/calculations/mathematics.ts b/src/calculations/mathematics.ts new file mode 100644 index 0000000..eaaeb2b --- /dev/null +++ b/src/calculations/mathematics.ts @@ -0,0 +1,208 @@ +export function checkIfValidMatrix(matrix: number[][]): boolean { + if (matrix.length <= 0) { + return false; + } + + // Just make sure each row has equally many columns + var numberOfColumns = -1; + matrix.forEach(row => { + if (numberOfColumns == -1) { + numberOfColumns = row.length; + } + + if (row.length != numberOfColumns) { + return false; + } + }); + + return true; +} + +export function matrixMultiply(matrixOne: number[][], matrixTwo: number[][]): number[][] { + if (!checkIfValidMatrix(matrixOne) || !checkIfValidMatrix(matrixTwo)) { + throw new TypeError("Two valid matrices are required"); + } + + var rowsFirstMatrix = matrixOne.length; + var colsFirstMatrix = matrixOne[0].length; + + var rowsSecondMatrix = matrixTwo.length; + var colsSecondMatrix = matrixTwo[0].length; + + if (colsFirstMatrix != rowsSecondMatrix) { + throw new TypeError("The two matrices do not have the correct dimensions to be multiplied together"); + } + + var result = []; + for (var i = 0; i < rowsFirstMatrix; i++) { + var currentRow = []; + for (var j = 0; j < colsSecondMatrix; j++) { + var currentResult = 0; + for (var k = 0; k < colsFirstMatrix; k++) { + currentResult += matrixOne[i][k] * matrixTwo[k][j]; + } + currentRow.push(currentResult); + } + result.push(currentRow); + } + + return result; +} + +export function vectorDotProduct(vectorOne: number[][], vectorTwo: number[][]): number { + if (!checkIfValidMatrix(vectorOne) || !checkIfValidMatrix(vectorTwo)) { + throw new TypeError("Two valid matrices are required"); + } + + if (vectorOne[0].length != 1 || vectorTwo[0].length != 1) { + throw new TypeError("Vectors can only have one column"); + } + + if (vectorOne.length != vectorTwo.length) { + throw new TypeError("The two vectors need to have the same dimensions"); + } + + var result = 0; + for (var i = 0; i < vectorOne.length; i++) { + result += vectorOne[i][0] * vectorTwo[i][0]; + } + + return result; +} + +export function vectorCrossProduct(vectorOne: number[][], vectorTwo: number[][]): number[][] { + if (!checkIfValidMatrix(vectorOne) || !checkIfValidMatrix(vectorTwo)) { + throw new TypeError("Two valid matrices are required"); + } + + if (vectorOne[0].length != 1 || vectorTwo[0].length != 1) { + throw new TypeError("Vectors can only have one column"); + } + + if (vectorOne.length != vectorTwo.length) { + throw new TypeError("The two vectors need to have the same dimensions"); + } + + if (vectorOne.length != 3) { + throw new TypeError("The vectors need to be three-dimensional"); + } + + return [ + [vectorOne[1][0]*vectorTwo[2][0] - vectorOne[2][0]*vectorTwo[1][0]], + [vectorOne[2][0]*vectorTwo[0][0] - vectorOne[0][0]*vectorTwo[2][0]], + [vectorOne[0][0]*vectorTwo[1][0] - vectorOne[1][0]*vectorTwo[0][0]] + ]; +} + +export function matrixTranspose(matrix: number[][]): number[][] { + if (!checkIfValidMatrix(matrix)) { + throw new TypeError("A valid matrix is required"); + } + + var result = []; + + for (var j = 0; j < matrix[0].length; j++) { + var currentRow = []; + for (var i = 0; i < matrix.length; i++) { + currentRow.push(matrix[i][j]); + } + result.push(currentRow); + } + + return result; +} + +export function addVector(vectorOne: number[][], vectorTwo: number[][]): number[][] { + if (!checkIfValidMatrix(vectorOne) || !checkIfValidMatrix(vectorTwo)) { + throw new TypeError("Two valid matrices are required"); + } + + if (vectorOne[0].length != 1 || vectorTwo[0].length != 1) { + throw new TypeError("Vectors can only have one column"); + } + + if (vectorOne.length != vectorTwo.length) { + throw new TypeError("The two vectors need to have the same dimensions"); + } + + var result = []; + for (var i = 0; i < vectorOne.length; i++) { + result.push([vectorOne[i][0] + vectorTwo[i][0]]); + } + + return result; +} + +export function subtractVector(vectorOne: number[][], vectorTwo: number[][]): number[][] { + if (!checkIfValidMatrix(vectorOne) || !checkIfValidMatrix(vectorTwo)) { + throw new TypeError("Two valid matrices are required"); + } + + if (vectorOne[0].length != 1 || vectorTwo[0].length != 1) { + throw new TypeError("Vectors can only have one column"); + } + + if (vectorOne.length != vectorTwo.length) { + throw new TypeError("The two vectors need to have the same dimensions"); + } + + var result = []; + for (var i = 0; i < vectorOne.length; i++) { + result.push([vectorOne[i][0] - vectorTwo[i][0]]); + } + + return result; +} + +export function getVectorMagnitude(vector: number[][]): number { + if (!checkIfValidMatrix(vector)) { + throw new TypeError("A valid matrix is required"); + } + + if (vector[0].length != 1) { + throw new TypeError("Vectors can only have one column"); + } + + var result = 0; + for (var i = 0; i < vector.length; i++) { + result += vector[i][0]**2; + } + + return Math.sqrt(result); +} + +export function normalizeVector(vector: number[][]) { + if (!checkIfValidMatrix(vector)) { + throw new TypeError("A valid matrix is required"); + } + + if (vector[0].length != 1) { + throw new TypeError("Vectors can only have one column"); + } + + const magnitude = getVectorMagnitude(vector); + + var result = []; + for (var i = 0; i < vector.length; i++) { + result.push([vector[i][0] / magnitude]); + } + + return result; +} + +export function multiplyMatrixWithScalar(scalar: number, matrix: number[][]): number[][] { + if (!checkIfValidMatrix(matrix)) { + throw new TypeError("A valid matrix is required"); + } + + var result = []; + for (var i = 0; i < matrix.length; i++) { + var row = []; + for (var j = 0; j < matrix[i].length; j++) { + row.push(scalar * matrix[i][j]); + } + result.push(row); + } + + return result; +} \ No newline at end of file diff --git a/src/calculations/orbit-calculations.ts b/src/calculations/orbit-calculations.ts index e69de29..b937130 100644 --- a/src/calculations/orbit-calculations.ts +++ b/src/calculations/orbit-calculations.ts @@ -0,0 +1,259 @@ +import type { Planet } from "./constants"; +import { getVectorMagnitude, matrixMultiply, matrixTranspose, multiplyMatrixWithScalar, normalizeVector, subtractVector, vectorCrossProduct, vectorDotProduct } from "./mathematics"; + +export interface Axes { + semiMajor: number, + semiMinor: number, + linearEccentricity: number, + eccentricity: number +} + +export interface OrbitalElementRotations { + argumentOfPeriapsisRotation: number[][], + inclinationRotation: number[][], + longitudeOfAscendingNodeRotation: number[][], + transformationOutOfPlane: number[][], + transformationIntoPlane: number[][] +} + +export interface OrbitalCoordinates { + meanAnomaly: number, + orbitalPeriod: number, + eccentricAnomaly: number, + position: number[][], + latitude: number, + longitude: number, + longitudeOverPlanet: number, + axes: Axes, + orbitalRotations: OrbitalElementRotations, + currentTime: number, + planet: Planet +} + +export interface Manoeuvre { + time: number, + progradeAcceleration: number, + radialAcceleration: number, + normalAcceleration: number, + totalAcceleration: number +} + +const zeroManoeuvre: Manoeuvre = { + time: 0, + progradeAcceleration: 0, + radialAcceleration: 0, + normalAcceleration: 0, + totalAcceleration: 0 +} + +export interface Transfer { + originalPlaneCoordinateAxes: [number[][], number[][], number[][]], + transferPlaneCoordinateAxes: [number[][], number[][], number[][]], + targetPlaneCoordinateAxes: [number[][], number[][], number[][]], + + firstManoeuvre: Manoeuvre, + secondManoeuvre: Manoeuvre +} + +export interface LambertFunction { + (lambda: number): Transfer +} + +export interface LambertSolver { + lambertFunction: LambertFunction, + extremeLambda: number, + parabolaLambda: number +} + +export function getAxes(periapsis: number, apoapsis: number, planet: Planet): Axes { + const semiMajor = (periapsis + apoapsis) / 2 + planet.radius; + const linearEccentricity = (semiMajor - periapsis - planet.radius); + const eccentricity = linearEccentricity / semiMajor; + const semiMinor = Math.sqrt(semiMajor**2 - linearEccentricity**2); + + return { + semiMajor: semiMajor, + semiMinor: semiMinor, + linearEccentricity: linearEccentricity, + eccentricity: eccentricity + }; +} + +export function getOrbitalPeriod(axes: Axes, gravitationalParameter: number): number { + return 2 * Math.PI * Math.sqrt(axes.semiMajor**3 / gravitationalParameter); +} + +export function getMeanAnomalyFromTimeToPeriapsis(timeToPeriapsis: number, periapsis: number, apoapsis: number, planet: Planet): number { + const axes = getAxes(periapsis, apoapsis, planet); + const orbitalPeriod = getOrbitalPeriod(axes, planet.gravitationalParameter); + return (orbitalPeriod - timeToPeriapsis) * 2 * Math.PI / orbitalPeriod; +} + +export function getMeanAnomalyFromEccentricAnomaly(eccentricAnomaly: number, eccentricity: number): number { + return eccentricAnomaly - eccentricity * Math.sin(eccentricAnomaly); +} + +export function getEccentricAnomalyFromMeanAnomaly(meanAnomaly: number, eccentricity: number) { + // Use fixed point iteration + var eccentricAnomaly = meanAnomaly; + const iterationFunction = (eccentricAnomaly: number): number => { + return meanAnomaly + eccentricity * Math.sin(eccentricAnomaly); + } + + while (Math.abs(eccentricAnomaly - eccentricity*Math.sin(eccentricAnomaly) - meanAnomaly) > 0.00000001) { + eccentricAnomaly = iterationFunction(eccentricAnomaly); + } + + return eccentricAnomaly; +} + +export function getOrbitalElementRotations(inclination: number, longitudeOfAscendingNode: number, argumentOfPeriapsis: number): OrbitalElementRotations { + const argumentOfPeriapsisRotation = + [ + [Math.cos(argumentOfPeriapsis), -Math.sin(argumentOfPeriapsis), 0], + [Math.sin(argumentOfPeriapsis), Math.cos(argumentOfPeriapsis), 0], + [0, 0, 1] + ]; + + const inclinationRotation = + [ + [1, 0, 0], + [0, Math.cos(inclination), -Math.sin(inclination)], + [0, Math.sin(inclination), Math.cos(inclination)] + ]; + + const longitudeOfAscendingNodeRotation = + [ + [Math.cos(longitudeOfAscendingNode), -Math.sin(longitudeOfAscendingNode), 0], + [Math.sin(longitudeOfAscendingNode), Math.cos(longitudeOfAscendingNode), 0], + [0, 0, 1] + ]; + + const transformationOutOfPlane = matrixMultiply(longitudeOfAscendingNodeRotation, matrixMultiply(inclinationRotation, argumentOfPeriapsisRotation)); + const transformationIntoPlane = matrixTranspose(transformationOutOfPlane); + + return { + argumentOfPeriapsisRotation: argumentOfPeriapsisRotation, + inclinationRotation: inclinationRotation, + longitudeOfAscendingNodeRotation: longitudeOfAscendingNodeRotation, + transformationOutOfPlane: transformationOutOfPlane, + transformationIntoPlane: transformationIntoPlane + }; +} + +export function getOrbitalCoordinates(currentTime: number, timeToPeriapsis: number, periapsis: number, apoapsis: number, inclination: number, longitudeOfAscendingNode: number, argumentOfPeriapsis: number, planet: Planet): OrbitalCoordinates { + const axes = getAxes(periapsis, apoapsis, planet); + const orbitalPeriod = getOrbitalPeriod(axes, planet.gravitationalParameter); + const meanAnomaly = getMeanAnomalyFromTimeToPeriapsis(timeToPeriapsis, periapsis, apoapsis, planet); + const eccentricAnomaly = getEccentricAnomalyFromMeanAnomaly(meanAnomaly, axes.eccentricity); + const orbitalRotations = getOrbitalElementRotations(inclination, longitudeOfAscendingNode, argumentOfPeriapsis); + const localPosition = [ + [axes.semiMajor * Math.cos(eccentricAnomaly) - axes.linearEccentricity], + [axes.semiMinor * Math.sin(eccentricAnomaly)], + [0] + ]; + + const globalPosition = matrixMultiply(orbitalRotations.transformationOutOfPlane, localPosition); + const longitude = Math.atan2(globalPosition[1][0], globalPosition[0][0]); + const latitude = Math.atan2(globalPosition[2][0], Math.sqrt(globalPosition[0][0]**2 + globalPosition[1][0]**2)); + + const currentMeridianLongitude = planet.initialMeridianLongitude + currentTime * 2 * Math.PI / planet.rotationPeriod; + const longitudeOverPlanet = ((longitude - currentMeridianLongitude) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI); + + return { + meanAnomaly: meanAnomaly, + orbitalPeriod: orbitalPeriod, + eccentricAnomaly: eccentricAnomaly, + position: globalPosition, + latitude: latitude, + longitude: longitude, + longitudeOverPlanet: longitudeOverPlanet, + axes: axes, + orbitalRotations: orbitalRotations, + currentTime: currentTime, + planet: planet + }; +} + +export function calculateSimplePlaneChange(coordinates: OrbitalCoordinates, targetInclination: number, targetLongitudeOfAscendingNode: number, circularizeOrbit: boolean): [Manoeuvre, Manoeuvre] { + const targetRotations = getOrbitalElementRotations(targetInclination, targetLongitudeOfAscendingNode, 0); + const targetPlaneNormalVector = normalizeVector(matrixMultiply(coordinates.orbitalRotations.transformationIntoPlane, matrixMultiply(targetRotations.transformationOutOfPlane, [[0], [0], [1]]))); + + // Check if target plane is equal to current plane + if (1 - Math.abs(vectorDotProduct(targetPlaneNormalVector, [[0], [0], [1]])) < 0.0001) { + return [zeroManoeuvre, zeroManoeuvre]; + } + + // Find vector that is normal to both current plane vector and target plane vector (i.e. lies in both planes) + const normalToAll = vectorCrossProduct(targetPlaneNormalVector, [[0], [0], [1]]); + + // Find the true anomaly of this vector + const trueAnomaly = Math.atan2(normalToAll[1][0], normalToAll[0][0]); + + // Caclulate the two possible manoeuvres + var manoeuvres: Manoeuvre[] = []; + const anomalies = [trueAnomaly, trueAnomaly + Math.PI]; + anomalies.forEach(anomaly => { + const eccentricAnomaly = 2 * Math.atan(Math.sqrt((1 - coordinates.axes.eccentricity) / (1 + coordinates.axes.eccentricity)) * Math.tan(anomaly / 2)); + var meanAnomaly = getMeanAnomalyFromEccentricAnomaly(eccentricAnomaly, coordinates.axes.eccentricity); + + while (meanAnomaly < coordinates.meanAnomaly) { + meanAnomaly += 2*Math.PI; + } + + const manoeuvreTime = (meanAnomaly - coordinates.meanAnomaly) * coordinates.orbitalPeriod / (2 * Math.PI) + coordinates.currentTime; + + const progradeVector = normalizeVector([ + [-coordinates.axes.semiMajor * Math.sin(eccentricAnomaly)], + [coordinates.axes.semiMinor * Math.cos(eccentricAnomaly)], + [0] + ]); + + const normalVector = [ + [0], + [0], + [1] + ]; + + const radialVector = vectorCrossProduct(normalVector, progradeVector); + + const radius = coordinates.axes.semiMajor * (1 - coordinates.axes.eccentricity * Math.cos(eccentricAnomaly)); + const speed = Math.sqrt(coordinates.planet.gravitationalParameter * (2 / radius - 1 / coordinates.axes.semiMajor)); + + const velocity = multiplyMatrixWithScalar(speed, progradeVector); + var velocityChange: number[][]; + var deltaV: number; + + if (!circularizeOrbit) { + deltaV = vectorDotProduct(velocity, multiplyMatrixWithScalar(-1, targetPlaneNormalVector)); + velocityChange = multiplyMatrixWithScalar(deltaV, targetPlaneNormalVector); + deltaV = Math.abs(deltaV); + } else { + const targetSpeed = Math.sqrt(coordinates.planet.gravitationalParameter / radius); + const positionVector = [ + [coordinates.axes.semiMajor * Math.cos(eccentricAnomaly)], + [coordinates.axes.semiMinor * Math.sin(eccentricAnomaly)], + [0] + ]; + + const targetDirection = normalizeVector(vectorCrossProduct(targetPlaneNormalVector, positionVector)); + const targetVelocity = multiplyMatrixWithScalar(targetSpeed, targetDirection); + velocityChange = subtractVector(targetVelocity, velocity); + deltaV = getVectorMagnitude(velocityChange); + } + + const progradeChange = vectorDotProduct(velocityChange, progradeVector); + const radialChange = -vectorDotProduct(velocityChange, radialVector); + const normalChange = vectorDotProduct(velocityChange, normalVector); + + manoeuvres.push({ + time: manoeuvreTime, + progradeAcceleration: progradeChange, + radialAcceleration: radialChange, + normalAcceleration: normalChange, + totalAcceleration: deltaV + }); + }); + + return [manoeuvres[0], manoeuvres[1]]; +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 6396b50..84f8183 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,24 +1,217 @@ -import './style.css' -import typescriptLogo from './typescript.svg' -import viteLogo from '/vite.svg' -import { setupCounter } from './counter.ts' +import { Eve, Gilly, Kerbin, Kerbol, Minmus, Moho, Mun, type Planet } from "./calculations/constants"; +import {calculateSimplePlaneChange, getOrbitalCoordinates} from "./calculations/orbit-calculations"; -document.querySelector('#app')!.innerHTML = ` -
- - - - - - -

Vite + TypeScript

-
- -
-

- Click on the Vite and TypeScript logos to learn more -

-
-` +const dateInputYears = document.getElementById("dateYear") as HTMLInputElement; +const dateInputDays = document.getElementById("dateDay") as HTMLInputElement; +const dateInputHours = document.getElementById("dateHours") as HTMLInputElement; +const dateInputMinutes = document.getElementById("dateMinutes") as HTMLInputElement; +const dateInputSeconds = document.getElementById("dateSeconds") as HTMLInputElement; -setupCounter(document.querySelector('#counter')!) +const periapsisInputYears = document.getElementById("periapsisYears") as HTMLInputElement; +const periapsisInputDays = document.getElementById("periapsisDays") as HTMLInputElement; +const periapsisInputHours = document.getElementById("periapsisHours") as HTMLInputElement; +const periapsisInputMinutes = document.getElementById("periapsisMinutes") as HTMLInputElement; +const periapsisInputSeconds = document.getElementById("periapsisSeconds") as HTMLInputElement; + +const mohoButton = document.getElementById("moho") as HTMLInputElement; +const eveButton = document.getElementById("eve") as HTMLInputElement; +const gillyButton = document.getElementById("gilly") as HTMLInputElement; +const kerbinButton = document.getElementById("kerbin") as HTMLInputElement; +const munButton = document.getElementById("mun") as HTMLInputElement; +const minmusButton = document.getElementById("minmus") as HTMLInputElement; + +const currentPeriapsisInput = document.getElementById("currentPeriapsis") as HTMLInputElement; +const currentApoapsisInput = document.getElementById("currentApoapsis") as HTMLInputElement; +const currentInclinationInput = document.getElementById("currentInclination") as HTMLInputElement; +const currentLANInput = document.getElementById("currentLAN") as HTMLInputElement; +const currentAOPInput = document.getElementById("currentAOP") as HTMLInputElement; + +const coordinatesRadio = document.getElementById("coordinates") as HTMLInputElement; +const simplePlaneChangeRadio = document.getElementById("simplePlaneChange") as HTMLInputElement; +const orbitChangeRadio = document.getElementById("orbitChange") as HTMLInputElement; + +const coordinatesDiv = document.getElementById("coordinatesDiv"); +const coordinateCalculationButton = document.getElementById("calculateCoordinatesButton") as HTMLButtonElement; +const meanAnomalyInput = document.getElementById("calculatedMeanAnomaly") as HTMLInputElement; +const eccentricAnomalyInput = document.getElementById("calculatedEccentricAnomaly") as HTMLInputElement; +const positionXInput = document.getElementById("calculatedX") as HTMLInputElement; +const positionYInput = document.getElementById("calculatedY") as HTMLInputElement; +const positionZInput = document.getElementById("calculatedZ") as HTMLInputElement; +const latitudeInput = document.getElementById("calculatedLatitude") as HTMLInputElement; +const longitudeInput = document.getElementById("calculatedLongitude") as HTMLInputElement; +const longitudeOverPlanetInput = document.getElementById("calculatedPlanetLongitude") as HTMLInputElement; + +const simplePlaneChangeDiv = document.getElementById("simplePlaneChangeDiv"); +const targetInclinationInput = document.getElementById("targetInclination") as HTMLInputElement; +const targetLANInput = document.getElementById("targetLAN") as HTMLInputElement; +const circularizeCheckbox = document.getElementById("circularizeOrbit") as HTMLInputElement; +const simplePlaneChangeButton = document.getElementById("simplePlaneChangeButton") as HTMLButtonElement; + +const orbitChangeDiv = document.getElementById("orbitChangeDiv"); + +const simplePlaneChangeTimes = [ + document.getElementById("simpleManoeuvreTime1") as HTMLInputElement, + document.getElementById("simpleManoeuvreTime2") as HTMLInputElement +]; +const simplePlaneChangeProgrades = [ + document.getElementById("simpleManoeuvrePrograde1") as HTMLInputElement, + document.getElementById("simpleManoeuvrePrograde2") as HTMLInputElement +]; + +const simplePlaneChangeRadials = [ + document.getElementById("simpleManoeuvreRadial1") as HTMLInputElement, + document.getElementById("simpleManoeuvreRadial2") as HTMLInputElement +]; + +const simplePlaneChangeNormals = [ + document.getElementById("simpleManoeuvreNormal1") as HTMLInputElement, + document.getElementById("simpleManoeuvreNormal2") as HTMLInputElement +]; + +const simplePlaneChangeTotals = [ + document.getElementById("simpleManoeuvreTotal1") as HTMLInputElement, + document.getElementById("simpleManoeuvreTotal2") as HTMLInputElement +]; + + +function getDate(): number { + const years = parseInt(dateInputYears.value); + const days = parseInt(dateInputDays.value); + const hours = parseInt(dateInputHours.value); + const minutes = parseInt(dateInputMinutes.value); + const seconds = parseInt(dateInputSeconds.value); + + return ((((years - 1) * 426 + days - 1) * 6 + hours) * 60 + minutes) * 60 + seconds; +} + +function getTimeToPeriapsis(): number { + const years = parseInt(periapsisInputYears.value); + const days = parseInt(periapsisInputDays.value); + const hours = parseInt(periapsisInputHours.value); + const minutes = parseInt(periapsisInputMinutes.value); + const seconds = parseInt(periapsisInputSeconds.value); + + return (((years * 426 + days) * 6 + hours) * 60 + minutes) * 60 + seconds; +} + +function getPlanet(): Planet { + if (mohoButton.checked) { + return Moho; + } else if (eveButton.checked) { + return Eve; + } else if (gillyButton.checked) { + return Gilly; + } else if (kerbinButton.checked) { + return Kerbin; + } else if (munButton.checked) { + return Mun; + } else if (minmusButton.checked) { + return Minmus; + } + + return Kerbol; +} + +function selectCalculationType() { + if (coordinatesRadio.checked) { + coordinatesDiv?.style.setProperty("display", "block"); + simplePlaneChangeDiv?.style.setProperty("display", "none"); + orbitChangeDiv?.style.setProperty("display", "none"); + } else if (simplePlaneChangeRadio.checked) { + coordinatesDiv?.style.setProperty("display", "none"); + simplePlaneChangeDiv?.style.setProperty("display", "block"); + orbitChangeDiv?.style.setProperty("display", "none"); + } else if (orbitChangeRadio.checked) { + coordinatesDiv?.style.setProperty("display", "none"); + simplePlaneChangeDiv?.style.setProperty("display", "none"); + orbitChangeDiv?.style.setProperty("display", "block"); + } +} + +coordinatesRadio.onclick = selectCalculationType; +simplePlaneChangeRadio.onclick = selectCalculationType; +orbitChangeRadio.onclick = selectCalculationType; + +interface CommonInputs { + periapsis: number, + apoapsis: number, + timeToPeriapsis: number, + planet: Planet, + inclination: number, + longitudeOfAscendingNode: number, + argumentOfPeriapsis: number, + timeElapsed: number +} + +function getCommonInputs(): CommonInputs { + const periapsis = parseFloat(currentPeriapsisInput.value); + const apoapsis = parseFloat(currentApoapsisInput.value); + const timeToPeriapsis = getTimeToPeriapsis(); + const planet = getPlanet(); + const inclination = parseFloat(currentInclinationInput.value) * Math.PI / 180.0; + const longitudeOfAscendingNode = parseFloat(currentLANInput.value) * Math.PI / 180.0; + const argumentOfPeriapsis = parseFloat(currentAOPInput.value) * Math.PI / 180.0; + const timeElapsed = getDate(); + + return { + periapsis: periapsis, + apoapsis: apoapsis, + timeToPeriapsis: timeToPeriapsis, + planet: planet, + inclination: inclination, + longitudeOfAscendingNode: longitudeOfAscendingNode, + argumentOfPeriapsis: argumentOfPeriapsis, + timeElapsed: timeElapsed + } +} + +coordinateCalculationButton.addEventListener("click", _ => { + const commonInputs = getCommonInputs(); + const orbitalCoordinates = getOrbitalCoordinates( + commonInputs.timeElapsed, + commonInputs.timeToPeriapsis, + commonInputs.periapsis, + commonInputs.apoapsis, + commonInputs.inclination, + commonInputs.longitudeOfAscendingNode, + commonInputs.argumentOfPeriapsis, + commonInputs.planet + ); + + meanAnomalyInput.value = orbitalCoordinates.meanAnomaly.toFixed(4); + eccentricAnomalyInput.value = orbitalCoordinates.eccentricAnomaly.toFixed(4); + positionXInput.value = orbitalCoordinates.position[0][0].toFixed(2); + positionYInput.value = orbitalCoordinates.position[1][0].toFixed(2); + positionZInput.value = orbitalCoordinates.position[2][0].toFixed(2); + latitudeInput.value = (orbitalCoordinates.latitude * 180 / Math.PI).toFixed(6); + longitudeInput.value = (orbitalCoordinates.longitude * 180 / Math.PI).toFixed(6); + longitudeOverPlanetInput.value = (orbitalCoordinates.longitudeOverPlanet * 180 / Math.PI).toFixed(6); +}); + +simplePlaneChangeButton.addEventListener("click", _ => { + const commonInputs = getCommonInputs(); + const orbitalCoordinates = getOrbitalCoordinates( + commonInputs.timeElapsed, + commonInputs.timeToPeriapsis, + commonInputs.periapsis, + commonInputs.apoapsis, + commonInputs.inclination, + commonInputs.longitudeOfAscendingNode, + commonInputs.argumentOfPeriapsis, + commonInputs.planet + ); + + const targetInclination = parseFloat(targetInclinationInput.value) * Math.PI / 180.0; + const targetLongitudeOfAscendingNode = parseFloat(targetLANInput.value) * Math.PI / 180.0; + const manoeuvres = calculateSimplePlaneChange(orbitalCoordinates, targetInclination, targetLongitudeOfAscendingNode, circularizeCheckbox.checked); + manoeuvres.sort((a, b) => a.totalAcceleration - b.totalAcceleration); + manoeuvres.forEach((manoeuvre, index) => { + simplePlaneChangeTimes[index].value = manoeuvre.time.toFixed(0); + simplePlaneChangeProgrades[index].value = manoeuvre.progradeAcceleration.toFixed(1); + simplePlaneChangeRadials[index].value = manoeuvre.radialAcceleration.toFixed(1); + simplePlaneChangeNormals[index].value = manoeuvre.normalAcceleration.toFixed(1); + simplePlaneChangeTotals[index].value = manoeuvre.totalAcceleration.toFixed(1); + }); +}); + +selectCalculationType(); \ No newline at end of file