diff --git a/.webpack/webpack.prod.mjs b/.webpack/webpack.prod.mjs
index f0656b67e09..0b2e7921517 100644
--- a/.webpack/webpack.prod.mjs
+++ b/.webpack/webpack.prod.mjs
@@ -15,5 +15,5 @@ export default merge(common, {
__OPENMCT_ROOT_RELATIVE__: '""'
})
],
- devtool: 'source-map'
+ devtool: 'eval-source-map'
});
diff --git a/e2e/appActions.js b/e2e/appActions.js
index 105854b4cc7..6d94b1dc022 100644
--- a/e2e/appActions.js
+++ b/e2e/appActions.js
@@ -560,7 +560,7 @@ async function setFixedIndependentTimeConductorBounds(page, { start, end }) {
await page.getByLabel('Enable Independent Time Conductor').click();
// Bring up the time conductor popup
- await page.getByLabel('Independent Time Conductor Settings').click();
+ await page.getByLabel('Independent Time Conductor Panel').click();
await expect(page.getByLabel('Time Conductor Options')).toBeInViewport();
await _setTimeBounds(page, start, end);
diff --git a/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js
index 8369012c7c5..6018b3f9b32 100644
--- a/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js
+++ b/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js
@@ -104,8 +104,9 @@ test.describe('Snapshot Container tests', () => {
await page.locator('.ptro-crp-el').click();
await page.locator('.ptro-text-tool-input').fill('...is there life on mars?');
// When working with Painterro, we need to check that the Apply button is hidden after clicking
- await page.getByTitle('Apply').click();
- await expect(page.getByTitle('Apply')).toBeHidden();
+ const painterroApplyButton = page.locator('.ptro-text-tool-buttons').getByTitle('Apply');
+ await painterroApplyButton.click();
+ await expect(painterroApplyButton).toBeHidden();
// Save and exit annotation window
await page.getByRole('button', { name: 'Save' }).click();
@@ -130,8 +131,9 @@ test.describe('Snapshot Container tests', () => {
await page.locator('.ptro-crp-el').click();
await page.locator('.ptro-text-tool-input').fill('...is there life on mars?');
// When working with Painterro, we need to check that the Apply button is hidden after clicking
- await page.getByTitle('Apply').click();
- await expect(page.getByTitle('Apply')).toBeHidden();
+ const painterroApplyButton = page.locator('.ptro-text-tool-buttons').getByTitle('Apply');
+ await painterroApplyButton.click();
+ await expect(painterroApplyButton).toBeHidden();
// Save and exit annotation window
await page.getByRole('button', { name: 'Save' }).click();
diff --git a/src/api/forms/components/controls/LocatorField.vue b/src/api/forms/components/controls/LocatorField.vue
index 7dc159a6dc2..192b42e71ac 100644
--- a/src/api/forms/components/controls/LocatorField.vue
+++ b/src/api/forms/components/controls/LocatorField.vue
@@ -23,7 +23,7 @@
@@ -43,6 +43,11 @@ export default {
}
},
emits: ['on-change'],
+ computed: {
+ initialSelection() {
+ return this.model.parent || this.model.value?.[0];
+ }
+ },
methods: {
handleItemSelection(item) {
const data = {
diff --git a/src/api/time/IndependentTimeContext.js b/src/api/time/IndependentTimeContext.js
index 148bf52adf4..c0f1132223d 100644
--- a/src/api/time/IndependentTimeContext.js
+++ b/src/api/time/IndependentTimeContext.js
@@ -321,8 +321,16 @@ class IndependentTimeContext extends TimeContext {
return this.upstreamTimeContext.setMode(...arguments);
}
- if (mode === MODES.realtime && this.activeClock === undefined) {
- throw `Unknown clock. Has a clock been registered with 'addClock'?`;
+ if (mode === MODES.realtime) {
+ // TODO: This should probably happen up front in creating an independent time context
+ // TODO: not just in time every time setMode is called
+ if (this.activeClock === undefined) {
+ this.activeClock = this.globalTimeContext.getClock();
+ }
+
+ if (this.activeClock === undefined) {
+ throw `Unknown clock. Has a clock been registered with 'addClock'?`;
+ }
}
if (mode !== this.mode) {
diff --git a/src/api/time/TimeAPI.js b/src/api/time/TimeAPI.js
index 8b29a7c5ff8..cbec60ee0a4 100644
--- a/src/api/time/TimeAPI.js
+++ b/src/api/time/TimeAPI.js
@@ -135,33 +135,33 @@ class TimeAPI extends GlobalTimeContext {
/**
* Get or set an independent time context which follows the TimeAPI timeSystem,
- * but with different offsets for a given domain object
- * @param {string} key The identifier key of the domain object these offsets are set for
- * @param {ClockOffsets | TimeConductorBounds} value This maintains a sliding time window of a fixed width that automatically updates
- * @param {key | string} clockKey the real time clock key currently in use
+ * but with different bounds for a given domain object
+ * @param {string} keyString The keyString identifier of the domain object these offsets are set for
+ * @param {TimeConductorBounds | ClockOffsets} boundsOrOffsets either bounds if in fixed mode, or offsets if in realtime mode
+ * @param {string} clockKey the key for the real time clock to use
*/
- addIndependentContext(key, value, clockKey) {
- let timeContext = this.getIndependentContext(key);
+ addIndependentContext(keyString, boundsOrOffsets, clockKey) {
+ let timeContext = this.getIndependentContext(keyString);
//stop following upstream time context since the view has its own
timeContext.resetContext();
if (clockKey) {
timeContext.setClock(clockKey);
- timeContext.setMode(REALTIME_MODE_KEY, value);
+ timeContext.setMode(REALTIME_MODE_KEY, boundsOrOffsets);
} else {
- timeContext.setMode(FIXED_MODE_KEY, value);
+ timeContext.setMode(FIXED_MODE_KEY, boundsOrOffsets);
}
// Also emit the mode in case it's different from the previous time context
timeContext.emit(TIME_CONTEXT_EVENTS.modeChanged, structuredClone(timeContext.getMode()));
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
- this.emit('refreshContext', key);
+ this.emit('refreshContext', keyString);
return () => {
//follow any upstream time context
- this.emit('removeOwnContext', key);
+ this.emit('removeOwnContext', keyString);
};
}
diff --git a/src/api/time/TimeContext.js b/src/api/time/TimeContext.js
index 6bdd6ec2046..c7e8726d1eb 100644
--- a/src/api/time/TimeContext.js
+++ b/src/api/time/TimeContext.js
@@ -193,10 +193,10 @@ class TimeContext extends EventEmitter {
valid: false,
message: 'Start and end must be specified as integer values'
};
- } else if (bounds.start > bounds.end) {
+ } else if (bounds.start >= bounds.end) {
return {
valid: false,
- message: 'Specified start date exceeds end bound'
+ message: 'Start bound must be less than end bound'
};
}
@@ -261,7 +261,7 @@ class TimeContext extends EventEmitter {
} else if (offsets.start >= offsets.end) {
return {
valid: false,
- message: 'Specified start offset must be < end offset'
+ message: 'Start offset must be less than end offset'
};
}
diff --git a/src/plugins/charts/scatter/ScatterPlotView.vue b/src/plugins/charts/scatter/ScatterPlotView.vue
index 95b623ecac1..b3084c3ca62 100644
--- a/src/plugins/charts/scatter/ScatterPlotView.vue
+++ b/src/plugins/charts/scatter/ScatterPlotView.vue
@@ -57,8 +57,8 @@ export default {
const yAxisUnit = yAxisMetadata.units ? `(${yAxisMetadata.units})` : '';
return {
- xAxisTitle: `${xAxisMetadata.name || ''} ${xAxisUnit}`,
- yAxisTitle: `${yAxisMetadata.name || ''} ${yAxisUnit}`
+ xAxisTitle: `${xAxis || ''} ${xAxisUnit}`,
+ yAxisTitle: `${yAxis || ''} ${yAxisUnit}`
};
}
},
diff --git a/src/plugins/correlationTelemetryPlugin/plugin.js b/src/plugins/correlationTelemetryPlugin/plugin.js
new file mode 100644
index 00000000000..a1857fd1560
--- /dev/null
+++ b/src/plugins/correlationTelemetryPlugin/plugin.js
@@ -0,0 +1,228 @@
+const CORRELATOR_TYPE = 'telemetry.correlator';
+
+export default function CorrelationTelemetryPlugin(openmct) {
+ // eslint-disable-next-line no-shadow
+ return function install(openmct) {
+ function getTelemetryObject(idString) {
+ return openmct.objects.get(idString);
+ }
+
+ function getTelemetry(object, options) {
+ return openmct.telemetry.request(object, options);
+ }
+
+ openmct.types.addType(CORRELATOR_TYPE, {
+ name: 'Correlation Telemetry',
+ description: `Combines telemetry from multiple sources to produce telemetry correlated by timestamp with a given time tolerance.`,
+ cssClass: 'icon-object',
+ creatable: true,
+ initialize: function (obj) {
+ obj.telemetry = {};
+ },
+ form: [
+ {
+ key: 'xSource',
+ name: 'X Axis Source',
+ control: 'locator',
+ required: true,
+ cssClass: 'grows'
+ },
+ {
+ key: 'ySource',
+ name: 'Y Axis Source',
+ control: 'locator',
+ required: true,
+ cssClass: 'grows'
+ }
+ ]
+ });
+
+ openmct.telemetry.addProvider({
+ supportsMetadata: function (domainObject) {
+ return domainObject.type === CORRELATOR_TYPE;
+ },
+ getMetadata: function (domainObject) {
+ let metadata = {};
+ metadata.values = openmct.time.getAllTimeSystems().map(function (timeSystem, i) {
+ return {
+ name: timeSystem.name,
+ key: timeSystem.key,
+ source: timeSystem.source,
+ format: timeSystem.timeFormat,
+ hints: { domain: i }
+ };
+ });
+ metadata.values.push({
+ name: 'X',
+ key: 'x',
+ source: 'x',
+ hints: { xSource: 1, range: 1 }
+ });
+ metadata.values.push({
+ name: 'Y',
+ key: 'y',
+ source: 'y',
+ hints: { ySource: 1, range: 2 }
+ });
+ return metadata;
+ },
+ supportsRequest: function (domainObject) {
+ return domainObject.type === CORRELATOR_TYPE;
+ },
+ request: function (domainObject, options) {
+ let telemResults = {};
+ let telemObject;
+
+ const xSourceIdentifier = openmct.objects.makeKeyString(domainObject.xSource[0].identifier);
+ let xPromise = getTelemetryObject(xSourceIdentifier)
+ .then((object) => {
+ telemObject = object;
+ return getTelemetry(object, options);
+ })
+ .then((data) => {
+ let source = 'x';
+ telemResults[source] = {
+ object: telemObject
+ };
+ let metadata = openmct.telemetry.getMetadata(telemObject);
+ let valueMeta = metadata.valuesForHints(['range'])[0];
+ telemResults[source].coorelatorFormat = openmct.telemetry.getValueFormatter(valueMeta);
+ telemResults[source].coorelatorFormat = openmct.telemetry.getValueFormatter(valueMeta);
+ telemResults[source].timestampFormat = openmct.telemetry.getValueFormatter(
+ metadata.value(options.domain)
+ );
+ telemResults[source].data = data;
+ });
+
+ const ySourceIdentifier = openmct.objects.makeKeyString(domainObject.ySource[0].identifier);
+ let yPromise = getTelemetryObject(ySourceIdentifier)
+ .then((object) => {
+ telemObject = object;
+ return getTelemetry(object, options);
+ })
+ .then((data) => {
+ let source = 'y';
+ telemResults[source] = {
+ object: telemObject
+ };
+ let metadata = openmct.telemetry.getMetadata(telemObject);
+ let valueMeta = metadata.valuesForHints(['range'])[0];
+ telemResults[source].coorelatorFormat = openmct.telemetry.getValueFormatter(valueMeta);
+ telemResults[source].coorelatorFormat = openmct.telemetry.getValueFormatter(valueMeta);
+ telemResults[source].timestampFormat = openmct.telemetry.getValueFormatter(
+ metadata.value(options.domain)
+ );
+ telemResults[source].data = data;
+ });
+
+ return Promise.all([xPromise, yPromise]).then(function () {
+ let results = [];
+ let xByTime = telemResults.x.data.reduce(function (m, datum) {
+ m[telemResults.x.timestampFormat.parse(datum)] =
+ telemResults.x.coorelatorFormat.parse(datum);
+ return m;
+ }, {});
+ telemResults.y.data.forEach(function (datum) {
+ let timestamp = telemResults.y.timestampFormat.parse(datum);
+ if (xByTime[timestamp] !== undefined) {
+ let resultDatum = {
+ x: xByTime[timestamp],
+ y: telemResults.y.coorelatorFormat.parse(datum)
+ };
+ resultDatum[options.domain] = timestamp;
+ results.push(resultDatum);
+ }
+ });
+ return results;
+ });
+ },
+ supportsSubscribe: function (domainObject) {
+ return domainObject.type === CORRELATOR_TYPE;
+ },
+ subscribe: function (domainObject, callback) {
+ let telem = {};
+ let done = false;
+ let unsubscribes = [];
+
+ function sendUpdate() {
+ if (done) {
+ return;
+ }
+ if (!telem.y.latest || !telem.x.latest) {
+ return;
+ }
+ if (telem.y.latestTimestamp !== telem.x.latestTimestamp) {
+ return;
+ }
+ let datum = {
+ x: telem.x.coorelatorFormat.parse(telem.x.latest),
+ y: telem.y.coorelatorFormat.parse(telem.y.latest)
+ };
+ datum[openmct.time.timeSystem().key] = Math.max(
+ telem.x.latestTimestamp,
+ telem.y.latestTimestamp
+ );
+ delete telem.x.latest;
+ delete telem.y.latest;
+ delete telem.x.latestTimestamp;
+ delete telem.y.latestTimestamp;
+ callback(datum);
+ }
+
+ const xSourceIdentifier = openmct.objects.makeKeyString(domainObject.xSource[0].identifier);
+ getTelemetryObject(xSourceIdentifier).then(function (xObject) {
+ if (done) {
+ return;
+ }
+ telem.x = {
+ object: xObject
+ };
+ let metadata = openmct.telemetry.getMetadata(xObject);
+ let valueMeta = metadata.valuesForHints(['range'])[0];
+ telem.x.coorelatorFormat = openmct.telemetry.getValueFormatter(valueMeta);
+ telem.x.timestampFormat = openmct.telemetry.getValueFormatter(
+ metadata.value(openmct.time.timeSystem().key)
+ );
+ unsubscribes.push(
+ openmct.telemetry.subscribe(xObject, function (datum) {
+ telem.x.latest = datum;
+ telem.x.latestTimestamp = telem.x.timestampFormat.parse(datum);
+ requestAnimationFrame(sendUpdate);
+ })
+ );
+ });
+
+ const ySourceIdentifier = openmct.objects.makeKeyString(domainObject.ySource[0].identifier);
+ getTelemetryObject(ySourceIdentifier).then(function (yObject) {
+ if (done) {
+ return;
+ }
+ telem.y = {
+ object: yObject
+ };
+ let metadata = openmct.telemetry.getMetadata(yObject);
+ let valueMeta = metadata.valuesForHints(['range'])[0];
+ telem.y.coorelatorFormat = openmct.telemetry.getValueFormatter(valueMeta);
+ telem.y.timestampFormat = openmct.telemetry.getValueFormatter(
+ metadata.value(openmct.time.timeSystem().key)
+ );
+ unsubscribes.push(
+ openmct.telemetry.subscribe(yObject, function (datum) {
+ telem.y.latest = datum;
+ telem.y.latestTimestamp = telem.y.timestampFormat.parse(datum);
+ requestAnimationFrame(sendUpdate);
+ })
+ );
+ });
+
+ return function unsubscribe() {
+ done = true;
+ unsubscribes.forEach(function (u) {
+ u();
+ });
+ unsubscribes = undefined;
+ };
+ }
+ });
+ };
+}
diff --git a/src/plugins/plot/axis/XAxis.vue b/src/plugins/plot/axis/XAxis.vue
index ceb58503312..d35bedb624b 100644
--- a/src/plugins/plot/axis/XAxis.vue
+++ b/src/plugins/plot/axis/XAxis.vue
@@ -132,7 +132,7 @@ export default {
};
});
}
-
+ console.log('xKeyOptions', this.xKeyOptions, 'this.xAxis', this.xAxis);
this.xAxisLabel = this.xAxis.get('label');
this.selectedXKeyOptionKey =
this.xKeyOptions.length > 0 ? this.getXKeyOption(xAxisKey).key : xAxisKey;
diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js
index 76b9e590d3d..470b90d97a6 100644
--- a/src/plugins/plugins.js
+++ b/src/plugins/plugins.js
@@ -34,6 +34,7 @@ import ClearData from './clearData/plugin.js';
import Clock from './clock/plugin.js';
import ConditionPlugin from './condition/plugin.js';
import ConditionWidgetPlugin from './conditionWidget/plugin.js';
+import CorrelationTelemetryPlugin from './correlationTelemetryPlugin/plugin.js';
import CouchDBSearchFolder from './CouchDBSearchFolder/plugin.js';
import DefaultRootName from './defaultRootName/plugin.js';
import DeviceClassifier from './DeviceClassifier/plugin.js';
@@ -177,6 +178,7 @@ plugins.Gauge = GaugePlugin;
plugins.Timelist = TimeList;
plugins.InspectorViews = InspectorViews;
plugins.InspectorDataVisualization = InspectorDataVisualization;
+plugins.CorrelationTelemetry = CorrelationTelemetryPlugin;
plugins.EventTimestripPlugin = EventTimestripPlugin;
export default plugins;
diff --git a/src/plugins/timeConductor/ConductorAxis.vue b/src/plugins/timeConductor/ConductorAxis.vue
index bc824d08a1b..688973799af 100644
--- a/src/plugins/timeConductor/ConductorAxis.vue
+++ b/src/plugins/timeConductor/ConductorAxis.vue
@@ -41,21 +41,16 @@ import { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';
import utcMultiTimeFormat from './utcMultiTimeFormat.js';
const PADDING = 1;
-const DEFAULT_DURATION_FORMATTER = 'duration';
const PIXELS_PER_TICK = 100;
const PIXELS_PER_TICK_WIDE = 200;
export default {
- inject: ['openmct'],
+ inject: ['openmct', 'isFixedTimeMode'],
props: {
viewBounds: {
type: Object,
required: true
},
- isFixed: {
- type: Boolean,
- required: true
- },
altPressed: {
type: Boolean,
required: true
@@ -198,22 +193,8 @@ export default {
this.axisElement.call(this.xAxis);
this.setScale();
},
- getActiveFormatter() {
- let timeSystem = this.openmct.time.getTimeSystem();
-
- if (this.isFixed) {
- return this.getFormatter(timeSystem.timeFormat);
- } else {
- return this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
- }
- },
- getFormatter(key) {
- return this.openmct.telemetry.getValueFormatter({
- format: key
- }).formatter;
- },
dragStart($event) {
- if (this.isFixed) {
+ if (this.isFixedTimeMode) {
this.dragStartX = $event.clientX;
if (this.altPressed) {
diff --git a/src/plugins/timeConductor/ConductorClock.vue b/src/plugins/timeConductor/ConductorClock.vue
index 23f29d48951..f4321ff1337 100644
--- a/src/plugins/timeConductor/ConductorClock.vue
+++ b/src/plugins/timeConductor/ConductorClock.vue
@@ -23,8 +23,8 @@