diff --git a/docs/jday.txt b/docs/jday.txt
new file mode 100644
index 00000000..f4349f7c
--- /dev/null
+++ b/docs/jday.txt
@@ -0,0 +1,44 @@
+ TABLE OF ORDINAL DAY NUMBER FOR VARIOUS CALENDAR DATES.
+ (After February, add 1 on leap years).
+
+ JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC
+
+ 1 1 32 60 91 121 152 182 213 244 274 305 335
+ 2 2 33 61 92 122 153 183 214 245 275 306 336
+ 3 3 34 62 93 123 154 184 215 246 276 307 337
+ 4 4 35 63 94 124 155 185 216 247 277 308 338
+ 5 5 36 64 95 125 156 186 217 248 278 309 339
+
+ 6 6 37 65 96 126 157 187 218 249 279 310 340
+ 7 7 38 66 97 127 158 188 219 250 280 311 341
+ 8 8 39 67 98 128 159 189 220 251 281 312 342
+ 9 9 40 68 99 129 160 190 221 252 282 313 343
+10 10 41 69 100 130 161 191 222 253 283 314 344
+
+11 11 42 70 101 131 162 192 223 254 284 315 345
+12 12 43 71 102 132 163 193 224 255 285 316 346
+13 13 44 72 103 133 164 194 225 256 286 317 347
+14 14 45 73 104 134 165 195 226 257 287 318 348
+15 15 46 74 105 135 166 196 227 258 288 319 349
+
+16 16 47 75 106 136 167 197 228 259 289 320 350
+17 17 48 76 107 137 168 198 229 260 290 321 351
+18 18 49 77 108 138 169 199 230 261 291 322 352
+19 19 50 78 109 139 170 200 231 262 292 323 353
+20 20 51 79 110 140 171 201 232 263 293 324 354
+
+21 21 52 80 111 141 172 202 233 264 294 325 355
+22 22 53 81 112 142 173 203 234 265 295 326 356
+23 23 54 82 113 143 174 204 235 266 296 327 357
+24 24 55 83 114 144 175 205 236 267 297 328 358
+25 25 56 84 115 145 176 206 237 268 298 329 359
+
+26 26 57 85 116 146 177 207 238 269 299 330 360
+27 27 58 86 117 147 178 208 239 270 300 331 361
+28 28 59 87 118 148 179 209 240 271 301 332 362
+29 29 *60 88 119 149 180 210 241 272 302 333 363
+30 30 89 120 150 181 211 242 273 303 334 364
+
+31 31 90 151 212 243 304 365
+
+* Feb 29 exists only on a leap year.
\ No newline at end of file
diff --git a/public/img/icons/calculator.png b/public/img/icons/calculator.png
new file mode 100644
index 00000000..04985fd5
--- /dev/null
+++ b/public/img/icons/calculator.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:012f43d4ce7b13ad70558195d15592a62216f5ab50f7036f6fa4c422e6f1c637
+size 9932
diff --git a/public/settings/settingsOverride.js b/public/settings/settingsOverride.js
index 974659f8..07d2c427 100644
--- a/public/settings/settingsOverride.js
+++ b/public/settings/settingsOverride.js
@@ -75,6 +75,7 @@ const settingsOverride = {
timeline: true,
timelineAlt: true,
transponderChannelData: true,
+ calculator: true,
},
/*
* searchLimit: 150,
diff --git a/src/locales/de.json b/src/locales/de.json
index badf061c..f4c01f0f 100644
--- a/src/locales/de.json
+++ b/src/locales/de.json
@@ -283,6 +283,11 @@
"title": "Analyse-Menü",
"helpBody": "Das Analyse-Menü bietet eine Reihe von Werkzeugen, die Ihnen helfen, die Daten in der aktuellen Ansicht zu analysieren. Die Werkzeuge sind:
Offizielle TLEs exportieren - Exportieren Sie echte Two-Line Element Sets. 3LES exportieren - Exportieren Sie Three-Line Element Sets. KeepTrack TLEs exportieren - Exportieren Sie alle KeepTrack Two-Line Element Sets einschließlich Analysten. KeepTrack 3LES exportieren - Exportieren Sie alle KeepTrack Three-Line Element Sets einschließlich Analysten. Nahe Objekte finden - Finden Sie Objekte, die sich nahe beieinander befinden. Wiedereintritte finden - Finden Sie Objekte, die wahrscheinlich wieder in die Atmosphäre eintreten werden. Beste Durchgänge - Finden Sie die besten Durchgänge für einen Satelliten basierend auf dem aktuell ausgewählten Sensor. "
},
+ "Calculator": {
+ "bottomIconLabel": "Referenzrahmen-Transformationen",
+ "title": "Referenzrahmen-Transformationen-Menü",
+ "helpBody": "Das Referenzrahmen-Transformationen-Menü wird verwendet, um zwischen verschiedenen Referenzrahmen zu konvertieren. Das Menü ermöglicht es Ihnen, zwischen den folgenden Referenzrahmen zu konvertieren: ECI - Erdzentriert Inertial ECEF - Erdzentriert Erd-Fest Geodätisch Topozentrisch "
+ },
"SettingsMenuPlugin": {
"bottomIconLabel": "Einstellungen",
"title": "Einstellungen-Menü",
diff --git a/src/locales/en.json b/src/locales/en.json
index 2e727f17..6688faf7 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -286,6 +286,11 @@
"title": "Analysis Menu",
"helpBody": "The Analysis Menu provides a number of tools to help you analyze the data in the current view. The tools are: Export Official TLEs - Export real two line element sets. Export 3LES - Export three line element sets. Export KeepTrack TLEs - Export All KeepTrack two line element sets including analysts. Export KeepTrack 3LES - Export All KeepTrack three line element sets including analysts. Find Close Objects - Find objects that are close to each other. Find Reentries - Find objects that are likely to reenter the atmosphere. Best Passes - Find the best passes for a satellite based on the currently selected sensor. "
},
+ "Calculator": {
+ "bottomIconLabel": "Reference Frame Transforms",
+ "title": "Reference Frame Transforms Menu",
+ "helpBody": "The Reference Frame Transforms Menu is used to convert between different reference frames. The menu allows you to convert between the following reference frames: ECI - Earth Centered Inertial ECEF - Earth Centered Earth Fixed Geodetic Topocentric "
+ },
"SettingsMenuPlugin": {
"bottomIconLabel": "Settings",
"title": "Settings Menu",
diff --git a/src/locales/es.json b/src/locales/es.json
index 8c221103..59dd04f4 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -283,6 +283,11 @@
"title": "Menú de Análisis",
"helpBody": "El Menú de Análisis proporciona una serie de herramientas para ayudarte a analizar los datos en la vista actual. Las herramientas son: Exportar TLEs Oficiales - Exportar conjuntos de elementos de dos líneas reales. Exportar 3LES - Exportar conjuntos de elementos de tres líneas. Exportar TLEs de KeepTrack - Exportar todos los conjuntos de elementos de dos líneas de KeepTrack, incluyendo analistas. Exportar 3LES de KeepTrack - Exportar todos los conjuntos de elementos de tres líneas de KeepTrack, incluyendo analistas. Encontrar Objetos Cercanos - Encontrar objetos que están cerca unos de otros. Encontrar Reentradas - Encontrar objetos que probablemente reentren en la atmósfera. Mejores Pases - Encontrar los mejores pases para un satélite basado en el sensor actualmente seleccionado. "
},
+ "Calculator": {
+ "bottomIconLabel": "Transformaciones de Marco de Referencia",
+ "title": "Menú de Transformaciones de Marco de Referencia",
+ "helpBody": "El Menú de Transformaciones de Marco de Referencia se usa para convertir entre diferentes marcos de referencia. El menú te permite convertir entre los siguientes marcos de referencia: ECI - Inercial Centrado en la Tierra ECEF - Fijo a la Tierra Centrado en la Tierra Geodésico Topocéntrico "
+ },
"SettingsMenuPlugin": {
"bottomIconLabel": "Configuración",
"title": "Menú de Configuración",
diff --git a/src/locales/locales.ts b/src/locales/locales.ts
index b0d4b2f8..f020d03d 100644
--- a/src/locales/locales.ts
+++ b/src/locales/locales.ts
@@ -228,6 +228,11 @@ export const loadLocalization = () => ({
title: i18next.t('plugins.VideoDirectorPlugin.title'),
helpBody: i18next.t('plugins.VideoDirectorPlugin.helpBody'),
},
+ Calculator: {
+ bottomIconLabel: i18next.t('plugins.Calculator.bottomIconLabel'),
+ title: i18next.t('plugins.Calculator.title'),
+ helpBody: i18next.t('plugins.Calculator.helpBody'),
+ },
},
});
diff --git a/src/plugins/analysis/analysis.ts b/src/plugins/analysis/analysis.ts
index 8e5929f8..2668f412 100644
--- a/src/plugins/analysis/analysis.ts
+++ b/src/plugins/analysis/analysis.ts
@@ -1,20 +1,3 @@
-import { KeepTrackApiEvents, lookanglesRow, ToastMsgType } from '@app/interfaces';
-import { keepTrackApi } from '@app/keepTrackApi';
-import { clickAndDragWidth } from '@app/lib/click-and-drag';
-import { getEl } from '@app/lib/get-el';
-import { showLoading } from '@app/lib/showLoading';
-
-import { SatMath } from '@app/static/sat-math';
-
-import { getUnique } from '@app/lib/get-unique';
-import { saveCsv } from '@app/lib/saveVariable';
-import { CatalogExporter } from '@app/static/catalog-exporter';
-import { CatalogSearch } from '@app/static/catalog-search';
-import analysisPng from '@public/img/icons/analysis.png';
-import { DetailedSatellite, DetailedSensor, eci2rae, EciVec3, Kilometers, MILLISECONDS_PER_SECOND, MINUTES_PER_DAY, SatelliteRecord, TAU } from 'ootk';
-import { KeepTrackPlugin } from '../KeepTrackPlugin';
-import { WatchlistPlugin } from '../watchlist/watchlist';
-
/**
* /*! /////////////////////////////////////////////////////////////////////////////
*
@@ -40,6 +23,23 @@ import { WatchlistPlugin } from '../watchlist/watchlist';
* /////////////////////////////////////////////////////////////////////////////
*/
+import { KeepTrackApiEvents, lookanglesRow, ToastMsgType } from '@app/interfaces';
+import { keepTrackApi } from '@app/keepTrackApi';
+import { clickAndDragWidth } from '@app/lib/click-and-drag';
+import { getEl } from '@app/lib/get-el';
+import { showLoading } from '@app/lib/showLoading';
+
+import { SatMath } from '@app/static/sat-math';
+
+import { getUnique } from '@app/lib/get-unique';
+import { saveCsv } from '@app/lib/saveVariable';
+import { CatalogExporter } from '@app/static/catalog-exporter';
+import { CatalogSearch } from '@app/static/catalog-search';
+import analysisPng from '@public/img/icons/analysis.png';
+import { DetailedSatellite, DetailedSensor, eci2rae, EciVec3, Kilometers, MILLISECONDS_PER_SECOND, MINUTES_PER_DAY, SatelliteRecord, TAU } from 'ootk';
+import { KeepTrackPlugin } from '../KeepTrackPlugin';
+import { WatchlistPlugin } from '../watchlist/watchlist';
+
export class AnalysisMenu extends KeepTrackPlugin {
readonly id = 'AnalysisMenu';
protected dependencies_: [];
diff --git a/src/plugins/calculator/calculator.ts b/src/plugins/calculator/calculator.ts
new file mode 100644
index 00000000..5dde7d8b
--- /dev/null
+++ b/src/plugins/calculator/calculator.ts
@@ -0,0 +1,551 @@
+import { KeepTrackApiEvents } from '@app/interfaces';
+import { keepTrackApi } from '@app/keepTrackApi';
+import calculatorPng from '@public/img/icons/calculator.png';
+
+import { getEl } from '@app/lib/get-el';
+import { errorManagerInstance } from '@app/singletons/errorManager';
+import { SatMath } from '@app/static/sat-math';
+import { Degrees, DetailedSensor, ecf2eci, eci2ecf, eci2rae, Kilometers, rae2eci, RaeVec3, Vector3D } from 'ootk';
+import { clickDragOptions, KeepTrackPlugin } from '../KeepTrackPlugin';
+
+enum CalculatorMode {
+ ITRF = 'ITRF',
+ J2000 = 'J2000',
+ RAE = 'RAE',
+}
+
+export class Calculator extends KeepTrackPlugin {
+ readonly id = 'Calculator';
+ protected dependencies_ = [];
+ bottomIconImg = calculatorPng;
+ currentMode: CalculatorMode = CalculatorMode.ITRF;
+ sensorUsedInCalculation: DetailedSensor | null = null;
+
+ sideMenuElementName = 'calculator-menu';
+ private readonly itrfHtml = keepTrackApi.html`
+
+
+
+
+
+
+
+ J2000
+
+
+
+
+
+
+ RAE
+
+
+
+
+
+
+
+ Draw Line
+
+
+ `;
+ private readonly raeHtml = keepTrackApi.html`
+
+
+
+
+
+
+
+ J2000
+
+
+
+
+
+
+ ITRF
+
+
+
+
+
+
+ Draw Line
+
+
+ `;
+ private readonly j2000Html = keepTrackApi.html`
+
+
+
+
+
+
+
+ ITRF
+
+
+
+
+
+
+ RAE
+
+
+
+
+
+
+
+ Draw Line
+
+
+ `;
+
+ sideMenuElementHtml = keepTrackApi.html`
+
+ ${this.itrfHtml}
+
`;
+ sideMenuSettingsHtml = keepTrackApi.html`
+
+
+ ITRF
+
+
+ J2000
+
+
+ RAE
+
+
+`;
+
+
+ dragOptions: clickDragOptions = {
+ isDraggable: true,
+ minWidth: 350,
+ };
+
+ addHtml(): void {
+ super.addHtml();
+ keepTrackApi.register({
+ event: KeepTrackApiEvents.uiManagerFinal,
+ cbName: 'calculator',
+ cb: () => {
+ // Nothing to do here
+ },
+ });
+ }
+
+ addJs(): void {
+ super.addJs();
+ keepTrackApi.register({
+ event: KeepTrackApiEvents.uiManagerFinal,
+ cbName: Calculator.name,
+ cb: () => {
+ getEl('calculator-itrf').addEventListener('click', () => {
+ this.changeToITRF_();
+ });
+
+ getEl('calculator-j2000').addEventListener('click', () => {
+ this.changeToJ2000_();
+ });
+
+ getEl('calculator-rae').addEventListener('click', () => {
+ this.changeToRAE_();
+ });
+
+ this.addRemovableListeners();
+ },
+ });
+ }
+
+ /**
+ * Adds event listeners to the calculator elements that can be removed later.
+ *
+ * This method attaches click event listeners to the elements with IDs
+ * 'calculator-draw-line' and 'calculator-submit'. When the 'calculator-draw-line'
+ * element is clicked, the `drawLine_` method is called. When the 'calculator-submit'
+ * element is clicked, the default form submission is prevented and the `handleSubmit_`
+ * method is called.
+ *
+ * @private
+ */
+ private addRemovableListeners() {
+ getEl('calculator-draw-line').addEventListener('click', () => {
+ this.drawLine_();
+ });
+
+ getEl('calculator-submit').addEventListener('click', (e) => {
+ e.preventDefault();
+ this.handleSubmit_();
+ });
+ }
+
+ private handleSubmit_(): void {
+ switch (this.currentMode) {
+ case CalculatorMode.ITRF:
+ this.calculateITRF_();
+ break;
+ case CalculatorMode.J2000:
+ this.calculateJ2000_();
+ break;
+ case CalculatorMode.RAE:
+ this.calculateRAE_();
+ break;
+ default:
+ errorManagerInstance.warn('Invalid calculator mode');
+ }
+ }
+
+ private calculateITRF_(): void {
+ const x = getEl('calc-itrf-x-input') as HTMLInputElement;
+ const y = getEl('calc-itrf-y-input') as HTMLInputElement;
+ const z = getEl('calc-itrf-z-input') as HTMLInputElement;
+
+ if (isNaN(Number(x.value)) || isNaN(Number(y.value)) || isNaN(Number(z.value))) {
+ errorManagerInstance.warn('Invalid input for ITRF. It must be a number.');
+
+ return;
+ }
+
+ const ecf = new Vector3D(Number(x.value) as Kilometers, Number(y.value) as Kilometers, Number(z.value) as Kilometers);
+ const date = keepTrackApi.getTimeManager().simulationTimeObj;
+ const gmst = SatMath.calculateTimeVariables(date).gmst;
+ const eci = ecf2eci(ecf, gmst);
+
+ (getEl('calc-j2000-x-input') as HTMLInputElement).value = eci.x.toString();
+ (getEl('calc-j2000-y-input') as HTMLInputElement).value = eci.y.toString();
+ (getEl('calc-j2000-z-input') as HTMLInputElement).value = eci.z.toString();
+
+ const currentSensor = keepTrackApi.getSensorManager().currentSensors[0];
+
+ if (!currentSensor) {
+ (getEl('calc-sensor-name') as HTMLInputElement).value = 'No sensor selected';
+ (getEl('calc-rae-r-input') as HTMLInputElement).value = 'No sensor selected';
+ (getEl('calc-rae-a-input') as HTMLInputElement).value = 'No sensor selected';
+ (getEl('calc-rae-e-input') as HTMLInputElement).value = 'No sensor selected';
+ } else {
+ const rae = eci2rae(date, eci, currentSensor);
+
+ this.sensorUsedInCalculation = new DetailedSensor(currentSensor);
+
+ (getEl('calc-sensor-name') as HTMLInputElement).value = currentSensor.name;
+ (getEl('calc-rae-r-input') as HTMLInputElement).value = rae.rng.toString();
+ (getEl('calc-rae-a-input') as HTMLInputElement).value = rae.az.toString();
+ (getEl('calc-rae-e-input') as HTMLInputElement).value = rae.el.toString();
+ }
+ }
+
+ private calculateJ2000_(): void {
+ const x = getEl('calc-j2000-x-input') as HTMLInputElement;
+ const y = getEl('calc-j2000-y-input') as HTMLInputElement;
+ const z = getEl('calc-j2000-z-input') as HTMLInputElement;
+
+ if (isNaN(Number(x.value)) || isNaN(Number(y.value)) || isNaN(Number(z.value))) {
+ errorManagerInstance.warn('Invalid input for J2000. It must be a number.');
+
+ return;
+ }
+
+ const eci = new Vector3D(Number(x.value) as Kilometers, Number(y.value) as Kilometers, Number(z.value) as Kilometers);
+ const date = keepTrackApi.getTimeManager().simulationTimeObj;
+ const gmst = SatMath.calculateTimeVariables(date).gmst;
+ const ecf = eci2ecf(eci, gmst);
+
+ (getEl('calc-itrf-x-input') as HTMLInputElement).value = ecf.x.toString();
+ (getEl('calc-itrf-y-input') as HTMLInputElement).value = ecf.y.toString();
+ (getEl('calc-itrf-z-input') as HTMLInputElement).value = ecf.z.toString();
+
+ const currentSensor = keepTrackApi.getSensorManager().currentSensors[0];
+
+ if (!currentSensor) {
+ (getEl('calc-sensor-name') as HTMLInputElement).value = 'No sensor selected';
+ (getEl('calc-rae-r-input') as HTMLInputElement).value = 'No sensor selected';
+ (getEl('calc-rae-a-input') as HTMLInputElement).value = 'No sensor selected';
+ (getEl('calc-rae-e-input') as HTMLInputElement).value = 'No sensor selected';
+ } else {
+ const rae = eci2rae(date, eci, currentSensor);
+
+ this.sensorUsedInCalculation = new DetailedSensor(currentSensor);
+
+ (getEl('calc-sensor-name') as HTMLInputElement).value = currentSensor.name;
+ (getEl('calc-rae-r-input') as HTMLInputElement).value = rae.rng.toString();
+ (getEl('calc-rae-a-input') as HTMLInputElement).value = rae.az.toString();
+ (getEl('calc-rae-e-input') as HTMLInputElement).value = rae.el.toString();
+ }
+ }
+
+ private calculateRAE_(): void {
+ const r = getEl('calc-rae-r-input') as HTMLInputElement;
+ const a = getEl('calc-rae-a-input') as HTMLInputElement;
+ const e = getEl('calc-rae-e-input') as HTMLInputElement;
+
+ if (isNaN(Number(r.value)) || isNaN(Number(a.value)) || isNaN(Number(e.value))) {
+ errorManagerInstance.warn('Invalid input for RAE. It must be a number.');
+
+ return;
+ }
+
+ const sensor = keepTrackApi.getSensorManager().currentSensors[0];
+
+ if (!sensor) {
+ errorManagerInstance.warn('No sensor selected');
+
+ return;
+ }
+
+ const rae = {
+ rng: Number(r.value) as Kilometers,
+ az: Number(a.value) as Degrees,
+ el: Number(e.value) as Degrees,
+ } as RaeVec3;
+ const eci = rae2eci(rae, sensor.lla(), SatMath.calculateTimeVariables(keepTrackApi.getTimeManager().simulationTimeObj).gmst);
+
+ (getEl('calc-j2000-x-input') as HTMLInputElement).value = eci.x.toString();
+ (getEl('calc-j2000-y-input') as HTMLInputElement).value = eci.y.toString();
+ (getEl('calc-j2000-z-input') as HTMLInputElement).value = eci.z.toString();
+
+ const ecf = eci2ecf(eci, SatMath.calculateTimeVariables(keepTrackApi.getTimeManager().simulationTimeObj).gmst);
+
+ (getEl('calc-itrf-x-input') as HTMLInputElement).value = ecf.x.toString();
+ (getEl('calc-itrf-y-input') as HTMLInputElement).value = ecf.y.toString();
+ (getEl('calc-itrf-z-input') as HTMLInputElement).value = ecf.z.toString();
+ }
+
+ private changeToITRF_(): void {
+ this.currentMode = CalculatorMode.ITRF;
+
+ getEl('calculator-content-wrapper').innerHTML = this.itrfHtml;
+ this.addRemovableListeners();
+
+ const x = getEl('calc-itrf-x-input') as HTMLInputElement;
+ const y = getEl('calc-itrf-y-input') as HTMLInputElement;
+ const z = getEl('calc-itrf-z-input') as HTMLInputElement;
+
+ x.value = '3000';
+ y.value = '3000';
+ z.value = '3000';
+ }
+
+ private changeToJ2000_(): void {
+ this.currentMode = CalculatorMode.J2000;
+
+ getEl('calculator-content-wrapper').innerHTML = this.j2000Html;
+ this.addRemovableListeners();
+
+ const x = getEl('calc-itrf-x-input') as HTMLInputElement;
+ const y = getEl('calc-itrf-y-input') as HTMLInputElement;
+ const z = getEl('calc-itrf-z-input') as HTMLInputElement;
+
+ x.value = '3000';
+ y.value = '3000';
+ z.value = '3000';
+ }
+
+ private changeToRAE_(): void {
+ this.currentMode = CalculatorMode.RAE;
+
+ getEl('calculator-content-wrapper').innerHTML = this.raeHtml;
+ this.addRemovableListeners();
+
+ const x = getEl('calc-itrf-x-input') as HTMLInputElement;
+ const y = getEl('calc-itrf-y-input') as HTMLInputElement;
+ const z = getEl('calc-itrf-z-input') as HTMLInputElement;
+
+ x.value = '3000';
+ y.value = '3000';
+ z.value = '3000';
+ }
+
+ private drawLine_(): void {
+ const r = getEl('calc-rae-r-input') as HTMLInputElement;
+ const a = getEl('calc-rae-a-input') as HTMLInputElement;
+ const e = getEl('calc-rae-e-input') as HTMLInputElement;
+
+ keepTrackApi.getLineManager().createSensorToRae(this.sensorUsedInCalculation,
+ { rng: Number(r.value) as Kilometers, az: Number(a.value) as Degrees, el: Number(e.value) as Degrees });
+ }
+}
diff --git a/src/plugins/plugins.ts b/src/plugins/plugins.ts
index 81742afa..a34b0076 100644
--- a/src/plugins/plugins.ts
+++ b/src/plugins/plugins.ts
@@ -68,6 +68,7 @@ import { TransponderChannelData } from './transponder-channel-data/transponder-c
import { VideoDirectorPlugin } from './video-director/video-director';
import { WatchlistPlugin } from './watchlist/watchlist';
import { WatchlistOverlay } from './watchlist/watchlist-overlay';
+import { Calculator } from './calculator/calculator';
export type KeepTrackPlugins = {
transponderChannelData?: boolean;
@@ -126,6 +127,7 @@ export type KeepTrackPlugins = {
polarPlot?: boolean;
timeline?: boolean;
timelineAlt?: boolean;
+ calculator?: boolean;
};
// Register all core modules
@@ -181,6 +183,7 @@ export const loadPlugins = (keepTrackApi: KeepTrackApi, plugins: KeepTrackPlugin
{ init: () => new SatellitePhotos().init(), enabled: plugins.photoManager },
{ init: () => new ScreenRecorder().init(), enabled: plugins.screenRecorder },
{ init: () => new AnalysisMenu().init(), enabled: plugins.analysis },
+ { init: () => new Calculator().init(), enabled: plugins.calculator },
/*
* { plugin: eciPlotsPlugin, enabled: plugins.plotAnalysis },
* { plugin: ecfPlotsPlugin, enabled: plugins.plotAnalysis },
diff --git a/src/settings/settings.ts b/src/settings/settings.ts
index 6a8404b1..0bfab9a0 100644
--- a/src/settings/settings.ts
+++ b/src/settings/settings.ts
@@ -87,6 +87,7 @@ export class SettingsManager {
timeline: true,
timelineAlt: true,
transponderChannelData: true,
+ calculator: true,
};
colors: ColorSchemeColorMap;
diff --git a/src/settings/versionDate.js b/src/settings/versionDate.js
index 4837fe47..e2eac3f8 100644
--- a/src/settings/versionDate.js
+++ b/src/settings/versionDate.js
@@ -1,2 +1,2 @@
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-export const VERSION_DATE = 'November 7, 2024';
+export const VERSION_DATE = 'December 12, 2024';
diff --git a/src/singletons/draw-manager/line-manager.ts b/src/singletons/draw-manager/line-manager.ts
index 3ce89828..b33a5189 100644
--- a/src/singletons/draw-manager/line-manager.ts
+++ b/src/singletons/draw-manager/line-manager.ts
@@ -2,7 +2,7 @@
/* eslint-disable complexity */
/* eslint-disable camelcase */
import { KeepTrackApiEvents, Singletons } from '@app/interfaces';
-import { BaseObject, DetailedSatellite, DetailedSensor } from 'ootk';
+import { BaseObject, DetailedSatellite, DetailedSensor, RaeVec3 } from 'ootk';
import { keepTrackApi } from '@app/keepTrackApi';
import { BufferAttribute } from '@app/static/buffer-attribute';
@@ -19,6 +19,7 @@ import { SatToRefLine } from './line-manager/sat-to-ref-line';
import { SatToSunLine } from './line-manager/sat-to-sun-line';
import { SensorScanHorizonLine } from './line-manager/sensor-scan-horizon-line';
import { SensorToMoonLine } from './line-manager/sensor-to-moon-line';
+import { SensorToRaeLine } from './line-manager/sensor-to-rae-line';
import { SensorToSatLine } from './line-manager/sensor-to-sat-line';
import { SensorToSunLine } from './line-manager/sensor-to-sun-line';
@@ -109,6 +110,14 @@ export class LineManager {
this.add(new SensorScanHorizonLine(sensor, face, faces, color));
}
+ createSensorToRae(sensor: DetailedSensor | null, rae: RaeVec3, color?: vec4): void {
+ if (!sensor) {
+ return;
+ }
+
+ this.add(new SensorToRaeLine(sensor, rae, color));
+ }
+
createSensorToSat(sensor: DetailedSensor | null, sat: DetailedSatellite | MissileObject | null, color?: vec4): void {
if (!sensor || !sat || !(sat instanceof DetailedSatellite)) {
return;
diff --git a/src/singletons/draw-manager/line-manager/sensor-to-rae-line.ts b/src/singletons/draw-manager/line-manager/sensor-to-rae-line.ts
new file mode 100644
index 00000000..b42e3b56
--- /dev/null
+++ b/src/singletons/draw-manager/line-manager/sensor-to-rae-line.ts
@@ -0,0 +1,33 @@
+import { EciArr3 } from '@app/interfaces';
+import { keepTrackApi } from '@app/keepTrackApi';
+import { SatMath } from '@app/static/sat-math';
+import { vec4 } from 'gl-matrix';
+import { DetailedSensor, rae2eci, RaeVec3 } from 'ootk';
+import { Line, LineColors } from './line';
+
+export class SensorToRaeLine extends Line {
+ sensor: DetailedSensor;
+ rae: RaeVec3;
+
+ constructor(sensor: DetailedSensor, rae: RaeVec3, color?: vec4) {
+ super();
+ this.rae = rae;
+ this.sensor = sensor;
+ this.color_ = color || LineColors.GREEN;
+ }
+
+ update(): void {
+ const posData = keepTrackApi.getDotsManager().positionData;
+ const id = this.sensor.id;
+ const sensorEciArr = [posData[id * 3], posData[id * 3 + 1], posData[id * 3 + 2]] as EciArr3;
+
+ const gmst = SatMath.calculateTimeVariables(keepTrackApi.getTimeManager().simulationTimeObj).gmst;
+
+ const raeInEci = rae2eci(this.rae, this.sensor.lla(), gmst);
+ const eciArr = [raeInEci.x, raeInEci.y, raeInEci.z] as EciArr3;
+
+ this.isDraw_ = true;
+
+ this.updateVertBuf(eciArr, sensorEciArr);
+ }
+}
diff --git a/src/static/catalog-search.ts b/src/static/catalog-search.ts
index c65293c4..910d7efe 100644
--- a/src/static/catalog-search.ts
+++ b/src/static/catalog-search.ts
@@ -103,18 +103,19 @@ export class CatalogSearch {
const maxInclination = sat.inclination + INC_MARGIN;
const minInclination = sat.inclination - INC_MARGIN;
- let maxRaan = sat.rightAscension + RAAN_MARGIN;
- let minRaan = sat.rightAscension - RAAN_MARGIN;
- if (sat.rightAscension >= 360 - RAAN_MARGIN) {
+ const now = new Date();
+ const normalizedSatRaan = SatMath.normalizeRaan(sat, now);
+ let maxRaan = normalizedSatRaan + RAAN_MARGIN;
+ let minRaan = normalizedSatRaan - RAAN_MARGIN;
+
+ if (normalizedSatRaan >= 360 - RAAN_MARGIN) {
maxRaan -= 360;
}
- if (sat.rightAscension <= RAAN_MARGIN) {
+ if (normalizedSatRaan <= RAAN_MARGIN) {
minRaan += 360;
}
- const now = new Date();
- const normalizedSatRaan = this.normalizeRaan(sat, now);
return satData
.filter((s) => {
@@ -133,7 +134,7 @@ export class CatalogSearch {
return false;
}
- const normalizedSearchRaan = this.normalizeRaan(s, now);
+ const normalizedSearchRaan = SatMath.normalizeRaan(s, now);
// Handle RAAN wraparound case
if (normalizedSatRaan > 360 - RAAN_MARGIN || normalizedSatRaan < RAAN_MARGIN) {
@@ -146,35 +147,6 @@ export class CatalogSearch {
.map((s) => s.id);
}
- // Normalize the RAAN based on nodal precession
- static normalizeRaan(sat: DetailedSatellite, now: Date): number {
- const precessionRate = this.getNodalPrecessionRate(sat);
- const daysSinceEpoch = SatMath.calcElsetAge(sat, now);
- let normalizedRaan = sat.rightAscension + (precessionRate * daysSinceEpoch);
-
- // Ensure RAAN stays within 0-360 range
- normalizedRaan = ((normalizedRaan % 360) + 360) % 360;
-
- return normalizedRaan;
- }
-
- // Calculate nodal precession rate (degrees per day)
- static getNodalPrecessionRate(s: DetailedSatellite): number {
- const Re = 6378137; // Earth radius in meters
- const J2 = 1.082626680e-3; // Earth's second dynamic form factor
- const period = s.period * 60; // Convert period from minutes to seconds
- const omega = (2 * Math.PI) / period; // Angular velocity in rad/s
- const a = s.semiMajorAxis * 1000; // Convert semi-major axis from km to meters
- const e = s.eccentricity;
- const i = s.inclination * Math.PI / 180; // Convert inclination to radians
-
- // Calculate precession rate in rad/s
- const omegaP = (-3 / 2) * (Re / a) ** 2 / (1 - e * e) ** 2 * J2 * omega * Math.cos(i);
-
- // Convert to degrees per day
- return omegaP * (180 / Math.PI) * 86400;
- }
-
/**
* This method is used to find the reentry objects from the given satellite data.
diff --git a/src/static/classification.ts b/src/static/classification.ts
index 2d6869e4..77ce9a4c 100644
--- a/src/static/classification.ts
+++ b/src/static/classification.ts
@@ -34,6 +34,10 @@ export class Classification {
}
static isValidClassification(classification: string): boolean {
+ if (!classification || classification === '') {
+ return false;
+ }
+
return ['Unclassified', 'Confidential', 'CUI', 'Secret', 'Top Secret', 'Top Secret//SCI'].some((validClassification) => classification.startsWith(validClassification));
}
}
diff --git a/src/static/sat-math.ts b/src/static/sat-math.ts
index f7af874f..313e2990 100644
--- a/src/static/sat-math.ts
+++ b/src/static/sat-math.ts
@@ -837,4 +837,67 @@ export abstract class SatMath {
return (inc * RAD2DEG);
}
+
+
+ /**
+ * Normalizes the Right Ascension of the Ascending Node (RAAN) for a given satellite.
+ *
+ * This function calculates the normalized RAAN by accounting for the nodal precession rate
+ * and the number of days since the satellite's epoch. The resulting RAAN is adjusted to
+ * ensure it stays within the 0-360 degree range.
+ *
+ * @param sat - The detailed satellite object containing its orbital parameters.
+ * @param now - The current date used to calculate the number of days since the satellite's epoch.
+ * @returns The normalized RAAN value within the 0-360 degree range.
+ */
+ static normalizeRaan(sat: DetailedSatellite, now: Date): number {
+ const precessionRate = this.getNodalPrecessionRate(sat);
+ const daysSinceEpoch = SatMath.calcElsetAge(sat, now);
+ let normalizedRaan = sat.rightAscension + (precessionRate * daysSinceEpoch);
+
+ // Ensure RAAN stays within 0-360 range
+ normalizedRaan = ((normalizedRaan % 360) + 360) % 360;
+
+ return normalizedRaan;
+ }
+
+ /**
+ * Calculates the nodal precession rate of a satellite.
+ *
+ * @param {DetailedSatellite} s - The satellite object containing its orbital parameters.
+ * @returns {number} The nodal precession rate in degrees per day.
+ *
+ * @remarks
+ * The nodal precession rate is influenced by the Earth's oblateness (J2), the satellite's
+ * semi-major axis, eccentricity, inclination, and orbital period. This function converts
+ * the inclination from degrees to radians and the semi-major axis from kilometers to meters
+ * before performing the calculation.
+ *
+ * @example
+ * ```typescript
+ * const satellite = {
+ * period: 90, // in minutes
+ * semiMajorAxis: 7000, // in kilometers
+ * eccentricity: 0.001,
+ * inclination: 98.7 // in degrees
+ * };
+ * const precessionRate = getNodalPrecessionRate(satellite);
+ * console.log(precessionRate); // Output: nodal precession rate in degrees per day
+ * ```
+ */
+ static getNodalPrecessionRate(s: DetailedSatellite): number {
+ const Re = 6378137; // Earth radius in meters
+ const J2 = 1.082626680e-3; // Earth's second dynamic form factor
+ const period = s.period * 60; // Convert period from minutes to seconds
+ const omega = (2 * Math.PI) / period; // Angular velocity in rad/s
+ const a = s.semiMajorAxis * 1000; // Convert semi-major axis from km to meters
+ const e = s.eccentricity;
+ const i = s.inclination * Math.PI / 180; // Convert inclination to radians
+
+ // Calculate precession rate in rad/s
+ const omegaP = (-3 / 2) * (Re / a) ** 2 / (1 - e * e) ** 2 * J2 * omega * Math.cos(i);
+
+ // Convert to degrees per day
+ return omegaP * (180 / Math.PI) * 86400;
+ }
}
diff --git a/test/catalog-manager.test.ts b/test/catalog-manager.test.ts
index 384d4a21..e4ca4bd9 100644
--- a/test/catalog-manager.test.ts
+++ b/test/catalog-manager.test.ts
@@ -75,6 +75,12 @@ describe('calcSatrec', () => {
selectSataManagerInstance.selectedSat = defaultSat.id;
catalogManagerInstance.objectCache = [defaultSat, matchSat, nonmatchSat, nonmatchSat2, nonmatchSat3, nonmatchSat4];
+
+ // mock new Date() with new Date(2021, 6, 22, 12);
+ const mockDate = new Date(2021, 6, 22, 12);
+
+ jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
+
const satData = CatalogSearch.findObjsByOrbit(catalogManagerInstance.objectCache as DetailedSatellite[], defaultSat);
expect(satData).toStrictEqual([0, 1]);
diff --git a/test/catalog-search.test.ts b/test/catalog-search.test.ts
index 2ef95d3e..503c068f 100644
--- a/test/catalog-search.test.ts
+++ b/test/catalog-search.test.ts
@@ -28,7 +28,7 @@ describe('CatalogSearch_class', () => {
// Tests that year method filters correctly based on year
it('test_year_filters_correctly', () => {
- const filteredData = CatalogSearch.year(satData as any, 98);
+ const filteredData = CatalogSearch.year(satData, 98);
expect(filteredData.length).toBe(1);
expect(filteredData[0].id).toBe(1);
@@ -36,49 +36,49 @@ describe('CatalogSearch_class', () => {
// Tests that yearOrLess method filters correctly based on year
it('test_year_or_less_filters_correctly', () => {
- const filteredData = CatalogSearch.yearOrLess(satData as any, 99);
+ const filteredData = CatalogSearch.yearOrLess(satData, 99);
expect(filteredData.length).toBe(1);
});
// Tests that yearOrLess method filters correctly when year greater than 99
it('test_year_or_less_filters_correctly_when_year_greater_than_99', () => {
- const filteredData = CatalogSearch.yearOrLess(satData as any, 2);
+ const filteredData = CatalogSearch.yearOrLess(satData, 2);
expect(filteredData.length).toBe(2);
});
// Tests that objectName method filters correctly based on object name
it('test_object_name_filters_correctly', () => {
- const filteredData = CatalogSearch.objectName(satData as any, /ISS/u);
+ const filteredData = CatalogSearch.objectName(satData, /ISS/u);
expect(filteredData.length).toBe(1);
});
// Tests that country method filters correctly based on country
it('test_country_filters_correctly', () => {
- const filteredData = CatalogSearch.country(satData as any, /USA/u);
+ const filteredData = CatalogSearch.country(satData, /USA/u);
expect(filteredData.length).toBe(1);
});
// Tests that shape method filters correctly based on shape
it('test_shape_filters_correctly', () => {
- const filteredData = CatalogSearch.shape(satData as any, 'SPHERICAL');
+ const filteredData = CatalogSearch.shape(satData, 'SPHERICAL');
expect(filteredData.length).toBe(1);
});
// Tests that bus method filters correctly based on bus
it('test_bus_filters_correctly', () => {
- const filteredData = CatalogSearch.bus(satData as any, 'A2100');
+ const filteredData = CatalogSearch.bus(satData, 'A2100');
expect(filteredData.length).toBe(1);
});
// Tests that type method filters correctly based on type
it('test_type_filters_correctly', () => {
- const filteredData = CatalogSearch.type(satData as any, SpaceObjectType.PAYLOAD as SpaceObjectType);
+ const filteredData = CatalogSearch.type(satData, SpaceObjectType.PAYLOAD as SpaceObjectType);
expect(filteredData.length).toBe(1);
});