diff --git a/index.html b/index.html index 2dfe660..4f8e682 100644 --- a/index.html +++ b/index.html @@ -3,209 +3,23 @@ + 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:

- - - - - - - - - - - - - - - - - - - - - -
- -
- +

Current time in game:

+
+

Time to periapsis:

+
+

Planet:

+
+

Orbital parameters:

+
+

What to calculate:

+
+
diff --git a/package-lock.json b/package-lock.json index d48d2bf..39e9e2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "kerbal-calculations", "version": "0.0.0", "devDependencies": { + "typescript": "^6.0.2", "vite": "^7.3.1" } }, @@ -912,9 +913,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -1025,6 +1026,20 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "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 fab0842..aebc5a2 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "devDependencies": { + "typescript": "^6.0.2", "vite": "^7.3.1" } } diff --git a/src/calculations/constants.ts b/src/calculations/constants.ts index c910a57..d35a6d7 100644 --- a/src/calculations/constants.ts +++ b/src/calculations/constants.ts @@ -1,63 +1,106 @@ -export interface Planet { +export interface Body { + planetName: string, + type: "star" | "planet" | "moon", radius: number, gravitationalParameter: number, rotationPeriod: number, sphereOfInfluence: number, + closestSafeDistance: number, initialMeridianLongitude: number } -export const Kerbol: Planet = { +export const Kerbol: Body = { + planetName: "Kerbol", + type: "star", radius: 261600000, gravitationalParameter: 1.1723328e18, rotationPeriod: 432000, sphereOfInfluence: 1e99, + closestSafeDistance: 261600000 + 600000, initialMeridianLongitude: 0 }; -export const Moho: Planet = { +export const Moho: Body = { + planetName: "Moho", + type: "planet", radius: 250000, gravitationalParameter: 1.6860938e11, rotationPeriod: 1210000, sphereOfInfluence: 9646663, + closestSafeDistance: 257000, initialMeridianLongitude: 0 }; -export const Eve: Planet = { +export const Eve: Body = { + planetName: "Eve", + type: "planet", radius: 700000, gravitationalParameter: 8.1717302e12, rotationPeriod: 80500, sphereOfInfluence: 85109365, + closestSafeDistance: 790000, initialMeridianLongitude: 0 }; -export const Gilly: Planet = { +export const Gilly: Body = { + planetName: "Gilly", + type: "moon", radius: 13000, gravitationalParameter: 8289449.8, rotationPeriod: 28255, sphereOfInfluence: 126123.27, + closestSafeDistance: 19400, initialMeridianLongitude: 0.0859373 }; -export const Kerbin: Planet = { +export const Kerbin: Body = { + planetName: "Kerbin", + type: "planet", radius: 600000, gravitationalParameter: 3.5316000e12, rotationPeriod: 21549.425, sphereOfInfluence: 84159286, + closestSafeDistance: 670000, initialMeridianLongitude: 1.571261023 }; -export const Mun: Planet = { +export const Mun: Body = { + planetName: "Mun", + type: "moon", radius: 200000, gravitationalParameter: 6.5138398e10, rotationPeriod: 138984.38, sphereOfInfluence: 2429559.1, + closestSafeDistance: 207500, initialMeridianLongitude: 4.0145103174219114 }; -export const Minmus: Planet = { +export const Minmus: Body = { + planetName: "Minmus", + type: "moon", radius: 60000, gravitationalParameter: 1.7658000e9, rotationPeriod: 40400, sphereOfInfluence: 2247428.4, + closestSafeDistance: 66000, initialMeridianLongitude: 4.014486824 }; + +export const PlanetList = new Map([ + [Kerbol.planetName, Kerbol], + [Moho.planetName, Moho], + [Eve.planetName, Eve], + [Gilly.planetName, Gilly], + [Kerbin.planetName, Kerbin], + [Mun.planetName, Mun], + [Minmus.planetName, Minmus] +]); + +export function getPlanetByName(name: string): Body { + let foundBody = PlanetList.get(name); + if (!foundBody) { + foundBody = Kerbin; + } + + return foundBody; +} \ No newline at end of file diff --git a/src/calculations/orbit-calculations.ts b/src/calculations/orbit-calculations.ts index b937130..46f6a79 100644 --- a/src/calculations/orbit-calculations.ts +++ b/src/calculations/orbit-calculations.ts @@ -1,259 +1,611 @@ -import type { Planet } from "./constants"; -import { getVectorMagnitude, matrixMultiply, matrixTranspose, multiplyMatrixWithScalar, normalizeVector, subtractVector, vectorCrossProduct, vectorDotProduct } from "./mathematics"; +import type { Body } from "./constants"; +import { addVector, getVectorMagnitude, multiplyMatrixWithScalar, normalizeVector, subtractVector, vectorCrossProduct, vectorDotProduct } from "./mathematics"; -export interface Axes { - semiMajor: number, - semiMinor: number, - linearEccentricity: number, - eccentricity: number +export interface Orbit { + semiLatusRectum: number, + eccentricity: number, + coordinateAxes: [number[][], number[][], number[][]] } -export interface OrbitalElementRotations { - argumentOfPeriapsisRotation: number[][], - inclinationRotation: number[][], - longitudeOfAscendingNodeRotation: number[][], - transformationOutOfPlane: number[][], - transformationIntoPlane: number[][] +export interface LocalVectors { + prograde: number[][], + radial: number[][], + normal: number[][] } export interface OrbitalCoordinates { + orbit: Orbit, meanAnomaly: number, - orbitalPeriod: number, eccentricAnomaly: number, - position: number[][], - latitude: number, - longitude: number, - longitudeOverPlanet: number, - axes: Axes, - orbitalRotations: OrbitalElementRotations, - currentTime: number, - planet: Planet + trueAnomaly: number, + position: number[][] } 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 + progradeDeltaV: number, + radialDeltaV: number, + normalDeltaV: number, + totalDeltaV: number } export interface Transfer { - originalPlaneCoordinateAxes: [number[][], number[][], number[][]], - transferPlaneCoordinateAxes: [number[][], number[][], number[][]], - targetPlaneCoordinateAxes: [number[][], number[][], number[][]], + transferOrbit: Orbit, + firstManoeuvre: Manoeuvre, + secondManoeuvre: Manoeuvre, + farthestPointDistance: number, + closestPointDistance: number, +} +export class LambertSolutions { + // 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 + // https://en.wikipedia.org/wiki/Lambert%27s_problem#Parametrization_of_the_transfer_trajectories + normalVector: number[][]; + positionOne: number[][]; + positionTwo: number[][]; + positionOneMagnitude: number; + positionTwoMagnitude: number; + + multiplierSemiLatusRectum: number; + firstTermSemiLatusRectum: number; + gammaMultiplierSemiLatusRectum: number; + + firstTermEccentricity: number[][]; + secondTermEccentricity: number[][]; + + startingLocalVectors: LocalVectors; + startingVelocity: number[][]; + goalLocalVectors: LocalVectors; + goalVelocity: number[][]; + + extremalGamma: number; + parabolaGamma: number; + + body: Body; + + constructor(startingOrbit: Orbit, startingTrueAnomaly: number, goalOrbit: Orbit, goalTrueAnomaly: number, body: Body, backwards?: boolean) { + this.body = body; + + let startingRadius = startingOrbit.semiLatusRectum / (1 + startingOrbit.eccentricity * Math.cos(startingTrueAnomaly)); + let startingLocalX = startingRadius * Math.cos(startingTrueAnomaly); + let startingLocalY = startingRadius * Math.sin(startingTrueAnomaly); + + let goalRadius = goalOrbit.semiLatusRectum / (1 + goalOrbit.eccentricity * Math.cos(goalTrueAnomaly)); + let goalLocalX = goalRadius * Math.cos(goalTrueAnomaly); + let goalLocalY = goalRadius * Math.sin(goalTrueAnomaly); + + this.positionOne = addVector( + multiplyMatrixWithScalar(startingLocalX, startingOrbit.coordinateAxes[0]), + multiplyMatrixWithScalar(startingLocalY, startingOrbit.coordinateAxes[1]) + ); + + this.positionTwo = addVector( + multiplyMatrixWithScalar(goalLocalX, goalOrbit.coordinateAxes[0]), + multiplyMatrixWithScalar(goalLocalY, goalOrbit.coordinateAxes[1]) + ); + + // First, find the normal vector + let crossProduct = vectorCrossProduct(this.positionOne, this.positionTwo); + this.normalVector = normalizeVector(crossProduct); + if (backwards) { + this.normalVector = multiplyMatrixWithScalar(-1, this.normalVector); + } + + this.gammaMultiplierSemiLatusRectum = vectorDotProduct(this.normalVector, crossProduct); + + this.positionOneMagnitude = getVectorMagnitude(this.positionOne); + this.positionTwoMagnitude = getVectorMagnitude(this.positionTwo); + let vectorDifference = subtractVector(this.positionTwo, this.positionOne); + + let differenceMagnitudeSquared = getVectorMagnitude(vectorDifference)**2; + this.multiplierSemiLatusRectum = (this.positionOneMagnitude + this.positionTwoMagnitude) / differenceMagnitudeSquared; + this.firstTermSemiLatusRectum = this.positionOneMagnitude*this.positionTwoMagnitude - vectorDotProduct(this.positionOne, this.positionTwo); + + this.firstTermEccentricity = multiplyMatrixWithScalar((this.positionOneMagnitude - this.positionTwoMagnitude) / differenceMagnitudeSquared, vectorDifference); + this.secondTermEccentricity = vectorCrossProduct(multiplyMatrixWithScalar((this.positionOneMagnitude + this.positionTwoMagnitude) / differenceMagnitudeSquared, this.normalVector), vectorDifference); + + // Calculate starting and goal velocities + const startingSpeed = getSpeed(startingTrueAnomaly, startingOrbit, body); + this.startingLocalVectors = getLocalVectors(startingTrueAnomaly, startingOrbit); + this.startingVelocity = multiplyMatrixWithScalar(startingSpeed, this.startingLocalVectors.prograde); + + const goalSpeed = getSpeed(goalTrueAnomaly, goalOrbit, body); + this.goalLocalVectors = getLocalVectors(goalTrueAnomaly, goalOrbit); + this.goalVelocity = multiplyMatrixWithScalar(goalSpeed, this.goalLocalVectors.prograde); + + this.extremalGamma = -(this.positionOneMagnitude*this.positionTwoMagnitude - vectorDotProduct(this.positionOne, this.positionTwo)) / vectorDotProduct(this.normalVector, (crossProduct)); + this.parabolaGamma = Math.sqrt(2*this.positionOneMagnitude*this.positionTwoMagnitude - vectorDotProduct(this.positionOne, this.positionTwo)) / getVectorMagnitude(addVector(this.positionOne, this.positionTwo)); + } + + getTransfer(gamma: number): Transfer { + let semiLatusRectum = this.multiplierSemiLatusRectum * (this.firstTermSemiLatusRectum + gamma * this.gammaMultiplierSemiLatusRectum); + let eccentricityVector = subtractVector(this.firstTermEccentricity, multiplyMatrixWithScalar(gamma, this.secondTermEccentricity)); + + let eccentricity = getVectorMagnitude(eccentricityVector); + + // If the eccentrity is near zero, the orbit is near circular, and the choice of localX is pretty much irrelevant + let localX; + if (eccentricity < 0.0001) { + localX = normalizeVector(this.positionOne); + } else { + localX = multiplyMatrixWithScalar(1 / eccentricity, eccentricityVector); + } + let localY = normalizeVector(vectorCrossProduct(this.normalVector, localX)); + + let transferOrbit: Orbit = { + semiLatusRectum: semiLatusRectum, + eccentricity: eccentricity, + coordinateAxes: [localX, localY, this.normalVector] + }; + + let transferStartTrueAnomaly = Math.atan2(vectorDotProduct(this.positionOne, localY), vectorDotProduct(this.positionOne, localX)); + let transferGoalTrueAnomaly = Math.atan2(vectorDotProduct(this.positionTwo, localY), vectorDotProduct(this.positionTwo, localX)); + + while (transferGoalTrueAnomaly < transferStartTrueAnomaly) { + transferGoalTrueAnomaly += 2 * Math.PI; + } + + if (transferGoalTrueAnomaly > transferStartTrueAnomaly + 2 * Math.PI) { + transferGoalTrueAnomaly -= 2 * Math.PI; + }; + + while (transferStartTrueAnomaly < -Math.PI) { + transferStartTrueAnomaly += 2 * Math.PI; + transferGoalTrueAnomaly += 2 * Math.PI; + } + + while (transferStartTrueAnomaly >= Math.PI) { + transferStartTrueAnomaly -= 2 * Math.PI; + transferGoalTrueAnomaly -= 2 * Math.PI; + } + + let closestPoint; + if (transferStartTrueAnomaly < 0 && transferGoalTrueAnomaly >= 0) { + closestPoint = semiLatusRectum / (1 + eccentricity); + } else { + closestPoint = Math.min(this.positionOneMagnitude, this.positionTwoMagnitude); + } + + let farthestPoint; + if (transferStartTrueAnomaly < Math.PI && transferGoalTrueAnomaly >= Math.PI) { + if (eccentricity < 1) { + farthestPoint = semiLatusRectum / (1 - eccentricity); + } else { + farthestPoint = 1e200; + } + } else { + farthestPoint = Math.max(this.positionOneMagnitude, this.positionTwoMagnitude); + } + + const transferStartSpeed = getSpeed(transferStartTrueAnomaly, transferOrbit, this.body); + const transferStartVectors = getLocalVectors(transferStartTrueAnomaly, transferOrbit); + const transferStartVelocity = multiplyMatrixWithScalar(transferStartSpeed, transferStartVectors.prograde); + + const transferStartVelocityChange = subtractVector(transferStartVelocity, this.startingVelocity); + const transferStartTotalDeltaV = getVectorMagnitude(transferStartVelocityChange); + + const transferStartPrograde = vectorDotProduct(transferStartVelocityChange, this.startingLocalVectors.prograde); + const transferStartNormal = vectorDotProduct(transferStartVelocityChange, this.startingLocalVectors.normal); + const transferStartRadial = vectorDotProduct(transferStartVelocityChange, this.startingLocalVectors.radial); + + const startManoeuvre: Manoeuvre = { + time: 0, + progradeDeltaV: transferStartPrograde, + radialDeltaV: transferStartRadial, + normalDeltaV: transferStartNormal, + totalDeltaV: transferStartTotalDeltaV + }; + + const transferGoalSpeed = getSpeed(transferGoalTrueAnomaly, transferOrbit, this.body); + const transferGoalVectors = getLocalVectors(transferGoalTrueAnomaly, transferOrbit); + const transferGoalVelocity = multiplyMatrixWithScalar(transferGoalSpeed, transferGoalVectors.prograde); + + const transferGoalVelocityChange = subtractVector(this.goalVelocity, transferGoalVelocity); + const transferGoalTotalDeltaV = getVectorMagnitude(transferGoalVelocityChange); + + const transferGoalPrograde = vectorDotProduct(transferGoalVelocityChange, transferGoalVectors.prograde); + const transferGoalRadial = vectorDotProduct(transferGoalVelocityChange, transferGoalVectors.radial); + const transferGoalNormal = vectorDotProduct(transferGoalVelocityChange, transferGoalVectors.normal); + + const timeToTransfer = getTimeBetweenTrueAnomalies(transferStartTrueAnomaly, transferGoalTrueAnomaly, transferOrbit, this.body); + const goalManoeuvre: Manoeuvre = { + time: timeToTransfer, + progradeDeltaV: transferGoalPrograde, + radialDeltaV: transferGoalRadial, + normalDeltaV: transferGoalNormal, + totalDeltaV: transferGoalTotalDeltaV + }; + + return { + transferOrbit: transferOrbit, + firstManoeuvre: startManoeuvre, + secondManoeuvre: goalManoeuvre, + closestPointDistance: closestPoint, + farthestPointDistance: farthestPoint + } + } +} + +const ZeroManoeuvre: Manoeuvre = { + time: 0, + progradeDeltaV: 0, + radialDeltaV: 0, + normalDeltaV: 0, + totalDeltaV: 0 +} + +export interface SimplePlaneChange { firstManoeuvre: Manoeuvre, secondManoeuvre: Manoeuvre } -export interface LambertFunction { - (lambda: number): Transfer +export function getCoordinateAxes(inclination: number, longitudeOfAscendingNode: number, argumentOfPeriapsis: number): [number[][], number[][], number[][]] { + const xAxis = [ + [Math.cos(longitudeOfAscendingNode)*Math.cos(argumentOfPeriapsis) - Math.sin(longitudeOfAscendingNode)*Math.cos(inclination)*Math.sin(argumentOfPeriapsis)], + [Math.sin(longitudeOfAscendingNode)*Math.cos(argumentOfPeriapsis) + Math.cos(longitudeOfAscendingNode)*Math.cos(inclination)*Math.sin(argumentOfPeriapsis)], + [Math.sin(inclination)*Math.sin(argumentOfPeriapsis)] + ]; + + const yAxis = [ + [-Math.cos(longitudeOfAscendingNode)*Math.sin(argumentOfPeriapsis) - Math.sin(longitudeOfAscendingNode)*Math.cos(inclination)*Math.cos(argumentOfPeriapsis)], + [-Math.sin(longitudeOfAscendingNode)*Math.sin(argumentOfPeriapsis) + Math.cos(longitudeOfAscendingNode)*Math.cos(inclination)*Math.cos(argumentOfPeriapsis)], + [Math.sin(inclination)*Math.cos(argumentOfPeriapsis)] + ]; + + const zAxis = [ + [Math.sin(longitudeOfAscendingNode)*Math.sin(inclination)], + [-Math.cos(longitudeOfAscendingNode)*Math.sin(inclination)], + [Math.cos(inclination)] + ]; + + return [xAxis, yAxis, zAxis]; } -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); +export function getOrbit(periapsis: number, apoapsis: number, inclination: number, longitudeOfAscendingNode: number, argumentOfPeriapsis: number): Orbit { + const semiMajor = (periapsis + apoapsis) / 2; + const linearEccentricity = semiMajor - periapsis; const eccentricity = linearEccentricity / semiMajor; - const semiMinor = Math.sqrt(semiMajor**2 - linearEccentricity**2); return { - semiMajor: semiMajor, - semiMinor: semiMinor, - linearEccentricity: linearEccentricity, - eccentricity: eccentricity + semiLatusRectum: semiMajor * (1 - eccentricity**2), + eccentricity: eccentricity, + coordinateAxes: getCoordinateAxes(inclination, longitudeOfAscendingNode, argumentOfPeriapsis) }; -} +}; -export function getOrbitalPeriod(axes: Axes, gravitationalParameter: number): number { - return 2 * Math.PI * Math.sqrt(axes.semiMajor**3 / gravitationalParameter); -} +export function getOrbitFromEccentricity(periapsis: number, eccentricity: number, inclination: number, longitudeOfAscendingNode: number, argumentOfPeriapsis: number): Orbit { + return { + semiLatusRectum: periapsis * (eccentricity + 1), + eccentricity: eccentricity, + coordinateAxes: getCoordinateAxes(inclination, longitudeOfAscendingNode, argumentOfPeriapsis) + } +}; -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 getOrbitalCoordinates(timeToPeriapsis: number, orbit: Orbit, planet: Body) { + const meanAnomaly = -Math.sqrt(planet.gravitationalParameter / Math.abs(orbit.semiLatusRectum / (orbit.eccentricity**2 - 1))**3) * timeToPeriapsis; + var eccentricAnomaly; + var trueAnomaly; -export function getMeanAnomalyFromEccentricAnomaly(eccentricAnomaly: number, eccentricity: number): number { - return eccentricAnomaly - eccentricity * Math.sin(eccentricAnomaly); -} + if (Math.abs(orbit.eccentricity - 1) < 0.0001) { + // Parabolic trajectory, Barker's equation + const A = 3 * meanAnomaly / Math.sqrt(8); + const B = Math.pow(A + Math.sqrt(A**2 + 1), 1/3); + trueAnomaly = 2 * Math.atan(B - 1 / B); + eccentricAnomaly = trueAnomaly; + } else { + // Elliptical or hyperbolic orbit, use Newton's method to find eccentric anomaly + var keplerEquation; + var keplerEquationDerivative; + eccentricAnomaly = meanAnomaly; -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); + if (orbit.eccentricity < 1) { + keplerEquation = (guess: number) => guess - orbit.eccentricity * Math.sin(guess) - meanAnomaly; + keplerEquationDerivative = (guess: number) => 1 - orbit.eccentricity * Math.cos(guess); + } else { + keplerEquation = (guess: number) => orbit.eccentricity * Math.sinh(guess) - guess - meanAnomaly; + keplerEquationDerivative = (guess: number) => orbit.eccentricity * Math.cosh(guess) - 1; + } + + while (Math.abs(keplerEquation(eccentricAnomaly)) > 0.000001) { + eccentricAnomaly = eccentricAnomaly - keplerEquation(eccentricAnomaly) / keplerEquationDerivative(eccentricAnomaly); + } + + if (orbit.eccentricity < 1) { + trueAnomaly = 2*Math.atan(Math.sqrt((1 + orbit.eccentricity) / (1 - orbit.eccentricity)) * Math.tan(eccentricAnomaly / 2)); + } else { + trueAnomaly = 2*Math.atan(Math.sqrt((orbit.eccentricity + 1) / (orbit.eccentricity - 1)) * Math.tanh(eccentricAnomaly / 2)); + } } - 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); + const radius = orbit.semiLatusRectum / (1 + orbit.eccentricity * Math.cos(trueAnomaly)); + const localX = radius * Math.cos(trueAnomaly); + const localY = radius * Math.sin(trueAnomaly); + const globalPosition = addVector(multiplyMatrixWithScalar(localX, orbit.coordinateAxes[0]), multiplyMatrixWithScalar(localY, orbit.coordinateAxes[1])); return { - argumentOfPeriapsisRotation: argumentOfPeriapsisRotation, - inclinationRotation: inclinationRotation, - longitudeOfAscendingNodeRotation: longitudeOfAscendingNodeRotation, - transformationOutOfPlane: transformationOutOfPlane, - transformationIntoPlane: transformationIntoPlane - }; + orbit: orbit, + meanAnomaly: meanAnomaly, + eccentricAnomaly: eccentricAnomaly, + trueAnomaly: trueAnomaly, + position: globalPosition, + } } -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)], +export function getTimeBetweenTrueAnomalies(startingTrueAnomaly: number, endingTrueAnomaly: number, orbit: Orbit, planet: Body): number { + if (Math.abs(orbit.eccentricity - 1) < 0.00001) { + // Parabola. Solve using Barker's equation + const startingD = Math.tan(startingTrueAnomaly / 2); + const endingD = Math.tan(endingTrueAnomaly / 2); + + const startingTime = Math.sqrt(orbit.semiLatusRectum**3 / planet.gravitationalParameter) * (startingD + startingD**3/3) / 2; + const endingTime = Math.sqrt(orbit.semiLatusRectum**3 / planet.gravitationalParameter) * (endingD + endingD**3/3) / 2; + + return endingTime - startingTime; + } else { + var startingMeanAnomaly; + var endingMeanAnomaly; + if (orbit.eccentricity < 1) { + // Ellipse + const startingEccentricAnomaly = Math.atan2( + Math.sqrt(1 - orbit.eccentricity**2) * Math.sin(startingTrueAnomaly), + orbit.eccentricity + Math.cos(startingTrueAnomaly) + ); + + const endingEccentricAnomaly = Math.atan2( + Math.sqrt(1 - orbit.eccentricity**2) * Math.sin(endingTrueAnomaly), + orbit.eccentricity + Math.cos(endingTrueAnomaly) + ); + + startingMeanAnomaly = startingEccentricAnomaly - orbit.eccentricity * Math.sin(startingEccentricAnomaly); + endingMeanAnomaly = endingEccentricAnomaly - orbit.eccentricity * Math.sin(endingEccentricAnomaly); + + while (endingMeanAnomaly < startingMeanAnomaly) { + endingMeanAnomaly += 2*Math.PI; + } + } else { + const startingEccentricAnomaly = 2*Math.atanh(Math.sqrt((orbit.eccentricity - 1)/(orbit.eccentricity + 1)) * Math.tan(startingTrueAnomaly / 2)); + const endingEccentricAnomaly = 2*Math.atanh(Math.sqrt((orbit.eccentricity - 1)/(orbit.eccentricity + 1)) * Math.tan(endingTrueAnomaly / 2)); + + startingMeanAnomaly = orbit.eccentricity * Math.sinh(startingEccentricAnomaly) - startingEccentricAnomaly; + endingMeanAnomaly = orbit.eccentricity * Math.sinh(endingEccentricAnomaly) - endingEccentricAnomaly; + } + + const startingTime = Math.sqrt(orbit.semiLatusRectum**3 / (planet.gravitationalParameter * Math.abs(1 - orbit.eccentricity**2)**3)) * startingMeanAnomaly; + const endingTime = Math.sqrt(orbit.semiLatusRectum**3 / (planet.gravitationalParameter * Math.abs(1 - orbit.eccentricity**2)**3)) * endingMeanAnomaly; + + return endingTime - startingTime; + } +} + +export function getLocalVectors(trueAnomaly: number, orbit: Orbit): LocalVectors { + const changeInX = -orbit.semiLatusRectum * Math.sin(trueAnomaly) / (1 + orbit.eccentricity * Math.cos(trueAnomaly))**2; + const changeInY = orbit.semiLatusRectum * (orbit.eccentricity + Math.cos(trueAnomaly)) / (1 + orbit.eccentricity * Math.cos(trueAnomaly))**2; + const localHeading = Math.atan2(changeInY, changeInX); + const localPrograde = [ + [Math.cos(localHeading)], + [Math.sin(localHeading)], [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); + const localRadial = [ + [Math.sin(localHeading)], + [-Math.cos(localHeading)], + [0] + ]; + + const globalPrograde = addVector( + multiplyMatrixWithScalar(localPrograde[0][0], orbit.coordinateAxes[0]), + multiplyMatrixWithScalar(localPrograde[1][0], orbit.coordinateAxes[1]) + ) + + const globalRadial = addVector( + multiplyMatrixWithScalar(localRadial[0][0], orbit.coordinateAxes[0]), + multiplyMatrixWithScalar(localRadial[1][0], orbit.coordinateAxes[1]) + ); return { - meanAnomaly: meanAnomaly, - orbitalPeriod: orbitalPeriod, - eccentricAnomaly: eccentricAnomaly, - position: globalPosition, - latitude: latitude, - longitude: longitude, - longitudeOverPlanet: longitudeOverPlanet, - axes: axes, - orbitalRotations: orbitalRotations, - currentTime: currentTime, - planet: planet - }; + prograde: globalPrograde, + radial: globalRadial, + normal: orbit.coordinateAxes[2] + } } -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]]))); +export function getSpeed(trueAnomaly: number, orbit: Orbit, planet: Body): number { + return Math.sqrt(planet.gravitationalParameter * (1 + 2 * orbit.eccentricity * Math.cos(trueAnomaly) + orbit.eccentricity**2) / orbit.semiLatusRectum); +} - // Check if target plane is equal to current plane - if (1 - Math.abs(vectorDotProduct(targetPlaneNormalVector, [[0], [0], [1]])) < 0.0001) { - return [zeroManoeuvre, zeroManoeuvre]; - } +export function calculateSimplePlaneChange(coordinates: OrbitalCoordinates, planet: Body, targetInclination: number, targetLongitudeOfAscendingNode: number, circularizeOrbit: boolean): SimplePlaneChange { + const otherPlaneNormal = [ + [Math.sin(targetLongitudeOfAscendingNode)*Math.sin(targetInclination)], + [-Math.cos(targetLongitudeOfAscendingNode)*Math.sin(targetInclination)], + [Math.cos(targetInclination)] + ]; - // 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; + var planesIntersection = vectorCrossProduct(otherPlaneNormal, coordinates.orbit.coordinateAxes[2]); + if (getVectorMagnitude(planesIntersection) < 0.0001) { + return { + firstManoeuvre: ZeroManoeuvre, + secondManoeuvre: ZeroManoeuvre } + }; - const manoeuvreTime = (meanAnomaly - coordinates.meanAnomaly) * coordinates.orbitalPeriod / (2 * Math.PI) + coordinates.currentTime; + planesIntersection = normalizeVector(planesIntersection); - const progradeVector = normalizeVector([ - [-coordinates.axes.semiMajor * Math.sin(eccentricAnomaly)], - [coordinates.axes.semiMinor * Math.cos(eccentricAnomaly)], - [0] - ]); + // Find true anomalies of crossings + const intersectionTrueAnomaly = Math.atan2(vectorDotProduct(planesIntersection, coordinates.orbit.coordinateAxes[1]), vectorDotProduct(planesIntersection, coordinates.orbit.coordinateAxes[0])); + var firstManoeuvre: Manoeuvre = ZeroManoeuvre; + var secondManoeuvre: Manoeuvre = ZeroManoeuvre; - const normalVector = [ - [0], - [0], - [1] - ]; + var intersections = [intersectionTrueAnomaly, intersectionTrueAnomaly + Math.PI]; + intersections = intersections.map(anomaly => (anomaly - coordinates.trueAnomaly + 10 * Math.PI) % (2 * Math.PI) + coordinates.trueAnomaly).sort(); - const radialVector = vectorCrossProduct(normalVector, progradeVector); + intersections.forEach((trueAnomaly, index) => { + const speed = getSpeed(trueAnomaly, coordinates.orbit, planet); + const localVectors = getLocalVectors(trueAnomaly, coordinates.orbit); - 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, localVectors.prograde); + var excess: number[][]; + if (circularizeOrbit) { + const radius = coordinates.orbit.semiLatusRectum / (1 + coordinates.orbit.eccentricity * Math.cos(trueAnomaly)); + const targetSpeed = Math.sqrt(planet.gravitationalParameter / radius); - const velocity = multiplyMatrixWithScalar(speed, progradeVector); - var velocityChange: number[][]; - var deltaV: number; + const radiusVector = addVector( + multiplyMatrixWithScalar(Math.cos(trueAnomaly), coordinates.orbit.coordinateAxes[0]), + multiplyMatrixWithScalar(Math.sin(trueAnomaly), coordinates.orbit.coordinateAxes[1]) + ); - if (!circularizeOrbit) { - deltaV = vectorDotProduct(velocity, multiplyMatrixWithScalar(-1, targetPlaneNormalVector)); - velocityChange = multiplyMatrixWithScalar(deltaV, targetPlaneNormalVector); - deltaV = Math.abs(deltaV); + const velocityDirection = normalizeVector(vectorCrossProduct(otherPlaneNormal, radiusVector)); + const targetVelocity = multiplyMatrixWithScalar(targetSpeed, velocityDirection); + + excess = subtractVector(velocity, targetVelocity); } 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); + excess = multiplyMatrixWithScalar(vectorDotProduct(velocity, otherPlaneNormal), otherPlaneNormal); } - const progradeChange = vectorDotProduct(velocityChange, progradeVector); - const radialChange = -vectorDotProduct(velocityChange, radialVector); - const normalChange = vectorDotProduct(velocityChange, normalVector); + const totalChange = getVectorMagnitude(excess); + const progradeChange = -vectorDotProduct(excess, localVectors.prograde); + const radialChange = -vectorDotProduct(excess, localVectors.radial); + const normalChange = -vectorDotProduct(excess, localVectors.normal); - manoeuvres.push({ - time: manoeuvreTime, - progradeAcceleration: progradeChange, - radialAcceleration: radialChange, - normalAcceleration: normalChange, - totalAcceleration: deltaV - }); + const timeUntil = getTimeBetweenTrueAnomalies(coordinates.trueAnomaly, trueAnomaly, coordinates.orbit, planet); + + const manoeuvre: Manoeuvre = { + time: timeUntil, + progradeDeltaV: progradeChange, + radialDeltaV: radialChange, + normalDeltaV: normalChange, + totalDeltaV: totalChange + } + + if (index == 0) { + firstManoeuvre = manoeuvre; + } else { + secondManoeuvre = manoeuvre; + } }); - return [manoeuvres[0], manoeuvres[1]]; + return { + firstManoeuvre: firstManoeuvre, + secondManoeuvre: secondManoeuvre + } +} + +export function findCheapestLambertSolution(lambertSolutions: LambertSolutions): Transfer | null { + // Test a bunch of gamma values + let stepLength = (lambertSolutions.parabolaGamma - lambertSolutions.extremalGamma) / 100; + let bestTransfer = null; + let bestDeltaV = null; + + for (var i = 1; i < 200; i++) { + let gamma = lambertSolutions.extremalGamma + i * stepLength; + let transfer = lambertSolutions.getTransfer(gamma); + + if (transfer.closestPointDistance < lambertSolutions.body.closestSafeDistance) { + continue; + } + + if (transfer.farthestPointDistance > lambertSolutions.body.sphereOfInfluence) { + continue; + } + + let totalDeltaV = transfer.firstManoeuvre.totalDeltaV + transfer.secondManoeuvre.totalDeltaV; + if (isNaN(totalDeltaV)) { + continue; + } + + if (bestDeltaV == null || totalDeltaV < bestDeltaV) { + bestDeltaV = totalDeltaV; + bestTransfer = transfer; + } + } + + return bestTransfer; +} + +export function findCheapestTransfer(startingSituation: OrbitalCoordinates, targetOrbit: Orbit, body: Body, progressCallback?: (anomaliesChecked: number, totalAnomalies: number, currentBestDeltaV: number | null, currentBestTransfer: Transfer | null) => void): Transfer | null { + // First, create a set of starting true anomalies + let startingTrueAnomalies = []; + + let stableOrbit = false; + if (startingSituation.orbit.eccentricity < 1) { + // We might still not be in a stable orbit, if the apoapsis is beyond the body's sphere of influence + let apoapsis = startingSituation.orbit.semiLatusRectum / (1 - startingSituation.orbit.eccentricity); + if (apoapsis < body.sphereOfInfluence) { + stableOrbit = true; + // If the orbit is stable and all that, just sample the true anomalies equally + } + } + + if (stableOrbit) { + for (var i = 0; i < 100; i++) { + startingTrueAnomalies.push(i * 2 * Math.PI / 100); + } + } else { + let finalAnomaly = Math.abs(Math.acos((startingSituation.orbit.semiLatusRectum - body.sphereOfInfluence) / (body.sphereOfInfluence * startingSituation.orbit.eccentricity))); + for (var i = 0; i < 100; i++) { + let step = (finalAnomaly - startingSituation.trueAnomaly) / 100; + startingTrueAnomalies.push(startingSituation.trueAnomaly + i * step); + } + } + + // Next, find a set of true anomalies to aim for in the target orbit + let endingTrueAnomalies = []; + let targetIsStable = false; + if (targetOrbit.eccentricity < 1) { + let targetApoapsis = targetOrbit.semiLatusRectum / (1 - targetOrbit.eccentricity); + if (targetApoapsis < body.sphereOfInfluence) { + targetIsStable = true; + } + } + + if (targetIsStable) { + for (var i = 0; i < 100; i++) { + endingTrueAnomalies.push(i * 2 * Math.PI / 100); + } + } else { + let finalAnomaly = Math.abs(Math.acos((targetOrbit.semiLatusRectum - body.sphereOfInfluence) / (body.sphereOfInfluence * targetOrbit.eccentricity))); + let step = 2 * finalAnomaly / 100; + for (var i = 0; i < 100; i++) { + endingTrueAnomalies.push(-finalAnomaly + i * step); + } + } + + let bestTransfer: Transfer | null = null; + let bestDeltaV: number | null = null; + + let totalAnomalies = startingTrueAnomalies.length * endingTrueAnomalies.length * 2; + let numberChecked = 0; + + startingTrueAnomalies.forEach(startingAnomaly => { + while (startingAnomaly < startingSituation.trueAnomaly) { + startingAnomaly += 2*Math.PI; + } + + let timeToAnomaly = getTimeBetweenTrueAnomalies(startingSituation.trueAnomaly, startingAnomaly, startingSituation.orbit, body); + + endingTrueAnomalies.forEach(endingAnomaly => { + [true, false].forEach(goBackwards => { + let lambertSolutions = new LambertSolutions(startingSituation.orbit, startingAnomaly, targetOrbit, endingAnomaly, body, goBackwards); + let currentBestTransfer = findCheapestLambertSolution(lambertSolutions); + if (currentBestTransfer) { + let totalDeltaV = currentBestTransfer.firstManoeuvre.totalDeltaV + currentBestTransfer.secondManoeuvre.totalDeltaV; + if (bestDeltaV == null || totalDeltaV < bestDeltaV) { + bestDeltaV = totalDeltaV; + bestTransfer = currentBestTransfer; + bestTransfer.firstManoeuvre.time += timeToAnomaly; + bestTransfer.secondManoeuvre.time += timeToAnomaly; + } + } + + if (progressCallback) { + numberChecked += 1; + progressCallback(numberChecked, totalAnomalies, bestDeltaV, bestTransfer); + } + }); + }) + }); + + return bestTransfer; } \ No newline at end of file diff --git a/src/gui/common.ts b/src/gui/common.ts new file mode 100644 index 0000000..525d2d8 --- /dev/null +++ b/src/gui/common.ts @@ -0,0 +1,87 @@ +import { getOrbit, getOrbitFromEccentricity, type Orbit } from "../calculations/orbit-calculations"; + +export interface OrbitalParameters { + periapsis: number; + eccentricChoice: "apoapsis" | "eccentricity"; + apoapsis: number; + eccentricity: number; + inclination: number; + longitudeOfAscendingNode: number; + argumentOfPeriapsis: number; +} + +export const DefaultOrbitalParameters: OrbitalParameters = { + periapsis: 100000, + eccentricChoice: "apoapsis", + apoapsis: 100000, + eccentricity: 0, + inclination: 0, + longitudeOfAscendingNode: 0, + argumentOfPeriapsis: 0 +} + +export function encodeOrbitalParameters(orbitalParameters: OrbitalParameters): string { + return JSON.stringify(orbitalParameters); +} + +export function decodeOrbitalParameters(jsonString: string): OrbitalParameters { + let parsedObject = JSON.parse(jsonString); + var orbitalParameters: OrbitalParameters; + if (parsedObject as OrbitalParameters === undefined) { + orbitalParameters = DefaultOrbitalParameters; + } else { + orbitalParameters = parsedObject; + } + + return orbitalParameters; +} + +export function createLabel(forId: string, labelText: string): HTMLLabelElement { + let label = document.createElement("label"); + label.setAttribute("for", forId); + label.appendChild(document.createTextNode(labelText)); + return label; +} + +export function createNumberInput(id: string, minimum: number, maximum?: number, size?: number): HTMLInputElement { + let numberInput = document.createElement("input"); + numberInput.setAttribute("id", id); + numberInput.setAttribute("type", "number"); + numberInput.setAttribute("min", minimum.toFixed(0)); + if (maximum !== undefined) { + numberInput.setAttribute("max", maximum.toFixed(0)); + } + + if (size !== undefined) { + numberInput.setAttribute("size", size.toFixed(0)); + } + + return numberInput; +} + +export function createDisabledInput(id: string) { + let input = document.createElement("input"); + input.setAttribute("id", id); + input.setAttribute("disabled", ""); + return input; +} + +export function getOrbitFromParameters(orbitalParameters: OrbitalParameters, planetRadius: number): Orbit { + if (orbitalParameters.eccentricChoice == "apoapsis") { + return getOrbit( + orbitalParameters.periapsis + planetRadius, + orbitalParameters.apoapsis + planetRadius, + orbitalParameters.inclination, + orbitalParameters.longitudeOfAscendingNode, + orbitalParameters.argumentOfPeriapsis + ) + } else { + return getOrbitFromEccentricity( + orbitalParameters.periapsis + planetRadius, + orbitalParameters.eccentricity, + orbitalParameters.inclination, + orbitalParameters.longitudeOfAscendingNode, + orbitalParameters.argumentOfPeriapsis + ); + } +} \ No newline at end of file diff --git a/src/gui/orbit.ts b/src/gui/orbit.ts new file mode 100644 index 0000000..01ec194 --- /dev/null +++ b/src/gui/orbit.ts @@ -0,0 +1,174 @@ +import { createLabel, createNumberInput, type OrbitalParameters, DefaultOrbitalParameters } from "./common"; + +export class OrbitalParametersGui { + listeners: ((newValue: OrbitalParameters) => void)[]; + orbitalParameters: OrbitalParameters; + + periapsisInput: HTMLInputElement; + apoapsisChoiceButton: HTMLInputElement; + eccentricityChoiceButton: HTMLInputElement; + apoapsisInput: HTMLInputElement; + eccentricityInput: HTMLInputElement; + inclinationInput: HTMLInputElement; + lanInput: HTMLInputElement; + aopInput: HTMLInputElement; + + constructor(htmlContainer: HTMLElement, startingValue?: OrbitalParameters, containerCreator?: () => HTMLElement) { + this.listeners = []; + + if (startingValue) { + this.orbitalParameters = structuredClone(startingValue); + } else { + this.orbitalParameters = structuredClone(DefaultOrbitalParameters); + } + + const addToParent = (toWrap: HTMLElement | HTMLElement[]) => { + var children = []; + if (toWrap instanceof HTMLElement) { + children = [toWrap]; + } else { + children = toWrap; + } + + let containerToAppendTo = htmlContainer; + if (containerCreator !== undefined) { + let childContainer = containerCreator(); + containerToAppendTo.appendChild(childContainer); + containerToAppendTo = childContainer; + } + + children.forEach(child => containerToAppendTo.appendChild(child)); + }; + + let periapsisId = crypto.randomUUID(); + this.periapsisInput = createNumberInput(periapsisId, 0); + + this.periapsisInput.addEventListener("change", () => { + let newValue = parseFloat(this.periapsisInput.value); + this.orbitalParameters.periapsis = newValue; + this.informListeners(this.orbitalParameters); + }); + + addToParent([createLabel(periapsisId, "Periapsis:"), this.periapsisInput]); + + let choiceId = crypto.randomUUID(); + let apoapsisChoiceId = crypto.randomUUID(); + let eccentricityChoiceId = crypto.randomUUID(); + + this.apoapsisChoiceButton = document.createElement("input"); + this.apoapsisChoiceButton.setAttribute("type", "radio"); + this.apoapsisChoiceButton.setAttribute("name", choiceId); + this.apoapsisChoiceButton.setAttribute("id", apoapsisChoiceId); + this.apoapsisChoiceButton.setAttribute("value", "apoapsis"); + if (this.orbitalParameters.eccentricChoice === "apoapsis") { + this.apoapsisChoiceButton.setAttribute("checked", ""); + } + + this.eccentricityChoiceButton = document.createElement("input"); + this.eccentricityChoiceButton.setAttribute("type", "radio"); + this.eccentricityChoiceButton.setAttribute("name", choiceId); + this.eccentricityChoiceButton.setAttribute("id", eccentricityChoiceId); + this.eccentricityChoiceButton.setAttribute("value", "eccentricity"); + if (this.orbitalParameters.eccentricChoice == "eccentricity") { + this.eccentricityChoiceButton.setAttribute("checked", ""); + }; + + addToParent([this.apoapsisChoiceButton, createLabel(apoapsisChoiceId, "Use apoapsis"), this.eccentricityChoiceButton, createLabel(eccentricityChoiceId, "Use eccentricity")]); + + let apoapsisId = crypto.randomUUID(); + this.apoapsisInput = createNumberInput(apoapsisId, 0); + if (this.orbitalParameters.eccentricChoice !== "apoapsis") { + this.apoapsisInput.setAttribute("disabled", "true"); + } + this.apoapsisInput.addEventListener("change", () => { + this.orbitalParameters.apoapsis = parseFloat(this.apoapsisInput.value); + this.informListeners(this.orbitalParameters); + }) + addToParent([createLabel(apoapsisId, "Apoapsis:"), this.apoapsisInput]); + + let eccentricityId = crypto.randomUUID(); + this.eccentricityInput = createNumberInput(eccentricityId, 0); + if (this.orbitalParameters.eccentricChoice !== "eccentricity") { + this.eccentricityInput.setAttribute("disabled", "true"); + } + this.eccentricityInput.addEventListener("change", () => { + this.orbitalParameters.eccentricity = parseFloat(this.eccentricityInput.value); + this.informListeners(this.orbitalParameters); + }); + addToParent([createLabel(eccentricityId, "Eccentricity:"), this.eccentricityInput]); + + this.apoapsisChoiceButton.addEventListener("change", () => { + this.orbitalParameters.eccentricChoice = "apoapsis"; + this.informListeners(this.orbitalParameters); + this.apoapsisInput.removeAttribute("disabled"); + this.eccentricityInput.setAttribute("disabled", "true"); + }) + + this.eccentricityChoiceButton.addEventListener("change", () => { + this.orbitalParameters.eccentricChoice = "eccentricity"; + this.informListeners(this.orbitalParameters); + this.apoapsisInput.setAttribute("disabled", "true"); + this.eccentricityInput.removeAttribute("disabled"); + }); + + let inclinationId = crypto.randomUUID(); + this.inclinationInput = createNumberInput(inclinationId, -360, 360); + this.inclinationInput.addEventListener("change", () => { + this.orbitalParameters.inclination = parseFloat(this.inclinationInput.value) * Math.PI / 180.0; + this.informListeners(this.orbitalParameters); + }); + addToParent([createLabel(inclinationId, "Inclination:"), this.inclinationInput]); + + let lanId = crypto.randomUUID(); + this.lanInput = createNumberInput(lanId, -360, 360); + this.lanInput.addEventListener("change", () => { + this.orbitalParameters.longitudeOfAscendingNode = parseFloat(this.lanInput.value) * Math.PI / 180.0; + this.informListeners(this.orbitalParameters); + }); + addToParent([createLabel(lanId, "Longitude of ascending node:"), this.lanInput]); + + let aopId = crypto.randomUUID(); + this.aopInput = createNumberInput(aopId, -360, 360); + this.aopInput.addEventListener("change", () => { + this.orbitalParameters.argumentOfPeriapsis = parseFloat(this.aopInput.value) * Math.PI / 180.0; + this.informListeners(this.orbitalParameters); + }); + addToParent([createLabel(aopId, "Argument of periapsis:"), this.aopInput]); + + this.populateValues(); + } + + addListener(listener: (newValue: OrbitalParameters) => void) { + this.listeners.push(listener); + } + + informListeners(newValue: OrbitalParameters) { + this.listeners.forEach(listener => listener(newValue)); + } + + setValues(values: OrbitalParameters) { + this.orbitalParameters = values; + this.populateValues(); + } + + populateValues() { + this.periapsisInput.setAttribute("value", this.orbitalParameters.periapsis.toString()); + this.apoapsisInput.setAttribute("value", this.orbitalParameters.apoapsis.toString()); + this.eccentricityInput.setAttribute("value", this.orbitalParameters.eccentricity.toString()); + this.inclinationInput.setAttribute("value", (this.orbitalParameters.inclination * 180 / Math.PI).toString()); + this.lanInput.setAttribute("value", (this.orbitalParameters.longitudeOfAscendingNode * 180 / Math.PI).toString()); + this.aopInput.setAttribute("value", (this.orbitalParameters.argumentOfPeriapsis * 180 / Math.PI).toString()); + + if (this.orbitalParameters.eccentricChoice == "apoapsis") { + this.apoapsisInput.removeAttribute("disabled"); + this.eccentricityInput.setAttribute("disabled", ""); + this.eccentricityChoiceButton.removeAttribute("checked"); + this.apoapsisChoiceButton.setAttribute("checked", ""); + } else if (this.orbitalParameters.eccentricChoice == "eccentricity") { + this.apoapsisInput.setAttribute("disabled", ""); + this.eccentricityInput.removeAttribute("disabled"); + this.apoapsisChoiceButton.removeAttribute("checked"); + this.eccentricityChoiceButton.setAttribute("checked", ""); + } + } +} \ No newline at end of file diff --git a/src/gui/planet.ts b/src/gui/planet.ts new file mode 100644 index 0000000..032772d --- /dev/null +++ b/src/gui/planet.ts @@ -0,0 +1,61 @@ +import { type Body, Kerbol, PlanetList } from "../calculations/constants"; + +export class PlanetGui { + listeners: ((newValue: Body) => void)[]; + + constructor(htmlContainer: HTMLElement, startingValue?: Body, containerCreator?: (body: Body) => HTMLElement) { + this.listeners = []; + + if (!startingValue) { + startingValue = Kerbol; + } + + const addToParent = (toWrap: HTMLElement | HTMLElement[], body: Body) => { + var children = []; + if (toWrap instanceof HTMLElement) { + children = [toWrap]; + } else { + children = toWrap; + } + + let containerToAppendTo = htmlContainer; + if (containerCreator !== undefined) { + let childContainer = containerCreator(body); + containerToAppendTo.appendChild(childContainer); + containerToAppendTo = childContainer; + } + + children.forEach(child => containerToAppendTo.appendChild(child)); + }; + + let planetChoiceName = crypto.randomUUID(); + + PlanetList.forEach(body => { + let radioButton = document.createElement("input"); + let buttonId = crypto.randomUUID(); + radioButton.setAttribute("type", "radio"); + radioButton.setAttribute("id", buttonId); + radioButton.setAttribute("name", planetChoiceName); + radioButton.setAttribute("value", body.planetName); + if (body === startingValue) { + radioButton.setAttribute("checked", ""); + } + + radioButton.addEventListener("change", () => this.informListeners(body)); + + let label = document.createElement("label"); + label.setAttribute("for", buttonId); + label.appendChild(document.createTextNode(body.planetName)); + + addToParent([radioButton, label], body); + }); + } + + addListener(listener: (newValue: Body) => void) { + this.listeners.push(listener); + } + + informListeners(newValue: Body) { + this.listeners.forEach(listener => listener(newValue)); + } +} \ No newline at end of file diff --git a/src/gui/simpleplanechange.ts b/src/gui/simpleplanechange.ts new file mode 100644 index 0000000..be53ba7 --- /dev/null +++ b/src/gui/simpleplanechange.ts @@ -0,0 +1,226 @@ +import type { Wrapper } from "../storage"; +import { createDisabledInput, createLabel, createNumberInput, getOrbitFromParameters, type OrbitalParameters } from "./common"; +import { type Body } from "../calculations/constants"; +import { calculateSimplePlaneChange, getOrbitalCoordinates} from "../calculations/orbit-calculations"; + +export class SimplePlaneChangeGui { + listeners: ((targetInclination: number, targetLongitudeOfAscendingNode: number, circularize: boolean) => void)[]; + + currentTime: Wrapper; + timeToPeriapsis: Wrapper; + planet: Wrapper; + startingOrbitalParameters: Wrapper; + goalOrbitalParameters: Wrapper; + circularizeOrbit: boolean; + + inputHeader: HTMLElement; + + targetInclinationLabel: HTMLLabelElement; + targetInclinationInput: HTMLInputElement; + targetLongitudeOfAscendingNodeLabel: HTMLLabelElement; + targetLongitudeOfAscendingNodeInput: HTMLInputElement; + circularizeOrbitLabel: HTMLLabelElement; + circularizeOrbitInput: HTMLInputElement; + + calculateButton: HTMLButtonElement; + + outputHeader: HTMLElement; + manoeuvresContainer: HTMLDivElement; + + firstManoeuvreTime: HTMLInputElement; + firstManoeuvrePrograde: HTMLInputElement; + firstManoeuvreRadial: HTMLElement; + firstManoeuvreNormal: HTMLElement; + firstManoeuvreTotal: HTMLElement; + + secondManoeuvreTime: HTMLInputElement; + secondManoeuvrePrograde: HTMLInputElement; + secondManoeuvreRadial: HTMLElement; + secondManoeuvreNormal: HTMLElement; + secondManoeuvreTotal: HTMLElement; + + constructor(startingParameters: Wrapper, goalParameters: Wrapper, currentTime: Wrapper, timeToPeriapsis: Wrapper, planet: Wrapper, defaultCircularizeOrbit?: boolean) { + this.listeners = []; + this.startingOrbitalParameters = startingParameters; + this.goalOrbitalParameters = goalParameters; + this.currentTime = currentTime; + this.timeToPeriapsis = timeToPeriapsis; + this.planet = planet; + + if (defaultCircularizeOrbit) { + this.circularizeOrbit = defaultCircularizeOrbit; + } else { + this.circularizeOrbit = false; + } + + this.inputHeader = document.createElement("h3"); + this.inputHeader.appendChild(document.createTextNode("Target plane:")); + + let targetInclinationId = crypto.randomUUID(); + this.targetInclinationLabel = createLabel(targetInclinationId, "Target inclination:"); + this.targetInclinationInput = createNumberInput(targetInclinationId, -360, 360); + + let targetLongitudeOfAscendingNodeId = crypto.randomUUID(); + this.targetLongitudeOfAscendingNodeLabel = createLabel(targetLongitudeOfAscendingNodeId, "Target longitude of ascending node:"); + this.targetLongitudeOfAscendingNodeInput = createNumberInput(targetLongitudeOfAscendingNodeId, -360, 360); + + let circularizeOrbitId = crypto.randomUUID(); + this.circularizeOrbitLabel = createLabel(circularizeOrbitId, "Circularize orbit"); + this.circularizeOrbitInput = document.createElement("input"); + this.circularizeOrbitInput.setAttribute("id", circularizeOrbitId); + this.circularizeOrbitInput.setAttribute("type", "checkbox"); + + this.targetInclinationInput.addEventListener("change", this.informListeners.bind(this)); + this.targetLongitudeOfAscendingNodeInput.addEventListener("change", this.informListeners.bind(this)); + this.circularizeOrbitInput.addEventListener("change", this.informListeners.bind(this)); + + this.calculateButton = document.createElement("button"); + this.calculateButton.appendChild(document.createTextNode("Calculate")); + this.calculateButton.addEventListener("click", this.performCalculation.bind(this)); + + this.outputHeader = document.createElement("h3"); + this.outputHeader.appendChild(document.createTextNode("Possible manoeuvres:")); + + this.manoeuvresContainer = document.createElement("div"); + this.manoeuvresContainer.classList.add("flexContainer"); + + let manoeuvreOneContainer = document.createElement("div"); + let manoeuvreTwoContainer = document.createElement("div"); + + this.manoeuvresContainer.appendChild(manoeuvreOneContainer); + this.manoeuvresContainer.appendChild(manoeuvreTwoContainer); + + let manoeuvreOneHeader = document.createElement("h4"); + manoeuvreOneHeader.appendChild(document.createTextNode("First manoeuvre:")); + manoeuvreOneContainer.appendChild(manoeuvreOneHeader); + + let manoeuvreTwoHeader = document.createElement("h4"); + manoeuvreTwoHeader.appendChild(document.createTextNode("Second manoeuvre:")); + manoeuvreTwoContainer.appendChild(manoeuvreTwoHeader); + + const addTo = (container: HTMLElement, children: HTMLElement[]) => { + children.forEach(child => { + container.appendChild(child); + }) + container.appendChild(document.createElement("br")); + }; + + let firstTimeId = crypto.randomUUID(); + let firstTimeLabel = createLabel(firstTimeId, "Time:"); + this.firstManoeuvreTime = createDisabledInput(firstTimeId); + addTo(manoeuvreOneContainer, [firstTimeLabel, this.firstManoeuvreTime]); + + let firstProgradeId = crypto.randomUUID(); + let firstProgradeLabel = createLabel(firstProgradeId, "Prograde delta-v:"); + this.firstManoeuvrePrograde = createDisabledInput(firstProgradeId); + addTo(manoeuvreOneContainer, [firstProgradeLabel, this.firstManoeuvrePrograde]); + + let firstNormalId = crypto.randomUUID(); + let firstNormalLabel = createLabel(firstNormalId, "Normal delta-v:"); + this.firstManoeuvreNormal = createDisabledInput(firstNormalId); + addTo(manoeuvreOneContainer, [firstNormalLabel, this.firstManoeuvreNormal]); + + let firstRadialId = crypto.randomUUID(); + let firstRadialLabel = createLabel(firstRadialId, "Radial delta-v:"); + this.firstManoeuvreRadial = createDisabledInput(firstRadialId); + addTo(manoeuvreOneContainer, [firstRadialLabel, this.firstManoeuvreRadial]); + + let firstTotalId = crypto.randomUUID(); + let firstTotalLabel = createLabel(firstTotalId, "Total delta-v:"); + this.firstManoeuvreTotal = createDisabledInput(firstTotalId); + addTo(manoeuvreOneContainer, [firstTotalLabel, this.firstManoeuvreTotal]); + + let secondTimeId = crypto.randomUUID(); + let secondTimeLabel = createLabel(secondTimeId, "Time:"); + this.secondManoeuvreTime = createDisabledInput(secondTimeId); + addTo(manoeuvreTwoContainer, [secondTimeLabel, this.secondManoeuvreTime]); + + let secondProgradeId = crypto.randomUUID(); + let secondProgradeLabel = createLabel(secondProgradeId, "Prograde delta-v:"); + this.secondManoeuvrePrograde = createDisabledInput(secondProgradeId); + addTo(manoeuvreTwoContainer, [secondProgradeLabel, this.secondManoeuvrePrograde]); + + let secondNormalId = crypto.randomUUID(); + let secondNormalLabel = createLabel(secondNormalId, "Normal delta-v:"); + this.secondManoeuvreNormal = createDisabledInput(secondNormalId); + addTo(manoeuvreTwoContainer, [secondNormalLabel, this.secondManoeuvreNormal]); + + let secondRadialId = crypto.randomUUID(); + let secondRadialLabel = createLabel(secondRadialId, "Radial delta-v:"); + this.secondManoeuvreRadial = createDisabledInput(secondRadialId); + addTo(manoeuvreTwoContainer, [secondRadialLabel, this.secondManoeuvreRadial]); + + let secondTotalId = crypto.randomUUID(); + let secondTotalLabel = createLabel(secondTotalId, "Total delta-v:"); + this.secondManoeuvreTotal = createDisabledInput(secondTotalId); + addTo(manoeuvreTwoContainer, [secondTotalLabel, this.secondManoeuvreTotal]); + + this.populateValues(); + } + + addToParentElement(parentElement: HTMLElement) { + parentElement.appendChild(this.inputHeader); + parentElement.appendChild(this.targetInclinationLabel); + parentElement.appendChild(this.targetInclinationInput); + parentElement.appendChild(document.createElement("br")); + parentElement.appendChild(this.targetLongitudeOfAscendingNodeLabel); + parentElement.appendChild(this.targetLongitudeOfAscendingNodeInput); + parentElement.appendChild(document.createElement("br")); + parentElement.appendChild(this.circularizeOrbitInput); + parentElement.appendChild(this.circularizeOrbitLabel); + parentElement.appendChild(document.createElement("br")); + parentElement.appendChild(this.calculateButton); + + parentElement.appendChild(this.outputHeader); + parentElement.appendChild(this.manoeuvresContainer); + if (this.circularizeOrbit) { + this.circularizeOrbitInput.setAttribute("checked", ""); + } else { + this.circularizeOrbitInput.removeAttribute("checked"); + } + + this.populateValues(); + } + + addListener(listener: (targetInclination: number, targetLongitudeOfAscendingNode: number, circularize: boolean) => void) { + this.listeners.push(listener); + } + + informListeners() { + let targetInclination = parseFloat(this.targetInclinationInput.value) * Math.PI / 180.0; + let targetLAN = parseFloat(this.targetLongitudeOfAscendingNodeInput.value) * Math.PI / 180.0; + this.circularizeOrbit = this.circularizeOrbitInput.checked; + + this.listeners.forEach(listener => listener(targetInclination, targetLAN, this.circularizeOrbit)); + } + + populateValues() { + this.targetInclinationInput.setAttribute("value", (this.goalOrbitalParameters.value.inclination * 180 / Math.PI).toString()); + this.targetLongitudeOfAscendingNodeInput.setAttribute("value", (this.goalOrbitalParameters.value.longitudeOfAscendingNode * 180 / Math.PI).toString()); + } + + performCalculation() { + let orbit = getOrbitFromParameters(this.startingOrbitalParameters.value, this.planet.value.radius); + + let coordinates = getOrbitalCoordinates(this.timeToPeriapsis.value, orbit, this.planet.value); + let simplePlaneChange = calculateSimplePlaneChange( + coordinates, + this.planet.value, + this.goalOrbitalParameters.value.inclination, + this.goalOrbitalParameters.value.longitudeOfAscendingNode, + this.circularizeOrbit + ); + + this.firstManoeuvreTime.setAttribute("value", (simplePlaneChange.firstManoeuvre.time + this.currentTime.value).toFixed(0)); + this.firstManoeuvrePrograde.setAttribute("value", simplePlaneChange.firstManoeuvre.progradeDeltaV.toFixed(1)); + this.firstManoeuvreRadial.setAttribute("value", simplePlaneChange.firstManoeuvre.radialDeltaV.toFixed(1)); + this.firstManoeuvreNormal.setAttribute("value", simplePlaneChange.firstManoeuvre.normalDeltaV.toFixed(1)); + this.firstManoeuvreTotal.setAttribute("value", simplePlaneChange.firstManoeuvre.totalDeltaV.toFixed(1)); + + this.secondManoeuvreTime.setAttribute("value", (simplePlaneChange.secondManoeuvre.time + this.currentTime.value).toFixed(0)); + this.secondManoeuvrePrograde.setAttribute("value", simplePlaneChange.secondManoeuvre.progradeDeltaV.toFixed(1)); + this.secondManoeuvreRadial.setAttribute("value", simplePlaneChange.secondManoeuvre.radialDeltaV.toFixed(1)); + this.secondManoeuvreNormal.setAttribute("value", simplePlaneChange.secondManoeuvre.normalDeltaV.toFixed(1)); + this.secondManoeuvreTotal.setAttribute("value", simplePlaneChange.secondManoeuvre.totalDeltaV.toFixed(1)); + } +} \ No newline at end of file diff --git a/src/gui/targetorbit.ts b/src/gui/targetorbit.ts new file mode 100644 index 0000000..f410d66 --- /dev/null +++ b/src/gui/targetorbit.ts @@ -0,0 +1,209 @@ +import { getOrbitalCoordinates } from "../calculations/orbit-calculations"; +import type { Wrapper } from "../storage"; +import { createDisabledInput, createLabel, getOrbitFromParameters, type OrbitalParameters } from "./common"; +import { OrbitalParametersGui } from "./orbit"; +import { type Body } from "../calculations/constants"; +import type { FindBestTransferMessage, FindBestTransferResponse } from "./worker"; + +export class TargetOrbitGui { + startingParameters: Wrapper; + goalParameters: Wrapper; + currentTime: Wrapper; + timeToPeriapsis: Wrapper; + body: Wrapper; + + orbitGui: OrbitalParametersGui; + parentDiv: HTMLDivElement; + + progressParagraph: HTMLParagraphElement; + + firstManoeuvreTime: HTMLInputElement; + firstManoeuvrePrograde: HTMLInputElement; + firstManoeuvreNormal: HTMLInputElement; + firstManoeuvreRadial: HTMLInputElement; + firstManoeuvreTotal: HTMLInputElement; + + secondManoeuvreTime: HTMLInputElement; + secondManoeuvrePrograde: HTMLInputElement; + secondManoeuvreNormal: HTMLInputElement; + secondManoeuvreRadial: HTMLInputElement; + secondManoeuvreTotal: HTMLInputElement; + + worker: Worker | null; + + constructor(startingParameters: Wrapper, goalParameters: Wrapper, currentTime: Wrapper, timeToPeriapsis: Wrapper, body: Wrapper) { + this.startingParameters = startingParameters; + this.goalParameters = goalParameters; + this.currentTime = currentTime; + this.timeToPeriapsis = timeToPeriapsis; + this.body = body; + + this.parentDiv = document.createElement("div"); + let parametersHeader = document.createElement("h3"); + parametersHeader.appendChild(document.createTextNode("Target orbit:")); + this.parentDiv.appendChild(parametersHeader); + + let orbitalParameterContainerCreator = () => { + let orbitalParameterContainer = document.createElement("div"); + orbitalParameterContainer.classList.add("orbitalParameter"); + return orbitalParameterContainer; + } + + this.orbitGui = new OrbitalParametersGui(this.parentDiv, goalParameters.value, orbitalParameterContainerCreator); + + let searchButton = document.createElement("button"); + searchButton.appendChild(document.createTextNode("Search for cheapest transfer")); + this.parentDiv.appendChild(searchButton); + + let searchHeader = document.createElement("h3"); + searchHeader.appendChild(document.createTextNode("Manoeuvre search")); + this.parentDiv.appendChild(searchHeader); + + this.progressParagraph = document.createElement("p"); + this.parentDiv.appendChild(this.progressParagraph); + + let manoeuvresContainer = document.createElement("div"); + manoeuvresContainer.classList.add("flexContainer"); + + this.parentDiv.appendChild(manoeuvresContainer) + + let manoeuvreOneContainer = document.createElement("div"); + let manoeuvreTwoContainer = document.createElement("div"); + + manoeuvresContainer.appendChild(manoeuvreOneContainer); + manoeuvresContainer.appendChild(manoeuvreTwoContainer); + + let manoeuvreOneHeader = document.createElement("h4"); + manoeuvreOneHeader.appendChild(document.createTextNode("First manoeuvre:")); + manoeuvreOneContainer.appendChild(manoeuvreOneHeader); + let manoeuvreTwoHeader = document.createElement("h4"); + manoeuvreTwoHeader.appendChild(document.createTextNode("Second manoeuvre:")); + manoeuvreTwoContainer.appendChild(manoeuvreTwoHeader); + + const addTo = (container: HTMLElement, children: HTMLElement[]) => { + children.forEach(child => { + container.appendChild(child); + }) + container.appendChild(document.createElement("br")); + }; + + let firstTimeId = crypto.randomUUID(); + let firstTimeLabel = createLabel(firstTimeId, "Time:"); + this.firstManoeuvreTime = createDisabledInput(firstTimeId); + addTo(manoeuvreOneContainer, [firstTimeLabel, this.firstManoeuvreTime]); + + let firstProgradeId = crypto.randomUUID(); + let firstProgradeLabel = createLabel(firstProgradeId, "Prograde delta-v:"); + this.firstManoeuvrePrograde = createDisabledInput(firstProgradeId); + addTo(manoeuvreOneContainer, [firstProgradeLabel, this.firstManoeuvrePrograde]); + + let firstNormalId = crypto.randomUUID(); + let firstNormalLabel = createLabel(firstNormalId, "Normal delta-v:"); + this.firstManoeuvreNormal = createDisabledInput(firstNormalId); + addTo(manoeuvreOneContainer, [firstNormalLabel, this.firstManoeuvreNormal]); + + let firstRadialId = crypto.randomUUID(); + let firstRadialLabel = createLabel(firstRadialId, "Radial delta-v:"); + this.firstManoeuvreRadial = createDisabledInput(firstRadialId); + addTo(manoeuvreOneContainer, [firstRadialLabel, this.firstManoeuvreRadial]); + + let firstTotalId = crypto.randomUUID(); + let firstTotalLabel = createLabel(firstTotalId, "Total delta-v:"); + this.firstManoeuvreTotal = createDisabledInput(firstTotalId); + addTo(manoeuvreOneContainer, [firstTotalLabel, this.firstManoeuvreTotal]); + + let secondTimeId = crypto.randomUUID(); + let secondTimeLabel = createLabel(secondTimeId, "Time:"); + this.secondManoeuvreTime = createDisabledInput(secondTimeId); + addTo(manoeuvreTwoContainer, [secondTimeLabel, this.secondManoeuvreTime]); + + let secondProgradeId = crypto.randomUUID(); + let secondProgradeLabel = createLabel(secondProgradeId, "Prograde delta-v:"); + this.secondManoeuvrePrograde = createDisabledInput(secondProgradeId); + addTo(manoeuvreTwoContainer, [secondProgradeLabel, this.secondManoeuvrePrograde]); + + let secondNormalId = crypto.randomUUID(); + let secondNormalLabel = createLabel(secondNormalId, "Normal delta-v:"); + this.secondManoeuvreNormal = createDisabledInput(secondNormalId); + addTo(manoeuvreTwoContainer, [secondNormalLabel, this.secondManoeuvreNormal]); + + let secondRadialId = crypto.randomUUID(); + let secondRadialLabel = createLabel(secondRadialId, "Radial delta-v:"); + this.secondManoeuvreRadial = createDisabledInput(secondRadialId); + addTo(manoeuvreTwoContainer, [secondRadialLabel, this.secondManoeuvreRadial]); + + let secondTotalId = crypto.randomUUID(); + let secondTotalLabel = createLabel(secondTotalId, "Total delta-v:"); + this.secondManoeuvreTotal = createDisabledInput(secondTotalId); + addTo(manoeuvreTwoContainer, [secondTotalLabel, this.secondManoeuvreTotal]); + + this.worker = null; + + searchButton.addEventListener("click", () => { + if (this.worker !== null) { + this.worker.terminate(); + this.worker = null; + searchButton.innerHTML = "Search for cheapest transfer"; + } else { + searchButton.innerHTML = "Cancel search"; + + let startingOrbit = getOrbitFromParameters(startingParameters.value, this.body.value.radius); + let endingOrbit = getOrbitFromParameters(goalParameters.value, this.body.value.radius); + let currentCoordinates = getOrbitalCoordinates(timeToPeriapsis.value, startingOrbit, this.body.value); + + this.worker = new Worker(new URL('./worker.ts', import.meta.url), {type: 'module'}); + this.worker.addEventListener("message", event => { + let transferResponse = event.data as FindBestTransferResponse; + if (transferResponse) { + if (transferResponse.finished) { + searchButton.innerHTML = "Search for cheapest transfer"; + this.worker = null; + } + + this.progressParagraph.innerHTML = ""; + this.progressParagraph.appendChild(document.createTextNode(`Search is ${transferResponse.percentDone.toFixed(2)}% finished`)); + if (transferResponse.bestDeltaV !== null && transferResponse.bestTransfer != null) { + this.progressParagraph.appendChild(document.createElement("br")); + this.progressParagraph.appendChild(document.createTextNode(`Best transfer costs ${transferResponse.bestDeltaV.toFixed(3)} m/s`)); + + let transfer = transferResponse.bestTransfer; + + transfer.firstManoeuvre.time += currentTime.value; + transfer.secondManoeuvre.time += currentTime.value; + + this.firstManoeuvreTime.value = transfer.firstManoeuvre.time.toFixed(0); + this.firstManoeuvrePrograde.value = transfer.firstManoeuvre.progradeDeltaV.toFixed(2); + this.firstManoeuvreNormal.value = transfer.firstManoeuvre.normalDeltaV.toFixed(2); + this.firstManoeuvreRadial.value = transfer.firstManoeuvre.radialDeltaV.toFixed(2); + this.firstManoeuvreTotal.value = transfer.firstManoeuvre.totalDeltaV.toFixed(2); + + this.secondManoeuvreTime.value = transfer.secondManoeuvre.time.toFixed(0); + this.secondManoeuvrePrograde.value = transfer.secondManoeuvre.progradeDeltaV.toFixed(2); + this.secondManoeuvreNormal.value = transfer.secondManoeuvre.normalDeltaV.toFixed(2); + this.secondManoeuvreRadial.value = transfer.secondManoeuvre.radialDeltaV.toFixed(2); + this.secondManoeuvreTotal.value = transfer.secondManoeuvre.totalDeltaV.toFixed(2); + } + } + }); + + let workerMessage: FindBestTransferMessage = { + type: "FindBestTransfer", + startingSituation: currentCoordinates, + targetOrbit: endingOrbit, + body: body.value + }; + + this.worker.postMessage(workerMessage); + } + }); + } + + addToParentElement(parentElement: HTMLElement) { + this.orbitGui.setValues(this.goalParameters.value); + parentElement.appendChild(this.parentDiv); + } + + addListener(listener: (newValue: OrbitalParameters) => void) { + this.orbitGui.addListener(listener); + } +} \ No newline at end of file diff --git a/src/gui/time.ts b/src/gui/time.ts new file mode 100644 index 0000000..919ae17 --- /dev/null +++ b/src/gui/time.ts @@ -0,0 +1,112 @@ +import { createLabel, createNumberInput } from "./common"; + +export class TimeGui { + dateListeners: ((newDate: number) => void)[]; + isDate: boolean; + yearsInput: HTMLInputElement; + daysInput: HTMLInputElement; + hoursInput: HTMLInputElement; + minutesInput: HTMLInputElement; + secondsInput: HTMLInputElement; + + constructor(htmlContainer: HTMLElement, isDate?: boolean, startingValue?: number, containerCreator?: () => HTMLElement) { + this.dateListeners = []; + if (isDate !== undefined) { + this.isDate = isDate; + } else { + this.isDate = false; + } + + var minimumYears = 0; + var minimumDays = 0; + if (isDate !== undefined && isDate) { + minimumYears = 1; + minimumDays = 1; + } + + const addToParent = (toWrap: HTMLElement | HTMLElement[]) => { + var children = []; + if (toWrap instanceof HTMLElement) { + children = [toWrap]; + } else { + children = toWrap; + } + + let containerToAppendTo = htmlContainer; + if (containerCreator !== undefined) { + let childContainer = containerCreator(); + containerToAppendTo.appendChild(childContainer); + containerToAppendTo = childContainer; + } + + children.forEach(child => containerToAppendTo.appendChild(child)); + }; + + let yearsInputId = crypto.randomUUID(); + this.yearsInput = createNumberInput(yearsInputId, minimumYears, undefined, 2); + addToParent([createLabel(yearsInputId, "Years:"), this.yearsInput]); + + let daysInputId = crypto.randomUUID(); + this.daysInput = createNumberInput(daysInputId, minimumDays, 424, 1); + addToParent([createLabel(daysInputId, "Days:"), this.daysInput]); + + let hoursInputId = crypto.randomUUID(); + this.hoursInput = createNumberInput(hoursInputId, 0, 5, 1); + addToParent([createLabel(hoursInputId, "Hours:"), this.hoursInput]); + + let minutesInputId = crypto.randomUUID(); + this.minutesInput = createNumberInput(minutesInputId, 0, 59, 1); + addToParent([createLabel(minutesInputId, "Minutes:"), this.minutesInput]); + + let secondsInputId = crypto.randomUUID(); + this.secondsInput = createNumberInput(secondsInputId, 0, 59, 1); + addToParent([createLabel(secondsInputId, "Seconds:"), this.secondsInput]); + + if (startingValue === undefined) { + startingValue = 0; + } + + const startingSeconds = startingValue % 60; + startingValue = Math.floor(startingValue / 60); + const startingMinutes = startingValue % 60; + startingValue = Math.floor(startingValue / 60); + const startingHours = (startingValue % 6); + startingValue = Math.floor(startingValue / 6); + const startingDays = (startingValue % 426) + (this.isDate ? 1 : 0); + startingValue = Math.floor(startingValue / 426); + const startingYears = startingValue + (this.isDate ? 1 : 0); + + this.yearsInput.setAttribute("value", startingYears.toFixed(0)); + this.daysInput.setAttribute("value", startingDays.toFixed(0)); + this.hoursInput.setAttribute("value", startingHours.toFixed(0)); + this.minutesInput.setAttribute("value", startingMinutes.toFixed(0)); + this.secondsInput.setAttribute("value", startingSeconds.toFixed(0)); + + this.yearsInput.addEventListener("change", this.calculateTime.bind(this)); + this.daysInput.addEventListener("change", this.calculateTime.bind(this)); + this.hoursInput.addEventListener("change", this.calculateTime.bind(this)); + this.minutesInput.addEventListener("change", this.calculateTime.bind(this)); + this.secondsInput.addEventListener("change", this.calculateTime.bind(this)); + } + + addListener(listenerFunction: (newDate: number) => void) { + this.dateListeners.push(listenerFunction); + } + + calculateTime() { + let years = parseInt(this.yearsInput.value); + let days = parseInt(this.daysInput.value); + let hours = parseInt(this.hoursInput.value); + let minutes = parseInt(this.minutesInput.value); + let seconds = parseInt(this.secondsInput.value); + + if (this.isDate) { + years -= 1; + days -= 1; + } + + let calculatedTime = (((years * 426 + days) * 6 + hours) * 60 + minutes) * 60 + seconds; + + this.dateListeners.forEach(listener => listener(calculatedTime)); + } +} \ No newline at end of file diff --git a/src/gui/worker.ts b/src/gui/worker.ts new file mode 100644 index 0000000..ec61b99 --- /dev/null +++ b/src/gui/worker.ts @@ -0,0 +1,54 @@ +import type { Body } from "../calculations/constants"; +import { findCheapestTransfer, type Orbit, type OrbitalCoordinates, type Transfer } from "../calculations/orbit-calculations"; + +const ctx: Worker = self as any; + +export interface FindBestTransferMessage { + type: "FindBestTransfer", + startingSituation: OrbitalCoordinates, + targetOrbit: Orbit, + body: Body +} + +export interface FindBestTransferResponse { + type: "FindBestTransferResponse" + finished: boolean, + percentDone: number, + bestDeltaV: number | null, + bestTransfer: Transfer | null +} + +ctx.addEventListener("message", event => { + let findBestTransferMessage = event.data as FindBestTransferMessage; + if (findBestTransferMessage) { + const progressCallback = (numberChecked: number, totalNumber: number, bestDeltaV: number | null, bestTransfer: Transfer | null) => { + if (numberChecked % 100 == 0) { + let percentDone = numberChecked * 100 / totalNumber; + let message: FindBestTransferResponse = { + type: "FindBestTransferResponse", + finished: false, + percentDone: percentDone, + bestDeltaV: bestDeltaV, + bestTransfer: bestTransfer + }; + + ctx.postMessage(message); + } + } + + let bestTransfer = findCheapestTransfer(findBestTransferMessage.startingSituation, findBestTransferMessage.targetOrbit, findBestTransferMessage.body, progressCallback); + let bestDeltaV = null; + if (bestTransfer) { + bestDeltaV = bestTransfer.firstManoeuvre.totalDeltaV + bestTransfer.secondManoeuvre.totalDeltaV; + } + + let finishedMessage: FindBestTransferResponse = { + type: "FindBestTransferResponse", + finished: true, + percentDone: 100, + bestDeltaV: bestDeltaV, + bestTransfer: bestTransfer + }; + ctx.postMessage(finishedMessage); + } +}); \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 84f8183..aa679da 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,217 +1,133 @@ -import { Eve, Gilly, Kerbin, Kerbol, Minmus, Moho, Mun, type Planet } from "./calculations/constants"; -import {calculateSimplePlaneChange, getOrbitalCoordinates} from "./calculations/orbit-calculations"; +import { getPlanetByName, Kerbol, type Body } from "./calculations/constants"; +import { DefaultOrbitalParameters } from "./gui/common"; +import { OrbitalParametersGui } from "./gui/orbit"; +import { decodeOrbitalParameters } from "./gui/common"; +import { encodeOrbitalParameters } from "./gui/common"; +import { PlanetGui } from "./gui/planet"; +import { TimeGui } from "./gui/time"; +import { createLocalStorageVariable } from "./storage"; +import { SimplePlaneChangeGui } from "./gui/simpleplanechange"; +import { TargetOrbitGui } from "./gui/targetorbit"; -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; - -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; +type CalculationType = "planeChange" | "targetOrbit"; +function decodeCalculationType(input: string): CalculationType { + let calculationType = input as CalculationType; + if (calculationType !== undefined) { + return calculationType; } - return Kerbol; + return "planeChange"; } -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"); - } +let [currentTime, setCurrentTime] = createLocalStorageVariable("currentTime", parseInt, n => n.toFixed(0), 0); +let [timeToPeriapsis, setTimeToPeriapsis] = createLocalStorageVariable("timeToPeriapsis", parseInt, n => n.toFixed(0), 0); +let [planet, setPlanet] = createLocalStorageVariable("planet", getPlanetByName, p => p.planetName, Kerbol); +let [orbitalParameters, setOrbitalParameters] = createLocalStorageVariable("orbitalParameters", decodeOrbitalParameters, encodeOrbitalParameters, DefaultOrbitalParameters); +let [calculationType, setCalculationType] = createLocalStorageVariable("calculationType", decodeCalculationType, s => s, "planeChange"); + +let [targetOrbitalParameters, setTargetOrbitalParameters] = createLocalStorageVariable("targetOrbitalParameters", decodeOrbitalParameters, encodeOrbitalParameters, DefaultOrbitalParameters); +let [circularizeOrbit, setCircularizeOrbit] = createLocalStorageVariable("circularizeOrbit", s => s == "true", bool => bool ? "true" : "false", false) + +let dateInputContainerCreator = () => { + let dateInputContainer = document.createElement("span"); + dateInputContainer.classList.add("dateInputContainer"); + return dateInputContainer; } -coordinatesRadio.onclick = selectCalculationType; -simplePlaneChangeRadio.onclick = selectCalculationType; -orbitChangeRadio.onclick = selectCalculationType; +let planetaryBodyContainerCreator = (body: Body) => { + let bodyContainer = document.createElement("div"); + bodyContainer.classList.add(body.type); + return bodyContainer; +}; -interface CommonInputs { - periapsis: number, - apoapsis: number, - timeToPeriapsis: number, - planet: Planet, - inclination: number, - longitudeOfAscendingNode: number, - argumentOfPeriapsis: number, - timeElapsed: number +let orbitalParameterContainerCreator = () => { + let orbitalParameterContainer = document.createElement("div"); + orbitalParameterContainer.classList.add("orbitalParameter"); + return orbitalParameterContainer; } -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(); +let timesDiv = document.getElementById("timesDiv") as HTMLElement; +let dateGui = new TimeGui(timesDiv, true, currentTime.value, dateInputContainerCreator); +dateGui.addListener(setCurrentTime); - return { - periapsis: periapsis, - apoapsis: apoapsis, - timeToPeriapsis: timeToPeriapsis, - planet: planet, - inclination: inclination, - longitudeOfAscendingNode: longitudeOfAscendingNode, - argumentOfPeriapsis: argumentOfPeriapsis, - timeElapsed: timeElapsed - } -} +let periapsisDiv = document.getElementById("periapsisDiv") as HTMLElement; +let periapsisGui = new TimeGui(periapsisDiv, false, timeToPeriapsis.value, dateInputContainerCreator); +periapsisGui.addListener(setTimeToPeriapsis); -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 - ); +let planetsDiv = document.getElementById("planetsDiv") as HTMLElement; +let planetsGui = new PlanetGui(planetsDiv, planet.value, planetaryBodyContainerCreator); +planetsGui.addListener(setPlanet); - 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); +let orbitalParametersDiv = document.getElementById("orbitalParametersDiv") as HTMLElement; +let orbitalParametersGui = new OrbitalParametersGui(orbitalParametersDiv, orbitalParameters.value, orbitalParameterContainerCreator); +orbitalParametersGui.addListener(setOrbitalParameters); + +let choiceDiv = document.getElementById("calculationChoice") as HTMLElement; +let calculationDiv = document.getElementById("calculation") as HTMLElement; + +const simplePlaneChangeGui = new SimplePlaneChangeGui(orbitalParameters, targetOrbitalParameters, currentTime, timeToPeriapsis, planet, circularizeOrbit.value); +simplePlaneChangeGui.addListener((targetInclination, targetLongitudeOfAscendingNode, circularizeOrbit) => { + setCircularizeOrbit(circularizeOrbit); + + let newTargetParameters = structuredClone(targetOrbitalParameters.value); + newTargetParameters.inclination = targetInclination; + newTargetParameters.longitudeOfAscendingNode = targetLongitudeOfAscendingNode; + + setTargetOrbitalParameters(newTargetParameters); }); -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 targetOrbitGui = new TargetOrbitGui(orbitalParameters, targetOrbitalParameters, currentTime, timeToPeriapsis, planet); +targetOrbitGui.addListener(setTargetOrbitalParameters); - 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); - }); +function populateCalculationDiv() { + calculationDiv.innerHTML = ""; + calculationDiv.className = ""; + if (calculationType.value == "planeChange") { + calculationDiv.classList.add("simplePlaneChange"); + simplePlaneChangeGui.addToParentElement(calculationDiv); + } else if (calculationType.value == "targetOrbit") { + calculationDiv.classList.add("targetOrbit"); + targetOrbitGui + targetOrbitGui.addToParentElement(calculationDiv); + } +} + +// Run this when creating the document +populateCalculationDiv(); + +// Create the choices +const createRadioButton = (name: string, id: string, value: string) => { + let radioButton = document.createElement("input"); + radioButton.setAttribute("type", "radio"); + radioButton.setAttribute("id", id); + radioButton.setAttribute("name", name); + radioButton.setAttribute("value", value); + + let label = document.createElement("label"); + label.setAttribute("for", id); + label.appendChild(document.createTextNode(value)); + + return [radioButton, label]; +} + +let [simplePlaneChangeButton, planeChangeLabel] = createRadioButton("calculationType", "spc", "Simple Plane Change"); +choiceDiv.appendChild(simplePlaneChangeButton); +choiceDiv.appendChild(planeChangeLabel); +if (calculationType.value === "planeChange") { + simplePlaneChangeButton.setAttribute("checked", ""); +} +simplePlaneChangeButton.addEventListener("change", () => { + setCalculationType("planeChange"); + populateCalculationDiv(); }); -selectCalculationType(); \ No newline at end of file +let [targetOrbitButton, targetOrbitLabel] = createRadioButton("calculationType", "to", "Target Orbit"); +choiceDiv.appendChild(targetOrbitButton); +choiceDiv.appendChild(targetOrbitLabel); +if (calculationType.value == "targetOrbit") { + targetOrbitButton.setAttribute("checked", ""); +} +targetOrbitButton.addEventListener("change", () => { + setCalculationType("targetOrbit"); + populateCalculationDiv(); +}); \ No newline at end of file diff --git a/src/storage.ts b/src/storage.ts new file mode 100644 index 0000000..77ad548 --- /dev/null +++ b/src/storage.ts @@ -0,0 +1,24 @@ +export interface Wrapper { + value: Type; +} + +export function createLocalStorageVariable(variableName: string, decoder: (a: string) => Type, encoder: (a: Type) => string, defaultValue: Type): [Wrapper, (value: Type) => void] { + var localStorageString = localStorage.getItem(variableName); + var variable: Wrapper; + if (localStorageString) { + variable = { + value: decoder(localStorageString) + }; + } else { + variable = { + value: defaultValue + }; + } + + const setterFunction = (value: Type) => { + variable.value = value; + localStorage.setItem(variableName, encoder(value)); + } + + return [variable, setterFunction]; +} \ No newline at end of file diff --git a/src/style.css b/src/style.css index 3bcdbd0..6d7da46 100644 --- a/src/style.css +++ b/src/style.css @@ -1,96 +1,57 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; +.dateInputContainer { + display: inline-block; + min-width: 120px; + padding-right: 10px; } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; +.dateInputContainer label { + padding-right: 5px; } -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; +.planet { + margin-left: 20px; } -h1 { - font-size: 3.2em; - line-height: 1.1; +.moon { + margin-left: 40px; } -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; +.orbitalParameter { + margin-top: 4px; } -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.vanilla:hover { - filter: drop-shadow(0 0 2em #3178c6aa); +.orbitalParameter label { + display: inline-block; + width: 250px; } -.card { - padding: 2em; +.orbitalParameter input[type=number] { + width: 150px; } -.read-the-docs { - color: #888; +.simplePlaneChange label { + display: inline-block; + width: 300px; } -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; +.simplePlaneChange input[type=number] { + width: 150px; } -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } +.targetOrbit label { + display: inline-block; + width: 300px; } + +.targetOrbit input[type=number] { + width: 150px; +} + +.flexContainer { + display: flex; + flex-direction: row; +} + +.flexContainer div { + margin-right: 50px; +} \ No newline at end of file