KerbalCalculations/src/gui/interpolate.ts
2026-04-06 23:48:00 +02:00

259 lines
11 KiB
TypeScript

import type { Body } from "../calculations/constants";
import { extrapolateTrajectory, findOrbitThroughInterpolation, type OrbitalCoordinates } from "../calculations/orbit-calculations";
import type { ChangingStorageValue } from "../storage";
import { createLabel, createNumberInput, createRadioButton, getCoordinatesFromParameters, type OrbitalParameters } from "./common";
import { Renderer } from "./renderer";
export interface InterpolationParameters {
targetPeriapsis: number,
orbitChoice: "apoapsis" | "speedAndAltitude",
targetApoapsis: number,
targetSpeed: number,
targetAltitude: number,
firstOwnAltitude: number,
firstTargetAltitude: number,
firstDistance: number,
firstPhaseAngle: number,
secondOwnAltitude: number,
secondTargetAltitude: number,
secondDistance: number,
secondPhaseAngle: number,
}
export const DefaultInterpolationParameters: InterpolationParameters = {
targetPeriapsis: 100000,
orbitChoice: "apoapsis",
targetApoapsis: 200000,
targetSpeed: 0,
targetAltitude: 0,
firstOwnAltitude: 0,
firstTargetAltitude: 0,
firstDistance: 0,
firstPhaseAngle: 0,
secondOwnAltitude: 0,
secondTargetAltitude: 0,
secondDistance: 0,
secondPhaseAngle: 0
}
export class InterpolateOrbitGui {
parentDiv: HTMLDivElement;
startingOrbitalParameters: ChangingStorageValue<OrbitalParameters>;
body: ChangingStorageValue<Body>;
interpolationParameters: ChangingStorageValue<InterpolationParameters>;
interpolatedOrbitCoordinates: ChangingStorageValue<OrbitalCoordinates | null>;
interpolatedOrbitTime: ChangingStorageValue<number | null>;
sourceId: string;
renderDiv: HTMLDivElement;
constructor(interpolationParameters: ChangingStorageValue<InterpolationParameters>, startingOrbitParameters: ChangingStorageValue<OrbitalParameters>, interpolatedOrbitCoordinates: ChangingStorageValue<OrbitalCoordinates | null>, interpolatedOrbitTime: ChangingStorageValue<number | null>, body: ChangingStorageValue<Body>) {
this.parentDiv = document.createElement("div");
this.startingOrbitalParameters = startingOrbitParameters;
this.interpolationParameters = interpolationParameters;
this.interpolatedOrbitCoordinates = interpolatedOrbitCoordinates;
this.interpolatedOrbitTime = interpolatedOrbitTime;
this.renderDiv = document.createElement("div");
this.body = body;
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 firstPhaseAngleInput = createInputWithLabel("Phase angle:", -180, 180);
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);
let secondPhaseAngleInput = createInputWithLabel("Phase angle:", -180, 180);
let interpolateOrbitButton = document.createElement("button");
interpolateOrbitButton.appendChild(document.createTextNode("Find interpolated orbit"));
this.parentDiv.appendChild(interpolateOrbitButton);
this.parentDiv.appendChild(this.renderDiv);
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 firstPhaseAngle = parseFloat(firstPhaseAngleInput.value) * Math.PI / 180.0;
let secondTargetAltitude = parseFloat(secondTargetAltitudeInput.value);
let secondOwnAltitude = parseFloat(secondOwnAltitudeInput.value);
let secondDistance = parseFloat(secondDistanceInput.value);
let secondPhaseAngle = parseFloat(secondPhaseAngleInput.value) * Math.PI / 180.0;
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,
firstPhaseAngle: firstPhaseAngle,
secondTargetAltitude: secondTargetAltitude,
secondOwnAltitude: secondOwnAltitude,
secondDistance: secondDistance,
secondPhaseAngle: secondPhaseAngle
}, this.sourceId)
};
[
periapsisInput,
apoapsisButton,
speedAndAltitudeButton,
apoapsisInput,
speedInput,
altitudeInput,
firstOwnAltitudeInput,
firstTargetAltitudeInput,
firstDistanceInput,
firstPhaseAngleInput,
secondOwnAltitudeInput,
secondTargetAltitudeInput,
secondDistanceInput,
secondPhaseAngleInput
].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();
firstPhaseAngleInput.value = (value.firstPhaseAngle * 180 / Math.PI).toString();
secondOwnAltitudeInput.value = value.secondOwnAltitude.toString();
secondTargetAltitudeInput.value = value.secondTargetAltitude.toString();
secondDistanceInput.value = value.secondDistance.toString();
secondPhaseAngleInput.value = (value.secondPhaseAngle * 180 / Math.PI).toString();
});
// Interpolation action
interpolateOrbitButton.addEventListener("click", _ => {
this.renderDiv.innerHTML = "";
let startingOrbitalParameters = this.startingOrbitalParameters.getCurrentValue();
let interpolationParameters = structuredClone(this.interpolationParameters.getCurrentValue());
let body = this.body.getCurrentValue();
// All interpolation altitude parameters need to get the planet radius added to them
interpolationParameters.firstTargetAltitude += body.radius;
interpolationParameters.firstOwnAltitude += body.radius;
interpolationParameters.secondTargetAltitude += body.radius;
interpolationParameters.secondOwnAltitude += body.radius;
interpolationParameters.targetAltitude += body.radius;
interpolationParameters.targetApoapsis += body.radius;
interpolationParameters.targetPeriapsis += body.radius;
let ownCoordinates = getCoordinatesFromParameters(startingOrbitalParameters, body);
let results = findOrbitThroughInterpolation(ownCoordinates, interpolationParameters, body);
if (results.length == 1) {
this.interpolatedOrbitCoordinates.set(results[0][0], "null");
this.interpolatedOrbitTime.set(results[0][1], "null");
} else {
let chooseHeader = document.createElement("h3");
chooseHeader.appendChild(document.createTextNode("Choose interpolated orbit:"));
this.renderDiv.appendChild(chooseHeader);
let interpolationChoiceId = crypto.randomUUID();
results.forEach(([coordinates, time]) => {
let buttonId = crypto.randomUUID();
let button = createRadioButton(interpolationChoiceId, buttonId);
let label = createLabel(buttonId, "The orbit below fits best");
let renderer = new Renderer(body);
let extrapolatedOwnCoordinates = extrapolateTrajectory(time, ownCoordinates, body);
let usedCoordinates = ownCoordinates;
if (extrapolatedOwnCoordinates) {
usedCoordinates = extrapolatedOwnCoordinates;
}
renderer.addCoordinates(usedCoordinates, 0x00ffff);
renderer.addCoordinates(coordinates, 0xff00ff);
this.renderDiv.appendChild(button);
this.renderDiv.appendChild(label);
this.renderDiv.appendChild(renderer.parentDiv);
this.renderDiv.appendChild(document.createElement("br"));
button.addEventListener("change", _ => {
this.interpolatedOrbitCoordinates.set(coordinates, "null");
this.interpolatedOrbitTime.set(startingOrbitalParameters.currentTimeAtReading + time, "null");
});
});
}
});
}
}