diff --git a/package-lock.json b/package-lock.json index 77f52b5a1..6b8c0c5a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "new-github-issue-url": "^1.0.0", "node-fetch": "^3.3.2", "numeric": "^1.2.6", - "ootk": "^4.0.1", + "ootk": "^4.0.2", "papaparse": "^5.4.1", "resizable": "^1.2.1", "ts-node": "^10.9.1", @@ -12618,17 +12618,17 @@ } }, "node_modules/ootk": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/ootk/-/ootk-4.0.1.tgz", - "integrity": "sha512-om9mqrHy6kRlqPA58rPNVro/bdmAFbBOc0n431kEi5vrYvNCaQsUwVpH3yjhtKDUfNY/flUH/WPFPw2Loh+y4A==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/ootk/-/ootk-4.0.2.tgz", + "integrity": "sha512-XXbphQ+XfVUuaLO7GKI3erHrrQd1Up3I7Wc5D9JHTLP8W/Ki7FY1DeUMFidbJ2U6h/QMn9uspuLx+HvX8KBIkw==", "dependencies": { - "ootk-core": "^1.2.3" + "ootk-core": "^1.2.4" } }, "node_modules/ootk-core": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/ootk-core/-/ootk-core-1.2.3.tgz", - "integrity": "sha512-oUgmNMNhddSGdiw7m+/gJHxE/PiQikuVJNuGthrnqs9duk2hrPGEEwi9h8wlNnUv2BWddNFAcrF7H3ZZEwIjkw==" + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/ootk-core/-/ootk-core-1.2.4.tgz", + "integrity": "sha512-AykpA8qta0vhXoHJry/ebNJlZSP7jiTHlq/5FJ4yLKMww2iQ2LftZm8kAdwkjnO4tp1CpeWn84cknXBgh1m6tg==" }, "node_modules/opener": { "version": "1.5.2", diff --git a/package.json b/package.json index 4e4f0a4fe..5969e41d5 100644 --- a/package.json +++ b/package.json @@ -151,7 +151,7 @@ "new-github-issue-url": "^1.0.0", "node-fetch": "^3.3.2", "numeric": "^1.2.6", - "ootk": "^4.0.1", + "ootk": "^4.0.2", "papaparse": "^5.4.1", "resizable": "^1.2.1", "ts-node": "^10.9.1", diff --git a/src/lib/click-and-drag.ts b/src/lib/click-and-drag.ts index c481bcbef..ea058cb56 100644 --- a/src/lib/click-and-drag.ts +++ b/src/lib/click-and-drag.ts @@ -1,9 +1,6 @@ -interface ClickDragOptions { - minWidth?: number; - maxWidth?: number; -} +import { clickDragOptions } from '@app/plugins/KeepTrackPlugin'; -export const clickAndDragWidth = (el: HTMLElement | null, options: ClickDragOptions = {}): void => { +export const clickAndDragWidth = (el: HTMLElement | null, options: clickDragOptions = {}): void => { if (!el) { return; } @@ -19,10 +16,12 @@ export const clickAndDragWidth = (el: HTMLElement | null, options: ClickDragOpti settingsManager.isDragging = false; - // create new element on right edge - const edgeEl = createElWidth_(el); + if (options.isDraggable) { + // create new element on right edge + const edgeEl = createElWidth_(el); - addEventsWidth_(edgeEl, el, width, minWidth, maxWidth); + addEventsWidth_(edgeEl, el, width, minWidth, maxWidth); + } }; export const clickAndDragHeight = (el: HTMLElement, maxHeight?: number, callback?: () => void): void => { diff --git a/src/plugins/KeepTrackPlugin.ts b/src/plugins/KeepTrackPlugin.ts index 78d71b765..12d29ba89 100644 --- a/src/plugins/KeepTrackPlugin.ts +++ b/src/plugins/KeepTrackPlugin.ts @@ -225,7 +225,7 @@ export class KeepTrackPlugin { this.registerSubmitButtonClicked(this.submitCallback); } - if (this.dragOptions?.isDraggable) { + if (this.dragOptions) { this.registerClickAndDragOptions(this.dragOptions); } @@ -456,7 +456,7 @@ export class KeepTrackPlugin { * @param callback The callback function to run when the bottom icon is clicked. This is run * even if the bottom icon is disabled. */ - registerBottomMenuClicked(callback: () => void = () => {}) { + registerBottomMenuClicked(callback: () => void = () => { }) { if (this.isRequireSensorSelected && this.isRequireSatelliteSelected) { keepTrackApi.register({ event: KeepTrackApiEvents.selectSatData, diff --git a/src/plugins/reports/reports.ts b/src/plugins/reports/reports.ts index c0f42c715..b5ef8cda5 100644 --- a/src/plugins/reports/reports.ts +++ b/src/plugins/reports/reports.ts @@ -5,10 +5,18 @@ import { errorManagerInstance } from '@app/singletons/errorManager'; import analysisPng from '@public/img/icons/reports.png'; -import { BaseObject, DetailedSatellite, MILLISECONDS_PER_SECOND } from 'ootk'; +import { BaseObject, DetailedSatellite, DetailedSensor, MILLISECONDS_PER_SECOND } from 'ootk'; import { KeepTrackPlugin, clickDragOptions } from '../KeepTrackPlugin'; import { SelectSatManager } from '../select-sat-manager/select-sat-manager'; +interface ReportData { + filename: string; + header: string; + body: string; + columns?: number; + isHeaders?: boolean; +} + export class ReportsPlugin extends KeepTrackPlugin { static readonly PLUGIN_NAME = 'Reports'; dependencies = [SelectSatManager.PLUGIN_NAME]; @@ -33,10 +41,17 @@ export class ReportsPlugin extends KeepTrackPlugin {
Reports
-
-
+
+ +
@@ -48,7 +63,8 @@ export class ReportsPlugin extends KeepTrackPlugin { helpBody = keepTrackApi.html`The Reports Menu is a collection of tools to help you analyze and understand the data you are viewing.`; dragOptions: clickDragOptions = { - isDraggable: true, + isDraggable: false, + minWidth: 320, }; addJs(): void { @@ -57,7 +73,10 @@ export class ReportsPlugin extends KeepTrackPlugin { event: KeepTrackApiEvents.uiManagerFinal, cbName: this.PLUGIN_NAME, cb: () => { - getEl('aer-report-btn').addEventListener('click', () => this.generateAerReport()); + getEl('aer-report-btn').addEventListener('click', () => this.generateAzElRng_()); + getEl('coes-report-btn').addEventListener('click', () => this.generateClasicalOrbElJ2000_()); + getEl('eci-report-btn').addEventListener('click', () => this.generateEci_()); + getEl('lla-report-btn').addEventListener('click', () => this.generateLla_()); }, }); @@ -76,73 +95,175 @@ export class ReportsPlugin extends KeepTrackPlugin { }); } - generateAerReport() { - const sensorManager = keepTrackApi.getSensorManager(); - const sat = this.selectSatManager_.primarySatObj as DetailedSatellite; + private generateAzElRng_() { + const sat = this.getSat_(); + const sensor = this.getSensor_(); + if (!sat || !sensor) { + return; + } - if (!sensorManager.isSensorSelected()) { - errorManagerInstance.warn('Select a sensor first!'); + const header = `Azimuth Elevation Range Report\n-------------------------------\n${this.createHeader_(sat, sensor)}`; + let body = 'Time (UTC),Azimuth(°),Elevation(°),Range(km)\n'; + const durationInSeconds = 72 * 60 * 60; + let isInCoverage = false; + let time = this.getStartTime_(); - return; + for (let t = 0; t < durationInSeconds; t += 30) { + time = new Date(time.getTime() + MILLISECONDS_PER_SECOND * 30); + const rae = sensor.rae(sat, time); + + if (rae.el > 0) { + isInCoverage = true; + body += `${this.formatTime_(time)},${rae.az.toFixed(3)},${rae.el.toFixed(3)},${rae.rng.toFixed(3)}\n`; + } else if (isInCoverage) { + // If we were in coverage but now we are not, add a blank line to separate the passes + body += '\n\n'; + isInCoverage = false; + } } - if (!sat) { - errorManagerInstance.warn('Select a satellite first!'); - return; + if (body === 'Time (UTC),Azimuth(°),Elevation(°),Range(km)\n') { + body += 'No passes found!'; } - if (!(sat instanceof DetailedSatellite)) { - errorManagerInstance.warn('Satellite is not DetailedSatellite!'); + this.writeReport_({ + filename: `aer-${sat.sccNum}`, + header, + body, + }); + } + private formatTime_(time: Date) { + const timeStr = time.toISOString(); + const timeStrSplit = timeStr.split('T'); + const date = timeStrSplit[0]; + const timeSplit = timeStrSplit[1].split('.'); + const timeOut = timeSplit[0]; + + return `${date} ${timeOut}`; + } + + private generateLla_() { + const sat = this.getSat_(); + + if (!sat) { return; } - const sensor = sensorManager.currentSensors[0]; + const header = `Latitude Longitude Altitude Report\n-------------------------------\n${this.createHeader_(sat)}`; + let body = 'Time (UTC),Latitude(°),Longitude(°),Altitude(km)\n'; + const durationInSeconds = 72 * 60 * 60; + let time = this.getStartTime_(); + for (let t = 0; t < durationInSeconds; t += 30) { + time = new Date(time.getTime() + 30 * MILLISECONDS_PER_SECOND); + const lla = sat.lla(time); - /* - * Azimuth Elevation Range Report - * ------------------------------ - * Satellite: [Satellite Name] - * NORAD ID: [NORAD ID] - * Date: [Date] - */ + body += `${this.formatTime_(time)},${lla.lat.toFixed(3)},${lla.lon.toFixed(3)},${lla.alt.toFixed(3)}\n`; + } - const reportHeader = `Azimuth Elevation Range Report\n-------------------------------\nSatellite: ${sat.name}\nNORAD ID: ${sat.sccNum}\nDate: ${new Date().toISOString()}\n\n`; - let report = 'Time (UTC),Azimuth(°),Elevation(°),Range(km)\n'; - const durationInMinutes = 72 * 60; - let isInCoverage = false; + this.writeReport_({ + filename: `lla-${sat.sccNum}`, + header, + body, + }); + } - for (let t = 0; t < durationInMinutes; t++) { - const time = keepTrackApi.getTimeManager().getOffsetTimeObj(t * MILLISECONDS_PER_SECOND * 60); - const rae = sensor.rae(sat, time); + private generateEci_() { + const sat = this.getSat_(); - if (rae.el > 0) { - isInCoverage = true; - report += `${time.toISOString()},${rae.az.toFixed(3)},${rae.el.toFixed(3)},${rae.rng.toFixed(3)}\n`; - } else if (isInCoverage) { - // If we were in coverage but now we are not, add a blank line to separate the passes - report += '\n\n'; - isInCoverage = false; - } + if (!sat) { + return; } - if (report === 'Time (UTC),Azimuth(°),Elevation(°),Range(km)\n') { - report += 'No passes found!'; + const header = `Earth Centered Intertial Report\n-------------------------------\n${this.createHeader_(sat)}`; + let body = 'Time (UTC),Position X(km),Position Y(km),Position Z(km),Velocity X(km/s),Velocity Y(km/s),Velocity Z(km/s)\n'; + const durationInSeconds = 72 * 60 * 60; + let time = this.getStartTime_(); + + for (let t = 0; t < durationInSeconds; t += 30) { + time = new Date(time.getTime() + 30 * MILLISECONDS_PER_SECOND); + const eci = sat.eci(time); + + body += `${this.formatTime_(time)},${eci.position.x.toFixed(3)},${eci.position.y.toFixed(3)},${eci.position.z.toFixed(3)},` + + `${eci.velocity.x.toFixed(3)},${eci.velocity.y.toFixed(3)},${eci.velocity.z.toFixed(3)}\n`; } - this.writeReport(sat, reportHeader, report); + this.writeReport_({ + filename: `eci-${sat.sccNum}`, + header, + body, + columns: 7, + isHeaders: true, + }); + } + + private createHeader_(sat: DetailedSatellite, sensor?: DetailedSensor) { + const satData = '' + + `Date: ${new Date().toISOString()}\n` + + `Satellite: ${sat.name}\n` + + `NORAD ID: ${sat.sccNum}\n` + + `Alternate ID: ${sat.altId || 'None'}\n` + + `International Designator: ${sat.intlDes}\n\n`; + const sensorData = '' + + `Sensor: ${sensor ? sensor.name : 'None'}\n` + + `Type: ${sensor ? sensor.getTypeString() : 'None'}\n` + + `Latitude: ${sensor ? sensor.lat : 'None'}\n` + + `Longitude: ${sensor ? sensor.lon : 'None'}\n` + + `Altitude: ${sensor ? sensor.alt : 'None'}\n` + + `Min Azimuth: ${sensor ? sensor.minAz : 'None'}\n` + + `Max Azimuth: ${sensor ? sensor.maxAz : 'None'}\n` + + `Min Elevation: ${sensor ? sensor.minEl : 'None'}\n` + + `Max Elevation: ${sensor ? sensor.maxEl : 'None'}\n` + + `Min Range: ${sensor ? sensor.minRng : 'None'}\n` + + `Max Range: ${sensor ? sensor.maxRng : 'None'}\n\n`; + + + return sensor ? `${satData}${sensorData}` : `${satData}`; } - writeReport(sat: DetailedSatellite, reportHeader: string, report: string) { + private generateClasicalOrbElJ2000_() { + const sat = this.getSat_(); + + if (!sat) { + return; + } + + const header = `Classic Orbit Elements Report\n-------------------------------\n${this.createHeader_(sat)}`; + const classicalEls = sat.toJ2000().toClassicalElements(); + const body = '' + + `Epoch, ${classicalEls.epoch}\n` + + `Apogee, ${classicalEls.apogee.toFixed(3)} km\n` + + `Perigee, ${classicalEls.perigee.toFixed(3)} km\n` + + `Inclination, ${classicalEls.inclination.toFixed(3)}°\n` + + `Right Ascension, ${classicalEls.rightAscensionDegrees.toFixed(3)}°\n` + + `Argument of Perigee, ${classicalEls.argPerigeeDegrees.toFixed(3)}°\n` + + `True Anomaly, ${classicalEls.trueAnomalyDegrees.toFixed(3)}°\n` + + `Eccentricity, ${classicalEls.eccentricity.toFixed(3)}\n` + + `Period, ${classicalEls.period.toFixed(3)} min\n` + + `Semi-Major Axis, ${classicalEls.semimajorAxis.toFixed(3)} km\n` + + `Mean Motion, ${classicalEls.meanMotion.toFixed(3)} rev/day`; + + + this.writeReport_({ + filename: `coes-${sat.sccNum}`, + header, + body, + columns: 2, + isHeaders: false, + }); + } + + private writeReport_({ filename, header, body, columns = 4, isHeaders = true }: ReportData) { // Open a new window and write the report to it - the title of the window should be the satellite name - const win = window.open('text/plain', sat.name); + const win = window.open('text/plain', filename); - const colWidths = [0, 0, 0, 0]; + // Create an array that is columns long and fill it with 0s + const colWidths = new Array(columns).fill(0); if (win) { - const formattedReport = report + const formattedReport = body .split('\n') .map((line) => line.split(',')) .map((values, idx) => values.map((value, idx2) => { @@ -160,7 +281,7 @@ export class ReportsPlugin extends KeepTrackPlugin { .map((values, idx) => { const row = values.join(' '); - if (idx === 0) { + if (idx === 0 && isHeaders) { // Add ---- under the entire header const header = values.join(' '); const headerUnderline = header.replace(/./gu, '-'); @@ -174,12 +295,56 @@ export class ReportsPlugin extends KeepTrackPlugin { }) .join('\n'); - win.document.write(`${reportHeader}${formattedReport}</plaintext>`); - win.document.title = sat.name; - win.history.replaceState(null, sat.name, `/${sat.name}.txt`); + // Create a download button at the top so you can download the report as a .txt file + win.document.write(`<a href="data:text/plain;charset=utf-8,${encodeURIComponent(header + formattedReport)}" download="${filename}.txt">Download Report</a><br>`); + + win.document.write(`<plaintext>${header}${formattedReport}`); + win.document.title = filename; + win.history.replaceState(null, filename, `/${filename}.txt`); } else { // eslint-disable-next-line no-alert alert('Please allow popups for this site'); } } + + private getStartTime_() { + const time = keepTrackApi.getTimeManager().getOffsetTimeObj(0); + + time.setMilliseconds(0); + time.setSeconds(0); + + return time; + } + + private getSat_(): DetailedSatellite { + const sat = this.selectSatManager_.primarySatObj as DetailedSatellite; + + if (!sat) { + errorManagerInstance.warn('Select a satellite first!'); + + return null; + } + + if (!(sat instanceof DetailedSatellite)) { + errorManagerInstance.warn('Satellite is not DetailedSatellite!'); + + return null; + } + + return sat; + } + + private getSensor_(): DetailedSensor { + const sensorManager = keepTrackApi.getSensorManager(); + + if (!sensorManager.isSensorSelected()) { + errorManagerInstance.warn('Select a sensor first!'); + + return null; + } + + const sensor = sensorManager.currentSensors[0]; + + return sensor; + } } diff --git a/src/settings/versionDate.js b/src/settings/versionDate.js index ec7e19bb3..43eb2f633 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 = 'July 22, 2024'; +export const VERSION_DATE = 'August 3, 2024';