Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,17 @@
"requestfinished",
"LOCF",
"Unack",
"Tabnabbing"
"Tabnabbing",
"elset",
"statevector",
"NAVSTAR",
"raan",
"sbirs",
"SBIRS",
"FENGYUN",
"STARLINK",
"norad",
"NORAD"
Comment on lines +492 to +502

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: cspell relies on 3-letter identifiers being under minWordLength

The PR adds many domain terms to .cspell.json (elset, statevector, NAVSTAR, raan, sbirs, FENGYUN, STARLINK, norad) but not 'udl', 'gps', 'geo', 'wgs', 'deb', which appear widely in the new code. This passes CI only because cspell's default minWordLength (4) skips those 3-char tokens, and multi-char terms like 'ephemeris'/'conjunction'/'iridium'/'cosmos' are standard dictionary words. If the spellcheck config or dictionary set changes, these could start failing. Not flagged as a bug since the current CI check passes.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
"ignorePaths": [
Expand Down
87 changes: 87 additions & 0 deletions example/udlFederation/ConjunctionLimitProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/

// Screening thresholds commonly used for conjunction assessment triage.
const PC_CRITICAL = 1e-4;
const PC_WARNING = 1e-5;

const LIMITS = {
critical: {
cssClass: 'is-limit--upr is-limit--red',
low: PC_CRITICAL,
high: Number.POSITIVE_INFINITY,
name: 'Pc Critical'
},
warning: {
cssClass: 'is-limit--upr is-limit--yellow',
low: PC_WARNING,
high: PC_CRITICAL,
name: 'Pc Warning'
}
};

export default class ConjunctionLimitProvider {
supportsLimits(domainObject) {
return domainObject.type === 'udl.conjunctions';
}

getLimitEvaluator() {
return {
evaluate: function (datum, valueMetadata) {
if (!valueMetadata || valueMetadata.key !== 'pc') {
return undefined;
}

if (datum.pc >= PC_CRITICAL) {
return LIMITS.critical;
}

if (datum.pc >= PC_WARNING) {
return LIMITS.warning;
}

return undefined;
}
};
}

getLimits() {
return {
limits: function () {
return Promise.resolve({
WARNING: {
high: {
color: 'yellow',
pc: PC_WARNING
}
},
CRITICAL: {
high: {
color: 'red',
pc: PC_CRITICAL
}
}
});
}
};
}
Comment on lines +67 to +86

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Limit provider interface and getLimits format match existing conventions

I verified ConjunctionLimitProvider against src/api/telemetry/TelemetryAPI.js:180-231,961-1016 and the existing example/generator/SinewaveLimitProvider.js. supportsLimits/getLimitEvaluator/getLimits signatures are correct, and the getLimits payload uses the range key (pc) inside the high objects consistent with Sinewave's use of sin/cos. No issue here — noted since the limit shape is easy to get wrong.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +67 to +86

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Pc limit lines render a bogus value if a non-pc range is plotted

ConjunctionLimitProvider.getLimits() (example/udlFederation/ConjunctionLimitProvider.js:67-86) returns limit definitions keyed only under pc (e.g. { color, pc }). The plot's MCTChartAlarmLineSet.getLimitPoints() (src/plugins/plot/chart/MCTChartAlarmLineSet.js:71-89) draws a limit line for any level whose high exists, reading the series' y-value via series.getYVal(limitForLevel.high). If a user plots the missDistance (or another) range of the conjunction object, high has no missDistance key, so getYVal yields undefined and a meaningless/degenerate limit line could be drawn. The evaluator (:49-63) already guards on valueMetadata.key !== 'pc', so table highlighting is unaffected; only stray plot limit lines for non-pc ranges are the concern. This mirrors the simplicity of SinewaveLimitProvider, so it is an edge case rather than a clear defect.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}
129 changes: 129 additions & 0 deletions example/udlFederation/UDLObjectProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/

import { SATELLITES } from './satellites.js';

const EPHEMERIS_VALUES = [
{
key: 'utc',
source: 'utc',
name: 'Timestamp',
format: 'utc',
hints: { domain: 1 }
},
{
key: 'altitude',
name: 'Altitude',
unit: 'km',
formatString: '%0.2f',
hints: { range: 1 }
},
{
key: 'latitude',
name: 'Latitude',
unit: 'deg',
formatString: '%0.4f',
hints: { range: 2 }
},
{
key: 'longitude',
name: 'Longitude',
unit: 'deg',
formatString: '%0.4f',
hints: { range: 3 }
},
{
key: 'velocity',
name: 'Velocity',
unit: 'km/s',
formatString: '%0.3f',
hints: { range: 4 }
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
];

const CONJUNCTION_VALUES = [
{
key: 'utc',
source: 'utc',
name: 'Timestamp',
format: 'utc',
hints: { domain: 1 }
},
{
key: 'pc',
name: 'Probability of Collision',
hints: { range: 1 }
},
{
key: 'missDistance',
name: 'Miss Distance',
unit: 'km',
formatString: '%0.2f',
hints: { range: 2 }
},
{
key: 'secondary',
name: 'Secondary Object',
format: 'string'
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
];

export default class UDLObjectProvider {
get(identifier) {
if (identifier.key === 'node') {
return Promise.resolve({
identifier,
name: 'UDL Federation Node',
type: 'folder',
location: 'ROOT'
});
}

if (identifier.key === 'conjunctions') {
return Promise.resolve({
identifier,
name: 'Conjunction Assessments',
type: 'udl.conjunctions',
location: 'udl:node',
telemetry: {
values: CONJUNCTION_VALUES
}
});
}

const satellite = SATELLITES.find((candidate) => candidate.key === identifier.key);
if (satellite === undefined) {
return Promise.reject(new Error(`Unknown UDL object: ${identifier.key}`));
}

return Promise.resolve({
identifier,
name: satellite.name,
type: 'udl.ephemeris',
location: 'udl:node',
noradId: satellite.noradId,
telemetry: {
values: EPHEMERIS_VALUES
}
});
}
}
136 changes: 136 additions & 0 deletions example/udlFederation/UDLTelemetryProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/

import { EARTH_RADIUS_KM, MU_EARTH, SATELLITES } from './satellites.js';

const EPHEMERIS_PERIOD_MS = 1000;
const CONJUNCTION_PERIOD_MS = 5000;
const MAX_REQUEST_DATUMS = 50000;
const SECONDARY_OBJECTS = [
'COSMOS 2251 DEB',
'FENGYUN 1C DEB',
'SL-16 R/B',
'STARLINK-30452',
'IRIDIUM 33 DEB'
];

function ephemerisDatum(satellite, timestamp) {
const radiusKm = EARTH_RADIUS_KM + satellite.altitudeKm;
const periodSeconds = 2 * Math.PI * Math.sqrt(Math.pow(radiusKm, 3) / MU_EARTH);
const meanMotion = (2 * Math.PI) / periodSeconds;
const theta = meanMotion * (timestamp / 1000) + (satellite.phaseDeg * Math.PI) / 180;
const inclination = (satellite.inclinationDeg * Math.PI) / 180;

const latitude = (Math.asin(Math.sin(inclination) * Math.sin(theta)) * 180) / Math.PI;
const earthRotationDeg = ((timestamp / 1000) * 360) / 86164;
const longitude =
(((((Math.atan2(Math.cos(inclination) * Math.sin(theta), Math.cos(theta)) * 180) / Math.PI +
satellite.raanDeg -
earthRotationDeg) %
360) +
540) %
360) -
180;

return {
id: satellite.key,
utc: timestamp,
altitude: satellite.altitudeKm + Math.sin(theta * 3) * 2,
latitude,
longitude,
velocity: Math.sqrt(MU_EARTH / radiusKm)
};
}

function conjunctionDatum(timestamp) {
const slot = Math.floor(timestamp / CONJUNCTION_PERIOD_MS);
const wave = Math.abs(Math.sin(slot / 7));
const pc = wave > 0.92 ? 0.0002 + wave / 2000 : wave / 50000;

return {
id: 'conjunctions',
utc: timestamp,
pc: Number(pc.toPrecision(3)),
missDistance: Number((0.4 + (1 - wave) * 24).toFixed(2)),
secondary: SECONDARY_OBJECTS[slot % SECONDARY_OBJECTS.length]
};
}

export default class UDLTelemetryProvider {
supportsRequest(domainObject) {
return this.#isUDLTelemetry(domainObject);
}

supportsSubscribe(domainObject) {
return this.#isUDLTelemetry(domainObject);
}

request(domainObject, options) {
const period =
domainObject.type === 'udl.conjunctions' ? CONJUNCTION_PERIOD_MS : EPHEMERIS_PERIOD_MS;
const size = Math.min(options.size ?? MAX_REQUEST_DATUMS, MAX_REQUEST_DATUMS);
const start = Math.floor(options.start / period) * period;
const requestedCount = Math.floor((options.end - start) / period) + 1;
const count = Math.min(requestedCount, size);
const data = [];

if (options.strategy === 'latest') {
const alignedEnd = Math.floor(options.end / period) * period;
for (let i = count - 1; i >= 0; i--) {
data.push(this.#datumFor(domainObject, alignedEnd - i * period));
}
} else {
const step = count > 1 ? (options.end - start) / (count - 1) : period;
for (let i = 0; i < count; i++) {
data.push(this.#datumFor(domainObject, start + Math.round(i * step)));
}
}
Comment on lines +101 to +106

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Historical request spaces datums evenly rather than at true cadence when capped

In UDLTelemetryProvider.request() the non-latest branch (example/udlFederation/UDLTelemetryProvider.js:101-106) computes step = (options.end - start) / (count - 1) where count = Math.min(requestedCount, size) (capped at MAX_REQUEST_DATUMS = 50000). When requestedCount exceeds the cap/size, datums are spread evenly across the window instead of at the true period cadence (1s ephemeris / 5s conjunction). For plots this is fine (effectively downsampling), but a telemetry table viewing large historical windows would show rows at non-period-aligned timestamps. Not a correctness bug for the example's purpose, just worth noting.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


return Promise.resolve(data);
}
Comment on lines +87 to +109

@devin-ai-integration devin-ai-integration Bot Jul 29, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Ephemeris view could generate up to 50000 full-resolution datums per request

When no size is provided, request() defaults to MAX_REQUEST_DATUMS = 50000 and generates that many datums, each running trig-heavy orbital math (ephemerisDatum). Plots pass size: 1000 so this ceiling is normally not hit there, but other consumers (e.g. tables/exports without a size) could trigger large synchronous loops. Acceptable for an example plugin, but flagging the potential main-thread cost since the generator plugin offloads this to a web worker.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed — request() now honors options.size and applies a 50k-datum hard cap, keeping the most recent window when the requested range exceeds the bound, so very large custom time ranges can no longer allocate unbounded datum arrays.


subscribe(domainObject, callback) {
const period =
domainObject.type === 'udl.conjunctions' ? CONJUNCTION_PERIOD_MS : EPHEMERIS_PERIOD_MS;
const interval = setInterval(() => {
callback(this.#datumFor(domainObject, Date.now()));
}, period);

return function unsubscribe() {
clearInterval(interval);
};
}
Comment on lines +111 to +121

@devin-ai-integration devin-ai-integration Bot Jul 29, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Realtime datum timestamps are unaligned versus aligned historical timestamps

request() aligns timestamps to period boundaries (Math.floor(options.start / period) * period, example/udlFederation/UDLTelemetryProvider.js:91), while subscribe() emits at Date.now() (:115), which is not period-aligned. At the historical→realtime handoff in a plot this produces a small timestamp discontinuity. Cosmetic only; values are deterministic functions of the timestamp so no data corruption occurs.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — as noted, conjunction datum content is slot-keyed and ephemeris is a continuous function of timestamp, so the unaligned live-edge timestamps stay consistent with historical values. Leaving as-is.


#datumFor(domainObject, timestamp) {
if (domainObject.type === 'udl.conjunctions') {
return conjunctionDatum(timestamp);
}

const satellite = SATELLITES.find((candidate) => candidate.key === domainObject.identifier.key);

return ephemerisDatum(satellite, timestamp);
}
Comment on lines +123 to +131

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Ephemeris datum lookup assumes the satellite key always exists

example/udlFederation/UDLTelemetryProvider.js:128-130 looks up the satellite via SATELLITES.find(... === domainObject.identifier.key) and immediately dereferences it in ephemerisDatum (satellite.altitudeKm). If any object of type udl.ephemeris existed whose key is not in SATELLITES, this would throw. In the current design the only udl.ephemeris objects are the three provider-defined satellites (types are not creatable), so this is unreachable today. Worth noting if the type is ever made user-creatable.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


#isUDLTelemetry(domainObject) {
return domainObject.type === 'udl.ephemeris' || domainObject.type === 'udl.conjunctions';
}
}
Loading
Loading