Compare commits

..

3 Commits

Author SHA1 Message Date
bc72fd54e8 clean out summaryWidgets initialize block, move logic to separate functions 2017-11-29 10:43:05 -08:00
9569ee2b56 refactor SummaryWidget.js initialize 2017-11-28 15:15:20 -08:00
cea6d4978a partial fix for issue #1804
Rename Rule.initCondition to Rule.addCondition for parity with Rule.removeCondition
2017-11-28 13:38:03 -08:00
223 changed files with 10025 additions and 10055 deletions

View File

@ -21,6 +21,5 @@
"shadow": "outer",
"strict": "implied",
"undef": true,
"unused": "vars",
"latedef": "nofunc"
"unused": "vars"
}

View File

@ -88,7 +88,7 @@ and [`gulp`](http://gulpjs.com/).
To build Open MCT for deployment:
`npm run prepare`
`npm run prepublish`
This will compile and minify JavaScript sources, as well as copy over assets.
The contents of the `dist` folder will contain a runnable Open MCT

View File

@ -1,11 +1,3 @@
machine:
node:
version: 4.7.0
dependencies:
pre:
- npm install -g npm@latest
deployment:
production:
branch: master
@ -24,4 +16,4 @@ test:
general:
branches:
ignore:
- gh-pages
- gh-pages

View File

@ -2283,7 +2283,7 @@ To install build dependencies (only needs to be run once):
To build:
`npm run prepare`
`npm run prepublish`
This will compile and minify JavaScript sources, as well as copy over assets.
The contents of the `dist` folder will contain a runnable Open MCT

View File

@ -1,121 +0,0 @@
# Security Guide
Open MCT is a rich client with plugin support that executes as a single page
web application in a browser environment. Security concerns and
vulnerabilities associated with the web as a platform should be considered
before deploying Open MCT (or any other web application) for mission or
production usage.
This document describes several important points to consider when developing
for or deploying Open MCT securely. Other resources such as
[Open Web Application Security Project (OWASP)](https://www.owasp.org)
provide a deeper and more general overview of security for web applications.
## Security Model
Open MCT has been architected assuming the following deployment pattern:
* A tagged, tested Open MCT version will be used.
* Externally authored plugins will be installed.
* A server will provide persistent storage, telemetry, and other shared data.
* Authorization, authentication, and auditing will be handled by a server.
## Security Procedures
The Open MCT team secures our code base using a combination of code review,
dependency review, and periodic security reviews. Static analysis performed
during automated verification additionally safeguards against common
coding errors which may result in vulnerabilities.
### Code Review
All contributions are reviewed by internal team members. External
contributors receive increased scrutiny for security and quality,
and must sign a licensing agreement.
### Dependency Review
Before integrating third-party dependencies, they are reviewed for security
and quality, with consideration given to authors and users of these
dependencies, as well as review of open source code.
### Periodic Security Reviews
Open MCT's code, design, and architecture are periodically reviewed
(approximately annually) for common security issues, such as the
[OWASP Top Ten](https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project).
## Security Concerns
Certain security concerns deserve special attention when deploying Open MCT,
or when authoring plugins.
### Identity Spoofing
Open MCT issues calls to web services with the privileges of a logged in user.
Compromised sources (either for Open MCT itself or a plugin) could
therefore allow malicious code to execute with those privileges.
To avoid this:
* Serve Open MCT and other scripts over SSL (https rather than http)
to prevent man-in-the-middle attacks.
* Exercise precautions such as security reviews for any plugins or
applications built for or with Open MCT to reject malicious changes.
### Information Disclosure
If Open MCT is used to handle or display sensitive data, any components
(such as adapter plugins) must take care to avoid leaking or disclosing
this information. For example, avoid sending sensitive data to third-party
servers or insecure APIs.
### Data Tampering
The web application architecture leaves open the possibility that direct
calls will be made to back-end services, circumventing Open MCT entirely.
As such, Open MCT assumes that server components will perform any necessary
data validation during calls issues to the server.
Additionally, plugins which serialize and write data to the server must
escape that data to avoid database injection attacks, and similar.
### Repudiation
Open MCT assumes that servers log any relevant interactions and associates
these with a user identity; the specific user actions taken within the
application are assumed not to be of concern for auditing.
In the absence of server-side logging, users may disclaim (maliciously,
mistakenly, or otherwise) actions taken within the system without any
way to prove otherwise.
If keeping client-level interactions is important, this will need to be
implemented via a plugin.
### Denial-of-service
Open MCT assumes that server-side components will be insulated against
denial-of-service attacks. Services should only permit resource-intensive
tasks to be initiated by known or trusted users.
### Elevation of Privilege
Corollary to the assumption that servers guide against identity spoofing,
Open MCT assumes that services do not allow a user to act with
inappropriately escalated privileges. Open MCT cannot protect against
such escalation; in the clearest case, a malicious actor could interact
with web services directly to exploit such a vulnerability.
## Additional Reading
The following resources have been used as a basis for identifying potential
security threats to Open MCT deployments in preparation of this document:
* [STRIDE model](https://www.owasp.org/index.php/Threat_Risk_Modeling#STRIDE)
* [Attack Surface Analysis Cheat Sheet](https://www.owasp.org/index.php/Attack_Surface_Analysis_Cheat_Sheet)
* [XSS Prevention Cheat Sheet](https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet)

View File

@ -30,8 +30,7 @@ define([
amplitude: 1,
period: 10,
offset: 0,
dataRateInHz: 1,
phase: 0
dataRateInHz: 1
};
function GeneratorProvider() {
@ -51,12 +50,9 @@ define([
'amplitude',
'period',
'offset',
'dataRateInHz',
'phase',
'dataRateInHz'
];
request = request || {};
var workerRequest = {};
props.forEach(function (prop) {
@ -71,7 +67,7 @@ define([
}
workerRequest[prop] = Number(workerRequest[prop]);
});
workerRequest.name = domainObject.name;
return workerRequest;
};

View File

@ -1,80 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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 StateGeneratorProvider() {
}
function pointForTimestamp(timestamp, duration, name) {
return {
name: name,
utc: Math.floor(timestamp / duration) * duration,
value: Math.floor(timestamp / duration) % 2
};
}
StateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) {
return domainObject.type === 'example.state-generator';
};
StateGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
var duration = domainObject.telemetry.duration * 1000;
var interval = setInterval(function () {
var now = Date.now();
callback(pointForTimestamp(now, duration, domainObject.name));
}, duration);
return function () {
clearInterval(interval);
};
};
StateGeneratorProvider.prototype.supportsRequest = function (domainObject, options) {
return domainObject.type === 'example.state-generator';
};
StateGeneratorProvider.prototype.request = function (domainObject, options) {
var start = options.start;
var end = options.end;
var duration = domainObject.telemetry.duration * 1000;
if (options.strategy === 'latest' || options.size === 1) {
start = end;
}
var data = [];
while (start <= end && data.length < 5000) {
data.push(pointForTimestamp(start, duration, domainObject.name));
start += 5000;
}
return Promise.resolve(data);
};
return StateGeneratorProvider;
});

View File

@ -44,7 +44,9 @@ define([
message = message.data;
var callback = this.callbacks[message.id];
if (callback) {
callback(message);
if (callback(message)) {
delete this.callbacks[message.id];
}
}
};
@ -70,7 +72,6 @@ define([
deferred.resolve = resolve;
deferred.reject = reject;
});
var messageId;
function callback(message) {
if (message.error) {
@ -78,27 +79,33 @@ define([
} else {
deferred.resolve(message.data);
}
delete this.callbacks[messageId];
return true;
}
messageId = this.dispatch('request', request, callback.bind(this));
this.dispatch('request', request, callback);
return promise;
};
WorkerInterface.prototype.subscribe = function (request, cb) {
function callback(message) {
var isCancelled = false;
var callback = function (message) {
if (isCancelled) {
return true;
}
cb(message.data);
};
var messageId = this.dispatch('subscribe', request, callback);
var messageId = this.dispatch('subscribe', request, callback)
return function () {
isCancelled = true;
this.dispatch('unsubscribe', {
id: messageId
});
delete this.callbacks[messageId];
}.bind(this);
};

View File

@ -62,11 +62,10 @@
self.postMessage({
id: message.id,
data: {
name: data.name,
utc: nextStep,
yesterday: nextStep - 60*60*24*1000,
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase),
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase)
sin: sin(nextStep, data.period, data.amplitude, data.offset),
cos: cos(nextStep, data.period, data.amplitude, data.offset)
}
});
nextStep += step;
@ -83,22 +82,21 @@
}
function onRequest(message) {
var request = message.data;
if (request.end == undefined) {
request.end = Date.now();
var data = message.data;
if (data.end == undefined) {
data.end = Date.now();
}
if (request.start == undefined){
request.start = request.end - FIFTEEN_MINUTES;
if (data.start == undefined){
data.start = data.end - FIFTEEN_MINUTES;
}
var now = Date.now();
var start = request.start;
var end = request.end > now ? now : request.end;
var amplitude = request.amplitude;
var period = request.period;
var offset = request.offset;
var dataRateInHz = request.dataRateInHz;
var phase = request.phase;
var start = data.start;
var end = data.end > now ? now : data.end;
var amplitude = data.amplitude;
var period = data.period;
var offset = data.offset;
var dataRateInHz = data.dataRateInHz;
var step = 1000 / dataRateInHz;
var nextStep = start - (start % step) + step;
@ -107,11 +105,10 @@
for (; nextStep < end && data.length < 5000; nextStep += step) {
data.push({
name: request.name,
utc: nextStep,
yesterday: nextStep - 60*60*24*1000,
sin: sin(nextStep, period, amplitude, offset, phase),
cos: cos(nextStep, period, amplitude, offset, phase)
sin: sin(nextStep, period, amplitude, offset),
cos: cos(nextStep, period, amplitude, offset)
});
}
self.postMessage({
@ -120,14 +117,14 @@
});
}
function cos(timestamp, period, amplitude, offset, phase) {
function cos(timestamp, period, amplitude, offset) {
return amplitude *
Math.cos(phase + (timestamp / period / 1000 * Math.PI * 2)) + offset;
Math.cos(timestamp / period / 1000 * Math.PI * 2) + offset;
}
function sin(timestamp, period, amplitude, offset, phase) {
function sin(timestamp, period, amplitude, offset) {
return amplitude *
Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + offset;
Math.sin(timestamp / period / 1000 * Math.PI * 2) + offset;
}
function sendError(error, message) {

View File

@ -23,12 +23,10 @@
define([
"./GeneratorProvider",
"./SinewaveLimitCapability",
"./StateGeneratorProvider"
"./SinewaveLimitCapability"
], function (
GeneratorProvider,
SinewaveLimitCapability,
StateGeneratorProvider
SinewaveLimitCapability
) {
var legacyExtensions = {
@ -48,75 +46,6 @@ define([
openmct.legacyExtension(type, extension)
})
});
openmct.types.addType("example.state-generator", {
name: "State Generator",
description: "For development use. Generates test enumerated telemetry by cycling through a given set of states",
cssClass: "icon-telemetry",
creatable: true,
form: [
{
name: "State Duration (seconds)",
control: "textfield",
cssClass: "l-input-sm l-numeric",
key: "duration",
required: true,
property: [
"telemetry",
"duration"
],
pattern: "^\\d*(\\.\\d*)?$"
}
],
initialize: function (object) {
object.telemetry = {
duration: 5,
values: [
{
key: "name",
name: "Name"
},
{
key: "utc",
name: "Time",
format: "utc",
hints: {
domain: 1
}
},
{
key: "state",
source: "value",
name: "State",
format: "enum",
enumerations: [
{
value: 0,
string: "OFF"
},
{
value: 1,
string: "ON"
}
],
hints: {
range: 1
}
},
{
key: "value",
name: "Value",
hints: {
range: 2
}
}
]
}
}
});
openmct.telemetry.addProvider(new StateGeneratorProvider());
openmct.types.addType("generator", {
name: "Sine Wave Generator",
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
@ -170,18 +99,6 @@ define([
"dataRateInHz"
],
pattern: "^\\d*(\\.\\d*)?$"
},
{
name: "Phase (radians)",
control: "textfield",
cssClass: "l-input-sm l-numeric",
key: "phase",
required: true,
property: [
"telemetry",
"phase"
],
pattern: "^\\d*(\\.\\d*)?$"
}
],
initialize: function (object) {
@ -190,12 +107,7 @@ define([
amplitude: 1,
offset: 0,
dataRateInHz: 1,
phase: 0,
values: [
{
key: "name",
name: "Name"
},
{
key: "utc",
name: "Time",
@ -230,7 +142,6 @@ define([
};
}
});
openmct.telemetry.addProvider(new GeneratorProvider());
};

View File

@ -48,9 +48,8 @@ define([
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg"
];
function pointForTimestamp(timestamp, name) {
function pointForTimestamp(timestamp) {
return {
name: name,
utc: Math.floor(timestamp / 5000) * 5000,
url: IMAGE_SAMPLES[Math.floor(timestamp / 5000) % IMAGE_SAMPLES.length]
};
@ -62,7 +61,7 @@ define([
},
subscribe: function (domainObject, callback) {
var interval = setInterval(function () {
callback(pointForTimestamp(Date.now(), domainObject.name));
callback(pointForTimestamp(Date.now()));
}, 5000);
return function (interval) {
@ -80,8 +79,8 @@ define([
var start = options.start;
var end = options.end;
var data = [];
while (start <= end && data.length < 5000) {
data.push(pointForTimestamp(start, domainObject.name));
while (start < end && data.length < 5000) {
data.push(pointForTimestamp(start));
start += 5000;
}
return Promise.resolve(data);
@ -94,7 +93,7 @@ define([
options.strategy === 'latest';
},
request: function (domainObject, options) {
return Promise.resolve([pointForTimestamp(Date.now(), domainObject.name)]);
return Promise.resolve([pointForTimestamp(Date.now())]);
}
};
@ -110,10 +109,6 @@ define([
initialize: function (object) {
object.telemetry = {
values: [
{
name: 'Name',
key: 'name'
},
{
name: 'Time',
key: 'utc',

View File

@ -58,7 +58,11 @@
position: relative;
}
.w-mct-example > div { margin-bottom: $interiorMarginLg; }
.w-mct-example {
div {
margin-bottom: $interiorMarginLg;
}
}
code,
pre {

View File

@ -22,8 +22,6 @@
/*global require,__dirname*/
require("v8-compile-cache");
var gulp = require('gulp'),
sourcemaps = require('gulp-sourcemaps'),
path = require('path'),
@ -179,4 +177,4 @@ gulp.task('install', [ 'assets', 'scripts' ]);
gulp.task('verify', [ 'lint', 'test', 'checkstyle' ]);
gulp.task('build', [ 'verify', 'install' ]);
gulp.task('build', [ 'verify', 'install' ]);

View File

@ -43,9 +43,6 @@
openmct.install(openmct.plugins.ExampleImagery());
openmct.install(openmct.plugins.UTCTimeSystem());
openmct.install(openmct.plugins.ImportExport());
openmct.install(openmct.plugins.AutoflowView({
type: "telemetry.panel"
}));
openmct.install(openmct.plugins.Conductor({
menuOptions: [
{

View File

@ -36,7 +36,6 @@ module.exports = function(config) {
files: [
{pattern: 'bower_components/**/*.js', included: false},
{pattern: 'node_modules/d3-*/**/*.js', included: false},
{pattern: 'node_modules/vue/**/*.js', included: false},
{pattern: 'src/**/*.js', included: false},
{pattern: 'example/**/*.html', included: false},
{pattern: 'example/**/*.js', included: false},
@ -89,8 +88,7 @@ module.exports = function(config) {
"dist/reports/coverage",
check: {
global: {
lines: 80,
excludes: ['src/plugins/plot/**/*.js']
lines: 80
}
}
},

View File

@ -37,10 +37,9 @@ requirejs.config({
"screenfull": "bower_components/screenfull/dist/screenfull.min",
"text": "bower_components/text/text",
"uuid": "bower_components/node-uuid/uuid",
"vue": "node_modules/vue/dist/vue.min",
"zepto": "bower_components/zepto/zepto.min",
"lodash": "bower_components/lodash/lodash",
"d3-selection": "node_modules/d3-selection/dist/d3-selection.min",
"d3-selection": "node_modules/d3-selection/build/d3-selection.min",
"d3-scale": "node_modules/d3-scale/build/d3-scale.min",
"d3-axis": "node_modules/d3-axis/build/d3-axis.min",
"d3-array": "node_modules/d3-array/build/d3-array.min",
@ -67,9 +66,6 @@ requirejs.config({
"moment-duration-format": {
"deps": ["moment"]
},
"saveAs": {
"exports": "saveAs"
},
"screenfull": {
"exports": "screenfull"
},
@ -101,7 +97,6 @@ define([
var openmct = new MCT();
openmct.legacyRegistry = defaultRegistry;
openmct.install(openmct.plugins.Plot());
if (typeof BUILD_CONSTANTS !== 'undefined') {
openmct.install(buildInfo(BUILD_CONSTANTS));

View File

@ -15,8 +15,7 @@
"d3-time-format": "^2.0.3",
"express": "^4.13.1",
"minimist": "^1.1.1",
"request": "^2.69.0",
"vue": "^2.5.6"
"request": "^2.69.0"
},
"devDependencies": {
"bower": "^1.7.7",
@ -50,8 +49,7 @@
"moment": "^2.11.1",
"node-bourbon": "^4.2.3",
"requirejs": "2.1.x",
"split": "^1.0.0",
"v8-compile-cache": "^1.1.0"
"split": "^1.0.0"
},
"scripts": {
"start": "node app.js",
@ -61,7 +59,7 @@
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
"otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'",
"docs": "npm run jsdoc ; npm run otherdoc",
"prepare": "node ./node_modules/bower/bin/bower install && node ./node_modules/gulp/bin/gulp.js install"
"prepublish": "node ./node_modules/bower/bin/bower install && node ./node_modules/gulp/bin/gulp.js install"
},
"repository": {
"type": "git",

View File

@ -57,12 +57,7 @@
</div>
<mct-representation key="representation.selected.key"
mct-object="representation.selected.key && domainObject"
class="abs flex-elem grows object-holder-main scroll"
mct-selectable="{
item: domainObject.useCapability('adapter'),
oldItem: domainObject
}"
mct-init-select>
class="abs flex-elem grows object-holder-main scroll">
</mct-representation>
</div>
</div>

View File

@ -19,21 +19,12 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div ng-controller="InspectorController as controller">
<div ng-controller="InspectorController">
<div ng-repeat="region in regions">
<mct-representation
key="'object-properties'"
mct-object="controller.selectedItem()"
key="region.content.key"
mct-object="domainObject"
ng-model="ngModel">
</mct-representation>
<div ng-if="!controller.hasProviderView()">
<mct-representation
key="inspectorKey"
mct-object="controller.selectedItem()"
ng-model="ngModel">
</mct-representation>
</div>
<div class='inspector-provider-view'>
</div>
</div>
</div>

View File

@ -38,6 +38,8 @@
ng-class="{ last:($index + 1) === contextualParents.length }">
<mct-representation key="'label'"
mct-object="parent"
ng-model="ngModel"
ng-click="ngModel.selectedObject = parent"
class="location-item">
</mct-representation>
</span>
@ -49,6 +51,8 @@
ng-class="{ last:($index + 1) === primaryParents.length }">
<mct-representation key="'label'"
mct-object="parent"
ng-model="ngModel"
ng-click="ngModel.selectedObject = parent"
class="location-item">
</mct-representation>
</span>

View File

@ -121,8 +121,7 @@ define([
"key": "ElementsController",
"implementation": ElementsController,
"depends": [
"$scope",
"openmct"
"$scope"
]
},
{
@ -300,6 +299,9 @@ define([
{
"key": "edit-elements",
"template": elementsTemplate,
"uses": [
"composition"
],
"gestures": [
"drop"
]
@ -383,10 +385,7 @@ define([
]
},
{
"implementation": EditToolbarRepresenter,
"depends": [
"openmct"
]
"implementation": EditToolbarRepresenter
}
],
"constants": [

View File

@ -61,12 +61,7 @@
<mct-representation key="representation.selected.key"
mct-object="representation.selected.key && domainObject"
class="abs flex-elem grows object-holder-main scroll"
toolbar="toolbar"
mct-selectable="{
item: domainObject.useCapability('adapter'),
oldItem: domainObject
}"
mct-init-select>
toolbar="toolbar">
</mct-representation>
</div><!--/ l-object-wrapper-inner -->
</div>

View File

@ -25,7 +25,7 @@
ng-model="filterBy">
</mct-include>
<div class="flex-elem grows vscroll">
<ul class="tree" ng-if="composition.length > 0">
<ul class="tree">
<li ng-repeat="containedObject in composition | filter:searchElements">
<span class="tree-item">
<mct-representation
@ -36,6 +36,5 @@
</span>
</li>
</ul>
<div ng-if="composition.length === 0">No contained elements</div>
</div>
</div>

View File

@ -28,6 +28,16 @@ define(
[],
function () {
function isDirty(domainObject) {
var navigatedObject = domainObject,
editorCapability = navigatedObject &&
navigatedObject.getCapability("editor");
return editorCapability &&
editorCapability.isEditContextRoot() &&
editorCapability.dirty();
}
function cancelEditing(domainObject) {
var navigatedObject = domainObject,
editorCapability = navigatedObject &&
@ -49,7 +59,10 @@ define(
var removeCheck = navigationService
.checkBeforeNavigation(function () {
return "Continuing will cause the loss of any unsaved changes.";
if (isDirty(domainObject)) {
return "Continuing will cause the loss of any unsaved changes.";
}
return false;
});
$scope.$on('$destroy', function () {

View File

@ -29,11 +29,7 @@ define(
*
* @constructor
*/
function ElementsController($scope, openmct) {
this.scope = $scope;
this.scope.composition = [];
var self = this;
function ElementsController($scope) {
function filterBy(text) {
if (typeof text === 'undefined') {
return $scope.searchText;
@ -51,54 +47,10 @@ define(
}
}
function setSelection(selection) {
if (!selection[0]) {
return;
}
if (self.mutationListener) {
self.mutationListener();
delete self.mutationListener;
}
var domainObject = selection[0].context.oldItem;
self.refreshComposition(domainObject);
if (domainObject) {
self.mutationListener = domainObject.getCapability('mutation')
.listen(self.refreshComposition.bind(self, domainObject));
}
}
$scope.filterBy = filterBy;
$scope.searchElements = searchElements;
openmct.selection.on('change', setSelection);
setSelection(openmct.selection.get());
$scope.$on("$destroy", function () {
openmct.selection.off("change", setSelection);
});
}
/**
* Gets the composition for the selected object and populates the scope with it.
*
* @param domainObject the selected object
* @private
*/
ElementsController.prototype.refreshComposition = function (domainObject) {
var selectedObjectComposition = domainObject && domainObject.useCapability('composition');
if (selectedObjectComposition) {
selectedObjectComposition.then(function (composition) {
this.scope.composition = composition;
}.bind(this));
} else {
this.scope.composition = [];
}
};
return ElementsController;
}
);

View File

@ -38,7 +38,7 @@ define(
* @constructor
* @implements {Representer}
*/
function EditToolbarRepresenter(openmct, scope, element, attrs) {
function EditToolbarRepresenter(scope, element, attrs) {
var self = this;
// Mark changes as ready to persist
@ -109,7 +109,6 @@ define(
this.updateSelection = updateSelection;
this.toolbar = undefined;
this.toolbarObject = {};
this.openmct = openmct;
// If this representation exposes a toolbar, set up watches
// to synchronize with it.
@ -147,7 +146,7 @@ define(
// Expose the toolbar object to the parent scope
initialize(definition);
// Create a selection scope
this.setSelection(new EditToolbarSelection(this.openmct));
this.setSelection(new EditToolbarSelection());
// Initialize toolbar to an empty selection
this.updateSelection([]);
};

View File

@ -38,24 +38,10 @@ define(
* @memberof platform/commonUI/edit
* @constructor
*/
function EditToolbarSelection(openmct) {
function EditToolbarSelection() {
this.selection = [{}];
this.selecting = false;
this.selectedObj = undefined;
openmct.selection.on('change', function (selection) {
var selected = selection[0];
if (selected && selected.context.toolbar) {
this.select(selected.context.toolbar);
} else {
this.deselect();
}
if (selected && selected.context.viewProxy) {
this.proxy(selected.context.viewProxy);
}
}.bind(this));
}
/**

View File

@ -104,10 +104,10 @@ define(
mockEditorCapability.isEditContextRoot.andReturn(false);
mockEditorCapability.dirty.andReturn(false);
expect(checkFn()).toBe("Continuing will cause the loss of any unsaved changes.");
expect(checkFn()).toBe(false);
mockEditorCapability.isEditContextRoot.andReturn(true);
expect(checkFn()).toBe("Continuing will cause the loss of any unsaved changes.");
expect(checkFn()).toBe(false);
mockEditorCapability.dirty.andReturn(true);
expect(checkFn())

View File

@ -27,47 +27,11 @@ define(
describe("The Elements Pane controller", function () {
var mockScope,
mockOpenMCT,
mockSelection,
mockDomainObject,
mockMutationCapability,
mockUnlisten,
selectable = [],
controller;
beforeEach(function () {
mockUnlisten = jasmine.createSpy('unlisten');
mockMutationCapability = jasmine.createSpyObj("mutationCapability", [
"listen"
]);
mockMutationCapability.listen.andReturn(mockUnlisten);
mockDomainObject = jasmine.createSpyObj("domainObject", [
"getCapability",
"useCapability"
]);
mockDomainObject.useCapability.andCallThrough();
mockDomainObject.getCapability.andReturn(mockMutationCapability);
mockScope = jasmine.createSpyObj("$scope", ['$on']);
mockSelection = jasmine.createSpyObj("selection", [
'on',
'off',
'get'
]);
mockSelection.get.andReturn([]);
mockOpenMCT = {
selection: mockSelection
};
selectable[0] = {
context: {
oldItem: mockDomainObject
}
};
spyOn(ElementsController.prototype, 'refreshComposition');
controller = new ElementsController(mockScope, mockOpenMCT);
mockScope = jasmine.createSpy("$scope");
controller = new ElementsController(mockScope);
});
function getModel(model) {
@ -99,44 +63,6 @@ define(
expect(objects.filter(mockScope.searchElements).length).toBe(4);
});
it("refreshes composition on selection", function () {
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(ElementsController.prototype.refreshComposition).toHaveBeenCalledWith(mockDomainObject);
});
it("listens on mutation and refreshes composition", function () {
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(mockDomainObject.getCapability).toHaveBeenCalledWith('mutation');
expect(mockMutationCapability.listen).toHaveBeenCalled();
expect(ElementsController.prototype.refreshComposition.calls.length).toBe(1);
mockMutationCapability.listen.mostRecentCall.args[0](mockDomainObject);
expect(ElementsController.prototype.refreshComposition.calls.length).toBe(2);
});
it("cleans up mutation listener when selection changes", function () {
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(mockMutationCapability.listen).toHaveBeenCalled();
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(mockUnlisten).toHaveBeenCalled();
});
it("does not listen on mutation for element proxy selectable", function () {
selectable[0] = {
context: {
elementProxy: {}
}
};
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(mockDomainObject.getCapability).not.toHaveBeenCalledWith('mutation');
});
});
}
);

View File

@ -29,9 +29,7 @@ define(
mockElement,
testAttrs,
mockUnwatch,
representer,
mockOpenMCT,
mockSelection;
representer;
beforeEach(function () {
mockScope = jasmine.createSpyObj(
@ -48,18 +46,7 @@ define(
mockScope.$parent.$watchCollection.andReturn(mockUnwatch);
mockSelection = jasmine.createSpyObj("selection", [
'on',
'off',
'get'
]);
mockSelection.get.andReturn([]);
mockOpenMCT = {
selection: mockSelection
};
representer = new EditToolbarRepresenter(
mockOpenMCT,
mockScope,
mockElement,
testAttrs

View File

@ -28,25 +28,13 @@ define(
var testProxy,
testElement,
otherElement,
selection,
mockSelection,
mockOpenMCT;
selection;
beforeEach(function () {
testProxy = { someKey: "some value" };
testElement = { someOtherKey: "some other value" };
otherElement = { yetAnotherKey: 42 };
mockSelection = jasmine.createSpyObj("selection", [
// 'select',
'on',
'off',
'get'
]);
mockSelection.get.andReturn([]);
mockOpenMCT = {
selection: mockSelection
};
selection = new EditToolbarSelection(mockOpenMCT);
selection = new EditToolbarSelection();
selection.proxy(testProxy);
});

View File

@ -121,9 +121,6 @@ define([
};
UTCTimeFormat.prototype.parse = function (text) {
if (typeof text === 'number') {
return text;
}
return moment.utc(text, DATE_FORMATS).valueOf();
};

View File

@ -41,7 +41,6 @@ define([
"./src/controllers/BannerController",
"./src/directives/MCTContainer",
"./src/directives/MCTDrag",
"./src/directives/MCTSelectable",
"./src/directives/MCTClickElsewhere",
"./src/directives/MCTResize",
"./src/directives/MCTPopup",
@ -91,7 +90,6 @@ define([
BannerController,
MCTContainer,
MCTDrag,
MCTSelectable,
MCTClickElsewhere,
MCTResize,
MCTPopup,
@ -330,13 +328,6 @@ define([
"$document"
]
},
{
"key": "mctSelectable",
"implementation": MCTSelectable,
"depends": [
"openmct"
]
},
{
"key": "mctClickElsewhere",
"implementation": MCTClickElsewhere,

View File

@ -99,7 +99,7 @@ $plotXBarH: 32px;
$plotLegendH: 20px;
$plotSwatchD: 8px;
// 1: Top, 2: right, 3: bottom, 4: left
$plotDisplayArea: (0, 0, $plotXBarH, $plotYBarW);
$plotDisplayArea: ($plotLegendH + $interiorMargin, 0, $plotXBarH, $plotYBarW);
/* min plot height is based on user testing to find minimum useful height */
$plotMinH: 95px;
/*************** Bubbles */

View File

@ -25,7 +25,6 @@
}
.l-fixed-position-item {
border-width: 1px;
position: absolute;
&.s-not-selected {
opacity: 0.8;

View File

@ -40,7 +40,7 @@
* Use https://icomoon.io/app with icomoon-project-openmct-symbols-12px.json
* to generate font files
*/
font-family: 'symbolsfont-12px';
font-family: 'symbolsfont 12px';
src: url($dirCommonRes + 'fonts/symbols/openmct-symbols-12px.eot');
src: url($dirCommonRes + 'fonts/symbols/openmct-symbols-12px.eot?#iefix') format('embedded-opentype'),
url($dirCommonRes + 'fonts/symbols/openmct-symbols-12px.woff') format('woff'),
@ -248,12 +248,6 @@ a.disabled {
color: rgba(#fff, 0.2);
}
.comma-list span {
&:not(:first-child) {
&:before { content: ', '; }
}
}
.test-stripes {
@include bgDiagonalStripes();
}

View File

@ -44,12 +44,6 @@
}
}
.t-alert-unsynced {
@extend .icon-alert-triangle;
color: $colorPausedBg;
}
.bar .ui-symbol {
display: inline-block;
}
@ -87,5 +81,18 @@
@include transform(scale(0.3));
z-index: 2;
}
/* .t-item-icon-glyph {
&:after {
color: $colorIconLink;
content: '\e921'; //$glyph-icon-link;
height: auto; width: auto;
position: absolute;
left: 0; top: 0; right: 0; bottom: 20%;
@include transform-origin(bottom left);
@include transform(scale(0.3));
z-index: 2;
}
}*/
}
}

View File

@ -53,7 +53,6 @@
.l-inspector-part {
box-sizing: border-box;
padding-right: $interiorMargin;
.tree .form {
margin-left: $treeVCW + $interiorMarginLg;
}
@ -79,7 +78,6 @@
}
}
.form-row {
// To be replaced with .inspector-config, see below.
@include align-items(center);
border: none !important;
margin-bottom: 0 !important;
@ -101,12 +99,15 @@
position: relative;
}
ul li {
margin-bottom: $interiorMarginLg;
}
em.t-inspector-part-header {
border-radius: $basicCr;
background-color: $colorInspectorSectionHeaderBg;
color: $colorInspectorSectionHeaderFg;
margin-top: $interiorMarginLg;
//margin-bottom: $interiorMargin;
margin-bottom: $interiorMargin;
padding: floor($formTBPad * .75) $formLRPad;
text-transform: uppercase;
}
@ -200,102 +201,3 @@ mct-representation:not(.s-status-editing) .l-inspect {
pointer-events: inherit;
}
}
// NEW COMPACT FORM, FOR USE IN INSPECTOR
// ul > li > label, control
// Make a new UL for each form section
// Allow control-first, controls-below
.l-inspect .tree ul li,
.inspector-config ul li {
padding: 2px 0;
}
.inspector-config {
$labelW: 40%;
$minW: $labelW;
ul {
margin-bottom: $interiorMarginLg;
li {
@include display(flex);
@include flex-wrap(wrap);
@include align-items(center);
label,
.control {
@include display(flex);
min-width: $minW;
}
label {
line-height: inherit;
padding: $interiorMarginSm 0;
width: $labelW;
}
.control {
@include flex-grow(1);
}
&:not(.section-header) {
&:not(.connects-to-previous) {
//border-top: 1px solid $colorFormLines;
}
}
&.connects-to-previous {
padding-top: 0 !important;
}
&.section-header {
margin-top: $interiorMarginLg;
border-top: 1px solid $colorFormLines;
}
&.controls-first {
.control {
@include flex-grow(0);
margin-right: $interiorMargin;
min-width: 0;
order: 1;
width: auto;
}
label {
@include flex-grow(1);
order: 2;
width: auto;
}
}
&.controls-under {
display: block;
.control, label {
display: block;
width: auto;
}
ul li {
border-top: none !important;
padding: 0;
}
}
}
}
.form-error {
// Block element that visually flags an error and contains a message
background-color: $colorFormFieldErrorBg;
color: $colorFormFieldErrorFg;
border-radius: $basicCr;
display: block;
padding: 1px 6px;
&:before {
content: $glyph-icon-alert-triangle;
display: inline;
font-family: symbolsfont;
margin-right: $interiorMarginSm;
}
}
}
.tree .inspector-config {
margin-left: $treeVCW + $interiorMarginLg;
}

View File

@ -70,7 +70,6 @@
@import "fixed-position";
@import "lists/tabular";
@import "plots/plots-main";
@import "plots/legend";
@import "iframe";
@import "views";
@import "items/item";

View File

@ -5,7 +5,6 @@
}
.l-view-section {
//@include test(orange, 0.1);
@include absPosDefault(0);
h2 {
color: #fff;

View File

@ -150,26 +150,6 @@
}
}
/******************************************************** VIEW CONTROLS */
// Expand/collapse > and v arrows, used in tree and plot legend
// Moved this over from a tree-only context 5/18/17
.view-control {
@extend .ui-symbol;
cursor: pointer;
height: 1em; width: 1em;
line-height: inherit;
&:before {
position: absolute;
@include trans-prop-nice(transform, 100ms);
content: $glyph-icon-arrow-right;
@include transform-origin(center);
}
&.expanded:before {
@include transform(rotate(90deg));
}
}
/******************************************************** CUSTOM CHECKBOXES */
label.checkbox.custom,
label.radio.custom {

View File

@ -398,6 +398,10 @@ body.desktop .t-message-list {
.object-header {
.t-object-alert {
display: inline;
&.t-alert-unsynced {
@extend .icon-alert-triangle;
color: $colorPausedBg;
}
}
}
}

View File

@ -20,70 +20,53 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
.l-palette {
$d: 16px;
$colorsPerRow: 10;
$m: 1;
box-sizing: border-box;
padding: $interiorMargin !important;
}
.l-palette-row {
$d: 16px;
$m: 1;
$colorsPerRow: 10;
display: flex;
flex-wrap: wrap;
line-height: $d;
width: ($d * $colorsPerRow) + ($m * $colorsPerRow);
.l-palette-row {
@include clearfix;
line-height: $d;
width: ($d * $colorsPerRow) + ($m * $colorsPerRow);
&.l-option-row {
margin-bottom: $interiorMargin;
.s-palette-item {
border-color: $colorPaletteFg;
}
}
.l-palette-item {
box-sizing: border-box;
display: block;
height: $d; width: $d;
min-width: $d;
line-height: $d * 0.9;
margin: 0 ($m * 1px) ($m * 1px) 0;
position: relative;
text-align: center;
}
}
.s-palette-item {
border: 1px solid transparent;
color: $colorPaletteFg;
text-shadow: $shdwPaletteFg;
@include trans-prop-nice-fade(0.25s);
&:hover {
@include trans-prop-nice-fade(0);
border-color: $colorPaletteSelected !important;
}
&.selected {
border-color: $colorPaletteSelected;
box-shadow: $shdwPaletteSelected; //Needed to see selection rect on light colored swatches
}
}
.l-palette-item-label {
margin-left: $interiorMargin;
}
.l-inline-palette {
.l-palette-row {
width: 100%;
.l-palette-item {
//@include display(flex);
@include flex(1 0 auto);
margin: 1px;
min-width: auto;
width: auto;
&:before {
content: '';
padding-top: 75%;
&.l-option-row {
margin-bottom: $interiorMargin;
.s-palette-item {
border-color: $colorPaletteFg;
}
}
}
}
.l-palette-item {
box-sizing: border-box;
display: block;
float: left;
height: $d; width: $d;
line-height: $d * 0.9;
margin: 0 ($m * 1px) ($m * 1px) 0;
position: relative;
text-align: center;
}
.s-palette-item {
border: 1px solid transparent;
color: $colorPaletteFg;
text-shadow: $shdwPaletteFg;
@include trans-prop-nice-fade(0.25s);
&:hover {
@include trans-prop-nice-fade(0);
border-color: $colorPaletteSelected !important;
}
&.selected {
border-color: $colorPaletteSelected;
box-shadow: $shdwPaletteSelected; //Needed to see selection rect on light colored swatches
}
}
.l-palette-item-label {
margin-left: $interiorMargin;
}
}
}

View File

@ -80,32 +80,23 @@
// Editing Grids
.l-grid-holder {
display: block;
.l-grid {
&.l-grid-x { @include bgTicks($colorGridLines, 'x'); }
&.l-grid-y { @include bgTicks($colorGridLines, 'y'); }
}
}
// Display grid when selected or selection parent.
.s-selected .l-grid-holder,
.s-selected-parent .l-grid-holder {
display: block;
// Prevent nested frames from showing their grids
.t-frame-outer .l-grid-holder { display: none !important; }
// Prevent nested elements from showing s-hover-border
.t-frame-outer .s-hover-border {
border: none !important;
}
// Display in nested frames...
.t-frame-outer {
// ...when drilled in or selection parent...
&.s-drilled-in, &.s-selected-parent {
.l-grid-holder {
display: block;
}
.t-frame-outer:not(.s-drilled-in) .l-grid-holder {
display: none;
}
}
// ...but hide otherwise.
.l-grid-holder {
display: none;
}
// Prevent nested frames from being selectable until we have proper sub-object editing
.t-frame-outer .t-frame-outer {
pointer-events: none;
}
}

View File

@ -34,7 +34,18 @@ body.touch {
line-height: $mobileTreeItemH !important;
.view-control {
font-size: 1em;
width: ceil($mobileTreeItemH * 0.5);
margin-right: $interiorMargin;
width: ceil($mobileTreeItemH * 0.75);
&.has-children {
&:before {
content: $glyph-icon-arrow-down;
left: 50%;
@include transform(translateX(-50%) rotate(-90deg));
}
&.expanded:before {
@include transform(translateX(-50%) rotate(0deg));
}
}
}
.t-object-label {
line-height: inherit;

View File

@ -1,208 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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.
*****************************************************************************/
.gl-plot {
.gl-plot-legend {
min-height: $plotLegendH;
.view-control {
font-size: 1em;
margin-right: $interiorMarginSm;
}
table {
table-layout: fixed;
tr {
display: table-row;
}
th,
td {
@include ellipsize(); // Note: this won't work if table-layout uses anything other than fixed.
display: table-cell;
padding: 1px 3px; // Tighter than standard tabular padding
}
}
&.hover-on-plot {
// User is hovering over the plot to get a value at a point
.hover-value-enabled {
background-color: $legendHoverValueBg;
border-radius: $smallCr;
padding: 0 $interiorMarginSm;
&:before {
opacity: 0.5;
}
&.cursor-hover,
.value-to-display-nearestTimestamp,
.value-to-display-nearestValue
{
@extend .icon-crosshair-12px;
&:before {
font-size: 9px;
}
}
&.value-to-display-min:before {
content: 'MIN ';
}
&.value-to-display-max:before {
content: 'MAX ';
}
}
}
}
&.plot-legend-collapsed .plot-wrapper-expanded-legend { display: none; }
&.plot-legend-expanded .plot-wrapper-collapsed-legend { display: none; }
/***************** GENERAL STYLES, ALL STATES */
.plot-legend-item {
// General styles for legend items, both expanded and collapsed legend states
.plot-series-color-swatch {
border-radius: $smallCr;
border: 1px solid $colorBodyBg;
display: inline-block;
height: $plotSwatchD;
width: $plotSwatchD;
}
.plot-series-name {
display: inline;
}
.plot-series-value {
@include ellipsize();
}
}
/***************** GENERAL STYLES, COLLAPSED */
&.plot-legend-collapsed {
// .plot-legend-item is a span of spans.
&.plot-legend-top .gl-plot-legend { margin-bottom: $interiorMargin; }
&.plot-legend-bottom .gl-plot-legend { margin-top: $interiorMargin; }
&.plot-legend-right .gl-plot-legend { margin-left: $interiorMargin; }
&.plot-legend-left .gl-plot-legend { margin-right: $interiorMargin; }
.plot-legend-item {
display: flex;
align-items: center;
&:not(:first-child) {
margin-left: $interiorMarginLg;
}
.plot-series-swatch-and-name,
.plot-series-value {
@include ellipsize();
flex: 1 1 auto;
}
.plot-series-swatch-and-name {
margin-right: $interiorMarginSm;
}
.plot-series-value {
text-align: left;
width: 170px;
}
}
}
/***************** GENERAL STYLES, EXPANDED */
&.plot-legend-expanded {
.gl-plot-legend {
max-height: 70%;
}
.plot-wrapper-expanded-legend {
overflow-y: auto;
}
&.plot-legend-top .gl-plot-legend {
margin-bottom: $interiorMargin;
}
&.plot-legend-bottom .gl-plot-legend {
margin-top: $interiorMargin;
}
}
/***************** TOP OR BOTTOM */
&.plot-legend-top,
&.plot-legend-bottom {
// General styles when legend is on the top or bottom
@extend .l-flex-col;
&.plot-legend-collapsed {
// COLLAPSED ON TOP OR BOTTOM
.plot-wrapper-collapsed-legend {
display: flex;
flex: 1 1 auto;
overflow: hidden;
}
}
}
/***************** EITHER SIDE */
&.plot-legend-left,
&.plot-legend-right {
@extend .l-flex-row;
// If the legend is expanded, use flex-col instead so that the legend gets the width it needs.
&.plot-legend-expanded {
// EXPANDED, ON EITHER SIDE
@extend .l-flex-col;
}
&.plot-legend-collapsed {
// COLLAPSED, ON EITHER SIDE
.gl-plot-legend {
max-height: inherit;
width: 25%;
}
.plot-wrapper-collapsed-legend {
display: flex;
flex-flow: column nowrap;
min-width: 0;
flex: 1 1 auto;
overflow-y: auto;
}
.plot-legend-item {
margin-bottom: 1px;
margin-left: 0;
flex-wrap: wrap;
.plot-series-swatch-and-name {
flex: 0 1 auto;
min-width: 20%;
}
.plot-series-value {
flex: 0 1 auto;
width: auto;
}
}
}
}
/***************** ON BOTTOM OR RIGHT */
&.plot-legend-right:not(.plot-legend-expanded),
&.plot-legend-bottom {
.gl-plot-legend {
order: 2;
}
.plot-wrapper-axis-and-display-area {
order: 1;
}
}
}

View File

@ -20,64 +20,18 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
.abs.holder-plot {
right: $interiorMargin; // Fend off the scrollbar when less than min-height;
.t-object-alert.t-alert-unsynced {
display: none;
}
// Fend off the scrollbar when less than min-height;
right: $interiorMargin;
}
/********************************************* STACKED PLOT LAYOUT */
.t-plot-stacked {
.l-view-section {
// Make this a flex container
display: flex;
flex-flow: column nowrap;
.gl-plot.child-frame {
mct-plot {
display: flex;
flex: 1 1 auto;
height: 100%;
position: relative;
}
flex: 1 1 auto;
&:not(:first-child) {
margin-top: $interiorMargin;
}
}
}
.s-status-timeconductor-unsynced .holder-plot {
.t-object-alert.t-alert-unsynced {
display: block;
}
}
}
.gl-plot {
color: $colorPlotFg;
display: flex;
font-size: 0.7rem;
position: relative;
width: 100%;
height: 100%;
min-height: $plotMinH;
/********************************************* AXIS AND DISPLAY AREA */
.plot-wrapper-axis-and-display-area {
margin-top: $interiorMargin; // Keep the top tick label from getting clipped
position: relative;
flex: 1 1 auto;
.t-object-alert {
position: absolute;
display: block;
font-size: 1.5em;
top: $interiorMarginSm; left: $interiorMarginSm;
}
}
.gl-plot-wrapper-display-area-and-x-axis {
// Holds the plot area and the X-axis only
position: absolute;
@ -95,6 +49,7 @@
}
.gl-plot-axis-area.gl-plot-x {
//@include test(green);
top: auto;
right: 0;
bottom: 0;
@ -108,7 +63,7 @@
.gl-plot-axis-area {
position: absolute;
&.gl-plot-y {
top: nth($plotDisplayArea, 1);
top: $plotLegendH + $interiorMargin;
right: auto;
bottom: nth($plotDisplayArea, 3);
left: 0;
@ -203,6 +158,17 @@
}
}
.gl-plot-legend {
position: absolute;
top: 0;
right: 0;
bottom: auto;
left: 0;
height: $plotLegendH;
overflow-x: hidden;
overflow-y: auto;
}
/****************************** Limits and Out-of-Bounds data */
.l-limit-bar,
@ -269,6 +235,39 @@
border: 1px solid $colorPlotAreaBorder;
}
.gl-plot-legend,
.legend {
.plot-legend-item,
.legend-item {
display: inline-block;
margin-right: $interiorMarginLg;
margin-bottom: $interiorMarginSm;
span {
vertical-align: middle;
}
.plot-color-swatch,
.color-swatch {
border-radius: 2px;
display: inline-block;
height: $plotSwatchD;
width: $plotSwatchD;
}
}
}
.gl-plot-legend {
.plot-legend-item {
border-radius: $smallCr;
line-height: 1.5em;
padding: 0px $itemPadLR;
.plot-color-swatch {
border: 1px solid $colorBodyBg;
height: $plotSwatchD + 1;
width: $plotSwatchD + 1;
}
}
}
.tick {
position: absolute;
border: 0 $colorPlotHash solid;

View File

@ -23,7 +23,7 @@
ul.tree {
@include menuUlReset();
@include user-select(none);
> li {
li {
display: block;
position: relative;
}
@ -53,10 +53,12 @@ ul.tree {
.view-control {
color: $colorItemTreeVC;
margin-right: $interiorMargin;
height: 100%;
line-height: inherit;
width: $treeVCW;
&:before { display: block; }
&.no-children {
&:before { display: none; }
&:before { display: none; }
&.has-children {
&:before { display: block; }
}
}

View File

@ -26,10 +26,12 @@
z-index: 0; // Needed to prevent child-frame controls from showing through when another child-frame is above
&:not(.no-frame) {
background: $colorBodyBg;
border-color: $bc;
border: 1px solid $bc;
&:hover {
border-color: lighten($bc, 10%);
}
}
}
.object-browse-bar {
font-size: 0.75em;
height: $ohH;
@ -90,9 +92,9 @@
&.no-frame {
background: transparent !important;
border-color: transparent;
border: none !important;
.object-browse-bar .right {
$m: 0;
$m: 0; // $interiorMarginSm;
background: rgba(black, 0.3);
border-radius: $basicCr;
padding: $interiorMarginSm;
@ -102,7 +104,7 @@
}
&.t-frame-outer > .t-rep-frame {
&.contents {
$m: 0px;
$m: 2px;
top: $m;
right: $m;
bottom: $m;
@ -113,7 +115,6 @@
display: none;
}
> .object-holder.abs {
overflow: hidden;
top: 0 !important;
}
}

View File

@ -20,52 +20,35 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
.s-hover-border {
border: 1px solid transparent;
&:hover {
border-color: rgba($colorSelectableSelectedPrimary, 0.5) !important;
}
border: 1px dotted transparent;
}
.s-status-editing {
// Limit to editing mode
$o: 0.5;
$oHover: 0.8;
$bc: $colorSelectableSelectedPrimary;
// Limit to editing mode until we have sub-object selection
.s-hover-border {
// Show a border by default so user can see object bounds and empty objects
border-color: rgba($bc, $o) !important;
border-style: dotted !important;
border: 1px dotted rgba($colorSelectableSelectedPrimary, 0.3) !important;
&:hover {
border-color: rgba($bc, $oHover) !important;
}
&.t-object-type-layout {
border-style: dashed !important;
border-color: rgba($colorSelectableSelectedPrimary, 0.7) !important;
}
}
.s-selected {
&.s-moveable {
&:not(.s-drilled-in) {
cursor: move;
.s-selected > .s-hover-border,
.s-selected.s-hover-border {
// Styles for a selected object. Also used by legacy Fixed Position/Panel objects.
border-color: $colorSelectableSelectedPrimary !important;
@include boxShdwLarge();
// Show edit-corners if you got 'em
.edit-corner {
display: block;
&:hover {
background-color: rgba($colorKey, 1);
}
}
}
}
.s-selected > .s-hover-border,
.s-selected.s-hover-border {
// Styles for a selected object. Also used by legacy Fixed Position/Panel objects.
border-color: $colorSelectableSelectedPrimary !important;
@include boxShdwLarge();
// Show edit-corners if you got 'em
.edit-corner {
display: block;
&:hover {
background-color: rgba($colorKey, 1);
}
.s-selected > .s-moveable,
.s-selected.s-moveable {
cursor: move;
}
}
}

View File

@ -20,7 +20,7 @@
at runtime from the About dialog for additional information.
-->
<span ng-controller="DateTimeFieldController">
<input type="text" autocorrect="off" spellcheck="false"
<input type="text"
ng-model="textValue"
ng-blur="restoreTextValue(); ngBlur()"
ng-mouseup="ngMouseup()"

View File

@ -40,7 +40,7 @@ define(
// Gets an array of the contextual parents/ancestors of the selected object
function getContextualPath() {
var currentObj = $scope.domainObject,
var currentObj = $scope.ngModel.selectedObject,
currentParent,
parents = [];
@ -68,7 +68,7 @@ define(
// If this the the initial call of this recursive function
if (!current) {
current = $scope.domainObject;
current = $scope.ngModel.selectedObject;
$scope.primaryParents = [];
}
@ -87,16 +87,16 @@ define(
// Gets the metadata for the selected object
function getMetadata() {
$scope.metadata = $scope.domainObject &&
$scope.domainObject.hasCapability('metadata') &&
$scope.domainObject.useCapability('metadata');
$scope.metadata = $scope.ngModel.selectedObject &&
$scope.ngModel.selectedObject.hasCapability('metadata') &&
$scope.ngModel.selectedObject.useCapability('metadata');
}
// Set scope variables when the selected object changes
$scope.$watch('domainObject', function () {
$scope.isLink = $scope.domainObject &&
$scope.domainObject.hasCapability('location') &&
$scope.domainObject.getCapability('location').isLink();
$scope.$watch('ngModel.selectedObject', function () {
$scope.isLink = $scope.ngModel.selectedObject &&
$scope.ngModel.selectedObject.hasCapability('location') &&
$scope.ngModel.selectedObject.getCapability('location').isLink();
if ($scope.isLink) {
getPrimaryPath();
@ -109,7 +109,7 @@ define(
getMetadata();
});
var mutation = $scope.domainObject.getCapability('mutation');
var mutation = $scope.ngModel.selectedObject.getCapability('mutation');
var unlisten = mutation.listen(getMetadata);
$scope.$on('$destroy', unlisten);
}

View File

@ -83,9 +83,9 @@ define([
this.activeObject = domainObject;
if (domainObject && domainObject.hasCapability('composition')) {
$(this.toggleView.elements()).removeClass('no-children');
$(this.toggleView.elements()).addClass('has-children');
} else {
$(this.toggleView.elements()).addClass('no-children');
$(this.toggleView.elements()).removeClass('has-children');
}
if (domainObject && domainObject.hasCapability('status')) {

View File

@ -41,6 +41,16 @@ define(
"$scope",
["$watch", "$on"]
);
mockScope.ngModel = {};
mockScope.ngModel.selectedObject = {
getCapability: function () {
return {
listen: function () {
return true;
}
};
}
};
mockObjectService = jasmine.createSpyObj(
"objectService",
@ -67,27 +77,22 @@ define(
"location capability",
["isLink"]
);
mockDomainObject.getCapability.andCallFake(function (param) {
if (param === 'location') {
return mockLocationCapability;
} else if (param === 'context') {
return mockContextCapability;
} else if (param === 'mutation') {
return {
listen: function () {
return true;
}
};
}
});
mockScope.domainObject = mockDomainObject;
controller = new ObjectInspectorController(mockScope, mockObjectService);
// Change the selected object to trigger the watch call
mockScope.ngModel.selectedObject = mockDomainObject;
});
it("watches for changes to the selected object", function () {
expect(mockScope.$watch).toHaveBeenCalledWith('domainObject', jasmine.any(Function));
expect(mockScope.$watch).toHaveBeenCalledWith('ngModel.selectedObject', jasmine.any(Function));
});
it("looks for contextual parent objects", function () {

View File

@ -65,10 +65,6 @@ define(
options = Object.create(OPTIONS);
options.marginX = -bubbleSpaceLR;
// prevent bubble from appearing right under pointer,
// which causes hover callback to be called multiple times
options.offsetX = 1;
// On a phone, bubble takes up more screen real estate,
// so position it differently (toward the bottom)
if (this.agentService.isPhone()) {

View File

@ -38,8 +38,7 @@ define([
"implementation": InspectorController,
"depends": [
"$scope",
"openmct",
"$document"
"policyService"
]
}
],

View File

@ -21,73 +21,44 @@
*****************************************************************************/
define(
[],
function () {
['../../browse/src/InspectorRegion'],
function (InspectorRegion) {
/**
* The InspectorController listens for the selection changes and adds the selection
* object to the scope.
* The InspectorController adds region data for a domain object's type
* to the scope.
*
* @constructor
*/
function InspectorController($scope, openmct, $document) {
var self = this;
self.$scope = $scope;
function InspectorController($scope, policyService) {
var domainObject = $scope.domainObject,
typeCapability = domainObject.getCapability('type'),
statusListener;
/**
* Callback handler for the selection change event.
* Adds the selection object to the scope. If the selected item has an inspector view,
* it puts the key in the scope. If provider view exists, it shows the view.
* Filters region parts to only those allowed by region policies
* @param regions
* @returns {{}}
*/
function setSelection(selection) {
if (selection[0]) {
var view = openmct.inspectorViews.get(selection);
var container = $document[0].querySelectorAll('.inspector-provider-view')[0];
container.innerHTML = "";
if (view) {
self.providerView = true;
view.show(container);
} else {
self.providerView = false;
var selectedItem = selection[0].context.oldItem;
if (selectedItem) {
$scope.inspectorKey = selectedItem.getCapability("type").typeDef.inspector;
}
}
}
self.$scope.selection = selection;
function filterRegions(inspector) {
//Dupe so we're not modifying the type definition.
return inspector.regions && inspector.regions.filter(function (region) {
return policyService.allow('region', region, domainObject);
});
}
openmct.selection.on("change", setSelection);
setSelection(openmct.selection.get());
function setRegions() {
$scope.regions = filterRegions(typeCapability.getDefinition().inspector || new InspectorRegion());
}
statusListener = domainObject.getCapability("status").listen(setRegions);
$scope.$on("$destroy", function () {
openmct.selection.off("change", setSelection);
statusListener();
});
setRegions();
}
/**
* Gets the selected item.
*
* @returns a domain object
*/
InspectorController.prototype.selectedItem = function () {
return this.$scope.selection[0].context.oldItem;
};
/**
* Checks if a provider view exists.
*
* @returns 'true' if provider view exists, 'false' otherwise
*/
InspectorController.prototype.hasProviderView = function () {
return this.providerView;
};
return InspectorController;
}
);

View File

@ -27,93 +27,82 @@ define(
describe("The inspector controller ", function () {
var mockScope,
mockDomainObject,
mockOpenMCT,
mockSelection,
mockInspectorViews,
mockTypeDef,
controller,
container,
$document = [],
selectable = [];
mockTypeCapability,
mockTypeDefinition,
mockPolicyService,
mockStatusCapability,
capabilities = {},
controller;
beforeEach(function () {
mockTypeDef = {
typeDef: {
inspector: "some-key"
}
mockTypeDefinition = {
inspector:
{
'regions': [
{'name': 'Part One'},
{'name': 'Part Two'}
]
}
};
mockTypeCapability = jasmine.createSpyObj('typeCapability', [
'getDefinition'
]);
mockTypeCapability.getDefinition.andReturn(mockTypeDefinition);
capabilities.type = mockTypeCapability;
mockStatusCapability = jasmine.createSpyObj('statusCapability', [
'listen'
]);
capabilities.status = mockStatusCapability;
mockDomainObject = jasmine.createSpyObj('domainObject', [
'getCapability'
]);
mockDomainObject.getCapability.andReturn(mockTypeDef);
mockDomainObject.getCapability.andCallFake(function (name) {
return capabilities[name];
});
mockPolicyService = jasmine.createSpyObj('policyService', [
'allow'
]);
mockScope = jasmine.createSpyObj('$scope',
['$on', 'selection']
['$on']
);
selectable[0] = {
context: {
oldItem: mockDomainObject
}
};
mockSelection = jasmine.createSpyObj("selection", [
'on',
'off',
'get'
]);
mockSelection.get.andReturn(selectable);
mockInspectorViews = jasmine.createSpyObj('inspectorViews', ['get']);
mockOpenMCT = {
selection: mockSelection,
inspectorViews: mockInspectorViews
};
container = jasmine.createSpy('container', ['innerHTML']);
$document[0] = jasmine.createSpyObj("$document", ['querySelectorAll']);
$document[0].querySelectorAll.andReturn([container]);
controller = new InspectorController(mockScope, mockOpenMCT, $document);
mockScope.domainObject = mockDomainObject;
});
it("listens for selection change event", function () {
expect(mockOpenMCT.selection.on).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
expect(controller.selectedItem()).toEqual(mockDomainObject);
var mockItem = jasmine.createSpyObj('domainObject', [
'getCapability'
]);
mockItem.getCapability.andReturn(mockTypeDef);
selectable[0].context.oldItem = mockItem;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(controller.selectedItem()).toEqual(mockItem);
it("filters out regions disallowed by region policy", function () {
mockPolicyService.allow.andReturn(false);
controller = new InspectorController(mockScope, mockPolicyService);
expect(mockScope.regions.length).toBe(0);
});
it("cleans up on scope destroy", function () {
expect(mockScope.$on).toHaveBeenCalledWith(
'$destroy',
jasmine.any(Function)
);
mockScope.$on.calls[0].args[1]();
expect(mockOpenMCT.selection.off).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
it("does not filter out regions allowed by region policy", function () {
mockPolicyService.allow.andReturn(true);
controller = new InspectorController(mockScope, mockPolicyService);
expect(mockScope.regions.length).toBe(2);
});
it("adds selection object to scope", function () {
expect(mockScope.selection).toEqual(selectable);
expect(controller.selectedItem()).toEqual(mockDomainObject);
it("Responds to status changes", function () {
mockPolicyService.allow.andReturn(true);
controller = new InspectorController(mockScope, mockPolicyService);
expect(mockScope.regions.length).toBe(2);
expect(mockStatusCapability.listen).toHaveBeenCalled();
mockPolicyService.allow.andReturn(false);
mockStatusCapability.listen.mostRecentCall.args[0]();
expect(mockScope.regions.length).toBe(0);
});
it("Unregisters status listener", function () {
var mockListener = jasmine.createSpy('listener');
mockStatusCapability.listen.andReturn(mockListener);
controller = new InspectorController(mockScope, mockPolicyService);
expect(mockScope.$on).toHaveBeenCalledWith("$destroy", jasmine.any(Function));
mockScope.$on.mostRecentCall.args[1]();
expect(mockListener).toHaveBeenCalled();
});
});
}

View File

@ -181,8 +181,6 @@ $colorPlotHash: $colorTick;
$stylePlotHash: dashed;
$colorPlotAreaBorder: $colorInteriorBorder;
$colorPlotLabelFg: pushBack($colorPlotFg, 20%);
$legendCollapsedNameMaxW: 50%;
$legendHoverValueBg: rgba($colorBodyFg, 0.1);
// Tree
$colorItemTreeHoverBg: pullForward($colorBodyBg, $hoverRatioPercent);

View File

@ -181,8 +181,6 @@ $colorPlotHash: $colorTick;
$stylePlotHash: dashed;
$colorPlotAreaBorder: $colorInteriorBorder;
$colorPlotLabelFg: pushBack($colorPlotFg, 20%);
$legendCollapsedNameMaxW: 50%;
$legendHoverValueBg: rgba($colorBodyFg, 0.2);
// Tree
$colorItemTreeHoverBg: pullForward($colorBodyBg, $hoverRatioPercent);

View File

@ -0,0 +1,54 @@
define([
'text!./res/templates/autoflow-tabular.html',
'./src/AutoflowTabularController',
'./src/MCTAutoflowTable'
], function (
autoflowTabularTemplate,
AutoflowTabularController,
MCTAutoflowTable
) {
return function (options) {
return function (openmct) {
openmct.legacyRegistry.register("platform/features/autoflow", {
"name": "WARP Telemetry Adapter",
"description": "Retrieves telemetry from the WARP Server and provides related types and views.",
"resources": "res",
"extensions": {
"views": [
{
"key": "autoflow",
"name": "Autoflow Tabular",
"cssClass": "icon-packet",
"description": "A tabular view of packet contents.",
"template": autoflowTabularTemplate,
"type": options && options.type,
"needs": [
"telemetry"
],
"delegation": true
}
],
"controllers": [
{
"key": "AutoflowTabularController",
"implementation": AutoflowTabularController,
"depends": [
"$scope",
"$timeout",
"telemetrySubscriber"
]
}
],
"directives": [
{
"key": "mctAutoflowTable",
"implementation": MCTAutoflowTable
}
]
}
});
openmct.legacyRegistry.enable("platform/features/autoflow");
};
};
});

View File

@ -0,0 +1,26 @@
<div class="items-holder abs contents autoflow obj-value-format"
ng-controller="AutoflowTabularController as autoflow">
<div class="abs l-flex-row holder t-autoflow-header l-autoflow-header">
<mct-include key="'input-filter'"
ng-model="autoflow.filter"
class="flex-elem">
</mct-include>
<div class="flex-elem grows t-last-update" title="Last Update">{{autoflow.updated()}}</div>
<a title="Change column width"
class="s-button flex-elem icon-arrows-right-left change-column-width"
ng-click="autoflow.increaseColumnWidth()"></a>
</div>
<div class="abs t-autoflow-items l-autoflow-items"
mct-resize="autoflow.setBounds(bounds)"
mct-resize-interval="50">
<mct-autoflow-table values="autoflow.rangeValues()"
objects="autoflow.getTelemetryObjects()"
rows="autoflow.getRows()"
classes="autoflow.classes()"
updated="autoflow.updated()"
column-width="autoflow.columnWidth()"
counter="autoflow.counter()"
>
</mct-autoflow-table>
</div>
</div>

View File

@ -0,0 +1,169 @@
/*global angular*/
define(
[],
function () {
/**
* The link step for the `mct-autoflow-table` directive;
* watches scope and updates the DOM appropriately.
* See documentation in `MCTAutoflowTable.js` for the rationale
* for including this directive, as well as for an explanation
* of which values are placed in scope.
*
* @constructor
* @param {Scope} scope the scope for this usage of the directive
* @param element the jqLite-wrapped element which used this directive
*/
function AutoflowTableLinker(scope, element) {
var objects, // Domain objects at last structure refresh
rows, // Number of rows from last structure refresh
priorClasses = {},
valueSpans = {}; // Span elements to put data values in
// Create a new name-value pair in the specified column
function createListItem(domainObject, ul) {
// Create a new li, and spans to go in it.
var li = angular.element('<li>'),
titleSpan = angular.element('<span>'),
valueSpan = angular.element('<span>');
// Place spans in the li, and li into the column.
// valueSpan must precede titleSpan in the DOM due to new CSS float approach
li.append(valueSpan).append(titleSpan);
ul.append(li);
// Style appropriately
li.addClass('l-autoflow-row');
titleSpan.addClass('l-autoflow-item l');
valueSpan.addClass('l-autoflow-item r l-obj-val-format');
// Set text/tooltip for the name-value row
titleSpan.text(domainObject.getModel().name);
titleSpan.attr("title", domainObject.getModel().name);
// Keep a reference to the span which will hold the
// data value, to populate in the next refreshValues call
valueSpans[domainObject.getId()] = valueSpan;
return li;
}
// Create a new column of name-value pairs in this table.
function createColumn(el) {
// Create a ul
var ul = angular.element('<ul>');
// Add it into the mct-autoflow-table
el.append(ul);
// Style appropriately
ul.addClass('l-autoflow-col');
// Get the current col width and apply at time of column creation
// Important to do this here, as new columns could be created after
// the user has changed the width.
ul.css('width', scope.columnWidth + 'px');
// Return it, so some li elements can be added
return ul;
}
// Change the width of the columns when user clicks the resize button.
function resizeColumn() {
element.find('ul').css('width', scope.columnWidth + 'px');
}
// Rebuild the DOM associated with this table.
function rebuild(domainObjects, rowCount) {
var activeColumn;
// Empty out our cached span elements
valueSpans = {};
// Start with an empty DOM beneath this directive
element.html("");
// Add DOM elements for each domain object being displayed
// in this table.
domainObjects.forEach(function (object, index) {
// Start a new column if we'd run out of room
if (index % rowCount === 0) {
activeColumn = createColumn(element);
}
// Add the DOM elements for that object to whichever
// column (a `ul` element) is current.
createListItem(object, activeColumn);
});
}
// Update spans with values, as made available via the
// `values` attribute of this directive.
function refreshValues() {
// Get the available values
var values = scope.values || {},
classes = scope.classes || {};
// Populate all spans with those values (or clear
// those spans if no value is available)
(objects || []).forEach(function (object) {
var id = object.getId(),
span = valueSpans[id],
value;
if (span) {
// Look up the value...
value = values[id];
// ...and convert to empty string if it's undefined
value = value === undefined ? "" : value;
span.attr("data-value", value);
// Update the span
span.text(value);
span.attr("title", value);
span.removeClass(priorClasses[id]);
span.addClass(classes[id]);
priorClasses[id] = classes[id];
}
// Also need stale/alert/ok class
// on span
});
}
// Refresh the DOM for this table, if necessary
function refreshStructure() {
// Only rebuild if number of rows or set of objects
// has changed; otherwise, our structure is still valid.
if (scope.objects !== objects ||
scope.rows !== rows) {
// Track those values to support future refresh checks
objects = scope.objects;
rows = scope.rows;
// Rebuild the DOM
rebuild(objects || [], rows || 1);
// Refresh all data values shown
refreshValues();
}
}
// Changing the domain objects in use or the number
// of rows should trigger a structure change (DOM rebuild)
scope.$watch("objects", refreshStructure);
scope.$watch("rows", refreshStructure);
// When the current column width has been changed, resize the column
scope.$watch('columnWidth', resizeColumn);
// When the last-updated time ticks,
scope.$watch("updated", refreshValues);
// Update displayed values when the counter changes.
scope.$watch("counter", refreshValues);
}
return AutoflowTableLinker;
}
);

View File

@ -0,0 +1,324 @@
define(
['moment'],
function (moment) {
var ROW_HEIGHT = 16,
SLIDER_HEIGHT = 10,
INITIAL_COLUMN_WIDTH = 225,
MAX_COLUMN_WIDTH = 525,
COLUMN_WIDTH_STEP = 25,
DEBOUNCE_INTERVAL = 100,
DATE_FORMAT = "YYYY-DDD HH:mm:ss.SSS\\Z",
NOT_UPDATED = "No updates",
EMPTY_ARRAY = [];
/**
* Responsible for supporting the autoflow tabular view.
* Implements the all-over logic which drives that view,
* mediating between template-provided areas, the included
* `mct-autoflow-table` directive, and the underlying
* domain object model.
* @constructor
*/
function AutflowTabularController(
$scope,
$timeout,
telemetrySubscriber
) {
var filterValue = "",
filterValueLowercase = "",
subscription,
filteredObjects = [],
lastUpdated = {},
updateText = NOT_UPDATED,
rangeValues = {},
classes = {},
limits = {},
updatePending = false,
lastBounce = Number.NEGATIVE_INFINITY,
columnWidth = INITIAL_COLUMN_WIDTH,
rows = 1,
counter = 0;
// Trigger an update of the displayed table by incrementing
// the counter that it watches.
function triggerDisplayUpdate() {
counter += 1;
}
// Check whether or not an object's name matches the
// user-entered filter value.
function filterObject(domainObject) {
return (domainObject.getModel().name || "")
.toLowerCase()
.indexOf(filterValueLowercase) !== -1;
}
// Comparator for sorting points back into packet order
function compareObject(objectA, objectB) {
var indexA = objectA.getModel().index || 0,
indexB = objectB.getModel().index || 0;
return indexA - indexB;
}
// Update the list of currently-displayed objects; these
// will be the subset of currently subscribed-to objects
// which match a user-entered filter.
function doUpdateFilteredObjects() {
// Generate the list
filteredObjects = (
subscription ?
subscription.getTelemetryObjects() :
[]
).filter(filterObject).sort(compareObject);
// Clear the pending flag
updatePending = false;
// Track when this occurred, so that we can wait
// a whole before updating again.
lastBounce = Date.now();
triggerDisplayUpdate();
}
// Request an update to the list of current objects; this may
// run on a timeout to avoid excessive calls, e.g. while the user
// is typing a filter.
function updateFilteredObjects() {
// Don't do anything if an update is already scheduled
if (!updatePending) {
if (Date.now() > lastBounce + DEBOUNCE_INTERVAL) {
// Update immediately if it's been long enough
doUpdateFilteredObjects();
} else {
// Otherwise, update later, and track that we have
// an update pending so that subsequent calls can
// be ignored.
updatePending = true;
$timeout(doUpdateFilteredObjects, DEBOUNCE_INTERVAL);
}
}
}
// Track the latest data values for this domain object
function recordData(telemetryObject) {
// Get latest domain/range values for this object.
var id = telemetryObject.getId(),
domainValue = subscription.getDomainValue(telemetryObject),
rangeValue = subscription.getRangeValue(telemetryObject);
// Track the most recent timestamp change observed...
if (domainValue !== undefined && domainValue !== lastUpdated[id]) {
lastUpdated[id] = domainValue;
// ... and update the displayable text for that timestamp
updateText = isNaN(domainValue) ? "" :
moment.utc(domainValue).format(DATE_FORMAT);
}
// Store data values into the rangeValues structure, which
// will be used to populate the table itself.
// Note that we want full precision here.
rangeValues[id] = rangeValue;
// Update limit states as well
classes[id] = limits[id] && (limits[id].evaluate({
// This relies on external knowledge that the
// range value of a telemetry point is encoded
// in its datum as "value."
value: rangeValue
}) || {}).cssClass;
}
// Look at telemetry objects from the subscription; this is watched
// to detect changes from the subscription.
function subscribedTelemetry() {
return subscription ?
subscription.getTelemetryObjects() : EMPTY_ARRAY;
}
// Update the data values which will be used to populate the table
function updateValues() {
subscribedTelemetry().forEach(recordData);
triggerDisplayUpdate();
}
// Getter-setter function for user-entered filter text.
function filter(value) {
// If value was specified, we're a setter
if (value !== undefined) {
// Store the new value
filterValue = value;
filterValueLowercase = value.toLowerCase();
// Change which objects appear in the table
updateFilteredObjects();
}
// Always act as a getter
return filterValue;
}
// Update the bounds (width and height) of this view;
// called from the mct-resize directive. Recalculates how
// many rows should appear in the contained table.
function setBounds(bounds) {
var availableSpace = bounds.height - SLIDER_HEIGHT;
rows = Math.max(1, Math.floor(availableSpace / ROW_HEIGHT));
}
// Increment the current column width, up to the defined maximum.
// When the max is hit, roll back to the default.
function increaseColumnWidth() {
columnWidth += COLUMN_WIDTH_STEP;
// Cycle down to the initial width instead of exceeding max
columnWidth = columnWidth > MAX_COLUMN_WIDTH ?
INITIAL_COLUMN_WIDTH : columnWidth;
}
// Get displayable text for last-updated value
function updated() {
return updateText;
}
// Unsubscribe, if a subscription is active.
function releaseSubscription() {
if (subscription) {
subscription.unsubscribe();
subscription = undefined;
}
}
// Update set of telemetry objects managed by this view
function updateTelemetryObjects(telemetryObjects) {
updateFilteredObjects();
limits = {};
telemetryObjects.forEach(function (telemetryObject) {
var id = telemetryObject.getId();
limits[id] = telemetryObject.getCapability('limit');
});
}
// Create a subscription for the represented domain object.
// This will resolve capability delegation as necessary.
function makeSubscription(domainObject) {
// Unsubscribe, if there is an existing subscription
releaseSubscription();
// Clear updated timestamp
lastUpdated = {};
updateText = NOT_UPDATED;
// Create a new subscription; telemetrySubscriber gets
// to do the meaningful work here.
subscription = domainObject && telemetrySubscriber.subscribe(
domainObject,
updateValues
);
// Our set of in-view telemetry objects may have changed,
// so update the set that is being passed down to the table.
updateFilteredObjects();
}
// Watch for changes to the set of objects which have telemetry
$scope.$watch(subscribedTelemetry, updateTelemetryObjects);
// Watch for the represented domainObject (this field will
// be populated by mct-representation)
$scope.$watch("domainObject", makeSubscription);
// Make sure we unsubscribe when this view is destroyed.
$scope.$on("$destroy", releaseSubscription);
return {
/**
* Get the number of rows which should be shown in this table.
* @return {number} the number of rows to show
*/
getRows: function () {
return rows;
},
/**
* Get the objects which should currently be displayed in
* this table. This will be watched, so the return value
* should be stable when this list is unchanging. Only
* objects which match the user-entered filter value should
* be returned here.
* @return {DomainObject[]} the domain objects to include in
* this table.
*/
getTelemetryObjects: function () {
return filteredObjects;
},
/**
* Set the bounds (width/height) of this autoflow tabular view.
* The template must ensure that these bounds are tracked on
* the table area only.
* @param bounds the bounds; and object with `width` and
* `height` properties, both as numbers, in pixels.
*/
setBounds: setBounds,
/**
* Increments the width of the autoflow column.
* Setting does not yet persist.
*/
increaseColumnWidth: increaseColumnWidth,
/**
* Get-or-set the user-supplied filter value.
* @param {string} [value] the new filter value; omit to use
* as a getter
* @returns {string} the user-supplied filter value
*/
filter: filter,
/**
* Get all range values for use in this table. These will be
* returned as an object of key-value pairs, where keys are
* domain object IDs, and values are the most recently observed
* data values associated with those objects, formatted for
* display.
* @returns {object.<string,string>} most recent values
*/
rangeValues: function () {
return rangeValues;
},
/**
* Get CSS classes to apply to specific rows, representing limit
* states and/or stale states. These are returned as key-value
* pairs where keys are domain object IDs, and values are CSS
* classes to display for domain objects with those IDs.
* @returns {object.<string,string>} CSS classes
*/
classes: function () {
return classes;
},
/**
* Get the "last updated" text for this view; this will be
* the most recent timestamp observed for any telemetry-
* providing object, formatted for display.
* @returns {string} the time of the most recent update
*/
updated: updated,
/**
* Get the current column width, in pixels.
* @returns {number} column width
*/
columnWidth: function () {
return columnWidth;
},
/**
* Keep a counter and increment this whenever the display
* should be updated; this will be watched by the
* `mct-autoflow-table`.
* @returns {number} a counter value
*/
counter: function () {
return counter;
}
};
}
return AutflowTabularController;
}
);

View File

@ -0,0 +1,60 @@
define(
["./AutoflowTableLinker"],
function (AutoflowTableLinker) {
/**
* The `mct-autoflow-table` directive specifically supports
* autoflow tabular views; it is not intended for use outside
* of that view.
*
* This directive is responsible for creating the structure
* of the table in this view, and for updating its values.
* While this is achievable using a regular Angular template,
* this is undesirable from the perspective of performance
* due to the number of watches that can be involved for large
* tables. Instead, this directive will maintain a small number
* of watches, rebuilding table structure only when necessary,
* and updating displayed values in the more common case of
* new data arriving.
*
* @constructor
*/
function MCTAutoflowTable() {
return {
// Only applicable at the element level
restrict: "E",
// The link function; handles DOM update/manipulation
link: AutoflowTableLinker,
// Parameters to pass from attributes into scope
scope: {
// Set of domain objects to show in the table
objects: "=",
// Values for those objects, by ID
values: "=",
// CSS classes to show for objects, by ID
classes: "=",
// Number of rows to show before autoflowing
rows: "=",
// Time of last update; watched to refresh values
updated: "=",
// Current width of the autoflow column
columnWidth: "=",
// A counter used to trigger display updates
counter: "="
}
};
}
return MCTAutoflowTable;
}
);

View File

@ -0,0 +1,178 @@
define(
["../src/AutoflowTableLinker"],
function (AutoflowTableLinker) {
describe("The mct-autoflow-table linker", function () {
var cachedAngular,
mockAngular,
mockScope,
mockElement,
mockElements,
linker;
// Utility function to generate more mock elements
function createMockElement(html) {
var mockEl = jasmine.createSpyObj(
"element-" + html,
[
"append",
"addClass",
"removeClass",
"text",
"attr",
"html",
"css",
"find"
]
);
mockEl.testHtml = html;
mockEl.append.andReturn(mockEl);
mockElements.push(mockEl);
return mockEl;
}
function createMockDomainObject(id) {
var mockDomainObject = jasmine.createSpyObj(
"domainObject-" + id,
["getId", "getModel"]
);
mockDomainObject.getId.andReturn(id);
mockDomainObject.getModel.andReturn({name: id.toUpperCase()});
return mockDomainObject;
}
function fireWatch(watchExpression, value) {
mockScope.$watch.calls.forEach(function (call) {
if (call.args[0] === watchExpression) {
call.args[1](value);
}
});
}
// AutoflowTableLinker accesses Angular in the global
// scope, since it is not injectable; we simulate that
// here by adding/removing it to/from the window object.
beforeEach(function () {
mockElements = [];
mockAngular = jasmine.createSpyObj("angular", ["element"]);
mockScope = jasmine.createSpyObj("scope", ["$watch"]);
mockElement = createMockElement('<div>');
mockAngular.element.andCallFake(createMockElement);
if (window.angular !== undefined) {
cachedAngular = window.angular;
}
window.angular = mockAngular;
linker = new AutoflowTableLinker(mockScope, mockElement);
});
afterEach(function () {
if (cachedAngular !== undefined) {
window.angular = cachedAngular;
} else {
delete window.angular;
}
});
it("watches for changes in inputs", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"objects",
jasmine.any(Function)
);
expect(mockScope.$watch).toHaveBeenCalledWith(
"rows",
jasmine.any(Function)
);
expect(mockScope.$watch).toHaveBeenCalledWith(
"counter",
jasmine.any(Function)
);
});
it("changes structure when domain objects change", function () {
// Set up scope
mockScope.rows = 4;
mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f']
.map(createMockDomainObject);
// Fire an update to the set of objects
fireWatch("objects");
// Should have rebuilt with two columns of
// four and two rows each; first, by clearing...
expect(mockElement.html).toHaveBeenCalledWith("");
// Should have appended two columns...
expect(mockElement.append.calls.length).toEqual(2);
// ...which should have received two and four rows each
expect(mockElement.append.calls[0].args[0].append.calls.length)
.toEqual(4);
expect(mockElement.append.calls[1].args[0].append.calls.length)
.toEqual(2);
});
it("updates values", function () {
var mockSpans;
mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f']
.map(createMockDomainObject);
mockScope.values = { a: 0 };
// Fire an update to the set of values
fireWatch("objects");
fireWatch("updated");
// Get all created spans
mockSpans = mockElements.filter(function (mockElem) {
return mockElem.testHtml === '<span>';
});
// First span should be a, should have gotten this value.
// This test detects, in particular, WTD-749
expect(mockSpans[0].text).toHaveBeenCalledWith('A');
expect(mockSpans[1].text).toHaveBeenCalledWith(0);
});
it("listens for changes in column width", function () {
var mockUL = createMockElement("<ul>");
mockElement.find.andReturn(mockUL);
mockScope.columnWidth = 200;
fireWatch("columnWidth", mockScope.columnWidth);
expect(mockUL.css).toHaveBeenCalledWith("width", "200px");
});
it("updates CSS classes", function () {
var mockSpans;
mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f']
.map(createMockDomainObject);
mockScope.values = { a: "a value to find" };
mockScope.classes = { a: 'class-a' };
// Fire an update to the set of values
fireWatch("objects");
fireWatch("updated");
// Figure out which span holds the relevant value...
mockSpans = mockElements.filter(function (mockElem) {
return mockElem.testHtml === '<span>';
}).filter(function (mockSpan) {
var attrCalls = mockSpan.attr.calls;
return attrCalls.some(function (call) {
return call.args[0] === 'title' &&
call.args[1] === mockScope.values.a;
});
});
// ...and make sure it also has had its class applied
expect(mockSpans[0].addClass)
.toHaveBeenCalledWith(mockScope.classes.a);
});
});
}
);

View File

@ -0,0 +1,341 @@
define(
["../src/AutoflowTabularController"],
function (AutoflowTabularController) {
describe("The autoflow tabular controller", function () {
var mockScope,
mockTimeout,
mockSubscriber,
mockDomainObject,
mockSubscription,
controller;
// Fire watches that are registered as functions.
function fireFnWatches() {
mockScope.$watch.calls.forEach(function (call) {
if (typeof call.args[0] === 'function') {
call.args[1](call.args[0]());
}
});
}
beforeEach(function () {
mockScope = jasmine.createSpyObj(
"$scope",
["$on", "$watch"]
);
mockTimeout = jasmine.createSpy("$timeout");
mockSubscriber = jasmine.createSpyObj(
"telemetrySubscriber",
["subscribe"]
);
mockDomainObject = jasmine.createSpyObj(
"domainObject",
["getId", "getModel", "getCapability"]
);
mockSubscription = jasmine.createSpyObj(
"subscription",
[
"unsubscribe",
"getTelemetryObjects",
"getDomainValue",
"getRangeValue"
]
);
mockSubscriber.subscribe.andReturn(mockSubscription);
mockDomainObject.getModel.andReturn({name: "something"});
controller = new AutoflowTabularController(
mockScope,
mockTimeout,
mockSubscriber
);
});
it("listens for the represented domain object", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"domainObject",
jasmine.any(Function)
);
});
it("provides a getter-setter function for filtering", function () {
expect(controller.filter()).toEqual("");
controller.filter("something");
expect(controller.filter()).toEqual("something");
});
it("tracks bounds and adjust number of rows accordingly", function () {
// Rows are 15px high, and need room for an 10px slider
controller.setBounds({ width: 700, height: 120 });
expect(controller.getRows()).toEqual(6); // 110 usable height / 16px
controller.setBounds({ width: 700, height: 240 });
expect(controller.getRows()).toEqual(14); // 230 usable height / 16px
});
it("subscribes to a represented object's telemetry", function () {
// Set up subscription, scope
mockSubscription.getTelemetryObjects
.andReturn([mockDomainObject]);
mockScope.domainObject = mockDomainObject;
// Invoke the watcher with represented domain object
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
// Should have subscribed to it
expect(mockSubscriber.subscribe).toHaveBeenCalledWith(
mockDomainObject,
jasmine.any(Function)
);
// Should report objects as reported from subscription
expect(controller.getTelemetryObjects())
.toEqual([mockDomainObject]);
});
it("releases subscriptions on destroy", function () {
// Set up subscription...
mockSubscription.getTelemetryObjects
.andReturn([mockDomainObject]);
mockScope.domainObject = mockDomainObject;
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
// Verify precondition
expect(mockSubscription.unsubscribe).not.toHaveBeenCalled();
// Make sure we're listening for $destroy
expect(mockScope.$on).toHaveBeenCalledWith(
"$destroy",
jasmine.any(Function)
);
// Fire a destroy event
mockScope.$on.mostRecentCall.args[1]();
// Should have unsubscribed
expect(mockSubscription.unsubscribe).toHaveBeenCalled();
});
it("presents latest values and latest update state", function () {
// Make sure values are available
mockSubscription.getDomainValue.andReturn(402654321123);
mockSubscription.getRangeValue.andReturn(789);
mockDomainObject.getId.andReturn('testId');
// Set up subscription...
mockSubscription.getTelemetryObjects
.andReturn([mockDomainObject]);
mockScope.domainObject = mockDomainObject;
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
// Fire subscription callback
mockSubscriber.subscribe.mostRecentCall.args[1]();
// ...and exposed the results for template to consume
expect(controller.updated()).toEqual("1982-278 08:25:21.123Z");
expect(controller.rangeValues().testId).toEqual(789);
});
it("sorts domain objects by index", function () {
var testIndexes = { a: 2, b: 1, c: 3, d: 0 },
mockDomainObjects = Object.keys(testIndexes).sort().map(function (id) {
var mockDomainObj = jasmine.createSpyObj(
"domainObject",
["getId", "getModel"]
);
mockDomainObj.getId.andReturn(id);
mockDomainObj.getModel.andReturn({ index: testIndexes[id] });
return mockDomainObj;
});
// Expose those domain objects...
mockSubscription.getTelemetryObjects.andReturn(mockDomainObjects);
mockScope.domainObject = mockDomainObject;
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
// Fire subscription callback
mockSubscriber.subscribe.mostRecentCall.args[1]();
// Controller should expose same objects, but sorted by index from model
expect(controller.getTelemetryObjects()).toEqual([
mockDomainObjects[3], // d, index=0
mockDomainObjects[1], // b, index=1
mockDomainObjects[0], // a, index=2
mockDomainObjects[2] // c, index=3
]);
});
it("uses a timeout to throttle update", function () {
// Set up subscription...
mockSubscription.getTelemetryObjects
.andReturn([mockDomainObject]);
mockScope.domainObject = mockDomainObject;
// Set the object in view; should not need a timeout
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(mockTimeout.calls.length).toEqual(0);
// Next call should schedule an update on a timeout
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(mockTimeout.calls.length).toEqual(1);
// ...but this last one should not, since existing
// timeout will cover it
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(mockTimeout.calls.length).toEqual(1);
});
it("allows changing column width", function () {
var initialWidth = controller.columnWidth();
controller.increaseColumnWidth();
expect(controller.columnWidth()).toBeGreaterThan(initialWidth);
});
describe("filter", function () {
var doFilter,
filteredObjects,
filteredObjectNames;
beforeEach(function () {
var telemetryObjects,
updateFilteredObjects;
telemetryObjects = [
'DEF123',
'abc789',
'456abc',
'4ab3cdef',
'hjs[12].*(){}^\\'
].map(function (objectName, index) {
var mockTelemetryObject = jasmine.createSpyObj(
objectName,
["getId", "getModel"]
);
mockTelemetryObject.getId.andReturn(objectName);
mockTelemetryObject.getModel.andReturn({
name: objectName,
index: index
});
return mockTelemetryObject;
});
mockSubscription
.getTelemetryObjects
.andReturn(telemetryObjects);
// Trigger domainObject change to create subscription.
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
updateFilteredObjects = function () {
filteredObjects = controller.getTelemetryObjects();
filteredObjectNames = filteredObjects.map(function (o) {
return o.getModel().name;
});
};
doFilter = function (term) {
controller.filter(term);
// Filter is debounced so we have to force it to occur.
mockTimeout.mostRecentCall.args[0]();
updateFilteredObjects();
};
updateFilteredObjects();
});
it("initially shows all objects", function () {
expect(filteredObjectNames).toEqual([
'DEF123',
'abc789',
'456abc',
'4ab3cdef',
'hjs[12].*(){}^\\'
]);
});
it("by blank string matches all objects", function () {
doFilter('');
expect(filteredObjectNames).toEqual([
'DEF123',
'abc789',
'456abc',
'4ab3cdef',
'hjs[12].*(){}^\\'
]);
});
it("exactly matches an object name", function () {
doFilter('4ab3cdef');
expect(filteredObjectNames).toEqual(['4ab3cdef']);
});
it("partially matches object names", function () {
doFilter('abc');
expect(filteredObjectNames).toEqual([
'abc789',
'456abc'
]);
});
it("matches case insensitive names", function () {
doFilter('def');
expect(filteredObjectNames).toEqual([
'DEF123',
'4ab3cdef'
]);
});
it("works as expected with special characters", function () {
doFilter('[12]');
expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']);
doFilter('.*');
expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']);
doFilter('.*()');
expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']);
doFilter('.*?');
expect(filteredObjectNames).toEqual([]);
doFilter('.+');
expect(filteredObjectNames).toEqual([]);
});
it("exposes CSS classes from limits", function () {
var id = mockDomainObject.getId(),
testClass = "some-css-class",
mockLimitCapability =
jasmine.createSpyObj('limit', ['evaluate']);
mockDomainObject.getCapability.andCallFake(function (key) {
return key === 'limit' && mockLimitCapability;
});
mockLimitCapability.evaluate
.andReturn({ cssClass: testClass });
mockSubscription.getTelemetryObjects
.andReturn([mockDomainObject]);
fireFnWatches();
mockSubscriber.subscribe.mostRecentCall.args[1]();
expect(controller.classes()[id]).toEqual(testClass);
});
it("exposes a counter that changes with each update", function () {
var i, prior;
for (i = 0; i < 10; i += 1) {
prior = controller.counter();
expect(controller.counter()).toEqual(prior);
mockSubscriber.subscribe.mostRecentCall.args[1]();
expect(controller.counter()).not.toEqual(prior);
}
});
});
});
}
);

View File

@ -0,0 +1,39 @@
define(
["../src/MCTAutoflowTable"],
function (MCTAutoflowTable) {
describe("The mct-autoflow-table directive", function () {
var mctAutoflowTable;
beforeEach(function () {
mctAutoflowTable = new MCTAutoflowTable();
});
// Real functionality is contained/tested in the linker,
// so just check to make sure we're exposing the directive
// appropriately.
it("is applicable at the element level", function () {
expect(mctAutoflowTable.restrict).toEqual("E");
});
it("two-ways binds needed scope variables", function () {
expect(mctAutoflowTable.scope).toEqual({
objects: "=",
values: "=",
rows: "=",
updated: "=",
classes: "=",
columnWidth: "=",
counter: "="
});
});
it("provides a link function", function () {
expect(mctAutoflowTable.link).toEqual(jasmine.any(Function));
});
});
}
);

View File

@ -45,7 +45,7 @@ define(
FollowIndicator.prototype.getText = function () {
var timer = this.timerService.getTimer();
return timer ? ('Following timer ' + timer.name) : NO_TIMER;
return (timer) ? 'Following timer ' + timer.getModel().name : NO_TIMER;
};
FollowIndicator.prototype.getDescription = function () {

View File

@ -42,15 +42,18 @@ define(["../../src/indicators/FollowIndicator"], function (FollowIndicator) {
});
describe("when a timer is set", function () {
var testObject;
var testModel;
var mockDomainObject;
beforeEach(function () {
testObject = { name: "some timer!" };
mockTimerService.getTimer.andReturn(testObject);
testModel = { name: "some timer!" };
mockDomainObject = jasmine.createSpyObj('timer', ['getModel']);
mockDomainObject.getModel.andReturn(testModel);
mockTimerService.getTimer.andReturn(mockDomainObject);
});
it("displays the timer's name", function () {
expect(indicator.getText().indexOf(testObject.name))
expect(indicator.getText().indexOf(testModel.name))
.not.toEqual(-1);
});
});

View File

@ -260,9 +260,7 @@ define([
"key": "LayoutController",
"implementation": LayoutController,
"depends": [
"$scope",
"$element",
"openmct"
"$scope"
]
},
{
@ -272,8 +270,7 @@ define([
"$scope",
"$q",
"dialogService",
"openmct",
"$element"
"openmct"
]
}
],
@ -337,6 +334,46 @@ define([
"conversion": "number[]"
}
]
},
{
"key": "telemetry.panel",
"name": "Telemetry Panel",
"cssClass": "icon-telemetry-panel",
"description": "A panel for collecting telemetry elements.",
"priority": 899,
"delegates": [
"telemetry"
],
"features": "creation",
"contains": [
{
"has": "telemetry"
}
],
"model": {
"composition": []
},
"properties": [
{
"name": "Layout Grid",
"control": "composite",
"items": [
{
"name": "Horizontal grid (px)",
"control": "textfield",
"cssClass": "l-input-sm l-numeric"
},
{
"name": "Vertical grid (px)",
"control": "textfield",
"cssClass": "l-input-sm l-numeric"
}
],
"pattern": "^(\\d*[1-9]\\d*)?$",
"property": "layoutGrid",
"conversion": "number[]"
}
]
}
]
}

View File

@ -23,7 +23,7 @@
ng-controller="FixedController as controller">
<!-- Background grid -->
<div class="l-grid-holder" ng-click="controller.bypassSelection($event)">
<div class="l-grid-holder" ng-click="controller.clearSelection()">
<div class="l-grid l-grid-x"
ng-if="!controller.getGridSize()[0] < 3"
ng-style="{ 'background-size': controller.getGridSize() [0] + 'px 100%' }"></div>
@ -35,28 +35,33 @@
<!-- Fixed position elements -->
<div ng-repeat="element in controller.getElements()"
class="l-fixed-position-item s-selectable s-moveable s-hover-border"
ng-class="{
's-not-selected': controller.selected() && !controller.selected(element),
's-selected': controller.selected(element)
}"
ng-style="element.style"
mct-selectable="controller.getContext(element)"
mct-init-select="controller.shouldSelect(element)">
ng-click="controller.select(element)">
<mct-include key="element.template"
parameters="{ gridSize: controller.getGridSize() }"
ng-model="element">
</mct-include>
</mct-include>
</div>
<!-- Selection highlight, handles -->
<span class="s-selected s-moveable" ng-if="controller.isElementSelected()">
<span class="s-selected s-moveable" ng-if="controller.selected()">
<div class="l-fixed-position-item t-edit-handle-holder"
mct-drag-down="controller.moveHandle().startDrag()"
mct-drag-down="controller.moveHandle().startDrag(controller.selected())"
mct-drag="controller.moveHandle().continueDrag(delta)"
mct-drag-up="controller.endDrag()"
ng-style="controller.getSelectedElementStyle()">
mct-drag-up="controller.moveHandle().endDrag()"
ng-style="controller.selected().style">
</div>
<div ng-repeat="handle in controller.handles()"
class="l-fixed-position-item-handle edit-corner"
ng-style="handle.style()"
mct-drag-down="handle.startDrag()"
mct-drag="handle.continueDrag(delta)"
mct-drag-up="controller.endDrag(handle)">
mct-drag-up="handle.endDrag()">
</div>
</span>
</div>

View File

@ -22,12 +22,10 @@
<div class="abs l-layout"
ng-controller="LayoutController as controller"
ng-click="controller.bypassSelection($event)">
ng-click="controller.clearSelection()">
<!-- Background grid -->
<div class="l-grid-holder"
ng-show="!controller.drilledIn"
ng-click="controller.bypassSelection($event)">
<div class="l-grid-holder" ng-click="controller.clearSelection()">
<div class="l-grid l-grid-x"
ng-if="!controller.getGridSize()[0] < 3"
ng-style="{ 'background-size': controller.getGridSize() [0] + 'px 100%' }"></div>
@ -36,13 +34,10 @@
ng-style="{ 'background-size': '100% ' + controller.getGridSize() [1] + 'px' }"></div>
</div>
<div class="abs frame t-frame-outer child-frame panel s-selectable s-moveable s-hover-border t-object-type-{{ childObject.getModel().type }}"
data-layout-id="{{childObject.getId() + '-' + $id}}"
ng-class="{ 'no-frame': !controller.hasFrame(childObject), 's-drilled-in': controller.isDrilledIn(childObject) }"
<div class='abs frame t-frame-outer child-frame panel s-selectable s-moveable s-hover-border'
ng-class="{ 'no-frame': !controller.hasFrame(childObject), 's-selected':controller.selected(childObject) }"
ng-repeat="childObject in composition"
ng-init="controller.selectIfNew(childObject.getId() + '-' + $id, childObject)"
mct-selectable="controller.getContext(childObject, true)"
ng-dblclick="controller.drill($event, childObject)"
ng-click="controller.select($event, childObject.getId())"
ng-style="controller.getFrameStyle(childObject.getId())">
<mct-representation key="'frame'"
@ -50,7 +45,7 @@
mct-object="childObject">
</mct-representation>
<!-- Drag handles -->
<span class="abs t-edit-handle-holder" ng-if="controller.selected(childObject) && !controller.isDrilledIn(childObject)">
<span class="abs t-edit-handle-holder s-hover-border" ng-if="controller.selected(childObject)">
<span class="edit-handle edit-move"
mct-drag-down="controller.startDrag(childObject.getId(), [1,1], [0,0])"
mct-drag="controller.continueDrag(delta)"
@ -78,6 +73,7 @@
mct-drag-up="controller.endDrag()">
</span>
</span>
</div>
</div>

View File

@ -47,7 +47,7 @@ define(
* @constructor
* @param {Scope} $scope the controller's Angular scope
*/
function FixedController($scope, $q, dialogService, openmct, $element) {
function FixedController($scope, $q, dialogService, openmct) {
this.names = {}; // Cache names by ID
this.values = {}; // Cache values by ID
this.elementProxiesById = {};
@ -55,11 +55,9 @@ define(
this.telemetryObjects = [];
this.subscriptions = [];
this.openmct = openmct;
this.$element = $element;
this.$scope = $scope;
this.gridSize = $scope.domainObject && $scope.domainObject.getModel().layoutGrid;
this.fixedViewSelectable = false;
var self = this;
[
@ -89,8 +87,9 @@ define(
// Update the style for a selected element
function updateSelectionStyle() {
if (self.selectedElementProxy) {
self.selectedElementProxy.style = convertPosition(self.selectedElementProxy);
var element = self.selection && self.selection.get();
if (element) {
element.style = convertPosition(element);
}
}
@ -137,19 +136,25 @@ define(
// Decorate elements in the current configuration
function refreshElements() {
var elements = (($scope.configuration || {}).elements || []);
// Cache selection; we are instantiating new proxies
// so we may want to restore this.
var selected = self.selection && self.selection.get(),
elements = (($scope.configuration || {}).elements || []),
index = -1; // Start with a 'not-found' value
// Find the selection in the new array
if (selected !== undefined) {
index = elements.indexOf(selected.element);
}
// Create the new proxies...
self.elementProxies = elements.map(makeProxyElement);
// If selection is not in array, select parent.
// Otherwise, set the element to select after refresh.
if (self.selectedElementProxy) {
var index = elements.indexOf(self.selectedElementProxy.element);
if (index === -1) {
self.$element[0].click();
} else if (!self.elementToSelectAfterRefresh) {
self.elementToSelectAfterRefresh = self.elementProxies[index].element;
// Clear old selection, and restore if appropriate
if (self.selection) {
self.selection.deselect();
if (index > -1) {
self.select(self.elementProxies[index]);
}
}
@ -219,12 +224,12 @@ define(
$scope.configuration.elements || [];
// Store the position of this element.
$scope.configuration.elements.push(element);
self.elementToSelectAfterRefresh = element;
// Refresh displayed elements
refreshElements();
// Select the newly-added element
self.select(
self.elementProxies[self.elementProxies.length - 1]
);
// Mark change as persistable
if ($scope.commit) {
$scope.commit("Dropped an element.");
@ -258,36 +263,21 @@ define(
self.getTelemetry($scope.domainObject);
}
// Sets the selectable object in response to the selection change event.
function setSelection(selectable) {
var selection = selectable[0];
if (!selection) {
return;
}
if (selection.context.elementProxy) {
self.selectedElementProxy = selection.context.elementProxy;
self.mvHandle = self.generateDragHandle(self.selectedElementProxy);
self.resizeHandles = self.generateDragHandles(self.selectedElementProxy);
} else {
// Make fixed view selectable if it's not already.
if (!self.fixedViewSelectable && selectable.length === 1) {
self.fixedViewSelectable = true;
selection.context.viewProxy = new FixedProxy(addElement, $q, dialogService);
self.openmct.selection.select(selection);
}
self.resizeHandles = [];
self.mvHandle = undefined;
self.selectedElementProxy = undefined;
}
}
this.elementProxies = [];
this.generateDragHandle = generateDragHandle;
this.generateDragHandles = generateDragHandles;
this.updateSelectionStyle = updateSelectionStyle;
// Track current selection state
$scope.$watch("selection", function (selection) {
this.selection = selection;
// Expose the view's selection proxy
if (this.selection) {
this.selection.proxy(
new FixedProxy(addElement, $q, dialogService)
);
}
}.bind(this));
// Detect changes to grid size
$scope.$watch("model.layoutGrid", updateElementPositions);
@ -308,13 +298,10 @@ define(
$scope.$on("$destroy", function () {
self.unsubscribe();
self.openmct.time.off("bounds", updateDisplayBounds);
self.openmct.selection.off("change", setSelection);
});
// Respond to external bounds changes
this.openmct.time.on("bounds", updateDisplayBounds);
this.openmct.selection.on('change', setSelection);
this.$element.on('click', this.bypassSelection.bind(this));
}
/**
@ -505,56 +492,38 @@ define(
};
/**
* Checks if the element should be selected or not.
*
* @param elementProxy the element to check
* @returns {boolean} true if the element should be selected.
* Check if the element is currently selected, or (if no
* argument is supplied) get the currently selected element.
* @returns {boolean} true if selected
*/
FixedController.prototype.shouldSelect = function (elementProxy) {
if (elementProxy.element === this.elementToSelectAfterRefresh) {
delete this.elementToSelectAfterRefresh;
return true;
} else {
return false;
FixedController.prototype.selected = function (element) {
var selection = this.selection;
return selection && ((arguments.length > 0) ?
selection.selected(element) : selection.get());
};
/**
* Set the active user selection in this view.
* @param element the element to select
*/
FixedController.prototype.select = function select(element) {
if (this.selection) {
// Update selection...
this.selection.select(element);
// ...as well as move, resize handles
this.mvHandle = this.generateDragHandle(element);
this.resizeHandles = this.generateDragHandles(element);
}
};
/**
* Checks if an element is currently selected.
*
* @returns {boolean} true if an element is selected.
* Clear the current user selection.
*/
FixedController.prototype.isElementSelected = function () {
return (this.selectedElementProxy) ? true : false;
};
/**
* Gets the style for the selected element.
*
* @returns {string} element style
*/
FixedController.prototype.getSelectedElementStyle = function () {
return (this.selectedElementProxy) ? this.selectedElementProxy.style : undefined;
};
/**
* Gets the selected element.
*
* @returns the selected element
*/
FixedController.prototype.getSelectedElement = function () {
return this.selectedElementProxy;
};
/**
* Prevents the event from bubbling up if drag is in progress.
*/
FixedController.prototype.bypassSelection = function ($event) {
if (this.dragInProgress) {
if ($event) {
$event.stopPropagation();
}
return;
FixedController.prototype.clearSelection = function () {
if (this.selection) {
this.selection.deselect();
this.resizeHandles = [];
this.mvHandle = undefined;
}
};
@ -575,38 +544,6 @@ define(
return this.mvHandle;
};
/**
* Gets the selection context.
*
* @param elementProxy the element proxy
* @returns {object} the context object which includes elementProxy and toolbar
*/
FixedController.prototype.getContext = function (elementProxy) {
return {
elementProxy: elementProxy,
toolbar: elementProxy
};
};
/**
* End drag.
*
* @param handle the resize handle
*/
FixedController.prototype.endDrag = function (handle) {
this.dragInProgress = true;
setTimeout(function () {
this.dragInProgress = false;
}.bind(this), 0);
if (handle) {
handle.endDrag();
} else {
this.moveHandle().endDrag();
}
};
return FixedController;
}
);

View File

@ -65,7 +65,7 @@ define(
* Start a drag gesture. This should be called when a drag
* begins to track initial state.
*/
FixedDragHandle.prototype.startDrag = function () {
FixedDragHandle.prototype.startDrag = function startDrag() {
// Cache initial x/y positions
this.dragging = {
x: this.elementHandle.x(),

View File

@ -27,11 +27,9 @@
*/
define(
[
'zepto',
'./LayoutDrag'
],
function (
$,
LayoutDrag
) {
@ -52,12 +50,10 @@ define(
* @constructor
* @param {Scope} $scope the controller's Angular scope
*/
function LayoutController($scope, $element, openmct) {
function LayoutController($scope) {
var self = this,
callbackCount = 0;
this.$element = $element;
// Update grid size when it changed
function updateGridSize(layoutGrid) {
var oldSize = self.gridSize;
@ -127,11 +123,12 @@ define(
self.layoutPanels(ids);
self.setFrames(ids);
if (self.selectedId &&
self.selectedId !== $scope.domainObject.getId() &&
composition.indexOf(self.selectedId) === -1) {
// Click triggers selection of layout parent.
self.$element[0].click();
// If there is a newly-dropped object, select it.
if (self.droppedIdToSelectAfterRefresh) {
self.select(null, self.droppedIdToSelectAfterRefresh);
delete self.droppedIdToSelectAfterRefresh;
} else if (composition.indexOf(self.selectedId) === -1) {
self.clearSelection();
}
}
});
@ -163,39 +160,22 @@ define(
}
};
// Sets the selectable object in response to the selection change event.
function setSelection(selectable) {
var selection = selectable[0];
if (!selection) {
delete self.selectedId;
return;
}
self.selectedId = selection.context.oldItem.getId();
self.drilledIn = undefined;
self.selectable = selectable;
}
this.positions = {};
this.rawPositions = {};
this.gridSize = DEFAULT_GRID_SIZE;
this.$scope = $scope;
this.drilledIn = undefined;
this.openmct = openmct;
// Watch for changes to the grid size in the model
$scope.$watch("model.layoutGrid", updateGridSize);
$scope.$watch("selection", function (selection) {
this.selection = selection;
}.bind(this));
// Update composed objects on screen, and position panes
$scope.$watchCollection("model.composition", refreshComposition);
openmct.selection.on('change', setSelection);
$scope.$on("$destroy", function () {
openmct.selection.off("change", setSelection);
});
// Position panes where they are dropped
$scope.$on("mctDrop", handleDrop);
}
@ -377,14 +357,37 @@ define(
};
/**
* Checks if the object is currently selected.
* Check if the object is currently selected.
*
* @param {string} obj the object to check for selection
* @returns {boolean} true if selected, otherwise false
*/
LayoutController.prototype.selected = function (obj) {
var sobj = this.openmct.selection.get()[0];
return (sobj && sobj.context.oldItem.getId() === obj.getId()) ? true : false;
return !!this.selectedId && this.selectedId === obj.getId();
};
/**
* Set the active user selection in this view.
*
* @param event the mouse event
* @param {string} id the object id
*/
LayoutController.prototype.select = function (event, id) {
if (event) {
event.stopPropagation();
if (this.selection) {
event.preventDefault();
}
}
this.selectedId = id;
var selectedObj = {};
selectedObj[this.frames[id] ? 'hideFrame' : 'showFrame'] = this.toggleFrame.bind(this, id);
if (this.selection) {
this.selection.select(selectedObj);
}
};
/**
@ -393,7 +396,7 @@ define(
* @param {string} id the object id
* @private
*/
LayoutController.prototype.toggleFrame = function (id, domainObject) {
LayoutController.prototype.toggleFrame = function (id) {
var configuration = this.$scope.configuration;
if (!configuration.panels[id]) {
@ -401,75 +404,21 @@ define(
}
this.frames[id] = configuration.panels[id].hasFrame = !this.frames[id];
var selection = this.openmct.selection.get();
selection[0].context.toolbar = this.getToolbar(id, domainObject);
this.openmct.selection.select(selection); // reselect so toolbar updates
this.select(undefined, id); // reselect so toolbar updates
};
/**
* Gets the toolbar object for the given domain object.
*
* @param id the domain object id
* @param domainObject the domain object
* @returns {object}
* @private
* Clear the current user selection.
*/
LayoutController.prototype.getToolbar = function (id, domainObject) {
var toolbarObj = {};
toolbarObj[this.frames[id] ? 'hideFrame' : 'showFrame'] = this.toggleFrame.bind(this, id, domainObject);
return toolbarObj;
};
/**
* Bypasses selection if drag is in progress.
*
* @param event the angular event object
*/
LayoutController.prototype.bypassSelection = function (event) {
LayoutController.prototype.clearSelection = function () {
if (this.dragInProgress) {
if (event) {
event.stopPropagation();
}
return;
}
};
/**
* Checks if the domain object is drilled in.
*
* @param domainObject the domain object
* @return true if the object is drilled in, false otherwise
*/
LayoutController.prototype.isDrilledIn = function (domainObject) {
return this.drilledIn === domainObject.getId();
};
/**
* Puts the given object in the drilled-in mode.
*
* @param event the angular event object
* @param domainObject the domain object
*/
LayoutController.prototype.drill = function (event, domainObject) {
if (event) {
event.stopPropagation();
}
if (!domainObject.getCapability('editor').inEditContext()) {
return;
}
if (!domainObject.hasCapability('composition')) {
return;
if (this.selection) {
this.selection.deselect();
delete this.selectedId;
}
// Disable since fixed position doesn't use the selection API yet
if (domainObject.getModel().type === 'telemetry.fixed') {
return;
}
this.drilledIn = domainObject.getId();
};
/**
@ -491,36 +440,6 @@ define(
return this.gridSize;
};
/**
* Gets the selection context.
*
* @param domainObject the domain object
* @returns {object} the context object which includes
* item, oldItem and toolbar
*/
LayoutController.prototype.getContext = function (domainObject, toolbar) {
return {
item: domainObject.useCapability('adapter'),
oldItem: domainObject,
toolbar: toolbar ? this.getToolbar(domainObject.getId(), domainObject) : undefined
};
};
/**
* Selects a newly-dropped object.
*
* @param classSelector the css class selector
* @param domainObject the domain object
*/
LayoutController.prototype.selectIfNew = function (selector, domainObject) {
if (domainObject.getId() === this.droppedIdToSelectAfterRefresh) {
setTimeout(function () {
$('[data-layout-id="' + selector + '"]')[0].click();
delete this.droppedIdToSelectAfterRefresh;
}.bind(this), 0);
}
};
return LayoutController;
}
);

View File

@ -55,8 +55,8 @@ define(
* @param element the fixed position element, as stored in its
* configuration
* @param index the element's index within its array
* @param {Array} elements the full array of elements
* @param {number[]} gridSize the current layout grid size in [x,y] from
* @param {Array} elements the full array of elements
*/
function ElementProxy(element, index, elements, gridSize) {
/**

View File

@ -21,14 +21,8 @@
*****************************************************************************/
define(
[
"../src/FixedController",
"zepto"
],
function (
FixedController,
$
) {
["../src/FixedController"],
function (FixedController) {
describe("The Fixed Position controller", function () {
var mockScope,
@ -52,9 +46,6 @@ define(
mockMetadata,
mockTimeSystem,
mockLimitEvaluator,
mockSelection,
$element = [],
selectable = [],
controller;
// Utility function; find a watch for a given expression
@ -189,30 +180,17 @@ define(
mockScope.model = testModel;
mockScope.configuration = testConfiguration;
selectable[0] = {
context: {
oldItem: mockDomainObject
}
};
mockSelection = jasmine.createSpyObj("selection", [
'select',
'on',
'off',
'get'
]);
mockSelection.get.andCallThrough();
mockScope.selection = jasmine.createSpyObj(
'selection',
['select', 'get', 'selected', 'deselect', 'proxy']
);
mockOpenMCT = {
time: mockConductor,
telemetry: mockTelemetryAPI,
composition: mockCompositionAPI,
selection: mockSelection
composition: mockCompositionAPI
};
$element = $('<div></div>');
spyOn($element[0], 'click');
mockMetadata = jasmine.createSpyObj('mockMetadata', [
'valuesForHints',
'value',
@ -248,11 +226,11 @@ define(
mockScope,
mockQ,
mockDialogService,
mockOpenMCT,
$element
mockOpenMCT
);
findWatch("model.layoutGrid")(testModel.layoutGrid);
findWatch("selection")(mockScope.selection);
});
it("subscribes when a domain object is available", function () {
@ -328,41 +306,41 @@ define(
});
it("allows elements to be selected", function () {
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
selectable[0].context.elementProxy = controller.getElements()[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(controller.isElementSelected()).toBe(true);
});
it("allows selection retrieval", function () {
var elements;
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
elements = controller.getElements();
selectable[0].context.elementProxy = elements[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(controller.getSelectedElement()).toEqual(elements[1]);
controller.select(elements[1]);
expect(mockScope.selection.select)
.toHaveBeenCalledWith(elements[1]);
});
it("selects the parent view when selected element is removed", function () {
it("allows selection retrieval", function () {
// selected with no arguments should give the current
// selection
var elements;
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
var elements = controller.getElements();
selectable[0].context.elementProxy = elements[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
elements = controller.getElements();
controller.select(elements[1]);
mockScope.selection.get.andReturn(elements[1]);
expect(controller.selected()).toEqual(elements[1]);
});
elements[1].remove();
testModel.modified = 2;
it("allows selections to be cleared", function () {
var elements;
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
expect($element[0].click).toHaveBeenCalled();
elements = controller.getElements();
controller.select(elements[1]);
controller.clearSelection();
expect(controller.selected(elements[1])).toBeFalsy();
});
it("retains selections during refresh", function () {
@ -374,21 +352,23 @@ define(
findWatch("model.modified")(testModel.modified);
elements = controller.getElements();
selectable[0].context.elementProxy = elements[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
controller.select(elements[1]);
expect(controller.getSelectedElement()).toEqual(elements[1]);
// Verify precondition
expect(mockScope.selection.select.calls.length).toEqual(1);
// Mimic selection behavior
mockScope.selection.get.andReturn(elements[1]);
elements[2].remove();
testModel.modified = 2;
findWatch("model.modified")(testModel.modified);
elements = controller.getElements();
// Verify removal, as test assumes this
expect(elements.length).toEqual(2);
expect(controller.shouldSelect(elements[1])).toBe(true);
expect(mockScope.selection.select.calls.length).toEqual(2);
});
it("Displays received values for telemetry elements", function () {
@ -525,25 +505,21 @@ define(
});
it("exposes a view-level selection proxy", function () {
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
var selection = mockOpenMCT.selection.select.mostRecentCall.args[0];
expect(mockOpenMCT.selection.select).toHaveBeenCalled();
expect(selection.context.viewProxy).toBeDefined();
expect(mockScope.selection.proxy).toHaveBeenCalledWith(
jasmine.any(Object)
);
});
it("exposes drag handles", function () {
var handles;
// Select something so that drag handles are expected
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
selectable[0].context.elementProxy = controller.getElements()[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
controller.select(controller.getElements()[1]);
// Should have a non-empty array of handles
handles = controller.handles();
expect(handles).toEqual(jasmine.any(Array));
expect(handles.length).not.toEqual(0);
@ -556,14 +532,15 @@ define(
});
it("exposes a move handle", function () {
var handle;
// Select something so that drag handles are expected
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
selectable[0].context.elementProxy = controller.getElements()[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
controller.select(controller.getElements()[1]);
// Should have a move handle
var handle = controller.moveHandle();
handle = controller.moveHandle();
// And it should have start/continue/end drag methods
expect(handle.startDrag).toEqual(jasmine.any(Function));
@ -574,40 +551,26 @@ define(
it("updates selection style during drag", function () {
var oldStyle;
// Select something so that drag handles are expected
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
selectable[0].context.elementProxy = controller.getElements()[1];
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
controller.select(controller.getElements()[1]);
mockScope.selection.get.andReturn(controller.getElements()[1]);
// Get style
oldStyle = controller.getSelectedElementStyle();
oldStyle = controller.selected().style;
// Start a drag gesture
controller.moveHandle().startDrag();
// Haven't moved yet; style shouldn't have updated yet
expect(controller.getSelectedElementStyle()).toEqual(oldStyle);
expect(controller.selected().style).toEqual(oldStyle);
// Drag a little
controller.moveHandle().continueDrag([1000, 100]);
// Style should have been updated
expect(controller.getSelectedElementStyle()).not.toEqual(oldStyle);
});
it("cleans up slection on scope destroy", function () {
expect(mockScope.$on).toHaveBeenCalledWith(
'$destroy',
jasmine.any(Function)
);
mockScope.$on.mostRecentCall.args[1]();
expect(mockOpenMCT.selection.off).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
expect(controller.selected().style).not.toEqual(oldStyle);
});
describe("on display bounds changes", function () {
@ -739,14 +702,6 @@ define(
expect(controller.getElements()[0].cssClass).toEqual("alarm-a");
});
});
it("listens for selection change events", function () {
expect(mockOpenMCT.selection.on).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
});
});
});
}

View File

@ -21,14 +21,8 @@
*****************************************************************************/
define(
[
"../src/LayoutController",
"zepto"
],
function (
LayoutController,
$
) {
["../src/LayoutController"],
function (LayoutController) {
describe("The Layout controller", function () {
var mockScope,
@ -38,12 +32,7 @@ define(
controller,
mockCompositionCapability,
mockComposition,
mockCompositionObjects,
mockOpenMCT,
mockSelection,
mockDomainObjectCapability,
$element = [],
selectable = [];
mockCompositionObjects;
function mockPromise(value) {
return {
@ -69,18 +58,21 @@ define(
} else {
return {};
}
},
getCapability: function () {
return mockDomainObjectCapability;
},
hasCapability: function (param) {
if (param === 'composition') {
return id !== 'b';
}
}
};
}
// Utility function to find a watch for a given expression
function findWatch(expr) {
var watch;
mockScope.$watch.calls.forEach(function (call) {
if (call.args[0] === expr) {
watch = call.args[1];
}
});
return watch;
}
beforeEach(function () {
mockScope = jasmine.createSpyObj(
"$scope",
@ -96,6 +88,7 @@ define(
mockComposition = ["a", "b", "c"];
mockCompositionObjects = mockComposition.map(mockDomainObject);
testConfiguration = {
panels: {
a: {
@ -104,70 +97,27 @@ define(
}
}
};
mockDomainObjectCapability = jasmine.createSpyObj('capability',
['inEditContext']
);
mockCompositionCapability = mockPromise(mockCompositionObjects);
mockScope.domainObject = mockDomainObject("mockDomainObject");
mockScope.model = testModel;
mockScope.configuration = testConfiguration;
selectable[0] = {
context: {
oldItem: mockScope.domainObject
}
};
mockSelection = jasmine.createSpyObj("selection", [
'select',
'on',
'off',
'get'
]);
mockSelection.get.andReturn(selectable);
mockOpenMCT = {
selection: mockSelection
};
$element = $('<div></div>');
$(document).find('body').append($element);
spyOn($element[0], 'click');
mockScope.selection = jasmine.createSpyObj(
'selection',
['select', 'get', 'selected', 'deselect']
);
spyOn(mockScope.domainObject, "useCapability").andCallThrough();
controller = new LayoutController(mockScope, $element, mockOpenMCT);
controller = new LayoutController(mockScope);
spyOn(controller, "layoutPanels").andCallThrough();
findWatch("selection")(mockScope.selection);
jasmine.Clock.useMock();
});
afterEach(function () {
$element.remove();
});
it("listens for selection change events", function () {
expect(mockOpenMCT.selection.on).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
});
it("cleans up on scope destroy", function () {
expect(mockScope.$on).toHaveBeenCalledWith(
'$destroy',
jasmine.any(Function)
);
mockScope.$on.calls[0].args[1]();
expect(mockOpenMCT.selection.off).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
});
// Model changes will indicate that panel positions
// may have changed, for instance.
it("watches for changes to composition", function () {
@ -370,35 +320,67 @@ define(
.not.toEqual(oldStyle);
});
it("allows objects to be selected", function () {
it("allows panels to be selected", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
selectable[0].context.oldItem = childObj;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
controller.select(mockEvent, childObj.getId());
expect(mockEvent.stopPropagation).toHaveBeenCalled();
expect(controller.selected(childObj)).toBe(true);
});
it("prevents event bubbling while drag is in progress", function () {
it("allows selection to be cleared", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
controller.select(null, childObj.getId());
controller.clearSelection();
expect(controller.selected(childObj)).toBeFalsy();
});
it("prevents clearing selection while drag is in progress", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
var id = childObj.getId();
controller.select(mockEvent, id);
// Do a drag
controller.startDrag(childObj.getId(), [1, 1], [0, 0]);
controller.startDrag(id, [1, 1], [0, 0]);
controller.continueDrag([100, 100]);
controller.endDrag();
// Because mouse position could cause the parent object to be selected, this should be ignored.
controller.bypassSelection(mockEvent);
// Because mouse position could cause clearSelection to be called, this should be ignored.
controller.clearSelection();
expect(mockEvent.stopPropagation).toHaveBeenCalled();
expect(controller.selected(childObj)).toBe(true);
// Shoud be able to select another object when dragging is done.
// Shoud be able to clear the selection after dragging is done.
jasmine.Clock.tick(0);
mockEvent.stopPropagation.reset();
controller.bypassSelection(mockEvent);
controller.clearSelection();
expect(mockEvent.stopPropagation).not.toHaveBeenCalled();
expect(controller.selected(childObj)).toBe(false);
});
it("clears selection after moving/resizing", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
var id = childObj.getId();
controller.select(mockEvent, id);
// Do a drag
controller.startDrag(id, [1, 1], [0, 0]);
controller.continueDrag([100, 100]);
controller.endDrag();
jasmine.Clock.tick(0);
controller.clearSelection();
expect(controller.selected(childObj)).toBe(false);
});
it("shows frames by default", function () {
@ -416,74 +398,43 @@ define(
it("hides frame when selected object has frame ", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
selectable[0].context.oldItem = childObj;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
var toolbarObj = controller.getToolbar(childObj.getId(), childObj);
controller.select(mockEvent, childObj.getId());
expect(mockScope.selection.select).toHaveBeenCalled();
var selectedObj = mockScope.selection.select.mostRecentCall.args[0];
expect(controller.hasFrame(childObj)).toBe(true);
expect(toolbarObj.hideFrame).toBeDefined();
expect(toolbarObj.hideFrame).toEqual(jasmine.any(Function));
expect(selectedObj.hideFrame).toBeDefined();
expect(selectedObj.hideFrame).toEqual(jasmine.any(Function));
});
it("shows frame when selected object has no frame", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[1];
selectable[0].context.oldItem = childObj;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
var toolbarObj = controller.getToolbar(childObj.getId(), childObj);
controller.select(mockEvent, childObj.getId());
expect(mockScope.selection.select).toHaveBeenCalled();
var selectedObj = mockScope.selection.select.mostRecentCall.args[0];
expect(controller.hasFrame(childObj)).toBe(false);
expect(toolbarObj.showFrame).toBeDefined();
expect(toolbarObj.showFrame).toEqual(jasmine.any(Function));
expect(selectedObj.showFrame).toBeDefined();
expect(selectedObj.showFrame).toEqual(jasmine.any(Function));
});
it("selects the parent object when selected object is removed", function () {
it("deselects the object that is no longer in the composition", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
selectable[0].context.oldItem = childObj;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
controller.select(mockEvent, childObj.getId());
var composition = ["b", "c"];
mockScope.$watchCollection.mostRecentCall.args[1](composition);
expect($element[0].click).toHaveBeenCalled();
expect(controller.selected(childObj)).toBe(false);
});
it("allows objects to be drilled-in only when editing", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
childObj.getCapability().inEditContext.andReturn(false);
controller.drill(mockEvent, childObj);
expect(controller.isDrilledIn(childObj)).toBe(false);
});
it("allows objects to be drilled-in only if it has sub objects", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[1];
childObj.getCapability().inEditContext.andReturn(true);
controller.drill(mockEvent, childObj);
expect(controller.isDrilledIn(childObj)).toBe(false);
});
it("selects a newly-dropped object", function () {
mockScope.$on.mostRecentCall.args[1](
mockEvent,
'd',
{ x: 300, y: 100 }
);
var childObj = mockDomainObject("d");
var testElement = $("<div data-layout-id='some-id'></div>");
$element.append(testElement);
spyOn(testElement[0], 'click');
controller.selectIfNew('some-id', childObj);
jasmine.Clock.tick(0);
expect(testElement[0].click).toHaveBeenCalled();
});
});
}
);

View File

@ -0,0 +1,37 @@
# Plot README
## Chart
The `mct-chart` directive is used to support drawing of simple charts. It is
present to support the Plot view, and its functionality is limited to the
functionality that is relevant for that view.
This directive is used at the element level and takes one attribute, `draw`
which is an Angular expression which will should evaluate to a drawing object.
This drawing object should contain the following properties:
* `dimensions`: The size, in logical coordinates, of the chart area. A
two-element array or numbers.
* `origin`: The position, in logical coordinates, of the lower-left corner of
the chart area. A two-element array or numbers.
* `lines`: An array of lines (e.g. as a plot line) to draw, where each line is
expressed as an object containing:
* `buffer`: A Float32Array containing points in the line, in logical
coordinates, in sequential x,y pairs.
* `color`: The color of the line, as a four-element RGBA array, where
each element is a number in the range of 0.0-1.0.
* `points`: The number of points in the line.
* `boxes`: An array of rectangles to draw in the chart area. Each is an object
containing:
* `start`: The first corner of the rectangle, as a two-element array of
numbers, in logical coordinates.
* `end`: The opposite corner of the rectangle, as a two-element array of
numbers, in logical coordinates. color : The color of the line, as a
four-element RGBA array, where each element is a number in the range of
0.0-1.0.
While `mct-chart` is intended to support plots specifically, it does perform
some useful management of canvas objects (e.g. choosing between WebGL and Canvas
2D APIs for drawing based on browser support) so its usage is recommended when
its supported drawing primitives are sufficient for other charting tasks.

View File

@ -0,0 +1,157 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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([
"./src/MCTChart",
"./src/PlotController",
"./src/policies/PlotViewPolicy",
"./src/PlotOptionsController",
"./src/services/ExportImageService",
"text!./res/templates/plot.html",
"text!./res/templates/plot-options-browse.html",
'legacyRegistry'
], function (
MCTChart,
PlotController,
PlotViewPolicy,
PlotOptionsController,
exportImageService,
plotTemplate,
plotOptionsBrowseTemplate,
legacyRegistry
) {
legacyRegistry.register("platform/features/plot", {
"name": "Plot view for telemetry",
"extensions": {
"views": [
{
"name": "Plot",
"key": "plot",
"cssClass": "icon-sine",
"template": plotTemplate,
"needs": [
"telemetry"
],
"priority": "preferred",
"delegation": true
}
],
"directives": [
{
"key": "mctChart",
"implementation": MCTChart,
"depends": [
"$interval",
"$log"
]
}
],
"controllers": [
{
"key": "PlotController",
"implementation": PlotController,
"depends": [
"$scope",
"$element",
"exportImageService",
"telemetryFormatter",
"telemetryHandler",
"throttle",
"PLOT_FIXED_DURATION",
"openmct"
]
},
{
"key": "PlotOptionsController",
"implementation": PlotOptionsController,
"depends": [
"$scope"
]
}
],
"services": [
{
"key": "exportImageService",
"implementation": exportImageService,
"depends": [
"$q",
"$timeout",
"$log",
"EXPORT_IMAGE_TIMEOUT"
]
}
],
"constants": [
{
"key": "PLOT_FIXED_DURATION",
"value": 900000,
"priority": "fallback",
"comment": "Fifteen minutes."
},
{
"key": "EXPORT_IMAGE_TIMEOUT",
"value": 500,
"priority": "fallback"
}
],
"policies": [
{
"category": "view",
"implementation": PlotViewPolicy,
"depends": [
"openmct"
]
}
],
"representations": [
{
"key": "plot-options-browse",
"template": plotOptionsBrowseTemplate
}
],
"licenses": [
{
"name": "FileSaver.js",
"version": "0.0.2",
"author": "Eli Grey",
"description": "File download initiator (for file exports)",
"website": "https://github.com/eligrey/FileSaver.js/",
"copyright": "Copyright © 2015 Eli Grey.",
"license": "license-mit",
"link": "https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md"
},
{
"name": "html2canvas",
"version": "0.4.1",
"author": "Niklas von Hertzen",
"description": "JavaScript HTML renderer",
"website": "https://github.com/niklasvh/html2canvas",
"copyright": "Copyright © 2012 Niklas von Hertzen.",
"license": "license-mit",
"link": "https://github.com/niklasvh/html2canvas/blob/master/LICENSE"
}
]
}
});
});

View File

@ -0,0 +1,70 @@
<!--
Open MCT, Copyright (c) 2014-2017, 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.
-->
<div ng-controller="PlotOptionsController" class="flex-elem grows l-inspector-part">
<em class="t-inspector-part-header" title="Display properties for this object">Plot Options</em>
<mct-form
ng-model="configuration.plot.xAxis"
structure="xAxisForm"
name="xAxisFormState"
class="flex-elem l-flex-row no-margin">
</mct-form>
<mct-form
ng-model="configuration.plot.yAxis"
structure="yAxisForm"
name="yAxisFormState"
class="flex-elem l-flex-row no-margin">
</mct-form>
<div class="form">
<div class="section-header ng-binding ng-scope">
Plot Series
</div>
<ul class="first flex-elem grows vscroll">
<ul class="tree">
<li ng-repeat="child in children">
<span ng-controller="ToggleController as toggle">
<span ng-controller="TreeNodeController as treeNode">
<span class="tree-item menus-to-left">
<span
class='ui-symbol view-control flex-elem has-children'
ng-class="{ expanded: toggle.isActive() }"
ng-click="toggle.toggle(); treeNode.trackExpansion()">
</span>
<mct-representation
class="rep-object-label"
key="'label'"
mct-object="child">
</mct-representation>
</span>
</span>
<mct-form
ng-class="{hidden: !toggle.isActive()}"
ng-model="configuration.plot.series[$index]"
structure="plotSeriesForm"
name="plotOptionsState"
class="flex-elem l-flex-row">
</mct-form>
</span>
</li>
</ul>
</ul>
</div>
</div>

View File

@ -0,0 +1,165 @@
<!--
Open MCT, Copyright (c) 2014-2017, 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.
-->
<span ng-controller="PlotController as plot"
class="abs holder holder-plot has-control-bar">
<div class="l-control-bar" ng-show="!plot.hideExportButtons">
<span class="l-btn-set">
<a class="s-button t-export labeled icon-download"
ng-click="plot.exportPNG()"
title="Export This View's Data as PNG">
PNG
</a>
<a class="s-button t-export labeled"
ng-click="plot.exportJPG()"
title="Export This View's Data as JPG">
JPG
</a>
</span>
</div>
<div class="l-view-section">
<div class="gl-plot"
ng-style="{ height: 100 / plot.getSubPlots().length + '%'}"
ng-repeat="subplot in plot.getSubPlots()">
<div class="gl-plot-legend">
<span class='plot-legend-item'
ng-repeat="telemetryObject in subplot.getTelemetryObjects()"
ng-class="plot.getLegendClass(telemetryObject)">
<span class='plot-color-swatch'
ng-style="{ 'background-color': plot.getColor($index) }">
</span>
<span class='title-label'>{{telemetryObject.getModel().name}}</span>
</span>
</div>
<div class="gl-plot-axis-area gl-plot-y">
<div class="gl-plot-label gl-plot-y-label">
{{axes[1].active.name}}
</div>
<div ng-repeat="tick in subplot.getRangeTicks()"
class="gl-plot-tick gl-plot-y-tick-label"
ng-style="{ bottom: (100 * $index / (subplot.getRangeTicks().length - 1)) + '%' }">
{{tick.label | reverse}}
</div>
<div class="gl-plot-y-options gl-plot-local-controls"
ng-if="axes[1].options.length > 1">
<div class='form-control shell select'>
<select class="form-control input shell"
ng-model="axes[1].active"
ng-options="option.name for option in axes[1].options">
</select>
</div>
</div>
</div>
<div class="gl-plot-wrapper-display-area-and-x-axis">
<mct-include key="'time-of-interest'"
class="l-toi-holder show-val"
ng-if="toiPerc"
ng-class="{ 'pinned': toiPinned, 'val-to-left': toiPerc > 80 }"
ng-style="{'left': toiPerc + '%'}"></mct-include>
<div class="gl-plot-coords"
ng-if="subplot.isHovering() && subplot.getHoverCoordinates()">
{{subplot.getHoverCoordinates()}}
</div>
<div class="gl-plot-display-area"
ng-mouseenter="subplot.isHovering(true);"
ng-mouseleave="subplot.isHovering(false)"
ng-class="{ loading: plot.isRequestPending() }">
<!-- Out-of-bounds data indicators -->
<!-- ng-show is temporarily hard-coded in next element -->
<div ng-show="false" class="l-oob-data l-oob-data-up"></div>
<div ng-show="false" class="l-oob-data l-oob-data-dwn"></div>
<div class="gl-plot-hash hash-v"
ng-repeat="tick in subplot.getDomainTicks()"
ng-style="{ left: (100 * $index / (subplot.getDomainTicks().length - 1)) + '%', height: '100%' }"
ng-show="$index > 0 && $index < (subplot.getDomainTicks().length - 1)">
</div>
<div class="gl-plot-hash hash-h"
ng-repeat="tick in subplot.getRangeTicks()"
ng-style="{ bottom: (100 * $index / (subplot.getRangeTicks().length - 1)) + '%', width: '100%' }"
ng-show="$index > 0 && $index < (subplot.getRangeTicks().length - 1)">
</div>
<mct-chart draw="subplot.getDrawingObject()"
ng-if="subplot.getTelemetryObjects().length > 0"
ng-mousemove="subplot.hover($event)"
mct-drag="subplot.continueDrag($event)"
mct-drag-down="subplot.startDrag($event)"
mct-drag-up="subplot.endDrag($event); plot.update()">
</mct-chart>
<!-- TODO: Move into correct position; make part of group; infer from set of actions -->
<div class="l-local-controls gl-plot-local-controls t-plot-display-controls"
ng-if="$first">
<a class="s-button icon-arrow-left"
ng-click="plot.stepBackPanZoom()"
ng-show="plot.isZoomed()"
title="Restore previous pan/zoom">
</a>
<a class="s-button icon-arrows-out"
ng-click="plot.unzoom()"
ng-show="plot.isZoomed()"
title="Reset pan/zoom">
</a>
<div class="menu-element s-menu-button menus-to-left {{plot.getMode().cssClass}}"
ng-if="plot.getModeOptions().length > 1"
ng-controller="ClickAwayController as toggle">
<span class="l-click-area" ng-click="toggle.toggle()"></span>
<span>{{plot.getMode().name}}</span>
<div class="menu" ng-show="toggle.isActive()">
<ul>
<li ng-repeat="option in plot.getModeOptions()"
ng-click="plot.setMode(option); toggle.setState(false)"
class="{{option.cssClass}}">
{{option.name}}
</li>
</ul>
</div>
</div>
</div>
</div>
<div ng-if="$last" class="gl-plot-axis-area gl-plot-x">
<div ng-repeat="tick in subplot.getDomainTicks()"
class="gl-plot-tick gl-plot-x-tick-label"
ng-show="$index > 0 && $index < (subplot.getDomainTicks().length - 1)"
ng-style="{ left: (100 * $index / (subplot.getDomainTicks().length - 1)) + '%' }">
{{tick.label | reverse}}
</div>
<div class="gl-plot-label gl-plot-x-label">
{{axes[0].active.name}}
</div>
<div class="gl-plot-x-options gl-plot-local-controls"
ng-if="axes[0].options.length > 1">
<div class='form-control shell select'>
<select class="form-control input shell"
ng-model="axes[0].active"
ng-options="option.name for option in axes[0].options">
</select>
</div>
</div>
</div>
</div>
</div>
</div>
</span>

View File

@ -0,0 +1,117 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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 () {
/**
* Create a new chart which uses Canvas's 2D API for rendering.
*
* @memberof platform/features/plot
* @constructor
* @implements {platform/features/plot.Chart}
* @param {CanvasElement} canvas the canvas object to render upon
* @throws {Error} an error is thrown if Canvas's 2D API is unavailable.
*/
function Canvas2DChart(canvas) {
this.canvas = canvas;
this.c2d = canvas.getContext('2d');
this.width = canvas.width;
this.height = canvas.height;
this.dimensions = [this.width, this.height];
this.origin = [0, 0];
if (!this.c2d) {
throw new Error("Canvas 2d API unavailable.");
}
}
// Convert from logical to physical x coordinates
Canvas2DChart.prototype.x = function (v) {
return ((v - this.origin[0]) / this.dimensions[0]) * this.width;
};
// Convert from logical to physical y coordinates
Canvas2DChart.prototype.y = function (v) {
return this.height -
((v - this.origin[1]) / this.dimensions[1]) * this.height;
};
// Set the color to be used for drawing operations
Canvas2DChart.prototype.setColor = function (color) {
var mappedColor = color.map(function (c, i) {
return i < 3 ? Math.floor(c * 255) : (c);
}).join(',');
this.c2d.strokeStyle = "rgba(" + mappedColor + ")";
this.c2d.fillStyle = "rgba(" + mappedColor + ")";
};
Canvas2DChart.prototype.clear = function () {
var canvas = this.canvas;
this.width = canvas.width;
this.height = canvas.height;
this.c2d.clearRect(0, 0, this.width, this.height);
};
Canvas2DChart.prototype.setDimensions = function (newDimensions, newOrigin) {
this.dimensions = newDimensions;
this.origin = newOrigin;
};
Canvas2DChart.prototype.drawLine = function (buf, color, points) {
var i;
this.setColor(color);
// Configure context to draw two-pixel-thick lines
this.c2d.lineWidth = 2;
// Start a new path...
if (buf.length > 1) {
this.c2d.beginPath();
this.c2d.moveTo(this.x(buf[0]), this.y(buf[1]));
}
// ...and add points to it...
for (i = 2; i < points * 2; i = i + 2) {
this.c2d.lineTo(this.x(buf[i]), this.y(buf[i + 1]));
}
// ...before finally drawing it.
this.c2d.stroke();
};
Canvas2DChart.prototype.drawSquare = function (min, max, color) {
var x1 = this.x(min[0]),
y1 = this.y(min[1]),
w = this.x(max[0]) - x1,
h = this.y(max[1]) - y1;
this.setColor(color);
this.c2d.fillRect(x1, y1, w, h);
};
return Canvas2DChart;
}
);

View File

@ -0,0 +1,160 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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.
*****************************************************************************/
/**
* Module defining GLPlot. Created by vwoeltje on 11/12/14.
*/
define(
[],
function () {
// WebGL shader sources (for drawing plain colors)
var FRAGMENT_SHADER = [
"precision mediump float;",
"uniform vec4 uColor;",
"void main(void) {",
"gl_FragColor = uColor;",
"}"
].join('\n'),
VERTEX_SHADER = [
"attribute vec2 aVertexPosition;",
"uniform vec2 uDimensions;",
"uniform vec2 uOrigin;",
"void main(void) {",
"gl_Position = vec4(2.0 * ((aVertexPosition - uOrigin) / uDimensions) - vec2(1,1), 0, 1);",
"}"
].join('\n');
/**
* Create a new chart which uses WebGL for rendering.
*
* @memberof platform/features/plot
* @constructor
* @implements {platform/features/plot.Chart}
* @param {CanvasElement} canvas the canvas object to render upon
* @throws {Error} an error is thrown if WebGL is unavailable.
*/
function GLChart(canvas) {
var gl = canvas.getContext("webgl", { preserveDrawingBuffer: true }) ||
canvas.getContext("experimental-webgl", { preserveDrawingBuffer: true }),
vertexShader,
fragmentShader,
program,
aVertexPosition,
uColor,
uDimensions,
uOrigin;
// Ensure a context was actually available before proceeding
if (!gl) {
throw new Error("WebGL unavailable.");
}
// Initialize shaders
vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, VERTEX_SHADER);
gl.compileShader(vertexShader);
fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, FRAGMENT_SHADER);
gl.compileShader(fragmentShader);
// Assemble vertex/fragment shaders into programs
program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
// Get locations for attribs/uniforms from the
// shader programs (to pass values into shaders at draw-time)
aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
uColor = gl.getUniformLocation(program, "uColor");
uDimensions = gl.getUniformLocation(program, "uDimensions");
uOrigin = gl.getUniformLocation(program, "uOrigin");
gl.enableVertexAttribArray(aVertexPosition);
// Create a buffer to holds points which will be drawn
this.buffer = gl.createBuffer();
// Use a line width of 2.0 for legibility
gl.lineWidth(2.0);
// Enable blending, for smoothness
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
this.gl = gl;
this.aVertexPosition = aVertexPosition;
this.uColor = uColor;
this.uDimensions = uDimensions;
this.uOrigin = uOrigin;
}
// Utility function to handle drawing of a buffer;
// drawType will determine whether this is a box, line, etc.
GLChart.prototype.doDraw = function (drawType, buf, color, points) {
var gl = this.gl;
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
gl.bufferData(gl.ARRAY_BUFFER, buf, gl.DYNAMIC_DRAW);
gl.vertexAttribPointer(this.aVertexPosition, 2, gl.FLOAT, false, 0, 0);
gl.uniform4fv(this.uColor, color);
gl.drawArrays(drawType, 0, points);
};
GLChart.prototype.clear = function () {
var gl = this.gl;
// Set the viewport size; note that we use the width/height
// that our WebGL context reports, which may be lower
// resolution than the canvas we requested.
gl.viewport(
0,
0,
gl.drawingBufferWidth,
gl.drawingBufferHeight
);
gl.clear(gl.COLOR_BUFFER_BIT + gl.DEPTH_BUFFER_BIT);
};
GLChart.prototype.setDimensions = function (dimensions, origin) {
var gl = this.gl;
if (dimensions && dimensions.length > 0 &&
origin && origin.length > 0) {
gl.uniform2fv(this.uDimensions, dimensions);
gl.uniform2fv(this.uOrigin, origin);
}
};
GLChart.prototype.drawLine = function (buf, color, points) {
this.doDraw(this.gl.LINE_STRIP, buf, color, points);
};
GLChart.prototype.drawSquare = function (min, max, color) {
this.doDraw(this.gl.TRIANGLE_FAN, new Float32Array(
min.concat([min[0], max[1]]).concat(max).concat([max[0], min[1]])
), color, 4);
};
return GLChart;
}
);

View File

@ -0,0 +1,250 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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.
*****************************************************************************/
/**
* Module defining MCTChart. Created by vwoeltje on 11/12/14.
*/
define(
["./GLChart", "./Canvas2DChart"],
function (GLChart, Canvas2DChart) {
var TEMPLATE = "<canvas style='position: absolute; background: none; width: 100%; height: 100%;'></canvas>";
/**
* The mct-chart directive provides a canvas element which can be
* drawn upon, to support Plot view and similar visualizations.
*
* This directive takes one attribute, "draw", which is an Angular
* expression which will be two-way bound to a drawing object. This
* drawing object should contain:
*
* * `dimensions`: An object describing the logical bounds of the
* drawable area, containing two fields:
* * `origin`: The position, in logical coordinates, of the
* lower-left corner of the chart area. A two-element array.
* * `dimensions`: A two-element array containing the width
* and height of the chart area, in logical coordinates.
* * `lines`: An array of lines to be drawn, where each line is
* expressed as an object containing:
* * `buffer`: A Float32Array containing points in the line,
* in logical coordinate, in sequential x/y pairs.
* * `color`: The color of the line, as a four-element RGBA
* array, where each element is in the range of 0.0-1.0
* * `points`: The number of points in the line.
* * `boxes`: An array of rectangles to draw in the chart area
* (used for marquee zoom). Each is an object containing:
* * `start`: The first corner of the rectangle (as a two-element
* array, logical coordinates)
* * `end`: The opposite corner of the rectangle (again, as a
* two-element array)
* * `color`: The color of the box, as a four-element RGBA
* array, where each element is in the range of 0.0-1.0
*
* @memberof platform/features/plot
* @constructor
*/
function MCTChart($interval, $log) {
// Get an underlying chart implementation
function getChart(Charts, canvas) {
// Try the first available option...
var Chart = Charts[0];
// This function recursively try-catches all options;
// if these all fail, issue a warning.
if (!Chart) {
$log.warn("Cannot initialize mct-chart.");
return undefined;
}
// Try first option; if it fails, try remaining options
try {
return new Chart(canvas);
} catch (e) {
$log.warn([
"Could not instantiate chart",
Chart.name,
";",
e.message
].join(" "));
return getChart(Charts.slice(1), canvas);
}
}
function linkChart(scope, element) {
var canvas = element.find("canvas")[0],
activeInterval,
chart;
// Handle drawing, based on contents of the "draw" object
// in scope
function doDraw(draw) {
// Ensure canvas context has same resolution
// as canvas element
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
// Clear previous contents
chart.clear();
// Nothing to draw if no draw object defined
if (!draw) {
return;
}
// Set logical boundaries for the chart
chart.setDimensions(
draw.dimensions || [1, 1],
draw.origin || [0, 0]
);
// Draw line segments
(draw.lines || []).forEach(function (line) {
chart.drawLine(
line.buffer,
line.color,
line.points
);
});
// Draw boxes (e.g. marquee zoom rect)
(draw.boxes || []).forEach(function (box) {
chart.drawSquare(
box.start,
box.end,
box.color
);
});
}
// Issue a drawing call, if-and-only-if canvas size
// has changed. This will be called on a timer, since
// there is no event to depend on.
function drawIfResized() {
if (canvas.width !== canvas.offsetWidth ||
canvas.height !== canvas.offsetHeight) {
doDraw(scope.draw);
scope.$apply();
}
}
// Stop watching for changes to size (scope destroyed)
function releaseInterval() {
if (activeInterval) {
$interval.cancel(activeInterval);
}
}
// Switch from WebGL to plain 2D if context is lost
function fallbackFromWebGL() {
element.html(TEMPLATE);
canvas = element.find("canvas")[0];
chart = getChart([Canvas2DChart], canvas);
if (chart) {
doDraw(scope.draw);
}
}
// Try to initialize a chart.
chart = getChart([GLChart, Canvas2DChart], canvas);
// If that failed, there's nothing more we can do here.
// (A warning will already have been issued)
if (!chart) {
return;
}
// WebGL is a bit of a special case; it may work, then fail
// later for various reasons, so we need to listen for this
// and fall back to plain canvas drawing when it occurs.
canvas.addEventListener("webglcontextlost", fallbackFromWebGL);
// Check for resize, on a timer
activeInterval = $interval(drawIfResized, 1000, 0, false);
// Watch "draw" for external changes to the set of
// things to be drawn.
scope.$watchCollection("draw", doDraw);
// Stop checking for resize when scope is destroyed
scope.$on("$destroy", releaseInterval);
}
return {
// Apply directive only to elements
restrict: "E",
// Template to use (a canvas element)
template: TEMPLATE,
// Link function; set up scope
link: linkChart,
// Initial, isolate scope for the directive
scope: { draw: "=" }
};
}
/**
* @interface platform/features/plot.Chart
* @private
*/
/**
* Clear the chart.
* @method platform/features/plot.Chart#clear
*/
/**
* Set the logical boundaries of the chart.
* @param {number[]} dimensions the horizontal and
* vertical dimensions of the chart
* @param {number[]} origin the horizontal/vertical
* origin of the chart
* @memberof platform/features/plot.Chart#setDimensions
*/
/**
* Draw the supplied buffer as a line strip (a sequence
* of line segments), in the chosen color.
* @param {Float32Array} buf the line strip to draw,
* in alternating x/y positions
* @param {number[]} color the color to use when drawing
* the line, as an RGBA color where each element
* is in the range of 0.0-1.0
* @param {number} points the number of points to draw
* @memberof platform/features/plot.Chart#drawLine
*/
/**
* Draw a rectangle extending from one corner to another,
* in the chosen color.
* @param {number[]} min the first corner of the rectangle
* @param {number[]} max the opposite corner
* @param {number[]} color the color to use when drawing
* the rectangle, as an RGBA color where each element
* is in the range of 0.0-1.0
* @memberof platform/features/plot.Chart#drawSquare
*/
return MCTChart;
}
);

View File

@ -0,0 +1,437 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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.
*****************************************************************************/
/**
* This bundle adds a "Plot" view for numeric telemetry data.
* @namespace platform/features/plot
*/
define(
[
"./elements/PlotUpdater",
"./elements/PlotPalette",
"./elements/PlotAxis",
"./elements/PlotLimitTracker",
"./elements/PlotTelemetryFormatter",
"./modes/PlotModeOptions",
"./SubPlotFactory"
],
function (
PlotUpdater,
PlotPalette,
PlotAxis,
PlotLimitTracker,
PlotTelemetryFormatter,
PlotModeOptions,
SubPlotFactory
) {
var AXIS_DEFAULTS = [
{ "name": "Time" },
{ "name": "Value" }
];
/**
* The PlotController is responsible for any computation/logic
* associated with displaying the plot view. Specifically, these
* responsibilities include:
*
* * Describing axes and labeling.
* * Handling user interactions.
* * Deciding what needs to be drawn in the chart area.
*
* @memberof platform/features/plot
* @constructor
*/
function PlotController(
$scope,
$element,
exportImageService,
telemetryFormatter,
telemetryHandler,
throttle,
PLOT_FIXED_DURATION,
openmct
) {
var self = this,
plotTelemetryFormatter =
new PlotTelemetryFormatter(telemetryFormatter),
subPlotFactory =
new SubPlotFactory(plotTelemetryFormatter),
cachedObjects = [],
updater,
lastBounds,
lastRange,
lastDomain,
handle;
var timeAPI = openmct.time;
// Populate the scope with axis information (specifically, options
// available for each axis.)
function setupAxes(metadatas) {
$scope.axes.forEach(function (axis) {
axis.updateMetadata(metadatas);
});
}
// Trigger an update of a specific subplot;
// used in a loop to update all subplots.
function updateSubplot(subplot) {
subplot.update();
}
// Set up available modes (stacked/overlaid), based on the
// set of telemetry objects in this plot view.
function setupModes(telemetryObjects) {
if (cachedObjects !== telemetryObjects) {
cachedObjects = telemetryObjects;
self.modeOptions = new PlotModeOptions(
telemetryObjects || [],
subPlotFactory
);
}
}
// Change the displayable bounds
function setBasePanZoom(bounds) {
var start = bounds.start,
end = bounds.end;
if (updater) {
updater.setDomainBounds(start, end);
self.update();
}
lastBounds = bounds;
}
// Reinstantiate the plot updater (e.g. because we have a
// new subscription.) This will clear the plot.
function recreateUpdater() {
var domain = $scope.axes[0].active.key,
range = $scope.axes[1].active.key,
duration = PLOT_FIXED_DURATION;
updater = new PlotUpdater(handle, domain, range, duration);
lastDomain = domain;
lastRange = range;
self.limitTracker = new PlotLimitTracker(handle, range);
// Keep any externally-provided bounds
if (lastBounds) {
setBasePanZoom(lastBounds);
}
}
function getUpdater() {
if (!updater) {
recreateUpdater();
}
return updater;
}
// Handle new telemetry data in this plot
function updateValues() {
self.pending = false;
if (handle) {
setupModes(handle.getTelemetryObjects());
setupAxes(handle.getMetadata());
getUpdater().update();
self.modeOptions.getModeHandler().plotTelemetry(updater);
self.limitTracker.update();
self.update();
}
}
// Display new historical data as it becomes available
function addHistoricalData(domainObject, series) {
self.pending = false;
getUpdater().addHistorical(domainObject, series);
self.modeOptions.getModeHandler().plotTelemetry(updater);
self.update();
}
// Issue a new request for historical telemetry
function requestTelemetry() {
if (handle) {
handle.request({}, addHistoricalData);
}
}
// Requery for data entirely
function replot() {
if (handle) {
updater = undefined;
requestTelemetry();
}
}
function changeTimeOfInterest(timeOfInterest) {
if (timeOfInterest !== undefined) {
var bounds = timeAPI.bounds();
var range = bounds.end - bounds.start;
$scope.toiPerc = ((timeOfInterest - bounds.start) / range) * 100;
$scope.toiPinned = true;
} else {
$scope.toiPerc = undefined;
$scope.toiPinned = false;
}
}
// Create a new subscription; telemetrySubscriber gets
// to do the meaningful work here.
function subscribe(domainObject) {
if (handle) {
handle.unsubscribe();
}
handle = domainObject && telemetryHandler.handle(
domainObject,
updateValues,
true // Lossless
);
replot();
changeTimeOfInterest(timeAPI.timeOfInterest());
timeAPI.on("timeOfInterest", changeTimeOfInterest);
}
// Release the current subscription (called when scope is destroyed)
function releaseSubscription() {
if (handle) {
handle.unsubscribe();
handle = undefined;
}
timeAPI.off("timeOfInterest", changeTimeOfInterest);
}
function requery() {
self.pending = true;
releaseSubscription();
subscribe($scope.domainObject);
}
function updateDomainFormat() {
var domainAxis = $scope.axes[0];
plotTelemetryFormatter
.setDomainFormat(domainAxis.active.format);
}
function domainRequery(newDomain) {
if (newDomain !== lastDomain) {
updateDomainFormat();
requery();
}
}
function rangeRequery(newRange) {
if (newRange !== lastRange) {
requery();
}
}
// Respond to a display bounds change (requery for data)
function changeDisplayBounds(event, bounds, follow) {
//'hack' for follow mode
if (follow === true) {
setBasePanZoom(bounds);
} else {
var domainAxis = $scope.axes[0];
if (bounds.domain) {
domainAxis.chooseOption(bounds.domain);
}
updateDomainFormat();
setBasePanZoom(bounds);
requery();
}
self.setUnsynchedStatus($scope.domainObject, follow && self.isZoomed());
changeTimeOfInterest(timeAPI.timeOfInterest());
}
this.modeOptions = new PlotModeOptions([], subPlotFactory);
this.updateValues = updateValues;
// Create a throttled update function
this.scheduleUpdate = throttle(function () {
self.modeOptions.getModeHandler().getSubPlots()
.forEach(updateSubplot);
});
self.pending = true;
self.$element = $element;
self.exportImageService = exportImageService;
// Initialize axes; will get repopulated when telemetry
// metadata becomes available.
$scope.axes = [
new PlotAxis("domains", [], AXIS_DEFAULTS[0]),
new PlotAxis("ranges", [], AXIS_DEFAULTS[1])
];
//Are some initialized bounds defined?
var bounds = timeAPI.bounds();
if (bounds &&
bounds.start !== undefined &&
bounds.end !== undefined) {
changeDisplayBounds(undefined, timeAPI.bounds(), timeAPI.clock() !== undefined);
}
// Watch for changes to the selected axis
$scope.$watch("axes[0].active.key", domainRequery);
$scope.$watch("axes[1].active.key", rangeRequery);
// Subscribe to telemetry when a domain object becomes available
$scope.$watch('domainObject', subscribe);
// Respond to external bounds changes
$scope.$on("telemetry:display:bounds", changeDisplayBounds);
// Unsubscribe when the plot is destroyed
$scope.$on("$destroy", releaseSubscription);
}
/**
* Get the color (as a style-friendly string) to use
* for plotting the trace at the specified index.
* @param {number} index the index of the trace
* @returns {string} the color, in #RRGGBB form
*/
PlotController.prototype.getColor = function (index) {
return PlotPalette.getStringColor(index);
};
/**
* Check if the plot is zoomed or panned out
* of its default state (to determine whether back/unzoom
* controls should be shown)
* @returns {boolean} true if not in default state
*/
PlotController.prototype.isZoomed = function () {
return this.modeOptions.getModeHandler().isZoomed();
};
/**
* Undo the most recent pan/zoom change and restore
* the prior state.
*/
PlotController.prototype.stepBackPanZoom = function () {
return this.modeOptions.getModeHandler().stepBackPanZoom();
};
/**
* Undo all pan/zoom changes and restore the initial state.
*/
PlotController.prototype.unzoom = function () {
return this.modeOptions.getModeHandler().unzoom();
};
/**
* Get the mode options (Stacked/Overlaid) that are applicable
* for this plot.
*/
PlotController.prototype.getModeOptions = function () {
return this.modeOptions.getModeOptions();
};
/**
* Get the current mode that is applicable to this plot. This
* will include key, name, and cssClass fields.
*/
PlotController.prototype.getMode = function () {
return this.modeOptions.getMode();
};
/**
* Set the mode which should be active in this plot.
* @param mode one of the mode options returned from
* getModeOptions()
*/
PlotController.prototype.setMode = function (mode) {
this.modeOptions.setMode(mode);
this.updateValues();
};
/**
* Get all individual plots contained within this Plot view.
* (Multiple may be contained when in Stacked mode).
* @returns {SubPlot[]} all subplots in this Plot view
*/
PlotController.prototype.getSubPlots = function () {
return this.modeOptions.getModeHandler().getSubPlots();
};
/**
* Get the CSS class to apply to the legend for this domain
* object; this will reflect limit state.
* @returns {string} the CSS class
*/
PlotController.prototype.getLegendClass = function (telemetryObject) {
return this.limitTracker &&
this.limitTracker.getLegendClass(telemetryObject);
};
/**
* Explicitly update all plots.
*/
PlotController.prototype.update = function () {
this.scheduleUpdate();
};
/**
* Check if a request is pending (to show the wait spinner)
*/
PlotController.prototype.isRequestPending = function () {
// Placeholder; this should reflect request state
// when requesting historical telemetry
return this.pending;
};
PlotController.prototype.setUnsynchedStatus = function (domainObject, status) {
if (domainObject.hasCapability('status')) {
domainObject.getCapability('status').set('timeconductor-unsynced', status);
}
};
/**
* Export the plot to PNG
*/
PlotController.prototype.exportPNG = function () {
var self = this;
self.hideExportButtons = true;
self.exportImageService.exportPNG(self.$element[0], "plot.png").finally(function () {
self.hideExportButtons = false;
});
};
/**
* Export the plot to JPG
*/
PlotController.prototype.exportJPG = function () {
var self = this;
self.hideExportButtons = true;
self.exportImageService.exportJPG(self.$element[0], "plot.jpg").finally(function () {
self.hideExportButtons = false;
});
};
return PlotController;
}
);

View File

@ -0,0 +1,195 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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(
['./PlotOptionsForm'],
function (PlotOptionsForm) {
/**
* Notes on implementation of plot options
*
* Multiple y-axes will have to be handled with multiple forms as
* they will need to be stored on distinct model object
*
* Likewise plot series options per-child will need to be separate
* forms.
*/
/**
* The LayoutController is responsible for supporting the
* Layout view. It arranges frames according to saved configuration
* and provides methods for updating these based on mouse
* movement.
* @memberof platform/features/plot
* @constructor
* @param {Scope} $scope the controller's Angular scope
*/
function PlotOptionsController($scope) {
var self = this;
this.$scope = $scope;
this.domainObject = $scope.domainObject;
this.configuration = this.domainObject.getModel().configuration || {};
this.plotOptionsForm = new PlotOptionsForm();
this.composition = [];
this.watches = [];
/*
Listen for changes to the domain object and update the object's
children.
*/
this.mutationListener = this.domainObject.getCapability('mutation').listen(function (model) {
if (self.hasCompositionChanged(self.composition, model.composition)) {
self.updateChildren();
}
});
/*
Set form structures on scope
*/
$scope.plotSeriesForm = this.plotOptionsForm.plotSeriesForm;
$scope.xAxisForm = this.plotOptionsForm.xAxisForm;
$scope.yAxisForm = this.plotOptionsForm.yAxisForm;
$scope.$on("$destroy", function () {
//Clean up any listeners on destruction of controller
self.mutationListener();
});
this.defaultConfiguration();
this.updateChildren();
/*
* Setup a number of watches for changes to form values. On
* change, update the model configuration via mutation
*/
$scope.$watchCollection('configuration.plot.yAxis', function (newValue, oldValue) {
self.updateConfiguration(newValue, oldValue);
});
$scope.$watchCollection('configuration.plot.xAxis', function (newValue, oldValue) {
self.updateConfiguration(newValue, oldValue);
});
this.watchSeries();
}
/**
* Unregister all watches for series data (ie. the configuration for
* child objects)
* @private
*/
PlotOptionsController.prototype.clearSeriesWatches = function () {
this.watches.forEach(function (watch) {
watch();
});
this.watches = [];
};
/**
* Attach watches for each object in the plot's composition
* @private
*/
PlotOptionsController.prototype.watchSeries = function () {
var self = this;
this.clearSeriesWatches();
(self.$scope.children || []).forEach(function (child, index) {
self.watches.push(
self.$scope.$watchCollection(
'configuration.plot.series[' + index + ']',
function (newValue, oldValue) {
self.updateConfiguration(newValue, oldValue);
}
)
);
});
};
/**
* Determine whether the changes to the model that triggered a
* mutation event were purely compositional.
*
* @private
*/
PlotOptionsController.prototype.hasCompositionChanged = function (oldComposition, newComposition) {
// Framed slightly strangely, but the boolean logic is
// easier to follow for the unchanged case.
var isUnchanged = oldComposition === newComposition ||
(
oldComposition.length === newComposition.length &&
oldComposition.every(function (currentValue, index) {
return newComposition[index] && currentValue === newComposition[index];
})
);
return !isUnchanged;
};
/**
* Default the plot options model
*
* @private
*/
PlotOptionsController.prototype.defaultConfiguration = function () {
this.configuration.plot = this.configuration.plot || {};
this.configuration.plot.xAxis = this.configuration.plot.xAxis || {};
this.configuration.plot.yAxis = this.configuration.plot.yAxis || {}; // y-axes will be associative array keyed on axis key
this.configuration.plot.series = this.configuration.plot.series || []; // series will be associative array keyed on sub-object id
this.$scope.configuration = this.configuration;
};
/**
* When a child is added to, or removed from a plot, update the
* plot options model
* @private
*/
PlotOptionsController.prototype.updateChildren = function () {
var self = this;
this.domainObject.useCapability('composition').then(function (children) {
self.$scope.children = children;
self.composition = self.domainObject.getModel().composition;
children.forEach(function (child, index) {
self.configuration.plot.series[index] =
self.configuration.plot.series[index] || {'id': child.getId()};
});
self.watchSeries();
});
};
/**
* On changes to the form, update the configuration on the domain
* object
* @private
*/
PlotOptionsController.prototype.updateConfiguration = function () {
var self = this;
this.domainObject.useCapability('mutation', function (model) {
model.configuration = model.configuration || {};
model.configuration.plot = self.configuration.plot;
});
};
return PlotOptionsController;
}
);

View File

@ -0,0 +1,150 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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 () {
/**
* A class for encapsulating structure and behaviour of the plot
* options form
* @memberOf platform/features/plot
* @param topic
* @constructor
*/
function PlotOptionsForm() {
/*
Defined below are the form structures for the plot options.
*/
this.xAxisForm = {
'name': 'x-axis',
'sections': [{
'name': 'x-axis',
'rows': [
{
'name': 'Domain',
'control': 'select',
'key': 'key',
'options': [
{'name': 'SCET', 'value': 'scet'},
{'name': 'SCLK', 'value': 'sclk'},
{'name': 'LST', 'value': 'lst'}
]
}
]
}]};
this.yAxisForm = {
'name': 'y-axis',
'sections': [{
// Will need to be repeated for each y-axis, with a
// distinct name for each. Ideally the name of the axis
// itself.
'name': 'y-axis',
'rows': [
{
'name': 'Range',
'control': 'select',
'key': 'key',
'options': [
{'name': 'EU', 'value': 'eu'},
{'name': 'DN', 'value': 'dn'},
{'name': 'Status', 'value': 'status'}
]
},
{
'name': 'Autoscale',
'control': 'checkbox',
'key': 'autoscale'
},
{
'name': 'Min',
'control': 'textfield',
'key': 'min',
'pattern': '[0-9]',
'inputsize' : 'sm'
},
{
'name': 'Max',
'control': 'textfield',
'key': 'max',
'pattern': '[0-9]',
'inputsize' : 'sm'
}
]
}]
};
this.plotSeriesForm = {
'name': 'Series Options',
'sections': [
{
rows: [
{
'name': 'Color',
'control': 'color',
'key': 'color'
}]
},
{
'rows': [
{
'name': 'Markers',
'control': 'checkbox',
'key': 'markers',
'layout': 'control-first'
}
]
},
{
'rows': [
{
'name': 'No Line',
'control': 'radio',
'key': 'lineType',
'value': 'noLine',
'layout': 'control-first'
},
{
'name': 'Step Line',
'control': 'radio',
'key': 'lineType',
'value': 'stepLine',
'layout': 'control-first'
},
{
'name': 'Linear Line',
'control': 'radio',
'key': 'lineType',
'value': 'linearLine',
'layout': 'control-first'
}
]
}
]
};
}
return PlotOptionsForm;
}
);

View File

@ -0,0 +1,415 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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(
[
'./elements/PlotPosition',
'./elements/PlotTickGenerator'
],
function (PlotPosition, PlotTickGenerator) {
var DOMAIN_TICKS = 5,
RANGE_TICKS = 7;
/**
* A SubPlot is an individual plot within a Plot View (which
* may contain multiple plots, specifically when in Stacked
* plot mode.)
* @memberof platform/features/plot
* @constructor
* @param {DomainObject[]} telemetryObjects the domain objects
* which will be plotted in this sub-plot
* @param {PlotPanZoomStack} panZoomStack the stack of pan-zoom
* states which is applicable to this sub-plot
* @param {TelemetryFormatter} telemetryFormatter the telemetry
* formatting service; used to convert domain/range values
* from telemetry data sets to a human-readable form.
*/
function SubPlot(telemetryObjects, panZoomStack, telemetryFormatter) {
// We are used from a template often, so maintain
// state in local variables to allow for fast look-up,
// as is normal for controllers.
this.telemetryObjects = telemetryObjects;
this.domainTicks = [];
this.rangeTicks = [];
this.formatter = telemetryFormatter;
this.draw = {};
this.hovering = false;
this.panZoomStack = panZoomStack;
// Start with the right initial drawing bounds,
// tick marks
this.updateDrawingBounds();
this.updateTicks();
}
/**
* Tests whether this subplot has domain data to show for the current pan/zoom level. Absence of domain data
* implies that there is no range data displayed either
* @returns {boolean} true if domain data exists for the current pan/zoom level
*/
SubPlot.prototype.hasDomainData = function () {
return this.panZoomStack &&
this.panZoomStack.getDimensions()[0] > 0;
};
// Utility function for filtering out empty strings.
function isNonEmpty(v) {
return typeof v === 'string' && v !== "";
}
// Converts from pixel coordinates to domain-range,
// to interpret mouse gestures.
SubPlot.prototype.mousePositionToDomainRange = function (mousePosition) {
return new PlotPosition(
mousePosition.x,
mousePosition.y,
mousePosition.width,
mousePosition.height,
this.panZoomStack
).getPosition();
};
// Utility function to get the mouse position (in x,y
// pixel coordinates in the canvas area) from a mouse
// event object.
SubPlot.prototype.toMousePosition = function ($event) {
var bounds = this.subPlotBounds;
return {
x: $event.clientX - bounds.left,
y: $event.clientY - bounds.top,
width: bounds.width,
height: bounds.height
};
};
// Convert a domain-range position to a displayable
// position. This will subtract the domain offset, which
// is used to bias domain values to minimize loss-of-precision
// associated with conversion to a 32-bit floating point
// format (which is needed in the chart area itself, by WebGL.)
SubPlot.prototype.toDisplayable = function (position) {
return [position[0] - this.domainOffset, position[1]];
};
// Update the current hover coordinates
SubPlot.prototype.updateHoverCoordinates = function () {
var formatter = this.formatter;
// Utility, for map/forEach loops. Index 0 is domain,
// index 1 is range.
function formatValue(v, i) {
return i ?
formatter.formatRangeValue(v) :
formatter.formatDomainValue(v);
}
this.hoverCoordinates = this.mousePosition &&
this.mousePositionToDomainRange(this.mousePosition)
.map(formatValue)
.filter(isNonEmpty)
.join(", ");
};
// Update the drawable marquee area to reflect current
// mouse position (or don't show it at all, if no marquee
// zoom is in progress)
SubPlot.prototype.updateMarqueeBox = function () {
// Express this as a box in the draw object, which
// is passed to an mct-chart in the template for rendering.
this.draw.boxes = this.marqueeStart ?
[{
start: this.toDisplayable(
this.mousePositionToDomainRange(this.marqueeStart)
),
end: this.toDisplayable(
this.mousePositionToDomainRange(this.mousePosition)
),
color: [1, 1, 1, 0.5]
}] : undefined;
};
// Update the bounds (origin and dimensions) of the drawing area.
SubPlot.prototype.updateDrawingBounds = function () {
var panZoom = this.panZoomStack.getPanZoom();
// Communicate pan-zoom state from stack to the draw object
// which is passed to mct-chart in the template.
this.draw.dimensions = panZoom.dimensions;
this.draw.origin = [
panZoom.origin[0] - this.domainOffset,
panZoom.origin[1]
];
};
// Update tick marks in scope.
SubPlot.prototype.updateTicks = function () {
var tickGenerator =
new PlotTickGenerator(this.panZoomStack, this.formatter);
this.domainTicks =
tickGenerator.generateDomainTicks(DOMAIN_TICKS);
this.rangeTicks =
tickGenerator.generateRangeTicks(RANGE_TICKS);
};
SubPlot.prototype.updatePan = function () {
var start, current, delta, nextOrigin;
// Clear the previous panning pan-zoom state
this.panZoomStack.popPanZoom();
// Calculate what the new resulting pan-zoom should be
start = this.mousePositionToDomainRange(
this.panStart,
this.panZoomStack
);
current = this.mousePositionToDomainRange(
this.mousePosition,
this.panZoomStack
);
delta = [current[0] - start[0], current[1] - start[1]];
nextOrigin = [
this.panStartBounds.origin[0] - delta[0],
this.panStartBounds.origin[1] - delta[1]
];
// ...and push a new one at the current mouse position
this.panZoomStack
.pushPanZoom(nextOrigin, this.panStartBounds.dimensions);
};
/**
* Get the set of domain objects which are being
* represented in this sub-plot.
* @returns {DomainObject[]} the domain objects which
* will have data plotted in this sub-plot
*/
SubPlot.prototype.getTelemetryObjects = function () {
return this.telemetryObjects;
};
/**
* Get ticks mark information appropriate for using in the
* template for this sub-plot's domain axis, as prepared
* by the PlotTickGenerator.
* @returns {Array} tick marks for the domain axis
*/
SubPlot.prototype.getDomainTicks = function () {
return this.domainTicks;
};
/**
* Get ticks mark information appropriate for using in the
* template for this sub-plot's range axis, as prepared
* by the PlotTickGenerator.
* @returns {Array} tick marks for the range axis
*/
SubPlot.prototype.getRangeTicks = function () {
return this.rangeTicks;
};
/**
* Get the drawing object associated with this sub-plot;
* this object will be passed to the mct-chart in which
* this sub-plot's lines will be plotted, as its "draw"
* attribute, and should have the same internal format
* expected by that directive.
* @return {object} the drawing object
*/
SubPlot.prototype.getDrawingObject = function () {
return this.draw;
};
/**
* Get the coordinates (as displayable text) for the
* current mouse position.
* @returns {string[]} the displayable domain and range
* coordinates over which the mouse is hovered
*/
SubPlot.prototype.getHoverCoordinates = function () {
return this.hoverCoordinates;
};
/**
* Handle mouse movement over the chart area.
* @param $event the mouse event
* @memberof platform/features/plot.SubPlot#
*/
SubPlot.prototype.hover = function ($event) {
this.hovering = true;
this.subPlotBounds = $event.target.getBoundingClientRect();
this.mousePosition = this.toMousePosition($event);
//If there is a domain to display, show hover coordinates, otherwise hover coordinates are meaningless
if (this.hasDomainData()) {
this.updateHoverCoordinates();
}
if (this.marqueeStart) {
this.updateMarqueeBox();
}
if (this.panStart) {
this.updatePan();
this.updateDrawingBounds();
this.updateTicks();
}
};
/**
* Continue a previously-start pan or zoom gesture.
* @param $event the mouse event
* @memberof platform/features/plot.SubPlot#
*/
SubPlot.prototype.continueDrag = function ($event) {
this.mousePosition = this.toMousePosition($event);
if (this.marqueeStart) {
this.updateMarqueeBox();
}
if (this.panStart) {
this.updatePan();
this.updateDrawingBounds();
this.updateTicks();
}
};
/**
* Initiate a marquee zoom action.
* @param $event the mouse event
*/
SubPlot.prototype.startDrag = function ($event) {
this.subPlotBounds = $event.target.getBoundingClientRect();
this.mousePosition = this.toMousePosition($event);
// Treat any modifier key as a pan
if ($event.altKey || $event.shiftKey || $event.ctrlKey) {
// Start panning
this.panStart = this.mousePosition;
this.panStartBounds = this.panZoomStack.getPanZoom();
// We're starting a pan, so add this back as a
// state on the stack; it will get replaced
// during the pan.
this.panZoomStack.pushPanZoom(
this.panStartBounds.origin,
this.panStartBounds.dimensions
);
$event.preventDefault();
} else {
// Start marquee zooming
this.marqueeStart = this.mousePosition;
this.updateMarqueeBox();
}
};
/**
* Complete a marquee zoom action.
* @param $event the mouse event
*/
SubPlot.prototype.endDrag = function ($event) {
var self = this;
// Perform a marquee zoom.
function marqueeZoom(start, end) {
// Determine what boundary is described by the marquee,
// in domain-range values. Use the minima for origin, so that
// it doesn't matter what direction the user marqueed in.
var a = self.mousePositionToDomainRange(start),
b = self.mousePositionToDomainRange(end),
origin = [
Math.min(a[0], b[0]),
Math.min(a[1], b[1])
],
dimensions = [
Math.max(a[0], b[0]) - origin[0],
Math.max(a[1], b[1]) - origin[1]
];
// Proceed with zoom if zoom dimensions are non zeros
if (!(dimensions[0] === 0 && dimensions[1] === 0)) {
// Push the new state onto the pan-zoom stack
self.panZoomStack.pushPanZoom(origin, dimensions);
// Make sure tick marks reflect new bounds
self.updateTicks();
}
}
this.mousePosition = this.toMousePosition($event);
this.subPlotBounds = undefined;
if (this.marqueeStart) {
marqueeZoom(this.marqueeStart, this.mousePosition);
this.marqueeStart = undefined;
this.updateMarqueeBox();
this.updateDrawingBounds();
this.updateTicks();
}
if (this.panStart) {
// End panning
this.panStart = undefined;
this.panStartBounds = undefined;
}
};
/**
* Update the drawing bounds, marquee box, and
* tick marks for this subplot.
*/
SubPlot.prototype.update = function () {
this.updateDrawingBounds();
this.updateMarqueeBox();
this.updateTicks();
};
/**
* Set the domain offset associated with this sub-plot.
* A domain offset is subtracted from all domain
* before lines are drawn to avoid artifacts associated
* with the use of 32-bit floats when domain values
* are often timestamps (due to insufficient precision.)
* A SubPlot will be drawing boxes (for marquee zoom) in
* the same offset coordinate space, so it needs to know
* the value of this to position that marquee box
* correctly.
* @param {number} value the domain offset
*/
SubPlot.prototype.setDomainOffset = function (value) {
this.domainOffset = value;
};
/**
* When used with no argument, check whether or not the user
* is currently hovering over this subplot. When used with
* an argument, set that state.
* @param {boolean} [state] the new hovering state
* @returns {boolean} the hovering state
*/
SubPlot.prototype.isHovering = function (state) {
if (state !== undefined) {
this.hovering = state;
}
return this.hovering;
};
return SubPlot;
}
);

View File

@ -21,40 +21,39 @@
*****************************************************************************/
define(
[],
function () {
["./SubPlot"],
function (SubPlot) {
/**
* The mct-selectable directive allows selection functionality
* (click) to be attached to specific elements.
*
* @memberof platform/commonUI/general
* Utility factory; wraps the SubPlot constructor and adds
* in a reference to the telemetryFormatter, which will be
* used to represent telemetry values (timestamps or data
* values) as human-readable strings.
* @memberof platform/features/plot
* @constructor
*/
function MCTSelectable(openmct) {
// Link; install event handlers.
function link(scope, element, attrs) {
var removeSelectable = openmct.selection.selectable(
element[0],
scope.$eval(attrs.mctSelectable),
attrs.hasOwnProperty('mctInitSelect') && scope.$eval(attrs.mctInitSelect) !== false
);
scope.$on("$destroy", function () {
removeSelectable();
});
}
return {
// mct-selectable only makes sense as an attribute
restrict: "A",
// Link function, to install event handlers
link: link
};
function SubPlotFactory(telemetryFormatter) {
this.telemetryFormatter = telemetryFormatter;
}
return MCTSelectable;
/**
* Instantiate a new sub-plot.
* @param {DomainObject[]} telemetryObjects the domain objects
* which will be plotted in this sub-plot
* @param {PlotPanZoomStack} panZoomStack the stack of pan-zoom
* states which is applicable to this sub-plot
* @returns {SubPlot} the instantiated sub-plot
* @method
*/
SubPlotFactory.prototype.createSubPlot = function (telemetryObjects, panZoomStack) {
return new SubPlot(
telemetryObjects,
panZoomStack,
this.telemetryFormatter
);
};
return SubPlotFactory;
}
);

View File

@ -0,0 +1,134 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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 () {
/**
* A PlotAxis provides a template-ready set of options
* for the domain or range axis, sufficient to populate
* selectors.
*
* @memberof platform/features/plot
* @constructor
* @param {string} axisType the field in metadatas to
* look at for axis options; usually one of
* "domains" or "ranges"
* @param {object[]} metadatas metadata objects, as
* returned by the `getMetadata()` method of
* a `telemetry` capability.
* @param {object} defaultValue the value to use for the
* active state in the event that no options are
* found; should contain "name" and "key" at
* minimum.
*
*/
function PlotAxis(axisType, metadatas, defaultValue) {
this.axisType = axisType;
this.defaultValue = defaultValue;
this.optionKeys = {};
/**
* The currently chosen option for this axis. An
* initial value is provided; this will be updated
* directly form the plot template.
* @memberof platform/features/plot.PlotAxis#
*/
this.active = defaultValue;
/**
* The set of options applicable for this axis;
* an array of objects, where each object contains a
* "key" field and a "name" field (for machine- and
* human-readable names respectively)
* @memberof platform/features/plot.PlotAxis#
*/
this.options = [];
// Initialize options from metadata objects
this.updateMetadata(metadatas);
}
/**
* Update axis options to reflect current metadata.
* @param {TelemetryMetadata[]} metadata objects describing
* applicable telemetry
*/
PlotAxis.prototype.updateMetadata = function (metadatas) {
var axisType = this.axisType,
optionKeys = this.optionKeys,
newOptions = {},
toAdd = [];
function isValid(option) {
return option && optionKeys[option.key];
}
metadatas.forEach(function (m) {
(m[axisType] || []).forEach(function (option) {
var key = option.key;
if (!optionKeys[key] && !newOptions[key]) {
toAdd.push(option);
}
newOptions[key] = true;
});
});
optionKeys = this.optionKeys = newOptions;
// General approach here is to avoid changing object
// instances unless something has really changed, since
// Angular is watching; don't want to trigger extra digests.
if (!this.options.every(isValid)) {
this.options = this.options.filter(isValid);
}
if (toAdd.length > 0) {
this.options = this.options.concat(toAdd);
}
if (!isValid(this.active)) {
this.active = this.options[0] || this.defaultValue;
}
};
/**
* Change the domain/range selection for this axis. If the
* provided `key` is not recognized as an option, no change
* will occur.
* @param {string} key the identifier for the domain/range
*/
PlotAxis.prototype.chooseOption = function (key) {
var self = this;
this.options.forEach(function (option) {
if (option.key === key) {
self.active = option;
}
});
};
return PlotAxis;
}
);

View File

@ -0,0 +1,78 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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 () {
/**
* Tracks the limit state of telemetry objects being plotted.
* @memberof platform/features/plot
* @constructor
* @param {platform/telemetry.TelemetryHandle} handle the handle
* to telemetry access
* @param {string} range the key to use when looking up range values
*/
function PlotLimitTracker(handle, range) {
this.handle = handle;
this.range = range;
this.legendClasses = {};
}
/**
* Update limit states to reflect the latest data.
*/
PlotLimitTracker.prototype.update = function () {
var legendClasses = {},
range = this.range,
handle = this.handle;
function updateLimit(telemetryObject) {
var limit = telemetryObject.getCapability('limit'),
datum = handle.getDatum(telemetryObject);
if (limit && datum) {
legendClasses[telemetryObject.getId()] =
(limit.evaluate(datum, range) || {}).cssClass;
}
}
handle.getTelemetryObjects().forEach(updateLimit);
this.legendClasses = legendClasses;
};
/**
* Get the CSS class associated with any limit violations for this
* telemetry object.
* @param {DomainObject} domainObject the telemetry object to check
* @returns {string} the CSS class name, if any
*/
PlotLimitTracker.prototype.getLegendClass = function (domainObject) {
var id = domainObject && domainObject.getId();
return id && this.legendClasses[id];
};
return PlotLimitTracker;
}
);

View File

@ -0,0 +1,118 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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(
['./PlotSeriesWindow'],
function (PlotSeriesWindow) {
/**
* Represents a single line or trace of a plot.
* @param {{PlotLineBuffer}} buffer the plot buffer
* @constructor
*/
function PlotLine(buffer) {
this.buffer = buffer;
}
/**
* Add a point to this plot line.
* @param {number} domainValue the domain value
* @param {number} rangeValue the range value
*/
PlotLine.prototype.addPoint = function (domainValue, rangeValue) {
var buffer = this.buffer,
index;
// Make sure we got real/useful values here...
if (domainValue !== undefined && rangeValue !== undefined) {
index = buffer.findInsertionIndex(domainValue);
// Already in the buffer? Skip insertion
if (index < 0) {
return;
}
// Insert the point
if (!buffer.insertPoint(domainValue, rangeValue, index)) {
// If insertion failed, trim from the beginning...
buffer.trim(1);
// ...and try again.
buffer.insertPoint(domainValue, rangeValue, index);
}
}
};
/**
* Add a series of telemetry data to this plot line.
* @param {TelemetrySeries} series the data series
* @param {string} [domain] the key indicating which domain
* to use when looking up data from this series
* @param {string} [range] the key indicating which range
* to use when looking up data from this series
*/
PlotLine.prototype.addSeries = function (series, domain, range) {
var buffer = this.buffer;
// Insert a time-windowed data series into the buffer
function insertSeriesWindow(seriesWindow) {
var count = seriesWindow.getPointCount();
function doInsert() {
var firstTimestamp = seriesWindow.getDomainValue(0),
lastTimestamp = seriesWindow.getDomainValue(count - 1),
startIndex = buffer.findInsertionIndex(firstTimestamp),
endIndex = buffer.findInsertionIndex(lastTimestamp);
// Does the whole series fit in between two adjacent indexes?
if ((startIndex === endIndex) && startIndex > -1) {
// Insert it in between
buffer.insert(seriesWindow, startIndex);
} else {
// Split it up, and add the two halves
seriesWindow.split().forEach(insertSeriesWindow);
}
}
// Only insert if there are points to insert
if (count > 0) {
doInsert();
}
}
// Should try to add via insertion if a
// clear insertion point is available;
// if not, should split and add each half.
// Insertion operation also needs to factor out
// redundant timestamps, for overlapping data
insertSeriesWindow(new PlotSeriesWindow(
series,
domain,
range,
0,
series.getPointCount()
));
};
return PlotLine;
}
);

View File

@ -0,0 +1,268 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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 () {
/**
* Contains the buffer used to draw a plot.
* @param {number} domainOffset number to subtract from domain values
* @param {number} initialSize initial buffer size
* @param {number} maxSize maximum buffer size
* @memberof platform/features/plot
* @constructor
*/
function PlotLineBuffer(domainOffset, initialSize, maxSize) {
this.buffer = new Float32Array(initialSize * 2);
this.rangeExtrema =
[Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
this.length = 0;
this.domainOffset = domainOffset;
this.initialSize = initialSize;
this.maxSize = maxSize;
}
// Binary search for an insertion index
PlotLineBuffer.prototype.binSearch = function (value, min, max) {
var mid = Math.floor((min + max) / 2),
found = this.buffer[mid * 2];
// On collisions, insert at same index
if (found === value) {
return mid;
}
// Otherwise, if we're down to a single index,
// we've found our insertion point
if (min >= max) {
// Compare the found timestamp with the search
// value to decide if we'll insert after or before.
return min + ((found < value) ? 1 : 0);
}
// Finally, do the recursive step
if (found < value) {
return this.binSearch(value, mid + 1, max);
} else {
return this.binSearch(value, min, mid - 1);
}
};
// Increase the size of the buffer
PlotLineBuffer.prototype.doubleBufferSize = function () {
var sz = Math.min(this.maxSize * 2, this.buffer.length * 2),
canDouble = sz > this.buffer.length,
doubled = canDouble && new Float32Array(sz);
if (canDouble) {
doubled.set(this.buffer); // Copy contents of original
this.buffer = doubled;
}
return canDouble;
};
// Decrease the size of the buffer
PlotLineBuffer.prototype.halveBufferSize = function () {
var sz = Math.max(this.initialSize * 2, this.buffer.length / 2),
canHalve = sz < this.buffer.length;
if (canHalve) {
this.buffer = new Float32Array(this.buffer.subarray(0, sz));
}
return canHalve;
};
// Set a value in the buffer
PlotLineBuffer.prototype.setValue = function (index, domainValue, rangeValue) {
this.buffer[index * 2] = domainValue - this.domainOffset;
this.buffer[index * 2 + 1] = rangeValue;
// Track min/max of range values (min/max for
// domain values can be read directly from buffer)
this.rangeExtrema[0] = Math.min(this.rangeExtrema[0], rangeValue);
this.rangeExtrema[1] = Math.max(this.rangeExtrema[1], rangeValue);
};
/**
* Get the WebGL-displayable buffer of points to plot.
* @returns {Float32Array} displayable buffer for this line
*/
PlotLineBuffer.prototype.getBuffer = function () {
return this.buffer;
};
/**
* Get the number of points stored in this buffer.
* @returns {number} the number of points stored
*/
PlotLineBuffer.prototype.getLength = function () {
return this.length;
};
/**
* Get the min/max range values that are currently in this
* buffer. Unlike range extrema, these will change as the
* buffer gets trimmed.
* @returns {number[]} min, max domain values
*/
PlotLineBuffer.prototype.getDomainExtrema = function () {
// Since these are ordered in the buffer, assume
// these are the values at the first and last index
return [
this.buffer[0] + this.domainOffset,
this.buffer[this.length * 2 - 2] + this.domainOffset
];
};
/**
* Get the min/max range values that have been observed for this
* buffer. Note that these values may have been trimmed out at
* some point.
* @returns {number[]} min, max range values
*/
PlotLineBuffer.prototype.getRangeExtrema = function () {
return this.rangeExtrema;
};
/**
* Remove values from this buffer.
* Normally, values are removed from the start
* of the buffer; a truthy value in the second argument
* will cause values to be removed from the end.
* @param {number} count number of values to remove
* @param {boolean} [fromEnd] true if the most recent
* values should be removed
*/
PlotLineBuffer.prototype.trim = function (count, fromEnd) {
// If we're removing values from the start...
if (!fromEnd) {
// ...do so by shifting buffer contents over
this.buffer.set(this.buffer.subarray(2 * count));
}
// Reduce used buffer size accordingly
this.length -= count;
// Finally, if less than half of the buffer is being
// used, free up some memory.
if (this.length < this.buffer.length / 4) {
this.halveBufferSize();
}
};
/**
* Insert data from the provided series at the specified
* index. If this would exceed the buffer's maximum capacity,
* this operation fails and the buffer is unchanged.
* @param {TelemetrySeries} series the series to insert
* @param {number} index the index at which to insert this
* series
* @returns {boolean} true if insertion succeeded; otherwise
* false
*/
PlotLineBuffer.prototype.insert = function (series, index) {
var sz = series.getPointCount(),
i;
// Don't allow append after the end; that doesn't make sense
index = Math.min(index, this.length);
// Resize if necessary
while (sz > ((this.buffer.length / 2) - this.length)) {
if (!this.doubleBufferSize()) {
// Can't make room for this, insertion fails
return false;
}
}
// Shift data over if necessary
if (index < this.length) {
this.buffer.set(
this.buffer.subarray(index * 2, this.length * 2),
(index + sz) * 2
);
}
// Insert data into the set
for (i = 0; i < sz; i += 1) {
this.setValue(
i + index,
series.getDomainValue(i),
series.getRangeValue(i)
);
}
// Increase the length
this.length += sz;
// Indicate that insertion was successful
return true;
};
/**
* Append a single data point.
* @memberof platform/features/plot.PlotLineBuffer#
*/
PlotLineBuffer.prototype.insertPoint = function (domainValue, rangeValue) {
// Ensure there is space for this point
if (this.length >= (this.buffer.length / 2)) {
if (!this.doubleBufferSize()) {
return false;
}
}
// Put the data in the buffer
this.setValue(this.length, domainValue, rangeValue);
// Update length
this.length += 1;
// Indicate that this was successful
return true;
};
/**
* Find an index for inserting data with this
* timestamp. The second argument indicates whether
* we are searching for insert-before or insert-after
* positions.
* Timestamps are meant to be unique, so if a collision
* occurs, this will return -1.
* @param {number} timestamp timestamp to insert
* @returns {number} the index for insertion (or -1)
*/
PlotLineBuffer.prototype.findInsertionIndex = function (timestamp) {
var value = timestamp - this.domainOffset;
// Handle empty buffer case and check for an
// append opportunity (which is most common case for
// real-time data so is optimized-for) before falling
// back to a binary search for the insertion point.
return (this.length < 1) ? 0 :
(value > this.buffer[this.length * 2 - 2]) ? this.length :
this.binSearch(value, 0, this.length - 1);
};
return PlotLineBuffer;
}
);

View File

@ -0,0 +1,133 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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.
*****************************************************************************/
/**
* Plot palette. Defines colors for various plot lines.
*/
define(
function () {
// Prepare different forms of the palette, since we wish to
// describe colors in several ways (as RGB 0-255, as
// RGB 0.0-1.0, or as stylesheet-appropriate #-prefixed colors).
var integerPalette = [
[0x20, 0xB2, 0xAA],
[0x9A, 0xCD, 0x32],
[0xFF, 0x8C, 0x00],
[0xD2, 0xB4, 0x8C],
[0x40, 0xE0, 0xD0],
[0x41, 0x69, 0xFF],
[0xFF, 0xD7, 0x00],
[0x6A, 0x5A, 0xCD],
[0xEE, 0x82, 0xEE],
[0xCC, 0x99, 0x66],
[0x99, 0xCC, 0xCC],
[0x66, 0xCC, 0x33],
[0xFF, 0xCC, 0x00],
[0xFF, 0x66, 0x33],
[0xCC, 0x66, 0xFF],
[0xFF, 0x00, 0x66],
[0xFF, 0xFF, 0x00],
[0x80, 0x00, 0x80],
[0x00, 0x86, 0x8B],
[0x00, 0x8A, 0x00],
[0xFF, 0x00, 0x00],
[0x00, 0x00, 0xFF],
[0xF5, 0xDE, 0xB3],
[0xBC, 0x8F, 0x8F],
[0x46, 0x82, 0xB4],
[0xFF, 0xAF, 0xAF],
[0x43, 0xCD, 0x80],
[0xCD, 0xC1, 0xC5],
[0xA0, 0x52, 0x2D],
[0x64, 0x95, 0xED]
], stringPalette = integerPalette.map(function (arr) {
// Convert to # notation for use in styles
return '#' + arr.map(function (c) {
return (c < 16 ? '0' : '') + c.toString(16);
}).join('');
}), floatPalette = integerPalette.map(function (arr) {
return arr.map(function (c) {
return c / 255.0;
}).concat([1]); // RGBA
});
/**
* PlotPalette allows a consistent set of colors to be retrieved
* by index, in various color formats. All PlotPalette methods are
* static, so there is no need for a constructor call; using
* this will simply return PlotPalette itself.
* @memberof platform/features/plot
* @constructor
*/
function PlotPalette() {
return PlotPalette;
}
/**
* Look up a color in the plot's palette, by index.
* This will be returned as a three element array of RGB
* values, as integers in the range of 0-255.
* @param {number} i the index of the color to look up
* @return {number[]} the color, as integer RGB values
*/
PlotPalette.getIntegerColor = function (i) {
return integerPalette[Math.floor(i) % integerPalette.length];
};
/**
* Look up a color in the plot's palette, by index.
* This will be returned as a three element array of RGB
* values, in the range of 0.0-1.0.
*
* This format is present specifically to support use with
* WebGL, which expects colors of that form.
*
* @param {number} i the index of the color to look up
* @return {number[]} the color, as floating-point RGB values
*/
PlotPalette.getFloatColor = function (i) {
return floatPalette[Math.floor(i) % floatPalette.length];
};
/**
* Look up a color in the plot's palette, by index.
* This will be returned as a string using #-prefixed
* six-digit RGB hex notation (e.g. #FF0000)
* See http://www.w3.org/TR/css3-color/#rgb-color.
*
* This format is useful for representing colors in in-line
* styles.
*
* @param {number} i the index of the color to look up
* @return {string} the color, as a style-friendly string
*/
PlotPalette.getStringColor = function (i) {
return stringPalette[Math.floor(i) % stringPalette.length];
};
return PlotPalette;
}
);

View File

@ -0,0 +1,141 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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 () {
/**
* The PlotPanZoomStack is responsible for maintaining the
* pan-zoom state of a plot (expressed as a boundary starting
* at an origin and extending to certain dimensions) in a
* stack, to support the back and unzoom buttons in plot controls.
*
* Dimensions and origins are here described each by two-element
* arrays, where the first element describes a value or quantity
* along the domain axis, and the second element describes the same
* along the range axis.
*
* @memberof platform/features/plot
* @constructor
* @param {number[]} origin the plot's origin, initially
* @param {number[]} dimensions the plot's dimensions, initially
*/
function PlotPanZoomStack(origin, dimensions) {
// Use constructor parameters as the stack's initial state
this.stack = [{ origin: origin, dimensions: dimensions }];
}
// Various functions which follow are simply wrappers for
// normal stack-like array methods, with the exception that
// they prevent undesired modification and enforce that this
// stack must remain non-empty.
// See JSDoc for specific methods below for more detail.
/**
* Get the current stack depth; that is, the number
* of items on the stack. A depth of one means that no
* panning or zooming relative to the base value has
* been applied.
* @returns {number} the depth of the stack
*/
PlotPanZoomStack.prototype.getDepth = function getDepth() {
return this.stack.length;
};
/**
* Push a new pan-zoom state onto the stack; this will
* become the active pan-zoom state.
* @param {number[]} origin the new origin
* @param {number[]} dimensions the new dimensions
*/
PlotPanZoomStack.prototype.pushPanZoom = function (origin, dimensions) {
this.stack.push({ origin: origin, dimensions: dimensions });
};
/**
* Pop a pan-zoom state from the stack. Whatever pan-zoom
* state was previously present will become current.
* If called when there is only one pan-zoom state on the
* stack, this acts as a no-op (that is, the lowest
* pan-zoom state on the stack cannot be popped, to ensure
* that some pan-zoom state is always available.)
*/
PlotPanZoomStack.prototype.popPanZoom = function popPanZoom() {
if (this.stack.length > 1) {
this.stack.pop();
}
};
/**
* Set the base pan-zoom state; that is, the state at the
* bottom of the stack. This allows the "unzoomed" state of
* a plot to be updated (e.g. as new data comes in) without
* interfering with the user's chosen zoom level.
* @param {number[]} origin the base origin
* @param {number[]} dimensions the base dimensions
* @memberof platform/features/plot.PlotPanZoomStack#
*/
PlotPanZoomStack.prototype.setBasePanZoom = function (origin, dimensions) {
this.stack[0] = { origin: origin, dimensions: dimensions };
};
/**
* Clear the pan-zoom stack down to its bottom element;
* in effect, pop all elements but the last, e.g. to remove
* any temporary user modifications to pan-zoom state.
*/
PlotPanZoomStack.prototype.clearPanZoom = function clearPanZoom() {
this.stack = [this.stack[0]];
};
/**
* Get the current pan-zoom state (the state at the top
* of the stack), expressed as an object with "origin" and
* "dimensions" fields.
* @returns {object} the current pan-zoom state
*/
PlotPanZoomStack.prototype.getPanZoom = function getPanZoom() {
return this.stack[this.stack.length - 1];
};
/**
* Get the current origin, as represented on the top of the
* stack.
* @returns {number[]} the current plot origin
*/
PlotPanZoomStack.prototype.getOrigin = function getOrigin() {
return this.getPanZoom().origin;
};
/**
* Get the current dimensions, as represented on the top of
* the stack.
* @returns {number[]} the current plot dimensions
*/
PlotPanZoomStack.prototype.getDimensions = function getDimensions() {
return this.getPanZoom().dimensions;
};
return PlotPanZoomStack;
}
);

View File

@ -0,0 +1,167 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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(
['./PlotPanZoomStack'],
function (PlotPanZoomStack) {
/**
* A plot pan zoom stack group provides a collection of individual
* pan-zoom stacks that synchronize upon the domain axis, but
* remain independent upon the range axis. This supports panning
* and zooming in stacked-plot mode (and, importantly,
* stepping back through those states.)
* @memberof platform/features/plot
* @constructor
* @param {number} count the number of stacks to include in this
* group
*/
function PlotPanZoomStackGroup(count) {
var self = this;
// Push a pan-zoom state; the index argument identifies
// which stack originated the request (all other stacks
// will ignore the range part of the change.)
function pushPanZoom(origin, dimensions, index) {
self.stacks.forEach(function (stack, i) {
if (i === index) {
// Do a normal push for the specified stack
stack.pushPanZoom(origin, dimensions);
} else {
// For other stacks, do a push, but repeat
// their current range axis bounds.
stack.pushPanZoom(
[origin[0], stack.getOrigin()[1]],
[dimensions[0], stack.getDimensions()[1]]
);
}
});
}
// Decorate a pan-zoom stack; returns an object with
// the same interface, but whose stack-mutation methods
// effect all items in the group.
function decorateStack(stack, index) {
var result = Object.create(stack);
// Use the methods defined above
result.pushPanZoom = function (origin, dimensions) {
pushPanZoom(origin, dimensions, index);
};
result.setBasePanZoom = function () {
self.setBasePanZoom.apply(self, arguments);
};
result.popPanZoom = function () {
self.popPanZoom.apply(self, arguments);
};
result.clearPanZoom = function () {
self.clearPanZoom.apply(self, arguments);
};
return result;
}
// Create the stacks in this group ...
this.stacks = [];
while (this.stacks.length < count) {
this.stacks.push(new PlotPanZoomStack([], []));
}
// ... and their decorated-to-synchronize versions.
this.decoratedStacks = this.stacks.map(decorateStack);
}
/**
* Pop a pan-zoom state from all stacks in the group.
* If called when there is only one pan-zoom state on each
* stack, this acts as a no-op (that is, the lowest
* pan-zoom state on the stack cannot be popped, to ensure
* that some pan-zoom state is always available.)
*/
PlotPanZoomStackGroup.prototype.popPanZoom = function () {
this.stacks.forEach(function (stack) {
stack.popPanZoom();
});
};
/**
* Set the base pan-zoom state for all stacks in this group.
* This changes the state at the bottom of each stack.
* This allows the "unzoomed" state of plots to be updated
* (e.g. as new data comes in) without
* interfering with the user's chosen pan/zoom states.
* @param {number[]} origin the base origin
* @param {number[]} dimensions the base dimensions
*/
PlotPanZoomStackGroup.prototype.setBasePanZoom = function (origin, dimensions) {
this.stacks.forEach(function (stack) {
stack.setBasePanZoom(origin, dimensions);
});
};
/**
* Clear all pan-zoom stacks in this group down to
* their bottom element; in effect, pop all elements
* but the last, e.g. to remove any temporary user
* modifications to pan-zoom state.
*/
PlotPanZoomStackGroup.prototype.clearPanZoom = function () {
this.stacks.forEach(function (stack) {
stack.clearPanZoom();
});
};
/**
* Get the current stack depth; that is, the number
* of items on each stack in the group.
* A depth of one means that no
* panning or zooming relative to the base value has
* been applied.
* @returns {number} the depth of the stacks in this group
*/
PlotPanZoomStackGroup.prototype.getDepth = function () {
// All stacks are kept in sync, so look up depth
// from the first one.
return this.stacks.length > 0 ? this.stacks[0].getDepth() : 0;
};
/**
* Get a specific pan-zoom stack in this group.
* Stacks are specified by index; this index must be less
* than the count provided at construction time, and must
* not be less than zero.
* The stack returned by this function will be synchronized
* to other stacks in this group; that is, mutating that
* stack directly will result in other stacks in this group
* undergoing similar updates to ensure that domain bounds
* remain the same.
* @param {number} index the index of the stack to get
* @returns {PlotPanZoomStack} the pan-zoom stack in the
* group identified by that index
*/
PlotPanZoomStackGroup.prototype.getPanZoomStack = function (index) {
return this.decoratedStacks[index];
};
return PlotPanZoomStackGroup;
}
);

View File

@ -0,0 +1,95 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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 () {
/**
* A PlotPosition converts from pixel coordinates to domain-range
* coordinates, based on the current plot boundary as described on
* the pan-zoom stack.
*
* These coordinates are not updated after construction; that is,
* they represent the result of the conversion at the time the
* PlotPosition was instantiated. Care should be taken when retaining
* PlotPosition objects across changes to the pan-zoom stack.
*
* @memberof platform/features/plot
* @constructor
* @param {number} x the horizontal pixel position in the plot area
* @param {number} y the vertical pixel position in the plot area
* @param {number} width the width of the plot area
* @param {number} height the height of the plot area
* @param {PanZoomStack} panZoomStack the applicable pan-zoom stack,
* used to determine the plot's domain-range boundaries.
*/
function PlotPosition(x, y, width, height, panZoomStack) {
var panZoom = panZoomStack.getPanZoom(),
origin = panZoom.origin,
dimensions = panZoom.dimensions;
function convert(v, i) {
return v * dimensions[i] + origin[i];
}
if (!dimensions || !origin) {
// We need both dimensions and origin to compute a position
this.position = [];
} else {
// Convert from pixel to domain-range space.
// Note that range is reversed from the y-axis in pixel space
//(positive range points up, positive pixel-y points down)
this.position =
[x / width, (height - y) / height].map(convert);
}
}
/**
* Get the domain value corresponding to this pixel position.
* @returns {number} the domain value
*/
PlotPosition.prototype.getDomain = function () {
return this.position[0];
};
/**
* Get the range value corresponding to this pixel position.
* @returns {number} the range value
*/
PlotPosition.prototype.getRange = function () {
return this.position[1];
};
/**
* Get the domain and values corresponding to this
* pixel position.
* @returns {number[]} an array containing the domain and
* the range value, in that order
*/
PlotPosition.prototype.getPosition = function () {
return this.position;
};
return PlotPosition;
}
);

View File

@ -0,0 +1,153 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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.
*****************************************************************************/
/**
* Prepares data to be rendered in a GL Plot. Handles
* the conversion from data API to displayable buffers.
*/
define(
function () {
function identity(x) {
return x;
}
/**
* The PlotPreparer is responsible for handling data sets and
* preparing them to be rendered. It creates a WebGL-plottable
* Float32Array for each trace, and tracks the boundaries of the
* data sets (since this is convenient to do during the same pass).
* @memberof platform/features/plot
* @constructor
* @param {Telemetry[]} datas telemetry data objects
* @param {string} domain the key to use when looking up domain values
* @param {string} range the key to use when looking up range values
*/
function PlotPreparer(datas, domain, range) {
var index,
vertices = [],
max = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
min = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
x,
y,
domainOffset = Number.POSITIVE_INFINITY;
// Remove any undefined data sets
datas = (datas || []).filter(identity);
// Do a first pass to determine the domain offset.
// This will be use to reduce the magnitude of domain values
// in the buffer, to minimize loss-of-precision when
// converting to a 32-bit float.
datas.forEach(function (data) {
domainOffset = Math.min(data.getDomainValue(0, domain), domainOffset);
});
// Assemble buffers, and track bounds of the data present
datas.forEach(function (data, i) {
vertices.push([]);
for (index = 0; index < data.getPointCount(); index += 1) {
x = data.getDomainValue(index, domain);
y = data.getRangeValue(index, range);
vertices[i].push(x - domainOffset);
vertices[i].push(y);
min[0] = Math.min(min[0], x);
min[1] = Math.min(min[1], y);
max[0] = Math.max(max[0], x);
max[1] = Math.max(max[1], y);
}
});
// If range is empty, add some padding
if (max[1] === min[1]) {
max[1] = max[1] + 1.0;
min[1] = min[1] - 1.0;
}
// Convert to Float32Array
this.buffers = vertices.map(function (v) {
return new Float32Array(v);
});
this.min = min;
this.max = max;
this.domainOffset = domainOffset;
}
/**
* Get the dimensions which bound all data in the provided
* data sets. This is given as a two-element array where the
* first element is domain, and second is range.
* @returns {number[]} the dimensions which bound this data set
*/
PlotPreparer.prototype.getDimensions = function () {
var max = this.max, min = this.min;
return [max[0] - min[0], max[1] - min[1]];
};
/**
* Get the origin of this data set's boundary.
* This is given as a two-element array where the
* first element is domain, and second is range.
* The domain value here is not adjusted by the domain offset.
* @returns {number[]} the origin of this data set's boundary
*/
PlotPreparer.prototype.getOrigin = function () {
return this.min;
};
/**
* Get the domain offset; this offset will have been subtracted
* from all domain values in all buffers returned by this
* preparer, in order to minimize loss-of-precision due to
* conversion to the 32-bit float format needed by WebGL.
* @returns {number} the domain offset
*/
PlotPreparer.prototype.getDomainOffset = function () {
return this.domainOffset;
};
/**
* Get all renderable buffers for this data set. This will
* be returned as an array which can be correlated back to
* the provided telemetry data objects (from the constructor
* call) by index.
*
* Internally, these are flattened; each buffer contains a
* sequence of alternating domain and range values.
*
* All domain values in all buffers will have been adjusted
* from their original values by subtraction of the domain
* offset; this minimizes loss-of-precision resulting from
* the conversion to 32-bit floats, which may otherwise
* cause aliasing artifacts (particularly for timestamps)
*
* @returns {Float32Array[]} the buffers for these traces
*/
PlotPreparer.prototype.getBuffers = function () {
return this.buffers;
};
return PlotPreparer;
}
);

Some files were not shown because too many files have changed in this diff Show More