Can calculate transfers to specific orbits
This commit is contained in:
parent
400bf42b12
commit
30ede19096
210
index.html
210
index.html
@ -3,209 +3,23 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/satellite.png" />
|
||||
<link rel="stylesheet" href="./src/style.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Kerbal calculations</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Kerbal calculations</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<td colspan="10"><h3>Current time in game:</h3></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="dateYear">Year:</label></td>
|
||||
<td><input id="dateYear" type="number" min="1" size="2" value="1" /></td>
|
||||
<td><label for="dateDay">Day:</label></td>
|
||||
<td><input id="dateDay" type="number" min="1" max="424" size="1" value="1" /></td>
|
||||
<td><label for="dateHours">Hour:</label></td>
|
||||
<td><input id="dateHours" type="number" min="0" max="5" size="1" value="0" /></td>
|
||||
<td><label for="dateMinutes">Minute:</label></td>
|
||||
<td><input id="dateMinutes" type="number" min="0" max="59" size="1" value="0" /></td>
|
||||
<td><label for="dateSeconds">Second:</label></td>
|
||||
<td><input id="dateSeconds" type="number" min="0" max="59" size="1" value="0" /></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="10">
|
||||
<h3>Time to periapsis:</h3>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="periapsisYears">Years:</label></td>
|
||||
<td><input id="periapsisYears" type="number" min="0" size="2" value="0" /></td>
|
||||
<td><label for="periapsisDays">Days:</label></td>
|
||||
<td><input id="periapsisDays" type="number" min="0" max="424" size="1" value="0" /></td>
|
||||
<td><label for="periapsisHours">Hours:</label></td>
|
||||
<td><input id="periapsisHours" type="number" min="0" max="5" size="1" value="0" /></td>
|
||||
<td><label for="periapsisMinutes">Minutes:</label></td>
|
||||
<td><input id="periapsisMinutes" type="number" min="0" max="59" size="1" value="0" /></td>
|
||||
<td><label for="periapsisSeconds">Seconds:</label></td>
|
||||
<td><input id="periapsisSeconds" type="number" min="0" max="59" size="1" value="0" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3>Planet:</h3>
|
||||
<input type="radio" id="kerbol" name="planet" value="kerbol">
|
||||
<label for="kerbol">Kerbol</label>
|
||||
<br />
|
||||
<input type="radio" id="moho" name="planet" value="moho">
|
||||
<label for="moho">Moho</label>
|
||||
<br />
|
||||
<input type="radio" id="eve" name="planet" value="eve">
|
||||
<label for="eve">Eve</label>
|
||||
<br />
|
||||
<input type="radio" id="gilly" name="planet" value="gilly">
|
||||
<label for="gilly">Gilly</label>
|
||||
<br />
|
||||
<input type="radio" id="kerbin" name="planet" value="kerbin" checked>
|
||||
<label for="kerbin">Kerbin</label>
|
||||
<br />
|
||||
<input type="radio" id="mun" name="planet" value="mun">
|
||||
<label for="mun">Mun</label>
|
||||
<br />
|
||||
<input type="radio" id="minmus" name="planet" value="minmus">
|
||||
<label for="minmus">Minmus</label>
|
||||
<br />
|
||||
<input type="radio" id="duna" name="planet" value="duna">
|
||||
<label for="duna">Duna</label>
|
||||
<br />
|
||||
<input type="radio" id="ike" name="planet" value="ike">
|
||||
<label for="ike">Ike</label>
|
||||
<br />
|
||||
<input type="radio" id="dres" name="planet" value="dres">
|
||||
<label for="dres">Dres</label>
|
||||
<br />
|
||||
<input type="radio" id="jool" name="planet" value="jool">
|
||||
<label for="jool">Jool</label>
|
||||
<br />
|
||||
|
||||
<h3>Orbital parameters:</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td><label for="currentPeriapsis">Periapsis (m):</label></td>
|
||||
<td><input id="currentPeriapsis" type="number" size="10" value="0.0" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="currentApoapsis">Apoapsis (m):</label></td>
|
||||
<td><input id="currentApoapsis" type="number" size="10" value="0.0" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="currentInclination">Inclination (degrees):</label></td>
|
||||
<td><input id="currentInclination" type="number" size="10" value="0.0" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="currentLAN">Longitude of ascending node (degrees):</label></td>
|
||||
<td><input id="currentLAN" type="number" size="10" value="0.0" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="currentAOP">Argument of peripasis (degrees):</label></td>
|
||||
<td><input id="currentAOP" type="number" size="10" value="0.0" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3>What to calculate:</h3>
|
||||
<input type="radio" id="coordinates" name="typeOfcalculation" value="coordinates" />
|
||||
<label for="coordinates">Calculate coordinates</label>
|
||||
<input type="radio" id="simplePlaneChange" name="typeOfcalculation" value="simplePlaneChange" />
|
||||
<label for="simplePlaneChange">Simple plane change</label>
|
||||
<input type="radio" id="orbitChange" name="typeOfcalculation" value="orbitChange" />
|
||||
<label for="orbitChange">Orbit change</label>
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<div id="coordinatesDiv">
|
||||
<button id="calculateCoordinatesButton">Perform calculation</button>
|
||||
<table>
|
||||
<tr>
|
||||
<td><label for="calculatedMeanAnomaly">Mean anomaly:</label></td><td><input id="calculatedMeanAnomaly" type="number" disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="calculatedEccentricAnomaly">Eccentric anomaly:</label></td><td><input id="calculatedEccentricAnomaly" type="number" disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="calculatedX">Position X:</label></td><td><input id="calculatedX" type="number" disabled /></td>
|
||||
<td><label for="calculatedY">Position Y:</label></td><td><input id="calculatedY" type="number" disabled /></td>
|
||||
<td><label for="calculatedZ">Position Z:</label></td><td><input id="calculatedZ" type="number" disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="calculatedLatitude">Latitude:</label></td><td><input id="calculatedLatitude" type="number" disabled /></td>
|
||||
<td><label for="calculatedLongitude">Longitude:</label></td><td><input id="calculatedLongitude" type="number" disabled /></td>
|
||||
<td><label for="calculatedPlanetLongitude">Longitude over planet:</label></td><td><input id="calculatedPlanetLongitude" type="number" disabled /></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="simplePlaneChangeDiv">
|
||||
<h3>Target plane:</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td><label for="targetInclination">Target inclination:</label></td>
|
||||
<td><input id="targetInclination" type="number" value="0.0" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="targetLAN">Target longitude of ascending node:</label></td>
|
||||
<td><input id="targetLAN" type="number" value="0.0" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
<input type="checkbox" id="circularizeOrbit" /><label for="circularizeOrbit">Circularize orbit</label>
|
||||
<br />
|
||||
<button id="simplePlaneChangeButton">Perform calculation</button>
|
||||
|
||||
<h3>Choose one of the following manoeuvres:</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<th colspan="2">Manoeuvre 1</th><th colspan="2">Manoeuvre 2</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="simpleManoeuvreTime1">Time: </label></td><td><input id="simpleManoeuvreTime1" type="number" disabled /></td>
|
||||
<td><label for="simpleManoeuvreTime2">Time: </label></td><td><input id="simpleManoeuvreTime2" type="number" disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="simpleManoeuvrePrograde1">Prograde acceleration:</label></td><td><input id="simpleManoeuvrePrograde1" type="number" disabled /></td>
|
||||
<td><label for="simpleManoeuvrePrograde2">Prograde acceleration:</label></td><td><input id="simpleManoeuvrePrograde2" type="number" disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="simpleManoeuvreNormal1">Normal acceleration:</label></td><td><input id="simpleManoeuvreNormal1" type="number" disabled /></td>
|
||||
<td><label for="simpleManoeuvreNormal2">Normal acceleration:</label></td><td><input id="simpleManoeuvreNormal2" type="number" disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="simpleManoeuvreRadial1">Radial acceleration:</label></td><td><input id="simpleManoeuvreRadial1" type="number" disabled /></td>
|
||||
<td><label for="simpleManoeuvreRadial2">Radial acceleration:</label></td><td><input id="simpleManoeuvreRadial2" type="number" disabled /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="simpleManoeuvreTotal1">Total acceleration:</label></td><td><input id="simpleManoeuvreTotal1" type="number" disabled /></td>
|
||||
<td><label for="simpleManoeuvreTotal2">Total acceleration:</label></td><td><input id="simpleManoeuvreTotal2" type="number" disabled /></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="orbitChangeDiv">
|
||||
<h3>Target orbit:</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td><label for="orbitChangeTargetInclination">Target inclination:</label></td>
|
||||
<td><input id="orbitChangeTargetInclination" type="number" value="0.0" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="orbitChangeTargetLAN">Target longitude of ascending node:</label></td>
|
||||
<td><input id="orbitChangeTargetLAN" type="number" value="0.0" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="orbitChangeTargetAOP">Target argument of periapsis:</label></td>
|
||||
<td><input id="orbitChangeTargetAOP" type="number" value="0.0" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="orbitChangeTargetPeriapsis">Target periapsis:</label></td>
|
||||
<td><input id="orbitChangeTargetPeriapsis" type="number" value="0.0" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="orbitChangeTargetApoapsis">Target apoapsis:</label></td>
|
||||
<td><input id="orbitChangeTargetApoapsis" type="number" value="0.0" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
<button id="orbitChangeButton">Find best transfer</button>
|
||||
</div>
|
||||
|
||||
<h3>Current time in game:</h3>
|
||||
<div id="timesDiv"></div>
|
||||
<h3>Time to periapsis:</h3>
|
||||
<div id="periapsisDiv"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<h3>Planet:</h3>
|
||||
<div id="planetsDiv"></div>
|
||||
<h3>Orbital parameters:</h3>
|
||||
<div id="orbitalParametersDiv"></div>
|
||||
<h3>What to calculate:</h3>
|
||||
<div id="calculationChoice"></div>
|
||||
<div id="calculation"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
21
package-lock.json
generated
21
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^6.0.2",
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<string, Body>([
|
||||
[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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
87
src/gui/common.ts
Normal file
87
src/gui/common.ts
Normal file
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
174
src/gui/orbit.ts
Normal file
174
src/gui/orbit.ts
Normal file
@ -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", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
61
src/gui/planet.ts
Normal file
61
src/gui/planet.ts
Normal file
@ -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));
|
||||
}
|
||||
}
|
||||
226
src/gui/simpleplanechange.ts
Normal file
226
src/gui/simpleplanechange.ts
Normal file
@ -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<number>;
|
||||
timeToPeriapsis: Wrapper<number>;
|
||||
planet: Wrapper<Body>;
|
||||
startingOrbitalParameters: Wrapper<OrbitalParameters>;
|
||||
goalOrbitalParameters: Wrapper<OrbitalParameters>;
|
||||
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<OrbitalParameters>, goalParameters: Wrapper<OrbitalParameters>, currentTime: Wrapper<number>, timeToPeriapsis: Wrapper<number>, planet: Wrapper<Body>, 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));
|
||||
}
|
||||
}
|
||||
209
src/gui/targetorbit.ts
Normal file
209
src/gui/targetorbit.ts
Normal file
@ -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<OrbitalParameters>;
|
||||
goalParameters: Wrapper<OrbitalParameters>;
|
||||
currentTime: Wrapper<number>;
|
||||
timeToPeriapsis: Wrapper<number>;
|
||||
body: Wrapper<Body>;
|
||||
|
||||
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<OrbitalParameters>, goalParameters: Wrapper<OrbitalParameters>, currentTime: Wrapper<number>, timeToPeriapsis: Wrapper<number>, body: Wrapper<Body>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
112
src/gui/time.ts
Normal file
112
src/gui/time.ts
Normal file
@ -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));
|
||||
}
|
||||
}
|
||||
54
src/gui/worker.ts
Normal file
54
src/gui/worker.ts
Normal file
@ -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);
|
||||
}
|
||||
});
|
||||
314
src/main.ts
314
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();
|
||||
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();
|
||||
});
|
||||
24
src/storage.ts
Normal file
24
src/storage.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export interface Wrapper<Type> {
|
||||
value: Type;
|
||||
}
|
||||
|
||||
export function createLocalStorageVariable<Type>(variableName: string, decoder: (a: string) => Type, encoder: (a: Type) => string, defaultValue: Type): [Wrapper<Type>, (value: Type) => void] {
|
||||
var localStorageString = localStorage.getItem(variableName);
|
||||
var variable: Wrapper<Type>;
|
||||
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];
|
||||
}
|
||||
115
src/style.css
115
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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user