Compare commits

...

10 Commits

Author SHA1 Message Date
bfa604efa0 Condition Widgets trigger hundreds of persistence calls. #5137 2022-04-29 02:03:23 -07:00
e75befafbd Dynamic dial-type Gauge sizing by height and width (#5129)
* Improve sizing strategy for gauges.
* Do not install gauge by default for now

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Jamie Vigliotta <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-04-28 14:03:35 -07:00
d7d06b59ea Gauge edit enabled 2.0.3 (#5133)
* Gauge plugin #4896, add edit mode
2022-04-28 13:32:12 -07:00
8b4a55a7ec Fix transactions overwriting latest objects with stale objects on save (#5132)
* use object (map) instead of set to track dirty objects
* fix tests due to internals change

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
2022-04-28 12:43:30 -07:00
2519e601d7 Added animation styling for POS and CAM; adjusted cutoff for isNewImage (#5116)
* Added animation styling for POS and CAM; adjusted cutoff for isNewImage

* Remove animation from POS and CAM
2022-04-28 12:03:42 -07:00
b7b205621b added telemetry collection to alphanumeric telemetry view (#5131) 2022-04-28 11:35:35 -07:00
3d2d932323 [LAD Tables] Use Telemetry Collections (#5127)
* Use telemetry collections to handle bounds checks
2022-04-28 11:21:58 -07:00
ce28dd2b9f Handle scrolling to focused image on resize/new data (#5121)
* Scroll to focused image when view resizes - this will force scrolling to focused image when going to/from view large mode

* Scroll to the right if there is no paused focused image
2022-04-28 10:44:53 -07:00
286a533dad Fix tick values for plots ticks in log mode and null check (#5119)
* [2297] When there is no display range or range, skip setting the range value when auto scale is turned off.

* If the formatted value is a number and a float, set precision to 2 decimal points.

* Fix value assignment

* Use whole numbers in log mode

* Revert whole numbers fix - need floats for values between 0 and 1.
2022-04-26 13:40:19 -07:00
378a4ca282 Release 2.0.3 2022-04-25 11:58:50 -07:00
24 changed files with 551 additions and 394 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "openmct", "name": "openmct",
"version": "2.0.3-SNAPSHOT", "version": "2.0.3",
"description": "The Open MCT core platform", "description": "The Open MCT core platform",
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "7.16.3", "@babel/eslint-parser": "7.16.3",

View File

@ -232,6 +232,7 @@ define([
this.actions = new api.ActionsAPI(this); this.actions = new api.ActionsAPI(this);
this.status = new api.StatusAPI(this); this.status = new api.StatusAPI(this);
this.styleManager = new api.StyleManagerAPI(this);
this.priority = api.PriorityAPI; this.priority = api.PriorityAPI;
@ -241,8 +242,6 @@ define([
this.branding = BrandingAPI.default; this.branding = BrandingAPI.default;
// Plugins that are installed by default // Plugins that are installed by default
this.install(this.plugins.Gauge());
this.install(this.plugins.Plot()); this.install(this.plugins.Plot());
this.install(this.plugins.Chart()); this.install(this.plugins.Chart());
this.install(this.plugins.TelemetryTable.default()); this.install(this.plugins.TelemetryTable.default());

View File

@ -31,6 +31,7 @@ define([
'./objects/ObjectAPI', './objects/ObjectAPI',
'./priority/PriorityAPI', './priority/PriorityAPI',
'./status/StatusAPI', './status/StatusAPI',
'./styles/StyleManagerAPI',
'./telemetry/TelemetryAPI', './telemetry/TelemetryAPI',
'./time/TimeAPI', './time/TimeAPI',
'./types/TypeRegistry', './types/TypeRegistry',
@ -46,6 +47,7 @@ define([
ObjectAPI, ObjectAPI,
PriorityAPI, PriorityAPI,
StatusAPI, StatusAPI,
StyleManagerAPI,
TelemetryAPI, TelemetryAPI,
TimeAPI, TimeAPI,
TypeRegistry, TypeRegistry,
@ -62,6 +64,7 @@ define([
ObjectAPI: ObjectAPI, ObjectAPI: ObjectAPI,
PriorityAPI: PriorityAPI.default, PriorityAPI: PriorityAPI.default,
StatusAPI: StatusAPI.default, StatusAPI: StatusAPI.default,
StyleManagerAPI: StyleManagerAPI.default,
TelemetryAPI: TelemetryAPI, TelemetryAPI: TelemetryAPI,
TimeAPI: TimeAPI.default, TimeAPI: TimeAPI.default,
TypeRegistry: TypeRegistry, TypeRegistry: TypeRegistry,

View File

@ -22,12 +22,14 @@
export default class Transaction { export default class Transaction {
constructor(objectAPI) { constructor(objectAPI) {
this.dirtyObjects = new Set(); this.dirtyObjects = {};
this.objectAPI = objectAPI; this.objectAPI = objectAPI;
} }
add(object) { add(object) {
this.dirtyObjects.add(object); const key = this.objectAPI.makeKeyString(object.identifier);
this.dirtyObjects[key] = object;
} }
cancel() { cancel() {
@ -37,7 +39,8 @@ export default class Transaction {
commit() { commit() {
const promiseArray = []; const promiseArray = [];
const save = this.objectAPI.save.bind(this.objectAPI); const save = this.objectAPI.save.bind(this.objectAPI);
this.dirtyObjects.forEach(object => {
Object.values(this.dirtyObjects).forEach(object => {
promiseArray.push(this.createDirtyObjectPromise(object, save)); promiseArray.push(this.createDirtyObjectPromise(object, save));
}); });
@ -48,7 +51,9 @@ export default class Transaction {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
action(object) action(object)
.then((success) => { .then((success) => {
this.dirtyObjects.delete(object); const key = this.objectAPI.makeKeyString(object.identifier);
delete this.dirtyObjects[key];
resolve(success); resolve(success);
}) })
.catch(reject); .catch(reject);
@ -57,7 +62,8 @@ export default class Transaction {
getDirtyObject(identifier) { getDirtyObject(identifier) {
let dirtyObject; let dirtyObject;
this.dirtyObjects.forEach(object => {
Object.values(this.dirtyObjects).forEach(object => {
const areIdsEqual = this.objectAPI.areIdsEqual(object.identifier, identifier); const areIdsEqual = this.objectAPI.areIdsEqual(object.identifier, identifier);
if (areIdsEqual) { if (areIdsEqual) {
dirtyObject = object; dirtyObject = object;
@ -67,14 +73,11 @@ export default class Transaction {
return dirtyObject; return dirtyObject;
} }
start() {
this.dirtyObjects = new Set();
}
_clear() { _clear() {
const promiseArray = []; const promiseArray = [];
const refresh = this.objectAPI.refresh.bind(this.objectAPI); const refresh = this.objectAPI.refresh.bind(this.objectAPI);
this.dirtyObjects.forEach(object => {
Object.values(this.dirtyObjects).forEach(object => {
promiseArray.push(this.createDirtyObjectPromise(object, refresh)); promiseArray.push(this.createDirtyObjectPromise(object, refresh));
}); });

View File

@ -34,24 +34,24 @@ describe("Transaction Class", () => {
}); });
it('has no dirty objects', () => { it('has no dirty objects', () => {
expect(transaction.dirtyObjects.size).toEqual(0); expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
}); });
it('add(), adds object to dirtyObjects', () => { it('add(), adds object to dirtyObjects', () => {
const mockDomainObjects = createMockDomainObjects(); const mockDomainObjects = createMockDomainObjects();
transaction.add(mockDomainObjects[0]); transaction.add(mockDomainObjects[0]);
expect(transaction.dirtyObjects.size).toEqual(1); expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
}); });
it('cancel(), clears all dirtyObjects', (done) => { it('cancel(), clears all dirtyObjects', (done) => {
const mockDomainObjects = createMockDomainObjects(3); const mockDomainObjects = createMockDomainObjects(3);
mockDomainObjects.forEach(transaction.add.bind(transaction)); mockDomainObjects.forEach(transaction.add.bind(transaction));
expect(transaction.dirtyObjects.size).toEqual(3); expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
transaction.cancel() transaction.cancel()
.then(success => { .then(success => {
expect(transaction.dirtyObjects.size).toEqual(0); expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
}).finally(done); }).finally(done);
}); });
@ -59,12 +59,12 @@ describe("Transaction Class", () => {
const mockDomainObjects = createMockDomainObjects(3); const mockDomainObjects = createMockDomainObjects(3);
mockDomainObjects.forEach(transaction.add.bind(transaction)); mockDomainObjects.forEach(transaction.add.bind(transaction));
expect(transaction.dirtyObjects.size).toEqual(3); expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
spyOn(objectAPI, 'save').and.callThrough(); spyOn(objectAPI, 'save').and.callThrough();
transaction.commit() transaction.commit()
.then(success => { .then(success => {
expect(transaction.dirtyObjects.size).toEqual(0); expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
expect(objectAPI.save.calls.count()).toEqual(3); expect(objectAPI.save.calls.count()).toEqual(3);
}).finally(done); }).finally(done);
}); });
@ -73,7 +73,7 @@ describe("Transaction Class", () => {
const mockDomainObjects = createMockDomainObjects(); const mockDomainObjects = createMockDomainObjects();
transaction.add(mockDomainObjects[0]); transaction.add(mockDomainObjects[0]);
expect(transaction.dirtyObjects.size).toEqual(1); expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier); const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
expect(dirtyObject).toEqual(mockDomainObjects[0]); expect(dirtyObject).toEqual(mockDomainObjects[0]);
@ -82,7 +82,7 @@ describe("Transaction Class", () => {
it('getDirtyObject(), returns empty dirtyObject for no active transaction', () => { it('getDirtyObject(), returns empty dirtyObject for no active transaction', () => {
const mockDomainObjects = createMockDomainObjects(); const mockDomainObjects = createMockDomainObjects();
expect(transaction.dirtyObjects.size).toEqual(0); expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier); const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
expect(dirtyObject).toEqual(undefined); expect(dirtyObject).toEqual(undefined);

View File

@ -0,0 +1,67 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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 EventEmitter from 'EventEmitter';
export default class StyleManagerAPI extends EventEmitter {
constructor(openmct) {
super();
this._openmct = openmct;
this._styleCache = {};
this.get = this.get.bind(this);
this.set = this.set.bind(this);
this.observe = this.observe.bind(this);
}
get(identifier) {
let keyString = this._openmct.objects.makeKeyString(identifier);
return this._styleCache[keyString];
}
set(identifier, value) {
let keyString = this._openmct.objects.makeKeyString(identifier);
this._styleCache[keyString] = value;
this.emit(keyString, value);
}
delete(identifier) {
let keyString = this._openmct.objects.makeKeyString(identifier);
this._styleCache[keyString] = undefined;
this.emit(keyString, undefined);
delete this._styleCache[keyString];
}
observe(identifier, callback) {
let key = this._openmct.objects.makeKeyString(identifier);
this.on(key, callback);
return () => {
this.off(key, callback);
};
}
}

View File

@ -1,4 +1,3 @@
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government * Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space * as represented by the Administrator of the National Aeronautics and Space
@ -114,14 +113,12 @@ export default {
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.formats = this.openmct.telemetry.getFormatMap(this.metadata); this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.bounds = this.openmct.time.bounds();
this.limitEvaluator = this.openmct this.limitEvaluator = this.openmct
.telemetry .telemetry
.limitEvaluator(this.domainObject); .limitEvaluator(this.domainObject);
this.openmct.time.on('timeSystem', this.updateTimeSystem); this.openmct.time.on('timeSystem', this.updateTimeSystem);
this.openmct.time.on('bounds', this.updateBounds);
this.timestampKey = this.openmct.time.timeSystem().key; this.timestampKey = this.openmct.time.timeSystem().key;
@ -135,72 +132,39 @@ export default {
this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined; this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
this.unsubscribe = this.openmct this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
.telemetry size: 1,
.subscribe(this.domainObject, this.setLatestValues); strategy: 'latest'
});
this.requestHistory(); this.telemetryCollection.on('add', this.setLatestValues);
this.telemetryCollection.on('clear', this.resetValues);
this.telemetryCollection.load();
if (this.hasUnits) { if (this.hasUnits) {
this.setUnit(); this.setUnit();
} }
}, },
destroyed() { destroyed() {
this.unsubscribe();
this.openmct.time.off('timeSystem', this.updateTimeSystem); this.openmct.time.off('timeSystem', this.updateTimeSystem);
this.openmct.time.off('bounds', this.updateBounds); this.telemetryCollection.off('add', this.setLatestValues);
this.telemetryCollection.off('clear', this.resetValues);
}, },
methods: { methods: {
updateView() { updateView() {
if (!this.updatingView) { if (!this.updatingView) {
this.updatingView = true; this.updatingView = true;
requestAnimationFrame(() => { requestAnimationFrame(() => {
let newTimestamp = this.getParsedTimestamp(this.latestDatum); this.timestamp = this.getParsedTimestamp(this.latestDatum);
this.datum = this.latestDatum;
if (this.shouldUpdate(newTimestamp)) {
this.timestamp = newTimestamp;
this.datum = this.latestDatum;
}
this.updatingView = false; this.updatingView = false;
}); });
} }
}, },
setLatestValues(datum) { setLatestValues(data) {
this.latestDatum = datum; this.latestDatum = data[data.length - 1];
this.updateView(); this.updateView();
}, },
shouldUpdate(newTimestamp) {
return this.inBounds(newTimestamp)
&& (this.timestamp === undefined || newTimestamp > this.timestamp);
},
requestHistory() {
this.openmct
.telemetry
.request(this.domainObject, {
start: this.bounds.start,
end: this.bounds.end,
size: 1,
strategy: 'latest'
})
.then((array) => this.setLatestValues(array[array.length - 1]))
.catch((error) => {
console.warn('Error fetching data', error);
});
},
updateBounds(bounds, isTick) {
this.bounds = bounds;
if (!isTick) {
this.resetValues();
this.requestHistory();
}
},
inBounds(timestamp) {
return timestamp >= this.bounds.start && timestamp <= this.bounds.end;
},
updateTimeSystem(timeSystem) { updateTimeSystem(timeSystem) {
this.resetValues();
this.timestampKey = timeSystem.key; this.timestampKey = timeSystem.key;
}, },
updateViewContext() { updateViewContext() {
@ -241,4 +205,3 @@ export default {
} }
}; };
</script> </script>

View File

@ -46,6 +46,7 @@ describe("The LAD Table", () => {
let openmct; let openmct;
let ladPlugin; let ladPlugin;
let historicalProvider;
let parent; let parent;
let child; let child;
let telemetryCount = 3; let telemetryCount = 3;
@ -81,6 +82,13 @@ describe("The LAD Table", () => {
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({})); spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
historicalProvider = {
request: () => {
return Promise.resolve([]);
}
};
spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider);
openmct.time.bounds({ openmct.time.bounds({
start: bounds.start, start: bounds.start,
end: bounds.end end: bounds.end
@ -147,7 +155,7 @@ describe("The LAD Table", () => {
// add another telemetry object as composition in lad table to test multi rows // add another telemetry object as composition in lad table to test multi rows
mockObj.ladTable.composition.push(anotherTelemetryObj.identifier); mockObj.ladTable.composition.push(anotherTelemetryObj.identifier);
beforeEach(async () => { beforeEach(async (done) => {
let telemetryRequestResolve; let telemetryRequestResolve;
let telemetryObjectResolve; let telemetryObjectResolve;
let anotherTelemetryObjectResolve; let anotherTelemetryObjectResolve;
@ -166,11 +174,12 @@ describe("The LAD Table", () => {
callBack(); callBack();
}); });
openmct.telemetry.request.and.callFake(() => { historicalProvider.request = () => {
telemetryRequestResolve(mockTelemetry); telemetryRequestResolve(mockTelemetry);
return telemetryRequestPromise; return telemetryRequestPromise;
}); };
openmct.objects.get.and.callFake((obj) => { openmct.objects.get.and.callFake((obj) => {
if (obj.key === 'telemetry-object') { if (obj.key === 'telemetry-object') {
telemetryObjectResolve(mockObj.telemetry); telemetryObjectResolve(mockObj.telemetry);
@ -195,6 +204,8 @@ describe("The LAD Table", () => {
await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]); await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]);
await Vue.nextTick(); await Vue.nextTick();
done();
}); });
it("should show one row per object in the composition", () => { it("should show one row per object in the composition", () => {

View File

@ -25,6 +25,7 @@ import EventEmitter from 'EventEmitter';
export default class StyleRuleManager extends EventEmitter { export default class StyleRuleManager extends EventEmitter {
constructor(styleConfiguration, openmct, callback, suppressSubscriptionOnEdit) { constructor(styleConfiguration, openmct, callback, suppressSubscriptionOnEdit) {
super(); super();
this.openmct = openmct; this.openmct = openmct;
this.callback = callback; this.callback = callback;
this.refreshData = this.refreshData.bind(this); this.refreshData = this.refreshData.bind(this);
@ -152,6 +153,7 @@ export default class StyleRuleManager extends EventEmitter {
updateDomainObjectStyle() { updateDomainObjectStyle() {
if (this.callback) { if (this.callback) {
this.emit('updateStyles', this.currentStyle);
this.callback(Object.assign({}, this.currentStyle)); this.callback(Object.assign({}, this.currentStyle));
} }
} }

View File

@ -27,7 +27,7 @@
:href="url" :href="url"
> >
<div class="c-condition-widget__label"> <div class="c-condition-widget__label">
{{ internalDomainObject.conditionalLabel || internalDomainObject.label }} {{ label }}
</div> </div>
</component> </component>
</template> </template>
@ -39,10 +39,16 @@ export default {
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject'],
data: function () { data: function () {
return { return {
internalDomainObject: this.domainObject internalDomainObject: this.domainObject,
conditionalLabel: ''
}; };
}, },
computed: { computed: {
label() {
return this.conditionalLabel.length
? this.conditionalLabel
: this.internalDomainObject.label;
},
urlDefined() { urlDefined() {
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0; return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
}, },
@ -52,13 +58,31 @@ export default {
}, },
mounted() { mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject); this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
this.unobserve = this.openmct.styleManager.observe(this.internalDomainObject.identifier, this.observeStyleManagerChanges.bind(this));
}, },
beforeDestroy() { beforeDestroy() {
if (this.unlisten) { if (this.unlisten) {
this.unlisten(); this.unlisten();
} }
if (this.unobserve) {
this.openmct.styleManager.delete(this.internalDomainObject.identifier);
this.unobserve();
}
}, },
methods: { methods: {
observeStyleManagerChanges(styleManager) {
if (styleManager) {
this.styleManager = styleManager;
this.styleManager.on('updateStyles', this.updateConditionLabel);
} else {
this.styleManager.off('updateStyles', this.updateConditionLabel);
}
},
updateConditionLabel(styleObj = {}) {
this.conditionalLabel = styleObj.output || '';
},
updateInternalDomainObject(domainObject) { updateInternalDomainObject(domainObject) {
this.internalDomainObject = domainObject; this.internalDomainObject = domainObject;
} }

View File

@ -222,20 +222,18 @@ export default {
.then(this.setObject); .then(this.setObject);
} }
this.openmct.time.on("bounds", this.refreshData);
this.status = this.openmct.status.get(this.item.identifier); this.status = this.openmct.status.get(this.item.identifier);
this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus); this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus);
}, },
beforeDestroy() { beforeDestroy() {
this.removeSubscription();
this.removeStatusListener(); this.removeStatusListener();
if (this.removeSelectable) { if (this.removeSelectable) {
this.removeSelectable(); this.removeSelectable();
} }
this.openmct.time.off("bounds", this.refreshData); this.telemetryCollection.off('add', this.setLatestValues);
this.telemetryCollection.off('clear', this.refreshData);
if (this.mutablePromise) { if (this.mutablePromise) {
this.mutablePromise.then(() => { this.mutablePromise.then(() => {
@ -253,34 +251,9 @@ export default {
return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue}${unit}`; return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue}${unit}`;
}, },
requestHistoricalData() { setLatestValues(data) {
let bounds = this.openmct.time.bounds(); this.latestDatum = data[data.length - 1];
let options = { this.updateView();
start: bounds.start,
end: bounds.end,
size: 1,
strategy: 'latest'
};
this.openmct.telemetry.request(this.domainObject, options)
.then(data => {
if (data.length > 0) {
this.latestDatum = data[data.length - 1];
this.updateView();
}
});
},
subscribeToObject() {
this.subscription = this.openmct.telemetry.subscribe(this.domainObject, function (datum) {
const key = this.openmct.time.timeSystem().key;
const datumTimeStamp = datum[key];
if (this.openmct.time.clock() !== undefined
|| (datumTimeStamp
&& (this.openmct.time.bounds().end >= datumTimeStamp))
) {
this.latestDatum = datum;
this.updateView();
}
}.bind(this));
}, },
updateView() { updateView() {
if (!this.updatingView) { if (!this.updatingView) {
@ -291,17 +264,10 @@ export default {
}); });
} }
}, },
removeSubscription() {
if (this.subscription) {
this.subscription();
this.subscription = undefined;
}
},
refreshData(bounds, isTick) { refreshData(bounds, isTick) {
if (!isTick) { if (!isTick) {
this.latestDatum = undefined; this.latestDatum = undefined;
this.updateView(); this.updateView();
this.requestHistoricalData(this.domainObject);
} }
}, },
setObject(domainObject) { setObject(domainObject) {
@ -315,8 +281,13 @@ export default {
const valueMetadata = this.metadata.value(this.item.value); const valueMetadata = this.metadata.value(this.item.value);
this.customStringformatter = this.openmct.telemetry.customStringFormatter(valueMetadata, this.item.format); this.customStringformatter = this.openmct.telemetry.customStringFormatter(valueMetadata, this.item.format);
this.requestHistoricalData(); this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
this.subscribeToObject(); size: 1,
strategy: 'latest'
});
this.telemetryCollection.on('add', this.setLatestValues);
this.telemetryCollection.on('clear', this.refreshData);
this.telemetryCollection.load();
this.currentObjectPath = this.objectPath.slice(); this.currentObjectPath = this.objectPath.slice();
this.currentObjectPath.unshift(this.domainObject); this.currentObjectPath.unshift(this.domainObject);

View File

@ -53,6 +53,8 @@ describe('Gauge plugin', () => {
openmct = createOpenMct(); openmct = createOpenMct();
openmct.on('start', done); openmct.on('start', done);
openmct.install(openmct.plugins.Gauge());
openmct.startHeadless(); openmct.startHeadless();
}); });
@ -190,28 +192,27 @@ describe('Gauge plugin', () => {
}); });
it('renders gauge element', () => { it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge'); const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1); expect(gaugeElement.length).toBe(1);
}); });
it('renders major elements', () => { it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper'); const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.c-gauge__range'); const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
const curveElement = gaugeHolder.querySelector('.c-gauge__curval'); const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
const dialElement = gaugeHolder.querySelector('.c-dial');
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement); const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true); expect(hasMajorElements).toBe(true);
}); });
it('renders correct min max values', () => { it('renders correct min max values', () => {
expect(gaugeHolder.querySelector('.c-gauge__range').textContent).toEqual(`${minValue} ${maxValue}`); expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toEqual(`${minValue} ${maxValue}`);
}); });
it('renders correct current value', (done) => { it('renders correct current value', (done) => {
function WatchUpdateValue() { function WatchUpdateValue() {
const textElement = gaugeHolder.querySelector('.c-gauge__curval-text'); const textElement = gaugeHolder.querySelector('.js-dial-current-value');
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision)); expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
done(); done();
} }
@ -326,28 +327,27 @@ describe('Gauge plugin', () => {
}); });
it('renders gauge element', () => { it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge'); const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1); expect(gaugeElement.length).toBe(1);
}); });
it('renders major elements', () => { it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper'); const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.c-gauge__range'); const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
const curveElement = gaugeHolder.querySelector('.c-gauge__curval'); const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
const dialElement = gaugeHolder.querySelector('.c-dial');
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement); const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true); expect(hasMajorElements).toBe(true);
}); });
it('renders correct min max values', () => { it('renders correct min max values', () => {
expect(gaugeHolder.querySelector('.c-gauge__range').textContent).toEqual(`${minValue} ${maxValue}`); expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toEqual(`${minValue} ${maxValue}`);
}); });
it('renders correct current value', (done) => { it('renders correct current value', (done) => {
function WatchUpdateValue() { function WatchUpdateValue() {
const textElement = gaugeHolder.querySelector('.c-gauge__curval-text'); const textElement = gaugeHolder.querySelector('.js-dial-current-value');
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision)); expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
done(); done();
} }
@ -462,28 +462,27 @@ describe('Gauge plugin', () => {
}); });
it('renders gauge element', () => { it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge'); const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1); expect(gaugeElement.length).toBe(1);
}); });
it('renders major elements', () => { it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper'); const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.c-gauge__range'); const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range');
const curveElement = gaugeHolder.querySelector('.c-meter'); const valueElement = gaugeHolder.querySelector('.js-meter-current-value');
const dialElement = gaugeHolder.querySelector('.c-meter__bg');
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement); const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true); expect(hasMajorElements).toBe(true);
}); });
it('renders correct min max values', () => { it('renders correct min max values', () => {
expect(gaugeHolder.querySelector('.c-gauge__range').textContent).toEqual(`${maxValue} ${minValue}`); expect(gaugeHolder.querySelector('.js-gauge-meter-range').textContent).toEqual(`${maxValue} ${minValue}`);
}); });
it('renders correct current value', (done) => { it('renders correct current value', (done) => {
function WatchUpdateValue() { function WatchUpdateValue() {
const textElement = gaugeHolder.querySelector('.c-gauge__curval-text'); const textElement = gaugeHolder.querySelector('.js-meter-current-value');
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision)); expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
done(); done();
} }
@ -560,17 +559,16 @@ describe('Gauge plugin', () => {
}); });
it('renders gauge element', () => { it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge'); const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1); expect(gaugeElement.length).toBe(1);
}); });
it('renders major elements', () => { it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper'); const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.c-gauge__range'); const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range');
const curveElement = gaugeHolder.querySelector('.c-meter'); const valueElement = gaugeHolder.querySelector('.js-meter-current-value');
const dialElement = gaugeHolder.querySelector('.c-meter__bg');
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement); const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true); expect(hasMajorElements).toBe(true);
}); });
@ -643,17 +641,16 @@ describe('Gauge plugin', () => {
}); });
it('renders gauge element', () => { it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge'); const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1); expect(gaugeElement.length).toBe(1);
}); });
it('renders major elements', () => { it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper'); const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.c-gauge__range'); const rangeElement = gaugeHolder.querySelector('.c-gauge__range');
const curveElement = gaugeHolder.querySelector('.c-meter'); const curveElement = gaugeHolder.querySelector('.c-meter');
const dialElement = gaugeHolder.querySelector('.c-meter__bg');
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement); const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement);
expect(hasMajorElements).toBe(true); expect(hasMajorElements).toBe(true);
}); });
@ -772,28 +769,27 @@ describe('Gauge plugin', () => {
}); });
it('renders gauge element', () => { it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge'); const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1); expect(gaugeElement.length).toBe(1);
}); });
it('renders major elements', () => { it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper'); const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.c-gauge__range'); const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
const curveElement = gaugeHolder.querySelector('.c-gauge__curval'); const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
const dialElement = gaugeHolder.querySelector('.c-dial');
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement); const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true); expect(hasMajorElements).toBe(true);
}); });
it('renders correct min max values', () => { it('renders correct min max values', () => {
expect(gaugeHolder.querySelector('.c-gauge__range').textContent).toEqual(`${gaugeViewObject.configuration.gaugeController.min} ${gaugeViewObject.configuration.gaugeController.max}`); expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toEqual(`${gaugeViewObject.configuration.gaugeController.min} ${gaugeViewObject.configuration.gaugeController.max}`);
}); });
it('renders correct current value', (done) => { it('renders correct current value', (done) => {
function WatchUpdateValue() { function WatchUpdateValue() {
const textElement = gaugeHolder.querySelector('.c-gauge__curval-text'); const textElement = gaugeHolder.querySelector('.js-dial-current-value');
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision)); expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
done(); done();
} }

View File

@ -32,7 +32,9 @@ export default function GaugeViewProvider(openmct) {
return domainObject.type === 'gauge'; return domainObject.type === 'gauge';
}, },
canEdit: function (domainObject) { canEdit: function (domainObject) {
return false; if (domainObject.type === 'gauge') {
return true;
}
}, },
view: function (domainObject) { view: function (domainObject) {
let component; let component;

View File

@ -21,168 +21,259 @@
*****************************************************************************/ *****************************************************************************/
<template> <template>
<div <div
class="c-gauge" class="c-gauge__wrapper js-gauge-wrapper"
:class="`c-gauge--${gaugeType}`" :class="`c-gauge--${gaugeType}`"
> >
<div class="c-gauge__wrapper"> <template v-if="typeDial">
<template v-if="typeDial"> <svg
<svg width="0"
class="c-gauge__range" height="0"
viewBox="0 0 512 512" class="c-dial__clip-paths"
> >
<text <defs>
v-if="displayMinMax" <clipPath
font-size="35" id="gaugeBgMask"
transform="translate(105 455) rotate(-45)" clipPathUnits="objectBoundingBox"
>{{ rangeLow }}</text> >
<text <path d="M0.853553 0.853553C0.944036 0.763071 1 0.638071 1 0.5C1 0.223858 0.776142 0 0.5 0C0.223858 0 0 0.223858 0 0.5C0 0.638071 0.0559644 0.763071 0.146447 0.853553L0.285934 0.714066C0.23115 0.659281 0.197266 0.583598 0.197266 0.5C0.197266 0.332804 0.332804 0.197266 0.5 0.197266C0.667196 0.197266 0.802734 0.332804 0.802734 0.5C0.802734 0.583598 0.76885 0.659281 0.714066 0.714066L0.853553 0.853553Z" />
v-if="displayMinMax" </clipPath>
font-size="35" <clipPath
transform="translate(407 455) rotate(45)" id="gaugeValueMask"
text-anchor="end" clipPathUnits="objectBoundingBox"
>{{ rangeHigh }}</text> >
</svg> <path d="M0.18926 0.81074C0.109735 0.731215 0.0605469 0.621351 0.0605469 0.5C0.0605469 0.257298 0.257298 0.0605469 0.5 0.0605469C0.742702 0.0605469 0.939453 0.257298 0.939453 0.5C0.939453 0.621351 0.890265 0.731215 0.81074 0.81074L0.714066 0.714066C0.76885 0.659281 0.802734 0.583599 0.802734 0.5C0.802734 0.332804 0.667196 0.197266 0.5 0.197266C0.332804 0.197266 0.197266 0.332804 0.197266 0.5C0.197266 0.583599 0.23115 0.659281 0.285934 0.714066L0.18926 0.81074Z" />
</clipPath>
</defs>
</svg>
<svg
class="c-dial__range c-gauge__range js-gauge-dial-range"
viewBox="0 0 512 512"
>
<text
v-if="displayMinMax"
font-size="35"
transform="translate(105 455) rotate(-45)"
>{{ rangeLow }}</text>
<text
v-if="displayMinMax"
font-size="35"
transform="translate(407 455) rotate(45)"
text-anchor="end"
>{{ rangeHigh }}</text>
</svg>
<svg
class="c-dial__current-value-text-wrapper"
viewBox="0 0 512 512"
>
<svg <svg
v-if="displayCurVal" v-if="displayCurVal"
class="c-gauge__curval" class="c-dial__current-value-text-sizer"
:viewBox="curValViewBox" :viewBox="curValViewBox"
> >
<text <text
class="c-gauge__curval-text" class="c-dial__current-value-text js-dial-current-value"
lengthAdjust="spacing" lengthAdjust="spacing"
text-anchor="middle" text-anchor="middle"
style="transform: translate(50%, 70%)"
>{{ curVal }}</text> >{{ curVal }}</text>
</svg> </svg>
</svg>
<div class="c-dial"> <svg
<svg class="c-dial__bg"
class="c-dial__bg" viewBox="0 0 10 10"
viewBox="0 0 512 512" >
>
<path d="M256,0C114.6,0,0,114.6,0,256S114.6,512,256,512,512,397.4,512,256,397.4,0,256,0Zm0,412A156,156,0,1,1,412,256,155.9,155.9,0,0,1,256,412Z" />
</svg>
<svg <g
v-if="limitHigh && dialHighLimitDeg < 270" v-if="limitLow !== null && dialLowLimitDeg < getLimitDegree('low', 'max')"
class="c-dial__limit-high" class="c-dial__limit-low"
viewBox="0 0 512 512" :style="`transform: rotate(${dialLowLimitDeg}deg)`"
:class="{ >
'c-high-limit-clip--90': dialHighLimitDeg > 90, <rect
'c-high-limit-clip--180': dialHighLimitDeg >= 180 v-if="dialLowLimitDeg >= getLimitDegree('low', 'q1')"
}" class="c-dial__low-limit__low"
> x="5"
<path y="5"
d="M100,256A156,156,0,1,1,366.3,366.3L437,437a255.2,255.2,0,0,0,75-181C512,114.6,397.4,0,256,0S0,114.6,0,256A255.2,255.2,0,0,0,75,437l70.7-70.7A155.5,155.5,0,0,1,100,256Z" width="5"
:style="`transform: rotate(${dialHighLimitDeg}deg)`" height="5"
/> />
</svg> <rect
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q2')"
class="c-dial__low-limit__mid"
x="5"
y="0"
width="5"
height="5"
/>
<rect
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q3')"
class="c-dial__low-limit__high"
x="0"
y="0"
width="5"
height="5"
/>
</g>
<svg <g
v-if="limitLow && dialLowLimitDeg < 270" v-if="limitHigh !== null && dialHighLimitDeg < getLimitDegree('high', 'max')"
class="c-dial__limit-low" class="c-dial__limit-high"
viewBox="0 0 512 512" :style="`transform: rotate(${dialHighLimitDeg}deg)`"
:class="{ >
'c-dial-clip--90': dialLowLimitDeg < 90, <rect
'c-dial-clip--180': dialLowLimitDeg >= 90 && dialLowLimitDeg < 180 v-if="dialHighLimitDeg <= getLimitDegree('high', 'max')"
}" class="c-dial__high-limit__low"
> x="0"
<path y="5"
d="M256,100c86.2,0,156,69.8,156,156s-69.8,156-156,156c-43.1,0-82.1-17.5-110.3-45.7L75,437 c46.3,46.3,110.3,75,181,75c141.4,0,256-114.6,256-256S397.4,0,256,0C185.3,0,121.3,28.7,75,75l70.7,70.7 C173.9,117.5,212.9,100,256,100z" width="5"
:style="`transform: rotate(${dialLowLimitDeg}deg)`" height="5"
/> />
</svg> <rect
v-if="dialHighLimitDeg <= getLimitDegree('high', 'q2')"
class="c-dial__high-limit__mid"
x="0"
y="0"
width="5"
height="5"
/>
<rect
v-if="dialHighLimitDeg <= getLimitDegree('high', 'q3')"
class="c-dial__high-limit__high"
x="5"
y="0"
width="5"
height="5"
/>
</g>
</svg>
<svg <svg
class="c-dial__value" v-if="typeFilledDial"
viewBox="0 0 512 512" class="c-dial__filled-value-wrapper"
:class="{ viewBox="0 0 10 10"
'c-dial-clip--90': degValue < 90 && typeFilledDial, >
'c-dial-clip--180': degValue >= 90 && degValue < 180 && typeFilledDial <g
}" class="c-dial__filled-value"
> :style="`transform: rotate(${degValueFilledDial}deg)`"
<path >
v-if="typeFilledDial && degValue > 0" <rect
d="M256,31A224.3,224.3,0,0,0,98.3,95.5l48.4,49.2a156,156,0,1,1-1,221.6L96.9,415.1A224.4,224.4,0,0,0,256,481c124.3,0,225-100.7,225-225S380.3,31,256,31Z" v-if="degValue >= getLimitDegree('low', 'q1')"
:style="`transform: rotate(${degValue}deg)`" class="c-dial__filled-value__low"
/> x="5"
<path y="5"
v-if="typeNeedleDial && valueInBounds" width="5"
d="M256,86c-93.9,0-170,76.1-170,170c0,43.9,16.6,83.9,43.9,114.1l-38.7,38.7c-3.3,3.3-3.3,8.7,0,12s8.7,3.3,12,0 l38.7-38.7C172.1,409.4,212.1,426,256,426c93.9,0,170-76.1,170-170S349.9,86,256,86z M256,411.7c-86,0-155.7-69.7-155.7-155.7 S170,100.3,256,100.3S411.7,170,411.7,256S342,411.7,256,411.7z" height="5"
:style="`transform: rotate(${degValue}deg)`" />
/> <rect
</svg> v-if="degValue >= getLimitDegree('low', 'q2')"
class="c-dial__filled-value__mid"
x="5"
y="0"
width="5"
height="5"
/>
<rect
v-if="degValue >= getLimitDegree('low', 'q3')"
class="c-dial__filled-value__high"
x="0"
y="0"
width="5"
height="5"
/>
</g>
</svg>
<svg
v-if="valueInBounds && typeNeedleDial"
class="c-dial__needle-value-wrapper"
viewBox="0 0 10 10"
>
<g
class="c-dial__needle-value"
:style="`transform: rotate(${degValue}deg)`"
>
<path d="M4.90234 9.39453L5.09766 9.39453L5.30146 8.20874C6.93993 8.05674 8.22265 6.67817 8.22266 5C8.22266 3.22018 6.77982 1.77734 5 1.77734C3.22018 1.77734 1.77734 3.22018 1.77734 5C1.77734 6.67817 3.06007 8.05674 4.69854 8.20874L4.90234 9.39453Z" />
</g>
</svg>
</template>
<template v-if="typeMeter">
<div class="c-meter">
<div
v-if="displayMinMax"
class="c-gauge__range c-meter__range js-gauge-meter-range"
>
<div class="c-meter__range__high">{{ rangeHigh }}</div>
<div class="c-meter__range__low">{{ rangeLow }}</div>
</div> </div>
</template> <div class="c-meter__bg">
<template v-if="typeMeterVertical">
<div
class="c-meter__value"
:style="`transform: translateY(${meterValueToPerc}%)`"
></div>
<template v-if="typeMeter"> <div
<div class="c-meter"> v-if="limitHigh !== null && meterHighLimitPerc > 0"
<div class="c-meter__limit-high"
v-if="displayMinMax" :style="`height: ${meterHighLimitPerc}%`"
class="c-gauge__range c-meter__range" ></div>
<div
v-if="limitLow !== null && meterLowLimitPerc > 0"
class="c-meter__limit-low"
:style="`height: ${meterLowLimitPerc}%`"
></div>
</template>
<template v-if="typeMeterHorizontal">
<div
class="c-meter__value"
:style="`transform: translateX(${meterValueToPerc * -1}%)`"
></div>
<div
v-if="limitHigh !== null && meterHighLimitPerc > 0"
class="c-meter__limit-high"
:style="`width: ${meterHighLimitPerc}%`"
></div>
<div
v-if="limitLow !== null && meterLowLimitPerc > 0"
class="c-meter__limit-low"
:style="`width: ${meterLowLimitPerc}%`"
></div>
</template>
<svg
class="c-meter__current-value-text-wrapper"
viewBox="0 0 512 512"
> >
<div class="c-meter__range__high">{{ rangeHigh }}</div>
<div class="c-meter__range__low">{{ rangeLow }}</div>
</div>
<div class="c-meter__bg">
<template v-if="typeMeterVertical">
<div
class="c-meter__value"
:style="`transform: translateY(${meterValueToPerc}%)`"
></div>
<div
v-if="limitHigh && meterHighLimitPerc > 0"
class="c-meter__limit-high"
:style="`height: ${meterHighLimitPerc}%`"
></div>
<div
v-if="limitLow && meterLowLimitPerc > 0"
class="c-meter__limit-low"
:style="`height: ${meterLowLimitPerc}%`"
></div>
</template>
<template v-if="typeMeterHorizontal">
<div
class="c-meter__value"
:style="`transform: translateX(${meterValueToPerc * -1}%)`"
></div>
<div
v-if="limitHigh && meterHighLimitPerc > 0"
class="c-meter__limit-high"
:style="`width: ${meterHighLimitPerc}%`"
></div>
<div
v-if="limitLow && meterLowLimitPerc > 0"
class="c-meter__limit-low"
:style="`width: ${meterLowLimitPerc}%`"
></div>
</template>
<svg <svg
v-if="displayCurVal" v-if="displayCurVal"
class="c-gauge__curval" class="c-meter__current-value-text-sizer"
:viewBox="curValViewBox" :viewBox="curValViewBox"
preserveAspectRatio="xMidYMid meet" preserveAspectRatio="xMidYMid meet"
> >
<text <text
class="c-gauge__curval-text" class="c-dial__current-value-text js-meter-current-value"
text-anchor="middle"
lengthAdjust="spacing" lengthAdjust="spacing"
text-anchor="middle"
style="transform: translate(50%, 70%)"
>{{ curVal }}</text> >{{ curVal }}</text>
</svg> </svg>
</div> </svg>
</div> </div>
</template> </div>
</div> </template>
</div> </div>
</template> </template>
<script> <script>
import { DIAL_VALUE_DEG_OFFSET, getLimitDegree } from '../gauge-limit-util';
const LIMIT_PADDING_IN_PERCENT = 10; const LIMIT_PADDING_IN_PERCENT = 10;
export default { export default {
@ -209,6 +300,13 @@ export default {
degValue() { degValue() {
return this.percentToDegrees(this.valToPercent(this.curVal)); return this.percentToDegrees(this.valToPercent(this.curVal));
}, },
degValueFilledDial() {
if (this.curVal > this.rangeHigh) {
return this.percentToDegrees(100);
}
return this.percentToDegrees(this.valToPercent(this.curVal));
},
dialHighLimitDeg() { dialHighLimitDeg() {
return this.percentToDegrees(this.valToPercent(this.limitHigh)); return this.percentToDegrees(this.valToPercent(this.limitHigh));
}, },
@ -299,6 +397,7 @@ export default {
this.openmct.time.off('timeSystem', this.setTimeSystem); this.openmct.time.off('timeSystem', this.setTimeSystem);
}, },
methods: { methods: {
getLimitDegree: getLimitDegree,
addTelemetryObjectAndSubscribe(domainObject) { addTelemetryObjectAndSubscribe(domainObject) {
this.telemetryObject = domainObject; this.telemetryObject = domainObject;
this.request(); this.request();
@ -340,7 +439,7 @@ export default {
return this.gaugeType.indexOf(str) !== -1; return this.gaugeType.indexOf(str) !== -1;
}, },
percentToDegrees(vPercent) { percentToDegrees(vPercent) {
return this.round((vPercent / 100) * 270, 2); return this.round(((vPercent / 100) * 270) + DIAL_VALUE_DEG_OFFSET, 2);
}, },
removeFromComposition(telemetryObject = this.telemetryObject) { removeFromComposition(telemetryObject = this.telemetryObject) {
let composition = this.domainObject.composition.filter(id => let composition = this.domainObject.composition.filter(id =>
@ -453,7 +552,7 @@ export default {
valToPercent(vValue) { valToPercent(vValue) {
// Used by dial // Used by dial
if (vValue >= this.rangeHigh && this.typeFilledDial) { if (vValue >= this.rangeHigh && this.typeFilledDial) {
// Don't peg at 100% if the gaugeType isn't a filled shape // For filled dial, clip values over the high range to prevent over-rotation
return 100; return 100;
} }

View File

@ -27,6 +27,7 @@
:class="model.cssClass" :class="model.cssClass"
> >
<ToggleSwitch <ToggleSwitch
:id="'gaugeToggle'"
:checked="isUseTelemetryLimits" :checked="isUseTelemetryLimits"
label="Use telemetry limits for minimum and maximum ranges" label="Use telemetry limits for minimum and maximum ranges"
@change="toggleUseTelemetryLimits" @change="toggleUseTelemetryLimits"

View File

@ -0,0 +1,39 @@
const GAUGE_LIMITS = {
q1: 0,
q2: 90,
q3: 180,
q4: 270
};
export const DIAL_VALUE_DEG_OFFSET = 45;
// type: low, high
// quadrant: low, mid, high, max
export function getLimitDegree(type, quadrant) {
if (quadrant === 'max') {
return GAUGE_LIMITS.q4 + DIAL_VALUE_DEG_OFFSET;
}
return type === 'low'
? getLowLimitDegree(quadrant)
: getHighLimitDegree(quadrant)
;
}
function getLowLimitDegree(quadrant) {
return GAUGE_LIMITS[quadrant] + DIAL_VALUE_DEG_OFFSET;
}
function getHighLimitDegree(quadrant) {
if (quadrant === 'q1') {
return GAUGE_LIMITS.q4 + DIAL_VALUE_DEG_OFFSET;
}
if (quadrant === 'q2') {
return GAUGE_LIMITS.q3 + DIAL_VALUE_DEG_OFFSET;
}
if (quadrant === 'q3') {
return GAUGE_LIMITS.q2 + DIAL_VALUE_DEG_OFFSET;
}
}

View File

@ -1,9 +1,3 @@
$dialClip: polygon(0 0, 100% 0, 100% 100%, 50% 50%, 0 100%);
$dialClip90: polygon(0 0, 50% 50%, 0 100%);
$dialClip180: polygon(0 0, 100% 0, 0 100%);
$limitHighClip90: polygon(0 0, 100% 0, 100% 100%);
$limitHighClip180: polygon(100% 0, 100% 100%, 0 100%);
.is-object-type-gauge { .is-object-type-gauge {
overflow: hidden; overflow: hidden;
} }
@ -13,10 +7,8 @@ $limitHighClip180: polygon(100% 0, 100% 100%, 0 100%);
&.invalid, &.invalid,
&.invalid.req { @include validationState($glyph-icon-x, $colorFormInvalid); } &.invalid.req { @include validationState($glyph-icon-x, $colorFormInvalid); }
&.valid, &.valid,
&.valid.req { @include validationState($glyph-icon-check, $colorFormValid); } &.valid.req { @include validationState($glyph-icon-check, $colorFormValid); }
&.req { @include validationState($glyph-icon-asterisk, $colorFormRequired); } &.req { @include validationState($glyph-icon-asterisk, $colorFormRequired); }
} }
@ -37,92 +29,47 @@ $limitHighClip180: polygon(100% 0, 100% 100%, 0 100%);
@include abs(); @include abs();
overflow: hidden; overflow: hidden;
} }
svg {
path {
transform-origin: center;
}
&.c-gauge__curval {
@include abs();
fill: $colorGaugeTextValue;
position: absolute;
width: 100%;
height: 100%;
z-index: 2;
.c-gauge__curval-text {
font-family: $heroFont;
transform: translate(50%, 75%);
}
}
}
&[class*='dial'] {
// Square aspect ratio
width: 100%;
padding-bottom: 100%;
}
&[class*='meter'] {
@include abs();
}
} }
/********************************************** DIAL GAUGE */ /********************************************** DIAL GAUGE */
.c-dial { svg[class*='c-dial'] {
// Dial elements max-height: 100%;
@include abs(); max-width: 100%;
clip-path: $dialClip; position: absolute;
svg, g {
&__ticks, transform-origin: center;
&__bg,
&[class*='__limit'],
&__value {
@include abs();
}
.c-high-limit-clip--90 {
clip-path: $limitHighClip90;
}
.c-high-limit-clip--180 {
clip-path: $limitHighClip180;
}
&__limit-high path { fill: $colorGaugeLimitHigh; }
&__limit-low path { fill: $colorGaugeLimitLow; }
&__value,
&__limit-low {
&.c-dial-clip--90 {
clip-path: $dialClip90;
}
&.c-dial-clip--180 {
clip-path: $dialClip180;
}
}
&__value {
path,
polygon {
fill: $colorGaugeValue;
}
}
&__bg {
path {
fill: $colorGaugeBg;
}
} }
} }
.c-gauge--dial-needle .c-dial__value { .c-dial {
path { &__bg {
background: $colorGaugeBg;
clip-path: url(#gaugeBgMask);
}
&__limit-high rect { fill: $colorGaugeLimitHigh; }
&__limit-low rect { fill: $colorGaugeLimitLow; }
&__filled-value-wrapper {
clip-path: url(#gaugeValueMask);
}
&__needle-value-wrapper {
clip-path: url(#gaugeValueMask);
}
&__filled-value { fill: $colorGaugeValue; }
&__needle-value {
fill: $colorGaugeValue;
transition: transform $transitionTimeGauge; transition: transform $transitionTimeGauge;
} }
&__current-value-text {
fill: $colorGaugeTextValue;
font-family: $heroFont;
}
} }
/********************************************** METER GAUGE */ /********************************************** METER GAUGE */
@ -131,6 +78,13 @@ $limitHighClip180: polygon(100% 0, 100% 100%, 0 100%);
@include abs(); @include abs();
display: flex; display: flex;
svg {
// current-value-text
position: absolute;
height: 100%;
width: 100%;
}
&__range { &__range {
display: flex; display: flex;
flex: 0 0 auto; flex: 0 0 auto;

View File

@ -143,13 +143,13 @@
<!-- spacecraft position fresh --> <!-- spacecraft position fresh -->
<div <div
v-if="relatedTelemetry.hasRelatedTelemetry && isSpacecraftPositionFresh" v-if="relatedTelemetry.hasRelatedTelemetry && isSpacecraftPositionFresh"
class="c-imagery__age icon-check c-imagery--new" class="c-imagery__age icon-check c-imagery--new no-animation"
>POS</div> >POS</div>
<!-- camera position fresh --> <!-- camera position fresh -->
<div <div
v-if="relatedTelemetry.hasRelatedTelemetry && isCameraPositionFresh" v-if="relatedTelemetry.hasRelatedTelemetry && isCameraPositionFresh"
class="c-imagery__age icon-check c-imagery--new" class="c-imagery__age icon-check c-imagery--new no-animation"
>CAM</div> >CAM</div>
</div> </div>
<div class="h-local-controls"> <div class="h-local-controls">
@ -331,6 +331,16 @@ export default {
}, },
isImageNew() { isImageNew() {
let cutoff = FIVE_MINUTES; let cutoff = FIVE_MINUTES;
if (this.imageFreshnessOptions) {
const { fadeOutDelayTime, fadeOutDurationTime} = this.imageFreshnessOptions;
// convert css duration to IS8601 format for parsing
const isoFormattedDuration = 'PT' + fadeOutDurationTime.toUpperCase();
const isoFormattedDelay = 'PT' + fadeOutDelayTime.toUpperCase();
const parsedDuration = moment.duration(isoFormattedDuration).asMilliseconds();
const parsedDelay = moment.duration(isoFormattedDelay).asMilliseconds();
cutoff = parsedDuration + parsedDelay;
}
let age = this.numericDuration; let age = this.numericDuration;
return age < cutoff && !this.refreshCSS; return age < cutoff && !this.refreshCSS;
@ -514,6 +524,8 @@ export default {
if (!this.isPaused) { if (!this.isPaused) {
this.setFocusedImage(imageIndex); this.setFocusedImage(imageIndex);
this.scrollToRight(); this.scrollToRight();
} else {
this.scrollToFocused();
} }
}, },
deep: true deep: true
@ -977,6 +989,7 @@ export default {
this.setSizedImageDimensions(); this.setSizedImageDimensions();
this.calculateViewHeight(); this.calculateViewHeight();
this.scrollToFocused();
}, },
setSizedImageDimensions() { setSizedImageDimensions() {
this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight; this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;

View File

@ -1,3 +1,5 @@
@use 'sass:math';
@keyframes fade-out { @keyframes fade-out {
from { from {
background-color: rgba($colorOk, 0.5); background-color: rgba($colorOk, 0.5);
@ -138,6 +140,9 @@
animation-timing-function: ease-in; animation-timing-function: ease-in;
animation-iteration-count: 1; animation-iteration-count: 1;
animation-fill-mode: forwards; animation-fill-mode: forwards;
&.no-animation {
animation: none;
}
} }
@ -211,7 +216,7 @@
} }
.c-thumb { .c-thumb {
$w: $imageThumbsD / 2; $w: math.div($imageThumbsD, 2);
min-width: $w; min-width: $w;
width: $w; width: $w;

View File

@ -204,6 +204,8 @@ export default {
updateTicks(forceRegeneration = false) { updateTicks(forceRegeneration = false) {
const range = this.axis.get('displayRange'); const range = this.axis.get('displayRange');
const logMode = this.axis.get('logMode');
if (!range) { if (!range) {
delete this.min; delete this.min;
delete this.max; delete this.max;
@ -231,7 +233,7 @@ export default {
step: newTicks[1] - newTicks[0] step: newTicks[1] - newTicks[0]
}; };
newTicks = getFormattedTicks(newTicks, format); newTicks = getFormattedTicks(newTicks, format, logMode);
this.ticks = newTicks; this.ticks = newTicks;
this.shouldCheckWidth = true; this.shouldCheckWidth = true;

View File

@ -215,6 +215,10 @@ export default class YAxisModel extends Model {
const _range = this.get('displayRange'); const _range = this.get('displayRange');
if (!_range) {
return;
}
if (this.get('logMode')) { if (this.get('logMode')) {
_range.min = antisymlog(_range.min, 10); _range.min = antisymlog(_range.min, 10);
_range.max = antisymlog(_range.max, 10); _range.max = antisymlog(_range.max, 10);

View File

@ -132,12 +132,6 @@ export default {
} }
} }
}); });
if (this.object && this.object.type === 'conditionWidget' && keys.includes('output')) {
this.openmct.objects.mutate(this.object, 'conditionalLabel', styleObj.output);
} else {
this.openmct.objects.mutate(this.object, 'conditionalLabel', '');
}
} }
} }
}; };

View File

@ -131,12 +131,17 @@ export function commonSuffix(a, b) {
return a.slice(a.length - breakpoint); return a.slice(a.length - breakpoint);
} }
export function getFormattedTicks(newTicks, format) { export function getFormattedTicks(newTicks, format, formatFloat) {
newTicks = newTicks newTicks = newTicks
.map(function (tickValue) { .map(function (tickValue) {
let formattedValue = format(tickValue);
if (formatFloat === true && typeof formattedValue === 'number' && !Number.isInteger(formattedValue)) {
formattedValue = parseFloat(formattedValue).toFixed(2);
}
return { return {
value: tickValue, value: tickValue,
text: format(tickValue) text: formattedValue
}; };
}); });

View File

@ -127,6 +127,10 @@ export default {
}, },
methods: { methods: {
clear() { clear() {
if (this.domainObject) {
this.openmct.styleManager.delete(this.domainObject.identifier);
}
if (this.currentView) { if (this.currentView) {
this.currentView.destroy(); this.currentView.destroy();
if (this.$refs.objectViewWrapper) { if (this.$refs.objectViewWrapper) {
@ -213,12 +217,6 @@ export default {
} }
} }
}); });
if (this.domainObject && this.domainObject.type === 'conditionWidget' && keys.includes('output')) {
this.openmct.objects.mutate(this.domainObject, 'conditionalLabel', styleObj.output);
} else {
this.openmct.objects.mutate(this.domainObject, 'conditionalLabel', '');
}
}, },
updateView(immediatelySelect) { updateView(immediatelySelect) {
this.clear(); this.clear();
@ -310,8 +308,10 @@ export default {
this.initObjectStyles(); this.initObjectStyles();
}, },
initObjectStyles() { initObjectStyles() {
this.styleRuleManager = this.openmct.styleManager.get(this.domainObject.identifier);
if (!this.styleRuleManager) { if (!this.styleRuleManager) {
this.styleRuleManager = new StyleRuleManager((this.domainObject.configuration && this.domainObject.configuration.objectStyles), this.openmct, this.updateStyle.bind(this), true); this.styleRuleManager = new StyleRuleManager((this.domainObject.configuration && this.domainObject.configuration.objectStyles), this.openmct, this.updateStyle.bind(this), true);
this.openmct.styleManager.set(this.domainObject.identifier, this.styleRuleManager);
} else { } else {
this.styleRuleManager.updateObjectStyleConfig(this.domainObject.configuration && this.domainObject.configuration.objectStyles); this.styleRuleManager.updateObjectStyleConfig(this.domainObject.configuration && this.domainObject.configuration.objectStyles);
} }