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", "./GeneratorProvider",
"./SinewaveLimitProvider", "./SinewaveLimitProvider",
"./StateGeneratorProvider", "./StateGeneratorProvider",
"./SpectralGeneratorProvider",
"./SpectralAggregateGeneratorProvider",
"./GeneratorMetadataProvider" "./GeneratorMetadataProvider"
], function ( ], function (
GeneratorProvider, GeneratorProvider,
SinewaveLimitProvider, SinewaveLimitProvider,
StateGeneratorProvider, StateGeneratorProvider,
SpectralGeneratorProvider,
SpectralAggregateGeneratorProvider,
GeneratorMetadataProvider GeneratorMetadataProvider
) { ) {
@ -65,37 +61,6 @@ define([
openmct.telemetry.addProvider(new StateGeneratorProvider()); 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", { openmct.types.addType("generator", {
name: "Sine Wave Generator", name: "Sine Wave Generator",
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.", description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",

View File

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

View File

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

View File

@ -49,7 +49,6 @@ export class TelemetryCollection extends EventEmitter {
this.parseTime = undefined; this.parseTime = undefined;
this.metadata = this.openmct.telemetry.getMetadata(domainObject); this.metadata = this.openmct.telemetry.getMetadata(domainObject);
this.unsubscribe = undefined; this.unsubscribe = undefined;
this.historicalProvider = undefined;
this.options = options; this.options = options;
this.pageState = undefined; this.pageState = undefined;
this.lastBounds = undefined; this.lastBounds = undefined;
@ -65,13 +64,13 @@ export class TelemetryCollection extends EventEmitter {
this._error(ERRORS.LOADED); this._error(ERRORS.LOADED);
} }
this._timeSystem(this.openmct.time.timeSystem()); this._setTimeSystem(this.openmct.time.timeSystem());
this.lastBounds = this.openmct.time.bounds(); this.lastBounds = this.openmct.time.bounds();
this._watchBounds(); this._watchBounds();
this._watchTimeSystem(); this._watchTimeSystem();
this._initiateHistoricalRequests(); this._requestHistoricalTelemetry();
this._initiateSubscriptionTelemetry(); this._initiateSubscriptionTelemetry();
this.loaded = true; this.loaded = true;
@ -103,31 +102,25 @@ export class TelemetryCollection extends EventEmitter {
return this.boundedTelemetry; 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 * If a historical provider exists, then historical requests will be made
* @private * @private
*/ */
async _requestHistoricalTelemetry() { 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; return;
} }
let historicalData; let historicalData;
this.options.onPartialResponse = this._processNewTelemetry.bind(this); options.onPartialResponse = this._processNewTelemetry.bind(this);
try { try {
if (this.requestAbort) { if (this.requestAbort) {
@ -135,9 +128,9 @@ export class TelemetryCollection extends EventEmitter {
} }
this.requestAbort = new AbortController(); this.requestAbort = new AbortController();
this.options.signal = this.requestAbort.signal; options.signal = this.requestAbort.signal;
this.emit('requestStarted'); this.emit('requestStarted');
historicalData = await this.historicalProvider.request(this.domainObject, this.options); historicalData = await historicalProvider.request(this.domainObject, options);
} catch (error) { } catch (error) {
if (error.name !== 'AbortError') { if (error.name !== 'AbortError') {
console.error('Error requesting telemetry data...'); console.error('Error requesting telemetry data...');
@ -179,6 +172,10 @@ export class TelemetryCollection extends EventEmitter {
* @private * @private
*/ */
_processNewTelemetry(telemetryData) { _processNewTelemetry(telemetryData) {
if (telemetryData === undefined) {
return;
}
let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData]; let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
let parsedValue; let parsedValue;
let beforeStartOfBounds; let beforeStartOfBounds;
@ -205,9 +202,10 @@ export class TelemetryCollection extends EventEmitter {
if (endIndex > startIndex) { if (endIndex > startIndex) {
let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex); let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex);
isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum)); isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum));
} }
} else if (startIndex === this.boundedTelemetry.length) {
isDuplicate = _.isEqual(datum, this.boundedTelemetry[this.boundedTelemetry.length - 1]);
} }
if (!isDuplicate) { if (!isDuplicate) {
@ -323,7 +321,7 @@ export class TelemetryCollection extends EventEmitter {
* Time System * Time System
* @private * @private
*/ */
_timeSystem(timeSystem) { _setTimeSystem(timeSystem) {
let domains = this.metadata.valuesForHints(['domain']); let domains = this.metadata.valuesForHints(['domain']);
let domain = domains.find((d) => d.key === timeSystem.key); let domain = domains.find((d) => d.key === timeSystem.key);
@ -339,7 +337,10 @@ export class TelemetryCollection extends EventEmitter {
this.parseTime = (datum) => { this.parseTime = (datum) => {
return valueFormatter.parse(datum); return valueFormatter.parse(datum);
}; };
}
_setTimeSystemAndFetchData(timeSystem) {
this._setTimeSystem(timeSystem);
this._reset(); 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 * @private
*/ */
_watchTimeSystem() { _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 * @private
*/ */
_unwatchTimeSystem() { _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# * @memberof module:openmct.TimeAPI#
* @method getContextForView * @method getContextForView
*/ */
getContextForView(objectPath) { getContextForView(objectPath = []) {
let timeContext = this; let timeContext = this;
objectPath.forEach(item => { objectPath.forEach(item => {

View File

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

View File

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

View File

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

View File

@ -26,12 +26,14 @@ import Vue from 'vue';
export default function BarGraphViewProvider(openmct) { export default function BarGraphViewProvider(openmct) {
function isCompactView(objectPath) { 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 { return {
key: BAR_GRAPH_VIEW, key: BAR_GRAPH_VIEW,
name: 'Spectral Aggregate Plot', name: 'Bar Graph',
cssClass: 'icon-telemetry', cssClass: 'icon-telemetry',
canView(domainObject, objectPath) { canView(domainObject, objectPath) {
return domainObject && domainObject.type === BAR_GRAPH_KEY; return domainObject && domainObject.type === BAR_GRAPH_KEY;
@ -54,7 +56,8 @@ export default function BarGraphViewProvider(openmct) {
}, },
provide: { provide: {
openmct, openmct,
domainObject domainObject,
path: objectPath
}, },
data() { data() {
return { return {

View File

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

View File

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

View File

@ -21,13 +21,18 @@
--> -->
<template> <template>
<ul> <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" <span class="c-disclosure-triangle is-enabled flex-elem"
:class="expandedCssClass" :class="expandedCssClass"
@click="expanded = !expanded" @click="expanded = !expanded"
> >
</span> </span>
<div>
<div class="c-object-label">
<div :class="[seriesCss]">
</div>
<div class="c-object-label__name">{{ name }}</div> <div class="c-object-label__name">{{ name }}</div>
</div> </div>
</li> </li>
@ -44,7 +49,8 @@
</template> </template>
<script> <script>
import ColorSwatch from '../../ColorSwatch.vue'; import ColorSwatch from '@/ui/color/ColorSwatch.vue';
import Color from "@/ui/color/Color";
export default { export default {
components: { components: {
@ -55,50 +61,90 @@ export default {
item: { item: {
type: Object, type: Object,
required: true required: true
},
colorPalette: {
type: Object,
required: true
} }
}, },
data() { data() {
return { return {
currentColor: undefined, currentColor: undefined,
name: '', name: '',
type: '',
isAlias: false,
expanded: false expanded: false
}; };
}, },
computed: { computed: {
expandedCssClass() { expandedCssClass() {
return this.expanded ? 'c-disclosure-triangle--expanded' : ''; 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: { watch: {
item: { item: {
handler() { handler() {
this.initColor(); this.initColorAndName();
}, },
deep: true deep: true
} }
}, },
mounted() { mounted() {
this.key = this.openmct.objects.makeKeyString(this.item); this.key = this.openmct.objects.makeKeyString(this.item);
this.initColor(); this.initColorAndName();
this.unObserve = this.openmct.objects.observe(this.domainObject, `this.domainObject.configuration.barStyles[${this.key}]`, this.initColor); this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName);
}, },
beforeDestroy() { beforeDestroy() {
if (this.unObserve) { if (this.removeBarStylesListener) {
this.unObserve(); this.removeBarStylesListener();
} }
}, },
methods: { methods: {
initColor() { initColorAndName() {
if (this.domainObject.configuration.barStyles && this.domainObject.configuration.barStyles[this.key]) { // this is called before the plot is initialized
this.currentColor = this.domainObject.configuration.barStyles[this.key].color; if (!this.domainObject.configuration.barStyles.series[this.key]) {
this.name = this.domainObject.configuration.barStyles[this.key].name; 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) { setColor(chosenColor) {
this.currentColor = chosenColor.asHexString(); this.currentColor = chosenColor.asHexString();
this.openmct.objects.mutate( this.openmct.objects.mutate(
this.domainObject, this.domainObject,
`configuration.barStyles[${this.key}].color`, `configuration.barStyles.series["${this.key}"].color`,
this.currentColor 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.trigger = conditionConfiguration.configuration.trigger;
this.description = ''; this.summary = '';
} }
updateResult(datum) { updateResult(datum) {
@ -134,7 +134,6 @@ export default class Condition extends EventEmitter {
criterionConfigurations.forEach((criterionConfiguration) => { criterionConfigurations.forEach((criterionConfiguration) => {
this.addCriterion(criterionConfiguration); this.addCriterion(criterionConfiguration);
}); });
this.updateDescription();
} }
updateCriteria(criterionConfigurations) { updateCriteria(criterionConfigurations) {
@ -146,7 +145,6 @@ export default class Condition extends EventEmitter {
this.criteria.forEach((criterion) => { this.criteria.forEach((criterion) => {
criterion.updateTelemetryObjects(this.conditionManager.telemetryObjects); 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('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.off('telemetryIsStale', (obj) => this.handleStaleCriterion(obj)); criterion.off('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
this.criteria.splice(found.index, 1, newCriterion); this.criteria.splice(found.index, 1, newCriterion);
this.updateDescription();
} }
} }
@ -216,7 +213,6 @@ export default class Condition extends EventEmitter {
}); });
criterion.destroy(); criterion.destroy();
this.criteria.splice(found.index, 1); this.criteria.splice(found.index, 1);
this.updateDescription();
return true; return true;
} }
@ -228,7 +224,6 @@ export default class Condition extends EventEmitter {
let found = this.findCriterion(criterion.id); let found = this.findCriterion(criterion.id);
if (found) { if (found) {
this.criteria[found.index] = criterion.data; 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 : ''}`; description = `${description} ${criterion.getDescription()} ${(index < this.criteria.length - 1) ? triggerDescription.conjunction : ''}`;
}); });
this.description = description; this.summary = description;
this.conditionManager.updateConditionDescription(this);
} }
getTriggerDescription() { getTriggerDescription() {

View File

@ -105,7 +105,14 @@ export default class ConditionManager extends EventEmitter {
} }
updateConditionTelemetryObjects() { 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() { removeConditionTelemetryObjects() {
@ -139,10 +146,17 @@ export default class ConditionManager extends EventEmitter {
} }
} }
updateConditionDescription(condition) {
condition.updateDescription();
return condition.summary;
}
updateCondition(conditionConfiguration) { updateCondition(conditionConfiguration) {
let condition = this.findConditionById(conditionConfiguration.id); let condition = this.findConditionById(conditionConfiguration.id);
if (condition) { if (condition) {
condition.update(conditionConfiguration); condition.update(conditionConfiguration);
conditionConfiguration.summary = this.updateConditionDescription(condition);
} }
let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === conditionConfiguration.id); 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) { initCondition(conditionConfiguration, index) {
let condition = new Condition(conditionConfiguration, this.openmct, this); let condition = new Condition(conditionConfiguration, this.openmct, this);
conditionConfiguration.summary = this.updateConditionDescription(condition);
if (index !== undefined) { if (index !== undefined) {
this.conditions.splice(index + 1, 0, condition); this.conditions.splice(index + 1, 0, condition);
} else { } else {

View File

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

View File

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

View File

@ -192,7 +192,11 @@ export default {
this.telemetry.forEach((telemetryObject) => { this.telemetry.forEach((telemetryObject) => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier); const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject); 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) { addTestInput(testInput) {

View File

@ -177,7 +177,7 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
const timeSystem = this.openmct.time.timeSystem(); const timeSystem = this.openmct.time.timeSystem();
telemetryRequestsResults.forEach((results, index) => { 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 datumId = keys[index];
const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObjects[datumId]); const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObjects[datumId]);

View File

@ -167,6 +167,11 @@ export default class TelemetryCriterion extends EventEmitter {
id: this.id, id: this.id,
data: this.formatData(normalizedDatum) 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 Vue from 'vue';
import {getApplicableStylesForItem} from "./utils/styleUtils"; import {getApplicableStylesForItem} from "./utils/styleUtils";
import ConditionManager from "@/plugins/condition/ConditionManager"; import ConditionManager from "@/plugins/condition/ConditionManager";
import StyleRuleManager from "./StyleRuleManager";
describe('the plugin', function () { describe('the plugin', function () {
let conditionSetDefinition; let conditionSetDefinition;
@ -96,6 +97,8 @@ describe('the plugin', function () {
mockListener = jasmine.createSpy('mockListener'); mockListener = jasmine.createSpy('mockListener');
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true);
conditionSetDefinition.initialize(mockConditionSetDomainObject); conditionSetDefinition.initialize(mockConditionSetDomainObject);
spyOn(openmct.objects, "save").and.returnValue(Promise.resolve(true)); 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 instanceof Array).toBeTrue();
expect(mockConditionSetDomainObject.composition.length).toEqual(0); 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', () => { describe('the condition set usage for multiple display layout items', () => {
@ -724,4 +712,123 @@ describe('the plugin', function () {
expect(result[2]).toBeUndefined(); 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> <template>
<component :is="urlDefined ? 'a' : 'span'" <component :is="urlDefined ? 'a' : 'span'"
class="c-condition-widget u-style-receiver js-style-receiver" class="c-condition-widget u-style-receiver js-style-receiver"
:href="urlDefined ? internalDomainObject.url : null" :href="url"
> >
<div class="c-condition-widget__label"> <div class="c-condition-widget__label">
{{ internalDomainObject.label }} {{ internalDomainObject.label }}
@ -32,6 +32,8 @@
</template> </template>
<script> <script>
const sanitizeUrl = require("@braintree/sanitize-url").sanitizeUrl;
export default { export default {
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject'],
data: function () { data: function () {
@ -42,6 +44,9 @@ export default {
computed: { computed: {
urlDefined() { urlDefined() {
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0; return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
},
url() {
return this.urlDefined ? sanitizeUrl(this.internalDomainObject.url) : null;
} }
}, },
mounted() { mounted() {

View File

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

View File

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

View File

@ -10,7 +10,14 @@ export default class ImageryView {
this.component = undefined; 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({ this.component = new Vue({
el: element, el: element,
components: { components: {
@ -19,10 +26,15 @@ export default class ImageryView {
provide: { provide: {
openmct: this.openmct, openmct: this.openmct,
domainObject: this.domainObject, domainObject: this.domainObject,
objectPath: this.objectPath, objectPath: alternateObjectPath || this.objectPath,
currentView: this 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 PADDING = 1;
const ROW_HEIGHT = 100; 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 { export default {
mixins: [imageryData], mixins: [imageryData],
@ -78,10 +84,12 @@ export default {
this.canvasContext = this.canvas.getContext('2d'); this.canvasContext = this.canvas.getContext('2d');
this.setDimensions(); 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.updateViewBounds();
this.openmct.time.on("bounds", this.updateViewBounds);
this.resize = _.debounce(this.resize, 400); this.resize = _.debounce(this.resize, 400);
this.imageryStripResizeObserver = new ResizeObserver(this.resize); this.imageryStripResizeObserver = new ResizeObserver(this.resize);
@ -90,25 +98,36 @@ export default {
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges); this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
}, },
beforeDestroy() { beforeDestroy() {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
if (this.imageryStripResizeObserver) { if (this.imageryStripResizeObserver) {
this.imageryStripResizeObserver.disconnect(); this.imageryStripResizeObserver.disconnect();
} }
this.openmct.time.off("timeSystem", this.setScaleAndPlotImagery); this.stopFollowingTimeContext();
this.openmct.time.off("bounds", this.updateViewBounds);
if (this.unlisten) { if (this.unlisten) {
this.unlisten(); this.unlisten();
} }
}, },
methods: { 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) { expand(index) {
const path = this.objectPath[0]; const path = this.objectPath[0];
this.previewAction.invoke([path]); this.previewAction.invoke([path], {
indexForFocusedImage: index,
objectPath: this.objectPath
});
}, },
observeForChanges(mutatedObject) { observeForChanges(mutatedObject) {
this.updateViewBounds(); this.updateViewBounds();
@ -134,14 +153,10 @@ export default {
return clientWidth; return clientWidth;
}, },
updateViewBounds(bounds, isTick) { updateViewBounds(bounds, isTick) {
this.viewBounds = this.openmct.time.bounds(); this.viewBounds = this.timeContext.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;
if (this.timeSystem === undefined) { if (this.timeSystem === undefined) {
this.timeSystem = this.openmct.time.timeSystem(); this.timeSystem = this.timeContext.timeSystem();
} }
this.setScaleAndPlotImagery(this.timeSystem, !isTick); this.setScaleAndPlotImagery(this.timeSystem, !isTick);
@ -172,18 +187,18 @@ export default {
}, },
clearPreviousImagery(clearAllImagery) { clearPreviousImagery(clearAllImagery) {
//TODO: Only clear items that are out of bounds //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 => { noItemsEl.forEach(item => {
item.remove(); item.remove();
}); });
let imagery = this.$el.querySelectorAll(".c-imagery-tsv__image-wrapper"); let imagery = this.$el.querySelectorAll(`.${IMAGE_WRAPPER_CLASS}`);
imagery.forEach(item => { imagery.forEach(item => {
if (clearAllImagery) { if (clearAllImagery) {
item.remove(); item.remove();
} else { } else {
const id = this.getNSAttributesForElement(item, 'id'); const id = item.getAttributeNS(null, 'id');
if (id) { if (id) {
const timestamp = id.replace('id-', ''); const timestamp = id.replace(ID_PREFIX, '');
if (!this.isImageryInBounds({ if (!this.isImageryInBounds({
time: timestamp time: timestamp
})) { })) {
@ -205,7 +220,7 @@ export default {
} }
if (timeSystem === undefined) { if (timeSystem === undefined) {
timeSystem = this.openmct.time.timeSystem(); timeSystem = this.timeContext.timeSystem();
} }
if (timeSystem.isUTCBased) { if (timeSystem.isUTCBased) {
@ -223,19 +238,17 @@ export default {
this.xScale.range([PADDING, this.width - PADDING * 2]); this.xScale.range([PADDING, this.width - PADDING * 2]);
}, },
isImageryInBounds(imageObj) { 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() { getImageryContainer() {
let svgHeight = 100; let containerHeight = 100;
let svgWidth = this.imageHistory.length ? this.width : 200; let containerWidth = this.imageHistory.length ? this.width : 200;
let groupSVG; let imageryContainer;
let existingSVG = this.$el.querySelector(".c-imagery-tsv__contents svg"); let existingContainer = this.$el.querySelector(`.${CONTAINER_CLASS}`);
if (existingSVG) { if (existingContainer) {
groupSVG = existingSVG; imageryContainer = existingContainer;
this.setNSAttributesForElement(groupSVG, { imageryContainer.style.maxWidth = `${containerWidth}px`;
width: svgWidth
});
} else { } else {
let component = new Vue({ let component = new Vue({
components: { components: {
@ -246,26 +259,20 @@ export default {
}, },
data() { data() {
return { return {
isNested: true, isNested: true
height: svgHeight,
width: svgWidth
}; };
}, },
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); this.$refs.imageryHolder.appendChild(component.$mount().$el);
groupSVG = component.$el.querySelector('svg'); imageryContainer = component.$el.querySelector(`.${CONTAINER_CLASS}`);
imageryContainer.style.maxWidth = `${containerWidth}px`;
groupSVG.addEventListener('mouseout', (event) => { imageryContainer.style.height = `${containerHeight}px`;
if (event.target.nodeName === 'svg' || event.target.nodeName === 'use') {
this.removeFromForeground();
}
});
} }
return groupSVG; return imageryContainer;
}, },
isImageryWidthAcceptable() { isImageryWidthAcceptable() {
// We're calculating if there is enough space between images to show the thumbnails. // 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; return imageContainerWidth < IMAGE_WIDTH_THRESHOLD;
}, },
drawImagery() { drawImagery() {
let groupSVG = this.getImageryContainer(); let imageryContainer = this.getImageryContainer();
const showImagePlaceholders = this.isImageryWidthAcceptable(); const showImagePlaceholders = this.isImageryWidthAcceptable();
let index = 0;
if (this.imageHistory.length) { if (this.imageHistory.length) {
this.imageHistory.forEach((currentImageObject, index) => { this.imageHistory.forEach((currentImageObject) => {
if (this.isImageryInBounds(currentImageObject)) { if (this.isImageryInBounds(currentImageObject)) {
this.plotImagery(currentImageObject, showImagePlaceholders, groupSVG, index); this.plotImagery(currentImageObject, showImagePlaceholders, imageryContainer, index);
index = index + 1;
} }
}); });
} else { } else {
this.plotNoItems(groupSVG); this.plotNoItems(imageryContainer);
} }
}, },
plotNoItems(svgElement) { plotNoItems(containerElement) {
let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text'); let textElement = document.createElement('text');
this.setNSAttributesForElement(textElement, { textElement.classList.add(NO_ITEMS_CLASS);
x: "10",
y: "20",
class: "c-imagery-tsv__no-items"
});
textElement.innerHTML = 'No images within timeframe'; textElement.innerHTML = 'No images within timeframe';
svgElement.appendChild(textElement); containerElement.appendChild(textElement);
}, },
setNSAttributesForElement(element, attributes) { setNSAttributesForElement(element, attributes) {
Object.keys(attributes).forEach((key) => { if (!element) {
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) {
return; 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, { this.setNSAttributesForElement(imageElement, {
url: url, src: item.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}`
}); });
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, { 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 //create image vertical tick indicator
hoverElement.addEventListener('mousedown', (e) => { 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) { if (e.button === 0) {
this.expand(index); this.expand(index);
} }
}); });
svgElement.appendChild(hoverElement); imagePlaceholder.appendChild(imageElement);
}, return imageWrapper;
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();
});
} }
} }
}; };

View File

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

View File

@ -315,13 +315,31 @@
/*************************************** IMAGERY IN TIMESTRIP VIEWS */ /*************************************** IMAGERY IN TIMESTRIP VIEWS */
.c-imagery-tsv { .c-imagery-tsv {
g.c-imagery-tsv__image-wrapper { div.c-imagery-tsv__image-wrapper {
cursor: pointer; 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; filter: brightness(1) contrast(1) !important;
[class*='__image-handle'] { [class*='__image-handle'] {
fill: $colorBodyFg; background-color: $colorBodyFg;
}
//[class*='__image-placeholder'] {
// display: none;
//}
img {
display: block !important;
} }
} }
} }
@ -331,14 +349,16 @@
} }
&__image-handle { &__image-handle {
fill: rgba($colorBodyFg, 0.5); background-color: rgba($colorBodyFg, 0.5);
} }
&__image-placeholder { &__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 // TODO CH: convert to theme constants
filter: brightness(0.5) contrast(0.7); filter: brightness(0.5) contrast(0.7);
} }

View File

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

View File

@ -22,6 +22,7 @@
import Vue from 'vue'; import Vue from 'vue';
import { import {
createMouseEvent,
createOpenMct, createOpenMct,
resetApplicationState, resetApplicationState,
simulateKeyEvent simulateKeyEvent
@ -32,19 +33,6 @@ const TEN_MINUTES = ONE_MINUTE * 10;
const MAIN_IMAGE_CLASS = '.js-imageryView-image'; const MAIN_IMAGE_CLASS = '.js-imageryView-image';
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new'; const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
const REFRESH_CSS_MS = 500; 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) { function getImageInfo(doc) {
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0]; let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
@ -90,11 +78,13 @@ describe("The Imagery View Layouts", () => {
const START = Date.now(); const START = Date.now();
const COUNT = 10; const COUNT = 10;
let resolveFunction; // let resolveFunction;
let originalRouterPath; let originalRouterPath;
let telemetryPromise;
let telemetryPromiseResolve;
let cleanupFirst;
let openmct; let openmct;
let appHolder;
let parent; let parent;
let child; let child;
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT); let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
@ -198,44 +188,63 @@ describe("The Imagery View Layouts", () => {
// this setups up the app // this setups up the app
beforeEach((done) => { beforeEach((done) => {
appHolder = document.createElement('div'); cleanupFirst = [];
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct(); openmct = createOpenMct();
openmct.time.timeSystem('utc', {
start: START - (5 * ONE_MINUTE),
end: START + (5 * ONE_MINUTE)
});
openmct.install(openmct.plugins.MyItems()); telemetryPromise = new Promise((resolve) => {
openmct.install(openmct.plugins.LocalTimeSystem()); telemetryPromiseResolve = resolve;
openmct.install(openmct.plugins.UTCTimeSystem()); });
spyOn(openmct.telemetry, 'request').and.callFake(() => {
telemetryPromiseResolve(imageTelemetry);
return telemetryPromise;
});
parent = document.createElement('div'); parent = document.createElement('div');
child = document.createElement('div'); parent.style.width = '640px';
parent.appendChild(child); 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({ spyOn(window, 'ResizeObserver').and.returnValue({
observe() {}, observe() {},
disconnect() {} 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({})); spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
originalRouterPath = openmct.router.path; originalRouterPath = openmct.router.path;
openmct.on('start', done); openmct.on('start', done);
openmct.start(appHolder); openmct.startHeadless();
}); });
afterEach(() => { afterEach((done) => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
openmct.router.path = originalRouterPath; 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", () => { 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", () => { 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( let imageryView = applicableViews.find(
viewProvider => viewProvider.key === imageryKey viewProvider => viewProvider.key === imageryKey
); );
@ -315,51 +324,53 @@ describe("The Imagery View Layouts", () => {
let imageryViewProvider; let imageryViewProvider;
let imageryView; let imageryView;
beforeEach(async () => { beforeEach(() => {
let telemetryRequestResolve;
let telemetryRequestPromise = new Promise((resolve) => {
telemetryRequestResolve = resolve;
});
openmct.telemetry.request.and.callFake(() => { applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]);
telemetryRequestResolve(imageTelemetry);
return telemetryRequestPromise;
});
applicableViews = openmct.objectViews.get(imageryObject, []);
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey); imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey);
imageryView = imageryViewProvider.view(imageryObject); imageryView = imageryViewProvider.view(imageryObject, [imageryObject]);
imageryView.show(child); imageryView.show(child);
await telemetryRequestPromise; return Vue.nextTick();
}); });
afterEach(() => { // afterEach(() => {
openmct.time.stopClock(); // openmct.time.stopClock();
openmct.router.removeListener('change:hash', resolveFunction); // openmct.router.removeListener('change:hash', resolveFunction);
//
// imageryView.destroy();
// });
imageryView.destroy(); it("on mount should show the the most recent image", (done) => {
}); //Looks like we need Vue.nextTick here so that computed properties settle down
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();
Vue.nextTick(() => { Vue.nextTick(() => {
const imageInfo = getImageInfo(parent); 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(); 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) => { xit("should show that an image is new", (done) => {
openmct.time.clock('local', {
start: -1000,
end: 1000
});
Vue.nextTick(() => { Vue.nextTick(() => {
// used in code, need to wait to the 500ms here too // used in code, need to wait to the 500ms here too
setTimeout(() => { setTimeout(() => {
@ -385,69 +396,148 @@ describe("The Imagery View Layouts", () => {
}); });
}); });
xit("should navigate via arrow keys", (done) => { it("should navigate via arrow keys", (done) => {
let keyOpts = {
element: parent.querySelector('.c-imagery'),
key: 'ArrowLeft',
keyCode: 37,
type: 'keyup'
};
simulateKeyEvent(keyOpts);
Vue.nextTick(() => { 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); simulateKeyEvent(keyOpts);
done();
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) => { 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(() => { 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(); 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) { export default function PlanViewProvider(openmct) {
function isCompactView(objectPath) { 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 { return {

View File

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

View File

@ -35,12 +35,15 @@
:tick-width="tickWidth" :tick-width="tickWidth"
:single-series="seriesModels.length === 1" :single-series="seriesModels.length === 1"
:series-model="seriesModels[0]" :series-model="seriesModels[0]"
:style="{
left: (plotWidth - tickWidth) + 'px'
}"
@yKeyChanged="setYAxisKey" @yKeyChanged="setYAxisKey"
@tickWidthChanged="onTickWidthChange" @tickWidthChanged="onTickWidthChange"
/> />
<div class="gl-plot-wrapper-display-area-and-x-axis" <div class="gl-plot-wrapper-display-area-and-x-axis"
:style="{ :style="{
left: (tickWidth + 20) + 'px' left: (plotWidth + 20) + 'px'
}" }"
> >
@ -219,7 +222,8 @@ export default {
isRealTime: this.openmct.time.clock() !== undefined, isRealTime: this.openmct.time.clock() !== undefined,
loaded: false, loaded: false,
isTimeOutOfSync: false, isTimeOutOfSync: false,
showLimitLineLabels: undefined showLimitLineLabels: undefined,
isFrozenOnMouseDown: false
}; };
}, },
computed: { computed: {
@ -235,11 +239,9 @@ export default {
} else { } else {
return 'plot-legend-collapsed'; return 'plot-legend-collapsed';
} }
} },
}, plotWidth() {
watch: { return this.plotTickWidth || this.tickWidth;
plotTickWidth(newTickWidth) {
this.onTickWidthChange(newTickWidth, true);
} }
}, },
mounted() { mounted() {
@ -336,6 +338,11 @@ export default {
}, },
loadSeriesData(series) { loadSeriesData(series) {
//this check ensures that duplicate requests don't happen on load
if (!this.timeContext) {
return;
}
if (this.$parent.$refs.plotWrapper.offsetWidth === 0) { if (this.$parent.$refs.plotWrapper.offsetWidth === 0) {
this.scheduleLoad(series); this.scheduleLoad(series);
@ -345,9 +352,12 @@ export default {
this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth; this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
this.startLoading(); this.startLoading();
const bounds = this.timeContext.bounds();
const options = { const options = {
size: this.$parent.$refs.plotWrapper.offsetWidth, 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) series.load(options)
@ -356,9 +366,10 @@ export default {
loadMoreData(range, purge) { loadMoreData(range, purge) {
this.config.series.forEach(plotSeries => { this.config.series.forEach(plotSeries => {
this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
this.startLoading(); this.startLoading();
plotSeries.load({ plotSeries.load({
size: this.$parent.$refs.plotWrapper.offsetWidth, size: this.offsetWidth,
start: range.min, start: range.min,
end: range.max, end: range.max,
domain: this.config.xAxis.get('key') 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) { trackMousePosition(event) {
@ -686,6 +698,11 @@ export default {
this.listenTo(window, 'mouseup', this.onMouseUp, this); this.listenTo(window, 'mouseup', this.onMouseUp, this);
this.listenTo(window, 'mousemove', this.trackMousePosition, 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) { if (event.altKey) {
return this.startPan(event); return this.startPan(event);
} else { } else {
@ -706,7 +723,14 @@ export default {
} }
if (this.marquee) { 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) { 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 { 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 PlotSeries from "./PlotSeries";
import Collection from "./Collection"; import Collection from "./Collection";
import Color from "../lib/Color"; import Color from "@/ui/color/Color";
import ColorPalette from "../lib/ColorPalette"; import ColorPalette from "@/ui/color/ColorPalette";
export default class SeriesCollection extends Collection { export default class SeriesCollection extends Collection {

View File

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

View File

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

View File

@ -25,7 +25,9 @@ import Vue from 'vue';
export default function OverlayPlotViewProvider(openmct) { export default function OverlayPlotViewProvider(openmct) {
function isCompactView(objectPath) { 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 { return {

View File

@ -19,18 +19,12 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import { BAR_GRAPH_KEY } from './barGraph/BarGraphConstants';
import PlotViewProvider from './PlotViewProvider'; import PlotViewProvider from './PlotViewProvider';
import SpectralPlotViewProvider from './spectralPlot/SpectralPlotViewProvider';
import BarGraphViewProvider from './barGraph/BarGraphViewProvider';
import OverlayPlotViewProvider from './overlayPlot/OverlayPlotViewProvider'; import OverlayPlotViewProvider from './overlayPlot/OverlayPlotViewProvider';
import StackedPlotViewProvider from './stackedPlot/StackedPlotViewProvider'; import StackedPlotViewProvider from './stackedPlot/StackedPlotViewProvider';
import PlotsInspectorViewProvider from './inspector/PlotsInspectorViewProvider'; import PlotsInspectorViewProvider from './inspector/PlotsInspectorViewProvider';
import BarGraphInspectorViewProvider from './barGraph/inspector/BarGraphInspectorViewProvider';
import OverlayPlotCompositionPolicy from './overlayPlot/OverlayPlotCompositionPolicy'; import OverlayPlotCompositionPolicy from './overlayPlot/OverlayPlotCompositionPolicy';
import StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy'; import StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy';
import SpectralPlotCompositionPolicy from './spectralPlot/SpectralPlotCompositionPolicy';
import BarGraphCompositionPolicy from './barGraph/BarGraphCompositionPolicy';
export default function () { export default function () {
return function install(openmct) { return function install(openmct) {
@ -64,48 +58,15 @@ export default function () {
}, },
priority: 890 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 StackedPlotViewProvider(openmct));
openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct)); openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct));
openmct.objectViews.addProvider(new PlotViewProvider(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 PlotsInspectorViewProvider(openmct));
openmct.inspectorViews.addProvider(new BarGraphInspectorViewProvider(openmct));
openmct.composition.addPolicy(new OverlayPlotCompositionPolicy(openmct).allow); openmct.composition.addPolicy(new OverlayPlotCompositionPolicy(openmct).allow);
openmct.composition.addPolicy(new StackedPlotCompositionPolicy(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 PlotVuePlugin from "./plugin";
import Vue from "vue"; import Vue from "vue";
import StackedPlot from "./stackedPlot/StackedPlot.vue"; import StackedPlot from "./stackedPlot/StackedPlot.vue";
// import SpectralPlot from "./spectralPlot/SpectralPlot.vue";
import configStore from "./configuration/ConfigStore"; import configStore from "./configuration/ConfigStore";
import EventEmitter from "EventEmitter"; import EventEmitter from "EventEmitter";
import PlotOptions from "./inspector/PlotOptions.vue"; import PlotOptions from "./inspector/PlotOptions.vue";
import PlotConfigurationModel from "./configuration/PlotConfigurationModel"; import PlotConfigurationModel from "./configuration/PlotConfigurationModel";
import { BAR_GRAPH_VIEW, BAR_GRAPH_KEY } from './barGraph/BarGraphConstants';
describe("the plugin", function () { describe("the plugin", function () {
let element; let element;
@ -143,6 +141,7 @@ describe("the plugin", function () {
spyOn(window, 'ResizeObserver').and.returnValue({ spyOn(window, 'ResizeObserver').and.returnValue({
observe() {}, observe() {},
unobserve() {},
disconnect() {} disconnect() {}
}); });
@ -315,37 +314,6 @@ describe("the plugin", function () {
expect(plotView).toBeDefined(); 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", () => { describe("The single plot view", () => {
@ -402,6 +370,10 @@ describe("the plugin", function () {
return Vue.nextTick(); 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", () => { it("Renders a collapsed legend for every telemetry", () => {
let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name"); let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
expect(legend.length).toBe(1); 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', () => { describe('controls in time strip view', () => {
it('zoom controls are hidden', () => { 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", () => { describe("The stacked plot view", () => {
let testTelemetryObject; let testTelemetryObject;
let testTelemetryObject2; let testTelemetryObject2;
@ -1162,42 +1052,11 @@ describe("the plugin", function () {
const yAxisProperties = editOptionsEl.querySelectorAll("div.grid-properties:first-of-type .l-inspector-part"); const yAxisProperties = editOptionsEl.querySelectorAll("div.grid-properties:first-of-type .l-inspector-part");
expect(yAxisProperties.length).toEqual(3); expect(yAxisProperties.length).toEqual(3);
}); });
});
});
describe("the spectral plot", () => { it('renders color palette options', () => {
const mockObject = { const colorSwatch = editOptionsEl.querySelector(".c-click-swatch");
name: 'A Very Nice Spectral Plot', expect(colorSwatch).toBeDefined();
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);
}); });
}); });
}); });

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

View File

@ -25,7 +25,9 @@ import Vue from 'vue';
export default function StackedPlotViewProvider(openmct) { export default function StackedPlotViewProvider(openmct) {
function isCompactView(objectPath) { 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 { return {

View File

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

View File

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

View File

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

View File

@ -222,6 +222,24 @@ describe("the plugin", () => {
openmct.router.path = originalRouterPath; 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", () => { it("Renders a row for every telemetry datum returned", () => {
let rows = element.querySelectorAll('table.c-telemetry-table__body tr'); let rows = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(rows.length).toBe(3); expect(rows.length).toBe(3);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,8 @@
<div <div
class="l-browse-bar__object-name--w c-object-label" 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" :class="type.definition.cssClass"
></div> ></div>
<span class="l-browse-bar__object-name c-object-label__name"> <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) => { provider.view = (domainObject, objectPath) => {
const viewObject = wrappedView(domainObject, objectPath); const viewObject = wrappedView(domainObject, objectPath);
const wrappedShow = viewObject.show.bind(viewObject); const wrappedShow = viewObject.show.bind(viewObject);
viewObject.show = (element, isEditing) => { viewObject.show = (element, isEditing, viewOptions) => {
viewObject.parentElement = element.parentElement; viewObject.parentElement = element.parentElement;
wrappedShow(element, isEditing); wrappedShow(element, isEditing, viewOptions);
}; };
return viewObject; 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 * @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)); 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) { isNavigatedObject(objectPath) {
let targetObject = objectPath[0]; let targetObject = objectPath[0];
let navigatedObject = this.path[0]; let navigatedObject = this.path[0];

View File

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