From 796e57f51eaa47cd3c8fd8677df4785a84f74aa8 Mon Sep 17 00:00:00 2001 From: Theodore Kruczek Date: Thu, 12 Dec 2024 08:00:19 -0500 Subject: [PATCH 1/3] refactor: :recycle: refactor catalog-search to move new functions to satMath class --- docs/jday.txt | 44 +++++++++++++++++++++++++ src/static/catalog-search.ts | 44 +++++-------------------- src/static/classification.ts | 4 +++ src/static/sat-math.ts | 63 ++++++++++++++++++++++++++++++++++++ test/catalog-search.test.ts | 16 ++++----- 5 files changed, 127 insertions(+), 44 deletions(-) create mode 100644 docs/jday.txt diff --git a/docs/jday.txt b/docs/jday.txt new file mode 100644 index 000000000..f4349f7cc --- /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/src/static/catalog-search.ts b/src/static/catalog-search.ts index c65293c42..01c5d1b80 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(2021, 6, 22, 12); + 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 2d6869e4b..77ce9a4ca 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 f7af874fd..313e2990e 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-search.test.ts b/test/catalog-search.test.ts index 2ef95d3e0..503c068f4 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); }); From 9ce354e2a96aa71357b2f9bb007c09f4846f0fdb Mon Sep 17 00:00:00 2001 From: Theodore Kruczek Date: Thu, 12 Dec 2024 08:06:31 -0500 Subject: [PATCH 2/3] fix: :poop: move mock date to test not live code --- src/settings/versionDate.js | 2 +- src/static/catalog-search.ts | 2 +- test/catalog-manager.test.ts | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/settings/versionDate.js b/src/settings/versionDate.js index 4837fe473..91639fe19 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 11, 2024'; diff --git a/src/static/catalog-search.ts b/src/static/catalog-search.ts index 01c5d1b80..910d7efec 100644 --- a/src/static/catalog-search.ts +++ b/src/static/catalog-search.ts @@ -104,7 +104,7 @@ export class CatalogSearch { const maxInclination = sat.inclination + INC_MARGIN; const minInclination = sat.inclination - INC_MARGIN; - const now = new Date(2021, 6, 22, 12); + const now = new Date(); const normalizedSatRaan = SatMath.normalizeRaan(sat, now); let maxRaan = normalizedSatRaan + RAAN_MARGIN; let minRaan = normalizedSatRaan - RAAN_MARGIN; diff --git a/test/catalog-manager.test.ts b/test/catalog-manager.test.ts index 384d4a21d..e4ca4bd95 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]); From 1c16fff0dd7ce6bc104b3a5fe2e00d2d590bad21 Mon Sep 17 00:00:00 2001 From: Theodore Kruczek Date: Thu, 12 Dec 2024 14:07:03 -0500 Subject: [PATCH 3/3] feat: :sparkles: add caclulator plugin --- public/img/icons/calculator.png | 3 + public/settings/settingsOverride.js | 1 + src/locales/de.json | 5 + src/locales/en.json | 5 + src/locales/es.json | 5 + src/locales/locales.ts | 5 + src/plugins/analysis/analysis.ts | 34 +- src/plugins/calculator/calculator.ts | 551 ++++++++++++++++++ src/plugins/plugins.ts | 3 + src/settings/settings.ts | 1 + src/settings/versionDate.js | 2 +- src/singletons/draw-manager/line-manager.ts | 11 +- .../line-manager/sensor-to-rae-line.ts | 33 ++ 13 files changed, 640 insertions(+), 19 deletions(-) create mode 100644 public/img/icons/calculator.png create mode 100644 src/plugins/calculator/calculator.ts create mode 100644 src/singletons/draw-manager/line-manager/sensor-to-rae-line.ts diff --git a/public/img/icons/calculator.png b/public/img/icons/calculator.png new file mode 100644 index 000000000..04985fd5f --- /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 974659f8f..07d2c4271 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 badf061c3..f4c01f0f6 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 2e727f17a..6688faf74 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 8c2211037..59dd04f48 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 b0d4b2f81..f020d03d4 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 8e5929f8a..2668f412c 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 000000000..5dde7d8b7 --- /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` +
+
+
+ ITRF +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+ +
+
+ +
+ J2000 +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+ RAE +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+ +
+
+ `; + private readonly raeHtml = keepTrackApi.html` +
+
+
+ RAE +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+ +
+
+ +
+ J2000 +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+ ITRF +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+ +
+
+ `; + private readonly j2000Html = keepTrackApi.html` +
+
+
+ J2000 +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+ +
+
+ +
+ ITRF +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+ RAE +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+ +
+
+ `; + + sideMenuElementHtml = keepTrackApi.html` +
+ ${this.itrfHtml} +
`; + sideMenuSettingsHtml = keepTrackApi.html` +
+
+ +
+
+ +
+
+ +
+
+`; + + + 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 81742afa7..a34b0076e 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 6a8404b19..0bfab9a0e 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 91639fe19..e2eac3f80 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 = 'December 11, 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 3ce898283..b33a51894 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 000000000..b42e3b56f --- /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); + } +}