Compare commits

...

25 Commits

Author SHA1 Message Date
ab7e2c5747 Change version to 1.7.8 2021-10-19 12:36:25 -07:00
4d0487631b Fix plots dup requests (#4324)
* If there is a pending create request for an id, queue a duplicate request.
* Add time context to imagery for time strip
* Revert previous fix for plots duplicate requests.
Instead, check for timeContext before loading series data on load. Also, pass in start and end timestamps since plots could be using an independent time context
* Adds test for fix
2021-10-15 11:47:40 -07:00
8b53618adb Remove additional request to load plots when mounted. (#4314)
* If there is a pending create request for an id, queue a duplicate request.
* Add time context to imagery for time strip
* Remove duplicate request
2021-10-13 15:18:41 -07:00
364a97d8b7 Mct4177 fix for telemetry endpoints with '.' in the key (#4308)
* use quotes to avoid telmetery ids with dots screwing up mutate
* added more tests
* Save domain object after telemetry configuration has been removed

Co-authored-by: Joshi <simplyrender@gmail.com>
2021-10-13 13:51:44 -07:00
de5da9445b Add braintree sanitize url lib and sanitize form urls (#4296) 2021-10-07 12:21:56 -07:00
53d63d69fe Duplicate Request Fixes (#4295)
normalizing start/end dates for pan/zoom in conductor to prevent decimal timestamps that cause 2 bounds events
removed un-needed submit input that was causing "random" refreshes on conductor inputs
moving parseInt to scale method
2021-10-06 11:59:36 -07:00
c5350659e2 Stacked plots need to align the Y axis (#4286)
adjust style for y axis text
Remove watcher for plotWidth
2021-10-06 11:57:04 -07:00
76830b24ac Bar graph style nullcheck (#4291)
When a new telemetry endpoint is added to a bar graph, initialize colors correctly.
2021-10-06 11:51:11 -07:00
99e90c7488 added icon for inspector (#4275)
added icons for inspector for object type, alias
2021-10-05 21:44:22 -07:00
2c4fb4cb9d Show clicked image in large view (#4280) 2021-10-05 15:38:34 -07:00
c91b9f7825 Bargraph time metadata should consider 'source' (#4289) 2021-10-05 15:02:20 -07:00
166211a8be doing the easy thing first (#4278) 2021-10-05 14:39:17 -07:00
c4e41d784a [Telemetry Collection] No duplicate requests on load (#4274)
* Do not request telemetry twice on load
2021-09-30 14:44:50 -07:00
35eceea793 Resume plot if no pan, zoom, or drag action is taken (#4138) (#4256) 2021-09-30 14:22:43 -07:00
903a44fd80 Fix plot zoom when child of time strip (#4272) 2021-09-30 10:17:00 -07:00
3c546a0a1f Mct4196 - Fixes Conditional Styling not being applied when editing a Condition Widget (#4255)
* destroy style manager when clearing object view
2021-09-30 06:58:48 -07:00
b6786b2be3 Bar graph review comment fixes (#4232)
Addresses review comments and cleans up code for bar graphs
2021-09-29 14:27:45 -07:00
4e13b3ff43 Fix Bar Graph related CSS (#4270)
- Cleaned up commenting;
- Removed suppression of 0 line;
2021-09-29 12:52:36 -07:00
c20369d9bf Equal stacked plot y widths for all it's sub-plots (#4267) 2021-09-29 11:01:21 -07:00
f58cd4b9ce Imagery views should follow time context (#4264)
* If there is a pending create request for an id, queue a duplicate request.
* Add time context to imagery for time strip
* Fix tests
2021-09-28 12:43:38 -07:00
476128ced8 Condition sets only persist if actively editing (#4262) 2021-09-28 12:43:14 -07:00
22c812d67d [Table/Collection Fixes] Clearing correctly, no mutating options, no duplicate requests (#4261)
* do not process undefined data safety net
* using initiateHistoricalRequests anytime we make new reqeusts, so options are handle correctly, previously dates were being reused
* passing request options into request method
* removing form submit button from time conductor fixed inputs form
* moving both request functions into one, as they are both done each time
* fixing case where very last datum if duplicate was not being checked correctly
2021-09-28 12:38:57 -07:00
648f3532c1 Remove alert styling and hide pause button if in Fixed Time mode. (#4263) 2021-09-28 09:57:00 -07:00
4fe44a5619 [Telemetry Table] Progress bar tests (#4249)
* telemetry table progress bar tests
2021-09-28 07:28:30 -07:00
d221610df9 Flexible Layouts display Condition Sets as their editing/browsing interface (#4179)
* Flexible Layouts display Condition Sets as their editing/browsing interface #4141

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-09-28 07:19:24 -07:00
71 changed files with 1622 additions and 1644 deletions

View File

@ -118,100 +118,6 @@ define([
}
}
]
},
'example.spectral-generator': {
values: [
{
key: "name",
name: "Name",
format: "string"
},
{
key: "utc",
name: "Time",
format: "utc",
hints: {
domain: 1
}
},
{
key: "wavelength",
name: "Wavelength",
unit: "Hz",
formatString: '%0.2f',
hints: {
domain: 2,
spectralAttribute: true
}
},
{
key: "cos",
name: "Cosine",
unit: "deg",
formatString: '%0.2f',
hints: {
range: 2,
spectralAttribute: true
}
}
]
},
'example.spectral-aggregate-generator': {
values: [
{
key: "name",
name: "Name",
format: "string"
},
{
key: "utc",
name: "Time",
format: "utc",
hints: {
domain: 1
}
},
{
key: "ch1",
name: "Channel 1",
format: "string",
hints: {
range: 1
}
},
{
key: "ch2",
name: "Channel 2",
format: "string",
hints: {
range: 2
}
},
{
key: "ch3",
name: "Channel 3",
format: "string",
hints: {
range: 3
}
},
{
key: "ch4",
name: "Channel 4",
format: "string",
hints: {
range: 4
}
},
{
key: "ch5",
name: "Channel 5",
format: "string",
hints: {
range: 5
}
}
]
}
};

View File

@ -1,86 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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.
*****************************************************************************/
define([
], function (
) {
function SpectralAggregateGeneratorProvider() {
}
function pointForTimestamp(timestamp, count, name) {
return {
name: name,
utc: String(Math.floor(timestamp / count) * count),
ch1: String(Math.floor(timestamp / count) % 1),
ch2: String(Math.floor(timestamp / count) % 2),
ch3: String(Math.floor(timestamp / count) % 3),
ch4: String(Math.floor(timestamp / count) % 4),
ch5: String(Math.floor(timestamp / count) % 5)
};
}
SpectralAggregateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) {
return domainObject.type === 'example.spectral-aggregate-generator';
};
SpectralAggregateGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
var count = 5000;
var interval = setInterval(function () {
var now = Date.now();
var datum = pointForTimestamp(now, count, domainObject.name);
callback(datum);
}, count);
return function () {
clearInterval(interval);
};
};
SpectralAggregateGeneratorProvider.prototype.supportsRequest = function (domainObject, options) {
return domainObject.type === 'example.spectral-aggregate-generator';
};
SpectralAggregateGeneratorProvider.prototype.request = function (domainObject, options) {
var start = options.start;
var end = Math.min(Date.now(), options.end); // no future values
var count = 5000;
if (options.strategy === 'latest' || options.size === 1) {
start = end;
}
var data = [];
while (start <= end && data.length < 5000) {
data.push(pointForTimestamp(start, count, domainObject.name));
start += count;
}
return Promise.resolve(data);
};
return SpectralAggregateGeneratorProvider;
});

View File

@ -1,102 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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.
*****************************************************************************/
define([
'./WorkerInterface'
], function (
WorkerInterface
) {
var REQUEST_DEFAULTS = {
amplitude: 1,
wavelength: 1,
period: 10,
offset: 0,
dataRateInHz: 1,
randomness: 0,
phase: 0
};
function SpectralGeneratorProvider() {
this.workerInterface = new WorkerInterface();
}
SpectralGeneratorProvider.prototype.canProvideTelemetry = function (domainObject) {
return domainObject.type === 'example.spectral-generator';
};
SpectralGeneratorProvider.prototype.supportsRequest =
SpectralGeneratorProvider.prototype.supportsSubscribe =
SpectralGeneratorProvider.prototype.canProvideTelemetry;
SpectralGeneratorProvider.prototype.makeWorkerRequest = function (domainObject, request = {}) {
var props = [
'amplitude',
'wavelength',
'period',
'offset',
'dataRateInHz',
'phase',
'randomness'
];
var workerRequest = {};
props.forEach(function (prop) {
if (domainObject.telemetry && Object.prototype.hasOwnProperty.call(domainObject.telemetry, prop)) {
workerRequest[prop] = domainObject.telemetry[prop];
}
if (request && Object.prototype.hasOwnProperty.call(request, prop)) {
workerRequest[prop] = request[prop];
}
if (!Object.prototype.hasOwnProperty.call(workerRequest, prop)) {
workerRequest[prop] = REQUEST_DEFAULTS[prop];
}
workerRequest[prop] = Number(workerRequest[prop]);
});
workerRequest.name = domainObject.name;
return workerRequest;
};
SpectralGeneratorProvider.prototype.request = function (domainObject, request) {
var workerRequest = this.makeWorkerRequest(domainObject, request);
workerRequest.start = request.start;
workerRequest.end = request.end;
workerRequest.spectra = true;
return this.workerInterface.request(workerRequest);
};
SpectralGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
var workerRequest = this.makeWorkerRequest(domainObject, {});
workerRequest.spectra = true;
return this.workerInterface.subscribe(workerRequest, callback);
};
return SpectralGeneratorProvider;
});

View File

@ -24,15 +24,11 @@ define([
"./GeneratorProvider",
"./SinewaveLimitProvider",
"./StateGeneratorProvider",
"./SpectralGeneratorProvider",
"./SpectralAggregateGeneratorProvider",
"./GeneratorMetadataProvider"
], function (
GeneratorProvider,
SinewaveLimitProvider,
StateGeneratorProvider,
SpectralGeneratorProvider,
SpectralAggregateGeneratorProvider,
GeneratorMetadataProvider
) {
@ -65,37 +61,6 @@ define([
openmct.telemetry.addProvider(new StateGeneratorProvider());
openmct.types.addType("example.spectral-generator", {
name: "Spectral Generator",
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
cssClass: "icon-generator-telemetry",
creatable: true,
initialize: function (object) {
object.telemetry = {
period: 10,
amplitude: 1,
wavelength: 1,
frequency: 1,
offset: 0,
dataRateInHz: 1,
phase: 0,
randomness: 0
};
}
});
openmct.telemetry.addProvider(new SpectralGeneratorProvider());
openmct.types.addType("example.spectral-aggregate-generator", {
name: "Spectral Aggregate Generator",
description: "For development use. Generates example streaming telemetry data using a simple state algorithm.",
cssClass: "icon-generator-telemetry",
creatable: true,
initialize: function (object) {
object.telemetry = {};
}
});
openmct.telemetry.addProvider(new SpectralAggregateGeneratorProvider());
openmct.types.addType("generator", {
name: "Sine Wave Generator",
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",

View File

@ -1,8 +1,9 @@
{
"name": "openmct",
"version": "1.7.8-SNAPSHOT",
"version": "1.7.8",
"description": "The Open MCT core platform",
"devDependencies": {
"@braintree/sanitize-url": "^5.0.2",
"angular": ">=1.8.0",
"angular-route": "1.4.14",
"babel-eslint": "10.0.3",

View File

@ -263,6 +263,7 @@ define([
// Plugins that are installed by default
this.install(this.plugins.Plot());
this.install(this.plugins.Chart());
this.install(this.plugins.TelemetryTable.default());
this.install(PreviewPlugin.default());
this.install(LegacyIndicatorsPlugin());

View File

@ -49,7 +49,6 @@ export class TelemetryCollection extends EventEmitter {
this.parseTime = undefined;
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
this.unsubscribe = undefined;
this.historicalProvider = undefined;
this.options = options;
this.pageState = undefined;
this.lastBounds = undefined;
@ -65,13 +64,13 @@ export class TelemetryCollection extends EventEmitter {
this._error(ERRORS.LOADED);
}
this._timeSystem(this.openmct.time.timeSystem());
this._setTimeSystem(this.openmct.time.timeSystem());
this.lastBounds = this.openmct.time.bounds();
this._watchBounds();
this._watchTimeSystem();
this._initiateHistoricalRequests();
this._requestHistoricalTelemetry();
this._initiateSubscriptionTelemetry();
this.loaded = true;
@ -103,31 +102,25 @@ export class TelemetryCollection extends EventEmitter {
return this.boundedTelemetry;
}
/**
* Sets up the telemetry collection for historical requests,
* this uses the "standardizeRequestOptions" from Telemetry API
* @private
*/
_initiateHistoricalRequests() {
this.openmct.telemetry.standardizeRequestOptions(this.options);
this.historicalProvider = this.openmct.telemetry.
findRequestProvider(this.domainObject, this.options);
this._requestHistoricalTelemetry();
}
/**
* If a historical provider exists, then historical requests will be made
* @private
*/
async _requestHistoricalTelemetry() {
if (!this.historicalProvider) {
let options = { ...this.options };
let historicalProvider;
this.openmct.telemetry.standardizeRequestOptions(options);
historicalProvider = this.openmct.telemetry.
findRequestProvider(this.domainObject, options);
if (!historicalProvider) {
return;
}
let historicalData;
this.options.onPartialResponse = this._processNewTelemetry.bind(this);
options.onPartialResponse = this._processNewTelemetry.bind(this);
try {
if (this.requestAbort) {
@ -135,9 +128,9 @@ export class TelemetryCollection extends EventEmitter {
}
this.requestAbort = new AbortController();
this.options.signal = this.requestAbort.signal;
options.signal = this.requestAbort.signal;
this.emit('requestStarted');
historicalData = await this.historicalProvider.request(this.domainObject, this.options);
historicalData = await historicalProvider.request(this.domainObject, options);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Error requesting telemetry data...');
@ -179,6 +172,10 @@ export class TelemetryCollection extends EventEmitter {
* @private
*/
_processNewTelemetry(telemetryData) {
if (telemetryData === undefined) {
return;
}
let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
let parsedValue;
let beforeStartOfBounds;
@ -205,9 +202,10 @@ export class TelemetryCollection extends EventEmitter {
if (endIndex > startIndex) {
let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex);
isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum));
}
} else if (startIndex === this.boundedTelemetry.length) {
isDuplicate = _.isEqual(datum, this.boundedTelemetry[this.boundedTelemetry.length - 1]);
}
if (!isDuplicate) {
@ -323,7 +321,7 @@ export class TelemetryCollection extends EventEmitter {
* Time System
* @private
*/
_timeSystem(timeSystem) {
_setTimeSystem(timeSystem) {
let domains = this.metadata.valuesForHints(['domain']);
let domain = domains.find((d) => d.key === timeSystem.key);
@ -339,7 +337,10 @@ export class TelemetryCollection extends EventEmitter {
this.parseTime = (datum) => {
return valueFormatter.parse(datum);
};
}
_setTimeSystemAndFetchData(timeSystem) {
this._setTimeSystem(timeSystem);
this._reset();
}
@ -376,19 +377,19 @@ export class TelemetryCollection extends EventEmitter {
}
/**
* adds the _timeSystem callback to the 'timeSystem' timeAPI listener
* adds the _setTimeSystemAndFetchData callback to the 'timeSystem' timeAPI listener
* @private
*/
_watchTimeSystem() {
this.openmct.time.on('timeSystem', this._timeSystem, this);
this.openmct.time.on('timeSystem', this._setTimeSystemAndFetchData, this);
}
/**
* removes the _timeSystem callback from the 'timeSystem' timeAPI listener
* removes the _setTimeSystemAndFetchData callback from the 'timeSystem' timeAPI listener
* @private
*/
_unwatchTimeSystem() {
this.openmct.time.off('timeSystem', this._timeSystem, this);
this.openmct.time.off('timeSystem', this._setTimeSystemAndFetchData, this);
}
/**

View File

@ -172,7 +172,7 @@ class TimeAPI extends GlobalTimeContext {
* @memberof module:openmct.TimeAPI#
* @method getContextForView
*/
getContextForView(objectPath) {
getContextForView(objectPath = []) {
let timeContext = this;
objectPath.forEach(item => {

View File

@ -23,30 +23,26 @@
import { BAR_GRAPH_KEY } from './BarGraphConstants';
export default function BarGraphCompositionPolicy(openmct) {
function hasAggregateDomainAndRange(metadata) {
function hasRange(metadata) {
const rangeValues = metadata.valuesForHints(['range']);
return rangeValues.length > 0;
}
function hasBarGraphTelemetry(domainObject) {
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
if (!openmct.telemetry.isTelemetryObject(domainObject)) {
return false;
}
let metadata = openmct.telemetry.getMetadata(domainObject);
return metadata.values().length > 0 && hasAggregateDomainAndRange(metadata);
}
function hasNoChildren(parentObject) {
return parentObject.composition && parentObject.composition.length < 1;
return metadata.values().length > 0 && hasRange(metadata);
}
return {
allow: function (parent, child) {
if ((parent.type === BAR_GRAPH_KEY)
&& ((child.type !== 'telemetry.plot.overlay') && (hasBarGraphTelemetry(child) === false))
&& (hasBarGraphTelemetry(child) === false)
) {
return false;
}

View File

@ -1,5 +1,3 @@
export const BAR_GRAPH_VIEW = 'bar-graph.view';
export const BAR_GRAPH_KEY = 'telemetry.plot.bar-graph';
export const BAR_GRAPH_INSPECTOR_KEY = 'telemetry.plot.bar-graph.inspector';
export const SUBSCRIBE = 'subscribe';
export const UNSUBSCRIBE = 'unsubscribe';

View File

@ -12,6 +12,7 @@
</div>
<div ref="plot"
class="c-bar-chart"
@plotly_relayout="zoom"
></div>
<div v-if="false"
ref="localControl"
@ -28,8 +29,7 @@
</div>
</template>
<script>
import Plotly from 'plotly.js-basic-dist';
import { SUBSCRIBE, UNSUBSCRIBE } from './BarGraphConstants';
import Plotly from 'plotly-basic';
const MULTI_AXES_X_PADDING_PERCENT = {
LEFT: 8,
@ -79,8 +79,6 @@ export default {
this.registerListeners();
},
beforeDestroy() {
this.$refs.plot.removeAllListeners();
if (this.plotResizeObserver) {
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
clearTimeout(this.resizeTimer);
@ -139,8 +137,8 @@ export default {
getYAxisMeta() {
const yAxisMeta = {};
this.data.forEach(d => {
const yAxisMetadata = d.yAxisMetadata;
this.data.forEach(datum => {
const yAxisMetadata = datum.yAxisMetadata;
const range = '1';
const side = 'left';
const name = '';
@ -203,8 +201,6 @@ export default {
return yaxis;
},
registerListeners() {
this.$refs.plot.on('plotly_relayout', this.zoom);
this.removeBarColorListener = this.openmct.objects.observe(
this.domainObject,
'configuration.barStyles',
@ -226,17 +222,17 @@ export default {
this.updatePlot();
this.isZoomed = false;
this.$emit(SUBSCRIBE);
this.$emit('subscribe');
},
barColorChanged() {
const colors = [];
const indices = [];
this.data.forEach((item, index) => {
const key = item.key;
const color = this.domainObject.configuration.barStyles[key] && this.domainObject.configuration.barStyles[key].color;
const colorExists = this.domainObject.configuration.barStyles.series[key] && this.domainObject.configuration.barStyles.series[key].color;
indices.push(index);
if (color) {
colors.push();
if (colorExists) {
colors.push(this.domainObject.configuration.barStyles.series[key].color);
} else {
colors.push(item.marker.color);
}
@ -285,7 +281,7 @@ export default {
}
this.isZoomed = true;
this.$emit(UNSUBSCRIBE);
this.$emit('unsubscribe');
}
}
};

View File

@ -25,33 +25,31 @@
class="c-plot c-bar-chart-view"
:data="trace"
:plot-axis-title="plotAxisTitle"
@subscribe="subscribeToAll"
@unsubscribe="removeAllSubscriptions"
/>
</template>
<script>
import * as SPECTRAL_AGGREGATE from './BarGraphConstants';
import ColorPalette from '../lib/ColorPalette';
import BarGraph from './BarGraphPlot.vue';
import Color from "@/plugins/plot/lib/Color";
import _ from 'lodash';
export default {
components: {
BarGraph
},
inject: ['openmct', 'domainObject'],
inject: ['openmct', 'domainObject', 'path'],
data() {
this.telemetryObjects = {};
this.telemetryObjectFormats = {};
this.subscriptions = [];
this.composition = {};
return {
composition: {},
currentDomainObject: this.domainObject,
subscriptions: [],
telemetryObjects: {},
trace: []
};
},
computed: {
activeClock() {
return this.openmct.time.activeClock;
},
plotAxisTitle() {
const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {};
const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : '';
@ -64,24 +62,14 @@ export default {
}
},
mounted() {
this.colorPalette = new ColorPalette();
this.loadComposition();
this.openmct.time.on('bounds', this.refreshData);
this.openmct.time.on('clock', this.clockChanged);
this.$refs.barGraph.$on(SPECTRAL_AGGREGATE.SUBSCRIBE, this.subscribeToAll);
this.$refs.barGraph.$on(SPECTRAL_AGGREGATE.UNSUBSCRIBE, this.removeAllSubscriptions);
this.unobserve = this.openmct.objects.observe(this.currentDomainObject, '*', this.updateDomainObject);
},
beforeDestroy() {
this.$refs.barGraph.$off();
this.openmct.time.off('bounds', this.refreshData);
this.openmct.time.off('clock', this.clockChanged);
this.removeAllSubscriptions();
this.unobserve();
if (!this.composition) {
return;
@ -92,35 +80,34 @@ export default {
},
methods: {
addTelemetryObject(telemetryObject) {
// grab information we need from the added telmetry object
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
this.telemetryObjects[key] = telemetryObject;
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
this.telemetryObjectFormats[key] = this.openmct.telemetry.getFormatMap(metadata);
const telemetryObjectPath = [telemetryObject, ...this.path];
const telemetryIsAlias = this.openmct.objects.isObjectPathToALink(telemetryObject, telemetryObjectPath);
if (!this.domainObject.configuration.barStyles) {
this.domainObject.configuration.barStyles = {};
// make an update object that's a clone of the existing styles object so we preserve existing choices
let stylesUpdate = {};
if (this.domainObject.configuration.barStyles.series[key]) {
stylesUpdate = _.clone(this.domainObject.configuration.barStyles.series[key]);
}
// check to see if we've set a bar color
if (!this.domainObject.configuration.barStyles[key] || !this.domainObject.configuration.barStyles[key].color) {
const color = this.colorPalette.getNextColor().asHexString();
this.domainObject.configuration.barStyles[key] = {
name: telemetryObject.name,
color
};
stylesUpdate.name = telemetryObject.name;
stylesUpdate.type = telemetryObject.type;
stylesUpdate.isAlias = telemetryIsAlias;
// if something has changed, mutate and notify listeners
if (!_.isEqual(stylesUpdate, this.domainObject.configuration.barStyles.series[key])) {
this.openmct.objects.mutate(
this.domainObject,
`configuration.barStyles[${key}]`,
this.domainObject.configuration.barStyles[key]
`configuration.barStyles.series["${key}"]`,
stylesUpdate
);
} else {
let color = this.domainObject.configuration.barStyles[key].color;
if (!(color instanceof Color)) {
color = Color.fromHexString(color);
}
this.colorPalette.remove(color);
}
this.telemetryObjects[key] = telemetryObject;
// ask for the current telemetry data, then subcribe for changes
this.requestDataFor(telemetryObject);
this.subscribeToObject(telemetryObject);
},
@ -144,10 +131,6 @@ export default {
this.trace = isInTrace ? newTrace : newTrace.concat([trace]);
},
clockChanged() {
this.removeAllSubscriptions();
this.subscribeToAll();
},
getAxisMetadata(telemetryObject) {
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
if (!metadata) {
@ -163,21 +146,19 @@ export default {
yAxisMetadata
};
},
getOptions(telemetryObject) {
getOptions() {
const { start, end } = this.openmct.time.bounds();
return {
end,
start,
startTime: null,
spectra: true
start
};
},
loadComposition() {
this.composition = this.openmct.composition.get(this.currentDomainObject);
this.composition = this.openmct.composition.get(this.domainObject);
if (!this.composition) {
this.addTelemetryObject(this.currentDomainObject);
this.addTelemetryObject(this.domainObject);
return;
}
@ -206,21 +187,34 @@ export default {
removeTelemetryObject(identifier) {
const key = this.openmct.objects.makeKeyString(identifier);
delete this.telemetryObjects[key];
if (this.domainObject.configuration.barStyles[key]) {
delete this.domainObject.configuration.barStyles[key];
if (this.telemetryObjectFormats && this.telemetryObjectFormats[key]) {
delete this.telemetryObjectFormats[key];
}
if (this.domainObject.configuration.barStyles.series[key]) {
delete this.domainObject.configuration.barStyles.series[key];
this.openmct.objects.mutate(
this.domainObject,
`configuration.barStyles.series["${key}"]`,
undefined
);
}
this.removeSubscription(key);
this.trace = this.trace.filter(t => t.key !== key);
},
processData(telemetryObject, data, axisMetadata) {
addDataToGraph(telemetryObject, data, axisMetadata) {
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (data.message) {
this.openmct.notifications.alert(data.message);
}
if (!this.isDataInTimeRange(data, key)) {
return;
}
let xValues = [];
let yValues = [];
@ -228,10 +222,10 @@ export default {
axisMetadata.xAxisMetadata.forEach((metadata) => {
xValues.push(metadata.name);
if (data[metadata.key]) {
//TODO: Format the data?
yValues.push(data[metadata.key]);
const formattedValue = this.format(key, metadata.key, data);
yValues.push(formattedValue);
} else {
yValues.push('');
yValues.push(null);
}
});
@ -245,19 +239,39 @@ export default {
yAxisMetadata: axisMetadata.yAxisMetadata,
type: 'bar',
marker: {
color: this.domainObject.configuration.barStyles[key].color
color: this.domainObject.configuration.barStyles.series[key].color
},
hoverinfo: 'skip'
};
this.addTrace(trace, key);
},
isDataInTimeRange(datum, key) {
const timeSystemKey = this.openmct.time.timeSystem().key;
let currentTimestamp = this.parse(key, timeSystemKey, datum);
return currentTimestamp && this.openmct.time.bounds().end >= currentTimestamp;
},
format(telemetryObjectKey, metadataKey, data) {
const formats = this.telemetryObjectFormats[telemetryObjectKey];
return formats[metadataKey].format(data);
},
parse(telemetryObjectKey, metadataKey, datum) {
if (!datum) {
return;
}
const formats = this.telemetryObjectFormats[telemetryObjectKey];
return formats[metadataKey].parse(datum);
},
requestDataFor(telemetryObject) {
const axisMetadata = this.getAxisMetadata(telemetryObject);
this.openmct.telemetry.request(telemetryObject, this.getOptions(telemetryObject))
this.openmct.telemetry.request(telemetryObject)
.then(data => {
data.forEach((datum) => {
this.processData(telemetryObject, datum, axisMetadata);
this.addDataToGraph(telemetryObject, datum, axisMetadata);
});
})
.catch((error) => {
@ -269,10 +283,10 @@ export default {
this.removeSubscription(key);
const options = this.getOptions(telemetryObject);
const options = this.getOptions();
const axisMetadata = this.getAxisMetadata(telemetryObject);
const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject,
data => this.processData(telemetryObject, data, axisMetadata)
data => this.addDataToGraph(telemetryObject, data, axisMetadata)
, options);
this.subscriptions.push({
@ -283,9 +297,6 @@ export default {
subscribeToAll() {
const telemetryObjects = Object.values(this.telemetryObjects);
telemetryObjects.forEach(this.subscribeToObject);
},
updateDomainObject(newDomainObject) {
this.currentDomainObject = newDomainObject;
}
}
};

View File

@ -26,12 +26,14 @@ import Vue from 'vue';
export default function BarGraphViewProvider(openmct) {
function isCompactView(objectPath) {
return objectPath.find(object => object.type === 'time-strip');
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
}
return {
key: BAR_GRAPH_VIEW,
name: 'Spectral Aggregate Plot',
name: 'Bar Graph',
cssClass: 'icon-telemetry',
canView(domainObject, objectPath) {
return domainObject && domainObject.type === BAR_GRAPH_KEY;
@ -54,7 +56,8 @@ export default function BarGraphViewProvider(openmct) {
},
provide: {
openmct,
domainObject
domainObject,
path: objectPath
},
data() {
return {

View File

@ -1,6 +1,6 @@
import { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY } from '../BarGraphConstants';
import Vue from 'vue';
import Options from "./Options.vue";
import BarGraphOptions from "./BarGraphOptions.vue";
export default function BarGraphInspectorViewProvider(openmct) {
return {
@ -24,13 +24,13 @@ export default function BarGraphInspectorViewProvider(openmct) {
component = new Vue({
el: element,
components: {
Options
BarGraphOptions
},
provide: {
openmct,
domainObject: selection[0][0].context.item
},
template: '<options></options>'
template: '<bar-graph-options></bar-graph-options>'
});
},
destroy: function () {

View File

@ -20,25 +20,31 @@
at runtime from the About dialog for additional information.
-->
<template>
<ul class="c-tree">
<ul class="c-tree c-bar-graph-options">
<h2 title="Display properties for this object">Bar Graph Series</h2>
<bar-graph-options v-for="series in domainObject.composition"
:key="series.key"
:item="series"
/>
<li v-for="series in domainObject.composition"
:key="series.key"
>
<series-options :item="series"
:color-palette="colorPalette"
/>
</li>
</ul>
</template>
<script>
import BarGraphOptions from "./BarGraphOptions.vue";
import SeriesOptions from "./SeriesOptions.vue";
import ColorPalette from '@/ui/color/ColorPalette';
export default {
components: {
BarGraphOptions
SeriesOptions
},
inject: ['openmct', 'domainObject'],
data() {
return {
isEditing: this.openmct.editor.isEditing()
isEditing: this.openmct.editor.isEditing(),
colorPalette: this.colorPalette
};
},
computed: {
@ -46,6 +52,9 @@ export default {
return this.isEditing && !this.domainObject.locked;
}
},
beforeMount() {
this.colorPalette = new ColorPalette();
},
mounted() {
this.openmct.editor.on('isEditing', this.setEditState);
},

View File

@ -21,13 +21,18 @@
-->
<template>
<ul>
<li class="c-tree__item menus-to-left">
<li class="c-tree__item menus-to-left"
:class="aliasCss"
>
<span class="c-disclosure-triangle is-enabled flex-elem"
:class="expandedCssClass"
@click="expanded = !expanded"
>
</span>
<div>
<div class="c-object-label">
<div :class="[seriesCss]">
</div>
<div class="c-object-label__name">{{ name }}</div>
</div>
</li>
@ -44,7 +49,8 @@
</template>
<script>
import ColorSwatch from '../../ColorSwatch.vue';
import ColorSwatch from '@/ui/color/ColorSwatch.vue';
import Color from "@/ui/color/Color";
export default {
components: {
@ -55,50 +61,90 @@ export default {
item: {
type: Object,
required: true
},
colorPalette: {
type: Object,
required: true
}
},
data() {
return {
currentColor: undefined,
name: '',
type: '',
isAlias: false,
expanded: false
};
},
computed: {
expandedCssClass() {
return this.expanded ? 'c-disclosure-triangle--expanded' : '';
},
seriesCss() {
const type = this.openmct.types.get(this.type);
if (type && type.definition && type.definition.cssClass) {
return `c-object-label__type-icon ${type.definition.cssClass}`;
}
return 'c-object-label__type-icon';
},
aliasCss() {
let cssClass = '';
if (this.isAlias) {
cssClass = 'is-alias';
}
return cssClass;
}
},
watch: {
item: {
handler() {
this.initColor();
this.initColorAndName();
},
deep: true
}
},
mounted() {
this.key = this.openmct.objects.makeKeyString(this.item);
this.initColor();
this.unObserve = this.openmct.objects.observe(this.domainObject, `this.domainObject.configuration.barStyles[${this.key}]`, this.initColor);
this.initColorAndName();
this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName);
},
beforeDestroy() {
if (this.unObserve) {
this.unObserve();
if (this.removeBarStylesListener) {
this.removeBarStylesListener();
}
},
methods: {
initColor() {
if (this.domainObject.configuration.barStyles && this.domainObject.configuration.barStyles[this.key]) {
this.currentColor = this.domainObject.configuration.barStyles[this.key].color;
this.name = this.domainObject.configuration.barStyles[this.key].name;
initColorAndName() {
// this is called before the plot is initialized
if (!this.domainObject.configuration.barStyles.series[this.key]) {
const color = this.colorPalette.getNextColor().asHexString();
this.domainObject.configuration.barStyles.series[this.key] = {
color,
type: '',
name: '',
isAlias: false
};
} else if (!this.domainObject.configuration.barStyles.series[this.key].color) {
this.domainObject.configuration.barStyles.series[this.key].color = this.colorPalette.getNextColor().asHexString();
}
this.currentColor = this.domainObject.configuration.barStyles.series[this.key].color;
this.name = this.domainObject.configuration.barStyles.series[this.key].name;
this.type = this.domainObject.configuration.barStyles.series[this.key].type;
this.isAlias = this.domainObject.configuration.barStyles.series[this.key].isAlias;
let colorHexString = this.currentColor;
const colorObject = Color.fromHexString(colorHexString);
this.colorPalette.remove(colorObject);
},
setColor(chosenColor) {
this.currentColor = chosenColor.asHexString();
this.openmct.objects.mutate(
this.domainObject,
`configuration.barStyles[${this.key}].color`,
`configuration.barStyles.series["${this.key}"].color`,
this.currentColor
);
}

View File

@ -0,0 +1,51 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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 { BAR_GRAPH_KEY } from './BarGraphConstants';
import BarGraphViewProvider from './BarGraphViewProvider';
import BarGraphInspectorViewProvider from './inspector/BarGraphInspectorViewProvider';
import BarGraphCompositionPolicy from './BarGraphCompositionPolicy';
export default function () {
return function install(openmct) {
openmct.types.addType(BAR_GRAPH_KEY, {
key: BAR_GRAPH_KEY,
name: "Bar Graph",
cssClass: "icon-bar-chart",
description: "View data as a bar graph. Can be added to Display Layouts.",
creatable: true,
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {
barStyles: { series: {} }
};
},
priority: 891
});
openmct.objectViews.addProvider(new BarGraphViewProvider(openmct));
openmct.inspectorViews.addProvider(new BarGraphInspectorViewProvider(openmct));
openmct.composition.addPolicy(new BarGraphCompositionPolicy(openmct).allow);
};
}

View File

@ -0,0 +1,486 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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 {createOpenMct, resetApplicationState, createMouseEvent} from "utils/testing";
import Vue from "vue";
import BarGraphPlugin from "./plugin";
import BarGraph from './BarGraphPlot.vue';
import EventEmitter from "EventEmitter";
import { BAR_GRAPH_VIEW, BAR_GRAPH_KEY, BAR_GRAPH_INSPECTOR_KEY } from './BarGraphConstants';
import BarGraphOptions from "./inspector/BarGraphOptions.vue";
describe("the plugin", function () {
let element;
let child;
let openmct;
let telemetryPromise;
let telemetryPromiseResolve;
let mockObjectPath;
beforeEach((done) => {
mockObjectPath = [
{
name: 'mock folder',
type: 'fake-folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
},
{
name: 'mock parent folder',
type: 'time-strip',
identifier: {
key: 'mock-parent-folder',
namespace: ''
}
}
];
const testTelemetry = [
{
'utc': 1,
'some-key': 'some-value 1',
'some-other-key': 'some-other-value 1'
},
{
'utc': 2,
'some-key': 'some-value 2',
'some-other-key': 'some-other-value 2'
},
{
'utc': 3,
'some-key': 'some-value 3',
'some-other-key': 'some-other-value 3'
}
];
openmct = createOpenMct();
telemetryPromise = new Promise((resolve) => {
telemetryPromiseResolve = resolve;
});
spyOn(openmct.telemetry, 'request').and.callFake(() => {
telemetryPromiseResolve(testTelemetry);
return telemetryPromise;
});
openmct.install(new BarGraphPlugin());
element = document.createElement("div");
element.style.width = "640px";
element.style.height = "480px";
child = document.createElement("div");
child.style.width = "640px";
child.style.height = "480px";
element.appendChild(child);
document.body.appendChild(element);
spyOn(window, 'ResizeObserver').and.returnValue({
observe() {},
unobserve() {},
disconnect() {}
});
openmct.time.timeSystem("utc", {
start: 0,
end: 4
});
openmct.types.addType("test-object", {
creatable: true
});
openmct.on("start", done);
openmct.startHeadless();
});
afterEach((done) => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
resetApplicationState(openmct).then(done).catch(done);
});
describe("The bar graph view", () => {
let testDomainObject;
let barGraphObject;
// eslint-disable-next-line no-unused-vars
let component;
let mockComposition;
beforeEach(async () => {
const getFunc = openmct.$injector.get;
spyOn(openmct.$injector, "get")
.withArgs("exportImageService").and.returnValue({
exportPNG: () => {},
exportJPG: () => {}
})
.and.callFake(getFunc);
barGraphObject = {
identifier: {
namespace: "",
key: "test-plot"
},
type: "telemetry.plot.bar-graph",
name: "Test Bar Graph"
};
testDomainObject = {
identifier: {
namespace: "",
key: "test-object"
},
configuration: {
barStyles: {
series: {}
}
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testDomainObject);
return [testDomainObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
let viewContainer = document.createElement("div");
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
BarGraph
},
provide: {
openmct: openmct,
domainObject: barGraphObject,
composition: openmct.composition.get(barGraphObject)
},
template: "<BarGraph></BarGraph>"
});
await Vue.nextTick();
});
it("provides a bar graph view", () => {
const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);
const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
expect(plotViewProvider).toBeDefined();
});
it("Renders plotly bar graph", () => {
let barChartElement = element.querySelectorAll(".plotly");
expect(barChartElement.length).toBe(1);
});
it("Handles dots in telemetry id", () => {
const dotFullTelemetryObject = {
identifier: {
namespace: "someNamespace",
key: "~OpenMCT~outer.test-object.foo.bar"
},
type: "test-dotful-object",
name: "A Dotful Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key.foo.name.45",
name: "Some dotful attribute",
hints: {
range: 1
}
}, {
key: "some-other-key.bar.344.rad",
name: "Another dotful attribute",
hints: {
range: 2
}
}]
}
};
const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);
const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
const barGraphView = plotViewProvider.view(testDomainObject, [testDomainObject]);
barGraphView.show(child, true);
expect(testDomainObject.configuration.barStyles.series["test-object"].name).toEqual("Test Object");
mockComposition.emit('add', dotFullTelemetryObject);
expect(testDomainObject.configuration.barStyles.series["someNamespace:~OpenMCT~outer.test-object.foo.bar"].name).toEqual("A Dotful Object");
barGraphView.destroy();
});
});
describe("the bar graph objects", () => {
const mockObject = {
name: 'A very nice bar graph',
key: BAR_GRAPH_KEY,
creatable: true
};
it('defines a bar graph object type with the correct key', () => {
const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition;
expect(objectDef.key).toEqual(mockObject.key);
});
it('is creatable', () => {
const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition;
expect(objectDef.creatable).toEqual(mockObject.creatable);
});
});
describe("The bar graph composition policy", () => {
it("allows composition for telemetry that contain at least one range", () => {
const parent = {
"composition": [],
"configuration": {},
"name": "Some Bar Graph",
"type": "telemetry.plot.bar-graph",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "some-key",
name: "Some attribute",
hints: {
domain: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 1
}
}]
}
};
const composition = openmct.composition.get(parent);
expect(() => {
composition.add(testTelemetryObject);
}).not.toThrow();
expect(parent.composition.length).toBe(1);
});
it("disallows composition for telemetry that don't contain any range hints", () => {
const parent = {
"composition": [],
"configuration": {},
"name": "Some Bar Graph",
"type": "telemetry.plot.bar-graph",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "some-key",
name: "Some attribute"
}, {
key: "some-other-key",
name: "Another attribute"
}]
}
};
const composition = openmct.composition.get(parent);
expect(() => {
composition.add(testTelemetryObject);
}).toThrow();
expect(parent.composition.length).toBe(0);
});
});
describe('the inspector view', () => {
let mockComposition;
let testDomainObject;
let selection;
let plotInspectorView;
let viewContainer;
let optionsElement;
beforeEach(async () => {
testDomainObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
selection = [
[
{
context: {
item: {
id: "test-object",
identifier: {
key: "test-object",
namespace: ''
},
type: "telemetry.plot.bar-graph",
configuration: {
barStyles: {
series: {
'~Some~foo.bar': {
name: 'A telemetry object',
type: 'some-type',
isAlias: true
}
}
}
},
composition: [
{
key: '~Some~foo.bar'
}
]
}
}
},
{
context: {
item: {
type: 'time-strip',
identifier: {
key: 'some-other-key',
namespace: ''
}
}
}
}
]
];
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testDomainObject);
return [testDomainObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
viewContainer = document.createElement('div');
child.append(viewContainer);
const applicableViews = openmct.inspectorViews.get(selection);
plotInspectorView = applicableViews[0];
plotInspectorView.show(viewContainer);
await Vue.nextTick();
optionsElement = element.querySelector('.c-bar-graph-options');
});
afterEach(() => {
plotInspectorView.destroy();
});
it('it renders the options', () => {
expect(optionsElement).toBeDefined();
});
it('shows the name', () => {
const seriesEl = optionsElement.querySelector('.c-object-label__name');
expect(seriesEl.innerHTML).toEqual('A telemetry object');
});
});
});

View File

@ -65,7 +65,7 @@ export default class Condition extends EventEmitter {
}
this.trigger = conditionConfiguration.configuration.trigger;
this.description = '';
this.summary = '';
}
updateResult(datum) {
@ -134,7 +134,6 @@ export default class Condition extends EventEmitter {
criterionConfigurations.forEach((criterionConfiguration) => {
this.addCriterion(criterionConfiguration);
});
this.updateDescription();
}
updateCriteria(criterionConfigurations) {
@ -146,7 +145,6 @@ export default class Condition extends EventEmitter {
this.criteria.forEach((criterion) => {
criterion.updateTelemetryObjects(this.conditionManager.telemetryObjects);
});
this.updateDescription();
}
/**
@ -200,7 +198,6 @@ export default class Condition extends EventEmitter {
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.off('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
this.criteria.splice(found.index, 1, newCriterion);
this.updateDescription();
}
}
@ -216,7 +213,6 @@ export default class Condition extends EventEmitter {
});
criterion.destroy();
this.criteria.splice(found.index, 1);
this.updateDescription();
return true;
}
@ -228,7 +224,6 @@ export default class Condition extends EventEmitter {
let found = this.findCriterion(criterion.id);
if (found) {
this.criteria[found.index] = criterion.data;
this.updateDescription();
}
}
@ -254,8 +249,7 @@ export default class Condition extends EventEmitter {
description = `${description} ${criterion.getDescription()} ${(index < this.criteria.length - 1) ? triggerDescription.conjunction : ''}`;
});
this.description = description;
this.conditionManager.updateConditionDescription(this);
this.summary = description;
}
getTriggerDescription() {

View File

@ -105,7 +105,14 @@ export default class ConditionManager extends EventEmitter {
}
updateConditionTelemetryObjects() {
this.conditions.forEach((condition) => condition.updateTelemetryObjects());
this.conditions.forEach((condition) => {
condition.updateTelemetryObjects();
let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === condition.id);
if (index > -1) {
//Only assign the summary, don't mutate the domain object
this.conditionSetDomainObject.configuration.conditionCollection[index].summary = this.updateConditionDescription(condition);
}
});
}
removeConditionTelemetryObjects() {
@ -139,10 +146,17 @@ export default class ConditionManager extends EventEmitter {
}
}
updateConditionDescription(condition) {
condition.updateDescription();
return condition.summary;
}
updateCondition(conditionConfiguration) {
let condition = this.findConditionById(conditionConfiguration.id);
if (condition) {
condition.update(conditionConfiguration);
conditionConfiguration.summary = this.updateConditionDescription(condition);
}
let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === conditionConfiguration.id);
@ -152,16 +166,10 @@ export default class ConditionManager extends EventEmitter {
}
}
updateConditionDescription(condition) {
const found = this.conditionSetDomainObject.configuration.conditionCollection.find(conditionConfiguration => (conditionConfiguration.id === condition.id));
if (found.summary !== condition.description) {
found.summary = condition.description;
this.persistConditions();
}
}
initCondition(conditionConfiguration, index) {
let condition = new Condition(conditionConfiguration, this.openmct, this);
conditionConfiguration.summary = this.updateConditionDescription(condition);
if (index !== undefined) {
this.conditions.splice(index + 1, 0, condition);
} else {

View File

@ -33,8 +33,10 @@ export default class ConditionSetViewProvider {
this.cssClass = 'icon-conditional';
}
canView(domainObject) {
return domainObject.type === 'conditionSet';
canView(domainObject, objectPath) {
const isConditionSet = domainObject.type === 'conditionSet';
return isConditionSet && this.openmct.router.isNavigatedObject(objectPath);
}
canEdit(domainObject) {

View File

@ -244,7 +244,7 @@ export default {
this.telemetryMetadataOptions = [];
telemetryObjects.forEach(telemetryObject => {
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
this.addMetaDataOptions(telemetryMetadata.values());
this.addMetaDataOptions(telemetryMetadata ? telemetryMetadata.values() : []);
});
this.updateOperations();
}

View File

@ -192,7 +192,11 @@ export default {
this.telemetry.forEach((telemetryObject) => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice();
if (telemetryMetadata) {
this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice();
} else {
this.telemetryMetadataOptions[id] = [];
}
});
},
addTestInput(testInput) {

View File

@ -177,7 +177,7 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
const timeSystem = this.openmct.time.timeSystem();
telemetryRequestsResults.forEach((results, index) => {
const latestDatum = results.length ? results[results.length - 1] : {};
const latestDatum = (Array.isArray(results) && results.length) ? results[results.length - 1] : {};
const datumId = keys[index];
const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObjects[datumId]);

View File

@ -167,6 +167,11 @@ export default class TelemetryCriterion extends EventEmitter {
id: this.id,
data: this.formatData(normalizedDatum)
};
}).catch((error) => {
return {
id: this.id,
data: this.formatData()
};
});
}

View File

@ -27,6 +27,7 @@ import StylesView from "./components/inspector/StylesView.vue";
import Vue from 'vue';
import {getApplicableStylesForItem} from "./utils/styleUtils";
import ConditionManager from "@/plugins/condition/ConditionManager";
import StyleRuleManager from "./StyleRuleManager";
describe('the plugin', function () {
let conditionSetDefinition;
@ -96,6 +97,8 @@ describe('the plugin', function () {
mockListener = jasmine.createSpy('mockListener');
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true);
conditionSetDefinition.initialize(mockConditionSetDomainObject);
spyOn(openmct.objects, "save").and.returnValue(Promise.resolve(true));
@ -128,21 +131,6 @@ describe('the plugin', function () {
expect(mockConditionSetDomainObject.composition instanceof Array).toBeTrue();
expect(mockConditionSetDomainObject.composition.length).toEqual(0);
});
it('provides a view', () => {
const testViewObject = {
id: "test-object",
type: "conditionSet",
configuration: {
conditionCollection: []
}
};
const applicableViews = openmct.objectViews.get(testViewObject, []);
let conditionSetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionSet.view');
expect(conditionSetView).toBeDefined();
});
});
describe('the condition set usage for multiple display layout items', () => {
@ -724,4 +712,123 @@ describe('the plugin', function () {
expect(result[2]).toBeUndefined();
});
});
describe('canView of ConditionSetViewProvider', () => {
let conditionSetView;
const testViewObject = {
id: "test-object",
type: "conditionSet",
configuration: {
conditionCollection: []
}
};
beforeEach(() => {
const applicableViews = openmct.objectViews.get(testViewObject, []);
conditionSetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionSet.view');
});
it('provides a view', () => {
expect(conditionSetView).toBeDefined();
});
it('returns true for type `conditionSet` and is a navigated Object', () => {
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true);
const isCanView = conditionSetView.canView(testViewObject, []);
expect(isCanView).toBe(true);
});
it('returns false for type `conditionSet` and is not a navigated Object', () => {
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false);
const isCanView = conditionSetView.canView(testViewObject, []);
expect(isCanView).toBe(false);
});
it('returns false for type `notConditionSet` and is a navigated Object', () => {
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true);
testViewObject.type = 'notConditionSet';
const isCanView = conditionSetView.canView(testViewObject, []);
expect(isCanView).toBe(false);
});
});
describe('The Style Rule Manager', () => {
it('should subscribe to the conditionSet after the editor saves', async () => {
const stylesObject = {
"styles": [
{
"conditionId": "a8bf7d1a-c1bb-4fc7-936a-62056a51b5cd",
"style": {
"backgroundColor": "#38761d",
"border": "",
"color": "#073763",
"isStyleInvisible": ""
}
},
{
"conditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e",
"style": {
"backgroundColor": "#980000",
"border": "",
"color": "#ff9900",
"isStyleInvisible": ""
}
}
],
"staticStyle": {
"style": {
"backgroundColor": "",
"border": "",
"color": ""
}
},
"selectedConditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e",
"defaultConditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e",
"conditionSetIdentifier": {
"namespace": "",
"key": "035c589c-d98f-429e-8b89-d76bd8d22b29"
}
};
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
const mockTransactionService = jasmine.createSpyObj(
'transactionService',
['commit']
);
openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', "subscribe", "getMetadata", "getValueFormatter", "request"]);
openmct.telemetry.isTelemetryObject.and.returnValue(true);
openmct.telemetry.subscribe.and.returnValue(function () {});
openmct.telemetry.getValueFormatter.and.returnValue({
parse: function (value) {
return value;
}
});
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
openmct.telemetry.request.and.returnValue(Promise.resolve([]));
mockTransactionService.commit = async () => {};
const mockIdentifierService = jasmine.createSpyObj(
'identifierService',
['parse']
);
mockIdentifierService.parse.and.returnValue({
getSpace: () => {
return '';
}
});
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
openmct.$injector.get.withArgs('identifierService').and.returnValue(mockIdentifierService)
.withArgs('transactionService').and.returnValue(mockTransactionService);
const styleRuleManger = new StyleRuleManager(stylesObject, openmct, null, true);
spyOn(styleRuleManger, 'subscribeToConditionSet');
await openmct.editor.save();
expect(styleRuleManger.subscribeToConditionSet).toHaveBeenCalledTimes(1);
});
});
});

View File

@ -23,7 +23,7 @@
<template>
<component :is="urlDefined ? 'a' : 'span'"
class="c-condition-widget u-style-receiver js-style-receiver"
:href="urlDefined ? internalDomainObject.url : null"
:href="url"
>
<div class="c-condition-widget__label">
{{ internalDomainObject.label }}
@ -32,6 +32,8 @@
</template>
<script>
const sanitizeUrl = require("@braintree/sanitize-url").sanitizeUrl;
export default {
inject: ['openmct', 'domainObject'],
data: function () {
@ -42,6 +44,9 @@ export default {
computed: {
urlDefined() {
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
},
url() {
return this.urlDefined ? sanitizeUrl(this.internalDomainObject.url) : null;
}
},
mounted() {

View File

@ -27,7 +27,7 @@
'c-hyperlink--button' : isButton
}"
:target="domainObject.linkTarget"
:href="domainObject.url"
:href="url"
>
<span class="c-hyperlink__label">{{ domainObject.displayText }}</span>
</a>
@ -35,6 +35,7 @@
</template>
<script>
const sanitizeUrl = require("@braintree/sanitize-url").sanitizeUrl;
export default {
inject: ['domainObject'],
@ -45,6 +46,9 @@ export default {
}
return true;
},
url() {
return sanitizeUrl(this.domainObject.url);
}
}
};

View File

@ -66,6 +66,10 @@ export default function ImageryTimestripViewProvider(openmct) {
destroy: function () {
component.$destroy();
component = undefined;
},
getComponent() {
return component;
}
};
}

View File

@ -10,7 +10,14 @@ export default class ImageryView {
this.component = undefined;
}
show(element) {
show(element, isEditing, viewOptions) {
let alternateObjectPath;
let indexForFocusedImage;
if (viewOptions) {
indexForFocusedImage = viewOptions.indexForFocusedImage;
alternateObjectPath = viewOptions.objectPath;
}
this.component = new Vue({
el: element,
components: {
@ -19,10 +26,15 @@ export default class ImageryView {
provide: {
openmct: this.openmct,
domainObject: this.domainObject,
objectPath: this.objectPath,
objectPath: alternateObjectPath || this.objectPath,
currentView: this
},
template: '<imagery-view ref="ImageryContainer"></imagery-view>'
data() {
return {
indexForFocusedImage
};
},
template: '<imagery-view :index-for-focused-image="indexForFocusedImage" ref="ImageryContainer"></imagery-view>'
});
}

View File

@ -41,7 +41,13 @@ import _ from "lodash";
const PADDING = 1;
const ROW_HEIGHT = 100;
const IMAGE_WIDTH_THRESHOLD = 40;
const IMAGE_SIZE = 85;
const IMAGE_WIDTH_THRESHOLD = 25;
const CONTAINER_CLASS = 'c-imagery-tsv-container';
const NO_ITEMS_CLASS = 'c-imagery-tsv__no-items';
const IMAGE_WRAPPER_CLASS = 'c-imagery-tsv__image-wrapper';
const ID_PREFIX = 'wrapper-';
const IMAGE_ID_PREFIX = 'image-';
export default {
mixins: [imageryData],
@ -78,10 +84,12 @@ export default {
this.canvasContext = this.canvas.getContext('2d');
this.setDimensions();
this.updateViewBounds();
this.setScaleAndPlotImagery = this.setScaleAndPlotImagery.bind(this);
this.updateViewBounds = this.updateViewBounds.bind(this);
this.setTimeContext = this.setTimeContext.bind(this);
this.setTimeContext();
this.openmct.time.on("timeSystem", this.setScaleAndPlotImagery);
this.openmct.time.on("bounds", this.updateViewBounds);
this.updateViewBounds();
this.resize = _.debounce(this.resize, 400);
this.imageryStripResizeObserver = new ResizeObserver(this.resize);
@ -90,25 +98,36 @@ export default {
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
},
beforeDestroy() {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
if (this.imageryStripResizeObserver) {
this.imageryStripResizeObserver.disconnect();
}
this.openmct.time.off("timeSystem", this.setScaleAndPlotImagery);
this.openmct.time.off("bounds", this.updateViewBounds);
this.stopFollowingTimeContext();
if (this.unlisten) {
this.unlisten();
}
},
methods: {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on("timeSystem", this.setScaleAndPlotImagery);
this.timeContext.on("bounds", this.updateViewBounds);
this.timeContext.on("timeContext", this.setTimeContext);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off("timeSystem", this.setScaleAndPlotImagery);
this.timeContext.off("bounds", this.updateViewBounds);
this.timeContext.off("timeContext", this.setTimeContext);
}
},
expand(index) {
const path = this.objectPath[0];
this.previewAction.invoke([path]);
this.previewAction.invoke([path], {
indexForFocusedImage: index,
objectPath: this.objectPath
});
},
observeForChanges(mutatedObject) {
this.updateViewBounds();
@ -134,14 +153,10 @@ export default {
return clientWidth;
},
updateViewBounds(bounds, isTick) {
this.viewBounds = this.openmct.time.bounds();
//Add a 50% padding to the end bounds to look ahead
let timespan = (this.viewBounds.end - this.viewBounds.start);
let padding = timespan / 2;
this.viewBounds.end = this.viewBounds.end + padding;
this.viewBounds = this.timeContext.bounds();
if (this.timeSystem === undefined) {
this.timeSystem = this.openmct.time.timeSystem();
this.timeSystem = this.timeContext.timeSystem();
}
this.setScaleAndPlotImagery(this.timeSystem, !isTick);
@ -172,18 +187,18 @@ export default {
},
clearPreviousImagery(clearAllImagery) {
//TODO: Only clear items that are out of bounds
let noItemsEl = this.$el.querySelectorAll(".c-imagery-tsv__no-items");
let noItemsEl = this.$el.querySelectorAll(`.${NO_ITEMS_CLASS}`);
noItemsEl.forEach(item => {
item.remove();
});
let imagery = this.$el.querySelectorAll(".c-imagery-tsv__image-wrapper");
let imagery = this.$el.querySelectorAll(`.${IMAGE_WRAPPER_CLASS}`);
imagery.forEach(item => {
if (clearAllImagery) {
item.remove();
} else {
const id = this.getNSAttributesForElement(item, 'id');
const id = item.getAttributeNS(null, 'id');
if (id) {
const timestamp = id.replace('id-', '');
const timestamp = id.replace(ID_PREFIX, '');
if (!this.isImageryInBounds({
time: timestamp
})) {
@ -205,7 +220,7 @@ export default {
}
if (timeSystem === undefined) {
timeSystem = this.openmct.time.timeSystem();
timeSystem = this.timeContext.timeSystem();
}
if (timeSystem.isUTCBased) {
@ -223,19 +238,17 @@ export default {
this.xScale.range([PADDING, this.width - PADDING * 2]);
},
isImageryInBounds(imageObj) {
return (imageObj.time < this.viewBounds.end) && (imageObj.time > this.viewBounds.start);
return (imageObj.time <= this.viewBounds.end) && (imageObj.time >= this.viewBounds.start);
},
getImageryContainer() {
let svgHeight = 100;
let svgWidth = this.imageHistory.length ? this.width : 200;
let groupSVG;
let containerHeight = 100;
let containerWidth = this.imageHistory.length ? this.width : 200;
let imageryContainer;
let existingSVG = this.$el.querySelector(".c-imagery-tsv__contents svg");
if (existingSVG) {
groupSVG = existingSVG;
this.setNSAttributesForElement(groupSVG, {
width: svgWidth
});
let existingContainer = this.$el.querySelector(`.${CONTAINER_CLASS}`);
if (existingContainer) {
imageryContainer = existingContainer;
imageryContainer.style.maxWidth = `${containerWidth}px`;
} else {
let component = new Vue({
components: {
@ -246,26 +259,20 @@ export default {
},
data() {
return {
isNested: true,
height: svgHeight,
width: svgWidth
isNested: true
};
},
template: `<swim-lane :is-nested="isNested" :hide-label="true"><template slot="object"><svg class="c-imagery-tsv-container" :height="height" :width="width"></svg></template></swim-lane>`
template: `<swim-lane :is-nested="isNested" :hide-label="true"><template slot="object"><div class="c-imagery-tsv-container"></div></template></swim-lane>`
});
this.$refs.imageryHolder.appendChild(component.$mount().$el);
groupSVG = component.$el.querySelector('svg');
groupSVG.addEventListener('mouseout', (event) => {
if (event.target.nodeName === 'svg' || event.target.nodeName === 'use') {
this.removeFromForeground();
}
});
imageryContainer = component.$el.querySelector(`.${CONTAINER_CLASS}`);
imageryContainer.style.maxWidth = `${containerWidth}px`;
imageryContainer.style.height = `${containerHeight}px`;
}
return groupSVG;
return imageryContainer;
},
isImageryWidthAcceptable() {
// We're calculating if there is enough space between images to show the thumbnails.
@ -281,194 +288,123 @@ export default {
return imageContainerWidth < IMAGE_WIDTH_THRESHOLD;
},
drawImagery() {
let groupSVG = this.getImageryContainer();
let imageryContainer = this.getImageryContainer();
const showImagePlaceholders = this.isImageryWidthAcceptable();
let index = 0;
if (this.imageHistory.length) {
this.imageHistory.forEach((currentImageObject, index) => {
this.imageHistory.forEach((currentImageObject) => {
if (this.isImageryInBounds(currentImageObject)) {
this.plotImagery(currentImageObject, showImagePlaceholders, groupSVG, index);
this.plotImagery(currentImageObject, showImagePlaceholders, imageryContainer, index);
index = index + 1;
}
});
} else {
this.plotNoItems(groupSVG);
this.plotNoItems(imageryContainer);
}
},
plotNoItems(svgElement) {
let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
this.setNSAttributesForElement(textElement, {
x: "10",
y: "20",
class: "c-imagery-tsv__no-items"
});
plotNoItems(containerElement) {
let textElement = document.createElement('text');
textElement.classList.add(NO_ITEMS_CLASS);
textElement.innerHTML = 'No images within timeframe';
svgElement.appendChild(textElement);
containerElement.appendChild(textElement);
},
setNSAttributesForElement(element, attributes) {
Object.keys(attributes).forEach((key) => {
if (key === 'url') {
element.setAttributeNS('http://www.w3.org/1999/xlink', 'href', attributes[key]);
} else {
element.setAttributeNS(null, key, attributes[key]);
}
});
},
getNSAttributesForElement(element, attribute) {
return element.getAttributeNS(null, attribute);
},
getImageWrapper(item) {
const id = `id-${item.time}`;
return this.$el.querySelector(`.c-imagery-tsv__contents g[id=${id}]`);
},
plotImagery(item, showImagePlaceholders, svgElement, index) {
//TODO: Placeholder image
let existingImageWrapper = this.getImageWrapper(item);
//imageWrapper wraps the vertical tick rect and the image
if (existingImageWrapper) {
this.updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders);
} else {
let imageWrapper = this.createImageWrapper(index, item, showImagePlaceholders, svgElement);
svgElement.appendChild(imageWrapper);
}
},
updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) {
//Update the x co-ordinates of the handle and image elements and the url of image
//this is to avoid tearing down all elements completely and re-drawing them
this.setNSAttributesForElement(existingImageWrapper, {
showImagePlaceholders
});
let imageTickElement = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-handle');
this.setNSAttributesForElement(imageTickElement, {
x: this.xScale(item.time)
});
let imageRect = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-placeholder');
this.setNSAttributesForElement(imageRect, {
x: this.xScale(item.time) + 2
});
let imageElement = existingImageWrapper.querySelector('image');
const selector = `href*=${existingImageWrapper.id}`;
let hoverEl = this.$el.querySelector(`.c-imagery-tsv__contents use[${selector}]`);
const hideImageUrl = (showImagePlaceholders && !hoverEl);
this.setNSAttributesForElement(imageElement, {
x: this.xScale(item.time) + 2,
url: hideImageUrl ? '' : item.url
});
},
createImageWrapper(index, item, showImagePlaceholders, svgElement) {
const id = `id-${item.time}`;
const imgSize = String(ROW_HEIGHT - 15);
let imageWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
this.setNSAttributesForElement(imageWrapper, {
id,
class: 'c-imagery-tsv__image-wrapper',
showImagePlaceholders
});
//create image tick indicator
let imageTickElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
this.setNSAttributesForElement(imageTickElement, {
class: 'c-imagery-tsv__image-handle',
x: this.xScale(item.time),
y: 5,
rx: 0,
width: 2,
height: String(ROW_HEIGHT - 10)
});
imageWrapper.appendChild(imageTickElement);
let imageRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
this.setNSAttributesForElement(imageRect, {
class: 'c-imagery-tsv__image-placeholder',
x: this.xScale(item.time) + 2,
y: 10,
rx: 0,
width: imgSize,
height: imgSize,
mask: `#image-${item.time}`
});
imageWrapper.appendChild(imageRect);
let imageElement = document.createElementNS('http://www.w3.org/2000/svg', 'image');
this.setNSAttributesForElement(imageElement, {
id: `image-${item.time}`,
x: this.xScale(item.time) + 2,
y: 10,
rx: 0,
width: imgSize,
height: imgSize,
url: showImagePlaceholders ? '' : item.url
});
imageWrapper.appendChild(imageElement);
//TODO: Don't add the hover listener if the width is too small
imageWrapper.addEventListener('mouseover', this.bringToForeground.bind(this, svgElement, imageWrapper, index, item.url));
return imageWrapper;
},
bringToForeground(svgElement, imageWrapper, index, url, event) {
const selector = `href*=${imageWrapper.id}`;
let hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use:not([${selector}])`);
if (hoverEls.length > 0) {
this.removeFromForeground(hoverEls);
}
hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use[${selector}]`);
if (hoverEls.length) {
if (!element) {
return;
}
let imageElement = imageWrapper.querySelector('image');
Object.keys(attributes).forEach((key) => {
element.setAttributeNS(null, key, attributes[key]);
});
},
setStyles(element, styles) {
if (!element) {
return;
}
Object.keys(styles).forEach((key) => {
element.style[key] = styles[key];
});
},
getImageWrapper(item) {
const id = `${ID_PREFIX}${item.time}`;
return this.$el.querySelector(`.c-imagery-tsv__contents div[id=${id}]`);
},
plotImagery(item, showImagePlaceholders, containerElement, index) {
let existingImageWrapper = this.getImageWrapper(item);
//imageWrapper wraps the vertical tick and the image
if (existingImageWrapper) {
this.updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders);
} else {
let imageWrapper = this.createImageWrapper(index, item, showImagePlaceholders);
containerElement.appendChild(imageWrapper);
}
},
setImageDisplay(imageElement, showImagePlaceholders) {
if (showImagePlaceholders) {
imageElement.style.display = 'none';
} else {
imageElement.style.display = 'block';
}
},
updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) {
//Update the x co-ordinates of the image wrapper and the url of image
//this is to avoid tearing down all elements completely and re-drawing them
this.setNSAttributesForElement(existingImageWrapper, {
'data-show-image-placeholders': showImagePlaceholders
});
existingImageWrapper.style.left = `${this.xScale(item.time)}px`;
let imageElement = existingImageWrapper.querySelector('img');
this.setNSAttributesForElement(imageElement, {
url: url,
fill: 'none'
});
let hoverElement = document.createElementNS('http://www.w3.org/2000/svg', 'use');
this.setNSAttributesForElement(hoverElement, {
class: 'image-highlight',
x: 0,
href: `#${imageWrapper.id}`
src: item.url
});
this.setImageDisplay(imageElement, showImagePlaceholders);
},
createImageWrapper(index, item, showImagePlaceholders) {
const id = `${ID_PREFIX}${item.time}`;
let imageWrapper = document.createElement('div');
imageWrapper.classList.add(IMAGE_WRAPPER_CLASS);
imageWrapper.style.left = `${this.xScale(item.time)}px`;
this.setNSAttributesForElement(imageWrapper, {
class: 'c-imagery-tsv__image-wrapper is-hovered'
id,
'data-show-image-placeholders': showImagePlaceholders
});
// We're using mousedown here and not 'click' because 'click' doesn't seem to be triggered reliably
hoverElement.addEventListener('mousedown', (e) => {
//create image vertical tick indicator
let imageTickElement = document.createElement('div');
imageTickElement.classList.add('c-imagery-tsv__image-handle');
imageTickElement.style.width = '2px';
imageTickElement.style.height = `${String(ROW_HEIGHT - 10)}px`;
imageWrapper.appendChild(imageTickElement);
//create placeholder - this will also hold the actual image
let imagePlaceholder = document.createElement('div');
imagePlaceholder.classList.add('c-imagery-tsv__image-placeholder');
imagePlaceholder.style.width = `${IMAGE_SIZE}px`;
imagePlaceholder.style.height = `${IMAGE_SIZE}px`;
imageWrapper.appendChild(imagePlaceholder);
//create image element
let imageElement = document.createElement('img');
this.setNSAttributesForElement(imageElement, {
src: item.url
});
imageElement.style.width = `${IMAGE_SIZE}px`;
imageElement.style.height = `${IMAGE_SIZE}px`;
this.setImageDisplay(imageElement, showImagePlaceholders);
//handle mousedown event to show the image in a large view
imageWrapper.addEventListener('mousedown', (e) => {
if (e.button === 0) {
this.expand(index);
}
});
svgElement.appendChild(hoverElement);
imagePlaceholder.appendChild(imageElement);
},
removeFromForeground(items) {
let hoverEls;
if (items) {
hoverEls = items;
} else {
hoverEls = this.$el.querySelectorAll(".c-imagery-tsv__contents use");
}
hoverEls.forEach(item => {
let selector = `id*=${this.getNSAttributesForElement(item, 'href').replace('#', '')}`;
let imageWrapper = this.$el.querySelector(`.c-imagery-tsv__contents g[${selector}]`);
this.setNSAttributesForElement(imageWrapper, {
class: 'c-imagery-tsv__image-wrapper'
});
let showImagePlaceholders = this.getNSAttributesForElement(imageWrapper, 'showImagePlaceholders');
if (showImagePlaceholders === 'true') {
let imageElement = imageWrapper.querySelector('image');
this.setNSAttributesForElement(imageElement, {
url: ''
});
}
item.remove();
});
return imageWrapper;
}
}
};

View File

@ -57,7 +57,7 @@
</div>
<div ref="imageBG"
class="c-imagery__main-image__bg"
:class="{'paused unnsynced': isPaused,'stale':false }"
:class="{'paused unnsynced': isPaused && !isFixed,'stale':false }"
@click="expand"
>
<div class="image-wrapper"
@ -122,6 +122,7 @@
</div>
<div class="h-local-controls">
<button
v-if="!isFixed"
class="c-button icon-pause pause-play"
:class="{'is-paused': isPaused}"
@click="paused(!isPaused, 'button')"
@ -131,7 +132,7 @@
</div>
<div class="c-imagery__thumbs-wrapper"
:class="[
{ 'is-paused': isPaused },
{ 'is-paused': isPaused && !isFixed },
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
]"
>
@ -199,6 +200,14 @@ export default {
},
mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
props: {
indexForFocusedImage: {
type: Number,
default() {
return undefined;
}
}
},
data() {
let timeSystem = this.openmct.time.timeSystem();
this.metadata = {};
@ -226,7 +235,8 @@ export default {
imageContainerWidth: undefined,
imageContainerHeight: undefined,
lockCompass: true,
resizingWindow: false
resizingWindow: false,
timeContext: undefined
};
},
computed: {
@ -258,7 +268,14 @@ export default {
return age < cutoff && !this.refreshCSS;
},
canTrackDuration() {
return this.openmct.time.clock() && this.timeSystem.isUTCBased;
let hasClock;
if (this.timeContext) {
hasClock = this.timeContext.clock();
} else {
hasClock = this.openmct.time.clock();
}
return hasClock && this.timeSystem.isUTCBased;
},
isNextDisabled() {
let disabled = false;
@ -379,11 +396,28 @@ export default {
}
return sizedImageDimensions;
},
isFixed() {
let clock;
if (this.timeContext) {
clock = this.timeContext.clock();
} else {
clock = this.openmct.time.clock();
}
return clock === undefined;
}
},
watch: {
imageHistorySize(newSize, oldSize) {
this.setFocusedImage(newSize - 1, false);
let imageIndex;
if (this.indexForFocusedImage !== undefined) {
imageIndex = this.initFocusedImageIndex;
} else {
imageIndex = newSize - 1;
}
this.setFocusedImage(imageIndex, false);
this.scrollToRight();
},
focusedImageIndex() {
@ -394,9 +428,14 @@ export default {
}
},
async mounted() {
//listen
this.openmct.time.on('timeSystem', this.trackDuration);
this.openmct.time.on('clock', this.trackDuration);
//We only need to use this till the user focuses an image manually
if (this.indexForFocusedImage !== undefined) {
this.initFocusedImageIndex = this.indexForFocusedImage;
this.isPaused = true;
}
this.setTimeContext = this.setTimeContext.bind(this);
this.setTimeContext();
// related telemetry keys
this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ'];
@ -432,8 +471,7 @@ export default {
},
beforeDestroy() {
this.openmct.time.off('timeSystem', this.trackDuration);
this.openmct.time.off('clock', this.trackDuration);
this.stopFollowingTimeContext();
if (this.thumbWrapperResizeObserver) {
this.thumbWrapperResizeObserver.disconnect();
@ -457,6 +495,21 @@ export default {
}
},
methods: {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
//listen
this.timeContext.on('timeSystem', this.trackDuration);
this.timeContext.on('clock', this.trackDuration);
this.timeContext.on("timeContext", this.setTimeContext);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off("timeSystem", this.trackDuration);
this.timeContext.off("clock", this.trackDuration);
this.timeContext.off("timeContext", this.setTimeContext);
}
},
expand() {
const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView);
const visibleActions = actionCollection.getVisibleActions();
@ -618,7 +671,12 @@ export default {
});
},
setFocusedImage(index, thumbnailClick = false) {
if (this.isPaused && !thumbnailClick) {
if (thumbnailClick) {
//We use the props till the user changes what they want to see
this.initFocusedImageIndex = undefined;
}
if (this.isPaused && !thumbnailClick && this.initFocusedImageIndex === undefined) {
this.nextImageIndex = index;
//this could happen if bounds changes
if (this.focusedImageIndex > this.imageHistory.length - 1) {
@ -649,8 +707,12 @@ export default {
window.clearInterval(this.durationTracker);
},
updateDuration() {
let currentTime = this.openmct.time.clock() && this.openmct.time.clock().currentValue();
this.numericDuration = currentTime - this.parsedSelectedTime;
let currentTime = this.timeContext.clock() && this.timeContext.clock().currentValue();
if (currentTime === undefined) {
this.numericDuration = currentTime;
} else {
this.numericDuration = currentTime - this.parsedSelectedTime;
}
},
resetAgeCSS() {
this.refreshCSS = true;

View File

@ -315,13 +315,31 @@
/*************************************** IMAGERY IN TIMESTRIP VIEWS */
.c-imagery-tsv {
g.c-imagery-tsv__image-wrapper {
div.c-imagery-tsv__image-wrapper {
cursor: pointer;
position: absolute;
top: 0;
display: flex;
z-index: 1;
margin-top: 5px;
img {
align-self: flex-end;
}
&:hover {
z-index: 2;
&.is-hovered {
filter: brightness(1) contrast(1) !important;
[class*='__image-handle'] {
fill: $colorBodyFg;
background-color: $colorBodyFg;
}
//[class*='__image-placeholder'] {
// display: none;
//}
img {
display: block !important;
}
}
}
@ -331,14 +349,16 @@
}
&__image-handle {
fill: rgba($colorBodyFg, 0.5);
background-color: rgba($colorBodyFg, 0.5);
}
&__image-placeholder {
fill: pushBack($colorBodyBg, 0.3);
background-color: pushBack($colorBodyBg, 0.3);
display: block;
align-self: flex-end;
}
&:hover g.c-imagery-tsv__image-wrapper {
&:hover div.c-imagery-tsv__image-wrapper {
// TODO CH: convert to theme constants
filter: brightness(0.5) contrast(0.7);
}

View File

@ -26,8 +26,10 @@ export default {
inject: ['openmct', 'domainObject', 'objectPath'],
mounted() {
// listen
this.openmct.time.on('bounds', this.boundsChange);
this.openmct.time.on('timeSystem', this.timeSystemChange);
this.boundsChange = this.boundsChange.bind(this);
this.timeSystemChange = this.timeSystemChange.bind(this);
this.setDataTimeContext = this.setDataTimeContext.bind(this);
this.setDataTimeContext();
// set
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
@ -51,10 +53,24 @@ export default {
delete this.unsubscribe;
}
this.openmct.time.off('bounds', this.boundsChange);
this.openmct.time.off('timeSystem', this.timeSystemChange);
this.stopFollowingDataTimeContext();
},
methods: {
setDataTimeContext() {
this.stopFollowingDataTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on('bounds', this.boundsChange);
this.boundsChange(this.timeContext.bounds());
this.timeContext.on('timeSystem', this.timeSystemChange);
this.timeContext.on("timeContext", this.setDataTimeContext);
},
stopFollowingDataTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.boundsChange);
this.timeContext.off('timeSystem', this.timeSystemChange);
this.timeContext.off("timeContext", this.setDataTimeContext);
}
},
datumIsNotValid(datum) {
if (this.imageHistory.length === 0) {
return false;
@ -111,7 +127,7 @@ export default {
}
},
async requestHistory() {
let bounds = this.openmct.time.bounds();
let bounds = this.timeContext.bounds();
this.requestCount++;
const requestId = this.requestCount;
this.imageHistory = [];
@ -132,7 +148,7 @@ export default {
}
},
timeSystemChange() {
this.timeSystem = this.openmct.time.timeSystem();
this.timeSystem = this.timeContext.timeSystem();
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
@ -141,7 +157,7 @@ export default {
this.unsubscribe = this.openmct.telemetry
.subscribe(this.domainObject, (datum) => {
let parsedTimestamp = this.parseTime(datum);
let bounds = this.openmct.time.bounds();
let bounds = this.timeContext.bounds();
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
let image = this.normalizeDatum(datum);

View File

@ -22,6 +22,7 @@
import Vue from 'vue';
import {
createMouseEvent,
createOpenMct,
resetApplicationState,
simulateKeyEvent
@ -32,19 +33,6 @@ const TEN_MINUTES = ONE_MINUTE * 10;
const MAIN_IMAGE_CLASS = '.js-imageryView-image';
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
const REFRESH_CSS_MS = 500;
// const TOLERANCE = 0.50;
// function comparisonFunction(valueOne, valueTwo) {
// let larger = valueOne;
// let smaller = valueTwo;
//
// if (larger < smaller) {
// larger = valueTwo;
// smaller = valueOne;
// }
//
// return (larger - smaller) < TOLERANCE;
// }
function getImageInfo(doc) {
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
@ -90,11 +78,13 @@ describe("The Imagery View Layouts", () => {
const START = Date.now();
const COUNT = 10;
let resolveFunction;
// let resolveFunction;
let originalRouterPath;
let telemetryPromise;
let telemetryPromiseResolve;
let cleanupFirst;
let openmct;
let appHolder;
let parent;
let child;
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
@ -198,44 +188,63 @@ describe("The Imagery View Layouts", () => {
// this setups up the app
beforeEach((done) => {
appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
cleanupFirst = [];
openmct = createOpenMct();
openmct.time.timeSystem('utc', {
start: START - (5 * ONE_MINUTE),
end: START + (5 * ONE_MINUTE)
});
openmct.install(openmct.plugins.MyItems());
openmct.install(openmct.plugins.LocalTimeSystem());
openmct.install(openmct.plugins.UTCTimeSystem());
telemetryPromise = new Promise((resolve) => {
telemetryPromiseResolve = resolve;
});
spyOn(openmct.telemetry, 'request').and.callFake(() => {
telemetryPromiseResolve(imageTelemetry);
return telemetryPromise;
});
parent = document.createElement('div');
child = document.createElement('div');
parent.appendChild(child);
parent.style.width = '640px';
parent.style.height = '480px';
// document.querySelector('body').append(parent);
child = document.createElement('div');
child.style.width = '640px';
child.style.height = '480px';
parent.appendChild(child);
document.body.appendChild(parent);
spyOn(window, 'ResizeObserver').and.returnValue({
observe() {},
disconnect() {}
});
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
// spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
originalRouterPath = openmct.router.path;
openmct.on('start', done);
openmct.start(appHolder);
openmct.startHeadless();
});
afterEach(() => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
afterEach((done) => {
openmct.router.path = originalRouterPath;
return resetApplicationState(openmct);
// Needs to be in a timeout because plots use a bunch of setTimeouts, some of which can resolve during or after
// teardown, which causes problems
// This is hacky, we should find a better approach here.
setTimeout(() => {
//Cleanup code that needs to happen before dom elements start being destroyed
cleanupFirst.forEach(cleanup => cleanup());
cleanupFirst = [];
document.body.removeChild(parent);
resetApplicationState(openmct).then(done).catch(done);
});
});
it("should provide an imagery time strip view when in a time strip", () => {
@ -262,7 +271,7 @@ describe("The Imagery View Layouts", () => {
});
it("should provide an imagery view only for imagery producing objects", () => {
let applicableViews = openmct.objectViews.get(imageryObject, []);
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]);
let imageryView = applicableViews.find(
viewProvider => viewProvider.key === imageryKey
);
@ -315,51 +324,53 @@ describe("The Imagery View Layouts", () => {
let imageryViewProvider;
let imageryView;
beforeEach(async () => {
let telemetryRequestResolve;
let telemetryRequestPromise = new Promise((resolve) => {
telemetryRequestResolve = resolve;
});
beforeEach(() => {
openmct.telemetry.request.and.callFake(() => {
telemetryRequestResolve(imageTelemetry);
return telemetryRequestPromise;
});
applicableViews = openmct.objectViews.get(imageryObject, []);
applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]);
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey);
imageryView = imageryViewProvider.view(imageryObject);
imageryView = imageryViewProvider.view(imageryObject, [imageryObject]);
imageryView.show(child);
await telemetryRequestPromise;
return Vue.nextTick();
});
afterEach(() => {
openmct.time.stopClock();
openmct.router.removeListener('change:hash', resolveFunction);
// afterEach(() => {
// openmct.time.stopClock();
// openmct.router.removeListener('change:hash', resolveFunction);
//
// imageryView.destroy();
// });
imageryView.destroy();
});
it("on mount should show the the most recent image", () => {
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
});
xit("should show the clicked thumbnail as the main image", (done) => {
const target = imageTelemetry[5].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
it("on mount should show the the most recent image", (done) => {
//Looks like we need Vue.nextTick here so that computed properties settle down
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
done();
});
});
it("should show the clicked thumbnail as the main image", (done) => {
//Looks like we need Vue.nextTick here so that computed properties settle down
Vue.nextTick(() => {
const target = imageTelemetry[5].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
done();
});
});
});
xit("should show that an image is new", (done) => {
openmct.time.clock('local', {
start: -1000,
end: 1000
});
Vue.nextTick(() => {
// used in code, need to wait to the 500ms here too
setTimeout(() => {
@ -385,69 +396,148 @@ describe("The Imagery View Layouts", () => {
});
});
xit("should navigate via arrow keys", (done) => {
let keyOpts = {
element: parent.querySelector('.c-imagery'),
key: 'ArrowLeft',
keyCode: 37,
type: 'keyup'
};
simulateKeyEvent(keyOpts);
it("should navigate via arrow keys", (done) => {
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
let keyOpts = {
element: parent.querySelector('.c-imagery'),
key: 'ArrowLeft',
keyCode: 37,
type: 'keyup'
};
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
done();
simulateKeyEvent(keyOpts);
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
done();
});
});
});
it("should navigate via numerous arrow keys", (done) => {
let element = parent.querySelector('.c-imagery');
let type = 'keyup';
let leftKeyOpts = {
element,
type,
key: 'ArrowLeft',
keyCode: 37
};
let rightKeyOpts = {
element,
type,
key: 'ArrowRight',
keyCode: 39
};
// left thrice
simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts);
// right once
simulateKeyEvent(rightKeyOpts);
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
let element = parent.querySelector('.c-imagery');
let type = 'keyup';
let leftKeyOpts = {
element,
type,
key: 'ArrowLeft',
keyCode: 37
};
let rightKeyOpts = {
element,
type,
key: 'ArrowRight',
keyCode: 39
};
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
// left thrice
simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts);
// right once
simulateKeyEvent(rightKeyOpts);
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
done();
});
});
});
it ('shows an auto scroll button when scroll to left', (done) => {
Vue.nextTick(() => {
// to mock what a scroll would do
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
Vue.nextTick(() => {
let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button');
expect(autoScrollButton).toBeTruthy();
done();
});
});
});
it ('scrollToRight is called when clicking on auto scroll button', (done) => {
Vue.nextTick(() => {
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
Vue.nextTick(() => {
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset');
done();
});
});
});
});
describe("imagery time strip view", () => {
let applicableViews;
let imageryViewProvider;
let imageryView;
let componentView;
beforeEach(() => {
openmct.time.timeSystem('utc', {
start: START - (5 * ONE_MINUTE),
end: START + (5 * ONE_MINUTE)
});
openmct.router.path = [{
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}];
applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}]);
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryForTimeStripKey);
imageryView = imageryViewProvider.view(imageryObject, [imageryObject, {
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}]);
imageryView.show(child);
componentView = imageryView.getComponent().$children[0];
spyOn(componentView.previewAction, 'invoke').and.callThrough();
return Vue.nextTick();
});
it("on mount should show imagery within the given bounds", (done) => {
Vue.nextTick(() => {
const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');
expect(imageElements.length).toEqual(6);
done();
});
});
it ('shows an auto scroll button when scroll to left', async () => {
// to mock what a scroll would do
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
await Vue.nextTick();
let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button');
expect(autoScrollButton).toBeTruthy();
});
it ('scrollToRight is called when clicking on auto scroll button', async () => {
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
await Vue.nextTick();
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset');
it("should show the clicked thumbnail as the preview image", (done) => {
Vue.nextTick(() => {
const mouseDownEvent = createMouseEvent("mousedown");
let imageWrapper = parent.querySelectorAll(`.c-imagery-tsv__image-wrapper`);
imageWrapper[2].dispatchEvent(mouseDownEvent);
Vue.nextTick(() => {
expect(componentView.previewAction.invoke).toHaveBeenCalledWith([componentView.objectPath[0]], {
indexForFocusedImage: 2,
objectPath: componentView.objectPath
});
done();
});
});
});
});
});

View File

@ -25,7 +25,9 @@ import Vue from 'vue';
export default function PlanViewProvider(openmct) {
function isCompactView(objectPath) {
return objectPath.find(object => object.type === 'time-strip') !== undefined;
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
}
return {

View File

@ -30,6 +30,7 @@ describe('the plugin', function () {
let child;
let openmct;
let appHolder;
let originalRouterPath;
beforeEach((done) => {
appHolder = document.createElement('div');
@ -49,11 +50,15 @@ describe('the plugin', function () {
child.style.height = '480px';
element.appendChild(child);
originalRouterPath = openmct.router.path;
openmct.on('start', done);
openmct.start(appHolder);
});
afterEach(() => {
openmct.router.path = originalRouterPath;
return resetApplicationState(openmct);
});
@ -78,8 +83,9 @@ describe('the plugin', function () {
id: "test-object",
type: "plan"
};
openmct.router.path = [testViewObject];
const applicableViews = openmct.objectViews.get(testViewObject, []);
const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]);
let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
expect(planView).toBeDefined();
});
@ -137,7 +143,9 @@ describe('the plugin', function () {
}
};
const applicableViews = openmct.objectViews.get(planDomainObject, []);
openmct.router.path = [planDomainObject];
const applicableViews = openmct.objectViews.get(planDomainObject, [planDomainObject]);
planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
let view = planView.view(planDomainObject, mockObjectPath);
view.show(child, true);

View File

@ -35,12 +35,15 @@
:tick-width="tickWidth"
:single-series="seriesModels.length === 1"
:series-model="seriesModels[0]"
:style="{
left: (plotWidth - tickWidth) + 'px'
}"
@yKeyChanged="setYAxisKey"
@tickWidthChanged="onTickWidthChange"
/>
<div class="gl-plot-wrapper-display-area-and-x-axis"
:style="{
left: (tickWidth + 20) + 'px'
left: (plotWidth + 20) + 'px'
}"
>
@ -219,7 +222,8 @@ export default {
isRealTime: this.openmct.time.clock() !== undefined,
loaded: false,
isTimeOutOfSync: false,
showLimitLineLabels: undefined
showLimitLineLabels: undefined,
isFrozenOnMouseDown: false
};
},
computed: {
@ -235,11 +239,9 @@ export default {
} else {
return 'plot-legend-collapsed';
}
}
},
watch: {
plotTickWidth(newTickWidth) {
this.onTickWidthChange(newTickWidth, true);
},
plotWidth() {
return this.plotTickWidth || this.tickWidth;
}
},
mounted() {
@ -336,6 +338,11 @@ export default {
},
loadSeriesData(series) {
//this check ensures that duplicate requests don't happen on load
if (!this.timeContext) {
return;
}
if (this.$parent.$refs.plotWrapper.offsetWidth === 0) {
this.scheduleLoad(series);
@ -345,9 +352,12 @@ export default {
this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
this.startLoading();
const bounds = this.timeContext.bounds();
const options = {
size: this.$parent.$refs.plotWrapper.offsetWidth,
domain: this.config.xAxis.get('key')
domain: this.config.xAxis.get('key'),
start: bounds.start,
end: bounds.end
};
series.load(options)
@ -356,9 +366,10 @@ export default {
loadMoreData(range, purge) {
this.config.series.forEach(plotSeries => {
this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
this.startLoading();
plotSeries.load({
size: this.$parent.$refs.plotWrapper.offsetWidth,
size: this.offsetWidth,
start: range.min,
end: range.max,
domain: this.config.xAxis.get('key')
@ -593,7 +604,8 @@ export default {
}
}
this.$emit('plotTickWidth', this.tickWidth);
const id = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.$emit('plotTickWidth', this.tickWidth, id);
},
trackMousePosition(event) {
@ -686,6 +698,11 @@ export default {
this.listenTo(window, 'mouseup', this.onMouseUp, this);
this.listenTo(window, 'mousemove', this.trackMousePosition, this);
// track frozen state on mouseDown to be read on mouseUp
const isFrozen = this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
this.isFrozenOnMouseDown = isFrozen;
if (event.altKey) {
return this.startPan(event);
} else {
@ -706,7 +723,14 @@ export default {
}
if (this.marquee) {
return this.endMarquee(event);
this.endMarquee(event);
}
// resume the plot if no pan, zoom, or drag action is taken
// needs to follow endMarquee so that plotHistory is pruned
const isAction = Boolean(this.plotHistory.length);
if (!isAction && !this.isFrozenOnMouseDown) {
return this.play();
}
},

View File

@ -44,7 +44,9 @@ export default function PlotViewProvider(openmct) {
}
function isCompactView(objectPath) {
return objectPath.find(object => object.type === 'time-strip');
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
}
return {

View File

@ -1,346 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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 BarGraphCompositionPolicy from "./BarGraphCompositionPolicy";
import { createOpenMct } from "utils/testing";
describe("The bar graph composition policy", () => {
let openmct;
const mockMetaDataWithNoRangeHints = {
"period": 10,
"amplitude": 1,
"offset": 0,
"dataRateInHz": 1,
"phase": 0,
"randomness": 0,
valuesForHints: () => {
return [];
},
values: [
{
"key": "name",
"name": "Name",
"format": "string"
},
{
"key": "utc",
"name": "Time",
"format": "utc",
"hints": {
"domain": 1,
"priority": 1
},
"source": "utc"
}
]
};
const mockMetaDataWithRangeHints = {
"period": 10,
"amplitude": 1,
"offset": 0,
"dataRateInHz": 1,
"phase": 0,
"randomness": 0,
"wavelength": 0,
valuesForHints: () => {
return [
{
"key": "sin",
"name": "Sine",
"unit": "Hz",
"formatString": "%0.2f",
"hints": {
"range": 1,
"priority": 4
},
"source": "sin"
},
{
"key": "cos",
"name": "Cosine",
"unit": "deg",
"formatString": "%0.2f",
"hints": {
"range": 2,
"priority": 5
},
"source": "cos"
}
];
},
values: [
{
"key": "name",
"name": "Name",
"format": "string",
"source": "name",
"hints": {
"priority": 0
}
},
{
"key": "utc",
"name": "Time",
"format": "utc",
"hints": {
"domain": 1,
"priority": 1
},
"source": "utc"
},
{
"key": "yesterday",
"name": "Yesterday",
"format": "utc",
"hints": {
"domain": 2,
"priority": 2
},
"source": "yesterday"
},
{
"key": "sin",
"name": "Sine",
"unit": "Hz",
"formatString": "%0.2f",
"hints": {
"range": 1,
"spectralAttribute": true
},
"source": "sin"
},
{
"key": "cos",
"name": "Cosine",
"unit": "deg",
"formatString": "%0.2f",
"hints": {
"range": 2,
"priority": 5
},
"source": "cos"
}
]
};
beforeEach(() => {
openmct = createOpenMct();
const mockTypeDef = {
telemetry: mockMetaDataWithRangeHints
};
const mockTypeService = {
getType: () => {
return {
typeDef: mockTypeDef
};
}
};
openmct.$injector = {
get: () => {
return mockTypeService;
}
};
openmct.telemetry.isTelemetryObject = function (domainObject) {
return true;
};
});
it("exists", () => {
expect(BarGraphCompositionPolicy(openmct).allow).toBeDefined();
});
xit("allow composition for telemetry that provides/supports bar graph meta data", () => {
const parent = {
"composition": [],
"configuration": {},
"name": "Some Bar Graph",
"type": "telemetry.plot.bar-graph",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const child = {
"telemetry": {
"period": 10,
"amplitude": 1,
"offset": 0,
"dataRateInHz": 1,
"phase": 0,
"randomness": 0
},
"name": "Unnamed Sine Wave Generator",
"type": "generator",
"location": "mine",
"modified": 1630399715531,
"persisted": 1630399715531,
"identifier": {
"namespace": "",
"key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c"
}
};
expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true);
});
it("allows composition for telemetry that contain at least one range", () => {
const mockTypeDef = {
telemetry: mockMetaDataWithRangeHints
};
const mockTypeService = {
getType: () => {
return {
typeDef: mockTypeDef
};
}
};
openmct.$injector = {
get: () => {
return mockTypeService;
}
};
const parent = {
"composition": [],
"configuration": {},
"name": "Some Bar Graph",
"type": "telemetry.plot.bar-graph",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const child = {
"telemetry": {
"period": 10,
"amplitude": 1,
"offset": 0,
"dataRateInHz": 1,
"phase": 0,
"randomness": 0
},
"name": "Unnamed Sine Wave Generator",
"type": "generator",
"location": "mine",
"modified": 1630399715531,
"persisted": 1630399715531,
"identifier": {
"namespace": "",
"key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c"
}
};
expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true);
});
it("disallows composition for telemetry that don't contain any range hints", () => {
const mockTypeDef = {
telemetry: mockMetaDataWithNoRangeHints
};
const mockTypeService = {
getType: () => {
return {
typeDef: mockTypeDef
};
}
};
openmct.$injector = {
get: () => {
return mockTypeService;
}
};
const parent = {
"composition": [],
"configuration": {},
"name": "Some Bar Graph",
"type": "telemetry.plot.bar-graph",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const child = {
"telemetry": {
"period": 10,
"amplitude": 1,
"offset": 0,
"dataRateInHz": 1,
"phase": 0,
"randomness": 0
},
"name": "Unnamed Sine Wave Generator",
"type": "generator",
"location": "mine",
"modified": 1630399715531,
"persisted": 1630399715531,
"identifier": {
"namespace": "",
"key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c"
}
};
expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(false);
});
it("passthrough for composition for non bar graph plots", () => {
const parent = {
"composition": [],
"configuration": {},
"name": "Some Stacked Plot",
"type": "telemetry.plot.stacked",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const child = {
"telemetry": {
"period": 10,
"amplitude": 1,
"offset": 0,
"dataRateInHz": 1,
"phase": 0,
"randomness": 0
},
"name": "Unnamed Sine Wave Generator",
"type": "generator",
"location": "mine",
"modified": 1630399715531,
"persisted": 1630399715531,
"identifier": {
"namespace": "",
"key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c"
}
};
expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true);
});
});

View File

@ -23,8 +23,8 @@ import _ from 'lodash';
import PlotSeries from "./PlotSeries";
import Collection from "./Collection";
import Color from "../lib/Color";
import ColorPalette from "../lib/ColorPalette";
import Color from "@/ui/color/Color";
import ColorPalette from "@/ui/color/ColorPalette";
export default class SeriesCollection extends Collection {

View File

@ -79,7 +79,7 @@
</template>
<script>
import ColorSwatch from "@/plugins/plot/ColorSwatch.vue";
import ColorSwatch from '@/ui/color/ColorSwatch.vue';
export default {
components: {

View File

@ -129,7 +129,7 @@
</template>
<script>
import ColorSwatch from '../../ColorSwatch.vue';
import ColorSwatch from '@/ui/color/ColorSwatch.vue';
import { MARKER_SHAPES } from "../../draw/MarkerShapes";
import { objectPath, validate, coerce } from "./formUtil";
import _ from 'lodash';

View File

@ -25,7 +25,9 @@ import Vue from 'vue';
export default function OverlayPlotViewProvider(openmct) {
function isCompactView(objectPath) {
return objectPath.find(object => object.type === 'time-strip');
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
}
return {

View File

@ -19,18 +19,12 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { BAR_GRAPH_KEY } from './barGraph/BarGraphConstants';
import PlotViewProvider from './PlotViewProvider';
import SpectralPlotViewProvider from './spectralPlot/SpectralPlotViewProvider';
import BarGraphViewProvider from './barGraph/BarGraphViewProvider';
import OverlayPlotViewProvider from './overlayPlot/OverlayPlotViewProvider';
import StackedPlotViewProvider from './stackedPlot/StackedPlotViewProvider';
import PlotsInspectorViewProvider from './inspector/PlotsInspectorViewProvider';
import BarGraphInspectorViewProvider from './barGraph/inspector/BarGraphInspectorViewProvider';
import OverlayPlotCompositionPolicy from './overlayPlot/OverlayPlotCompositionPolicy';
import StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy';
import SpectralPlotCompositionPolicy from './spectralPlot/SpectralPlotCompositionPolicy';
import BarGraphCompositionPolicy from './barGraph/BarGraphCompositionPolicy';
export default function () {
return function install(openmct) {
@ -64,48 +58,15 @@ export default function () {
},
priority: 890
});
openmct.types.addType('telemetry.plot.spectral', {
key: "telemetry.plot.spectral",
name: "Spectral Plot",
cssClass: "icon-plot-stacked",
description: "View Spectra on Y Axes with non-time domain on the X axis. Can be added to Display Layouts.",
//Temporarily disabling spectral plots
creatable: false,
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {};
},
priority: 890
});
openmct.types.addType(BAR_GRAPH_KEY, {
key: BAR_GRAPH_KEY,
name: "Bar Graph",
cssClass: "icon-bar-chart",
description: "View data as a bar graph. Can be added to Display Layouts.",
creatable: true,
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {
plotType: 'bar'
};
},
priority: 891
});
openmct.objectViews.addProvider(new StackedPlotViewProvider(openmct));
openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct));
openmct.objectViews.addProvider(new PlotViewProvider(openmct));
openmct.objectViews.addProvider(new SpectralPlotViewProvider(openmct));
openmct.objectViews.addProvider(new BarGraphViewProvider(openmct));
openmct.inspectorViews.addProvider(new PlotsInspectorViewProvider(openmct));
openmct.inspectorViews.addProvider(new BarGraphInspectorViewProvider(openmct));
openmct.composition.addPolicy(new OverlayPlotCompositionPolicy(openmct).allow);
openmct.composition.addPolicy(new StackedPlotCompositionPolicy(openmct).allow);
openmct.composition.addPolicy(new SpectralPlotCompositionPolicy(openmct).allow);
openmct.composition.addPolicy(new BarGraphCompositionPolicy(openmct).allow);
};
}

View File

@ -24,12 +24,10 @@ import {createMouseEvent, createOpenMct, resetApplicationState, spyOnBuiltins} f
import PlotVuePlugin from "./plugin";
import Vue from "vue";
import StackedPlot from "./stackedPlot/StackedPlot.vue";
// import SpectralPlot from "./spectralPlot/SpectralPlot.vue";
import configStore from "./configuration/ConfigStore";
import EventEmitter from "EventEmitter";
import PlotOptions from "./inspector/PlotOptions.vue";
import PlotConfigurationModel from "./configuration/PlotConfigurationModel";
import { BAR_GRAPH_VIEW, BAR_GRAPH_KEY } from './barGraph/BarGraphConstants';
describe("the plugin", function () {
let element;
@ -143,6 +141,7 @@ describe("the plugin", function () {
spyOn(window, 'ResizeObserver').and.returnValue({
observe() {},
unobserve() {},
disconnect() {}
});
@ -315,37 +314,6 @@ describe("the plugin", function () {
expect(plotView).toBeDefined();
});
it("provides a spectral plot view for objects with telemetry", () => {
const testTelemetryObject = {
id: "test-object",
type: "telemetry.plot.spectral",
telemetry: {
values: [{
key: "a-very-fine-key"
}]
}
};
const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-spectral");
expect(plotView).toBeDefined();
});
it("provides a spectral aggregate plot view for objects with telemetry", () => {
const testTelemetryObject = {
id: "test-object",
type: BAR_GRAPH_KEY,
telemetry: {
values: [{
key: "lots-of-aggregate-telemetry"
}]
}
};
const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
let plotView = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
expect(plotView).toBeDefined();
});
});
describe("The single plot view", () => {
@ -402,6 +370,10 @@ describe("the plugin", function () {
return Vue.nextTick();
});
it("Makes only one request for telemetry on load", () => {
expect(openmct.telemetry.request).toHaveBeenCalledTimes(1);
});
it("Renders a collapsed legend for every telemetry", () => {
let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
expect(legend.length).toBe(1);
@ -476,6 +448,64 @@ describe("the plugin", function () {
});
});
describe('resume actions on errant click', () => {
beforeEach(() => {
openmct.time.clock('local', {
start: -1000,
end: 100
});
return Vue.nextTick();
});
it("clicking the plot view without movement resumes the plot while active", async () => {
const pauseEl = element.querySelectorAll(".c-button-set .icon-pause");
// if the pause button is present, the chart is running
expect(pauseEl.length).toBe(1);
// simulate an errant mouse click
// the second item is the canvas we need to use
const canvas = element.querySelectorAll("canvas")[1];
const mouseDownEvent = new MouseEvent('mousedown');
const mouseUpEvent = new MouseEvent('mouseup');
canvas.dispatchEvent(mouseDownEvent);
// mouseup event is bound to the window
window.dispatchEvent(mouseUpEvent);
await Vue.nextTick();
const pauseElAfterClick = element.querySelectorAll(".c-button-set .icon-pause");
console.log('pauseElAfterClick', pauseElAfterClick);
expect(pauseElAfterClick.length).toBe(1);
});
it("clicking the plot view without movement leaves the plot paused", async () => {
const pauseEl = element.querySelector(".c-button-set .icon-pause");
// pause the plot
pauseEl.dispatchEvent(createMouseEvent('click'));
await Vue.nextTick();
const playEl = element.querySelectorAll('.c-button-set .is-paused');
expect(playEl.length).toBe(1);
// simulate an errant mouse click
// the second item is the canvas we need to use
const canvas = element.querySelectorAll("canvas")[1];
const mouseDownEvent = new MouseEvent('mousedown');
const mouseUpEvent = new MouseEvent('mouseup');
canvas.dispatchEvent(mouseDownEvent);
// mouseup event is bound to the window
window.dispatchEvent(mouseUpEvent);
await Vue.nextTick();
const playElAfterChartClick = element.querySelectorAll(".c-button-set .is-paused");
expect(playElAfterChartClick.length).toBe(1);
});
});
describe('controls in time strip view', () => {
it('zoom controls are hidden', () => {
@ -496,146 +526,6 @@ describe("the plugin", function () {
});
});
/*
* disabling this until we develop the plot view
describe("The spectral plot view", () => {
let testTelemetryObject;
// eslint-disable-next-line no-unused-vars
let testTelemetryObject2;
// eslint-disable-next-line no-unused-vars
let config;
let spectralPlotObject;
let component;
let mockComposition;
// eslint-disable-next-line no-unused-vars
let plotViewComponentObject;
beforeEach(() => {
const getFunc = openmct.$injector.get;
spyOn(openmct.$injector, "get")
.withArgs("exportImageService").and.returnValue({
exportPNG: () => {},
exportJPG: () => {}
})
.and.callFake(getFunc);
spectralPlotObject = {
identifier: {
namespace: "",
key: "test-spectral-plot"
},
type: "telemetry.plot.spectral",
name: "Test Spectral Plot"
};
testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
testTelemetryObject2 = {
identifier: {
namespace: "",
key: "test-object2"
},
type: "test-object",
name: "Test Object2",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "wavelength",
name: "Wavelength",
hints: {
range: 1
}
}, {
key: "some-other-key2",
name: "Another attribute2",
hints: {
range: 2
}
}]
}
};
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testTelemetryObject);
return [testTelemetryObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
let viewContainer = document.createElement("div");
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
SpectralPlot
},
provide: {
openmct: openmct,
domainObject: spectralPlotObject,
composition: openmct.composition.get(spectralPlotObject)
},
template: "<spectral-plot></spectral-plot>"
});
cleanupFirst.push(() => {
component.$destroy();
component = undefined;
});
return telemetryPromise
.then(Vue.nextTick())
.then(() => {
plotViewComponentObject = component.$root.$children[0];
const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier);
config = configStore.get(configId);
});
});
it("Renders a collapsed legend for every telemetry", () => {
let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
expect(legend.length).toBe(1);
expect(legend[0].innerHTML).toEqual("Test Object");
});
}); */
describe("The stacked plot view", () => {
let testTelemetryObject;
let testTelemetryObject2;
@ -1162,42 +1052,11 @@ describe("the plugin", function () {
const yAxisProperties = editOptionsEl.querySelectorAll("div.grid-properties:first-of-type .l-inspector-part");
expect(yAxisProperties.length).toEqual(3);
});
});
});
describe("the spectral plot", () => {
const mockObject = {
name: 'A Very Nice Spectral Plot',
key: 'telemetry.plot.spectral',
creatable: true
};
it('defines a spectral plot object type with the correct key', () => {
const objectDef = openmct.types.get('telemetry.plot.spectral').definition;
expect(objectDef.key).toEqual(mockObject.key);
});
xit('is creatable', () => {
const objectDef = openmct.types.get('telemetry.plot.spectral').definition;
expect(objectDef.creatable).toEqual(mockObject.creatable);
});
});
describe("the aggregate spectral plot", () => {
const mockObject = {
name: 'An Even Nicer Aggregate Spectral Plot',
key: BAR_GRAPH_KEY,
creatable: true
};
it('defines a spectral plot object type with the correct key', () => {
const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition;
expect(objectDef.key).toEqual(mockObject.key);
});
it('is creatable', () => {
const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition;
expect(objectDef.creatable).toEqual(mockObject.creatable);
it('renders color palette options', () => {
const colorSwatch = editOptionsEl.querySelector(".c-click-swatch");
expect(colorSwatch).toBeDefined();
});
});
});
});

View File

@ -1,36 +0,0 @@
export default function SpectralPlotCompositionPolicy(openmct) {
function hasSpectralDomainAndRange(metadata) {
const rangeValues = metadata.valuesForHints(['range']);
const domainValues = metadata.valuesForHints(['domain']);
const containsSomeSpectralData = domainValues.some(value => {
return ((value.key === 'wavelength') || (value.key === 'frequency'));
});
return rangeValues.length > 0
&& domainValues.length > 0
&& containsSomeSpectralData;
}
function hasSpectralTelemetry(domainObject) {
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
return false;
}
let metadata = openmct.telemetry.getMetadata(domainObject);
return metadata.values().length > 0 && hasSpectralDomainAndRange(metadata);
}
return {
allow: function (parent, child) {
if ((parent.type === 'telemetry.plot.spectral')
&& ((child.type !== 'telemetry.plot.overlay') && (hasSpectralTelemetry(child) === false))
) {
return false;
}
return true;
}
};
}

View File

@ -1,75 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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 SpectralView from './SpectralView.vue';
import Vue from 'vue';
export default function SpectralPlotViewProvider(openmct) {
function isCompactView(objectPath) {
return objectPath.find(object => object.type === 'time-strip');
}
return {
key: 'plot-spectral',
name: 'Spectral Plot',
cssClass: 'icon-telemetry',
canView(domainObject, objectPath) {
return domainObject && domainObject.type === 'telemetry.plot.spectral';
},
canEdit(domainObject, objectPath) {
return domainObject && domainObject.type === 'telemetry.plot.spectral';
},
view: function (domainObject, objectPath) {
let component;
return {
show: function (element) {
let isCompact = isCompactView(objectPath);
component = new Vue({
el: element,
components: {
SpectralView
},
provide: {
openmct,
domainObject
},
data() {
return {
options: {
compact: isCompact
}
};
},
template: '<spectral-view :options="options"></spectral-view>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}
};
}

View File

@ -1,13 +0,0 @@
<template>
<div>
</div>
</template>
<script>
export default {
inject: ['openmct', 'domainObject']
};
</script>

View File

@ -90,7 +90,8 @@ export default {
cursorGuide: false,
gridLines: true,
loading: false,
compositionObjects: []
compositionObjects: [],
tickWidthMap: {}
};
},
computed: {
@ -106,8 +107,6 @@ export default {
this.imageExporter = new ImageExporter(this.openmct);
this.tickWidthMap = {};
this.composition.on('add', this.addChild);
this.composition.on('remove', this.removeChild);
this.composition.on('reorder', this.compositionReorder);
@ -127,13 +126,15 @@ export default {
addChild(child) {
const id = this.openmct.objects.makeKeyString(child.identifier);
this.tickWidthMap[id] = 0;
this.$set(this.tickWidthMap, id, 0);
this.compositionObjects.push(child);
},
removeChild(childIdentifier) {
const id = this.openmct.objects.makeKeyString(childIdentifier);
delete this.tickWidthMap[id];
this.$delete(this.tickWidthMap, id);
const childObj = this.compositionObjects.filter((c) => {
const identifier = this.openmct.objects.makeKeyString(c.identifier);
@ -191,14 +192,7 @@ export default {
return;
}
//update the tickWidth for this plotId, the computed max tick width of the stacked plot will be cascaded down
//TODO: Might need to do this using $set
this.tickWidthMap[plotId] = Math.max(width, this.tickWidthMap[plotId]);
// const newTickWidth = Math.max(...Object.values(this.tickWidthMap));
// if (newTickWidth !== tickWidth || width !== tickWidth) {
// tickWidth = newTickWidth;
// $scope.$broadcast('plot:tickWidth', tickWidth);
// }
this.$set(this.tickWidthMap, plotId, width);
}
}
};

View File

@ -25,7 +25,9 @@ import Vue from 'vue';
export default function StackedPlotViewProvider(openmct) {
function isCompactView(objectPath) {
return objectPath.find(object => object.type === 'time-strip');
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
}
return {

View File

@ -36,6 +36,7 @@ define([
'./URLIndicatorPlugin/URLIndicatorPlugin',
'./telemetryMean/plugin',
'./plot/plugin',
'./charts/plugin',
'./telemetryTable/plugin',
'./staticRootPlugin/plugin',
'./notebook/plugin',
@ -87,6 +88,7 @@ define([
URLIndicatorPlugin,
TelemetryMean,
PlotPlugin,
ChartPlugin,
TelemetryTablePlugin,
StaticRootPlugin,
Notebook,
@ -189,6 +191,7 @@ define([
plugins.ExampleImagery = ExampleImagery;
plugins.ImageryPlugin = ImageryPlugin;
plugins.Plot = PlotPlugin.default;
plugins.Chart = ChartPlugin.default;
plugins.TelemetryTable = TelemetryTablePlugin;
plugins.SummaryWidget = SummaryWidget;

View File

@ -7,7 +7,8 @@ define([
'./eventHelpers',
'objectUtils',
'lodash',
'zepto'
'zepto',
'@braintree/sanitize-url'
], function (
widgetTemplate,
Rule,
@ -17,7 +18,8 @@ define([
eventHelpers,
objectUtils,
_,
$
$,
urlSanitizeLib
) {
//default css configuration for new rules
@ -88,7 +90,7 @@ define([
function toggleTestData() {
self.outerWrapper.toggleClass('expanded-widget-test-data');
self.toggleTestDataControl.toggleClass('c-disclosure-triangle--expanded');
}
}
this.listenTo(this.toggleTestDataControl, 'click', toggleTestData);
@ -99,7 +101,7 @@ define([
function toggleRules() {
self.outerWrapper.toggleClass('expanded-widget-rules');
self.toggleRulesControl.toggleClass('c-disclosure-triangle--expanded');
}
}
this.listenTo(this.toggleRulesControl, 'click', toggleRules);
@ -114,7 +116,7 @@ define([
*/
SummaryWidget.prototype.addHyperlink = function (url, openNewTab) {
if (url) {
this.widgetButton.attr('href', url);
this.widgetButton.attr('href', urlSanitizeLib.sanitizeUrl(url));
} else {
this.widgetButton.removeAttr('href');
}
@ -317,7 +319,7 @@ define([
expanded: 'true'
};
}
}
ruleConfig = this.domainObject.configuration.ruleConfigById[ruleId];
this.rulesById[ruleId] = new Rule(ruleConfig, this.domainObject, this.openmct,
@ -345,7 +347,7 @@ define([
ruleOrder.splice(targetIndex + 1, 0, event.draggingId);
this.domainObject.configuration.ruleOrder = ruleOrder;
this.updateDomainObject();
}
}
this.refreshRules();
};

View File

@ -1,7 +1,9 @@
define([
'./summary-widget.html'
'./summary-widget.html',
'@braintree/sanitize-url'
], function (
summaryWidgetTemplate
summaryWidgetTemplate,
urlSanitizeLib
) {
const WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon';
@ -35,8 +37,9 @@ define([
this.icon = this.container.querySelector('#widgetIcon');
this.label = this.container.querySelector('.js-sw__label');
if (this.domainObject.url) {
this.widget.setAttribute('href', this.domainObject.url);
let url = this.domainObject.url;
if (url) {
this.widget.setAttribute('href', urlSanitizeLib.sanitizeUrl(url));
} else {
this.widget.removeAttribute('href');
}

View File

@ -222,6 +222,24 @@ describe("the plugin", () => {
openmct.router.path = originalRouterPath;
});
it("Shows no progress bar initially", () => {
let progressBar = element.querySelector('.c-progress-bar');
expect(tableInstance.outstandingRequests).toBe(0);
expect(progressBar).toBeNull();
});
it("Shows a progress bar while making requests", async () => {
tableInstance.incrementOutstandingRequests();
await Vue.nextTick();
let progressBar = element.querySelector('.c-progress-bar');
expect(tableInstance.outstandingRequests).toBe(1);
expect(progressBar).not.toBeNull();
});
it("Renders a row for every telemetry datum returned", () => {
let rows = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(rows.length).toBe(3);

View File

@ -231,8 +231,8 @@ export default {
const panStart = bounds.start - percX * deltaTime;
return {
start: panStart,
end: panStart + deltaTime
start: parseInt(panStart, 10),
end: parseInt(panStart + deltaTime, 10)
};
},
startZoom() {
@ -296,7 +296,7 @@ export default {
const valueDelta = value - this.left;
const offset = valueDelta / this.width * timeDelta;
return bounds.start + offset;
return parseInt(bounds.start + offset, 10);
},
isChangingViewBounds() {
return this.dragStartX && this.dragX && this.dragStartX !== this.dragX;

View File

@ -1,13 +1,7 @@
<template>
<form ref="fixedDeltaInput"
class="c-conductor__inputs"
@submit.prevent="updateTimeFromConductor"
>
<button
ref="submitButton"
class="c-input--submit"
type="submit"
></button>
<div
class="c-ctrl-wrapper c-conductor-input c-conductor__start-fixed"
>
@ -56,10 +50,6 @@
@date-selected="endDateSelected"
/>
</div>
<input
type="submit"
class="invisible"
>
</form>
</template>
@ -183,10 +173,7 @@ export default {
submitForm() {
// Allow Vue model to catch up to user input.
// Submitting form will cause validation messages to display (but only if triggered by button click)
this.$nextTick(() => this.$refs.submitButton.click());
},
updateTimeFromConductor() {
this.setBoundsFromView();
this.$nextTick(() => this.setBoundsFromView());
},
validateAllBounds(ref) {
if (!this.areBoundsFormatsValid()) {

View File

@ -1,16 +1,23 @@
<template>
<div class="l-iframe abs">
<iframe :src="currentDomainObject.url"></iframe>
<iframe :src="url"></iframe>
</div>
</template>
<script>
const sanitizeUrl = require("@braintree/sanitize-url").sanitizeUrl;
export default {
inject: ['openmct', 'domainObject'],
data: function () {
return {
currentDomainObject: this.domainObject
};
},
computed: {
url() {
return sanitizeUrl(this.currentDomainObject.url);
}
}
};
</script>

View File

@ -729,7 +729,7 @@ mct-plot {
}
/********************************************************************* BAR CHARTS */
/***************** BAR GRAPHS */
.c-bar-chart {
flex: 1 1 auto;
overflow: hidden;

View File

@ -40,11 +40,6 @@
}
}
.zerolinelayer {
// Hide unneeded plotly-styled horizontal line
display: none;
}
path.xy2-y {
stroke: $colorPlotHash !important; // Using this instead of $colorPlotAreaBorder because that is an rgba
opacity: $opacityPlotHash !important;

View File

@ -76,7 +76,7 @@
</template>
<script>
import ColorPalette from './lib/ColorPalette';
import ColorPalette from './ColorPalette';
export default {
inject: ['openmct', 'domainObject'],

View File

@ -126,6 +126,11 @@ export default {
this.releaseEditModeHandler();
delete this.releaseEditModeHandler;
}
if (this.styleRuleManager) {
this.styleRuleManager.destroy();
delete this.styleRuleManager;
}
}
delete this.viewContainer;

View File

@ -46,6 +46,14 @@ export default {
'openmct',
'objectPath'
],
props: {
viewOptions: {
type: Object,
default() {
return undefined;
}
}
},
data() {
let domainObject = this.objectPath[0];
@ -109,7 +117,7 @@ export default {
this.view = this.currentView.view(this.domainObject, this.objectPath);
this.getActionsCollection();
this.view.show(this.viewContainer, false);
this.view.show(this.viewContainer, false, this.viewOptions);
this.initObjectStyles();
},
getActionsCollection() {

View File

@ -44,7 +44,7 @@ export default class PreviewAction {
}
}
invoke(objectPath) {
invoke(objectPath, viewOptions) {
let preview = new Vue({
components: {
Preview
@ -53,7 +53,12 @@ export default class PreviewAction {
openmct: this._openmct,
objectPath: objectPath
},
template: '<Preview></Preview>'
data() {
return {
viewOptions
};
},
template: '<Preview :view-options="viewOptions"></Preview>'
});
preview.$mount();

View File

@ -4,7 +4,8 @@
<div
class="l-browse-bar__object-name--w c-object-label"
>
<div class="c-object-label__type-icon"
<div v-if="type"
class="c-object-label__type-icon"
:class="type.definition.cssClass"
></div>
<span class="l-browse-bar__object-name c-object-label__name">

View File

@ -90,9 +90,9 @@ define(['EventEmitter'], function (EventEmitter) {
provider.view = (domainObject, objectPath) => {
const viewObject = wrappedView(domainObject, objectPath);
const wrappedShow = viewObject.show.bind(viewObject);
viewObject.show = (element, isEditing) => {
viewObject.show = (element, isEditing, viewOptions) => {
viewObject.parentElement = element.parentElement;
wrappedShow(element, isEditing);
wrappedShow(element, isEditing, viewOptions);
};
return viewObject;

View File

@ -126,7 +126,7 @@ class ApplicationRouter extends EventEmitter {
}
/**
* Navgate to given hash and update current location object and notify listeners about location change
* Navigate to given hash and update current location object and notify listeners about location change
*
* @param {string} paramName name of searchParam to get from current url searchParams
*
@ -136,6 +136,13 @@ class ApplicationRouter extends EventEmitter {
this.handleLocationChange(hash.substring(1));
}
/**
* Check if a given object and current location object are same
*
* @param {Array<Object>} objectPath Object path of a given Domain Object
*
* @returns {Boolean}
*/
isNavigatedObject(objectPath) {
let targetObject = objectPath[0];
let navigatedObject = this.path[0];

View File

@ -42,6 +42,8 @@ const webpackConfig = {
"csv": "comma-separated-values",
"EventEmitter": "eventemitter3",
"bourbon": "bourbon.scss",
"plotly-basic": "plotly.js-basic-dist",
"plotly-gl2d": "plotly.js-gl2d-dist",
"vue": vueFile,
"d3-scale": path.join(__dirname, "node_modules/d3-scale/build/d3-scale.min.js"),
"printj": path.join(__dirname, "node_modules/printj/dist/printj.min.js"),