Merge remote-tracking branch 'khalidadil/extend-conditional' into combined-rodap-stuff

This commit is contained in:
Scott Bell 2024-09-24 21:40:00 +02:00
commit f78f76818b
17 changed files with 866 additions and 137 deletions

View File

@ -51,7 +51,8 @@ const config = {
compsMathWorker: './src/plugins/comps/CompsMathWorker.js', compsMathWorker: './src/plugins/comps/CompsMathWorker.js',
espressoTheme: './src/plugins/themes/espresso-theme.scss', espressoTheme: './src/plugins/themes/espresso-theme.scss',
snowTheme: './src/plugins/themes/snow-theme.scss', snowTheme: './src/plugins/themes/snow-theme.scss',
darkmatterTheme: './src/plugins/themes/darkmatter-theme.scss' darkmatterTheme: './src/plugins/themes/darkmatter-theme.scss',
historicalTelemetryWorker: './src/plugins/condition/historicalTelemetryWorker.js',
}, },
output: { output: {
globalObject: 'this', globalObject: 'this',

View File

@ -66,6 +66,10 @@ module.exports = async (config) => {
{ {
pattern: 'dist/generatorWorker.js*', pattern: 'dist/generatorWorker.js*',
included: false included: false
},
{
pattern: 'dist/historicalTelemetryWorker.js*',
included: false
} }
], ],
port: 9876, port: 9876,

View File

@ -0,0 +1,53 @@
// src/plugins/condition/ConditionInspectorView.js
import mount from 'utils/mount';
import ConditionConfigView from './components/ConditionInspectorConfigView.vue';
export default function ConditionInspectorView(openmct) {
return {
key: 'condition-config',
name: 'Config',
canView: function (selection) {
return selection.length > 0 && selection[0][0].context.item.type === 'conditionSet';
},
view: function (selection) {
let _destroy = null;
const domainObject = selection[0][0].context.item;
return {
show: function (element) {
const { destroy } = mount(
{
el: element,
components: {
ConditionConfigView: ConditionConfigView
},
provide: {
openmct,
domainObject
},
template: '<condition-config-view></condition-config-view>'
},
{
app: openmct.app,
element
}
);
_destroy = destroy;
},
showTab: function (isEditing) {
return isEditing;
},
priority: function () {
return 1;
},
destroy: function () {
if (_destroy) {
_destroy();
}
}
};
}
};
}

View File

@ -24,6 +24,7 @@ import { EventEmitter } from 'eventemitter3';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import Condition from './Condition.js'; import Condition from './Condition.js';
import HistoricalTelemetryProvider from './historicalTelemetryProvider.js';
import { getLatestTimestamp } from './utils/time.js'; import { getLatestTimestamp } from './utils/time.js';
export default class ConditionManager extends EventEmitter { export default class ConditionManager extends EventEmitter {
@ -39,64 +40,57 @@ export default class ConditionManager extends EventEmitter {
this.shouldEvaluateNewTelemetry = this.shouldEvaluateNewTelemetry.bind(this); this.shouldEvaluateNewTelemetry = this.shouldEvaluateNewTelemetry.bind(this);
this.compositionLoad = this.composition.load(); this.compositionLoad = this.composition.load();
this.subscriptions = {}; this.telemetryCollections = {};
this.telemetryObjects = {}; this.telemetryObjects = {};
this.testData = { this.testData = {
conditionTestInputs: this.conditionSetDomainObject.configuration.conditionTestData, conditionTestInputs: this.conditionSetDomainObject.configuration.conditionTestData,
applied: false applied: false
}; };
this.initialize(); this.initialize();
this.telemetryBuffer = [];
this.isProcessing = false;
} }
async requestLatestValue(endpoint) { subscribeToTelemetry(telemetryObject) {
const options = { const keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (this.telemetryCollections[keyString]) {
return;
}
const requestOptions = {
size: 1, size: 1,
strategy: 'latest' strategy: 'latest'
}; };
const latestData = await this.openmct.telemetry.request(endpoint, options);
if (!latestData) { this.telemetryCollections[keyString] = this.openmct.telemetry.requestCollection(
throw new Error('Telemetry request failed by returning a falsy response'); telemetryObject,
} requestOptions
if (latestData.length === 0) {
return;
}
this.telemetryReceived(endpoint, latestData[0]);
}
subscribeToTelemetry(endpoint) {
const telemetryKeyString = this.openmct.objects.makeKeyString(endpoint.identifier);
if (this.subscriptions[telemetryKeyString]) {
return;
}
const metadata = this.openmct.telemetry.getMetadata(endpoint);
this.telemetryObjects[telemetryKeyString] = Object.assign({}, endpoint, {
telemetryMetaData: metadata ? metadata.valueMetadatas : []
});
// get latest telemetry value (in case subscription is cached and no new data is coming in)
this.requestLatestValue(endpoint);
this.subscriptions[telemetryKeyString] = this.openmct.telemetry.subscribe(
endpoint,
this.telemetryReceived.bind(this, endpoint)
); );
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
const telemetryMetaData = metadata ? metadata.valueMetadatas : [];
this.telemetryObjects[keyString] = { ...telemetryObject, telemetryMetaData };
this.telemetryCollections[keyString].on(
'add',
this.telemetryReceived.bind(this, telemetryObject)
);
this.telemetryCollections[keyString].load();
this.updateConditionTelemetryObjects(); this.updateConditionTelemetryObjects();
} }
unsubscribeFromTelemetry(endpointIdentifier) { unsubscribeFromTelemetry(endpointIdentifier) {
const id = this.openmct.objects.makeKeyString(endpointIdentifier); const keyString = this.openmct.objects.makeKeyString(endpointIdentifier);
if (!this.subscriptions[id]) { if (!this.telemetryCollections[keyString]) {
console.log('no subscription to remove');
return; return;
} }
this.subscriptions[id](); this.telemetryCollections[keyString].destroy();
delete this.subscriptions[id]; this.telemetryCollections[keyString] = null;
delete this.telemetryObjects[id]; this.telemetryObjects[keyString] = null;
this.removeConditionTelemetryObjects(); this.removeConditionTelemetryObjects();
//force re-computation of condition set result as we might be in a state where //force re-computation of condition set result as we might be in a state where
@ -107,7 +101,7 @@ export default class ConditionManager extends EventEmitter {
this.timeSystems, this.timeSystems,
this.openmct.time.getTimeSystem() this.openmct.time.getTimeSystem()
); );
this.updateConditionResults({ id: id }); this.updateConditionResults({ id: keyString });
this.updateCurrentCondition(latestTimestamp); this.updateCurrentCondition(latestTimestamp);
if (Object.keys(this.telemetryObjects).length === 0) { if (Object.keys(this.telemetryObjects).length === 0) {
@ -329,6 +323,19 @@ export default class ConditionManager extends EventEmitter {
return currentCondition; return currentCondition;
} }
getHistoricalData() {
if (!this.conditionSetDomainObject.configuration.shouldFetchHistorical) {
return [];
}
const historicalTelemetry = new HistoricalTelemetryProvider(
this.openmct,
this.telemetryObjects,
this.conditions,
this.conditionSetDomainObject
);
return historicalTelemetry.getHistoricalData();
}
getCurrentConditionLAD(conditionResults) { getCurrentConditionLAD(conditionResults) {
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection; const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
let currentCondition = conditionCollection[conditionCollection.length - 1]; let currentCondition = conditionCollection[conditionCollection.length - 1];
@ -384,8 +391,26 @@ export default class ConditionManager extends EventEmitter {
} }
const currentCondition = this.getCurrentConditionLAD(conditionResults); const currentCondition = this.getCurrentConditionLAD(conditionResults);
let output = currentCondition?.configuration?.output;
if (output === 'telemetry value') {
const { outputTelemetry, outputMetadata } = currentCondition.configuration;
const outputTelemetryObject = await this.openmct.objects.get(outputTelemetry);
const telemetryOptions = {
size: 1,
strategy: 'latest',
timeContext: this.openmct.time.getContextForView([])
};
const latestData = await this.openmct.telemetry.request(
outputTelemetryObject,
telemetryOptions
);
if (latestData?.[0]?.[outputMetadata]) {
output = latestData?.[0]?.[outputMetadata];
}
}
const currentOutput = { const currentOutput = {
output: currentCondition.configuration.output, output: output,
id: this.conditionSetDomainObject.identifier, id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id, conditionId: currentCondition.id,
...latestTimestamp ...latestTimestamp
@ -403,6 +428,18 @@ export default class ConditionManager extends EventEmitter {
} }
} }
const conditionTelemetries = [];
const conditions = this.conditionSetDomainObject.configuration.conditionCollection;
conditions.forEach((condition) => {
if (condition?.configuration?.outputTelemetry) {
conditionTelemetries.push(condition?.configuration?.outputTelemetry);
}
});
if (conditionTelemetries.includes(id)) {
return true;
}
return false; return false;
} }
@ -410,11 +447,13 @@ export default class ConditionManager extends EventEmitter {
return this.openmct.time.getBounds().end >= currentTimestamp; return this.openmct.time.getBounds().end >= currentTimestamp;
} }
telemetryReceived(endpoint, datum) { telemetryReceived(endpoint, data) {
if (!this.isTelemetryUsed(endpoint)) { if (!this.isTelemetryUsed(endpoint)) {
return; return;
} }
const datum = data[0];
const normalizedDatum = this.createNormalizedDatum(datum, endpoint); const normalizedDatum = this.createNormalizedDatum(datum, endpoint);
const timeSystemKey = this.openmct.time.getTimeSystem().key; const timeSystemKey = this.openmct.time.getTimeSystem().key;
let timestamp = {}; let timestamp = {};
@ -422,7 +461,7 @@ export default class ConditionManager extends EventEmitter {
timestamp[timeSystemKey] = currentTimestamp; timestamp[timeSystemKey] = currentTimestamp;
if (this.shouldEvaluateNewTelemetry(currentTimestamp)) { if (this.shouldEvaluateNewTelemetry(currentTimestamp)) {
this.updateConditionResults(normalizedDatum); this.updateConditionResults(normalizedDatum);
this.updateCurrentCondition(timestamp); this.updateCurrentCondition(timestamp, endpoint, datum);
} }
} }
@ -435,14 +474,12 @@ export default class ConditionManager extends EventEmitter {
}); });
} }
updateCurrentCondition(timestamp) { emitConditionSetResult(currentCondition, timestamp, outputValue) {
const currentCondition = this.getCurrentCondition();
this.emit( this.emit(
'conditionSetResultUpdated', 'conditionSetResultUpdated',
Object.assign( Object.assign(
{ {
output: currentCondition.configuration.output, output: outputValue,
id: this.conditionSetDomainObject.identifier, id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id conditionId: currentCondition.id
}, },
@ -451,6 +488,60 @@ export default class ConditionManager extends EventEmitter {
); );
} }
updateCurrentCondition(timestamp, telemetryObject, telemetryData) {
this.telemetryBuffer.push({ timestamp, telemetryObject, telemetryData });
if (!this.isProcessing) {
this.processBuffer();
}
}
async processBuffer() {
this.isProcessing = true;
while (this.telemetryBuffer.length > 0) {
const { timestamp, telemetryObject, telemetryData } = this.telemetryBuffer.shift();
await this.processCondition(timestamp, telemetryObject, telemetryData);
}
this.isProcessing = false;
}
async processCondition(timestamp, telemetryObject, telemetryData) {
const currentCondition = this.getCurrentCondition();
let telemetryValue = currentCondition.configuration.output;
if (currentCondition?.configuration?.outputTelemetry) {
const selectedOutputIdentifier = currentCondition?.configuration?.outputTelemetry;
const outputMetadata = currentCondition?.configuration?.outputMetadata;
const telemetryKeystring = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (selectedOutputIdentifier === telemetryKeystring) {
telemetryValue = telemetryData[outputMetadata];
} else {
const outputTelemetryObject = await this.openmct.objects.get(selectedOutputIdentifier);
const telemetryOptions = {
size: 1,
strategy: 'latest',
start: timestamp?.utc - 1000,
end: timestamp?.utc + 1000
};
const outputTelemetryData = await this.openmct.telemetry.request(
outputTelemetryObject,
telemetryOptions
);
const outputTelemetryValue =
outputTelemetryData?.length > 0 ? outputTelemetryData.slice(-1)[0] : null;
if (outputTelemetryData.length && outputTelemetryValue?.[outputMetadata]) {
telemetryValue = outputTelemetryValue?.[outputMetadata];
} else {
telemetryValue = undefined;
}
}
}
this.emitConditionSetResult(currentCondition, timestamp, telemetryValue);
}
getTestData(metadatum) { getTestData(metadatum) {
let data = undefined; let data = undefined;
if (this.testData.applied) { if (this.testData.applied) {
@ -507,8 +598,9 @@ export default class ConditionManager extends EventEmitter {
destroy() { destroy() {
this.composition.off('add', this.subscribeToTelemetry, this); this.composition.off('add', this.subscribeToTelemetry, this);
this.composition.off('remove', this.unsubscribeFromTelemetry, this); this.composition.off('remove', this.unsubscribeFromTelemetry, this);
Object.values(this.subscriptions).forEach((unsubscribe) => unsubscribe()); Object.values(this.telemetryCollections).forEach((telemetryCollection) =>
delete this.subscriptions; telemetryCollection.destroy()
);
this.conditions.forEach((condition) => { this.conditions.forEach((condition) => {
condition.destroy(); condition.destroy();

View File

@ -42,8 +42,9 @@ export default class ConditionSetTelemetryProvider {
async request(domainObject, options) { async request(domainObject, options) {
let conditionManager = this.getConditionManager(domainObject); let conditionManager = this.getConditionManager(domainObject);
const formattedHistoricalData = await conditionManager.getHistoricalData();
let latestOutput = await conditionManager.requestLADConditionSetOutput(options); let latestOutput = await conditionManager.requestLADConditionSetOutput(options);
return latestOutput; return [...formattedHistoricalData, ...latestOutput];
} }
subscribe(domainObject, callback) { subscribe(domainObject, callback) {

View File

@ -235,10 +235,11 @@ export default {
return arr; return arr;
}, },
addTelemetryObject(domainObject) { async addTelemetryObject(domainObject) {
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier); const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
const telemetryPath = await this.getFullTelemetryPath(domainObject);
this.telemetryObjs.push(domainObject); this.telemetryObjs.push({ ...domainObject, path: telemetryPath });
this.$emit('telemetry-updated', this.telemetryObjs); this.$emit('telemetry-updated', this.telemetryObjs);
this.subscribeToStaleness(domainObject, (stalenessResponse) => { this.subscribeToStaleness(domainObject, (stalenessResponse) => {
@ -248,6 +249,19 @@ export default {
}); });
}); });
}, },
async getFullTelemetryPath(telemetry) {
const keyString = this.openmct.objects.makeKeyString(telemetry.identifier);
const originalPathObjects = await this.openmct.objects.getOriginalPath(keyString, []);
const telemetryPath = originalPathObjects.reverse().map((pathObject) => {
if (pathObject.type !== 'root') {
return pathObject.name;
}
return undefined;
});
return telemetryPath.join('/');
},
removeTelemetryObject(identifier) { removeTelemetryObject(identifier) {
const keyString = this.openmct.objects.makeKeyString(identifier); const keyString = this.openmct.objects.makeKeyString(identifier);
const index = this.telemetryObjs.findIndex((obj) => { const index = this.telemetryObjs.findIndex((obj) => {

View File

@ -0,0 +1,62 @@
<template>
<div class="c-inspect-properties">
<h2>Configuration</h2>
<section>
<div class="c-form-row">
<label for="historical-toggle">Enable Historical: </label>
<ToggleSwitch
id="historical-toggle"
class="c-toggle-switch"
:checked="historicalEnabled"
name="condition-historical-toggle"
@change="onToggleChange"
/>
</div>
</section>
</div>
</template>
<script>
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
export default {
components: {
ToggleSwitch
},
inject: ['openmct', 'domainObject'],
data() {
return {
historicalEnabled: false
};
},
mounted() {
this.historicalEnabled = this.domainObject.configuration.shouldFetchHistorical;
},
methods: {
onToggleChange() {
this.historicalEnabled = !this.historicalEnabled;
this.openmct.objects.mutate(
this.domainObject,
'configuration.shouldFetchHistorical',
this.historicalEnabled
);
}
}
};
</script>
<style scoped>
.c-inspect-properties {
padding: 10px;
}
.c-form-row {
display: flex;
align-items: center;
margin-bottom: 10px;
}
label {
margin-right: 10px;
}
</style>

View File

@ -99,13 +99,13 @@
@change="setOutputValue" @change="setOutputValue"
> >
<option v-for="option in outputOptions" :key="option" :value="option"> <option v-for="option in outputOptions" :key="option" :value="option">
{{ initCap(option) }} {{ option }}
</option> </option>
</select> </select>
</span> </span>
<span class="c-cdef__control"> <span class="c-cdef__control">
<input <input
v-if="selectedOutputSelection === outputOptions[2]" v-if="selectedOutputSelection === outputOptions[3]"
v-model="condition.configuration.output" v-model="condition.configuration.output"
aria-label="Condition Output String" aria-label="Condition Output String"
class="t-condition-name-input" class="t-condition-name-input"
@ -113,8 +113,41 @@
@change="persist" @change="persist"
/> />
</span> </span>
<span v-if="selectedOutputSelection === 'telemetry value'" class="c-cdef__control">
<select
v-model="condition.configuration.outputTelemetry"
aria-label="Output Telemetry Selection"
@change="persist"
>
<option value="">- Select Telemetry -</option>
<option
v-for="telemetryOption in telemetry"
:key="openmct.objects.makeKeyString(telemetryOption.identifier)"
:value="openmct.objects.makeKeyString(telemetryOption.identifier)"
>
{{ telemetryOption.path }}
</option>
</select>
</span>
<span v-if="condition.configuration.outputTelemetry" class="c-cdef__control">
<select
v-model="condition.configuration.outputMetadata"
aria-label="Output Telemetry Metadata Selection"
@change="persist"
>
<option value="">- Select Field -</option>
<option
v-for="(option, index) in telemetryMetadataOptions[
condition.configuration.outputTelemetry
]"
:key="index"
:value="option.key"
>
{{ option.name }}
</option>
</select>
</span>
</span> </span>
<div v-if="!condition.isDefault" class="c-cdef__match-and-criteria"> <div v-if="!condition.isDefault" class="c-cdef__match-and-criteria">
<span class="c-cdef__separator c-row-separator"></span> <span class="c-cdef__separator c-row-separator"></span>
<span class="c-cdef__label">Match</span> <span class="c-cdef__label">Match</span>
@ -181,7 +214,12 @@
<span class="c-condition__name"> <span class="c-condition__name">
{{ condition.configuration.name }} {{ condition.configuration.name }}
</span> </span>
<span class="c-condition__output"> Output: {{ condition.configuration.output }} </span> <span class="c-condition__output">
Output:
{{
condition.configuration.output === undefined ? 'none' : condition.configuration.output
}}
</span>
</div> </div>
<div class="c-condition__summary"> <div class="c-condition__summary">
<ConditionDescription :show-label="false" :condition="condition" /> <ConditionDescription :show-label="false" :condition="condition" />
@ -250,10 +288,11 @@ export default {
expanded: true, expanded: true,
trigger: 'all', trigger: 'all',
selectedOutputSelection: '', selectedOutputSelection: '',
outputOptions: ['false', 'true', 'string'], outputOptions: ['none', 'false', 'true', 'string', 'telemetry value'],
criterionIndex: 0, criterionIndex: 0,
draggingOver: false, draggingOver: false,
isDefault: this.condition.isDefault isDefault: this.condition.isDefault,
telemetryMetadataOptions: {}
}; };
}, },
computed: { computed: {
@ -287,26 +326,51 @@ export default {
return false; return false;
} }
}, },
watch: {
condition: {
handler() {
if (this.condition.configuration.output !== 'telemetry value') {
this.condition.configuration.outputTelemetry = null;
this.condition.configuration.outputMetadata = null;
}
},
deep: true
},
isEditing(newValue, oldValue) {
if (newValue === true) {
this.initializeMetadata();
}
}
},
unmounted() { unmounted() {
this.destroy(); this.destroy();
}, },
mounted() { mounted() {
this.setOutputSelection(); this.setOutputSelection();
this.initializeMetadata();
}, },
methods: { methods: {
setOutputSelection() { setOutputSelection() {
let conditionOutput = this.condition.configuration.output; let conditionOutput = this.condition.configuration.output;
if (conditionOutput) { if (conditionOutput) {
if (conditionOutput !== 'false' && conditionOutput !== 'true') { if (
conditionOutput !== 'false' &&
conditionOutput !== 'true' &&
conditionOutput !== 'telemetry value'
) {
this.selectedOutputSelection = 'string'; this.selectedOutputSelection = 'string';
} else { } else {
this.selectedOutputSelection = conditionOutput; this.selectedOutputSelection = conditionOutput;
} }
} else if (conditionOutput === undefined) {
this.selectedOutputSelection = 'none';
} }
}, },
setOutputValue() { setOutputValue() {
if (this.selectedOutputSelection === 'string') { if (this.selectedOutputSelection === 'string') {
this.condition.configuration.output = ''; this.condition.configuration.output = '';
} else if (this.selectedOutputSelection === 'none') {
this.condition.configuration.output = undefined;
} else { } else {
this.condition.configuration.output = this.selectedOutputSelection; this.condition.configuration.output = this.selectedOutputSelection;
} }
@ -401,6 +465,24 @@ export default {
}, },
initCap(str) { initCap(str) {
return str.charAt(0).toUpperCase() + str.slice(1); return str.charAt(0).toUpperCase() + str.slice(1);
},
initializeMetadata() {
this.telemetry.forEach((telemetryObject) => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
if (telemetryMetadata) {
this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice();
} else {
this.telemetryMetadataOptions[id] = [];
}
});
},
getId(identifier) {
if (identifier) {
return this.openmct.objects.makeKeyString(identifier);
}
return [];
} }
} }
}; };

View File

@ -40,7 +40,7 @@
:key="telemetryOption.identifier.key" :key="telemetryOption.identifier.key"
:value="telemetryOption.identifier" :value="telemetryOption.identifier"
> >
{{ telemetryOption.name }} {{ telemetryOption.path }}
</option> </option>
</select> </select>
</span> </span>

View File

@ -63,7 +63,7 @@
:key="index" :key="index"
:value="telemetryOption.identifier" :value="telemetryOption.identifier"
> >
{{ telemetryOption.name }} {{ telemetryPaths[index] || telemetryOption.name }}
</option> </option>
</select> </select>
</span> </span>
@ -147,7 +147,8 @@ export default {
expanded: true, expanded: true,
isApplied: false, isApplied: false,
testInputs: [], testInputs: [],
telemetryMetadataOptions: {} telemetryMetadataOptions: {},
telemetryPaths: []
}; };
}, },
watch: { watch: {
@ -200,6 +201,10 @@ export default {
this.telemetryMetadataOptions[id] = []; this.telemetryMetadataOptions[id] = [];
} }
}); });
this.telemetry.forEach(async (telemetryOption, index) => {
const telemetryPath = await this.getFullTelemetryPath(telemetryOption);
this.telemetryPaths[index] = telemetryPath;
});
}, },
addTestInput(testInput) { addTestInput(testInput) {
this.testInputs.push( this.testInputs.push(
@ -244,6 +249,22 @@ export default {
applied: this.isApplied, applied: this.isApplied,
conditionTestInputs: this.testInputs conditionTestInputs: this.testInputs
}); });
},
async getFullTelemetryPath(telemetry) {
const keyStringForObject = this.openmct.objects.makeKeyString(telemetry.identifier);
const originalPathObjects = await this.openmct.objects.getOriginalPath(
keyStringForObject,
[]
);
const telemetryPath = originalPathObjects.reverse().map((pathObject) => {
if (pathObject.type !== 'root') {
return pathObject.name;
}
return undefined;
});
return telemetryPath.join('/');
} }
} }
}; };

View File

@ -0,0 +1,287 @@
export default class HistoricalTelemetryProvider {
constructor(openmct, telemetryObjects, conditions, conditionSetDomainObject) {
this.openmct = openmct;
this.telemetryObjects = telemetryObjects;
this.bounds = { start: null, end: null };
this.telemetryList = [];
this.conditions = conditions;
this.conditionSetDomainObject = conditionSetDomainObject;
this.historicalTelemetryPoolMap = new Map();
this.historicalTelemetryDateMap = new Map();
this.index = 0;
}
setTimeBounds(bounds) {
this.bounds = bounds;
}
refreshAllHistoricalTelemetries() {
const refreshPromises = [];
for (const [, value] of Object.entries(this.telemetryObjects)) {
refreshPromises.push(this.refreshHistoricalTelemetry(value));
}
return Promise.all(refreshPromises);
}
async refreshHistoricalTelemetry(domainObject, identifier) {
console.log('refreshHistoricalTelemetry');
if (!domainObject && identifier) {
domainObject = await this.openmct.objects.get(identifier);
}
const id = this.openmct.objects.makeKeyString(domainObject.identifier);
const telemetryOptions = { ...this.bounds };
const historicalTelemetry = await this.openmct.telemetry.request(
domainObject,
telemetryOptions
);
this.historicalTelemetryPoolMap.set(id, { domainObject, historicalTelemetry });
return { domainObject, historicalTelemetry };
}
evaluateTrueCondition(historicalDateMap, timestamp, condition, conditionCollectionMap) {
const telemetryData = historicalDateMap.get(timestamp);
const conditionConfiguration = conditionCollectionMap.get(condition.id)?.configuration;
const { outputTelemetry, outputMetadata } = conditionConfiguration;
let output = {};
if (outputTelemetry) {
const outputTelemetryID = this.openmct.objects.makeKeyString(outputTelemetry);
const outputTelemetryData = telemetryData.get(outputTelemetryID);
output.telemetry = outputTelemetryData;
output.value = outputTelemetryData[outputMetadata];
output.condition = condition;
} else if (conditionConfiguration?.output) {
output.telemetry = null;
output.value = conditionConfiguration?.output;
output.condition = condition;
}
return output;
}
async getAllTelemetries(conditionCollection) {
const conditionCollectionMap = new Map();
const inputTelemetries = [];
const outputTelemetries = [];
const historicalTelemetryPoolPromises = [];
conditionCollection.forEach((condition, index) => {
console.log('-------------------------');
console.log(condition);
const { id } = condition;
const { criteria, output, outputTelemetry, outputMetadata } = condition.configuration;
const inputTelemetry = criteria?.[0]?.telemetry;
console.log(id);
console.log(criteria);
console.log(output);
console.log(outputMetadata);
console.log('inputTelemetry', inputTelemetry);
console.log('outputTelemetry', outputTelemetry);
conditionCollectionMap.set(condition?.id, condition);
if (inputTelemetry) {
const inputTelemetryId = this.openmct.objects.makeKeyString(inputTelemetry);
if (![...inputTelemetries, ...outputTelemetries].includes(inputTelemetryId)) {
historicalTelemetryPoolPromises.push(
this.refreshHistoricalTelemetry(null, inputTelemetry)
);
}
inputTelemetries.push(inputTelemetryId);
} else {
inputTelemetries.push(null);
}
if (outputTelemetry) {
if (![...inputTelemetries, ...outputTelemetries].includes(outputTelemetry)) {
historicalTelemetryPoolPromises.push(
this.refreshHistoricalTelemetry(null, outputTelemetry)
);
}
outputTelemetries.push(outputTelemetry);
} else {
outputTelemetries.push(null);
}
});
const historicalTelemetriesPool = await Promise.all(historicalTelemetryPoolPromises);
return {
historicalTelemetriesPool,
inputTelemetries,
outputTelemetries,
conditionCollectionMap
};
}
sortTelemetriesByDate(historicalTelemetriesPool) {
const historicalTelemetryDateMap = new Map();
historicalTelemetriesPool.forEach((historicalTelemetryList) => {
const { historicalTelemetry, domainObject } = historicalTelemetryList;
const { identifier } = domainObject;
const telemetryIdentifier = this.openmct.objects.makeKeyString(identifier);
historicalTelemetry.forEach((historicalTelemetryItem) => {
if (!historicalTelemetryDateMap.get(historicalTelemetryItem.utc)) {
const telemetryMap = new Map();
telemetryMap.set(telemetryIdentifier, historicalTelemetryItem);
historicalTelemetryDateMap.set(historicalTelemetryItem.utc, telemetryMap);
} else {
const telemetryMap = historicalTelemetryDateMap.get(historicalTelemetryItem.utc);
telemetryMap.set(telemetryIdentifier, historicalTelemetryItem);
historicalTelemetryDateMap.set(historicalTelemetryItem.utc, telemetryMap);
}
});
});
return historicalTelemetryDateMap;
}
async sortTelemetriesInWorker(historicalTelemetriesPool) {
const sortedTelemetries = await this.startWorker('sortTelemetries', {
historicalTelemetriesPool
});
return sortedTelemetries;
}
async startWorker(type, data) {
// eslint-disable-next-line no-undef
const workerUrl = `${this.openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}historicalTelemetryWorker.js`;
const worker = new Worker(workerUrl);
try {
const result = await this.getDataFromWorker(worker, type, data);
return result;
} catch (error) {
console.error('Error in condition manager getHistoricalData:', error);
throw error;
} finally {
worker.terminate();
}
}
getDataFromWorker(worker, type, data) {
return new Promise((resolve, reject) => {
worker.onmessage = (e) => {
if (e.data.type === 'result') {
resolve(e.data.data);
} else if (e.data.type === 'error') {
reject(new Error(e.data.error));
}
};
worker.onerror = (error) => {
reject(error);
};
worker.postMessage({
type,
data
});
});
}
evaluateConditionsByDate(historicalTelemetryDateMap, conditionCollectionMap) {
const outputTelemetryDateMap = new Map();
historicalTelemetryDateMap.forEach((historicalTelemetryMap, timestamp) => {
let isConditionValid = false;
const evaluatedConditions = [];
this.conditions.forEach((condition) => {
if (isConditionValid) {
return;
}
const { id } = condition;
const conditionMetadata = { condition };
const conditionCriteria = condition.criteria[0];
let result;
if (conditionCriteria?.telemetry) {
const conditionInputTelemetryId = this.openmct.objects.makeKeyString(
conditionCriteria.telemetry
);
const inputTelemetry = historicalTelemetryMap.get(conditionInputTelemetryId);
conditionMetadata.inputTelemetry = inputTelemetry;
result = conditionCriteria.computeResult({
id,
...inputTelemetry
});
} else if (!conditionCriteria) {
const conditionDetails = conditionCollectionMap.get(id);
const { isDefault } = conditionDetails;
const conditionConfiguration = conditionDetails?.configuration;
const { outputTelemetry, outputMetadata, output } = conditionConfiguration;
if (isDefault) {
const conditionOutput = {
telemetry: null,
value: output,
condition
};
outputTelemetryDateMap.set(timestamp, conditionOutput);
}
}
conditionMetadata.result = result;
evaluatedConditions.push(conditionMetadata);
if (result === true) {
isConditionValid = true;
const conditionOutput = this.evaluateTrueCondition(
historicalTelemetryDateMap,
timestamp,
condition,
conditionCollectionMap
);
outputTelemetryDateMap.set(timestamp, conditionOutput);
}
});
});
return outputTelemetryDateMap;
}
async getHistoricalInputsByDate() {
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
const {
historicalTelemetriesPool,
inputTelemetries,
outputTelemetries,
conditionCollectionMap
} = await this.getAllTelemetries(conditionCollection);
const historicalTelemetryDateMap =
await this.sortTelemetriesInWorker(historicalTelemetriesPool);
const outputTelemetryDateMap = this.evaluateConditionsByDate(
historicalTelemetryDateMap,
conditionCollectionMap
);
console.log('*️⃣*️⃣*️⃣*️⃣*️⃣*️⃣*️⃣*️⃣*️⃣*️⃣*️⃣*️⃣*️⃣*️⃣');
console.log(historicalTelemetriesPool);
console.log(this.historicalTelemetryPoolMap);
console.log(inputTelemetries);
console.log(outputTelemetries);
console.log(historicalTelemetryDateMap);
console.log(outputTelemetryDateMap);
return outputTelemetryDateMap;
}
addItemToHistoricalTelemetryMap(telemetryMap, item, type, index) {
if (type === 'input') {
telemetryMap.set();
}
}
async getHistoricalData() {
console.log('getHistoricalData');
this.setTimeBounds(this.openmct.time.getBounds());
const outputTelemetryMap = await this.getHistoricalInputsByDate();
const formattedOutputTelemetry = this.formatOutputData(outputTelemetryMap);
console.log(formattedOutputTelemetry);
return formattedOutputTelemetry;
}
formatOutputData(outputTelemetryMap) {
const outputTelemetryList = [];
const domainObject = this.conditionSetDomainObject;
outputTelemetryMap.forEach((outputMetadata, timestamp) => {
const { condition, telemetry, value } = outputMetadata;
outputTelemetryList.push({
conditionId: condition.id,
id: domainObject.identifier,
output: value,
utc: timestamp
});
});
return outputTelemetryList;
}
}

View File

@ -0,0 +1,33 @@
import { makeKeyString } from '../../api/objects/object-utils.js';
function sortTelemetriesByDate(historicalTelemetriesPool) {
const historicalTelemetryDateMap = new Map();
historicalTelemetriesPool.forEach((historicalTelemetryList) => {
const { historicalTelemetry, domainObject } = historicalTelemetryList;
const { identifier } = domainObject;
const telemetryIdentifier = makeKeyString(identifier);
historicalTelemetry.forEach((historicalTelemetryItem) => {
if (!historicalTelemetryDateMap.get(historicalTelemetryItem.utc)) {
const telemetryMap = new Map();
telemetryMap.set(telemetryIdentifier, historicalTelemetryItem);
historicalTelemetryDateMap.set(historicalTelemetryItem.utc, telemetryMap);
} else {
const telemetryMap = historicalTelemetryDateMap.get(historicalTelemetryItem.utc);
telemetryMap.set(telemetryIdentifier, historicalTelemetryItem);
historicalTelemetryDateMap.set(historicalTelemetryItem.utc, telemetryMap);
}
});
});
return historicalTelemetryDateMap;
}
self.onmessage = function (e) {
const { type, data } = e.data;
if (type === 'sortTelemetries') {
const sortedTelemetries = sortTelemetriesByDate(data.historicalTelemetriesPool);
self.postMessage({ type: 'result', data: sortedTelemetries });
} else {
self.postMessage({ type: 'error', error: 'Unknown message type' });
}
};

View File

@ -21,6 +21,7 @@
*****************************************************************************/ *****************************************************************************/
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import ConditionInspectorViewProvider from './ConditionInspectorViewProvider.js';
import ConditionSetCompositionPolicy from './ConditionSetCompositionPolicy.js'; import ConditionSetCompositionPolicy from './ConditionSetCompositionPolicy.js';
import ConditionSetMetadataProvider from './ConditionSetMetadataProvider.js'; import ConditionSetMetadataProvider from './ConditionSetMetadataProvider.js';
import ConditionSetTelemetryProvider from './ConditionSetTelemetryProvider.js'; import ConditionSetTelemetryProvider from './ConditionSetTelemetryProvider.js';
@ -37,6 +38,7 @@ export default function ConditionPlugin() {
cssClass: 'icon-conditional', cssClass: 'icon-conditional',
initialize: function (domainObject) { initialize: function (domainObject) {
domainObject.configuration = { domainObject.configuration = {
shouldFetchHistorical: false,
conditionTestData: [], conditionTestData: [],
conditionCollection: [ conditionCollection: [
{ {
@ -61,5 +63,6 @@ export default function ConditionPlugin() {
openmct.telemetry.addProvider(new ConditionSetMetadataProvider(openmct)); openmct.telemetry.addProvider(new ConditionSetMetadataProvider(openmct));
openmct.telemetry.addProvider(new ConditionSetTelemetryProvider(openmct)); openmct.telemetry.addProvider(new ConditionSetTelemetryProvider(openmct));
openmct.objectViews.addProvider(new ConditionSetViewProvider(openmct)); openmct.objectViews.addProvider(new ConditionSetViewProvider(openmct));
openmct.inspectorViews.addProvider(new ConditionInspectorViewProvider(openmct));
}; };
} }

View File

@ -720,31 +720,57 @@ describe('the plugin', function () {
}; };
}); });
it('should evaluate as old when telemetry is not received in the allotted time', (done) => { it('should evaluate as old when telemetry is not received in the allotted time', async () => {
let onAddResolve;
const onAddCalledPromise = new Promise((resolve) => {
onAddResolve = resolve;
});
const mockTelemetryCollection = {
load: jasmine.createSpy('load'),
on: jasmine.createSpy('on').and.callFake((event, callback) => {
if (event === 'add') {
onAddResolve();
}
})
};
openmct.telemetry = jasmine.createSpyObj('telemetry', [ openmct.telemetry = jasmine.createSpyObj('telemetry', [
'subscribe',
'getMetadata', 'getMetadata',
'request', 'request',
'getValueFormatter', 'getValueFormatter',
'abortAllRequests' 'abortAllRequests',
'requestCollection'
]); ]);
openmct.telemetry.request.and.returnValue(Promise.resolve([]));
openmct.telemetry.getMetadata.and.returnValue({ openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry, ...testTelemetryObject.telemetry,
valueMetadatas: [] valueMetadatas: testTelemetryObject.telemetry.values,
valuesForHints: jasmine
.createSpy('valuesForHints')
.and.returnValue(testTelemetryObject.telemetry.values),
value: jasmine.createSpy('value').and.callFake((key) => {
return testTelemetryObject.telemetry.values.find((value) => value.key === key);
})
}); });
openmct.telemetry.request.and.returnValue(Promise.resolve([])); openmct.telemetry.requestCollection.and.returnValue(mockTelemetryCollection);
openmct.telemetry.getValueFormatter.and.returnValue({ openmct.telemetry.getValueFormatter.and.returnValue({
parse: function (value) { parse: function (value) {
return value; return value;
} }
}); });
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct); let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
conditionMgr.on('conditionSetResultUpdated', mockListener); conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.telemetryObjects = { conditionMgr.telemetryObjects = {
'test-object': testTelemetryObject 'test-object': testTelemetryObject
}; };
conditionMgr.updateConditionTelemetryObjects(); conditionMgr.updateConditionTelemetryObjects();
setTimeout(() => { // Wait for the 'on' callback to be called
await onAddCalledPromise;
// Simulate the passage of time and no data received
await new Promise((resolve) => setTimeout(resolve, 400));
expect(mockListener).toHaveBeenCalledWith({ expect(mockListener).toHaveBeenCalledWith({
output: 'Any old telemetry', output: 'Any old telemetry',
id: { id: {
@ -754,16 +780,9 @@ describe('the plugin', function () {
conditionId: '39584410-cbf9-499e-96dc-76f27e69885d', conditionId: '39584410-cbf9-499e-96dc-76f27e69885d',
utc: undefined utc: undefined
}); });
done();
}, 400);
}); });
it('should not evaluate as old when telemetry is received in the allotted time', (done) => { it('should not evaluate as old when telemetry is received in the allotted time', async () => {
openmct.telemetry.getMetadata = jasmine.createSpy('getMetadata');
openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry,
valueMetadatas: testTelemetryObject.telemetry.values
});
const testDatum = { const testDatum = {
'some-key2': '', 'some-key2': '',
utc: 1, utc: 1,
@ -771,8 +790,49 @@ describe('the plugin', function () {
'some-key': null, 'some-key': null,
id: 'test-object' id: 'test-object'
}; };
openmct.telemetry.request = jasmine.createSpy('request');
let onAddResolve;
let onAddCallback;
const onAddCalledPromise = new Promise((resolve) => {
onAddResolve = resolve;
});
const mockTelemetryCollection = {
load: jasmine.createSpy('load'),
on: jasmine.createSpy('on').and.callFake((event, callback) => {
if (event === 'add') {
onAddCallback = callback;
onAddResolve();
}
})
};
openmct.telemetry = jasmine.createSpyObj('telemetry', [
'getMetadata',
'getValueFormatter',
'request',
'subscribe',
'requestCollection'
]);
openmct.telemetry.subscribe.and.returnValue(function () {});
openmct.telemetry.request.and.returnValue(Promise.resolve([testDatum])); openmct.telemetry.request.and.returnValue(Promise.resolve([testDatum]));
openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry,
valueMetadatas: testTelemetryObject.telemetry.values,
valuesForHints: jasmine
.createSpy('valuesForHints')
.and.returnValue(testTelemetryObject.telemetry.values),
value: jasmine.createSpy('value').and.callFake((key) => {
return testTelemetryObject.telemetry.values.find((value) => value.key === key);
})
});
openmct.telemetry.requestCollection.and.returnValue(mockTelemetryCollection);
openmct.telemetry.getValueFormatter.and.returnValue({
parse: function (value) {
return value;
}
});
const date = 1; const date = 1;
conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input = conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input =
['0.4']; ['0.4'];
@ -782,8 +842,16 @@ describe('the plugin', function () {
'test-object': testTelemetryObject 'test-object': testTelemetryObject
}; };
conditionMgr.updateConditionTelemetryObjects(); conditionMgr.updateConditionTelemetryObjects();
conditionMgr.telemetryReceived(testTelemetryObject, testDatum);
setTimeout(() => { // Wait for the 'on' callback to be called
await onAddCalledPromise;
// Simulate receiving telemetry data
onAddCallback([testDatum]);
// Wait a bit for the condition manager to process the data
await new Promise((resolve) => setTimeout(resolve, 100));
expect(mockListener).toHaveBeenCalledWith({ expect(mockListener).toHaveBeenCalledWith({
output: 'Default', output: 'Default',
id: { id: {
@ -793,8 +861,6 @@ describe('the plugin', function () {
conditionId: '2532d90a-e0d6-4935-b546-3123522da2de', conditionId: '2532d90a-e0d6-4935-b546-3123522da2de',
utc: date utc: date
}); });
done();
}, 300);
}); });
}); });
@ -902,17 +968,25 @@ describe('the plugin', function () {
openmct.telemetry.getMetadata = jasmine.createSpy('getMetadata'); openmct.telemetry.getMetadata = jasmine.createSpy('getMetadata');
openmct.telemetry.getMetadata.and.returnValue({ openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry, ...testTelemetryObject.telemetry,
valueMetadatas: [] valueMetadatas: testTelemetryObject.telemetry.values,
valuesForHints: jasmine
.createSpy('valuesForHints')
.and.returnValue(testTelemetryObject.telemetry.values),
value: jasmine.createSpy('value').and.callFake((key) => {
return testTelemetryObject.telemetry.values.find((value) => value.key === key);
})
}); });
conditionMgr.on('conditionSetResultUpdated', mockListener); conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.telemetryObjects = { conditionMgr.telemetryObjects = {
'test-object': testTelemetryObject 'test-object': testTelemetryObject
}; };
conditionMgr.updateConditionTelemetryObjects(); conditionMgr.updateConditionTelemetryObjects();
conditionMgr.telemetryReceived(testTelemetryObject, { conditionMgr.telemetryReceived(testTelemetryObject, [
{
'some-key': 2, 'some-key': 2,
utc: date utc: date
}); }
]);
let result = conditionMgr.conditions.map((condition) => condition.result); let result = conditionMgr.conditions.map((condition) => condition.result);
expect(result[2]).toBeUndefined(); expect(result[2]).toBeUndefined();
}); });
@ -1002,26 +1076,37 @@ describe('the plugin', function () {
} }
}; };
openmct.$injector = jasmine.createSpyObj('$injector', ['get']); openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
// const mockTransactionService = jasmine.createSpyObj(
// 'transactionService',
// ['commit']
// );
openmct.telemetry = jasmine.createSpyObj('telemetry', [ openmct.telemetry = jasmine.createSpyObj('telemetry', [
'isTelemetryObject', 'isTelemetryObject',
'request',
'subscribe', 'subscribe',
'getMetadata', 'getMetadata',
'getValueFormatter', 'getValueFormatter',
'request' 'requestCollection'
]); ]);
openmct.telemetry.isTelemetryObject.and.returnValue(true);
openmct.telemetry.subscribe.and.returnValue(function () {}); openmct.telemetry.subscribe.and.returnValue(function () {});
openmct.telemetry.request.and.returnValue(Promise.resolve([]));
openmct.telemetry.isTelemetryObject.and.returnValue(true);
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry,
valueMetadatas: testTelemetryObject.telemetry.values,
valuesForHints: jasmine
.createSpy('valuesForHints')
.and.returnValue(testTelemetryObject.telemetry.values),
value: jasmine.createSpy('value').and.callFake((key) => {
return testTelemetryObject.telemetry.values.find((value) => value.key === key);
})
});
openmct.telemetry.getValueFormatter.and.returnValue({ openmct.telemetry.getValueFormatter.and.returnValue({
parse: function (value) { parse: function (value) {
return value; return value;
} }
}); });
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry); openmct.telemetry.requestCollection.and.returnValue({
openmct.telemetry.request.and.returnValue(Promise.resolve([])); load: jasmine.createSpy('load'),
on: jasmine.createSpy('on')
});
const styleRuleManger = new StyleRuleManager(stylesObject, openmct, null, true); const styleRuleManger = new StyleRuleManager(stylesObject, openmct, null, true);
spyOn(styleRuleManger, 'subscribeToConditionSet'); spyOn(styleRuleManger, 'subscribeToConditionSet');

View File

@ -243,6 +243,7 @@ export default {
domainObject: { domainObject: {
...this.childObject, ...this.childObject,
configuration: { configuration: {
...this.childObject.configuration,
series: [ series: [
{ {
identifier: this.childObject.identifier, identifier: this.childObject.identifier,

View File

@ -128,37 +128,27 @@ export default {
} }
}, },
updateStyle(styleObj) { updateStyle(styleObj) {
let elemToStyle = this.getStyleReceiver(); const elemToStyle = this.getStyleReceiver();
if (!styleObj || elemToStyle === undefined) { if (!styleObj || !elemToStyle) {
return; return;
} }
let keys = Object.keys(styleObj); // handle visibility separately
if (styleObj.isStyleInvisible !== undefined) {
keys.forEach((key) => { elemToStyle.classList.toggle(STYLE_CONSTANTS.isStyleInvisible, styleObj.isStyleInvisible);
if (elemToStyle) { styleObj.isStyleInvisible = null;
if (typeof styleObj[key] === 'string' && styleObj[key].indexOf('__no_value') > -1) {
if (elemToStyle.style[key]) {
elemToStyle.style[key] = '';
} }
requestAnimationFrame(() => {
Object.entries(styleObj).forEach(([key, value]) => {
if (typeof value !== 'string' || !value.includes('__no_value')) {
elemToStyle.style[key] = value;
} else { } else {
if ( elemToStyle.style[key] = ''; // remove the property
!styleObj.isStyleInvisible &&
elemToStyle.classList.contains(STYLE_CONSTANTS.isStyleInvisible)
) {
elemToStyle.classList.remove(STYLE_CONSTANTS.isStyleInvisible);
} else if (
styleObj.isStyleInvisible &&
!elemToStyle.classList.contains(styleObj.isStyleInvisible)
) {
elemToStyle.classList.add(styleObj.isStyleInvisible);
}
elemToStyle.style[key] = styleObj[key];
}
} }
}); });
});
} }
} }
}; };

View File

@ -94,7 +94,7 @@ export function ticks(start, stop, count) {
} }
export function commonPrefix(a, b) { export function commonPrefix(a, b) {
const maxLen = Math.min(a.length, b.length); const maxLen = Math.min(a.length, b?.length);
let breakpoint = 0; let breakpoint = 0;
for (let i = 0; i < maxLen; i++) { for (let i = 0; i < maxLen; i++) {
if (a[i] !== b[i]) { if (a[i] !== b[i]) {
@ -110,7 +110,7 @@ export function commonPrefix(a, b) {
} }
export function commonSuffix(a, b) { export function commonSuffix(a, b) {
const maxLen = Math.min(a.length, b.length); const maxLen = Math.min(a.length, b?.length);
let breakpoint = 0; let breakpoint = 0;
for (let i = 0; i <= maxLen; i++) { for (let i = 0; i <= maxLen; i++) {
if (a[a.length - i] !== b[b.length - i]) { if (a[a.length - i] !== b[b.length - i]) {