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: " }, + "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: " + }, "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: " }, + "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: " + }, "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: " }, + "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: " + }, "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` +
+
+
+ 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 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); });