diff --git a/src/calculations/constants.ts b/src/calculations/constants.ts index 822868c..396be46 100644 --- a/src/calculations/constants.ts +++ b/src/calculations/constants.ts @@ -39,7 +39,7 @@ export const Eve: Body = { rotationPeriod: 80500, sphereOfInfluence: 85109365, closestSafeDistance: 790000, - initialMeridianLongitude: 0 + initialMeridianLongitude: 0.00040726270866286995 }; export const Gilly: Body = { @@ -50,7 +50,7 @@ export const Gilly: Body = { rotationPeriod: 28255, sphereOfInfluence: 126123.27, closestSafeDistance: 19400, - initialMeridianLongitude: 0.0859373 + initialMeridianLongitude: 0.08784209395031439 }; export const Kerbin: Body = { diff --git a/src/calculations/mathematics.ts b/src/calculations/mathematics.ts index eaaeb2b..4dd7751 100644 --- a/src/calculations/mathematics.ts +++ b/src/calculations/mathematics.ts @@ -133,6 +133,27 @@ export function addVector(vectorOne: number[][], vectorTwo: number[][]): number[ return result; } +export function addMatrix(matrixOne: number[][], matrixTwo: number[][]): number[][] { + if (!checkIfValidMatrix(matrixOne) || !checkIfValidMatrix(matrixTwo)) { + throw new TypeError("Two valid matrices are required"); + } + + if (matrixOne.length != matrixTwo.length || matrixOne[0].length != matrixTwo[0].length) { + throw new TypeError("The two matrices need to have the same dimensions"); + } + + var result = []; + for (var i = 0; i < matrixOne.length; i++) { + let row = []; + for (var j = 0; j < matrixOne[0].length; j++) { + row.push(matrixOne[i][j] + matrixTwo[i][j]); + } + result.push(row); + } + + return result; +} + export function subtractVector(vectorOne: number[][], vectorTwo: number[][]): number[][] { if (!checkIfValidMatrix(vectorOne) || !checkIfValidMatrix(vectorTwo)) { throw new TypeError("Two valid matrices are required"); diff --git a/src/calculations/orbit-calculations.ts b/src/calculations/orbit-calculations.ts index be69f55..edd428c 100644 --- a/src/calculations/orbit-calculations.ts +++ b/src/calculations/orbit-calculations.ts @@ -1,5 +1,6 @@ +import type { InterpolationParameters } from "../gui/interpolate"; import type { Body } from "./constants"; -import { addVector, getVectorMagnitude, multiplyMatrixWithScalar, normalizeVector, subtractVector, vectorCrossProduct, vectorDotProduct } from "./mathematics"; +import { addMatrix, addVector, getVectorMagnitude, matrixMultiply, multiplyMatrixWithScalar, normalizeVector, subtractVector, vectorCrossProduct, vectorDotProduct } from "./mathematics"; export interface Orbit { semiLatusRectum: number, @@ -32,12 +33,42 @@ export interface Manoeuvre { export interface Transfer { transferOrbit: Orbit, + transferOrbitTrueAnomalyAtManoeuvreOne: number, + transferOrbitTrueanomalyAtManoeuvreTwo: number, firstManoeuvre: Manoeuvre, secondManoeuvre: Manoeuvre, farthestPointDistance: number, closestPointDistance: number, } +export interface Landing { + transferOrbit: Orbit, + transferManoeuvre: Manoeuvre, + brakingDeltaV: number, + brakingAltitude: number, + brakingTimestamp: number, + totalDeltaV: number +} + +export interface ShipParameters { + mass: number, + thrust: number, + specificImpulse: number +}; + +export interface LandingParameters { + targetLatitude: number; + targetLongitude: number; + targetAltitude: number; + minimumAngle: number; +}; + +export const DefaultShipParameters: ShipParameters = { + mass: 1000, + thrust: 100, + specificImpulse: 100 +}; + export class LambertSolutions { // 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 @@ -161,6 +192,11 @@ export class LambertSolutions { transferGoalTrueAnomaly -= 2 * Math.PI; } + if (transferStartTrueAnomaly < 2 * Math.PI && transferGoalTrueAnomaly > 2 * Math.PI) { + transferStartTrueAnomaly -= 2 * Math.PI; + transferGoalTrueAnomaly -= 2 * Math.PI; + } + let closestPoint; if (transferStartTrueAnomaly < 0 && transferGoalTrueAnomaly >= 0) { closestPoint = semiLatusRectum / (1 + eccentricity); @@ -220,6 +256,8 @@ export class LambertSolutions { return { transferOrbit: transferOrbit, + transferOrbitTrueAnomalyAtManoeuvreOne: transferStartTrueAnomaly, + transferOrbitTrueanomalyAtManoeuvreTwo: transferGoalTrueAnomaly, firstManoeuvre: startManoeuvre, secondManoeuvre: goalManoeuvre, closestPointDistance: closestPoint, @@ -308,14 +346,15 @@ export function getEccentricAndTrueAnomalyFromMeanAnomaly(meanAnomaly: number, e keplerEquationDerivative = (guess: number) => eccentricity * Math.cosh(guess) - 1; } - while (Math.abs(keplerEquation(eccentricAnomaly)) > 0.000001) { + while (Math.abs(keplerEquation(eccentricAnomaly)) > 0.0000000001) { eccentricAnomaly = eccentricAnomaly - keplerEquation(eccentricAnomaly) / keplerEquationDerivative(eccentricAnomaly); } if (eccentricity < 1) { - trueAnomaly = 2*Math.atan(Math.sqrt((1 + eccentricity) / (1 - eccentricity)) * Math.tan(eccentricAnomaly / 2)); + let beta = eccentricity / (1 + Math.sqrt(1 - eccentricity**2)); + trueAnomaly = eccentricAnomaly + 2 * Math.atan(beta * Math.sin(eccentricAnomaly) / (1 - beta * Math.cos(eccentricAnomaly))); } else { - trueAnomaly = 2*Math.atan(Math.sqrt((eccentricity + 1) / (eccentricity - 1)) * Math.tanh(eccentricAnomaly / 2)); + trueAnomaly = Math.atan2(Math.sqrt(eccentricity**2 - 1) * Math.sinh(eccentricAnomaly), (eccentricity - Math.cosh(eccentricAnomaly))); } } @@ -378,7 +417,7 @@ export function getOrbitalCoordinatesFromAltitude(altitude: number, headingInwar meanAnomaly = eccentricAnomaly; } else if (orbit.eccentricity < 1) { eccentricAnomaly = Math.atan2(Math.sqrt(1 - orbit.eccentricity**2)*Math.sin(trueAnomaly), orbit.eccentricity + Math.cos(trueAnomaly)); - meanAnomaly = eccentricAnomaly - eccentricAnomaly * Math.sin(eccentricAnomaly); + meanAnomaly = eccentricAnomaly - orbit.eccentricity * Math.sin(eccentricAnomaly); } else { eccentricAnomaly = 2 * Math.atanh(Math.sqrt((orbit.eccentricity - 1) / (orbit.eccentricity + 1)) * Math.tan(trueAnomaly / 2)); meanAnomaly = orbit.eccentricity * Math.sinh(eccentricAnomaly) - eccentricAnomaly; @@ -625,6 +664,7 @@ export function findCheapestLambertSolution(lambertSolutions: LambertSolutions): } export type ProgressCallbackFunction = (transfersChecked: number, totalNumberOfTransfers: number, currentBestDeltaV: number | null, currentBestTransfer: Transfer | null) => void; +export type LandingProgressCallbackFunction = (pathsChecked: number, totalNumberOfPaths: number, currentBestDeltaV: number | null, currentBestLanding: Landing | null) => void; export function findCheapestTransfer(startingSituation: OrbitalCoordinates, targetOrbit: Orbit, body: Body, progressCallback?: ProgressCallbackFunction): Transfer | null { // First, create a set of starting true anomalies @@ -647,7 +687,7 @@ export function findCheapestTransfer(startingSituation: OrbitalCoordinates, targ } else { let finalAnomaly = Math.abs(Math.acos((startingSituation.orbit.semiLatusRectum - body.sphereOfInfluence) / (body.sphereOfInfluence * startingSituation.orbit.eccentricity))); let step = (finalAnomaly - startingSituation.trueAnomaly) / 100; - for (var i = 1; i < 101; i++) { + for (var i = 0; i < 101; i++) { startingTrueAnomalies.push(startingSituation.trueAnomaly + i * step); } } @@ -712,70 +752,56 @@ export function findCheapestTransfer(startingSituation: OrbitalCoordinates, targ return bestTransfer; } -export function findLambertSolutionsWithCorrectTime(lambertSolutions: LambertSolutions, expectedTime: number): Transfer | null{ - const step = lambertSolutions.parabolaGamma - lambertSolutions.extremalGamma; +export function findLambertSolutionsWithCorrectTime(lambertSolutions: LambertSolutions, expectedTime: number): Transfer | null { + // Do a secant method search. Start near the parabola + let scale = Math.abs(lambertSolutions.parabolaGamma - lambertSolutions.extremalGamma); + let x0 = lambertSolutions.parabolaGamma; + let x1 = lambertSolutions.parabolaGamma + scale / 100; - const getGamma = (steps: number) => { - return lambertSolutions.extremalGamma + steps*step; - } + let t0 = lambertSolutions.getTransfer(x0); + let t1 = lambertSolutions.getTransfer(x1); - let lowerSteps = 0; - let lowerTransfer = lambertSolutions.getTransfer(getGamma(lowerSteps)); - let lowerTime = 0; + let f0 = t0.secondManoeuvre.time - expectedTime; + let f1 = t1.secondManoeuvre.time - expectedTime; - let upperSteps = 1; - let upperTransfer = lambertSolutions.getTransfer(getGamma(upperSteps)); - let upperTime = upperTransfer.secondManoeuvre.time; + let f2 = 100; + let t2 = null; - let foundWindow = false; - // Find window that contains transfer - for (let windowAttempts = 0; windowAttempts < 100; windowAttempts++) { - if (upperTime < 0 || expectedTime < upperTime) { - foundWindow = true; + let counter = 0; + + while (Math.abs(f2) > 0.5 && counter < 100) { + counter++; + let divisor = 1; + let step = f1 * (x1 - x0) / (f1 - f0); + + let manoeuvreTime = -1; + let x2 = 0; + let innerCounter = 0; + while ((isNaN(manoeuvreTime) || manoeuvreTime < 0) && innerCounter < 10) { + innerCounter++; + x2 = x1 - step / (2**divisor); + t2 = lambertSolutions.getTransfer(x2); + manoeuvreTime = t2.secondManoeuvre.time; + divisor += 1; + } + + if (innerCounter == 10) { break; } - lowerSteps = upperSteps; - lowerTransfer = upperTransfer; - lowerTime = upperTime; + f2 = manoeuvreTime - expectedTime; - upperSteps += 1; - upperTransfer = lambertSolutions.getTransfer(getGamma(upperSteps)); - upperTime = upperTransfer.secondManoeuvre.time; + x0 = x1; + x1 = x2; + f0 = f1; + f1 = f2; } - if (!foundWindow) { + if (Math.abs(f2) <= 0.5) { + return t2; + } else { return null; } - - const acceptablyClose = 0.5; - const numberOfAttempts = 200; - - for (let attempts = 0; attempts < numberOfAttempts; attempts++) { - if (Math.abs(lowerTime - expectedTime) < acceptablyClose) { - return lowerTransfer; - } - - if (Math.abs(upperTime - expectedTime) < acceptablyClose) { - return upperTransfer; - } - - let middleSteps = (upperSteps + lowerSteps) / 2; - let middleTransfer = lambertSolutions.getTransfer(getGamma(middleSteps)); - let middleTime = middleTransfer.secondManoeuvre.time; - - if (middleTime > 0 && middleTime < expectedTime) { - lowerSteps = middleSteps; - lowerTransfer = middleTransfer; - lowerTime = middleTime; - } else { - upperSteps = middleSteps; - upperTransfer = middleTransfer; - upperTime = middleTime; - } - } - - return null; } export function findCheapestIntercept(startingSituation: OrbitalCoordinates, targetSituation: OrbitalCoordinates, body: Body, extraTrueAnomaly: number, progressCallback?: ProgressCallbackFunction): Transfer | null { @@ -823,12 +849,14 @@ export function findCheapestIntercept(startingSituation: OrbitalCoordinates, tar if (!startingOrbitStable) { maxStartTrueAnomaly = Math.abs(Math.acos((startingSituation.orbit.semiLatusRectum - body.sphereOfInfluence) / (body.sphereOfInfluence * startingSituation.orbit.eccentricity))); maxStartTime = getTimeBetweenTrueAnomalies(startingSituation.trueAnomaly, maxStartTrueAnomaly, startingSituation.orbit, body); - maxEndTime = 2*maxStartTime; } if (!interceptOrbitStable) { maxEndTrueAnomaly = Math.abs(Math.acos((targetSituation.orbit.semiLatusRectum - body.sphereOfInfluence) / (body.sphereOfInfluence * targetSituation.orbit.eccentricity))); maxEndTime = getTimeBetweenTrueAnomalies(targetSituation.trueAnomaly, maxEndTrueAnomaly, targetSituation.orbit, body); + } else { + // Set the max end time to the max start time plus two full orbits of the target + maxEndTime = maxStartTime + 4 * Math.PI * Math.sqrt((targetSituation.orbit.semiLatusRectum / (1 - targetSituation.orbit.eccentricity**2))**3 / body.gravitationalParameter); } } @@ -899,4 +927,487 @@ export function findCheapestIntercept(startingSituation: OrbitalCoordinates, tar }); return bestTransfer; +} + +export function findCheapestLanding(startingCoordinates: OrbitalCoordinates, startTime: number, ship: ShipParameters, landingParameters: LandingParameters, body: Body, progressCallback?: LandingProgressCallbackFunction): Landing | null { + // Starting points are a list of true anomalies with corresponding times + let startingPoints: [number, number][] = []; + + let minimumStartingTrueAnomaly = startingCoordinates.trueAnomaly; + let maximumStartingTrueAnomaly = startingCoordinates.trueAnomaly + 2 * Math.PI; + + let boundedAbove = false; + + // Check if we're on a collision course with the planet + let minimumAltitude = startingCoordinates.orbit.semiLatusRectum / (1 + startingCoordinates.orbit.eccentricity); + + // If we're colliding, we need to do our manoeuvre quickly + if (minimumAltitude < body.closestSafeDistance) { + let criticalAnomaly = Math.acos((startingCoordinates.orbit.semiLatusRectum - body.closestSafeDistance) / (body.closestSafeDistance * startingCoordinates.orbit.eccentricity)); + + // We might be in a weird orbit where we're on the way away from the planet, in which case we shouldn't bound our starting + if (minimumStartingTrueAnomaly < -criticalAnomaly) { + boundedAbove = true; + maximumStartingTrueAnomaly = -criticalAnomaly; + } + } + + // If we're on a parabolic or hyperbolic orbit, or one that will leave the planet's sphere of influence, set an upper bound for the anomaly + if (!boundedAbove) { + let goesOutsideSphereOfIncluence = false; + if (startingCoordinates.orbit.eccentricity < 1) { + let maximumAltitude = startingCoordinates.orbit.semiLatusRectum / (1 - startingCoordinates.orbit.eccentricity); + if (maximumAltitude > body.sphereOfInfluence) { + goesOutsideSphereOfIncluence = true; + } + } else { + goesOutsideSphereOfIncluence = true; + } + + if (goesOutsideSphereOfIncluence) { + maximumStartingTrueAnomaly = Math.acos((startingCoordinates.orbit.semiLatusRectum - body.sphereOfInfluence) / (body.sphereOfInfluence * startingCoordinates.orbit.eccentricity)); + } + } + + // Our set of starting points is decided + let stepLength = (maximumStartingTrueAnomaly - minimumStartingTrueAnomaly) / 100.0; + for (let i = 0; i < 100; i++) { + let testAnomaly = minimumStartingTrueAnomaly + i * stepLength; + let timeUntilAnomaly = getTimeBetweenTrueAnomalies(minimumStartingTrueAnomaly, testAnomaly, startingCoordinates.orbit, body); + startingPoints.push([testAnomaly, timeUntilAnomaly + startTime]); + }; + + let bestLanding: Landing | null = null; + let bestLandingDeltaV: number | null = null; + + // We need to simulate the ship braking, in order to refine the manoeuvre + const simulateBraking = (transferTrueAnomaly: number, transferOrbit: Orbit, body: Body, ship: ShipParameters) => { + let localVectors = getLocalVectors(transferTrueAnomaly, transferOrbit); + let speed = getSpeed(transferTrueAnomaly, transferOrbit, body); + let velocity = multiplyMatrixWithScalar(speed, localVectors.prograde); + + let radius = transferOrbit.semiLatusRectum / (1 + transferOrbit.eccentricity * Math.cos(transferTrueAnomaly)); + let localX = radius * Math.cos(transferTrueAnomaly); + let localY = radius * Math.sin(transferTrueAnomaly); + + let startingPosition = addVector( + multiplyMatrixWithScalar(localX, transferOrbit.coordinateAxes[0]), + multiplyMatrixWithScalar(localY, transferOrbit.coordinateAxes[1]) + ); + + let position = startingPosition; + + let mass = ship.mass; + let massUse = ship.thrust / ship.specificImpulse; + + let timeSteps = 0; + let lengthOfStep = 0.01; + + let totalDeltaV = 0; + + while (true) { + let radius = getVectorMagnitude(position); + let downwardsVector = multiplyMatrixWithScalar(-1 / radius, position); + let gravityAcceleration = body.gravitationalParameter / radius**2; + let gravityVector = multiplyMatrixWithScalar(gravityAcceleration, downwardsVector); + + let latitude = Math.atan2(position[2][0], Math.sqrt(position[0][0]**2 + position[1][0]**2)); + let longitude = Math.atan2(position[1][0], position[0][0]); + + let surfaceSpeed = body.radius * Math.cos(latitude) * 2 * Math.PI / body.rotationPeriod; + let surfaceVelocity = [ + [-surfaceSpeed * Math.sin(longitude)], + [surfaceSpeed * Math.cos(longitude)], + [0] + ]; + + let excessVelocity = subtractVector(velocity, surfaceVelocity); + let excessSpeed = getVectorMagnitude(excessVelocity); + + let engineAcceleration = ship.thrust / mass; + if (excessSpeed < lengthOfStep * engineAcceleration) { + break; + } + + let engineAccelerationVector = multiplyMatrixWithScalar(-engineAcceleration, normalizeVector(excessVelocity)); + let totalAcceleration = addVector(engineAccelerationVector, gravityVector); + + position = addVector(position, addVector(multiplyMatrixWithScalar(lengthOfStep, velocity), multiplyMatrixWithScalar(lengthOfStep**2 / 2, totalAcceleration))); + velocity = addVector(velocity, multiplyMatrixWithScalar(lengthOfStep, totalAcceleration)); + mass -= massUse * lengthOfStep; + totalDeltaV += engineAcceleration * lengthOfStep; + + if (mass < 0) { + return null; + } + + timeSteps += 1; + } + + return { + timeSpent: timeSteps * lengthOfStep, + finalPosition: position, + deltaVSpent: totalDeltaV + }; + }; + + const createFakeOrbit = (position: number[][]): Orbit => { + let radius = getVectorMagnitude(position); + let longitude = Math.atan2(position[1][0], position[0][0]); + let latitude = Math.atan2(position[2][0], Math.sqrt(position[0][0]**2 + position[1][0]**2)); + + if (latitude > 0) { + return getOrbit(radius, radius, latitude, longitude - Math.PI / 2, Math.PI / 2); + } else { + return getOrbit(radius, radius, -latitude, longitude + Math.PI / 2, -Math.PI / 2); + } + } + + let freefallMultiplier = Math.PI / (2 * Math.sqrt(2 * body.gravitationalParameter)); + startingPoints.forEach(([trueAnomaly, startTime], startingIndex) => { + // Test 100 different landing times at each true anomaly. We'll set the upper bound for the transfer time to be one and a + // half the time it takes to free fall from the current position + let currentRadius = startingCoordinates.orbit.semiLatusRectum / (1 + startingCoordinates.orbit.eccentricity * Math.cos(trueAnomaly)); + let freeFallTime = freefallMultiplier * currentRadius ** (3 / 2); + + for (let i = 1; i < 101; i++) { + let targetTime = 1.5 * freeFallTime * i / 100; + let rotatedTargetLongitude = landingParameters.targetLongitude + body.initialMeridianLongitude + (targetTime + startTime) * 2 * Math.PI / body.rotationPeriod; + + let targetX = (landingParameters.targetAltitude + body.radius) * Math.cos(landingParameters.targetLatitude) * Math.cos(rotatedTargetLongitude); + let targetY = (landingParameters.targetAltitude + body.radius) * Math.cos(landingParameters.targetLatitude) * Math.sin(rotatedTargetLongitude); + let targetZ = (landingParameters.targetAltitude + body.radius) * Math.sin(landingParameters.targetLatitude); + + let target = [[targetX], [targetY], [targetZ]]; + + // Make up an orbit that goes through this point, so we can use it with the Lambert solver I made earlier that always + // assumes you want to go between two orbits + let fakeTargetOrbit = createFakeOrbit(target); + + [true, false].forEach(backwards => { + let lambertSolutions = new LambertSolutions(startingCoordinates.orbit, trueAnomaly, fakeTargetOrbit, 0, body, backwards); + let possibleTransfer = findLambertSolutionsWithCorrectTime(lambertSolutions, targetTime); + + if (possibleTransfer) { + // Throw out any transfers that go below the surface and come up on our rendezvous + if (possibleTransfer.closestPointDistance < landingParameters.targetAltitude + body.radius - 1) { + return; + } + + // Check if the transfer comes in steep enough + let localVectors = getLocalVectors(possibleTransfer.transferOrbitTrueanomalyAtManoeuvreTwo, possibleTransfer.transferOrbit); + let targetDownwardsVector = multiplyMatrixWithScalar(-1, normalizeVector(target)); + + let maximumAngle = Math.PI / 2 - landingParameters.minimumAngle; + let angle = Math.acos(vectorDotProduct(localVectors.prograde, targetDownwardsVector)); + if (angle > maximumAngle) { + return; + } + + // Do some iterative searching to find a transfer that lands in the middle + let foundGoodLanding = false; + let previousTargetPosition = target; + let previousTargetTime = targetTime; + + let landingCounter = 0; + + while (!foundGoodLanding && landingCounter < 100) { + landingCounter++; + let massAfterTransfer = ship.mass / (Math.exp(possibleTransfer.firstManoeuvre.totalDeltaV / (9.81 * ship.specificImpulse))); + let testShip: ShipParameters = { + thrust: ship.thrust, + mass: massAfterTransfer, + specificImpulse: ship.specificImpulse + }; + + let simulationResult = simulateBraking(possibleTransfer.transferOrbitTrueanomalyAtManoeuvreTwo, possibleTransfer.transferOrbit, body, testShip); + if (simulationResult == null) { + return; + } + let difference = subtractVector(simulationResult.finalPosition, target); + let timeDifference = possibleTransfer.secondManoeuvre.time + simulationResult.timeSpent - targetTime; + let differenceMagnitude = getVectorMagnitude(difference); + + if (timeDifference > 100 || differenceMagnitude > 10) { + // We're too far away, try another maneuver + let newTarget = subtractVector(previousTargetPosition, difference); + let newTime = previousTargetTime - timeDifference; + let newTargetOrbit = createFakeOrbit(newTarget); + + previousTargetPosition = newTarget; + previousTargetTime = newTime; + + lambertSolutions = new LambertSolutions(startingCoordinates.orbit, trueAnomaly, newTargetOrbit, 0, body, backwards); + possibleTransfer = findLambertSolutionsWithCorrectTime(lambertSolutions, newTime); + + if (!possibleTransfer) { + return; + } + } else { + foundGoodLanding = true; + + possibleTransfer.firstManoeuvre.time += startTime; + possibleTransfer.secondManoeuvre.time += startTime; + + let totalDeltaV = possibleTransfer.firstManoeuvre.totalDeltaV + simulationResult.deltaVSpent; + if (bestLandingDeltaV == null || totalDeltaV < bestLandingDeltaV) { + let secondManoeuvreAltitude = possibleTransfer.transferOrbit.semiLatusRectum / (1 + possibleTransfer.transferOrbit.eccentricity * Math.cos(possibleTransfer.transferOrbitTrueanomalyAtManoeuvreTwo)) - body.radius; + + let landing: Landing = { + transferOrbit: possibleTransfer.transferOrbit, + transferManoeuvre: possibleTransfer.firstManoeuvre, + brakingDeltaV: simulationResult.deltaVSpent, + brakingAltitude: secondManoeuvreAltitude, + brakingTimestamp: possibleTransfer.secondManoeuvre.time, + totalDeltaV: totalDeltaV + }; + + bestLanding = landing; + bestLandingDeltaV = totalDeltaV; + } + } + }; + } + }); + + if (progressCallback) { + progressCallback(startingIndex * 100 + i, 100*100, bestLandingDeltaV, bestLanding); + } + } + }); + + return bestLanding; +} + +export function findOrbitThroughInterpolation(ownCoordinates: OrbitalCoordinates, interpolationParameters: InterpolationParameters, body: Body): [OrbitalCoordinates, number] | null { + let targetPeriapsis = interpolationParameters.targetPeriapsis; + + let targetEccentricity; + let targetSemiLatusRectum; + if (interpolationParameters.orbitChoice == "apoapsis") { + let targetApoapsis = interpolationParameters.targetApoapsis; + const semiMajor = (targetPeriapsis + targetApoapsis) / 2; + const linearEccentricity = semiMajor - targetPeriapsis; + targetEccentricity = linearEccentricity / semiMajor; + targetSemiLatusRectum = semiMajor * (1 - targetEccentricity**2); + } else { + let semiMajorInverse = 2 / interpolationParameters.targetAltitude - interpolationParameters.targetSpeed**2 / body.gravitationalParameter; + + // If this is zero, we have a parabolic trajectory + if (Math.abs(semiMajorInverse) < 0.0001) { + targetEccentricity = 1; + targetSemiLatusRectum = 2 * targetPeriapsis; + } else { + let semiMajor = 1 / semiMajorInverse; + // If this is positive, we have an elliptical orbit + if (semiMajor > 0) { + const linearEccentricity = semiMajor - targetPeriapsis; + targetEccentricity = linearEccentricity / semiMajor; + targetSemiLatusRectum = semiMajor * (1 - targetEccentricity**2); + } else { + const linearEccentricity = semiMajor + targetPeriapsis; + targetEccentricity = linearEccentricity / semiMajor; + targetSemiLatusRectum = semiMajor * (targetEccentricity**2 - 1); + } + } + } + + // Now, we have a pretty good description of the orbit, we only need to find the plane it lies in + // Given the first measurement of distance to planet, distance to target, and target to planet, there exists a circle + // that the target could be in + let ownShipHeadedInwards = interpolationParameters.secondOwnAltitude < interpolationParameters.firstOwnAltitude; + let targetHeadedInwards = interpolationParameters.secondTargetAltitude < interpolationParameters.firstTargetAltitude; + + // Define some helper functions for later + const getRandomPerpendicularVector = (vector: number[][]) => { + let perpendicularX = Math.random(); + let perpendicularY = Math.random(); + let perpendicularZ = Math.random(); + + let normalVector: number[][]; + if (Math.abs(vector[0][0]) > 0.0001) { + normalVector = [ + [(-vector[1][0] * perpendicularY - vector[2][0]*perpendicularZ) / vector[0][0]], + [perpendicularY], + [perpendicularZ] + ]; + } else if (Math.abs(vector[1][0]) > 0.0001) { + normalVector = [ + [perpendicularX], + [(-vector[0][0] / perpendicularX - vector[2][0] / perpendicularZ) / vector[1][0]], + [perpendicularZ] + ]; + } else { + normalVector = [ + [perpendicularX], + [perpendicularY], + [(-vector[0][0] * perpendicularX - vector[1][0] * perpendicularY) / vector[2][0]] + ] + }; + + return normalizeVector(normalVector); + }; + + const getTrueAnomalyFromAltitude = (altitude: number, semiLatusRectum: number, eccentricity: number, headingInwards: boolean) => { + let trueAnomaly = Math.acos((semiLatusRectum - altitude) / (altitude * eccentricity)); + if (headingInwards) { + trueAnomaly = -trueAnomaly; + } + return trueAnomaly; + } + + + const getPossibleTargetLocationFunction = (ownOrbit: Orbit, ownAltitude: number, targetAltitude: number, distanceToTarget: number): ((angle: number) => number[][]) => { + let ownTrueAnomaly = getTrueAnomalyFromAltitude(ownAltitude, ownOrbit.semiLatusRectum, ownOrbit.eccentricity, ownShipHeadedInwards); + + let ownLocalX = interpolationParameters.firstOwnAltitude * Math.cos(ownTrueAnomaly); + let ownLocalY = interpolationParameters.firstOwnAltitude * Math.sin(ownTrueAnomaly); + + let ownDirection = normalizeVector( + addVector( + multiplyMatrixWithScalar(ownLocalX, ownOrbit.coordinateAxes[0]), + multiplyMatrixWithScalar(ownLocalY, ownOrbit.coordinateAxes[1]) + ) + ); + + let perpendicularOne = getRandomPerpendicularVector(ownDirection); + let perpendicularTwo = normalizeVector(vectorCrossProduct(ownDirection, perpendicularOne)); + + // Find phase angle to target + let phaseAngle = Math.acos((ownAltitude**2 + targetAltitude**2 - distanceToTarget**2) / (2 * ownAltitude * targetAltitude)); + + let distanceAlongDirection = targetAltitude * Math.cos(phaseAngle); + let distanceAlongNormal = targetAltitude * Math.sin(phaseAngle); + + let root = multiplyMatrixWithScalar(distanceAlongDirection, ownDirection); + + return function(angle: number) { + let addition = addVector( + multiplyMatrixWithScalar(distanceAlongNormal * Math.cos(angle), perpendicularOne), + multiplyMatrixWithScalar(distanceAlongNormal * Math.sin(angle), perpendicularTwo) + ); + + return addVector(root, addition); + } + } + + let firstTargetFunction = getPossibleTargetLocationFunction(ownCoordinates.orbit, interpolationParameters.firstOwnAltitude, interpolationParameters.firstTargetAltitude, interpolationParameters.firstDistance); + let secondTargetFunction = getPossibleTargetLocationFunction(ownCoordinates.orbit, interpolationParameters.secondOwnAltitude, interpolationParameters.secondTargetAltitude, interpolationParameters.secondDistance); + + // Since we know a lot about the target orbit, we can figure out how far it is supposed to be between the two points + let firstTargetTrueAnomaly = getTrueAnomalyFromAltitude(interpolationParameters.firstTargetAltitude, targetSemiLatusRectum, targetEccentricity, targetHeadedInwards); + let secondTargetTrueAnomaly = getTrueAnomalyFromAltitude(interpolationParameters.secondTargetAltitude, targetSemiLatusRectum, targetEccentricity, targetHeadedInwards); + + // Use cosine rule + let targetMovement = Math.sqrt(interpolationParameters.firstTargetAltitude**2 + interpolationParameters.secondTargetAltitude**2 - 2 * interpolationParameters.firstTargetAltitude * interpolationParameters.secondTargetAltitude * Math.cos(secondTargetTrueAnomaly - firstTargetTrueAnomaly)); + + // Now, we need to do multivariate optimization to find the two angles to give to the functions that make the distance between + // two target positions as equal to the target movement as possible + const functionToOptimize = (angleOne: number, angleTwo: number) => { + let firstTargetPosition = firstTargetFunction(angleOne); + let secondTargetPosition = secondTargetFunction(angleTwo); + + return Math.abs(getVectorMagnitude(subtractVector(secondTargetPosition, firstTargetPosition)) - targetMovement); + } + + let bestAngleOne = 0; + let bestAngleTwo = 0; + let bestValue = 1e99; + + for (var i = 0; i < 100; i++) { + for (var j = 0; j < 100; j++) { + let angleOne = 2 * Math.PI * i / 100; + let angleTwo = 2 * Math.PI * j / 100; + + let value = functionToOptimize(angleOne, angleTwo); + if (value < bestValue) { + bestAngleOne = angleOne; + bestAngleTwo = angleTwo; + bestValue = value; + } + } + } + + // Do some gradient descent on the best values found so far + while (bestValue > 0.5) { + let estimateDfDx = (functionToOptimize(bestAngleOne+0.0000001, bestAngleTwo) - bestValue) / 0.0000001; + let estimateDfDy = (functionToOptimize(bestAngleOne, bestAngleTwo+0.0000001) - bestValue) / 0.0000001; + + let slopeNormal = vectorCrossProduct( + [[1], [0], [estimateDfDx]], + [[0], [1], [estimateDfDy]] + ); + + let direction = normalizeVector([[slopeNormal[0][0]], [slopeNormal[1][0]], [0]]); + let estimateDfDv = (functionToOptimize(bestAngleOne + direction[0][0]*0.0001, bestAngleTwo + direction[1][0]*0.0001) - bestValue) / 0.0001; + + let bestValueImprovement = bestValue; + let bestAngleOneUpdate = bestAngleOne; + let bestAngleTwoUpdate = bestAngleTwo; + let divisor = 0; + let deadEnd = false; + + while (bestValueImprovement >= bestValue) { + bestAngleOneUpdate = bestAngleOne - bestValue * direction[0][0] / (estimateDfDv * 2**divisor); + bestAngleTwoUpdate = bestAngleTwo - bestValue * direction[1][0] / (estimateDfDv * 2**divisor); + bestValueImprovement = functionToOptimize(bestAngleOneUpdate, bestAngleTwoUpdate); + divisor += 1; + + if (divisor >= 32) { + deadEnd = true; + break; + } + } + + if (deadEnd) { + break; + } + bestAngleOne = bestAngleOneUpdate; + bestAngleTwo = bestAngleTwoUpdate; + bestValue = bestValueImprovement; + } + + let bestFirstPosition = firstTargetFunction(bestAngleOne); + let bestSecondPosition = secondTargetFunction(bestAngleTwo); + + if (bestFirstPosition == null || bestSecondPosition == null) { + return null; + } + // We can now find the vector that defines the plane the target is in + let normalVector = normalizeVector(vectorCrossProduct(bestFirstPosition, bestSecondPosition)); + let firstTargetPositionDirection = normalizeVector(bestFirstPosition); + + // Rotate the position vector about the normal vector by the true anomaly to find the vector pointing to the periapsis + let identityMatrix = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; + let crossProductMatrix = [ + [0, -normalVector[2][0], normalVector[1][0]], + [normalVector[2][0], 0, -normalVector[0][0]], + [-normalVector[1][0], normalVector[0][0], 0] + ]; + let outerProductMatrix = [ + [normalVector[0][0]**2, normalVector[0][0]*normalVector[1][0], normalVector[0][0]*normalVector[2][0]], + [normalVector[0][0]*normalVector[1][0], normalVector[1][0]**2, normalVector[1][0]*normalVector[2][0]], + [normalVector[0][0]*normalVector[2][0], normalVector[1][0]*normalVector[2][0], normalVector[2][0]**2] + ]; + + let rotationMatrix = addMatrix( + multiplyMatrixWithScalar(Math.cos(-firstTargetTrueAnomaly), identityMatrix), + addMatrix( + multiplyMatrixWithScalar(Math.sin(-firstTargetTrueAnomaly), crossProductMatrix), + multiplyMatrixWithScalar(1 - Math.cos(-firstTargetTrueAnomaly), outerProductMatrix) + ) + ); + + let targetOrbitXVector = matrixMultiply(rotationMatrix, firstTargetPositionDirection); + let targetOrbitYVector = vectorCrossProduct(normalVector, targetOrbitXVector); + + let targetOrbit: Orbit = { + semiLatusRectum: targetSemiLatusRectum, + eccentricity: targetEccentricity, + coordinateAxes: [targetOrbitXVector, targetOrbitYVector, normalVector] + }; + + let timePassed = getTimeBetweenTrueAnomalies(ownCoordinates.trueAnomaly, getTrueAnomalyFromAltitude(interpolationParameters.secondOwnAltitude, ownCoordinates.orbit.semiLatusRectum, ownCoordinates.orbit.eccentricity, ownShipHeadedInwards), ownCoordinates.orbit, body); + + return [getOrbitalCoordinatesFromAltitude(interpolationParameters.secondTargetAltitude, targetHeadedInwards, targetOrbit, body), timePassed]; } \ No newline at end of file diff --git a/src/gui/common.ts b/src/gui/common.ts index d59021c..2219acf 100644 --- a/src/gui/common.ts +++ b/src/gui/common.ts @@ -1,5 +1,5 @@ import type { Body } from "../calculations/constants"; -import { getOrbit, getOrbitalCoordinates, getOrbitalCoordinatesFromAltitude, getOrbitFromEccentricity, type Orbit, type OrbitalCoordinates } from "../calculations/orbit-calculations"; +import { getOrbit, getOrbitalCoordinates, getOrbitalCoordinatesFromAltitude, getOrbitFromEccentricity, type LandingParameters, type Orbit, type OrbitalCoordinates } from "../calculations/orbit-calculations"; export interface OrbitalParameters { periapsis: number; @@ -33,20 +33,26 @@ export const DefaultOrbitalParameters: OrbitalParameters = { currentTimeAtReading: 0 } -export function encodeOrbitalParameters(orbitalParameters: OrbitalParameters): string { - return JSON.stringify(orbitalParameters); +export const DefaultLandingParameters: LandingParameters = { + targetLatitude: 0, + targetLongitude: 0, + targetAltitude: 0, + minimumAngle: Math.PI / 4 +}; + +export function encodeJsonObject(object: Type): string { + return JSON.stringify(object); } -export function decodeOrbitalParameters(jsonString: string): OrbitalParameters { - let parsedObject = JSON.parse(jsonString); - var orbitalParameters: OrbitalParameters; - if (parsedObject as OrbitalParameters === undefined) { - orbitalParameters = DefaultOrbitalParameters; - } else { - orbitalParameters = parsedObject; +export function getDecoderFunction(defaultValue: Type): (jsonString: string) => Type { + return function(jsonString: string): Type { + let parsedObject = JSON.parse(jsonString); + if (parsedObject as Type === undefined) { + return defaultValue; + } else { + return parsedObject; + } } - - return orbitalParameters; } export function createLabel(forId: string, labelText: string): HTMLLabelElement { diff --git a/src/gui/intercept.ts b/src/gui/intercept.ts index 30bd7a9..0708df1 100644 --- a/src/gui/intercept.ts +++ b/src/gui/intercept.ts @@ -1,10 +1,20 @@ -import { createLabel, createNumberInput, getCoordinatesFromParameters, type OrbitalParameters } from "./common"; +import { createLabel, createNumberInput, createRadioButton, getCoordinatesFromParameters, type OrbitalParameters } from "./common"; import { OrbitalParametersGui } from "./orbit"; import { type Body } from "../calculations/constants"; import type { FindBestInterceptMessage, ProgressMessage } from "./worker"; import type { ChangingStorageValue } from "../storage"; import { ManoeuvresGui } from "./manoeuvres"; -import { extrapolateTrajectory, type OrbitalCoordinates } from "../calculations/orbit-calculations"; +import { extrapolateTrajectory, findOrbitThroughInterpolation, type OrbitalCoordinates } from "../calculations/orbit-calculations"; +import { InterpolateOrbitGui, type InterpolationParameters } from "./interpolate"; + +export type TargetOrbitChoice = "KnownOrbit" | "InterpolateOrbit"; +export function decodeTargetOrbitChoice(s: string): TargetOrbitChoice { + if (s == "KnownOrbit" || s == "InterpolateOrbit") { + return s; + } + + return "KnownOrbit"; +} export class InterceptTargetGui { currentTime: ChangingStorageValue; @@ -12,33 +22,72 @@ export class InterceptTargetGui { startingOrbitalParameters: ChangingStorageValue; targetOrbitalParameters: ChangingStorageValue; additionalTrueAnomaly: ChangingStorageValue; + targetOrbitChoice: ChangingStorageValue; + interpolationParameters: ChangingStorageValue; parentDiv: HTMLDivElement; orbitGui: OrbitalParametersGui; + interpolateGui: InterpolateOrbitGui; manoeuvresGui: ManoeuvresGui; sourceId: string; worker: Worker | null; - constructor(currentTime: ChangingStorageValue, body: ChangingStorageValue, startingOrbitalParameters: ChangingStorageValue, targetOrbitalParameters: ChangingStorageValue, additionalTrueAnomaly: ChangingStorageValue) { + constructor(currentTime: ChangingStorageValue, body: ChangingStorageValue, startingOrbitalParameters: ChangingStorageValue, targetOrbitalParameters: ChangingStorageValue, additionalTrueAnomaly: ChangingStorageValue, targetOrbitChoice: ChangingStorageValue, interpolationParameters: ChangingStorageValue) { this.currentTime = currentTime; this.body = body; this.startingOrbitalParameters = startingOrbitalParameters; this.targetOrbitalParameters = targetOrbitalParameters; this.additionalTrueAnomaly = additionalTrueAnomaly; + this.targetOrbitChoice = targetOrbitChoice; + this.interpolationParameters = interpolationParameters; this.orbitGui = new OrbitalParametersGui(targetOrbitalParameters, "orbitWithPosition"); + this.interpolateGui = new InterpolateOrbitGui(this.interpolationParameters); this.manoeuvresGui = new ManoeuvresGui(true); this.worker = null; this.parentDiv = document.createElement("div"); + let choiceHeader = document.createElement("h3") + choiceHeader.appendChild(document.createTextNode("Choose target orbit type")); + + this.parentDiv.appendChild(choiceHeader); + + let targetChoiceId = crypto.randomUUID(); + let knownOrbitId = crypto.randomUUID(); + let knownOrbitButton = createRadioButton(targetChoiceId, knownOrbitId); + let knownOrbitLabel = createLabel(knownOrbitId, "Known orbit"); + this.parentDiv.appendChild(knownOrbitButton); + this.parentDiv.appendChild(knownOrbitLabel); + + let interpolateOrbitId = crypto.randomUUID(); + let interpolateOrbitButton = createRadioButton(targetChoiceId, interpolateOrbitId); + let interpolateOrbitLabel = createLabel(interpolateOrbitId, "Interpolate orbit"); + this.parentDiv.appendChild(interpolateOrbitButton); + this.parentDiv.appendChild(interpolateOrbitLabel); + + let orbitContainer = document.createElement("div"); + this.parentDiv.appendChild(orbitContainer); + + let knownOrbitContainer = document.createElement("div"); + let parametersHeader = document.createElement("h3"); parametersHeader.appendChild(document.createTextNode("Target orbit:")); - this.parentDiv.appendChild(parametersHeader); + knownOrbitContainer.appendChild(parametersHeader); + knownOrbitContainer.appendChild(this.orbitGui.parentDiv); - this.parentDiv.appendChild(this.orbitGui.parentDiv); + let interpolateOrbitContainer = document.createElement("div"); + + let interpolateOrbitHeader = document.createElement("h3"); + interpolateOrbitHeader.appendChild(document.createTextNode("Interpolate orbit:")); + interpolateOrbitContainer.appendChild(interpolateOrbitHeader); + interpolateOrbitContainer.appendChild(this.interpolateGui.parentDiv); + + let offsetHeader = document.createElement("h3"); + offsetHeader.appendChild(document.createTextNode("Offset:")); + this.parentDiv.appendChild(offsetHeader); let additionalTrueAnomalyContainer = document.createElement("div"); additionalTrueAnomalyContainer.classList.add("orbitalParameter"); @@ -49,7 +98,6 @@ export class InterceptTargetGui { let additionalTrueAnomalyInput = createNumberInput(additionalTrueAnomalyId, -360, 360); [additionalTrueanomalyLabel, additionalTrueAnomalyInput].forEach(child => additionalTrueAnomalyContainer.appendChild(child)); - let searchButton = document.createElement("button"); searchButton.appendChild(document.createTextNode("Search for cheapest intercept")); this.parentDiv.appendChild(searchButton); @@ -72,25 +120,52 @@ export class InterceptTargetGui { let body = this.body.getCurrentValue(); let startingOrbitalParameters = this.startingOrbitalParameters.getCurrentValue(); let targetOrbitalParameters = this.targetOrbitalParameters.getCurrentValue(); + let additionalTrueAnomaly = this.additionalTrueAnomaly.getCurrentValue(); + let targetOrbitChoice = this.targetOrbitChoice.getCurrentValue(); + let interpolationParameters = this.interpolationParameters.getCurrentValue(); + + interpolationParameters = structuredClone(interpolationParameters); + interpolationParameters.firstOwnAltitude += body.radius; + interpolationParameters.firstTargetAltitude += body.radius; + interpolationParameters.secondOwnAltitude += body.radius; + interpolationParameters.secondTargetAltitude += body.radius; + interpolationParameters.targetAltitude += body.radius; + interpolationParameters.targetPeriapsis += body.radius; + interpolationParameters.targetApoapsis += body.radius; + let startingCoordinates: OrbitalCoordinates | null = getCoordinatesFromParameters(startingOrbitalParameters, body); - let targetCoordinates: OrbitalCoordinates | null = getCoordinatesFromParameters(targetOrbitalParameters, body); + let startTime: number | null = null; - let startTime = Math.max(currentTime, startingOrbitalParameters.currentTimeAtReading, targetOrbitalParameters.currentTimeAtReading); + let targetStartTime = 0; - if (startTime > startingOrbitalParameters.currentTimeAtReading) { - let addedTime = startTime - startingOrbitalParameters.currentTimeAtReading; - startingCoordinates = extrapolateTrajectory(addedTime, startingCoordinates, body); + let targetCoordinates: OrbitalCoordinates | null = null; + if (targetOrbitChoice == "InterpolateOrbit") { + let result = findOrbitThroughInterpolation(startingCoordinates, interpolationParameters, body); + if (result) { + targetCoordinates = result[0]; + targetStartTime = startingOrbitalParameters.currentTimeAtReading + result[1]; + } + } else if (targetOrbitChoice = "KnownOrbit") { + targetCoordinates = getCoordinatesFromParameters(targetOrbitalParameters, body); + targetStartTime = targetOrbitalParameters.currentTimeAtReading; } - if (startTime > targetOrbitalParameters.currentTimeAtReading) { - let addedTime = startTime - targetOrbitalParameters.currentTimeAtReading; - targetCoordinates = extrapolateTrajectory(addedTime, targetCoordinates, body); - } - - let additionalTrueAnomaly = this.additionalTrueAnomaly.getCurrentValue(); - if (startingCoordinates && targetCoordinates) { + startTime = Math.max(currentTime, startingOrbitalParameters.currentTimeAtReading, targetStartTime); + + if (startTime > startingOrbitalParameters.currentTimeAtReading) { + let addedTime = startTime - startingOrbitalParameters.currentTimeAtReading; + startingCoordinates = extrapolateTrajectory(addedTime, startingCoordinates, body); + } + + if (startTime > targetStartTime) { + let addedTime = startTime - targetStartTime; + targetCoordinates = extrapolateTrajectory(addedTime, targetCoordinates, body); + } + } + + if (startingCoordinates && targetCoordinates && startTime) { this.worker = new Worker(new URL('./worker.ts', import.meta.url), {type: 'module'}); this.worker.addEventListener("message", event => { let transferResponse = event.data as ProgressMessage; @@ -101,8 +176,8 @@ export class InterceptTargetGui { } if (transferResponse.bestTransfer) { - transferResponse.bestTransfer.firstManoeuvre.time += currentTime; - transferResponse.bestTransfer.secondManoeuvre.time += currentTime; + transferResponse.bestTransfer.firstManoeuvre.time += startTime; + transferResponse.bestTransfer.secondManoeuvre.time += startTime; } this.manoeuvresGui.displayProgress(transferResponse); @@ -143,5 +218,19 @@ export class InterceptTargetGui { let additionalTrueAnomaly = parseFloat(additionalTrueAnomalyInput.value) * Math.PI / 180.0; this.additionalTrueAnomaly.set(additionalTrueAnomaly, this.sourceId); }); + + knownOrbitButton.addEventListener("change", () => targetOrbitChoice.set("KnownOrbit", this.sourceId)); + interpolateOrbitButton.addEventListener("change", () => targetOrbitChoice.set("InterpolateOrbit", this.sourceId)); + + targetOrbitChoice.listenToValue((_, value) => { + orbitContainer.innerHTML = ""; + if (value == "KnownOrbit") { + knownOrbitButton.setAttribute("checked", ""); + orbitContainer.appendChild(knownOrbitContainer); + } else if (value == "InterpolateOrbit") { + interpolateOrbitButton.setAttribute("checked", ""); + orbitContainer.appendChild(interpolateOrbitContainer); + } + }); } } \ No newline at end of file diff --git a/src/gui/interpolate.ts b/src/gui/interpolate.ts new file mode 100644 index 0000000..aa0eb4f --- /dev/null +++ b/src/gui/interpolate.ts @@ -0,0 +1,171 @@ +import type { ChangingStorageValue } from "../storage"; +import { createLabel, createNumberInput, createRadioButton } from "./common"; + +export interface InterpolationParameters { + targetPeriapsis: number, + orbitChoice: "apoapsis" | "speedAndAltitude", + targetApoapsis: number, + targetSpeed: number, + targetAltitude: number, + firstOwnAltitude: number, + firstTargetAltitude: number, + firstDistance: number, + secondOwnAltitude: number, + secondTargetAltitude: number, + secondDistance: number, +} + +export const DefaultInterpolationParameters: InterpolationParameters = { + targetPeriapsis: 100000, + orbitChoice: "apoapsis", + targetApoapsis: 200000, + targetSpeed: 0, + targetAltitude: 0, + firstOwnAltitude: 0, + firstTargetAltitude: 0, + firstDistance: 0, + secondOwnAltitude: 0, + secondTargetAltitude: 0, + secondDistance: 0 +} + +export class InterpolateOrbitGui { + parentDiv: HTMLDivElement; + interpolationParameters: ChangingStorageValue + sourceId: string; + + constructor(interpolationParameters: ChangingStorageValue) { + this.parentDiv = document.createElement("div"); + this.interpolationParameters = interpolationParameters; + + let targetOrbitHeader = document.createElement("h4"); + targetOrbitHeader.appendChild(document.createTextNode("Describe target orbit:")); + this.parentDiv.appendChild(targetOrbitHeader); + + let addToParent = (elements: HTMLElement[]) => { + let container = document.createElement("div"); + container.classList.add("orbitalParameter"); + elements.forEach(child => container.appendChild(child)); + this.parentDiv.appendChild(container); + } + + let createInputWithLabel = (label: string, minimum: number, maximum?: number, size?: number) => { + let inputId = crypto.randomUUID(); + let labelElement = createLabel(inputId, label); + let numberInput = createNumberInput(inputId, minimum, maximum, size); + addToParent([labelElement, numberInput]); + + return numberInput; + } + + let periapsisInput = createInputWithLabel("Target peripasis:", 0); + + let descriptorChoiceId = crypto.randomUUID(); + let apoapsisButtonId = crypto.randomUUID(); + let apoapsisButton = createRadioButton(descriptorChoiceId, apoapsisButtonId); + let apoapsisButtonLabel = createLabel(apoapsisButtonId, "Use apoapsis"); + + let speedAndAltitudeId = crypto.randomUUID(); + let speedAndAltitudeButton = createRadioButton(descriptorChoiceId, speedAndAltitudeId); + let speedAndAltitudeLabel = createLabel(speedAndAltitudeId, "Use speed and altitude"); + addToParent([apoapsisButton, apoapsisButtonLabel, speedAndAltitudeButton, speedAndAltitudeLabel]); + + let apoapsisInput = createInputWithLabel("Target apopasis:", 0); + let speedInput = createInputWithLabel("Target speed:", 0); + let altitudeInput = createInputWithLabel("Target altitude:", 0); + + let instantOneHeader = document.createElement("h4"); + instantOneHeader.appendChild(document.createTextNode("Measurement one:")); + this.parentDiv.appendChild(instantOneHeader); + + let firstOwnAltitudeInput = createInputWithLabel("Own altitude:", 0); + let firstTargetAltitudeInput = createInputWithLabel("Target altitude:", 0); + let firstDistanceInput = createInputWithLabel("Distance to target:", 0); + + let instantTwoHeader = document.createElement("h4"); + instantTwoHeader.appendChild(document.createTextNode("Measurement two:")); + this.parentDiv.appendChild(instantTwoHeader); + + let secondOwnAltitudeInput = createInputWithLabel("Own altitude:", 0); + let secondTargetAltitudeInput = createInputWithLabel("Target altitude:", 0); + let secondDistanceInput = createInputWithLabel("Distance to target:", 0); + + this.sourceId = crypto.randomUUID(); + const onChange = () => { + let periapsis = parseFloat(periapsisInput.value); + let apoapsis = parseFloat(apoapsisInput.value); + let speed = parseFloat(speedInput.value); + let altitude = parseFloat(altitudeInput.value); + let firstTargetAltitude = parseFloat(firstTargetAltitudeInput.value); + let firstOwnAltitude = parseFloat(firstOwnAltitudeInput.value); + let firstDistance = parseFloat(firstDistanceInput.value); + let secondTargetAltitude = parseFloat(secondTargetAltitudeInput.value); + let secondOwnAltitude = parseFloat(secondOwnAltitudeInput.value); + let secondDistance = parseFloat(secondDistanceInput.value); + + let orbitChoice: "apoapsis" | "speedAndAltitude" = "apoapsis"; + if (speedAndAltitudeButton.checked) { + orbitChoice = "speedAndAltitude" + }; + + interpolationParameters.set({ + targetPeriapsis: periapsis, + orbitChoice: orbitChoice, + targetApoapsis: apoapsis, + targetSpeed: speed, + targetAltitude: altitude, + firstTargetAltitude: firstTargetAltitude, + firstOwnAltitude: firstOwnAltitude, + firstDistance: firstDistance, + secondTargetAltitude: secondTargetAltitude, + secondOwnAltitude: secondOwnAltitude, + secondDistance: secondDistance, + }, this.sourceId) + }; + + [ + periapsisInput, + apoapsisButton, + speedAndAltitudeButton, + apoapsisInput, + speedInput, + altitudeInput, + firstOwnAltitudeInput, + firstTargetAltitudeInput, + firstDistanceInput, + secondOwnAltitudeInput, + secondTargetAltitudeInput, + secondDistanceInput + ].forEach(input => input.addEventListener("change", onChange)); + + interpolationParameters.listenToValue((source, value) => { + if (value.orbitChoice == "apoapsis") { + apoapsisInput.removeAttribute("disabled"); + speedInput.setAttribute("disabled", ""); + altitudeInput.setAttribute("disabled", ""); + apoapsisButton.setAttribute("checked", ""); + } else if (value.orbitChoice == "speedAndAltitude") { + apoapsisInput.setAttribute("disabled", ""); + speedInput.removeAttribute("disabled"); + altitudeInput.removeAttribute("disabled"); + speedAndAltitudeButton.setAttribute("checked", ""); + } + + if (source == this.sourceId) { + return; + } + + periapsisInput.value = value.targetPeriapsis.toString(); + apoapsisInput.value = value.targetApoapsis.toString(); + speedInput.value = value.targetSpeed.toString(); + altitudeInput.value = value.targetAltitude.toString(); + firstOwnAltitudeInput.value = value.firstOwnAltitude.toString(); + firstTargetAltitudeInput.value = value.firstTargetAltitude.toString(); + firstDistanceInput.value = value.firstDistance.toString(); + + secondOwnAltitudeInput.value = value.secondOwnAltitude.toString(); + secondTargetAltitudeInput.value = value.secondTargetAltitude.toString(); + secondDistanceInput.value = value.secondDistance.toString(); + }); + } +} \ No newline at end of file diff --git a/src/gui/landing.ts b/src/gui/landing.ts new file mode 100644 index 0000000..ff5290c --- /dev/null +++ b/src/gui/landing.ts @@ -0,0 +1,177 @@ +import type { Body } from "../calculations/constants"; +import { extrapolateTrajectory, findCheapestLanding, getOrbitalCoordinates, type OrbitalCoordinates, type ShipParameters } from "../calculations/orbit-calculations"; +import type { ChangingStorageValue } from "../storage"; +import { createDisabledInput, createLabel, getCoordinatesFromParameters, getOrbitFromParameters, type OrbitalParameters } from "./common"; +import { type LandingParameters } from "../calculations/orbit-calculations"; +import { LandingParametersGui } from "./landingparameters"; +import { ShipGui } from "./ship"; +import type { FindBestLandingMessage, LandingProgressMessage } from "./worker"; + +export class LandingGui { + parentDiv: HTMLDivElement; + + currentTime: ChangingStorageValue; + body: ChangingStorageValue; + orbitalParameters: ChangingStorageValue; + shipParameters: ChangingStorageValue; + landingParameters: ChangingStorageValue; + + shipGui: ShipGui; + landingParametersGui: LandingParametersGui; + + worker: Worker | null = null; + + constructor(currentTime: ChangingStorageValue, body: ChangingStorageValue, orbitalParameters: ChangingStorageValue, shipParameters: ChangingStorageValue, landingParameters: ChangingStorageValue) { + this.parentDiv = document.createElement("div"); + + this.currentTime = currentTime; + this.body = body; + this.orbitalParameters = orbitalParameters; + this.shipParameters = shipParameters; + this.landingParameters = landingParameters; + + this.shipGui = new ShipGui(shipParameters); + this.landingParametersGui = new LandingParametersGui(this.landingParameters); + + let shipHeader = document.createElement("h3"); + shipHeader.appendChild(document.createTextNode("Ship attributes:")); + this.parentDiv.appendChild(shipHeader); + this.parentDiv.appendChild(this.shipGui.parentDiv); + + let landingHeader = document.createElement("h3"); + landingHeader.appendChild(document.createTextNode("Landing parameters")); + this.parentDiv.appendChild(landingHeader); + this.parentDiv.appendChild(this.landingParametersGui.parentDiv); + + let findLandingButton = document.createElement("button"); + findLandingButton.appendChild(document.createTextNode("Search for landing")); + this.parentDiv.appendChild(findLandingButton); + + let landingSolutionHeader = document.createElement("h3"); + landingSolutionHeader.appendChild(document.createTextNode("Landing:")); + this.parentDiv.appendChild(landingSolutionHeader); + + let searchExplanation = document.createElement("p"); + this.parentDiv.appendChild(searchExplanation); + + const addToParent = (elements: HTMLElement[]) => { + let containerDiv = document.createElement("div"); + containerDiv.classList.add("orbitalParameter"); + this.parentDiv.appendChild(containerDiv); + elements.forEach(child => containerDiv.appendChild(child)); + } + + let manoeuvreHeader = document.createElement("h4"); + manoeuvreHeader.appendChild(document.createTextNode("Manoeuvre:")); + this.parentDiv.appendChild(manoeuvreHeader); + + let manoeuvreTimeId = crypto.randomUUID(); + let manoeuvreTimeLabel = createLabel(manoeuvreTimeId, "Time:"); + let manoeuvreTimeInput = createDisabledInput(manoeuvreTimeId); + addToParent([manoeuvreTimeLabel, manoeuvreTimeInput]); + + let progradeId = crypto.randomUUID(); + let progradeLabel = createLabel(progradeId, "Prograde delta-v:"); + let progradeInput = createDisabledInput(progradeId); + addToParent([progradeLabel, progradeInput]); + + let normalId = crypto.randomUUID(); + let normalLabel = createLabel(normalId, "Normal delta-v:"); + let normalInput = createDisabledInput(normalId); + addToParent([normalLabel, normalInput]); + + let radialId = crypto.randomUUID(); + let radialLabel = createLabel(radialId, "Radial delta-v:"); + let radialInput = createDisabledInput(radialId); + addToParent([radialLabel, radialInput]); + + let totalId = crypto.randomUUID(); + let totalLabel = createLabel(totalId, "Total delta-v:"); + let totalInput = createDisabledInput(totalId); + addToParent([totalLabel, totalInput]); + + let brakingHeader = document.createElement("h4"); + brakingHeader.appendChild(document.createTextNode("Braking:")); + this.parentDiv.appendChild(brakingHeader); + + let brakingTimeId = crypto.randomUUID(); + let brakingTimeLabel = createLabel(brakingTimeId, "Time to start braking:"); + let brakingTimeInput = createDisabledInput(brakingTimeId); + addToParent([brakingTimeLabel, brakingTimeInput]); + + let brakingAltitudeId = crypto.randomUUID(); + let brakingAltitudeLabel = createLabel(brakingAltitudeId, "Altitude to start braking:"); + let brakingAltitudeInput = createDisabledInput(brakingAltitudeId); + addToParent([brakingAltitudeLabel, brakingAltitudeInput]); + + let brakingDeltaVId = crypto.randomUUID(); + let brakingDeltaVLabel = createLabel(brakingDeltaVId, "Delta-v required:"); + let brakingDeltaVInput = createDisabledInput(brakingDeltaVId); + addToParent([brakingDeltaVLabel, brakingDeltaVInput]); + + findLandingButton.addEventListener("click", () => { + if (this.worker !== null) { + this.worker.terminate(); + this.worker = null; + findLandingButton.innerHTML = "Search for landing"; + } else { + findLandingButton.innerHTML = "Cancel search"; + + let currentTime = this.currentTime.getCurrentValue(); + let orbitalParameters = this.orbitalParameters.getCurrentValue(); + let shipParameters = this.shipParameters.getCurrentValue(); + let landingParameters = this.landingParameters.getCurrentValue(); + let body = this.body.getCurrentValue(); + + let coordinates: OrbitalCoordinates | null = getCoordinatesFromParameters(orbitalParameters, body); + + let startTime = Math.max(currentTime, orbitalParameters.currentTimeAtReading); + if (startTime > orbitalParameters.currentTimeAtReading) { + let addedTime = startTime - orbitalParameters.currentTimeAtReading; + coordinates = extrapolateTrajectory(addedTime, coordinates, body); + } + + if (coordinates) { + this.worker = new Worker(new URL('./worker.ts', import.meta.url), {type: 'module'}); + + this.worker.addEventListener("message", e => { + let landingResponse = e.data as LandingProgressMessage; + if (landingResponse) { + searchExplanation.innerHTML = `Search is ${landingResponse.percentDone}% finished`; + if (landingResponse.bestDeltaV != null && landingResponse.bestLanding != null) { + searchExplanation.appendChild(document.createElement("br")); + searchExplanation.appendChild(document.createTextNode(`Current best landing costs ${landingResponse.bestDeltaV.toFixed(3)} m/s`)); + + manoeuvreTimeInput.value = landingResponse.bestLanding.transferManoeuvre.time.toFixed(0); + progradeInput.value = landingResponse.bestLanding.transferManoeuvre.progradeDeltaV.toFixed(3); + normalInput.value = landingResponse.bestLanding.transferManoeuvre.normalDeltaV.toFixed(3); + radialInput.value = landingResponse.bestLanding.transferManoeuvre.radialDeltaV.toFixed(3); + totalInput.value = landingResponse.bestLanding.transferManoeuvre.totalDeltaV.toFixed(3); + + brakingTimeInput.value = landingResponse.bestLanding.brakingTimestamp.toFixed(0); + brakingAltitudeInput.value = landingResponse.bestLanding.brakingAltitude.toFixed(0); + brakingDeltaVInput.value = landingResponse.bestLanding.brakingDeltaV.toFixed(3); + }; + + if (landingResponse.finished) { + this.worker?.terminate(); + this.worker = null; + findLandingButton.innerHTML = "Search for landing"; + } + } + }); + + let message: FindBestLandingMessage = { + type: "FindBestLandingMessage", + startingCoordinates: coordinates, + startTime: startTime, + shipParameters: shipParameters, + landingParameters: landingParameters, + body: body + }; + this.worker.postMessage(message); + } + } + }); + } +} \ No newline at end of file diff --git a/src/gui/landingparameters.ts b/src/gui/landingparameters.ts new file mode 100644 index 0000000..e2d29f9 --- /dev/null +++ b/src/gui/landingparameters.ts @@ -0,0 +1,71 @@ +import type { ChangingStorageValue } from "../storage"; +import { createLabel, createNumberInput } from "./common"; +import { type LandingParameters } from "../calculations/orbit-calculations"; + +export class LandingParametersGui { + parentDiv: HTMLElement; + + landingParameters: ChangingStorageValue; + sourceId: string; + + constructor(landingParameters: ChangingStorageValue) { + this.parentDiv = document.createElement("div"); + this.landingParameters = landingParameters; + + let addToParent = (elements: HTMLElement[]) => { + let containerDiv = document.createElement("div"); + containerDiv.classList.add("orbitalParameter"); + this.parentDiv.appendChild(containerDiv); + + elements.forEach(child => containerDiv.appendChild(child)); + } + + let targetLatitudeId = crypto.randomUUID(); + let targetLatitudeLabel = createLabel(targetLatitudeId, "Target latitude:"); + let targetLatitudeInput = createNumberInput(targetLatitudeId, -90, 90); + addToParent([targetLatitudeLabel, targetLatitudeInput]); + + let targetLongitudeId = crypto.randomUUID(); + let targetLongitudeLabel = createLabel(targetLongitudeId, "Target longitude:"); + let targetLongitudeInput = createNumberInput(targetLongitudeId, -360, 360); + addToParent([targetLongitudeLabel, targetLongitudeInput]); + + let targetAltitudeId = crypto.randomUUID(); + let targetAltitudeLabel = createLabel(targetAltitudeId, "Target altitude:"); + let targetAltitudeInput = createNumberInput(targetAltitudeId, 0); + addToParent([targetAltitudeLabel, targetAltitudeInput]); + + let minimumAngleId = crypto.randomUUID(); + let minimumAngleLabel = createLabel(minimumAngleId, "Minimum incoming angle:"); + let minimumAngleInput = createNumberInput(minimumAngleId, 0, 90); + addToParent([minimumAngleLabel, minimumAngleInput]); + + this.sourceId = crypto.randomUUID(); + const onChange = () => { + let targetLatitude = parseFloat(targetLatitudeInput.value) * Math.PI / 180.0; + let targetLongitude = parseFloat(targetLongitudeInput.value) * Math.PI / 180.0; + let targetAltitude = parseFloat(targetAltitudeInput.value); + let minimumAngle = parseFloat(minimumAngleInput.value) * Math.PI / 180.0; + + landingParameters.set({ + targetLatitude: targetLatitude, + targetLongitude: targetLongitude, + targetAltitude: targetAltitude, + minimumAngle: minimumAngle + }, this.sourceId); + }; + + [targetLatitudeInput, targetLongitudeInput, targetAltitudeInput, minimumAngleInput].forEach(child => child.addEventListener("change", onChange)); + + landingParameters.listenToValue((source, value) => { + if (source == this.sourceId) { + return; + } + + targetLatitudeInput.value = (value.targetLatitude * 180 / Math.PI).toString(); + targetLongitudeInput.value = (value.targetLongitude * 180 / Math.PI).toString(); + targetAltitudeInput.value = value.targetAltitude.toString(); + minimumAngleInput.value = (value.minimumAngle * 180 / Math.PI).toString(); + }); + } +} \ No newline at end of file diff --git a/src/gui/ship.ts b/src/gui/ship.ts new file mode 100644 index 0000000..c1bfb14 --- /dev/null +++ b/src/gui/ship.ts @@ -0,0 +1,65 @@ +import type { ShipParameters } from "../calculations/orbit-calculations"; +import type { ChangingStorageValue } from "../storage"; +import { createLabel, createNumberInput } from "./common"; + +export class ShipGui { + parentDiv: HTMLElement; + + shipParameters: ChangingStorageValue; + + sourceId: string; + + constructor(shipParameters: ChangingStorageValue) { + this.parentDiv = document.createElement("div"); + + this.shipParameters = shipParameters; + + let addToParent = (elements: HTMLElement[], parent: HTMLElement) => { + let containerDiv = document.createElement("div"); + containerDiv.classList.add("orbitalParameter"); + + elements.forEach(child => containerDiv.appendChild(child)); + parent.appendChild(containerDiv); + } + + let shipMassId = crypto.randomUUID(); + let shipMassLabel = createLabel(shipMassId, "Mass of ship (kg):") + let shipMassInput = createNumberInput(shipMassId, 0); + addToParent([shipMassLabel, shipMassInput], this.parentDiv); + + let shipThrustId = crypto.randomUUID(); + let shipThrustLabel = createLabel(shipThrustId, "Thrust of ship (N):"); + let shipThrustInput = createNumberInput(shipThrustId, 0); + addToParent([shipThrustLabel, shipThrustInput], this.parentDiv); + + let specificImpulseId = crypto.randomUUID(); + let specificImpulseLabel = createLabel(specificImpulseId, "Specific impulse of ship (m/s):"); + let specificImpulseInput = createNumberInput(specificImpulseId, 0); + addToParent([specificImpulseLabel, specificImpulseInput], this.parentDiv); + + this.sourceId = crypto.randomUUID(); + const onChange = () => { + let shipMass = parseFloat(shipMassInput.value); + let shipThrust = parseFloat(shipThrustInput.value); + let shipSpecificImpulse = parseFloat(specificImpulseInput.value); + + this.shipParameters.set({ + mass: shipMass, + thrust: shipThrust, + specificImpulse: shipSpecificImpulse + }, this.sourceId); + }; + + [shipMassInput, shipThrustInput, specificImpulseInput].forEach(child => child.addEventListener("change", onChange)); + + shipParameters.listenToValue((source, value) => { + if (source == this.sourceId) { + return; + } + + shipMassInput.value = value.mass.toString(); + shipThrustInput.value = value.thrust.toString(); + specificImpulseInput.value = value.specificImpulse.toString(); + }); + } +} \ No newline at end of file diff --git a/src/gui/simpleplanechange.ts b/src/gui/simpleplanechange.ts index 4ff83e8..897f45a 100644 --- a/src/gui/simpleplanechange.ts +++ b/src/gui/simpleplanechange.ts @@ -82,6 +82,8 @@ export class SimplePlaneChangeGui { let bestDeltaV = Math.min(planeChange.firstManoeuvre.totalDeltaV, planeChange.secondManoeuvre.totalDeltaV); let bestTransfer: Transfer = { transferOrbit: {semiLatusRectum: 0, eccentricity: 0, coordinateAxes: [[], [], []]}, + transferOrbitTrueAnomalyAtManoeuvreOne: 0, + transferOrbitTrueanomalyAtManoeuvreTwo: 0, firstManoeuvre: planeChange.firstManoeuvre, secondManoeuvre: planeChange.secondManoeuvre, closestPointDistance: 0, diff --git a/src/gui/targetorbit.ts b/src/gui/targetorbit.ts index 22865c7..399a844 100644 --- a/src/gui/targetorbit.ts +++ b/src/gui/targetorbit.ts @@ -79,8 +79,8 @@ export class TargetOrbitGui { } if (transferResponse.bestTransfer) { - transferResponse.bestTransfer.firstManoeuvre.time += currentTime; - transferResponse.bestTransfer.secondManoeuvre.time += currentTime; + transferResponse.bestTransfer.firstManoeuvre.time += startingTime; + transferResponse.bestTransfer.secondManoeuvre.time += startingTime; } this.manoeuvresGui.displayProgress(transferResponse); diff --git a/src/gui/worker.ts b/src/gui/worker.ts index dbb13bd..8dadc8b 100644 --- a/src/gui/worker.ts +++ b/src/gui/worker.ts @@ -1,5 +1,6 @@ import type { Body } from "../calculations/constants"; -import { findCheapestIntercept, findCheapestTransfer, type Orbit, type OrbitalCoordinates, type Transfer } from "../calculations/orbit-calculations"; +import { findCheapestIntercept, findCheapestLanding, findCheapestTransfer, type Orbit, type OrbitalCoordinates, type ShipParameters, type Transfer } from "../calculations/orbit-calculations"; +import type { Landing, LandingParameters, LandingProgressCallbackFunction, Manoeuvre } from "../calculations/orbit-calculations"; const ctx: Worker = self as any; @@ -18,6 +19,15 @@ export interface FindBestInterceptMessage { body: Body; } +export interface FindBestLandingMessage { + "type": "FindBestLandingMessage", + startTime: number, + startingCoordinates: OrbitalCoordinates, + shipParameters: ShipParameters, + landingParameters: LandingParameters, + body: Body +} + export interface ProgressMessage { type: "ProgressMessage" finished: boolean, @@ -26,6 +36,14 @@ export interface ProgressMessage { bestTransfer: Transfer | null } +export interface LandingProgressMessage { + type: "LandingProgressMessage", + finished: boolean, + percentDone: number, + bestDeltaV: number | null, + bestLanding: Landing | null +} + ctx.addEventListener("message", event => { const progressCallback = (numberChecked: number, totalNumber: number, bestDeltaV: number | null, bestTransfer: Transfer | null) => { if (numberChecked % 100 == 0) { @@ -42,9 +60,23 @@ ctx.addEventListener("message", event => { } }; - let message = event.data as FindBestTransferMessage | FindBestInterceptMessage; + const landingProgressCallback: LandingProgressCallbackFunction = (pathsChecked: number, totalNumberOfPaths: number, currentBestDeltaV: number | null, currentBestLanding: Landing | null) => { + if (pathsChecked % 100 == 0) { + let percentDone = pathsChecked * 100 / totalNumberOfPaths; + let message: LandingProgressMessage = { + type: "LandingProgressMessage", + finished: false, + percentDone: percentDone, + bestDeltaV: currentBestDeltaV, + bestLanding: currentBestLanding + }; + + ctx.postMessage(message); + } + } + + let message = event.data as FindBestTransferMessage | FindBestInterceptMessage | FindBestLandingMessage; if (message.type == "FindBestTransfer") { - console.log("Finding best transfer"); let bestTransfer = findCheapestTransfer(message.startingSituation, message.targetOrbit, message.body, progressCallback); let bestDeltaV = null; if (bestTransfer) { @@ -62,7 +94,6 @@ ctx.addEventListener("message", event => { } if (message.type == "FindBestIntercept") { - console.log("Finding best intercept"); let bestIntercept = findCheapestIntercept(message.startingSituation, message.targetSituation, message.body, message.additionalTrueAnomaly, progressCallback); let bestDeltaV = null; if (bestIntercept) { @@ -79,4 +110,17 @@ ctx.addEventListener("message", event => { ctx.postMessage(finishedMessage); } + + if (message.type == "FindBestLandingMessage") { + let bestLanding = findCheapestLanding(message.startingCoordinates, message.startTime, message.shipParameters, message.landingParameters, message.body, landingProgressCallback); + let finishedMessage: LandingProgressMessage = { + type: "LandingProgressMessage", + finished: true, + percentDone: 100, + bestDeltaV: bestLanding?.totalDeltaV ?? null, + bestLanding: bestLanding + }; + + ctx.postMessage(finishedMessage); + } }); \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 32bc18e..d50afd8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,9 @@ import { getPlanetByName, Kerbol } from "./calculations/constants"; -import { createLabel, createRadioButton, decodeOrbitalParameters, DefaultOrbitalParameters, encodeOrbitalParameters } from "./gui/common"; -import { InterceptTargetGui } from "./gui/intercept"; +import { DefaultShipParameters } from "./calculations/orbit-calculations"; +import { createLabel, createRadioButton, DefaultLandingParameters, DefaultOrbitalParameters, encodeJsonObject, getDecoderFunction } from "./gui/common"; +import { decodeTargetOrbitChoice, InterceptTargetGui } from "./gui/intercept"; +import { DefaultInterpolationParameters } from "./gui/interpolate"; +import { LandingGui } from "./gui/landing"; import { OrbitalParametersGui } from "./gui/orbit"; import { PlanetGui } from "./gui/planet"; import { SimplePlaneChangeGui } from "./gui/simpleplanechange"; @@ -8,7 +11,7 @@ import { TargetOrbitGui } from "./gui/targetorbit"; import { TimeGui } from "./gui/time"; import { ChangingStorageValue } from "./storage"; -type CalculationType = "planeChange" | "targetOrbit" | "intercept"; +type CalculationType = "planeChange" | "targetOrbit" | "intercept" | "landing"; function decodeCalculationType(input: string): CalculationType { let calculationType = input as CalculationType; if (calculationType !== undefined) { @@ -20,11 +23,15 @@ function decodeCalculationType(input: string): CalculationType { let currentTime = new ChangingStorageValue(0, "currentTime", parseInt, n => n.toString()); let body = new ChangingStorageValue(Kerbol, "planet", s => getPlanetByName(s), p => p.planetName); -let orbitalParameters = new ChangingStorageValue(DefaultOrbitalParameters, "orbitalParameters", decodeOrbitalParameters, encodeOrbitalParameters); +let orbitalParameters = new ChangingStorageValue(DefaultOrbitalParameters, "orbitalParameters", getDecoderFunction(DefaultOrbitalParameters), encodeJsonObject); let calculationType = new ChangingStorageValue("planeChange", "calculationType", decodeCalculationType, s => s); -let targetOrbitalParameters = new ChangingStorageValue(DefaultOrbitalParameters, "targetOrbitalParameters", decodeOrbitalParameters, encodeOrbitalParameters); +let targetOrbitalParameters = new ChangingStorageValue(DefaultOrbitalParameters, "targetOrbitalParameters", getDecoderFunction(DefaultOrbitalParameters), encodeJsonObject); let circularizeOrbit = new ChangingStorageValue(false, "circularizeOrbit", s => s == "true", b => b ? "true" : "false"); +let targetOrbitChoice = new ChangingStorageValue("KnownOrbit", "targetOrbitChoice", decodeTargetOrbitChoice, s => s); +let interpolationParameters = new ChangingStorageValue(DefaultInterpolationParameters, "InterpolationParameters", getDecoderFunction(DefaultInterpolationParameters), encodeJsonObject); let additionalInterceptTrueanomaly = new ChangingStorageValue(0, "additionalTrueAnomaly", parseFloat, s => s.toString()); +let shipParameters = new ChangingStorageValue(DefaultShipParameters, "shipParameters", getDecoderFunction(DefaultShipParameters), encodeJsonObject); +let landingParameters = new ChangingStorageValue(DefaultLandingParameters, "landingParameters", getDecoderFunction(DefaultLandingParameters), encodeJsonObject); let dateGui = new TimeGui(currentTime, true); let currentTimeDiv = document.getElementById("currentTimeDiv"); @@ -40,7 +47,8 @@ orbitalParametersDiv?.appendChild(orbitalParametersGui.parentDiv); let simplePlaneChangeGui = new SimplePlaneChangeGui(currentTime, body, orbitalParameters, targetOrbitalParameters, circularizeOrbit); let targetOrbitGui = new TargetOrbitGui(currentTime, body, orbitalParameters, targetOrbitalParameters); -let interceptTargetGui = new InterceptTargetGui(currentTime, body, orbitalParameters, targetOrbitalParameters, additionalInterceptTrueanomaly); +let interceptTargetGui = new InterceptTargetGui(currentTime, body, orbitalParameters, targetOrbitalParameters, additionalInterceptTrueanomaly, targetOrbitChoice, interpolationParameters); +let landingGui = new LandingGui(currentTime, body, orbitalParameters, shipParameters, landingParameters); let calculationChoiceDiv = document.getElementById("calculationChoice"); @@ -59,6 +67,11 @@ interceptTargetButton.addEventListener("change", () => calculationType.set("inte let interceptTargetLabel = createLabel("interceptTarget", "Intercept target"); [interceptTargetButton, interceptTargetLabel].forEach(child => calculationChoiceDiv?.appendChild(child)); +let landingButton = createRadioButton("calculationChoice", "landing"); +landingButton.addEventListener("change", () => calculationType.set("landing", "main")); +let landingLabel = createLabel("landing", "Landing"); +[landingButton, landingLabel].forEach(child => calculationChoiceDiv?.appendChild(child)); + let calculationsDiv = document.getElementById("calculation") as HTMLDivElement; calculationType.listenToValue((_, value) => { calculationsDiv.innerHTML = ""; @@ -71,6 +84,9 @@ calculationType.listenToValue((_, value) => { } else if (value == "intercept") { interceptTargetButton.setAttribute("checked", ""); calculationsDiv.appendChild(interceptTargetGui.parentDiv); + } else if (value == "landing") { + landingButton.setAttribute("checked", ""); + calculationsDiv.appendChild(landingGui.parentDiv); } });