Compare commits

..

2 Commits

Author SHA1 Message Date
Joshi
1a6ccc77fc Add controls for forms 2021-04-16 11:21:34 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
a5946aa10e WIP: form refactor 2021-04-15 11:26:55 -07:00
184 changed files with 9585 additions and 3453 deletions

3
API.md
View File

@@ -423,14 +423,13 @@ attribute | type | flags | notes
###### Value Hints
Each telemetry value description has an object defining hints. Keys in this object represent the hint itself, and the value represents the weight of that hint. A lower weight means the hint has a higher priority. For example, multiple values could be hinted for use as the y-axis of a plot (raw, engineering), but the highest priority would be the default choice. Likewise, a table will use hints to determine the default order of columns.
Each telemetry value description has an object defining hints. Keys in this this object represent the hint itself, and the value represents the weight of that hint. A lower weight means the hint has a higher priority. For example, multiple values could be hinted for use as the y-axis of a plot (raw, engineering), but the highest priority would be the default choice. Likewise, a table will use hints to determine the default order of columns.
Known hints:
* `domain`: Values with a `domain` hint will be used for the x-axis of a plot, and tables will render columns for these values first.
* `range`: Values with a `range` hint will be used as the y-axis on a plot, and tables will render columns for these values after the `domain` values.
* `image`: Indicates that the value may be interpreted as the URL to an image file, in which case appropriate views will be made available.
* `imageDownloadName`: Indicates that the value may be interpreted as the name of the image file.
##### The Time Conductor and Telemetry

View File

@@ -50,16 +50,11 @@ define([
const IMAGE_DELAY = 20000;
function pointForTimestamp(timestamp, name) {
const url = IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length];
const urlItems = url.split('/');
const imageDownloadName = `example.imagery.${urlItems[urlItems.length - 1]}`;
return {
name,
name: name,
utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
url,
imageDownloadName
url: IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length]
};
}
@@ -144,14 +139,6 @@ define([
hints: {
image: 1
}
},
{
name: 'Image Download Name',
key: 'imageDownloadName',
format: 'imageDownloadName',
hints: {
imageDownloadName: 1
}
}
]
};

View File

@@ -88,6 +88,7 @@
openmct.install(openmct.plugins.ExampleImagery());
openmct.install(openmct.plugins.PlanLayout());
openmct.install(openmct.plugins.Timeline());
openmct.install(openmct.plugins.PlotVue());
openmct.install(openmct.plugins.UTCTimeSystem());
openmct.install(openmct.plugins.AutoflowView({
type: "telemetry.panel"

View File

@@ -1,6 +1,6 @@
{
"name": "openmct",
"version": "1.7.3-SNAPSHOT",
"version": "1.7.1-SNAPSHOT",
"description": "The Open MCT core platform",
"dependencies": {},
"devDependencies": {

View File

@@ -26,7 +26,6 @@ define([
"./src/controllers/EditObjectController",
"./src/actions/EditAndComposeAction",
"./src/actions/EditAction",
"./src/actions/PropertiesAction",
"./src/actions/SaveAction",
"./src/actions/SaveAndStopEditingAction",
"./src/actions/SaveAsAction",
@@ -55,7 +54,6 @@ define([
EditObjectController,
EditAndComposeAction,
EditAction,
PropertiesAction,
SaveAction,
SaveAndStopEditingAction,
SaveAsAction,
@@ -143,22 +141,6 @@ define([
"group": "action",
"priority": 10
},
{
"key": "properties",
"category": [
"contextual",
"view-control"
],
"implementation": PropertiesAction,
"cssClass": "major icon-pencil",
"name": "Edit Properties...",
"group": "action",
"priority": 10,
"description": "Edit properties of this object.",
"depends": [
"dialogService"
]
},
{
"key": "save-and-stop-editing",
"category": "save",

View File

@@ -21,11 +21,11 @@
*****************************************************************************/
define([
'../creation/CreateWizard',
// '../creation/CreateWizard',
'./SaveInProgressDialog'
],
function (
CreateWizard,
// CreateWizard,
SaveInProgressDialog
) {
@@ -100,7 +100,8 @@ function (
toUndirty = [];
function doWizardSave(parent) {
var wizard = self.createWizard(parent);
console.log('SaveAsAction');
// var wizard = self.createWizard(parent);
return self.dialogService
.getUserInput(wizard.getFormStructure(true),

View File

@@ -141,17 +141,11 @@ define(
if (mutationResult !== false) {
// Copy values if result was a different object
// (either our clone or some other new thing)
let modelHasChanged = _.isEqual(model, result) === false;
if (modelHasChanged) {
if (model !== result) {
copyValues(model, result);
}
if (modelHasChanged
|| (useTimestamp !== undefined)
|| (model.modified === undefined)) {
model.modified = useTimestamp ? timestamp : now();
}
model.modified = useTimestamp ? timestamp : now();
notifyListeners(model);
}

View File

@@ -154,9 +154,7 @@ define(['zepto', 'objectUtils'], function ($, objectUtils) {
tree = JSON.stringify(tree).replace(new RegExp(oldIdKeyString, 'g'), newIdKeyString);
return JSON.parse(tree, (key, value) => {
if (value !== undefined
&& value !== null
&& Object.prototype.hasOwnProperty.call(value, 'key')
if (Object.prototype.hasOwnProperty.call(value, 'key')
&& Object.prototype.hasOwnProperty.call(value, 'namespace')
&& value.key === oldId.key
&& value.namespace === oldId.namespace) {

View File

@@ -0,0 +1,8 @@
# Couch DB Persistence Plugin
An adapter for using CouchDB for persistence of user-created objects. The plugin installation function takes the URL
for the CouchDB database as a parameter.
## Installation
```js
openmct.install(openmct.plugins.CouchDB('http://localhost:5984/openmct'))
```

View File

@@ -0,0 +1,78 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
"./src/CouchPersistenceProvider",
"./src/CouchIndicator"
], function (
CouchPersistenceProvider,
CouchIndicator
) {
return {
name: "platform/persistence/couch",
definition: {
"name": "Couch Persistence",
"description": "Adapter to read and write objects using a CouchDB instance.",
"extensions": {
"components": [
{
"provides": "persistenceService",
"type": "provider",
"implementation": CouchPersistenceProvider,
"depends": [
"$http",
"$q",
"PERSISTENCE_SPACE",
"COUCHDB_PATH"
]
}
],
"constants": [
{
"key": "PERSISTENCE_SPACE",
"value": "mct"
},
{
"key": "COUCHDB_PATH",
"value": "/couch/openmct"
},
{
"key": "COUCHDB_INDICATOR_INTERVAL",
"value": 15000
}
],
"indicators": [
{
"implementation": CouchIndicator,
"depends": [
"$http",
"$interval",
"COUCHDB_PATH",
"COUCHDB_INDICATOR_INTERVAL"
]
}
]
}
}
};
});

View File

@@ -0,0 +1,61 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[],
function () {
/**
* A CouchDocument describes domain object model in a format
* which is easily read-written to CouchDB. This includes
* Couch's _id and _rev fields, as well as a separate
* metadata field which contains a subset of information found
* in the model itself (to support search optimization with
* CouchDB views.)
* @memberof platform/persistence/couch
* @constructor
* @param {string} id the id under which to store this mode
* @param {object} model the model to store
* @param {string} rev the revision to include (or undefined,
* if no revision should be noted for couch)
* @param {boolean} whether or not to mark this document as
* deleted (see CouchDB docs for _deleted)
*/
function CouchDocument(id, model, rev, markDeleted) {
return {
"_id": id,
"_rev": rev,
"_deleted": markDeleted,
"metadata": {
"category": "domain object",
"type": model.type,
"owner": "admin",
"name": model.name,
"created": Date.now()
},
"model": model
};
}
return CouchDocument;
}
);

View File

@@ -0,0 +1,119 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[],
function () {
// Set of connection states; changing among these states will be
// reflected in the indicator's appearance.
// CONNECTED: Everything nominal, expect to be able to read/write.
// DISCONNECTED: HTTP failed; maybe misconfigured, disconnected.
// SEMICONNECTED: Connected to the database, but it reported an error.
// PENDING: Still trying to connect, and haven't failed yet.
var CONNECTED = {
text: "Connected",
glyphClass: "ok",
statusClass: "s-status-on",
description: "Connected to the domain object database."
},
DISCONNECTED = {
text: "Disconnected",
glyphClass: "err",
statusClass: "s-status-caution",
description: "Unable to connect to the domain object database."
},
SEMICONNECTED = {
text: "Unavailable",
glyphClass: "caution",
statusClass: "s-status-caution",
description: "Database does not exist or is unavailable."
},
PENDING = {
text: "Checking connection...",
statusClass: "s-status-caution"
};
/**
* Indicator for the current CouchDB connection. Polls CouchDB
* at a regular interval (defined by bundle constants) to ensure
* that the database is available.
* @constructor
* @memberof platform/persistence/couch
* @implements {Indicator}
* @param $http Angular's $http service
* @param $interval Angular's $interval service
* @param {string} path the URL to poll to check for couch availability
* @param {number} interval the interval, in milliseconds, to poll at
*/
function CouchIndicator($http, $interval, path, interval) {
var self = this;
// Track the current connection state
this.state = PENDING;
this.$http = $http;
this.$interval = $interval;
this.path = path;
this.interval = interval;
// Callback if the HTTP request to Couch fails
function handleError() {
self.state = DISCONNECTED;
}
// Callback if the HTTP request succeeds. CouchDB may
// report an error, so check for that.
function handleResponse(response) {
var data = response.data;
self.state = data.error ? SEMICONNECTED : CONNECTED;
}
// Try to connect to CouchDB, and update the indicator.
function updateIndicator() {
$http.get(path).then(handleResponse, handleError);
}
// Update the indicator initially, and start polling.
updateIndicator();
$interval(updateIndicator, interval);
}
CouchIndicator.prototype.getCssClass = function () {
return "c-indicator--clickable icon-suitcase " + this.state.statusClass;
};
CouchIndicator.prototype.getGlyphClass = function () {
return this.state.glyphClass;
};
CouchIndicator.prototype.getText = function () {
return this.state.text;
};
CouchIndicator.prototype.getDescription = function () {
return this.state.description;
};
return CouchIndicator;
}
);

View File

@@ -0,0 +1,145 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* This bundle implements a persistence service which uses CouchDB to
* store documents.
* @namespace platform/persistence/cache
*/
define(
["./CouchDocument"],
function (CouchDocument) {
// JSLint doesn't like dangling _'s, but CouchDB uses these, so
// hide this behind variables.
var REV = "_rev",
ID = "_id";
/**
* The CouchPersistenceProvider reads and writes JSON documents
* (more specifically, domain object models) to/from a CouchDB
* instance.
* @memberof platform/persistence/couch
* @constructor
* @implements {PersistenceService}
* @param $http Angular's $http service
* @param $interval Angular's $interval service
* @param {string} space the name of the persistence space being served
* @param {string} path the path to the CouchDB instance
*/
function CouchPersistenceProvider($http, $q, space, path) {
this.spaces = [space];
this.revs = {};
this.$q = $q;
this.$http = $http;
this.path = path;
}
// Pull out a list of document IDs from CouchDB's
// _all_docs response
function getIdsFromAllDocs(allDocs) {
return allDocs.rows.map(function (r) {
return r.id;
});
}
// Check the response to a create/update/delete request;
// track the rev if it's valid, otherwise return false to
// indicate that the request failed.
CouchPersistenceProvider.prototype.checkResponse = function (response) {
if (response && response.ok) {
this.revs[response.id] = response.rev;
return response.ok;
} else {
return false;
}
};
// Get a domain object model out of CouchDB's response
CouchPersistenceProvider.prototype.getModel = function (response) {
if (response && response.model) {
this.revs[response[ID]] = response[REV];
return response.model;
} else {
return undefined;
}
};
// Issue a request using $http; get back the plain JS object
// from the expected JSON response
CouchPersistenceProvider.prototype.request = function (subpath, method, value) {
return this.$http({
method: method,
url: this.path + '/' + subpath,
data: value
}).then(function (response) {
return response.data;
}, function () {
return undefined;
});
};
// Shorthand methods for GET/PUT methods
CouchPersistenceProvider.prototype.get = function (subpath) {
return this.request(subpath, "GET");
};
CouchPersistenceProvider.prototype.put = function (subpath, value) {
return this.request(subpath, "PUT", value);
};
CouchPersistenceProvider.prototype.listSpaces = function () {
return this.$q.when(this.spaces);
};
CouchPersistenceProvider.prototype.listObjects = function () {
return this.get("_all_docs").then(getIdsFromAllDocs.bind(this));
};
CouchPersistenceProvider.prototype.createObject = function (space, key, value) {
return this.put(key, new CouchDocument(key, value))
.then(this.checkResponse.bind(this));
};
CouchPersistenceProvider.prototype.readObject = function (space, key) {
return this.get(key).then(this.getModel.bind(this));
};
CouchPersistenceProvider.prototype.updateObject = function (space, key, value) {
var rev = this.revs[key];
return this.put(key, new CouchDocument(key, value, rev))
.then(this.checkResponse.bind(this));
};
CouchPersistenceProvider.prototype.deleteObject = function (space, key, value) {
var rev = this.revs[key];
return this.put(key, new CouchDocument(key, value, rev, true))
.then(this.checkResponse.bind(this));
};
return CouchPersistenceProvider;
}
);

View File

@@ -0,0 +1,63 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* DomainObjectProviderSpec. Created by vwoeltje on 11/6/14.
*/
define(
["../src/CouchDocument"],
function (CouchDocument) {
// JSLint doesn't like dangling _'s, but CouchDB uses these, so
// hide this behind variables.
var REV = "_rev",
ID = "_id",
DELETED = "_deleted";
describe("A couch document", function () {
it("includes an id", function () {
expect(new CouchDocument("testId", {})[ID])
.toEqual("testId");
});
it("includes a rev only when one is provided", function () {
expect(new CouchDocument("testId", {})[REV])
.not.toBeDefined();
expect(new CouchDocument("testId", {}, "testRev")[REV])
.toEqual("testRev");
});
it("includes the provided model", function () {
var model = { someKey: "some value" };
expect(new CouchDocument("testId", model).model)
.toEqual(model);
});
it("marks documents as deleted only on request", function () {
expect(new CouchDocument("testId", {}, "testRev")[DELETED])
.not.toBeDefined();
expect(new CouchDocument("testId", {}, "testRev", true)[DELETED])
.toBe(true);
});
});
}
);

View File

@@ -0,0 +1,129 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../src/CouchIndicator"],
function (CouchIndicator) {
xdescribe("The CouchDB status indicator", function () {
var mockHttp,
mockInterval,
testPath,
testInterval,
mockPromise,
indicator;
beforeEach(function () {
mockHttp = jasmine.createSpyObj("$http", ["get"]);
mockInterval = jasmine.createSpy("$interval");
mockPromise = jasmine.createSpyObj("promise", ["then"]);
testPath = "/test/path";
testInterval = 12321; // Some number
mockHttp.get.and.returnValue(mockPromise);
indicator = new CouchIndicator(
mockHttp,
mockInterval,
testPath,
testInterval
);
});
it("polls for changes", function () {
expect(mockInterval).toHaveBeenCalledWith(
jasmine.any(Function),
testInterval
);
});
it("has a database icon", function () {
expect(indicator.getCssClass()).toEqual("icon-database s-status-caution");
});
it("consults the database at the configured path", function () {
expect(mockHttp.get).toHaveBeenCalledWith(testPath);
});
it("changes when the database connection is nominal", function () {
var initialText = indicator.getText(),
initialDescrption = indicator.getDescription(),
initialGlyphClass = indicator.getGlyphClass();
// Nominal just means getting back an object, without
// an error field.
mockPromise.then.calls.mostRecent().args[0]({ data: {} });
// Verify that these values changed;
// don't test for specific text.
expect(indicator.getText()).not.toEqual(initialText);
expect(indicator.getGlyphClass()).not.toEqual(initialGlyphClass);
expect(indicator.getDescription()).not.toEqual(initialDescrption);
// Do check for specific class
expect(indicator.getGlyphClass()).toEqual("ok");
});
it("changes when the server reports an error", function () {
var initialText = indicator.getText(),
initialDescrption = indicator.getDescription(),
initialGlyphClass = indicator.getGlyphClass();
// Nominal just means getting back an object, with
// an error field.
mockPromise.then.calls.mostRecent().args[0](
{ data: { error: "Uh oh." } }
);
// Verify that these values changed;
// don't test for specific text.
expect(indicator.getText()).not.toEqual(initialText);
expect(indicator.getGlyphClass()).not.toEqual(initialGlyphClass);
expect(indicator.getDescription()).not.toEqual(initialDescrption);
// Do check for specific class
expect(indicator.getGlyphClass()).toEqual("caution");
});
it("changes when the server cannot be reached", function () {
var initialText = indicator.getText(),
initialDescrption = indicator.getDescription(),
initialGlyphClass = indicator.getGlyphClass();
// Nominal just means getting back an object, without
// an error field.
mockPromise.then.calls.mostRecent().args[1]({ data: {} });
// Verify that these values changed;
// don't test for specific text.
expect(indicator.getText()).not.toEqual(initialText);
expect(indicator.getGlyphClass()).not.toEqual(initialGlyphClass);
expect(indicator.getDescription()).not.toEqual(initialDescrption);
// Do check for specific class
expect(indicator.getGlyphClass()).toEqual("err");
});
});
}
);

View File

@@ -0,0 +1,223 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* DomainObjectProviderSpec. Created by vwoeltje on 11/6/14.
*/
define(
["../src/CouchPersistenceProvider"],
function (CouchPersistenceProvider) {
describe("The couch persistence provider", function () {
var mockHttp,
mockQ,
testSpace = "testSpace",
testPath = "/test/db",
capture,
provider;
function mockPromise(value) {
return {
then: function (callback) {
return mockPromise(callback(value));
}
};
}
beforeEach(function () {
mockHttp = jasmine.createSpy("$http");
mockQ = jasmine.createSpyObj("$q", ["when"]);
mockQ.when.and.callFake(mockPromise);
// Capture promise results
capture = jasmine.createSpy("capture");
provider = new CouchPersistenceProvider(
mockHttp,
mockQ,
testSpace,
testPath
);
});
it("reports available spaces", function () {
provider.listSpaces().then(capture);
expect(capture).toHaveBeenCalledWith([testSpace]);
});
// General pattern of tests below is to simulate CouchDB's
// response, verify that request looks like what CouchDB
// would expect, and finally verify that CouchPersistenceProvider's
// return values match what is expected.
it("lists all available documents", function () {
mockHttp.and.returnValue(mockPromise({
data: { rows: [{ id: "a" }, { id: "b" }, { id: "c" }] }
}));
provider.listObjects().then(capture);
expect(mockHttp).toHaveBeenCalledWith({
url: "/test/db/_all_docs", // couch document listing
method: "GET",
data: undefined
});
expect(capture).toHaveBeenCalledWith(["a", "b", "c"]);
});
it("allows object creation", function () {
var model = { someKey: "some value" };
mockHttp.and.returnValue(mockPromise({
data: {
"_id": "abc",
"_rev": "xyz",
"ok": true
}
}));
provider.createObject("testSpace", "abc", model).then(capture);
expect(mockHttp).toHaveBeenCalledWith({
url: "/test/db/abc",
method: "PUT",
data: {
"_id": "abc",
"_rev": undefined,
"_deleted": undefined,
metadata: jasmine.any(Object),
model: model
}
});
expect(capture).toHaveBeenCalledWith(true);
});
it("allows object models to be read back", function () {
var model = { someKey: "some value" };
mockHttp.and.returnValue(mockPromise({
data: {
"_id": "abc",
"_rev": "xyz",
"model": model
}
}));
provider.readObject("testSpace", "abc").then(capture);
expect(mockHttp).toHaveBeenCalledWith({
url: "/test/db/abc",
method: "GET",
data: undefined
});
expect(capture).toHaveBeenCalledWith(model);
});
it("allows object update", function () {
var model = { someKey: "some value" };
// First do a read to populate rev tags...
mockHttp.and.returnValue(mockPromise({
data: {
"_id": "abc",
"_rev": "xyz",
"model": {}
}
}));
provider.readObject("testSpace", "abc");
// Now perform an update
mockHttp.and.returnValue(mockPromise({
data: {
"_id": "abc",
"_rev": "uvw",
"ok": true
}
}));
provider.updateObject("testSpace", "abc", model).then(capture);
expect(mockHttp).toHaveBeenCalledWith({
url: "/test/db/abc",
method: "PUT",
data: {
"_id": "abc",
"_rev": "xyz",
"_deleted": undefined,
metadata: jasmine.any(Object),
model: model
}
});
expect(capture).toHaveBeenCalledWith(true);
});
it("allows object deletion", function () {
// First do a read to populate rev tags...
mockHttp.and.returnValue(mockPromise({
data: {
"_id": "abc",
"_rev": "xyz",
"model": {}
}
}));
provider.readObject("testSpace", "abc");
// Now perform an update
mockHttp.and.returnValue(mockPromise({
data: {
"_id": "abc",
"_rev": "uvw",
"ok": true
}
}));
provider.deleteObject("testSpace", "abc", {}).then(capture);
expect(mockHttp).toHaveBeenCalledWith({
url: "/test/db/abc",
method: "PUT",
data: {
"_id": "abc",
"_rev": "xyz",
"_deleted": true,
metadata: jasmine.any(Object),
model: {}
}
});
expect(capture).toHaveBeenCalledWith(true);
});
it("reports failure to create objects", function () {
var model = { someKey: "some value" };
mockHttp.and.returnValue(mockPromise({
data: {
"_id": "abc",
"_rev": "xyz",
"ok": false
}
}));
provider.createObject("testSpace", "abc", model).then(capture);
expect(capture).toHaveBeenCalledWith(false);
});
it("returns undefined when objects are not found", function () {
// Act like a 404
mockHttp.and.returnValue({
then: function (success, fail) {
return mockPromise(fail());
}
});
provider.readObject("testSpace", "abc").then(capture);
expect(capture).toHaveBeenCalledWith(undefined);
});
});
}
);

View File

@@ -252,6 +252,8 @@ define([
this.status = new api.StatusAPI(this);
this.forms = new api.FormsAPI.default(this);
this.router = new ApplicationRouter();
this.branding = BrandingAPI.default;

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import _ from 'lodash';
const INSIDE_EDIT_PATH_BLACKLIST = ["copy", "follow", "link", "locate", "move", "link"];
const OUTSIDE_EDIT_PATH_BLACKLIST = ["copy", "follow", "properties", "move", "link", "remove", "locate"];
const OUTSIDE_EDIT_PATH_BLACKLIST = ["copy", "follow", "move", "link", "remove", "locate"];
export default class LegacyContextMenuAction {
constructor(openmct, LegacyAction) {

View File

@@ -36,8 +36,7 @@ define([
'./views/installLegacyViews',
'./policies/LegacyCompositionPolicyAdapter',
'./actions/LegacyActionAdapter',
'./services/LegacyPersistenceAdapter',
'./services/ExportImageService'
'./services/LegacyPersistenceAdapter'
], function (
ActionDialogDecorator,
AdapterCapability,
@@ -54,8 +53,7 @@ define([
installLegacyViews,
legacyCompositionPolicyAdapter,
LegacyActionAdapter,
LegacyPersistenceAdapter,
ExportImageService
LegacyPersistenceAdapter
) {
return {
name: 'src/adapter',
@@ -84,13 +82,6 @@ define([
"identifierService",
"cacheService"
]
},
{
"key": "exportImageService",
"implementation": ExportImageService,
"depends": [
"dialogService"
]
}
],
components: [

View File

@@ -52,7 +52,7 @@ define([
oldStyleObject.getCapability('mutation').mutate(function () {
return utils.toOldFormat(newStyleObject);
}, newStyleObject.modified);
});
removeGeneralTopicListener = this.generalTopic.listen(handleLegacyMutation);
}.bind(this);

View File

@@ -21,41 +21,44 @@
*****************************************************************************/
define([
'./time/TimeAPI',
'./objects/ObjectAPI',
'./composition/CompositionAPI',
'./types/TypeRegistry',
'./telemetry/TelemetryAPI',
'./indicators/IndicatorAPI',
'./notifications/NotificationAPI',
'./Editor',
'./menu/MenuAPI',
'./actions/ActionsAPI',
'./status/StatusAPI'
'./composition/CompositionAPI',
'./Editor',
'./forms/FormsAPI',
'./indicators/IndicatorAPI',
'./menu/MenuAPI',
'./notifications/NotificationAPI',
'./objects/ObjectAPI',
'./status/StatusAPI',
'./telemetry/TelemetryAPI',
'./time/TimeAPI',
'./types/TypeRegistry'
], function (
TimeAPI,
ObjectAPI,
CompositionAPI,
TypeRegistry,
TelemetryAPI,
IndicatorAPI,
NotificationAPI,
EditorAPI,
MenuAPI,
ActionsAPI,
StatusAPI
CompositionAPI,
EditorAPI,
FormsAPI,
IndicatorAPI,
MenuAPI,
NotificationAPI,
ObjectAPI,
StatusAPI,
TelemetryAPI,
TimeAPI,
TypeRegistry
) {
return {
TimeAPI: TimeAPI,
ObjectAPI: ObjectAPI,
CompositionAPI: CompositionAPI,
TypeRegistry: TypeRegistry,
TelemetryAPI: TelemetryAPI,
IndicatorAPI: IndicatorAPI,
NotificationAPI: NotificationAPI.default,
EditorAPI: EditorAPI,
MenuAPI: MenuAPI.default,
ActionsAPI: ActionsAPI.default,
StatusAPI: StatusAPI.default
CompositionAPI: CompositionAPI,
EditorAPI: EditorAPI,
FormsAPI: FormsAPI,
IndicatorAPI: IndicatorAPI,
MenuAPI: MenuAPI.default,
NotificationAPI: NotificationAPI.default,
ObjectAPI: ObjectAPI,
StatusAPI: StatusAPI.default,
TelemetryAPI: TelemetryAPI,
TimeAPI: TimeAPI,
TypeRegistry: TypeRegistry,
};
});

View File

@@ -0,0 +1,178 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* A class for capturing user input data from an object creation
* dialog, and populating a domain object with that data.
*
* @param {DomainObject} domainObject the newly created object to
* populate with user input
* @param {DomainObject} parent the domain object to serve as
* the initial parent for the created object, in the dialog
* @memberof platform/commonUI/browse
* @constructor
*/
export default class CreateWizard {
constructor(domainObject, parent, openmct) {
this.type = openmct.types.get(domainObject.type);
this.model = domainObject;
this.domainObject = domainObject;
this.properties = this.type.definition.form || [];
this.parent = parent;
this.openmct = openmct;
}
/**
* Get the form model for this wizard; this is a description
* that will be rendered to an HTML form. See the
* platform/forms bundle
* @param {boolean} includeLocation if true, a 'location' section
* will be included that will allow the user to select the location
* of the newly created object, otherwise the .location property of
* the model will be used.
* @return {FormModel} formModel the form model to
* show in the create dialog
*/
getFormStructure(includeLocation) {
let sections = [];
let domainObject = this.domainObject;
let self = this;
function validateLocation(parent) {
return parent && self.openmct.composition.checkPolicy(parent.useCapability('adapter'), domainObject.useCapability('adapter'));
}
sections.push({
name: "Properties",
rows: this.properties.map(function (property, index) {
// Property definition is same as form row definition
let row = JSON.parse(JSON.stringify(property));
// Use index as the key into the formValue;
// this correlates to the indexing provided by
// getInitialFormValue
row.key = index;
return row;
}).filter(function (row) {
// Only show rows which have defined controls
return row && row.control;
})
});
// Ensure there is always a "save in" section
if (includeLocation) {
sections.push({
name: 'Location',
cssClass: "grows",
rows: [{
name: "Save In",
control: "locator",
validate: validateLocation.bind(this),
key: "createParent"
}]
});
}
return {
sections: sections,
name: "Create a New " + this.type.definition.name
};
}
/**
* Given some form input values and a domain object, populate the
* domain object used to create this wizard from the given form values.
* @param formValue
* @returns {DomainObject}
*/
populateObjectFromInput(formValue) {
let parent = this.getLocation(formValue);
let formModel = this.createModel(formValue);
formModel.location = parent.getId();
this.updateNamespaceFromParent(parent);
this.domainObject.useCapability("mutation", function () {
return formModel;
});
return this.domainObject;
}
updateNamespaceFromParent(parent) {
let childIdentifier = this.domainObject.useCapability('adapter').identifier;
let parentIdentifier = parent.useCapability('adapter').identifier;
childIdentifier.namespace = parentIdentifier.namespace;
this.domainObject.id = this.openmct.objects.makeKeyString(childIdentifier);
}
/**
* Get the initial value for the form being described.
* This will include the values for all properties described
* in the structure.
*
* @returns {object} the initial value of the form
*/
getInitialFormValue() {
// Start with initial values for properties
let model = this.model;
let formValue = this.properties.map(function (property) {
return property.getValue(model);
});
// Include the createParent
formValue.createParent = this.parent;
return formValue;
}
/**
* Based on a populated form, get the domain object which
* should be used as a parent for the newly-created object.
* @private
* @return {DomainObject}
*/
getLocation(formValue) {
return formValue.createParent || this.parent;
}
/**
* Create the domain object model for a newly-created object,
* based on user input read from a formModel.
* @private
* @return {object} the domain object model
*/
createModel(formValue) {
// Clone
let newModel = JSON.parse(JSON.stringify(this.domainObject));
// Update all properties
this.properties.forEach(function (property, index) {
property.setValue(newModel, formValue[index]);
});
return newModel;
}
}

40
src/api/forms/FormsAPI.js Normal file
View File

@@ -0,0 +1,40 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EditPropertiesAction from './actions/EditPropertiesAction';
export default class FormsAPI {
constructor(openmct) {
openmct.actions.register(new EditPropertiesAction(openmct));
}
addControl(name, actions) {
// TODO:
}
showForm() {
}
save() {
}
}

View File

@@ -0,0 +1,108 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* 'License'); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import PropertiesAction from './PropertiesAction';
import FormProperties from '../components/Form-properties.vue';
import CreateWizard from '../CreateWizard';
import Vue from "vue";
export default class CreateAction extends PropertiesAction {
constructor(openmct, key, parentDomainObject) {
super(openmct);
this.key = key;
this.parentDomainObject = parentDomainObject;
}
invoke() {
const definition = this._getTypeDefination(this.key);
console.log('CreateAction invoke, Show form', definition.form);
let newModel = {
type: this.key,
location: this.openmct.objects.makeKeyString(this.parentDomainObject.identifier)
};
if (definition.initialize) {
definition.initialize(newModel);
}
let openmct = this.openmct;
newModel.modified = Date.now();
let wizard = new CreateWizard(newModel, this.parentDomainObject, openmct);
let formStructure = wizard.getFormStructure(true);
// let initialFormValue = wizard.getInitialFormValue();
// if save navigateAndEdit
// this.openmct.editor.cancel();
this.showForm(formStructure);
}
showForm(formStructure) {
let vm = new Vue({
components: { FormProperties },
provide: {
openmct: this.openmct
},
data() {
return {
formStructure
};
},
template: '<form-properties :model="formStructure"></form-properties>'
}).$mount();
function dismissDialog(overlay, initialize) {
overlay.dismiss();
}
let overlay = this.openmct.overlays.overlay({
element: vm.$el, //TODO: create and show new form component
size: 'small',
buttons: [
{
label: 'OK',
emphasis: 'true',
callback: () => dismissDialog(overlay, true)
},
{
label: 'Cancel',
callback: () => dismissDialog(overlay, false)
}
],
onDestroy: () => vm.$destroy()
});
}
navigateAndEdit(object) {
let objectPath = object.getCapability('context').getPath();
let url = '#/browse/' + objectPath
.slice(1)
.map(function (o) {
return o && openmct.objects.makeKeyString(o.getId());
})
.join('/');
window.location.href = url;
if (isFirstViewEditable(object.useCapability('adapter'), objectPath)) {
openmct.editor.edit();
}
}
}

View File

@@ -0,0 +1,73 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* 'License'); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import PropertiesAction from './PropertiesAction';
export default class EditPropertiesAction extends PropertiesAction {
constructor(openmct) {
super(openmct);
this.name = 'Edit Properties...';
this.key = 'properties';
this.description = 'Edit properties of this object.';
this.cssClass = 'major icon-pencil';
this.hideInDefaultMenu = true;
this.group= 'action';
this.priority= 10;
}
appliesTo(objectPath) {
return Boolean(this._getForm(objectPath));
}
invoke(objectPath) {
const formElements = this._getFormElements(objectPath);
// Start:Testing
const parent = document.createElement('div');
formElements.forEach(e => {
const child = document.createElement('div');
const spanKey = document.createElement('span');
const spanValue = document.createElement('span');
spanKey.textContent = e.key;
spanValue.textContent = e.value;
child.appendChild(spanKey);
child.appendChild(spanValue);
parent.appendChild(child);
});
// End:Testing
let overlay = this.openmct.overlays.overlay({
element: parent, //TODO: create and show new form component
size: 'small',
buttons: [
{
label: 'Done',
// TODO: save form values into domain object properties
callback: () => overlay.dismiss()
}
],
onDestroy: () => {
}
});
}
}

View File

@@ -0,0 +1,91 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* 'License'); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default class PropertiesAction {
constructor(openmct) {
this.openmct = openmct;
}
invoke(objectPath) {
// Start:Testing
// const parent = document.createElement('div');
// formElements.forEach(e => {
// const child = document.createElement('div');
// const spanKey = document.createElement('span');
// const spanValue = document.createElement('span');
// spanKey.textContent = e.key;
// spanValue.textContent = e.value;
// child.appendChild(spanKey);
// child.appendChild(spanValue);
// parent.appendChild(child);
// });
// End:Testing
// let overlay = this.openmct.overlays.overlay({
// element: parent, //TODO: create and show new form component
// size: 'small',
// buttons: [
// {
// label: 'Done',
// // TODO: save form values into domain object properties
// callback: () => overlay.dismiss()
// }
// ],
// onDestroy: () => {
// }
// });
}
/**
* @private
*/
_getForm(objectPath) {
const definition = this._getTypeDefination(objectPath[0].type);
return definition.form;
}
/**
* @private
*/
_getFormElements(objectPath) {
const form = this._getForm(objectPath);
const domainObject = objectPath[0];
const formElements = [];
form.forEach(element => {
const value = element.property.reduce((acc, property) => acc[property], domainObject);
formElements.push({key: element.key, value});
});
return formElements;
}
/**
* @private
*/
_getTypeDefination(type) {
const TypeDefinition = this.openmct.types.get(type);
return TypeDefinition.definition;
}
}

View File

@@ -0,0 +1,53 @@
<template>
<form name="mctForm"
novalidate
class="form c-form"
autocomplete="off"
>
<span v-for="section in model.sections"
:key="section.name"
class="l-form-section c-form__section"
:class="section.cssClass"
>
<h2 class="c-form__header"
ng-if="section.name"
>
{{ section.name }}
</h2>
<div v-for="(row, index) in section.rows"
:key="row.name"
>
<form-row :css-class="section.cssClass"
:first="index < 1"
:row="row"
/>
</div>
</span>
</form>
</template>
<script>
import FormRow from "@/api/forms/components/FormRow.vue";
export default {
components: {
FormRow
},
props: {
model: {
type: Object,
required: true
},
value: {
type: Object,
required: false,
default() {
return {};
}
}
},
mounted() {
console.log(this.model, this.value);
}
};
</script>

View File

@@ -0,0 +1,115 @@
<template>
<div class="form-row c-form__row validates"
:class="rowClass"
>
<div class="c-form__row__label label flex-elem"
:title="row.description"
>
{{ row.name }}
</div>
<div class="c-form__row__controls controls flex-elem">
<div v-if="row.control && row.control !== 'locator'"
class="c-form__controls-wrapper wrapper"
>
<component
:is="row.control"
:key="row.key"
:ref="`form-control-${row.key}`"
:model="row"
:required="isRequired"
/>
</div>
</div>
</div>
</template>
<script>
import TextField from "@/api/forms/components/controls/TextField.vue";
import NumberField from "@/api/forms/components/controls/NumberField.vue";
import Composite from "@/api/forms/components/controls/Composite.vue";
import AutoCompleteField from "@/api/forms/components/controls/AutoCompleteField.vue";
const CONTROL_TYPE_VIEW_MAP = {
'textfield': TextField,
'numberfield': NumberField,
'composite': Composite,
'autocomplete': AutoCompleteField
};
export default {
name: 'FormRow',
components: CONTROL_TYPE_VIEW_MAP,
props: {
cssClass: {
type: String,
default: '',
required: true
},
first: {
type: Boolean,
default: false,
required: true
},
row: {
type: Object,
required: true
}
},
data() {
return {
valid: true
};
},
computed: {
isRequired() {
//TODO: Check if field is required
return false;
},
rowClass() {
let cssClass = this.cssClass;
if (this.first === true) {
cssClass = `${cssClass} first`;
}
if (this.row.required) {
cssClass = `${cssClass} req`;
}
if (this.row.layout === 'controls-first') {
cssClass = `${cssClass} l-controls-first`;
}
if (this.row.layout === 'controls-under') {
cssClass = `${cssClass} l-controls-under`;
}
if (this.valid === true) {
cssClass = `${cssClass} valid`;
} else {
cssClass = `${cssClass} invalid`;
}
return cssClass;
}
},
methods: {
// Check if an element is defined; the map step of isNonEmpty
isDefined(element) {
return typeof element !== 'undefined';
},
/**
* Check if an array contains anything other than
* undefined elements.
* @param {Array} value the array to check
* @returns {boolean} true if any non-undefined
* element is in the array
* @memberof platform/forms.CompositeController#
*/
isNonEmpty(value) {
return Array.isArray(value) && value.some(this.isDefined);
}
}
};
</script>

View File

@@ -0,0 +1,172 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div class="form-control autocomplete">
<input v-model="field"
class="autocompleteInput"
type="text"
@change="filterOptions(field)"
@click="inputClicked()"
@keydown="keyDown($event)"
>
<span class="icon-arrow-down"
@click="arrowClicked()"
></span>
<div v-show="hideOptions"
class="autocompleteOptions"
@blur="hideOptions = true"
>
<ul>
<li v-for="opt in filteredOptions"
:key="opt.optionId"
:class="{'optionPreSelected': optionIndex === opt.optionId}"
@click="fillInput(opt.name)"
@mouseover="optionMouseover(opt.optionId)"
>
<span class="optionText">{{ opt.name }}</span>
</li>
</ul>
</div>
</div>
</template>
<script>
const key = {
down: 40,
up: 38,
enter: 13
};
export default {
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
hideOptions: true,
filteredOptions: [],
optionIndex: 0,
field: ''
};
},
mounted() {
this.options = this.model.options;
this.autocompleteInputElement = this.$el.getElementsByClassName('autocompleteInput')[0];
if (this.options[0].name) {
// If "options" include name, value pair
this.optionNames = this.options.map((opt) => {
return opt.name;
});
} else {
// If options is only an array of string.
this.optionNames = this.options;
}
},
methods: {
fillInputWithIndexedOption() {
if (this.filteredOptions[this.optionIndex]) {
this.ngModel[this.field] = this.filteredOptions[this.optionIndex].name;
}
},
decrementOptionIndex() {
if (this.optionIndex === 0) {
this.optionIndex = this.filteredOptions.length;
}
this.optionIndex--;
this.fillInputWithIndexedOption();
},
incrementOptionIndex() {
if (this.optionIndex === this.filteredOptions.length - 1) {
this.optionIndex = -1;
}
this.optionIndex++;
this.fillInputWithIndexedOption();
},
fillInputWithString(string) {
this.hideOptions = true;
this.ngModel[this.field] = string;
},
showOptions(string) {
this.hideOptions = false;
this.filterOptions(string);
this.optionIndex = 0;
},
keyDown($event) {
if (this.filteredOptions) {
let keyCode = $event.keyCode;
switch (keyCode) {
case key.down:
this.incrementOptionIndex();
break;
case key.up:
$event.preventDefault(); // Prevents cursor jumping back and forth
this.decrementOptionIndex();
break;
case key.enter:
if (this.filteredOptions[this.optionIndex]) {
this.fillInputWithString(this.filteredOptions[this.optionIndex].name);
}
}
}
},
filterOptions() {
this.hideOptions = false;
this.filteredOptions = this.optionNames.filter((option) => {
return option.toLowerCase().indexOf(this.field.toLowerCase()) >= 0;
}).map((option, index) => {
return {
optionId: index,
name: option
};
});
},
inputClicked() {
this.autocompleteInputElement.select();
this.showOptions(this.autocompleteInputElement.value);
},
arrowClicked() {
this.autocompleteInputElement.select();
this.showOptions('');
},
fillInput(string) {
this.fillInputWithString(string);
},
optionMouseover(optionId) {
this.optionIndex = optionId;
}
}
};
</script>

View File

@@ -0,0 +1,46 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<span>
<composite-item v-for="(item, index) in model.items"
:key="item.name"
:first="index < 1"
:item="item"
/>
</span>
</template>
<script>
import CompositeItem from "@/api/forms/components/controls/CompositeItem.vue";
export default {
components: {
CompositeItem
},
props: {
model: {
type: Object,
required: true
}
}
};
</script>

View File

@@ -0,0 +1,57 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div :class="compositeCssClass">
<form-row :css-class="item.cssClass"
:first="first"
:row="item"
/>
<span class="composite-control-label">
{{ item.name }}
</span>
</div>
</template>
<script>
import FormRow from "@/api/forms/components/FormRow.vue";
export default {
components: {
FormRow
},
props: {
item: {
type: Object,
required: true
},
first: {
type: Boolean,
required: true
}
},
computed: {
compositeCssClass() {
return `l-composite-control l-${this.item.control}`;
}
}
};
</script>

View File

@@ -1,5 +1,5 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
@@ -20,44 +20,38 @@
at runtime from the About dialog for additional information.
-->
<template>
<div>
<div v-if="canEdit">
<plot-options-edit />
</div>
<div v-else>
<plot-options-browse />
</div>
</div>
<span class="form-control shell">
<span class="field control"
:class="model.cssClass"
>
<input v-model="field"
type="number"
:min="model.min"
:max="model.max"
:step="model.step"
@blur="blur()"
>
<!-- ng-pattern="ngPattern"-->
</span>
</span>
</template>
<script>
import PlotOptionsBrowse from "./PlotOptionsBrowse.vue";
import PlotOptionsEdit from "./PlotOptionsEdit.vue";
export default {
components: {
PlotOptionsBrowse,
PlotOptionsEdit
},
inject: ['openmct', 'domainObject'],
data() {
return {
isEditing: this.openmct.editor.isEditing()
};
},
computed: {
canEdit() {
return this.isEditing && !this.domainObject.locked;
props: {
model: {
type: Object,
required: true
}
},
mounted() {
this.openmct.editor.on('isEditing', this.setEditState);
},
beforeDestroy() {
this.openmct.editor.off('isEditing', this.setEditState);
data() {
return {
field: ''
};
},
methods: {
setEditState(isEditing) {
this.isEditing = isEditing;
blur() {
}
}
};

View File

@@ -0,0 +1,56 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<span class="form-control shell">
<span class="field control"
:class="model.cssClass"
>
<input v-model="field"
type="text"
:size="model.size"
@blur="blur()"
>
<!-- ng-pattern="ngPattern"-->
</span>
</span>
</template>
<script>
export default {
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
field: ''
};
},
methods: {
blur() {
}
}
};
</script>

View File

@@ -76,10 +76,7 @@ class MutableDomainObject {
}
$set(path, value) {
_.set(this, path, value);
if (path !== 'persisted' && path !== 'modified') {
_.set(this, 'modified', Date.now());
}
_.set(this, 'modified', Date.now());
//Emit secret synchronization event first, so that all objects are in sync before subsequent events fired.
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this);
@@ -115,11 +112,9 @@ class MutableDomainObject {
return () => this._instanceEventEmitter.off(event, callback);
}
$destroy() {
while (this._observers.length > 0) {
const observer = this._observers.pop();
observer();
}
this._observers.forEach(observer => observer());
delete this._globalEventEmitter;
delete this._observers;
this._instanceEventEmitter.emit('$_destroy');
}

View File

@@ -161,7 +161,6 @@ ObjectAPI.prototype.addProvider = function (namespace, provider) {
ObjectAPI.prototype.get = function (identifier, abortSignal) {
let keystring = this.makeKeyString(identifier);
if (this.cache[keystring] !== undefined) {
return this.cache[keystring];
}
@@ -177,16 +176,15 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
throw new Error('Provider does not support get!');
}
let objectPromise = provider.get(identifier, abortSignal).then(result => {
let objectPromise = provider.get(identifier, abortSignal);
this.cache[keystring] = objectPromise;
return objectPromise.then(result => {
delete this.cache[keystring];
result = this.applyGetInterceptors(identifier, result);
return result;
});
this.cache[keystring] = objectPromise;
return objectPromise;
};
/**
@@ -486,12 +484,6 @@ ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) {
});
};
ObjectAPI.prototype.isObjectPathToALink = function (domainObject, objectPath) {
return objectPath !== undefined
&& objectPath.length > 1
&& domainObject.location !== this.makeKeyString(objectPath[1].identifier);
};
/**
* Uniquely identifies a domain object.
*
@@ -528,10 +520,8 @@ ObjectAPI.prototype.isObjectPathToALink = function (domainObject, objectPath) {
*/
function hasAlreadyBeenPersisted(domainObject) {
const result = domainObject.persisted !== undefined
&& domainObject.persisted >= domainObject.modified;
return result;
return domainObject.persisted !== undefined
&& domainObject.persisted === domainObject.modified;
}
export default ObjectAPI;

View File

@@ -40,7 +40,6 @@ const DEFAULTS = [
'platform/features/clock',
'platform/features/hyperlink',
'platform/features/timeline',
'platform/forms',
'platform/identity',
'platform/persistence/aggregator',
'platform/persistence/queue',
@@ -85,11 +84,11 @@ define([
'../platform/features/hyperlink/bundle',
'../platform/features/static-markup/bundle',
'../platform/features/timeline/bundle',
'../platform/forms/bundle',
'../platform/framework/bundle',
'../platform/framework/src/load/Bundle',
'../platform/identity/bundle',
'../platform/persistence/aggregator/bundle',
'../platform/persistence/couch/bundle',
'../platform/persistence/elastic/bundle',
'../platform/persistence/local/bundle',
'../platform/persistence/queue/bundle',

View File

@@ -43,16 +43,12 @@ export default function LADTableSetViewProvider(openmct) {
components: {
LadTableSet: LadTableSet
},
data() {
return {
domainObject
};
},
provide: {
openmct,
domainObject,
objectPath
},
template: '<lad-table-set :domain-object="domainObject"></lad-table-set>'
template: '<lad-table-set></lad-table-set>'
});
},
destroy: function (element) {

View File

@@ -56,7 +56,7 @@ export default {
type: Object,
required: true
},
pathToTable: {
objectPath: {
type: Array,
required: true
},
@@ -66,19 +66,20 @@ export default {
}
},
data() {
let currentObjectPath = this.objectPath.slice();
currentObjectPath.unshift(this.domainObject);
return {
timestamp: undefined,
value: '---',
valueClass: '',
currentObjectPath,
unit: ''
};
},
computed: {
formattedTimestamp() {
return this.timestamp !== undefined ? this.getFormattedTimestamp(this.timestamp) : '---';
},
objectPath() {
return [this.domainObject, ...this.pathToTable];
}
},
mounted() {
@@ -181,7 +182,7 @@ export default {
};
},
showContextMenu(event) {
let actionCollection = this.openmct.actions.get(this.objectPath, this.getView());
let actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView());
let allActions = actionCollection.getActionsObject();
let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]);

View File

@@ -33,10 +33,10 @@
</thead>
<tbody>
<lad-row
v-for="ladRow in items"
:key="ladRow.key"
:domain-object="ladRow.domainObject"
:path-to-table="objectPath"
v-for="item in items"
:key="item.key"
:domain-object="item.domainObject"
:object-path="objectPath"
:has-units="hasUnits"
/>
</tbody>

View File

@@ -43,10 +43,9 @@
</td>
</tr>
<lad-row
v-for="ladRow in ladTelemetryObjects[ladTable.key]"
:key="ladRow.key"
:domain-object="ladRow.domainObject"
:path-to-table="ladTable.objectPath"
v-for="telemetryObject in ladTelemetryObjects[ladTable.key]"
:key="telemetryObject.key"
:domain-object="telemetryObject.domainObject"
:has-units="hasUnits"
/>
</template>
@@ -61,13 +60,7 @@ export default {
components: {
LadRow
},
inject: ['openmct', 'objectPath'],
props: {
domainObject: {
type: Object,
required: true
}
},
inject: ['openmct', 'domainObject'],
data() {
return {
ladTableObjects: [],
@@ -113,7 +106,6 @@ export default {
let ladTable = {};
ladTable.domainObject = domainObject;
ladTable.key = this.openmct.objects.makeKeyString(domainObject.identifier);
ladTable.objectPath = [domainObject, ...this.objectPath];
this.$set(this.ladTelemetryObjects, ladTable.key, []);
this.ladTableObjects.push(ladTable);

View File

@@ -82,9 +82,7 @@ export default class Condition extends EventEmitter {
if (this.isAnyOrAllTelemetry(criterion)) {
criterion.updateResult(datum, this.conditionManager.telemetryObjects);
} else {
if (criterion.usesTelemetry(datum.id)) {
criterion.updateResult(datum);
}
criterion.updateResult(datum);
}
});
@@ -104,7 +102,7 @@ export default class Condition extends EventEmitter {
isTelemetryUsed(id) {
return this.criteria.some(criterion => {
return this.isAnyOrAllTelemetry(criterion) || criterion.usesTelemetry(id);
return this.isAnyOrAllTelemetry(criterion) || criterion.telemetryObjectIdAsString === id;
});
}
@@ -272,11 +270,11 @@ export default class Condition extends EventEmitter {
}
}
requestLADConditionResult(options) {
requestLADConditionResult() {
let latestTimestamp;
let criteriaResults = {};
const criteriaRequests = this.criteria
.map(criterion => criterion.requestLAD(this.conditionManager.telemetryObjects, options));
.map(criterion => criterion.requestLAD(this.conditionManager.telemetryObjects));
return Promise.all(criteriaRequests)
.then(results => {

View File

@@ -282,7 +282,7 @@ export default class ConditionManager extends EventEmitter {
return currentCondition;
}
requestLADConditionSetOutput(options) {
requestLADConditionSetOutput() {
if (!this.conditions.length) {
return Promise.resolve([]);
}
@@ -291,7 +291,7 @@ export default class ConditionManager extends EventEmitter {
let latestTimestamp;
let conditionResults = {};
const conditionRequests = this.conditions
.map(condition => condition.requestLADConditionResult(options));
.map(condition => condition.requestLADConditionResult());
return Promise.all(conditionRequests)
.then((results) => {

View File

@@ -40,10 +40,10 @@ export default class ConditionSetTelemetryProvider {
return domainObject.type === 'conditionSet';
}
request(domainObject, options) {
request(domainObject) {
let conditionManager = this.getConditionManager(domainObject);
return conditionManager.requestLADConditionSetOutput(options)
return conditionManager.requestLADConditionSetOutput()
.then(latestOutput => {
return latestOutput;
});
@@ -52,9 +52,7 @@ export default class ConditionSetTelemetryProvider {
subscribe(domainObject, callback) {
let conditionManager = this.getConditionManager(domainObject);
conditionManager.on('conditionSetResultUpdated', (data) => {
callback(data);
});
conditionManager.on('conditionSetResultUpdated', callback);
return this.destroyConditionManager.bind(this, this.openmct.objects.makeKeyString(domainObject.identifier));
}

View File

@@ -35,7 +35,6 @@ export default class StyleRuleManager extends EventEmitter {
if (styleConfiguration) {
this.initialize(styleConfiguration);
if (styleConfiguration.conditionSetIdentifier) {
this.openmct.time.on("bounds", this.refreshData.bind(this));
this.subscribeToConditionSet();
} else {
this.applyStaticStyle();
@@ -84,25 +83,6 @@ export default class StyleRuleManager extends EventEmitter {
});
}
refreshData(bounds, isTick) {
if (!isTick) {
let options = {
start: bounds.start,
end: bounds.end,
size: 1,
strategy: 'latest'
};
this.openmct.objects.get(this.conditionSetIdentifier).then((conditionSetDomainObject) => {
this.openmct.telemetry.request(conditionSetDomainObject, options)
.then(output => {
if (output && output.length) {
this.handleConditionSetResultUpdated(output[0]);
}
});
});
}
}
updateObjectStyleConfig(styleConfiguration) {
if (!styleConfiguration || !styleConfiguration.conditionSetIdentifier) {
this.initialize(styleConfiguration || {});
@@ -180,14 +160,10 @@ export default class StyleRuleManager extends EventEmitter {
destroy() {
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
delete this.stopProvidingTelemetry;
}
this.openmct.time.off("bounds", this.refreshData);
this.openmct.editor.off('isEditing', this.toggleSubscription);
this.conditionSetIdentifier = undefined;
}

View File

@@ -344,11 +344,6 @@ export default {
const layoutItem = selectionItem[0].context.layoutItem;
const isChildItem = selectionItem.length > 1;
if (!item && !layoutItem) {
// cases where selection is used for table cells
return;
}
if (!isChildItem) {
domainObject = item;
itemStyle = getApplicableStylesForItem(item);

View File

@@ -147,16 +147,12 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
}
requestLAD(telemetryObjects, requestOptions) {
let options = {
requestLAD(telemetryObjects) {
const options = {
strategy: 'latest',
size: 1
};
if (requestOptions !== undefined) {
options = Object.assign(options, requestOptions);
}
if (!this.isValid()) {
return this.formatData({}, telemetryObjects);
}

View File

@@ -58,10 +58,6 @@ export default class TelemetryCriterion extends EventEmitter {
}
}
usesTelemetry(id) {
return this.telemetryObjectIdAsString && (this.telemetryObjectIdAsString === id);
}
subscribeForStaleData() {
if (this.stalenessSubscription) {
this.stalenessSubscription.clear();
@@ -137,16 +133,12 @@ export default class TelemetryCriterion extends EventEmitter {
}
}
requestLAD(telemetryObjects, requestOptions) {
let options = {
requestLAD() {
const options = {
strategy: 'latest',
size: 1
};
if (requestOptions !== undefined) {
options = Object.assign(options, requestOptions);
}
if (!this.isValid()) {
return {
id: this.id,

View File

@@ -104,7 +104,7 @@ export function getConsolidatedStyleValues(multipleItemStyles) {
const properties = Object.keys(styleProps);
properties.forEach((property) => {
const values = aggregatedStyleValues[property];
if (values && values.length) {
if (values.length) {
if (values.every(value => value === values[0])) {
styleValues[property] = values[0];
} else {

View File

@@ -235,7 +235,7 @@ define(['lodash'], function (_) {
message: `Warning! This action will remove this item from the Display Layout. Do you want to continue?`,
buttons: [
{
label: 'OK',
label: 'Ok',
emphasis: 'true',
callback: function () {
removeItem(getAllTypes(selection));

View File

@@ -147,7 +147,7 @@ export default {
this.mutablePromise.then(() => {
this.openmct.objects.destroyMutable(this.domainObject);
});
} else if (this.domainObject.isMutable) {
} else {
this.openmct.objects.destroyMutable(this.domainObject);
}
},

View File

@@ -240,7 +240,7 @@ export default {
this.mutablePromise.then(() => {
this.openmct.objects.destroyMutable(this.domainObject);
});
} else if (this.domainObject.isMutable) {
} else {
this.openmct.objects.destroyMutable(this.domainObject);
}
},
@@ -269,12 +269,7 @@ export default {
},
subscribeToObject() {
this.subscription = this.openmct.telemetry.subscribe(this.domainObject, function (datum) {
const key = this.openmct.time.timeSystem().key;
const datumTimeStamp = datum[key];
if (this.openmct.time.clock() !== undefined
|| (datumTimeStamp
&& (this.openmct.time.bounds().end >= datumTimeStamp))
) {
if (this.openmct.time.clock() !== undefined) {
this.updateView(datum);
}
}.bind(this));

View File

@@ -90,12 +90,14 @@ export default {
this.composition.load();
this.unobserve = this.openmct.objects.observe(this.providedObject, 'configuration.filters', this.updatePersistedFilters);
this.unobserveGlobalFilters = this.openmct.objects.observe(this.providedObject, 'configuration.globalFilters', this.updateGlobalFilters);
this.unobserveAllMutation = this.openmct.objects.observe(this.providedObject, '*', (mutatedObject) => this.providedObject = mutatedObject);
},
beforeDestroy() {
this.composition.off('add', this.addChildren);
this.composition.off('remove', this.removeChildren);
this.unobserve();
this.unobserveGlobalFilters();
this.unobserveAllMutation();
},
methods: {
addChildren(domainObject) {
@@ -156,28 +158,25 @@ export default {
},
getGlobalFiltersToRemove(keyString) {
let filtersToRemove = new Set();
const child = this.children[keyString];
if (child && child.metadataWithFilters) {
const metadataWithFilters = child.metadataWithFilters;
metadataWithFilters.forEach(metadatum => {
let keepFilter = false;
Object.keys(this.children).forEach(childKeyString => {
if (childKeyString !== keyString) {
let filterMatched = this.children[childKeyString].metadataWithFilters.some(childMetadatum => childMetadatum.key === metadatum.key);
if (filterMatched) {
keepFilter = true;
this.children[keyString].metadataWithFilters.forEach(metadatum => {
let keepFilter = false;
Object.keys(this.children).forEach(childKeyString => {
if (childKeyString !== keyString) {
let filterMatched = this.children[childKeyString].metadataWithFilters.some(childMetadatum => childMetadatum.key === metadatum.key);
return;
}
if (filterMatched) {
keepFilter = true;
return;
}
});
if (!keepFilter) {
filtersToRemove.add(metadatum.key);
}
});
}
if (!keepFilter) {
filtersToRemove.add(metadatum.key);
}
});
return Array.from(filtersToRemove);
},

View File

@@ -97,7 +97,7 @@ function ToolbarProvider(openmct) {
message: `This action will remove this frame from this Flexible Layout. Do you want to continue?`,
buttons: [
{
label: 'OK',
label: 'Ok',
emphasis: 'true',
callback: function () {
deleteFrameAction(primary.context.frameId);
@@ -162,7 +162,7 @@ function ToolbarProvider(openmct) {
message: 'This action will permanently delete this container from this Flexible Layout',
buttons: [
{
label: 'OK',
label: 'Ok',
emphasis: 'true',
callback: function () {
removeContainer(containerId);

View File

@@ -23,7 +23,7 @@
<template>
<div
class="c-compass"
:style="`width: ${ sizedImageDimensions.width }px; height: ${ sizedImageDimensions.height }px`"
:style="compassDimensionsStyle"
>
<CompassHUD
v-if="hasCameraFieldOfView"
@@ -32,9 +32,8 @@
:camera-pan="cameraPan"
/>
<CompassRose
v-if="true"
v-if="hasCameraFieldOfView"
:heading="heading"
:sized-image-width="sizedImageDimensions.width"
:sun-heading="sunHeading"
:camera-angle-of-view="cameraAngleOfView"
:camera-pan="cameraPan"
@@ -78,20 +77,6 @@ export default {
}
},
computed: {
sizedImageDimensions() {
let sizedImageDimensions = {};
if ((this.containerWidth / this.containerHeight) > this.naturalAspectRatio) {
// container is wider than image
sizedImageDimensions.width = this.containerHeight * this.naturalAspectRatio;
sizedImageDimensions.height = this.containerHeight;
} else {
// container is taller than image
sizedImageDimensions.width = this.containerWidth;
sizedImageDimensions.height = this.containerWidth * this.naturalAspectRatio;
}
return sizedImageDimensions;
},
hasCameraFieldOfView() {
return this.cameraPan !== undefined && this.cameraAngleOfView > 0;
},
@@ -109,6 +94,25 @@ export default {
},
cameraAngleOfView() {
return CAMERA_ANGLE_OF_VIEW;
},
compassDimensionsStyle() {
const containerAspectRatio = this.containerWidth / this.containerHeight;
let width;
let height;
if (containerAspectRatio < this.naturalAspectRatio) {
width = '100%';
height = `${ this.containerWidth / this.naturalAspectRatio }px`;
} else {
width = `${ this.containerHeight * this.naturalAspectRatio }px`;
height = '100%';
}
return {
width: width,
height: height
};
}
},
methods: {

View File

@@ -22,134 +22,129 @@
<template>
<div
class="w-direction-rose"
:class="compassRoseSizingClasses"
class="c-direction-rose"
@click="toggleLockCompass"
>
<div
class="c-direction-rose"
@click="toggleLockCompass"
class="c-nsew"
:style="compassRoseStyle"
>
<div
class="c-nsew"
:style="compassRoseStyle"
<svg
class="c-nsew__minor-ticks"
viewBox="0 0 100 100"
>
<svg
class="c-nsew__minor-ticks"
viewBox="0 0 100 100"
>
<rect
class="c-nsew__tick c-tick-ne"
x="49"
y="0"
width="2"
height="5"
/>
<rect
class="c-nsew__tick c-tick-se"
x="95"
y="49"
width="5"
height="2"
/>
<rect
class="c-nsew__tick c-tick-sw"
x="49"
y="95"
width="2"
height="5"
/>
<rect
class="c-nsew__tick c-tick-nw"
x="0"
y="49"
width="5"
height="2"
/>
<rect
class="c-nsew__tick c-tick-ne"
x="49"
y="0"
width="2"
height="5"
/>
<rect
class="c-nsew__tick c-tick-se"
x="95"
y="49"
width="5"
height="2"
/>
<rect
class="c-nsew__tick c-tick-sw"
x="49"
y="95"
width="2"
height="5"
/>
<rect
class="c-nsew__tick c-tick-nw"
x="0"
y="49"
width="5"
height="2"
/>
</svg>
</svg>
<svg
class="c-nsew__ticks"
viewBox="0 0 100 100"
>
<polygon
class="c-nsew__tick c-tick-n"
points="50,0 60,10 40,10"
/>
<rect
class="c-nsew__tick c-tick-e"
x="95"
y="49"
width="5"
height="2"
/>
<rect
class="c-nsew__tick c-tick-w"
x="0"
y="49"
width="5"
height="2"
/>
<rect
class="c-nsew__tick c-tick-s"
x="49"
y="95"
width="2"
height="5"
/>
<svg
class="c-nsew__ticks"
viewBox="0 0 100 100"
>
<polygon
class="c-nsew__tick c-tick-n"
points="50,0 57,5 43,5"
/>
<rect
class="c-nsew__tick c-tick-e"
x="95"
y="49"
width="5"
height="2"
/>
<rect
class="c-nsew__tick c-tick-w"
x="0"
y="49"
width="5"
height="2"
/>
<rect
class="c-nsew__tick c-tick-s"
x="49"
y="95"
width="2"
height="5"
/>
<text
class="c-nsew__label c-label-n"
text-anchor="middle"
:transform="northTextTransform"
>N</text>
<text
class="c-nsew__label c-label-e"
text-anchor="middle"
:transform="eastTextTransform"
>E</text>
<text
class="c-nsew__label c-label-w"
text-anchor="middle"
:transform="southTextTransform"
>W</text>
<text
class="c-nsew__label c-label-s"
text-anchor="middle"
:transform="westTextTransform"
>S</text>
</svg>
<text
class="c-nsew__label c-label-n"
text-anchor="middle"
:transform="northTextTransform"
>N</text>
<text
class="c-nsew__label c-label-e"
text-anchor="middle"
:transform="eastTextTransform"
>E</text>
<text
class="c-nsew__label c-label-w"
text-anchor="middle"
:transform="southTextTransform"
>W</text>
<text
class="c-nsew__label c-label-s"
text-anchor="middle"
:transform="westTextTransform"
>S</text>
</svg>
</div>
<div
v-if="hasHeading"
class="c-spacecraft-body"
:style="headingStyle"
>
</div>
<div
v-if="hasSunHeading"
class="c-sun"
:style="sunHeadingStyle"
></div>
<div
class="c-cam-field"
:style="cameraPanStyle"
>
<div class="cam-field-half cam-field-half-l">
<div
class="cam-field-area"
:style="cameraFOVStyleLeftHalf"
></div>
</div>
<div
v-if="hasHeading"
class="c-spacecraft-body"
:style="headingStyle"
>
</div>
<div
v-if="hasSunHeading"
class="c-sun"
:style="sunHeadingStyle"
></div>
<div
class="c-cam-field"
:style="cameraPanStyle"
>
<div class="cam-field-half cam-field-half-l">
<div
class="cam-field-area"
:style="cameraFOVStyleLeftHalf"
></div>
</div>
<div class="cam-field-half cam-field-half-r">
<div
class="cam-field-area"
:style="cameraFOVStyleRightHalf"
></div>
</div>
<div class="cam-field-half cam-field-half-r">
<div
class="cam-field-area"
:style="cameraFOVStyleRightHalf"
></div>
</div>
</div>
</div>
@@ -160,10 +155,6 @@ import { rotate } from './utils';
export default {
props: {
sizedImageWidth: {
type: Number,
required: true
},
heading: {
type: Number,
required: true
@@ -186,24 +177,12 @@ export default {
}
},
computed: {
compassRoseSizingClasses() {
let compassRoseSizingClasses = '';
if (this.sizedImageWidth < 300) {
compassRoseSizingClasses = '--rose-small --rose-min';
} else if (this.sizedImageWidth < 500) {
compassRoseSizingClasses = '--rose-small';
} else if (this.sizedImageWidth > 1000) {
compassRoseSizingClasses = '--rose-max';
}
return compassRoseSizingClasses;
north() {
return this.lockCompass ? rotate(-this.cameraPan) : 0;
},
compassRoseStyle() {
return { transform: `rotate(${ this.north }deg)` };
},
north() {
return this.lockCompass ? rotate(-this.cameraPan) : 0;
},
northTextTransform() {
return this.cardinalPointsTextTransform.north;
},
@@ -225,10 +204,10 @@ export default {
const rotation = `rotate(${ -this.north })`;
return {
north: `translate(50,23) ${ rotation }`,
east: `translate(82,50) ${ rotation }`,
south: `translate(18,50) ${ rotation }`,
west: `translate(50,82) ${ rotation }`
north: `translate(50,15) ${ rotation }`,
east: `translate(87,50) ${ rotation }`,
south: `translate(13,50) ${ rotation }`,
west: `translate(50,87) ${ rotation }`
};
},
hasHeading() {

View File

@@ -10,7 +10,6 @@ $elemBg: rgba(black, 0.7);
}
.c-compass {
pointer-events: none; // This allows the image element to receive a browser-level context click
position: absolute;
left: 50%;
top: 50%;
@@ -21,253 +20,195 @@ $elemBg: rgba(black, 0.7);
/***************************** COMPASS HUD */
.c-hud {
// To be placed within a imagery view, in the bounding box of the image
$m: 1px;
$padTB: 2px;
$padLR: $padTB;
color: $interfaceKeyColor;
font-size: 0.8em;
// To be placed within a imagery view, in the bounding box of the image
$m: 1px;
$padTB: 2px;
$padLR: $padTB;
color: $interfaceKeyColor;
font-size: 0.8em;
position: absolute;
top: $m; right: $m; left: $m;
height: 18px;
svg, div {
position: absolute;
top: $m;
right: $m;
left: $m;
height: 18px;
}
svg, div {
position: absolute;
}
&__display {
height: 30px;
pointer-events: all;
position: absolute;
top: 0;
right: 0;
left: 0;
}
&__display {
height: 30px;
pointer-events: all;
position: absolute;
top: 0;
right: 0;
left: 0;
}
&__range {
border: 1px solid $interfaceKeyColor;
border-top-color: transparent;
position: absolute;
top: 50%; right: $padLR; bottom: $padTB; left: $padLR;
}
&__range {
border: 1px solid $interfaceKeyColor;
border-top-color: transparent;
position: absolute;
top: 50%;
right: $padLR;
bottom: $padTB;
left: $padLR;
}
[class*="__dir"] {
// NSEW
display: inline-block;
font-weight: bold;
text-shadow: 0 1px 2px black;
top: 50%;
transform: translate(-50%,-50%);
z-index: 2;
}
[class*="__dir"] {
// NSEW
display: inline-block;
font-weight: bold;
text-shadow: 0 1px 2px black;
top: 50%;
transform: translate(-50%, -50%);
z-index: 2;
}
[class*="__dir--sub"] {
font-weight: normal;
opacity: 0.5;
}
[class*="__dir--sub"] {
font-weight: normal;
opacity: 0.5;
}
&__sun {
$s: 10px;
@include sun('circle farthest-side at bottom');
bottom: $padTB + 2px;
height: $s;
width: $s*2;
opacity: 0.8;
transform: translateX(-50%);
z-index: 1;
}
&__sun {
$s: 10px;
@include sun('circle farthest-side at bottom');
bottom: $padTB + 2px;
height: $s; width: $s*2;
opacity: 0.8;
transform: translateX(-50%);
z-index: 1;
}
}
/***************************** COMPASS DIRECTIONS */
.c-nsew {
$color: $interfaceKeyColor;
$inset: 5%;
$tickHeightPerc: 15%;
text-shadow: black 0 0 10px;
top: $inset;
right: $inset;
bottom: $inset;
left: $inset;
z-index: 3;
$color: $interfaceKeyColor;
$inset: 7%;
$tickHeightPerc: 15%;
text-shadow: black 0 0 10px;
top: $inset; right: $inset; bottom: $inset; left: $inset;
z-index: 3;
&__tick,
&__label {
fill: $color;
}
&__tick,
&__label {
fill: $color;
}
&__minor-ticks {
opacity: 0.5;
transform-origin: center;
transform: rotate(45deg);
}
&__minor-ticks {
opacity: 0.5;
transform-origin: center;
transform: rotate(45deg);
}
&__label {
dominant-baseline: central;
font-size: 1.25em;
font-weight: bold;
}
&__label {
dominant-baseline: central;
font-size: 0.8em;
font-weight: bold;
}
.c-label-n {
font-size: 2em;
}
.c-label-n {
font-size: 1.1em;
}
}
/***************************** CAMERA FIELD ANGLE */
.c-cam-field {
$color: white;
opacity: 0.3;
$color: white;
opacity: 0.2;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
.cam-field-half {
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
.cam-field-half {
top: 0;
right: 0;
bottom: 0;
left: 0;
.cam-field-area {
background: $color;
top: -30%;
right: 0;
bottom: -30%;
left: 0;
}
// clip-paths overlap a bit to avoid a gap between halves
&-l {
clip-path: polygon(0 0, 50.5% 0, 50.5% 100%, 0 100%);
.cam-field-area {
transform-origin: left center;
}
}
&-r {
clip-path: polygon(49.5% 0, 100% 0, 100% 100%, 49.5% 100%);
.cam-field-area {
transform-origin: right center;
}
}
.cam-field-area {
background: $color;
top: -30%;
right: 0;
bottom: -30%;
left: 0;
}
// clip-paths overlap a bit to avoid a gap between halves
&-l {
clip-path: polygon(0 0, 50.5% 0, 50.5% 100%, 0 100%);
.cam-field-area {
transform-origin: left center;
}
}
&-r {
clip-path: polygon(49.5% 0, 100% 0, 100% 100%, 49.5% 100%);
.cam-field-area {
transform-origin: right center;
}
}
}
}
/***************************** SPACECRAFT BODY */
.c-spacecraft-body {
$color: $interfaceKeyColor;
$s: 30%;
background: $color;
border-radius: 3px;
height: $s;
width: $s;
left: 50%;
top: 50%;
opacity: 0.4;
transform-origin: center top;
transform: translateX(-50%); // center by default, overridden by CompassRose.vue / headingStyle()
$color: $interfaceKeyColor;
$s: 30%;
background: $color;
border-radius: 3px;
height: $s; width: $s;
left: 50%; top: 50%;
opacity: 0.4;
transform-origin: center top;
&:before {
// Direction arrow
$color: rgba(black, 0.5);
$arwPointerY: 60%;
$arwBodyOffset: 25%;
background: $color;
content: '';
display: block;
position: absolute;
top: 10%;
right: 20%;
bottom: 50%;
left: 20%;
clip-path: polygon(50% 0, 100% $arwPointerY, 100%-$arwBodyOffset $arwPointerY, 100%-$arwBodyOffset 100%, $arwBodyOffset 100%, $arwBodyOffset $arwPointerY, 0 $arwPointerY);
}
&:before {
// Direction arrow
$color: rgba(black, 0.5);
$arwPointerY: 60%;
$arwBodyOffset: 25%;
background: $color;
content: '';
display: block;
position: absolute;
top: 10%; right: 20%; bottom: 50%; left: 20%;
clip-path: polygon(50% 0, 100% $arwPointerY, 100%-$arwBodyOffset $arwPointerY, 100%-$arwBodyOffset 100%, $arwBodyOffset 100%, $arwBodyOffset $arwPointerY, 0 $arwPointerY);
}
}
/***************************** DIRECTION ROSE */
.w-direction-rose {
$s: 10%;
$m: 2%;
position: absolute;
bottom: $m;
left: $m;
width: $s;
padding-top: $s;
&.--rose-min {
$s: 30px;
width: $s;
padding-top: $s;
}
&.--rose-small {
.c-nsew__minor-ticks,
.c-tick-w,
.c-tick-s,
.c-tick-e,
.c-label-w,
.c-label-s,
.c-label-e {
display: none;
}
.c-label-n {
font-size: 2.5em;
}
}
&.--rose-max {
$s: 100px;
width: $s;
padding-top: $s;
}
}
.c-direction-rose {
$c2: rgba(white, 0.1);
background: $elemBg;
background-image: radial-gradient(circle closest-side, transparent, transparent 80%, $c2);
transform-origin: 0 0;
$d: 100px;
$c2: rgba(white, 0.1);
background: $elemBg;
background-image: radial-gradient(circle closest-side, transparent, transparent 80%, $c2);
width: $d;
height: $d;
transform-origin: 0 0;
position: absolute;
bottom: 10px; left: 10px;
clip-path: circle(50% at 50% 50%);
border-radius: 100%;
svg, div {
position: absolute;
}
// Sun
.c-sun {
top: 0;
right: 0;
bottom: 0;
left: 0;
clip-path: circle(50% at 50% 50%);
border-radius: 100%;
pointer-events: all;
svg, div {
position: absolute;
}
// Sun
.c-sun {
top: 0;
right: 0;
bottom: 0;
left: 0;
&:before {
$s: 35%;
@include sun();
content: '';
display: block;
position: absolute;
opacity: 0.7;
top: 0;
left: 50%;
height: $s;
width: $s;
transform: translate(-50%, -60%);
}
&:before {
$s: 35%;
@include sun();
content: '';
display: block;
position: absolute;
opacity: 0.7;
top: 0; left: 50%;
height:$s; width: $s;
transform: translate(-50%, -60%);
}
}
}

View File

@@ -135,14 +135,9 @@
:class="{ selected: focusedImageIndex === index && isPaused }"
@click="setFocusedImage(index, thumbnailClick)"
>
<a href=""
:download="image.imageDownloadName"
@click.prevent
<img class="c-thumb__image"
:src="image.url"
>
<img class="c-thumb__image"
:src="image.url"
>
</a>
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
</div>
</div>
@@ -223,9 +218,6 @@ export default {
canTrackDuration() {
return this.openmct.time.clock() && this.timeSystem.isUTCBased;
},
focusedImageDownloadName() {
return this.getImageDownloadName(this.focusedImage);
},
isNextDisabled() {
let disabled = false;
@@ -353,7 +345,6 @@ export default {
this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] };
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]};
// related telemetry keys
this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ'];
@@ -541,15 +532,6 @@ export default {
// Replace ISO "T" with a space to allow wrapping
return dateTimeStr.replace("T", " ");
},
getImageDownloadName(datum) {
let imageDownloadName = '';
if (datum) {
const key = this.imageDownloadNameHints.key;
imageDownloadName = datum[key];
}
return imageDownloadName;
},
parseTime(datum) {
if (!datum) {
return;
@@ -673,7 +655,6 @@ export default {
image.formattedTime = this.formatTime(datum);
image.url = this.formatImageUrl(datum);
image.time = datum[this.timeKey];
image.imageDownloadName = this.getImageDownloadName(datum);
this.imageHistory.push(image);
@@ -796,9 +777,6 @@ export default {
this.focusedImageNaturalAspectRatio = undefined;
const img = this.$refs.focusedImage;
if (!img) {
return;
}
// TODO - should probably cache this
img.addEventListener('load', () => {

View File

@@ -26,7 +26,6 @@ import CouchObjectQueue from "./CouchObjectQueue";
const REV = "_rev";
const ID = "_id";
const HEARTBEAT = 50000;
const ALL_DOCS = "_all_docs?include_docs=true";
export default class CouchObjectProvider {
// options {
@@ -42,8 +41,6 @@ export default class CouchObjectProvider {
this.objectQueue = {};
this.observeEnabled = options.disableObserve !== true;
this.observers = {};
this.batchIds = [];
if (this.observeEnabled) {
this.observeObjectChanges(options.filter);
}
@@ -70,9 +67,6 @@ export default class CouchObjectProvider {
// stringify body if needed
if (fetchOptions.body) {
fetchOptions.body = JSON.stringify(fetchOptions.body);
fetchOptions.headers = {
"Content-Type": "application/json"
};
}
return fetch(this.url + '/' + subPath, fetchOptions)
@@ -84,18 +78,14 @@ export default class CouchObjectProvider {
});
}
/**
* Check the response to a create/update/delete request;
* track the rev if it's valid, otherwise return false to
* indicate that the request failed.
* persist any queued objects
* @private
*/
checkResponse(response, intermediateResponse, key) {
// Check the response to a create/update/delete request;
// track the rev if it's valid, otherwise return false to
// indicate that the request failed.
// persist any queued objects
checkResponse(response, intermediateResponse) {
let requestSuccess = false;
const id = response ? response.id : undefined;
let rev;
if (response && response.ok) {
rev = response.rev;
requestSuccess = true;
@@ -113,14 +103,9 @@ export default class CouchObjectProvider {
if (this.objectQueue[id].hasNext()) {
this.updateQueued(id);
}
} else {
this.objectQueue[key].pending = false;
}
}
/**
* @private
*/
getModel(response) {
if (response && response.model) {
let key = response[ID];
@@ -134,7 +119,8 @@ export default class CouchObjectProvider {
}
//Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress
if (!this.objectQueue[key].pending) {
//Only update the rev if it's the first time we're getting the object from CouchDB. Subsequent revs should only be updated by updates.
if (!this.objectQueue[key].pending && !this.objectQueue[key].rev) {
this.objectQueue[key].updateRevision(response[REV]);
}
@@ -145,118 +131,10 @@ export default class CouchObjectProvider {
}
get(identifier, abortSignal) {
this.batchIds.push(identifier.key);
if (this.bulkPromise === undefined) {
this.bulkPromise = this.deferBatchedGet(abortSignal);
}
return this.bulkPromise
.then((domainObjectMap) => {
return domainObjectMap[identifier.key];
});
return this.request(identifier.key, "GET", undefined, abortSignal).then(this.getModel.bind(this));
}
/**
* @private
*/
deferBatchedGet(abortSignal) {
// We until the next event loop cycle to "collect" all of the get
// requests triggered in this iteration of the event loop
return this.waitOneEventCycle().then(() => {
let batchIds = this.batchIds;
this.clearBatch();
if (batchIds.length === 1) {
let objectKey = batchIds[0];
//If there's only one request, just do a regular get
return this.request(objectKey, "GET", undefined, abortSignal)
.then(this.returnAsMap(objectKey));
} else {
return this.bulkGet(batchIds, abortSignal);
}
});
}
/**
* @private
*/
returnAsMap(objectKey) {
return (result) => {
let objectMap = {};
objectMap[objectKey] = this.getModel(result);
return objectMap;
};
}
/**
* @private
*/
clearBatch() {
this.batchIds = [];
delete this.bulkPromise;
}
/**
* @private
*/
waitOneEventCycle() {
return new Promise((resolve) => {
setTimeout(resolve);
});
}
/**
* @private
*/
bulkGet(ids, signal) {
ids = this.removeDuplicates(ids);
const query = {
'keys': ids
};
return this.request(ALL_DOCS, 'POST', query, signal).then((response) => {
if (response && response.rows !== undefined) {
return response.rows.reduce((map, row) => {
if (row.doc !== undefined) {
map[row.key] = this.getModel(row.doc);
}
return map;
}, {});
} else {
return {};
}
});
}
/**
* @private
*/
removeDuplicates(array) {
return Array.from(new Set(array));
}
search(query, abortSignal) {
const filter = {
"selector": {
"model": {
"name": {
"$regex": `(?i)${query}`
}
}
}
};
return this.getObjectsByFilter(filter, abortSignal);
}
async getObjectsByFilter(filter, abortSignal) {
async getObjectsByFilter(filter) {
let objects = [];
let url = `${this.url}/_find`;
@@ -271,7 +149,6 @@ export default class CouchObjectProvider {
headers: {
"Content-Type": "application/json"
},
signal: abortSignal,
body
});
@@ -326,9 +203,6 @@ export default class CouchObjectProvider {
};
}
/**
* @private
*/
abortGetChanges() {
if (this.controller) {
this.controller.abort();
@@ -338,9 +212,6 @@ export default class CouchObjectProvider {
return true;
}
/**
* @private
*/
async observeObjectChanges(filter) {
let intermediateResponse = this.getIntermediateResponse();
@@ -421,9 +292,6 @@ export default class CouchObjectProvider {
}
/**
* @private
*/
getIntermediateResponse() {
let intermediateResponse = {};
intermediateResponse.promise = new Promise(function (resolve, reject) {
@@ -434,9 +302,6 @@ export default class CouchObjectProvider {
return intermediateResponse;
}
/**
* @private
*/
enqueueObject(key, model, intermediateResponse) {
if (this.objectQueue[key]) {
this.objectQueue[key].enqueue({
@@ -459,22 +324,19 @@ export default class CouchObjectProvider {
const queued = this.objectQueue[key].dequeue();
let document = new CouchDocument(key, queued.model);
this.request(key, "PUT", document).then((response) => {
this.checkResponse(response, queued.intermediateResponse, key);
this.checkResponse(response, queued.intermediateResponse);
});
return intermediateResponse.promise;
}
/**
* @private
*/
updateQueued(key) {
if (!this.objectQueue[key].pending) {
this.objectQueue[key].pending = true;
const queued = this.objectQueue[key].dequeue();
let document = new CouchDocument(key, queued.model, this.objectQueue[key].rev);
this.request(key, "PUT", document).then((response) => {
this.checkResponse(response, queued.intermediateResponse, key);
this.checkResponse(response, queued.intermediateResponse);
});
}
}

View File

@@ -24,6 +24,7 @@ import {
createOpenMct,
resetApplicationState, spyOnBuiltins
} from 'utils/testing';
import CouchObjectProvider from './CouchObjectProvider';
describe('the plugin', () => {
let openmct;
@@ -41,8 +42,7 @@ describe('the plugin', () => {
namespace: '',
key: 'some-value'
},
type: 'mock-type',
modified: 0
type: 'mock-type'
};
options = {
url: testPath,
@@ -95,7 +95,6 @@ describe('the plugin', () => {
return {
ok: true,
_id: 'some-value',
id: 'some-value',
_rev: 1,
model: {}
};
@@ -105,130 +104,44 @@ describe('the plugin', () => {
});
it('gets an object', () => {
return openmct.objects.get(mockDomainObject.identifier).then((result) => {
openmct.objects.get(mockDomainObject.identifier).then((result) => {
expect(result.identifier.key).toEqual(mockDomainObject.identifier.key);
});
});
it('creates an object', () => {
return openmct.objects.save(mockDomainObject).then((result) => {
openmct.objects.save(mockDomainObject).then((result) => {
expect(provider.create).toHaveBeenCalled();
expect(result).toBeTrue();
});
});
it('updates an object', () => {
return openmct.objects.save(mockDomainObject).then((result) => {
openmct.objects.save(mockDomainObject).then((result) => {
expect(result).toBeTrue();
expect(provider.create).toHaveBeenCalled();
//Set modified timestamp it detects a change and persists the updated model.
mockDomainObject.modified = Date.now();
return openmct.objects.save(mockDomainObject).then((updatedResult) => {
openmct.objects.save(mockDomainObject).then((updatedResult) => {
expect(updatedResult).toBeTrue();
expect(provider.update).toHaveBeenCalled();
});
});
});
});
describe('batches requests', () => {
let mockPromise;
beforeEach(() => {
mockPromise = Promise.resolve({
json: () => {
return {
total_rows: 0,
rows: []
};
}
});
fetch.and.returnValue(mockPromise);
it('updates queued objects', () => {
let couchProvider = new CouchObjectProvider(openmct, options, '');
let intermediateResponse = couchProvider.getIntermediateResponse();
spyOn(couchProvider, 'updateQueued');
couchProvider.enqueueObject(mockDomainObject.identifier.key, mockDomainObject, intermediateResponse);
couchProvider.objectQueue[mockDomainObject.identifier.key].updateRevision(1);
couchProvider.update(mockDomainObject);
expect(couchProvider.objectQueue[mockDomainObject.identifier.key].hasNext()).toBe(2);
couchProvider.checkResponse({
ok: true,
rev: 2,
id: mockDomainObject.identifier.key
}, intermediateResponse);
expect(couchProvider.updateQueued).toHaveBeenCalledTimes(2);
});
it('for multiple simultaneous gets', () => {
const objectIds = [
{
namespace: '',
key: 'object-1'
}, {
namespace: '',
key: 'object-2'
}, {
namespace: '',
key: 'object-3'
}
];
const getAllObjects = Promise.all(
objectIds.map((identifier) =>
openmct.objects.get(identifier)
));
return getAllObjects.then(() => {
const requestUrl = fetch.calls.mostRecent().args[0];
const requestMethod = fetch.calls.mostRecent().args[1].method;
expect(fetch).toHaveBeenCalledTimes(1);
expect(requestUrl.includes('_all_docs')).toBeTrue();
expect(requestMethod).toEqual('POST');
});
});
it('but not for single gets', () => {
const objectId = {
namespace: '',
key: 'object-1'
};
const getObject = openmct.objects.get(objectId);
return getObject.then(() => {
const requestUrl = fetch.calls.mostRecent().args[0];
const requestMethod = fetch.calls.mostRecent().args[1].method;
expect(fetch).toHaveBeenCalledTimes(1);
expect(requestUrl.endsWith(`${objectId.key}`)).toBeTrue();
expect(requestMethod).toEqual('GET');
});
});
});
describe('implements server-side search', () => {
let mockPromise;
beforeEach(() => {
mockPromise = Promise.resolve({
body: {
getReader() {
return {
read() {
return Promise.resolve({
done: true,
value: undefined
});
}
};
}
}
});
fetch.and.returnValue(mockPromise);
});
it("using Couch's 'find' endpoint", () => {
return Promise.all(openmct.objects.search('test')).then(() => {
const requestUrl = fetch.calls.mostRecent().args[0];
expect(fetch).toHaveBeenCalled();
expect(requestUrl.endsWith('_find')).toBeTrue();
});
});
it("and supports search by object name", () => {
return Promise.all(openmct.objects.search('test')).then(() => {
const requestPayload = JSON.parse(fetch.calls.mostRecent().args[1].body);
expect(requestPayload).toBeDefined();
expect(requestPayload.selector.model.name.$regex).toEqual('(?i)test');
});
});
});
});

View File

@@ -1,201 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2020, 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.
-->
<template>
<div v-if="loaded"
class="js-plot-options-browse"
>
<ul class="c-tree">
<h2 title="Plot series display properties in this object">Plot Series</h2>
<plot-options-item v-for="series in plotSeries"
:key="series.key"
:series="series"
/>
</ul>
<div v-if="plotSeries.length"
class="grid-properties"
>
<ul class="l-inspector-part">
<h2 title="Y axis settings for this object">Y Axis</h2>
<li class="grid-row">
<div class="grid-cell label"
title="Manually override how the Y axis is labeled."
>Label</div>
<div class="grid-cell value">{{ label ? label : "Not defined" }}</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Automatically scale the Y axis to keep all values in view."
>Autoscale</div>
<div class="grid-cell value">
{{ autoscale ? "Enabled: " : "Disabled" }}
{{ autoscale ? autoscalePadding : "" }}
</div>
</li>
<li v-if="!autoscale && rangeMin"
class="grid-row"
>
<div class="grid-cell label"
title="Minimum Y axis value."
>Minimum value</div>
<div class="grid-cell value">{{ rangeMin }}</div>
</li>
<li v-if="!autoscale && rangeMax"
class="grid-row"
>
<div class="grid-cell label"
title="Maximum Y axis value."
>Maximum value</div>
<div class="grid-cell value">{{ rangeMax }}</div>
</li>
</ul>
<ul class="l-inspector-part">
<h2 title="Legend settings for this object">Legend</h2>
<li class="grid-row">
<div class="grid-cell label"
title="The position of the legend relative to the plot display area."
>Position</div>
<div class="grid-cell value capitalize">{{ position }}</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Hide the legend when the plot is small"
>Hide when plot small</div>
<div class="grid-cell value">{{ hideLegendWhenSmall ? "Yes" : "No" }}</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Show the legend expanded by default"
>Expand by Default</div>
<div class="grid-cell value">{{ expandByDefault ? "Yes" : "No" }}</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="What to display in the legend when it's collapsed."
>Show when collapsed:</div>
<div class="grid-cell value">{{
valueToShowWhenCollapsed.replace('nearest', '')
}}
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="What to display in the legend when it's expanded."
>Show when expanded:</div>
<div class="grid-cell value comma-list">
<span v-if="showTimestampWhenExpanded">Timestamp</span>
<span v-if="showValueWhenExpanded">Value</span>
<span v-if="showMinimumWhenExpanded">Min</span>
<span v-if="showMaximumWhenExpanded">Max</span>
<span v-if="showUnitsWhenExpanded">Units</span>
</div>
</li>
</ul>
</div>
</div>
</template>
<script>
import PlotOptionsItem from "./PlotOptionsItem.vue";
import configStore from "../configuration/configStore";
import eventHelpers from "../lib/eventHelpers";
export default {
components: {
PlotOptionsItem
},
inject: ['openmct', 'domainObject'],
data() {
return {
config: {},
label: '',
autoscale: '',
autoscalePadding: '',
rangeMin: '',
rangeMax: '',
position: '',
hideLegendWhenSmall: '',
expandByDefault: '',
valueToShowWhenCollapsed: '',
showTimestampWhenExpanded: '',
showValueWhenExpanded: '',
showMinimumWhenExpanded: '',
showMaximumWhenExpanded: '',
showUnitsWhenExpanded: '',
loaded: false,
plotSeries: []
};
},
mounted() {
eventHelpers.extend(this);
this.config = this.getConfig();
this.registerListeners();
this.initConfiguration();
this.loaded = true;
},
beforeDestroy() {
this.stopListening();
},
methods: {
initConfiguration() {
this.label = this.config.yAxis.get('label');
this.autoscale = this.config.yAxis.get('autoscale');
this.autoscalePadding = this.config.yAxis.get('autoscalePadding');
const range = this.config.yAxis.get('range');
if (range) {
this.rangeMin = range.min;
this.rangeMax = range.max;
}
this.position = this.config.legend.get('position');
this.hideLegendWhenSmall = this.config.legend.get('hideLegendWhenSmall');
this.expandByDefault = this.config.legend.get('expandByDefault');
this.valueToShowWhenCollapsed = this.config.legend.get('valueToShowWhenCollapsed');
this.showTimestampWhenExpanded = this.config.legend.get('showTimestampWhenExpanded');
this.showValueWhenExpanded = this.config.legend.get('showValueWhenExpanded');
this.showMinimumWhenExpanded = this.config.legend.get('showMinimumWhenExpanded');
this.showMaximumWhenExpanded = this.config.legend.get('showMaximumWhenExpanded');
this.showUnitsWhenExpanded = this.config.legend.get('showUnitsWhenExpanded');
},
getConfig() {
this.configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
return configStore.get(this.configId);
},
registerListeners() {
this.config.series.forEach(this.addSeries, this);
this.listenTo(this.config.series, 'add', this.addSeries, this);
this.listenTo(this.config.series, 'remove', this.resetAllSeries, this);
},
addSeries(series, index) {
this.$set(this.plotSeries, index, series);
this.initConfiguration();
},
resetAllSeries() {
this.plotSeries = [];
this.config.series.forEach(this.addSeries, this);
}
}
};
</script>

View File

@@ -1,100 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2020, 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.
-->
<template>
<div v-if="loaded"
class="js-plot-options-edit"
>
<ul class="c-tree">
<h2 title="Display properties for this object">Plot Series</h2>
<li v-for="series in plotSeries"
:key="series.key"
>
<series-form :series="series" />
</li>
</ul>
<y-axis-form v-if="plotSeries.length"
class="grid-properties"
:y-axis="config.yAxis"
/>
<ul class="l-inspector-part">
<h2 title="Legend options">Legend</h2>
<legend-form v-if="plotSeries.length"
class="grid-properties"
:legend="config.legend"
/>
</ul>
</div>
</template>
<script>
import SeriesForm from "./forms/SeriesForm.vue";
import YAxisForm from "./forms/YAxisForm.vue";
import LegendForm from "./forms/LegendForm.vue";
import eventHelpers from "../lib/eventHelpers";
import configStore from "../configuration/configStore";
export default {
components: {
LegendForm,
SeriesForm,
YAxisForm
},
inject: ['openmct', 'domainObject'],
data() {
return {
config: {},
plotSeries: [],
loaded: false
};
},
mounted() {
eventHelpers.extend(this);
this.config = this.getConfig();
this.registerListeners();
this.loaded = true;
},
beforeDestroy() {
this.stopListening();
},
methods: {
getConfig() {
this.configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
return configStore.get(this.configId);
},
registerListeners() {
this.config.series.forEach(this.addSeries, this);
this.listenTo(this.config.series, 'add', this.addSeries, this);
this.listenTo(this.config.series, 'remove', this.resetAllSeries, this);
},
addSeries(series, index) {
this.$set(this.plotSeries, index, series);
},
resetAllSeries() {
this.plotSeries = [];
this.config.series.forEach(this.addSeries, this);
}
}
};
</script>

View File

@@ -1,160 +0,0 @@
<template>
<ul>
<li class="c-tree__item menus-to-left"
:class="isAliasClass"
>
<span class="c-disclosure-triangle is-enabled flex-elem"
:class="expandedCssClass"
@click="toggleExpanded"
>
</span>
<div class="c-object-label"
:class="statusClass"
>
<div class="c-object-label__type-icon"
:class="getSeriesClass"
>
<span class="is-status__indicator"
title="This item is missing or suspect"
></span>
</div>
<div class="c-object-label__name">{{ series.domainObject.name }}</div>
</div>
</li>
<li v-show="expanded"
class="c-tree__item menus-to-left"
>
<ul class="grid-properties js-plot-options-browse-properties">
<li class="grid-row">
<div class="grid-cell label"
title="The field to be plotted as a value for this series."
>Value</div>
<div class="grid-cell value">
{{ yKey }}
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="The rendering method to join lines for this series."
>Line Method</div>
<div class="grid-cell value">{{ {
'none': 'None',
'linear': 'Linear interpolation',
'stepAfter': 'Step After'
}[interpolate] }}
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Whether markers are displayed, and their size."
>Markers</div>
<div class="grid-cell value">
{{ markerOptionsDisplayText }}
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Display markers visually denoting points in alarm."
>Alarm Markers</div>
<div class="grid-cell value">
{{ alarmMarkers ? "Enabled" : "Disabled" }}
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="The plot line and marker color for this series."
>Color</div>
<div class="grid-cell value">
<span class="c-color-swatch"
:style="{
'background': seriesHexColor
}"
>
</span>
</div>
</li>
</ul>
</li>
</ul>
</template>
<script>
export default {
inject: ['openmct', 'domainObject', 'path'],
props: {
series: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
expanded: false
};
},
computed: {
isAliasClass() {
let cssClass = '';
const domainObjectPath = [this.series.domainObject, ...this.path];
if (this.openmct.objects.isObjectPathToALink(this.series.domainObject, domainObjectPath)) {
cssClass = 'is-alias';
}
return cssClass;
},
getSeriesClass() {
let cssClass = '';
let type = this.openmct.types.get(this.series.domainObject.type);
if (type.definition.cssClass) {
cssClass = `${cssClass} ${type.definition.cssClass}`;
}
return cssClass;
},
expandedCssClass() {
if (this.expanded === true) {
return 'c-disclosure-triangle--expanded';
}
return '';
},
statusClass() {
return (this.status) ? `is-status--${this.status}` : '';
},
yKey() {
return this.series.get('yKey');
},
interpolate() {
return this.series.get('interpolate');
},
markerOptionsDisplayText() {
return this.series.markerOptionsDisplayText();
},
alarmMarkers() {
return this.series.get('alarmMarkers');
},
seriesHexColor() {
return this.series.get('color').asHexString();
}
},
mounted() {
this.status = this.openmct.status.get(this.series.domainObject.identifier);
this.removeStatusListener = this.openmct.status.observe(this.series.domainObject.identifier, this.setStatus);
},
beforeDestroy() {
if (this.removeStatusListener) {
this.removeStatusListener();
}
},
methods: {
toggleExpanded() {
this.expanded = !this.expanded;
},
setStatus(status) {
this.status = status;
}
}
};
</script>

View File

@@ -1,56 +0,0 @@
import PlotOptions from "./PlotOptions.vue";
import Vue from 'vue';
export default function PlotsInspectorViewProvider(openmct) {
return {
key: 'plots-inspector',
name: 'Plots Inspector View',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) {
return false;
}
let object = selection[0][0].context.item;
return object
&& object.type === 'telemetry.plot.overlay';
},
view: function (selection) {
let component;
let objectPath;
if (selection.length) {
objectPath = selection[0].map((selectionItem) => {
return selectionItem.context.item;
});
}
return {
show: function (element) {
component = new Vue({
el: element,
components: {
PlotOptions: PlotOptions
},
provide: {
openmct,
domainObject: selection[0][0].context.item,
path: objectPath
},
template: '<plot-options></plot-options>'
});
},
destroy: function () {
if (component) {
component.$destroy();
component = undefined;
}
}
};
},
priority: function () {
return 1;
}
};
}

View File

@@ -1,207 +0,0 @@
<template>
<div>
<li class="grid-row">
<div class="grid-cell label"
title="The position of the legend relative to the plot display area."
>Position</div>
<div class="grid-cell value">
<select v-model="position"
@change="updateForm('position')"
>
<option value="top">Top</option>
<option value="right">Right</option>
<option value="bottom">Bottom</option>
<option value="left">Left</option>
</select>
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Hide the legend when the plot is small"
>Hide when plot small</div>
<div class="grid-cell value"><input v-model="hideLegendWhenSmall"
type="checkbox"
@change="updateForm('hideLegendWhenSmall')"
></div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Show the legend expanded by default"
>Expand by default</div>
<div class="grid-cell value"><input v-model="expandByDefault"
type="checkbox"
@change="updateForm('expandByDefault')"
></div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="What to display in the legend when it's collapsed."
>When collapsed show</div>
<div class="grid-cell value">
<select v-model="valueToShowWhenCollapsed"
@change="updateForm('valueToShowWhenCollapsed')"
>
<option value="none">Nothing</option>
<option value="nearestTimestamp">Nearest timestamp</option>
<option value="nearestValue">Nearest value</option>
<option value="min">Minimum value</option>
<option value="max">Maximum value</option>
<option value="units">Units</option>
</select>
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="What to display in the legend when it's expanded."
>When expanded show</div>
<div class="grid-cell value">
<ul>
<li><input v-model="showTimestampWhenExpanded"
type="checkbox"
@change="updateForm('showTimestampWhenExpanded')"
> Nearest timestamp</li>
<li><input v-model="showValueWhenExpanded"
type="checkbox"
@change="updateForm('showValueWhenExpanded')"
> Nearest value</li>
<li><input v-model="showMinimumWhenExpanded"
type="checkbox"
@change="updateForm('showMinimumWhenExpanded')"
> Minimum value</li>
<li><input v-model="showMaximumWhenExpanded"
type="checkbox"
@change="updateForm('showMaximumWhenExpanded')"
> Maximum value</li>
<li><input v-model="showUnitsWhenExpanded"
type="checkbox"
@change="updateForm('showUnitsWhenExpanded')"
> Units</li>
</ul>
</div>
</li>
</div>
</template>
<script>
import {coerce, objectPath, validate} from "./formUtil";
import _ from "lodash";
export default {
inject: ['openmct', 'domainObject'],
props: {
legend: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
position: '',
hideLegendWhenSmall: '',
expandByDefault: '',
valueToShowWhenCollapsed: '',
showTimestampWhenExpanded: '',
showValueWhenExpanded: '',
showMinimumWhenExpanded: '',
showMaximumWhenExpanded: '',
showUnitsWhenExpanded: '',
validation: {}
};
},
mounted() {
this.initialize();
this.initFormValues();
},
methods: {
initialize() {
this.fields = [
{
modelProp: 'position',
objectPath: 'configuration.legend.position'
},
{
modelProp: 'hideLegendWhenSmall',
coerce: Boolean,
objectPath: 'configuration.legend.hideLegendWhenSmall'
},
{
modelProp: 'expandByDefault',
coerce: Boolean,
objectPath: 'configuration.legend.expandByDefault'
},
{
modelProp: 'valueToShowWhenCollapsed',
objectPath: 'configuration.legend.valueToShowWhenCollapsed'
},
{
modelProp: 'showValueWhenExpanded',
coerce: Boolean,
objectPath: 'configuration.legend.showValueWhenExpanded'
},
{
modelProp: 'showTimestampWhenExpanded',
coerce: Boolean,
objectPath: 'configuration.legend.showTimestampWhenExpanded'
},
{
modelProp: 'showMaximumWhenExpanded',
coerce: Boolean,
objectPath: 'configuration.legend.showMaximumWhenExpanded'
},
{
modelProp: 'showMinimumWhenExpanded',
coerce: Boolean,
objectPath: 'configuration.legend.showMinimumWhenExpanded'
},
{
modelProp: 'showUnitsWhenExpanded',
coerce: Boolean,
objectPath: 'configuration.legend.showUnitsWhenExpanded'
}
];
},
initFormValues() {
this.position = this.legend.get('position');
this.hideLegendWhenSmall = this.legend.get('hideLegendWhenSmall');
this.expandByDefault = this.legend.get('expandByDefault');
this.valueToShowWhenCollapsed = this.legend.get('valueToShowWhenCollapsed');
this.showTimestampWhenExpanded = this.legend.get('showTimestampWhenExpanded');
this.showValueWhenExpanded = this.legend.get('showValueWhenExpanded');
this.showMinimumWhenExpanded = this.legend.get('showMinimumWhenExpanded');
this.showMaximumWhenExpanded = this.legend.get('showMaximumWhenExpanded');
this.showUnitsWhenExpanded = this.legend.get('showUnitsWhenExpanded');
},
updateForm(formKey) {
const newVal = this[formKey];
const oldVal = this.legend.get(formKey);
const formField = this.fields.find((field) => field.modelProp === formKey);
const path = objectPath(formField.objectPath);
const validationResult = validate(newVal, this.legend, formField.validate);
if (validationResult === true) {
delete this.validation[formKey];
} else {
this.validation[formKey] = validationResult;
return;
}
if (!_.isEqual(coerce(newVal, formField.coerce), coerce(oldVal, formField.coerce))) {
this.legend.set(formKey, coerce(newVal, formField.coerce));
if (path) {
this.openmct.objects.mutate(
this.domainObject,
path(this.domainObject, this.legend),
coerce(newVal, formField.coerce)
);
}
}
},
setStatus(status) {
this.status = status;
}
}
};
</script>

View File

@@ -1,347 +0,0 @@
<template>
<ul>
<li class="c-tree__item menus-to-left"
:class="isAliasCss"
>
<span class="c-disclosure-triangle is-enabled flex-elem"
:class="expandedCssClass"
@click="toggleExpanded"
>
</span>
<div :class="objectLabelCss">
<div class="c-object-label__type-icon"
:class="[seriesCss]"
>
<span class="is-status__indicator"
title="This item is missing or suspect"
></span>
</div>
<div class="c-object-label__name">{{ series.domainObject.name }}</div>
</div>
</li>
<ul v-show="expanded"
class="grid-properties js-plot-options-edit-properties"
>
<li class="grid-row">
<!-- Value to be displayed -->
<div class="grid-cell label"
title="The field to be plotted as a value for this series."
>Value</div>
<div class="grid-cell value">
<select v-model="yKey"
@change="updateForm('yKey')"
>
<option v-for="option in yKeyOptions"
:key="option.value"
:value="option.value"
:selected="option.value == yKey"
>
{{ option.name }}
</option>
</select>
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="The rendering method to join lines for this series."
>Line Method</div>
<div class="grid-cell value">
<select v-model="interpolate"
@change="updateForm('interpolate')"
>
<option value="none">None</option>
<option value="linear">Linear interpolate</option>
<option value="stepAfter">Step after</option>
</select>
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Whether markers are displayed."
>Markers</div>
<div class="grid-cell value">
<input v-model="markers"
type="checkbox"
@change="updateForm('markers')"
>
<select
v-show="markers"
v-model="markerShape"
@change="updateForm('markerShape')"
>
<option
v-for="option in markerShapeOptions"
:key="option.value"
:value="option.value"
:selected="option.value == markerShape"
>
{{ option.name }}
</option>
</select>
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Display markers visually denoting points in alarm."
>Alarm Markers</div>
<div class="grid-cell value">
<input v-model="alarmMarkers"
type="checkbox"
@change="updateForm('alarmMarkers')"
>
</div>
</li>
<li v-show="markers || alarmMarkers"
class="grid-row"
>
<div class="grid-cell label"
title="The size of regular and alarm markers for this series."
>Marker Size:</div>
<div class="grid-cell value"><input v-model="markerSize"
class="c-input--flex"
type="text"
@change="updateForm('markerSize')"
></div>
</li>
<li v-show="interpolate !== 'none' || markers"
class="grid-row"
>
<div class="grid-cell label"
title="Manually set the plot line and marker color for this series."
>Color</div>
<div class="grid-cell value">
<div class="c-click-swatch c-click-swatch--menu"
@click="toggleSwatch()"
>
<span class="c-color-swatch"
:style="{ background: seriesColorAsHex }"
>
</span>
</div>
<div class="c-palette c-palette--color">
<div v-show="swatchActive"
class="c-palette__items"
>
<div v-for="(group, index) in colorPalette"
:key="index"
class="u-contents"
>
<div v-for="(color, colorIndex) in group"
:key="colorIndex"
class="c-palette__item"
:class="{ 'selected': series.get('color').equalTo(color) }"
:style="{ background: color.asHexString() }"
@click="setColor(color)"
>
</div>
</div>
</div>
</div>
</div>
</li>
</ul>
</ul>
</template>
<script>
import { MARKER_SHAPES } from "../../draw/MarkerShapes";
import { objectPath, validate, coerce } from "./formUtil";
import _ from 'lodash';
export default {
inject: ['openmct', 'domainObject', 'path'],
props: {
series: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
expanded: false,
markerShapeOptions: [],
yKey: this.series.get('yKey'),
yKeyOptions: [],
interpolate: this.series.get('interpolate'),
markers: this.series.get('markers'),
markerShape: this.series.get('markerShape'),
alarmMarkers: this.series.get('alarmMarkers'),
markerSize: this.series.get('markerSize'),
validation: {},
swatchActive: false
};
},
computed: {
colorPalette() {
return this.series.collection.palette.groups();
},
objectLabelCss() {
return this.status ? `c-object-label is-status--${this.status}'` : 'c-object-label';
},
seriesCss() {
let type = this.openmct.types.get(this.series.domainObject.type);
return type.definition.cssClass ? `c-object-label__type-icon ${type.definition.cssClass}` : `c-object-label__type-icon`;
},
isAliasCss() {
let cssClass = '';
const domainObjectPath = [this.series.domainObject, ...this.path];
if (this.openmct.objects.isObjectPathToALink(this.series.domainObject, domainObjectPath)) {
cssClass = 'is-alias';
}
return cssClass;
},
expandedCssClass() {
return this.expanded ? 'c-disclosure-triangle--expanded' : '';
},
seriesColorAsHex() {
return this.series.get('color').asHexString();
}
},
mounted() {
this.initialize();
this.status = this.openmct.status.get(this.series.domainObject.identifier);
this.removeStatusListener = this.openmct.status.observe(this.series.domainObject.identifier, this.setStatus);
},
beforeDestroy() {
if (this.removeStatusListener) {
this.removeStatusListener();
}
},
methods: {
initialize: function () {
this.fields = [
{
modelProp: 'yKey',
objectPath: this.dynamicPathForKey('yKey')
},
{
modelProp: 'interpolate',
objectPath: this.dynamicPathForKey('interpolate')
},
{
modelProp: 'markers',
objectPath: this.dynamicPathForKey('markers')
},
{
modelProp: 'markerShape',
objectPath: this.dynamicPathForKey('markerShape')
},
{
modelProp: 'markerSize',
coerce: Number,
objectPath: this.dynamicPathForKey('markerSize')
},
{
modelProp: 'alarmMarkers',
coerce: Boolean,
objectPath: this.dynamicPathForKey('alarmMarkers')
}
];
const metadata = this.series.metadata;
this.yKeyOptions = metadata
.valuesForHints(['range'])
.map(function (o) {
return {
name: o.key,
value: o.key
};
});
this.markerShapeOptions = Object.entries(MARKER_SHAPES)
.map(([key, obj]) => {
return {
name: obj.label,
value: key
};
});
},
dynamicPathForKey(key) {
return function (object, model) {
const modelIdentifier = model.get('identifier');
const index = object.configuration.series.findIndex(s => {
return _.isEqual(s.identifier, modelIdentifier);
});
return 'configuration.series[' + index + '].' + key;
};
},
/**
* Set the color for the current plot series. If the new color was
* already assigned to a different plot series, then swap the colors.
*/
setColor: function (color) {
const oldColor = this.series.get('color');
const otherSeriesWithColor = this.series.collection.filter(function (s) {
return s.get('color') === color;
})[0];
this.series.set('color', color);
const getPath = this.dynamicPathForKey('color');
const seriesColorPath = getPath(this.domainObject, this.series);
this.openmct.objects.mutate(
this.domainObject,
seriesColorPath,
color.asHexString()
);
if (otherSeriesWithColor) {
otherSeriesWithColor.set('color', oldColor);
const otherSeriesColorPath = getPath(
this.domainObject,
otherSeriesWithColor
);
this.openmct.objects.mutate(
this.domainObject,
otherSeriesColorPath,
oldColor.asHexString()
);
}
},
toggleExpanded() {
this.expanded = !this.expanded;
},
updateForm(formKey) {
const newVal = this[formKey];
const oldVal = this.series.get(formKey);
const formField = this.fields.find((field) => field.modelProp === formKey);
const path = objectPath(formField.objectPath);
const validationResult = validate(newVal, this.series, formField.validate);
if (validationResult === true) {
delete this.validation[formKey];
} else {
this.validation[formKey] = validationResult;
return;
}
if (!_.isEqual(coerce(newVal, formField.coerce), coerce(oldVal, formField.coerce))) {
this.series.set(formKey, coerce(newVal, formField.coerce));
if (path) {
this.openmct.objects.mutate(
this.domainObject,
path(this.domainObject, this.series),
coerce(newVal, formField.coerce)
);
}
}
},
setStatus(status) {
this.status = status;
},
toggleSwatch() {
this.swatchActive = !this.swatchActive;
}
}
};
</script>

View File

@@ -1,229 +0,0 @@
<template>
<div>
<ul class="l-inspector-part">
<h2>Y Axis</h2>
<li class="grid-row">
<div class="grid-cell label"
title="Manually override how the Y axis is labeled."
>Label</div>
<div class="grid-cell value"><input v-model="label"
class="c-input--flex"
type="text"
@change="updateForm('label')"
></div>
</li>
</ul>
<ul class="l-inspector-part">
<h2>Y Axis Scaling</h2>
<li class="grid-row">
<div class="grid-cell label"
title="Automatically scale the Y axis to keep all values in view."
>Auto scale</div>
<div class="grid-cell value"><input v-model="autoscale"
type="checkbox"
@change="updateForm('autoscale')"
></div>
</li>
<li v-show="autoscale"
class="grid-row"
>
<div class="grid-cell label"
title="Percentage of padding above and below plotted min and max values. 0.1, 1.0, etc."
>
Padding</div>
<div class="grid-cell value">
<input v-model="autoscalePadding"
class="c-input--flex"
type="text"
@change="updateForm('autoscalePadding')"
>
</div>
</li>
</ul>
<ul v-show="!autoscale"
class="l-inspector-part"
>
<div v-show="!autoscale && validation.range"
class="grid-span-all form-error"
>
{{ validation.range }}
</div>
<li class="grid-row force-border">
<div class="grid-cell label"
title="Minimum Y axis value."
>Minimum Value</div>
<div class="grid-cell value">
<input v-model="rangeMin"
class="c-input--flex"
type="number"
@change="updateForm('range')"
>
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Maximum Y axis value."
>Maximum Value</div>
<div class="grid-cell value"><input v-model="rangeMax"
class="c-input--flex"
type="number"
@change="updateForm('range')"
></div>
</li>
</ul>
</div>
</template>
<script>
import { objectPath, validate, coerce } from "./formUtil";
import _ from "lodash";
export default {
inject: ['openmct', 'domainObject'],
props: {
yAxis: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
label: '',
autoscale: '',
autoscalePadding: '',
rangeMin: '',
rangeMax: '',
validation: {}
};
},
mounted() {
this.initialize();
this.initFormValues();
},
methods: {
initialize: function () {
this.fields = [
{
modelProp: 'label',
objectPath: 'configuration.yAxis.label'
},
{
modelProp: 'autoscale',
coerce: Boolean,
objectPath: 'configuration.yAxis.autoscale'
},
{
modelProp: 'autoscalePadding',
coerce: Number,
objectPath: 'configuration.yAxis.autoscalePadding'
},
{
modelProp: 'range',
objectPath: 'configuration.yAxis.range',
coerce: function coerceRange(range) {
if (!range) {
return {
min: 0,
max: 0
};
}
const newRange = {};
if (typeof range.min !== 'undefined' && range.min !== null) {
newRange.min = Number(range.min);
}
if (typeof range.max !== 'undefined' && range.max !== null) {
newRange.max = Number(range.max);
}
return newRange;
},
validate: function validateRange(range, model) {
if (!range) {
return 'Need range';
}
if (range.min === '' || range.min === null || typeof range.min === 'undefined') {
return 'Must specify Minimum';
}
if (range.max === '' || range.max === null || typeof range.max === 'undefined') {
return 'Must specify Maximum';
}
if (Number.isNaN(Number(range.min))) {
return 'Minimum must be a number.';
}
if (Number.isNaN(Number(range.max))) {
return 'Maximum must be a number.';
}
if (Number(range.min) > Number(range.max)) {
return 'Minimum must be less than Maximum.';
}
if (model.get('autoscale')) {
return false;
}
return true;
}
}
];
},
initFormValues() {
this.label = this.yAxis.get('label');
this.autoscale = this.yAxis.get('autoscale');
this.autoscalePadding = this.yAxis.get('autoscalePadding');
const range = this.yAxis.get('range');
if (!range) {
this.rangeMin = undefined;
this.rangeMax = undefined;
} else {
this.rangeMin = range.min;
this.rangeMax = range.max;
}
},
updateForm(formKey) {
let newVal;
if (formKey === 'range') {
newVal = {
min: this.rangeMin,
max: this.rangeMax
};
} else {
newVal = this[formKey];
}
const oldVal = this.yAxis.get(formKey);
const formField = this.fields.find((field) => field.modelProp === formKey);
const path = objectPath(formField.objectPath);
const validationResult = validate(newVal, this.yAxis, formField.validate);
if (validationResult === true) {
delete this.validation[formKey];
} else {
this.validation[formKey] = validationResult;
return;
}
if (!_.isEqual(coerce(newVal, formField.coerce), coerce(oldVal, formField.coerce))) {
this.yAxis.set(formKey, coerce(newVal, formField.coerce));
if (path) {
this.openmct.objects.mutate(
this.domainObject,
path(this.domainObject, this.yAxis),
coerce(newVal, formField.coerce)
);
}
}
}
}
};
</script>

View File

@@ -1,29 +0,0 @@
export function coerce(value, coerceFunc) {
if (coerceFunc) {
return coerceFunc(value);
}
return value;
}
export function validate(value, model, validateFunc) {
if (validateFunc) {
return validateFunc(value, model);
}
return true;
}
export function objectPath(path) {
if (path) {
if (typeof path !== "function") {
const staticObjectPath = path;
return function (object, model) {
return staticObjectPath;
};
}
return path;
}
}

View File

@@ -1,29 +0,0 @@
export default function OverlayPlotCompositionPolicy(openmct) {
function hasNumericTelemetry(domainObject) {
const hasTelemetry = openmct.telemetry.isTelemetryObject(domainObject);
if (!hasTelemetry) {
return false;
}
let metadata = openmct.telemetry.getMetadata(domainObject);
return metadata.values().length > 0 && hasDomainAndRange(metadata);
}
function hasDomainAndRange(metadata) {
return (metadata.valuesForHints(['range']).length > 0
&& metadata.valuesForHints(['domain']).length > 0);
}
return {
allow: function (parent, child) {
if (parent.type === 'telemetry.plot.overlay'
&& (hasNumericTelemetry(child) === false)) {
return false;
}
return true;
}
};
}

View File

@@ -20,52 +20,262 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import PlotViewProvider from './PlotViewProvider';
import OverlayPlotViewProvider from './overlayPlot/OverlayPlotViewProvider';
import StackedPlotViewProvider from './stackedPlot/StackedPlotViewProvider';
import PlotsInspectorViewProvider from './inspector/PlotsInspectorViewProvider';
import OverlayPlotCompositionPolicy from './overlayPlot/OverlayPlotCompositionPolicy';
import StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy';
define([
"./src/chart/MCTChartDirective",
"./src/plot/MCTPlotDirective",
'./src/plot/MCTTicksDirective',
"./src/telemetry/MCTOverlayPlot",
"./src/telemetry/PlotController",
"./src/telemetry/StackedPlotController",
"./src/inspector/PlotInspector",
"./src/inspector/PlotOptionsController",
"./src/inspector/PlotLegendFormController",
"./src/inspector/PlotYAxisFormController",
"./src/inspector/PlotSeriesFormController",
"./src/inspector/HideElementPoolDirective",
"./src/services/ExportImageService",
'./src/PlotViewPolicy',
"./res/templates/plot-options.html",
"./res/templates/plot-options-browse.html",
"./res/templates/plot-options-edit.html",
"./res/templates/stacked-plot.html",
"./res/templates/plot.html"
], function (
MCTChartDirective,
MCTPlotDirective,
MCTTicksDirective,
MCTOverlayPlot,
PlotController,
StackedPlotController,
PlotInspector,
PlotOptionsController,
PlotLegendFormController,
PlotYAxisFormController,
PlotSeriesFormController,
HideElementPool,
ExportImageService,
PlotViewPolicy,
plotOptionsTemplate,
plotOptionsBrowseTemplate,
plotOptionsEditTemplate,
StackedPlotTemplate,
PlotTemplate
) {
export default function () {
return function install(openmct) {
let installed = false;
openmct.types.addType('telemetry.plot.overlay', {
key: "telemetry.plot.overlay",
name: "Overlay Plot",
cssClass: "icon-plot-overlay",
description: "Combine multiple telemetry elements and view them together as a plot with common X and Y axes. Can be added to Display Layouts.",
creatable: "true",
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {
series: [],
yAxis: {},
xAxis: {}
};
},
priority: 891
});
function PlotPlugin() {
return function install(openmct) {
if (installed) {
return;
}
openmct.types.addType('telemetry.plot.stacked', {
key: "telemetry.plot.stacked",
name: "Stacked Plot",
cssClass: "icon-plot-stacked",
description: "Combine multiple telemetry elements and view them together as a plot with a common X axis and individual Y axes. Can be added to Display Layouts.",
creatable: true,
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {};
},
priority: 890
});
installed = true;
openmct.objectViews.addProvider(new StackedPlotViewProvider(openmct));
openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct));
openmct.objectViews.addProvider(new PlotViewProvider(openmct));
openmct.inspectorViews.addProvider(new PlotsInspectorViewProvider(openmct));
openmct.composition.addPolicy(new OverlayPlotCompositionPolicy(openmct).allow);
openmct.composition.addPolicy(new StackedPlotCompositionPolicy(openmct).allow);
};
}
openmct.legacyRegistry.register("openmct/plot", {
"name": "Plot view for telemetry, reborn",
"extensions": {
"policies": [
{
"category": "view",
"implementation": PlotViewPolicy,
"depends": [
"openmct"
]
}
],
"views": [
{
"name": "Plot",
"key": "plot-single",
"cssClass": "icon-telemetry",
"template": PlotTemplate,
"needs": [
"telemetry"
],
"delegation": false,
"priority": "mandatory"
},
{
"name": "Overlay Plot",
"key": "overlayPlot",
"cssClass": "icon-plot-overlay",
"type": "telemetry.plot.overlay",
"template": PlotTemplate,
"editable": true
},
{
"name": "Stacked Plot",
"key": "stackedPlot",
"cssClass": "icon-plot-stacked",
"type": "telemetry.plot.stacked",
"template": StackedPlotTemplate,
"editable": true
}
],
"directives": [
{
"key": "mctTicks",
"implementation": MCTTicksDirective,
"depends": []
},
{
"key": "mctChart",
"implementation": MCTChartDirective,
"depends": [
"$interval",
"$log"
]
},
{
"key": "mctPlot",
"implementation": MCTPlotDirective,
"depends": [],
"templateUrl": "templates/mct-plot.html"
},
{
"key": "mctOverlayPlot",
"implementation": MCTOverlayPlot,
"depends": []
},
{
"key": "hideElementPool",
"implementation": HideElementPool,
"depends": []
}
],
"controllers": [
{
"key": "PlotController",
"implementation": PlotController,
"depends": [
"$scope",
"$element",
"formatService",
"openmct",
"objectService",
"exportImageService"
]
},
{
"key": "StackedPlotController",
"implementation": StackedPlotController,
"depends": [
"$scope",
"openmct",
"objectService",
"$element",
"exportImageService"
]
},
{
"key": "PlotOptionsController",
"implementation": PlotOptionsController,
"depends": [
"$scope",
"openmct",
"$timeout"
]
},
{
key: "PlotLegendFormController",
implementation: PlotLegendFormController,
depends: [
"$scope",
"openmct",
"$attrs"
]
},
{
key: "PlotYAxisFormController",
implementation: PlotYAxisFormController,
depends: [
"$scope",
"openmct",
"$attrs"
]
},
{
key: "PlotSeriesFormController",
implementation: PlotSeriesFormController,
depends: [
"$scope",
"openmct",
"$attrs"
]
}
],
"services": [
{
"key": "exportImageService",
"implementation": ExportImageService,
"depends": [
"dialogService"
]
}
],
"types": [
{
"key": "telemetry.plot.overlay",
"name": "Overlay Plot",
"cssClass": "icon-plot-overlay",
"description": "Combine multiple telemetry elements and view them together as a plot with common X and Y axes. Can be added to Display Layouts.",
"features": "creation",
"contains": [
{
"has": "telemetry"
}
],
"model": {
composition: [],
configuration: {
series: [],
yAxis: {},
xAxis: {}
}
},
"properties": [],
"inspector": "plot-options",
"priority": 891
},
{
"key": "telemetry.plot.stacked",
"name": "Stacked Plot",
"cssClass": "icon-plot-stacked",
"description": "Combine multiple telemetry elements and view them together as a plot with a common X axis and individual Y axes. Can be added to Display Layouts.",
"features": "creation",
"contains": [
"telemetry.plot.overlay",
{"has": "telemetry"}
],
"model": {
"composition": [],
"configuration": {}
},
"properties": [],
"priority": 890
}
],
"representations": [
{
"key": "plot-options",
"template": plotOptionsTemplate
},
{
"key": "plot-options-browse",
"template": plotOptionsBrowseTemplate
},
{
"key": "plot-options-edit",
"template": plotOptionsEditTemplate
}
]
}
});
openmct.legacyRegistry.enable("openmct/plot");
};
}
return PlotPlugin;
});

View File

@@ -0,0 +1,279 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div class="gl-plot plot-legend-{{legend.get('position')}} {{legend.get('expanded')? 'plot-legend-expanded' : 'plot-legend-collapsed'}}">
<div class="c-plot-legend gl-plot-legend"
ng-class="{
'hover-on-plot': !!highlights.length,
'is-legend-hidden': legend.get('hideLegendWhenSmall')
}"
>
<div class="c-plot-legend__view-control gl-plot-legend__view-control c-disclosure-triangle is-enabled"
ng-class="{ 'c-disclosure-triangle--expanded': legend.get('expanded') }"
ng-click="legend.set('expanded', !legend.get('expanded'));">
</div>
<div class="c-plot-legend__wrapper"
ng-class="{ 'is-cursor-locked': !!lockHighlightPoint }">
<!-- COLLAPSED PLOT LEGEND -->
<div class="plot-wrapper-collapsed-legend"
ng-class="{'is-cursor-locked': !!lockHighlightPoint }">
<div class="c-state-indicator__alert-cursor-lock icon-cursor-lock" title="Cursor is point locked. Click anywhere in the plot to unlock."></div>
<div class="plot-legend-item"
ng-class="{
'is-status--missing': series.domainObject.status === 'missing'
}"
ng-repeat="series in series track by $index"
>
<div class="plot-series-swatch-and-name">
<span class="plot-series-color-swatch"
ng-style="{ 'background-color': series.get('color').asHexString() }">
</span>
<span class="is-status__indicator" title="This item is missing or suspect"></span>
<span class="plot-series-name">{{ series.nameWithUnit() }}</span>
</div>
<div class="plot-series-value hover-value-enabled value-to-display-{{ legend.get('valueToShowWhenCollapsed') }} {{ series.closest.mctLimitState.cssClass }}"
ng-class="{ 'cursor-hover': (legend.get('valueToShowWhenCollapsed').indexOf('nearest') != -1) }"
ng-show="!!highlights.length && legend.get('valueToShowWhenCollapsed') !== 'none'">
{{ legend.get('valueToShowWhenCollapsed') === 'nearestValue' ?
series.formatY(series.closest) :
legend.get('valueToShowWhenCollapsed') === 'nearestTimestamp' ?
series.closest && series.formatX(series.closest) :
series.formatY(series.get('stats')[legend.get('valueToShowWhenCollapsed') + 'Point']);
}}
</div>
</div>
</div>
<!-- EXPANDED PLOT LEGEND -->
<div class="plot-wrapper-expanded-legend"
ng-class="{'is-cursor-locked': !!lockHighlightPoint }"
>
<div class="c-state-indicator__alert-cursor-lock--verbose icon-cursor-lock" title="Click anywhere in the plot to unlock."> Cursor locked to point</div>
<table>
<thead>
<tr>
<th>Name</th>
<th ng-if="legend.get('showTimestampWhenExpanded')">
Timestamp
</th>
<th ng-if="legend.get('showValueWhenExpanded')">
Value
</th>
<th ng-if="legend.get('showUnitsWhenExpanded')">
Unit
</th>
<th ng-if="legend.get('showMinimumWhenExpanded')"
class="mobile-hide">
Min
</th>
<th ng-if="legend.get('showMaximumWhenExpanded')"
class="mobile-hide">
Max
</th>
</tr>
</thead>
<tr ng-repeat="series in series"
class="plot-legend-item"
ng-class="{
'is-status--missing': series.domainObject.status === 'missing'
}"
>
<td class="plot-series-swatch-and-name">
<span class="plot-series-color-swatch"
ng-style="{ 'background-color': series.get('color').asHexString() }">
</span>
<span class="is-status__indicator" title="This item is missing or suspect"></span>
<span class="plot-series-name">{{ series.get('name') }}</span>
</td>
<td ng-if="legend.get('showTimestampWhenExpanded')">
<span class="plot-series-value cursor-hover hover-value-enabled">
{{ series.closest && series.formatX(series.closest) }}
</span>
</td>
<td ng-if="legend.get('showValueWhenExpanded')">
<span class="plot-series-value cursor-hover hover-value-enabled"
ng-class="series.closest.mctLimitState.cssClass">
{{ series.formatY(series.closest) }}
</span>
</td>
<td ng-if="legend.get('showUnitsWhenExpanded')">
<span class="plot-series-value cursor-hover hover-value-enabled">
{{ series.get('unit') }}
</span>
</td>
<td ng-if="legend.get('showMinimumWhenExpanded')"
class="mobile-hide">
<span class="plot-series-value">
{{ series.formatY(series.get('stats').minPoint) }}
</span>
</td>
<td ng-if="legend.get('showMaximumWhenExpanded')"
class="mobile-hide">
<span class="plot-series-value">
{{ series.formatY(series.get('stats').maxPoint) }}
</span>
</td>
</tr>
</table>
</div>
</div>
</div>
<div class="plot-wrapper-axis-and-display-area flex-elem grows">
<div class="gl-plot-axis-area gl-plot-y has-local-controls"
ng-style="{
width: (tickWidth + 20) + 'px'
}">
<div class="gl-plot-label gl-plot-y-label"
ng-class="{'icon-gear': (yKeyOptions.length > 1 && series.length === 1)}"
>{{yAxis.get('label')}}
</div>
<select class="gl-plot-y-label__select local-controls--hidden"
ng-if="yKeyOptions.length > 1 && series.length === 1"
ng-model="yAxisLabel" ng-change="plot.toggleYAxisLabel(yAxisLabel, yKeyOptions, series[0])">
<option ng-repeat="option in yKeyOptions"
value="{{option.name}}"
ng-selected="option.name === yAxisLabel">
{{option.name}}
</option>
</select>
<mct-ticks axis="yAxis">
<div ng-repeat="tick in ticks track by tick.value"
class="gl-plot-tick gl-plot-y-tick-label"
ng-style="{ top: (100 * (max - tick.value) / interval) + '%' }"
title="{{:: tick.fullText || tick.text }}"
style="margin-top: -0.50em; direction: ltr;">
<span>{{:: tick.text}}</span>
</div>
</mct-ticks>
</div>
<div class="gl-plot-wrapper-display-area-and-x-axis"
ng-style="{
left: (tickWidth + 20) + 'px'
}">
<div class="gl-plot-display-area has-local-controls has-cursor-guides">
<div class="l-state-indicators">
<span class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle"
title="This plot is not currently displaying the latest data. Reset pan/zoom to view latest data."></span>
</div>
<mct-ticks axis="xAxis">
<div class="gl-plot-hash hash-v"
ng-repeat="tick in ticks track by tick.value"
ng-style="{
right: (100 * (max - tick.value) / interval) + '%',
height: '100%'
}"
ng-show="plot.gridLines"
>
</div>
</mct-ticks>
<mct-ticks axis="yAxis">
<div class="gl-plot-hash hash-h"
ng-repeat="tick in ticks track by tick.value"
ng-style="{ bottom: (100 * (tick.value - min) / interval) + '%', width: '100%' }"
ng-show="plot.gridLines"
>
</div>
</mct-ticks>
<mct-chart config="config"
series="series"
rectangles="rectangles"
highlights="highlights"
the-x-axis="xAxis"
the-y-axis="yAxis">
</mct-chart>
<div class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover">
<div class="c-button-set c-button-set--strip-h">
<button class="c-button icon-minus"
ng-click="plot.zoom('out', 0.2)"
title="Zoom out">
</button>
<button class="c-button icon-plus"
ng-click="plot.zoom('in', 0.2)"
title="Zoom in">
</button>
</div>
<div class="c-button-set c-button-set--strip-h"
ng-disabled="!plotHistory.length">
<button class="c-button icon-arrow-left"
ng-click="plot.back()"
title="Restore previous pan/zoom">
</button>
<button class="c-button icon-reset"
ng-click="plot.clear()"
title="Reset pan/zoom">
</button>
</div>
</div>
<!--Cursor guides-->
<div class="c-cursor-guide--v js-cursor-guide--v"
ng-show="plot.cursorGuide">
</div>
<div class="c-cursor-guide--h js-cursor-guide--h"
ng-show="plot.cursorGuide">
</div>
</div>
<div class="gl-plot-axis-area gl-plot-x has-local-controls">
<mct-ticks axis="xAxis">
<div ng-repeat="tick in ticks track by tick.text"
class="gl-plot-tick gl-plot-x-tick-label"
ng-style="{
left: (100 * (tick.value - min) / interval) + '%'
}"
ng-title=":: tick.fullText || tick.text">
{{:: tick.text }}
</div>
</mct-ticks>
<div
class="gl-plot-label gl-plot-x-label"
ng-class="{'icon-gear': isEnabledXKeyToggle()}"
>
{{ xAxis.get('label') }}
</div>
<select
ng-show="plot.isEnabledXKeyToggle()"
ng-model="selectedXKeyOption.key"
ng-change="plot.toggleXKeyOption('{{selectedXKeyOption.key}}', series[0])"
class="gl-plot-x-label__select local-controls--hidden"
ng-options="option.key as option.name for option in xKeyOptions"
>
</select>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,148 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div ng-controller="PlotOptionsController">
<ul class="c-tree">
<h2 title="Plot series display properties in this object">Plot Series</h2>
<li ng-repeat="series in config.series.models">
<div class="c-tree__item menus-to-left">
<span class='c-disclosure-triangle is-enabled flex-elem'
ng-class="{ 'c-disclosure-triangle--expanded': series.expanded }"
ng-click="series.expanded = !series.expanded">
</span>
<mct-representation
class="rep-object-label c-tree__item__label"
key="'label'"
mct-object="series.oldObject">
</mct-representation>
</div>
<ul class="grid-properties" ng-show="series.expanded">
<li class="grid-row">
<div class="grid-cell label"
title="The field to be plotted as a value for this series.">Value</div>
<div class="grid-cell value">
{{series.get('yKey')}}
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="The rendering method to join lines for this series.">Line Method</div>
<div class="grid-cell value">{{ {
'none': 'None',
'linear': 'Linear interpolation',
'stepAfter': 'Step After'
}[series.get('interpolate')] }}
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Whether markers are displayed, and their size.">Markers</div>
<div class="grid-cell value">
{{ series.markerOptionsDisplayText() }}
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Display markers visually denoting points in alarm.">Alarm Markers</div>
<div class="grid-cell value">
{{series.get('alarmMarkers') ? "Enabled" : "Disabled"}}
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="The plot line and marker color for this series.">Color</div>
<div class="grid-cell value">
<span class="c-color-swatch"
ng-style="{
'background': series.get('color').asHexString()
}">
</span>
</div>
</li>
</ul>
</li><!-- end repeat -->
</ul>
<div class="grid-properties">
<ul class="l-inspector-part">
<h2 title="Y axis settings for this object">Y Axis</h2>
<li class="grid-row">
<div class="grid-cell label"
title="Manually override how the Y axis is labeled.">Label</div>
<div class="grid-cell value">{{ config.yAxis.get('label') ? config.yAxis.get('label') : "Not defined" }}</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Automatically scale the Y axis to keep all values in view.">Autoscale</div>
<div class="grid-cell value">
{{ config.yAxis.get('autoscale') ? "Enabled: " : "Disabled" }}
{{ config.yAxis.get('autoscale') ? (config.yAxis.get('autoscalePadding')) : ""}}
</div>
</li>
<li class="grid-row" ng-if="!form.yAxis.autoscale">
<div class="grid-cell label"
title="Minimum Y axis value.">Minimum value</div>
<div class="grid-cell value">{{ config.yAxis.get('range').min }}</div>
</li>
<li class="grid-row" ng-if="!form.yAxis.autoscale">
<div class="grid-cell label"
title="Maximum Y axis value.">Maximum value</div>
<div class="grid-cell value">{{ config.yAxis.get('range').max }}</div>
</li>
</ul>
<ul class="l-inspector-part">
<h2 title="Legend settings for this object">Legend</h2>
<li class="grid-row">
<div class="grid-cell label"
title="The position of the legend relative to the plot display area.">Position</div>
<div class="grid-cell value capitalize">{{ config.legend.get('position') }}</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Hide the legend when the plot is small">Hide when plot small</div>
<div class="grid-cell value">{{ config.legend.get('hideLegendWhenSmall') ? "Yes" : "No" }}</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Show the legend expanded by default">Expand by Default</div>
<div class="grid-cell value">{{ config.legend.get('expandByDefault') ? "Yes" : "No" }}</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="What to display in the legend when it's collapsed.">Show when collapsed:</div>
<div class="grid-cell value">{{
config.legend.get('valueToShowWhenCollapsed').replace('nearest', '')
}}
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="What to display in the legend when it's expanded.">Show when expanded:</div>
<div class="grid-cell value comma-list">
<span ng-if="config.legend.get('showTimestampWhenExpanded')">Timestamp</span>
<span ng-if="config.legend.get('showValueWhenExpanded')">Value</span>
<span ng-if="config.legend.get('showMinimumWhenExpanded')">Min</span>
<span ng-if="config.legend.get('showMaximumWhenExpanded')">Max</span>
<span ng-if="config.legend.get('showUnitsWhenExpanded')">Units</span>
</div>
</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,229 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div ng-controller="PlotOptionsController">
<ul class="c-tree">
<h2 title="Display properties for this object">Plot Series</h2>
<li ng-repeat="series in plotSeries"
ng-controller="PlotSeriesFormController"
form-model="series">
<div class="c-tree__item menus-to-left">
<span class='c-disclosure-triangle is-enabled flex-elem'
ng-class="{ 'c-disclosure-triangle--expanded': expanded }"
ng-click="expanded = !expanded">
</span>
<mct-representation class="rep-object-label c-tree__item__label"
key="'label'"
mct-object="series.oldObject">
</mct-representation>
</div>
<ul class="grid-properties" ng-show="expanded">
<li class="grid-row">
<!-- Value to be displayed -->
<div class="grid-cell label"
title="The field to be plotted as a value for this series.">Value</div>
<div class="grid-cell value">
<select ng-model="form.yKey">
<option ng-repeat="option in yKeyOptions"
value="{{option.value}}"
ng-selected="option.value == form.yKey">
{{option.name}}
</option>
</select>
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="The rendering method to join lines for this series.">Line Method</div>
<div class="grid-cell value">
<select ng-model="form.interpolate">
<option value="none">None</option>
<option value="linear">Linear interpolate</option>
<option value="stepAfter">Step after</option>
</select>
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Whether markers are displayed.">Markers</div>
<div class="grid-cell value">
<input type="checkbox" ng-model="form.markers"/>
<select
ng-show="form.markers"
ng-model="form.markerShape">
<option
ng-repeat="option in markerShapeOptions"
value="{{ option.value }}"
ng-selected="option.value == form.markerShape"
>
{{ option.name }}
</option>
</select>
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Display markers visually denoting points in alarm.">Alarm Markers</div>
<div class="grid-cell value">
<input type="checkbox" ng-model="form.alarmMarkers"/>
</div>
</li>
<li class="grid-row" ng-show="form.markers || form.alarmMarkers">
<div class="grid-cell label"
title="The size of regular and alarm markers for this series.">Marker Size:</div>
<div class="grid-cell value"><input class="c-input--flex" type="text" ng-model="form.markerSize"/></div>
</li>
<li class="grid-row"
ng-controller="ClickAwayController as toggle"
ng-show="form.interpolate !== 'none' || form.markers">
<div class="grid-cell label"
title="Manually set the plot line and marker color for this series.">Color</div>
<div class="grid-cell value">
<div class="c-click-swatch c-click-swatch--menu" ng-click="toggle.toggle()">
<span class="c-color-swatch"
ng-style="{ background: series.get('color').asHexString() }">
</span>
</div>
<div class="c-palette c-palette--color">
<div class="c-palette__items" ng-show="toggle.isActive()">
<div class="u-contents" ng-repeat="group in config.series.palette.groups()">
<div class="c-palette__item"
ng-repeat="color in group"
ng-class="{ 'selected': series.get('color').equalTo(color) }"
ng-style="{ background: color.asHexString() }"
ng-click="setColor(color)">
</div>
</div>
</div>
</div>
</div>
</li>
</ul>
</li>
</ul>
<div class="grid-properties"
ng-show="!!config.series.models.length"
ng-controller="PlotYAxisFormController"
form-model="config.yAxis">
<ul class="l-inspector-part">
<h2>Y Axis</h2>
<li class="grid-row">
<div class="grid-cell label"
title="Manually override how the Y axis is labeled.">Label</div>
<div class="grid-cell value"><input class="c-input--flex" type="text" ng-model="form.label"/></div>
</li>
</ul>
<ul class="l-inspector-part">
<h2>Y Axis Scaling</h2>
<li class="grid-row">
<div class="grid-cell label"
title="Automatically scale the Y axis to keep all values in view.">Autoscale</div>
<div class="grid-cell value"><input type="checkbox" ng-model="form.autoscale"/></div>
</li>
<li class="grid-row" ng-show="form.autoscale">
<div class="grid-cell label"
title="Percentage of padding above and below plotted min and max values. 0.1, 1.0, etc.">
Padding</div>
<div class="grid-cell value">
<input class="c-input--flex" type="text" ng-model="form.autoscalePadding"/>
</div>
</li>
</ul>
<ul class="l-inspector-part" ng-show="!form.autoscale">
<div class="grid-span-all form-error"
ng-show="!form.autoscale && validation.range">
{{ validation.range }}
</div>
<li class="grid-row force-border">
<div class="grid-cell label"
title="Minimum Y axis value.">Minimum Value</div>
<div class="grid-cell value">
<input class="c-input--flex" type="number" ng-model="form.range.min"/>
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Maximum Y axis value.">Maximum Value</div>
<div class="grid-cell value"><input class="c-input--flex" type="number" ng-model="form.range.max"/></div>
</li>
</ul>
</div>
<div class="grid-properties" ng-show="!!config.series.models.length">
<ul class="l-inspector-part" ng-controller="PlotLegendFormController" form-model="config.legend">
<h2 title="Legend options">Legend</h2>
<li class="grid-row">
<div class="grid-cell label"
title="The position of the legend relative to the plot display area.">Position</div>
<div class="grid-cell value">
<select ng-model="form.position">
<option value="top">Top</option>
<option value="right">Right</option>
<option value="bottom">Bottom</option>
<option value="left">Left</option>
</select>
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Hide the legend when the plot is small">Hide when plot small</div>
<div class="grid-cell value"><input type="checkbox" ng-model="form.hideLegendWhenSmall"/></div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Show the legend expanded by default">Expand by default</div>
<div class="grid-cell value"><input type="checkbox" ng-model="form.expandByDefault"/></div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="What to display in the legend when it's collapsed.">When collapsed show</div>
<div class="grid-cell value">
<select ng-model="form.valueToShowWhenCollapsed">
<option value="none">Nothing</option>
<option value="nearestTimestamp">Nearest timestamp</option>
<option value="nearestValue">Nearest value</option>
<option value="min">Minimum value</option>
<option value="max">Maximum value</option>
<option value="units">showUnitsWhenExpanded</option>
</select>
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="What to display in the legend when it's expanded.">When expanded show</div>
<div class="grid-cell value">
<ul>
<li><input type="checkbox"
ng-model="form.showTimestampWhenExpanded"/> Nearest timestamp</li>
<li><input type="checkbox"
ng-model="form.showValueWhenExpanded"/> Nearest value</li>
<li><input type="checkbox"
ng-model="form.showMinimumWhenExpanded"/> Minimum value</li>
<li><input type="checkbox"
ng-model="form.showMaximumWhenExpanded"/> Maximum value</li>
<li><input type="checkbox"
ng-model="form.showUnitsWhenExpanded"/> Units</li>
</ul>
</div>
</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,31 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div ng-if="!domainObject.model.locked && domainObject.getCapability('editor').inEditContext()">
<mct-representation key="'plot-options-edit'"
mct-object="domainObject">
</mct-representation>
</div>
<div ng-if="domainObject.model.locked || !domainObject.getCapability('editor').inEditContext()">
<mct-representation key="'plot-options-browse'"
mct-object="domainObject">
</mct-representation>
</div>

View File

@@ -0,0 +1,58 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div ng-controller="PlotController as controller"
class="c-plot holder holder-plot has-control-bar">
<div class="c-control-bar" ng-show="!controller.hideExportButtons">
<span class="c-button-set c-button-set--strip-h">
<button class="c-button icon-download"
ng-click="controller.exportPNG()"
title="Export This View's Data as PNG">
<span class="c-button__label">PNG</span>
</button>
<button class="c-button"
ng-click="controller.exportJPG()"
title="Export This View's Data as JPG">
<span class="c-button__label">JPG</span>
</button>
</span>
<button class="c-button icon-crosshair"
ng-class="{ 'is-active': controller.cursorGuide }"
ng-click="controller.toggleCursorGuide($event)"
title="Toggle cursor guides">
</button>
<button class="c-button"
ng-class="{ 'icon-grid-on': controller.gridLines, 'icon-grid-off': !controller.gridLines }"
ng-click="controller.toggleGridLines($event)"
title="Toggle grid lines">
</button>
</div>
<div class="l-view-section u-style-receiver js-style-receiver">
<div class="c-loading--overlay loading"
ng-show="!!pending"></div>
<mct-plot config="controller.config"
series="series"
the-y-axis="yAxis"
the-x-axis="xAxis">
</mct-plot>
</div>
</div>

View File

@@ -0,0 +1,65 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div ng-controller="StackedPlotController as stackedPlot"
class="c-plot c-plot--stacked holder holder-plot has-control-bar">
<div class="c-control-bar" ng-show="!stackedPlot.hideExportButtons">
<span class="c-button-set c-button-set--strip-h">
<button class="c-button icon-download"
ng-click="stackedPlot.exportPNG()"
title="Export This View's Data as PNG">
<span class="c-button__label">PNG</span>
</button>
<button class="c-button"
ng-click="stackedPlot.exportJPG()"
title="Export This View's Data as JPG">
<span class="c-button__label">JPG</span>
</button>
</span>
<button class="c-button icon-crosshair"
ng-class="{ 'is-active': stackedPlot.cursorGuide }"
ng-click="stackedPlot.toggleCursorGuide($event)"
title="Toggle cursor guides">
</button>
<button class="c-button"
ng-class="{ 'icon-grid-on': stackedPlot.gridLines, 'icon-grid-off': !stackedPlot.gridLines }"
ng-click="stackedPlot.toggleGridLines($event)"
title="Toggle grid lines">
</button>
</div>
<div class="l-view-section u-style-receiver js-style-receiver">
<div class="c-loading--overlay loading"
ng-show="!!currentRequest.pending"></div>
<div class="gl-plot child-frame u-inspectable"
ng-repeat="telemetryObject in telemetryObjects"
ng-class="{
's-status-timeconductor-unsynced': telemetryObject
.getCapability('status')
.get('timeconductor-unsynced')
}"
mct-selectable="{
item: telemetryObject.useCapability('adapter'),
oldItem: telemetryObject
}">
<mct-overlay-plot domain-object="telemetryObject"></mct-overlay-plot>
</div>
</div>
</div>

View File

@@ -0,0 +1,68 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
function () {
/**
* Policy preventing the Plot view from being made available for
* domain objects which have non-numeric telemetry.
* @implements {Policy.<View, DomainObject>}
* @constructor
* @memberof platform/features/plot
*/
function PlotViewPolicy(openmct) {
this.openmct = openmct;
}
PlotViewPolicy.prototype.hasNumericTelemetry = function (domainObject) {
const adaptedObject = domainObject.useCapability('adapter');
if (!adaptedObject.telemetry) {
return domainObject.hasCapability('delegation')
&& domainObject.getCapability('delegation')
.doesDelegateCapability('telemetry');
}
const metadata = this.openmct.telemetry.getMetadata(adaptedObject);
const rangeValues = metadata.valuesForHints(['range']);
if (rangeValues.length === 0) {
return false;
}
return !rangeValues.every(function (value) {
return value.format === 'string';
});
};
PlotViewPolicy.prototype.allow = function (view, domainObject) {
if (view.key === 'plot-single') {
return this.hasNumericTelemetry(domainObject);
}
return true;
};
return PlotViewPolicy;
}
);

View File

@@ -0,0 +1,74 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'../lib/extend',
'../lib/eventHelpers'
], function (
extend,
eventHelpers
) {
function MCTChartAlarmPointSet(series, chart, offset) {
this.series = series;
this.chart = chart;
this.offset = offset;
this.points = [];
this.listenTo(series, 'add', this.append, this);
this.listenTo(series, 'remove', this.remove, this);
this.listenTo(series, 'reset', this.reset, this);
this.listenTo(series, 'destroy', this.destroy, this);
series.data.forEach(function (point, index) {
this.append(point, index, series);
}, this);
}
MCTChartAlarmPointSet.prototype.append = function (datum) {
if (datum.mctLimitState) {
this.points.push({
x: this.offset.xVal(datum, this.series),
y: this.offset.yVal(datum, this.series),
datum: datum
});
}
};
MCTChartAlarmPointSet.prototype.remove = function (datum) {
this.points = this.points.filter(function (p) {
return p.datum !== datum;
});
};
MCTChartAlarmPointSet.prototype.reset = function () {
this.points = [];
};
MCTChartAlarmPointSet.prototype.destroy = function () {
this.stopListening();
};
eventHelpers.extend(MCTChartAlarmPointSet.prototype);
return MCTChartAlarmPointSet;
});

View File

@@ -0,0 +1,441 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Module defining MCTChart. Created by vwoeltje on 11/12/14.
*/
define([
'./MCTChartLineLinear',
'./MCTChartLineStepAfter',
'./MCTChartPointSet',
'./MCTChartAlarmPointSet',
'../draw/DrawLoader',
'../lib/eventHelpers'
],
function (
MCTChartLineLinear,
MCTChartLineStepAfter,
MCTChartPointSet,
MCTChartAlarmPointSet,
DrawLoader,
eventHelpers
) {
const MARKER_SIZE = 6.0;
const HIGHLIGHT_SIZE = MARKER_SIZE * 2.0;
/**
* Offsetter adjusts x and y values by a fixed amount,
* generally increasing the precision of the 32 bit float representation
* required for plotting.
*
* @constructor
*/
function MCTChartController($scope) {
this.$onInit = () => {
this.$scope = $scope;
this.isDestroyed = false;
this.lines = [];
this.pointSets = [];
this.alarmSets = [];
this.offset = {};
this.config = $scope.config;
this.listenTo(this.$scope, '$destroy', this.destroy, this);
this.draw = this.draw.bind(this);
this.scheduleDraw = this.scheduleDraw.bind(this);
this.seriesElements = new WeakMap();
this.listenTo(this.config.series, 'add', this.onSeriesAdd, this);
this.listenTo(this.config.series, 'remove', this.onSeriesRemove, this);
this.listenTo(this.config.yAxis, 'change:key', this.clearOffset, this);
this.listenTo(this.config.yAxis, 'change', this.scheduleDraw);
this.listenTo(this.config.xAxis, 'change', this.scheduleDraw);
this.$scope.$watch('highlights', this.scheduleDraw);
this.$scope.$watch('rectangles', this.scheduleDraw);
this.config.series.forEach(this.onSeriesAdd, this);
};
}
eventHelpers.extend(MCTChartController.prototype);
MCTChartController.$inject = ['$scope'];
MCTChartController.prototype.reDraw = function (mode, o, series) {
this.changeInterpolate(mode, o, series);
this.changeMarkers(mode, o, series);
this.changeAlarmMarkers(mode, o, series);
};
MCTChartController.prototype.onSeriesAdd = function (series) {
this.listenTo(series, 'change:xKey', this.reDraw, this);
this.listenTo(series, 'change:interpolate', this.changeInterpolate, this);
this.listenTo(series, 'change:markers', this.changeMarkers, this);
this.listenTo(series, 'change:alarmMarkers', this.changeAlarmMarkers, this);
this.listenTo(series, 'change', this.scheduleDraw);
this.listenTo(series, 'add', this.scheduleDraw);
this.makeChartElement(series);
};
MCTChartController.prototype.changeInterpolate = function (mode, o, series) {
if (mode === o) {
return;
}
const elements = this.seriesElements.get(series);
elements.lines.forEach(function (line) {
this.lines.splice(this.lines.indexOf(line), 1);
line.destroy();
}, this);
elements.lines = [];
const newLine = this.lineForSeries(series);
if (newLine) {
elements.lines.push(newLine);
this.lines.push(newLine);
}
};
MCTChartController.prototype.changeAlarmMarkers = function (mode, o, series) {
if (mode === o) {
return;
}
const elements = this.seriesElements.get(series);
if (elements.alarmSet) {
elements.alarmSet.destroy();
this.alarmSets.splice(this.alarmSets.indexOf(elements.alarmSet), 1);
}
elements.alarmSet = this.alarmPointSetForSeries(series);
if (elements.alarmSet) {
this.alarmSets.push(elements.alarmSet);
}
};
MCTChartController.prototype.changeMarkers = function (mode, o, series) {
if (mode === o) {
return;
}
const elements = this.seriesElements.get(series);
elements.pointSets.forEach(function (pointSet) {
this.pointSets.splice(this.pointSets.indexOf(pointSet), 1);
pointSet.destroy();
}, this);
elements.pointSets = [];
const pointSet = this.pointSetForSeries(series);
if (pointSet) {
elements.pointSets.push(pointSet);
this.pointSets.push(pointSet);
}
};
MCTChartController.prototype.onSeriesRemove = function (series) {
this.stopListening(series);
this.removeChartElement(series);
this.scheduleDraw();
};
MCTChartController.prototype.destroy = function () {
this.isDestroyed = true;
this.stopListening();
this.lines.forEach(line => line.destroy());
DrawLoader.releaseDrawAPI(this.drawAPI);
};
MCTChartController.prototype.clearOffset = function () {
delete this.offset.x;
delete this.offset.y;
delete this.offset.xVal;
delete this.offset.yVal;
delete this.offset.xKey;
delete this.offset.yKey;
this.lines.forEach(function (line) {
line.reset();
});
this.pointSets.forEach(function (pointSet) {
pointSet.reset();
});
};
MCTChartController.prototype.setOffset = function (offsetPoint, index, series) {
if (this.offset.x && this.offset.y) {
return;
}
const offsets = {
x: series.getXVal(offsetPoint),
y: series.getYVal(offsetPoint)
};
this.offset.x = function (x) {
return x - offsets.x;
}.bind(this);
this.offset.y = function (y) {
return y - offsets.y;
}.bind(this);
this.offset.xVal = function (point, pSeries) {
return this.offset.x(pSeries.getXVal(point));
}.bind(this);
this.offset.yVal = function (point, pSeries) {
return this.offset.y(pSeries.getYVal(point));
}.bind(this);
};
MCTChartController.prototype.initializeCanvas = function (canvas, overlay) {
this.canvas = canvas;
this.overlay = overlay;
this.drawAPI = DrawLoader.getDrawAPI(canvas, overlay);
if (this.drawAPI) {
this.listenTo(this.drawAPI, 'error', this.fallbackToCanvas, this);
}
return Boolean(this.drawAPI);
};
MCTChartController.prototype.fallbackToCanvas = function () {
this.stopListening(this.drawAPI);
DrawLoader.releaseDrawAPI(this.drawAPI);
// Have to throw away the old canvas elements and replace with new
// canvas elements in order to get new drawing contexts.
const div = document.createElement('div');
div.innerHTML = this.TEMPLATE;
const mainCanvas = div.querySelectorAll("canvas")[1];
const overlayCanvas = div.querySelectorAll("canvas")[0];
this.canvas.parentNode.replaceChild(mainCanvas, this.canvas);
this.canvas = mainCanvas;
this.overlay.parentNode.replaceChild(overlayCanvas, this.overlay);
this.overlay = overlayCanvas;
this.drawAPI = DrawLoader.getFallbackDrawAPI(this.canvas, this.overlay);
this.$scope.$emit('plot:reinitializeCanvas');
};
MCTChartController.prototype.removeChartElement = function (series) {
const elements = this.seriesElements.get(series);
elements.lines.forEach(function (line) {
this.lines.splice(this.lines.indexOf(line), 1);
line.destroy();
}, this);
elements.pointSets.forEach(function (pointSet) {
this.pointSets.splice(this.pointSets.indexOf(pointSet), 1);
pointSet.destroy();
}, this);
if (elements.alarmSet) {
elements.alarmSet.destroy();
this.alarmSets.splice(this.alarmSets.indexOf(elements.alarmSet), 1);
}
this.seriesElements.delete(series);
};
MCTChartController.prototype.lineForSeries = function (series) {
if (series.get('interpolate') === 'linear') {
return new MCTChartLineLinear(
series,
this,
this.offset
);
}
if (series.get('interpolate') === 'stepAfter') {
return new MCTChartLineStepAfter(
series,
this,
this.offset
);
}
};
MCTChartController.prototype.pointSetForSeries = function (series) {
if (series.get('markers')) {
return new MCTChartPointSet(
series,
this,
this.offset
);
}
};
MCTChartController.prototype.alarmPointSetForSeries = function (series) {
if (series.get('alarmMarkers')) {
return new MCTChartAlarmPointSet(
series,
this,
this.offset
);
}
};
MCTChartController.prototype.makeChartElement = function (series) {
const elements = {
lines: [],
pointSets: []
};
const line = this.lineForSeries(series);
if (line) {
elements.lines.push(line);
this.lines.push(line);
}
const pointSet = this.pointSetForSeries(series);
if (pointSet) {
elements.pointSets.push(pointSet);
this.pointSets.push(pointSet);
}
elements.alarmSet = this.alarmPointSetForSeries(series);
if (elements.alarmSet) {
this.alarmSets.push(elements.alarmSet);
}
this.seriesElements.set(series, elements);
};
MCTChartController.prototype.canDraw = function () {
if (!this.offset.x || !this.offset.y) {
return false;
}
return true;
};
MCTChartController.prototype.scheduleDraw = function () {
if (!this.drawScheduled) {
requestAnimationFrame(this.draw);
this.drawScheduled = true;
}
};
MCTChartController.prototype.draw = function () {
this.drawScheduled = false;
if (this.isDestroyed) {
return;
}
this.drawAPI.clear();
if (this.canDraw()) {
this.updateViewport();
this.drawSeries();
this.drawRectangles();
this.drawHighlights();
}
};
MCTChartController.prototype.updateViewport = function () {
const xRange = this.config.xAxis.get('displayRange');
const yRange = this.config.yAxis.get('displayRange');
if (!xRange || !yRange) {
return;
}
const dimensions = [
xRange.max - xRange.min,
yRange.max - yRange.min
];
const origin = [
this.offset.x(xRange.min),
this.offset.y(yRange.min)
];
this.drawAPI.setDimensions(
dimensions,
origin
);
};
MCTChartController.prototype.drawSeries = function () {
this.lines.forEach(this.drawLine, this);
this.pointSets.forEach(this.drawPoints, this);
this.alarmSets.forEach(this.drawAlarmPoints, this);
};
MCTChartController.prototype.drawAlarmPoints = function (alarmSet) {
this.drawAPI.drawLimitPoints(
alarmSet.points,
alarmSet.series.get('color').asRGBAArray(),
alarmSet.series.get('markerSize')
);
};
MCTChartController.prototype.drawPoints = function (chartElement) {
this.drawAPI.drawPoints(
chartElement.getBuffer(),
chartElement.color().asRGBAArray(),
chartElement.count,
chartElement.series.get('markerSize'),
chartElement.series.get('markerShape')
);
};
MCTChartController.prototype.drawLine = function (chartElement) {
this.drawAPI.drawLine(
chartElement.getBuffer(),
chartElement.color().asRGBAArray(),
chartElement.count
);
};
MCTChartController.prototype.drawHighlights = function () {
if (this.$scope.highlights && this.$scope.highlights.length) {
this.$scope.highlights.forEach(this.drawHighlight, this);
}
};
MCTChartController.prototype.drawHighlight = function (highlight) {
const points = new Float32Array([
this.offset.xVal(highlight.point, highlight.series),
this.offset.yVal(highlight.point, highlight.series)
]);
const color = highlight.series.get('color').asRGBAArray();
const pointCount = 1;
const shape = highlight.series.get('markerShape');
this.drawAPI.drawPoints(points, color, pointCount, HIGHLIGHT_SIZE, shape);
};
MCTChartController.prototype.drawRectangles = function () {
if (this.$scope.rectangles) {
this.$scope.rectangles.forEach(this.drawRectangle, this);
}
};
MCTChartController.prototype.drawRectangle = function (rect) {
this.drawAPI.drawSquare(
[
this.offset.x(rect.start.x),
this.offset.y(rect.start.y)
],
[
this.offset.x(rect.end.x),
this.offset.y(rect.end.y)
],
rect.color
);
};
return MCTChartController;
});

View File

@@ -0,0 +1,67 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Module defining MCTChart. Created by vwoeltje on 11/12/14.
*/
define([
'./MCTChartController'
], function (
MCTChartController
) {
let TEMPLATE = "<canvas style='position: absolute; background: none; width: 100%; height: 100%;'></canvas>";
TEMPLATE += TEMPLATE;
/**
* MCTChart draws charts utilizing a drawAPI.
*
* @constructor
*/
function MCTChart() {
return {
restrict: "E",
template: TEMPLATE,
link: function ($scope, $element, attrs, ctrl) {
ctrl.TEMPLATE = TEMPLATE;
const mainCanvas = $element.find("canvas")[1];
const overlayCanvas = $element.find("canvas")[0];
if (ctrl.initializeCanvas(mainCanvas, overlayCanvas)) {
ctrl.draw();
}
},
controller: MCTChartController,
scope: {
config: "=",
draw: "=",
rectangles: "=",
series: "=",
xAxis: "=theXAxis",
yAxis: "=theYAxis",
highlights: "=?"
}
};
}
return MCTChart;
});

View File

@@ -0,0 +1,39 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./MCTChartSeriesElement'
], function (
MCTChartSeriesElement
) {
const MCTChartLineLinear = MCTChartSeriesElement.extend({
addPoint: function (point, start, count) {
this.buffer[start] = point.x;
this.buffer[start + 1] = point.y;
}
});
return MCTChartLineLinear;
});

View File

@@ -0,0 +1,79 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./MCTChartSeriesElement'
], function (
MCTChartSeriesElement
) {
const MCTChartLineStepAfter = MCTChartSeriesElement.extend({
removePoint: function (point, index, count) {
if (index > 0 && index / 2 < this.count) {
this.buffer[index + 1] = this.buffer[index - 1];
}
},
vertexCountForPointAtIndex: function (index) {
if (index === 0 && this.count === 0) {
return 2;
}
return 4;
},
startIndexForPointAtIndex: function (index) {
if (index === 0) {
return 0;
}
return 2 + ((index - 1) * 4);
},
addPoint: function (point, start, count) {
if (start === 0 && this.count === 0) {
// First point is easy.
this.buffer[start] = point.x;
this.buffer[start + 1] = point.y; // one point
} else if (start === 0 && this.count > 0) {
// Unshifting requires adding an extra point.
this.buffer[start] = point.x;
this.buffer[start + 1] = point.y;
this.buffer[start + 2] = this.buffer[start + 4];
this.buffer[start + 3] = point.y;
} else {
// Appending anywhere in line, insert standard two points.
this.buffer[start] = point.x;
this.buffer[start + 1] = this.buffer[start - 1];
this.buffer[start + 2] = point.x;
this.buffer[start + 3] = point.y;
if (start < this.count * 2) {
// Insert into the middle, need to update the following
// point.
this.buffer[start + 5] = point.y;
}
}
}
});
return MCTChartLineStepAfter;
});

View File

@@ -0,0 +1,39 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./MCTChartSeriesElement'
], function (
MCTChartSeriesElement
) {
const MCTChartPointSet = MCTChartSeriesElement.extend({
addPoint: function (point, start, count) {
this.buffer[start] = point.x;
this.buffer[start + 1] = point.y;
}
});
return MCTChartPointSet;
});

View File

@@ -0,0 +1,164 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'../lib/extend',
'../lib/eventHelpers'
], function (
extend,
eventHelpers
) {
function MCTChartSeriesElement(series, chart, offset) {
this.series = series;
this.chart = chart;
this.offset = offset;
this.buffer = new Float32Array(20000);
this.count = 0;
this.listenTo(series, 'add', this.append, this);
this.listenTo(series, 'remove', this.remove, this);
this.listenTo(series, 'reset', this.reset, this);
this.listenTo(series, 'destroy', this.destroy, this);
series.data.forEach(function (point, index) {
this.append(point, index, series);
}, this);
}
MCTChartSeriesElement.extend = extend;
eventHelpers.extend(MCTChartSeriesElement.prototype);
MCTChartSeriesElement.prototype.getBuffer = function () {
if (this.isTempBuffer) {
this.buffer = new Float32Array(this.buffer);
this.isTempBuffer = false;
}
return this.buffer;
};
MCTChartSeriesElement.prototype.color = function () {
return this.series.get('color');
};
MCTChartSeriesElement.prototype.vertexCountForPointAtIndex = function (index) {
return 2;
};
MCTChartSeriesElement.prototype.startIndexForPointAtIndex = function (index) {
return 2 * index;
};
MCTChartSeriesElement.prototype.removeSegments = function (index, count) {
const target = index;
const start = index + count;
const end = this.count * 2;
this.buffer.copyWithin(target, start, end);
for (let zero = end - count; zero < end; zero++) {
this.buffer[zero] = 0;
}
};
MCTChartSeriesElement.prototype.removePoint = function (point, index, count) {
// by default, do nothing.
};
MCTChartSeriesElement.prototype.remove = function (point, index, series) {
const vertexCount = this.vertexCountForPointAtIndex(index);
const removalPoint = this.startIndexForPointAtIndex(index);
this.removeSegments(removalPoint, vertexCount);
this.removePoint(
this.makePoint(point, series),
removalPoint,
vertexCount
);
this.count -= (vertexCount / 2);
};
MCTChartSeriesElement.prototype.makePoint = function (point, series) {
if (!this.offset.xVal) {
this.chart.setOffset(point, undefined, series);
}
return {
x: this.offset.xVal(point, series),
y: this.offset.yVal(point, series)
};
};
MCTChartSeriesElement.prototype.append = function (point, index, series) {
const pointsRequired = this.vertexCountForPointAtIndex(index);
const insertionPoint = this.startIndexForPointAtIndex(index);
this.growIfNeeded(pointsRequired);
this.makeInsertionPoint(insertionPoint, pointsRequired);
this.addPoint(
this.makePoint(point, series),
insertionPoint,
pointsRequired
);
this.count += (pointsRequired / 2);
};
MCTChartSeriesElement.prototype.makeInsertionPoint = function (insertionPoint, pointsRequired) {
if (this.count * 2 > insertionPoint) {
if (!this.isTempBuffer) {
this.buffer = Array.prototype.slice.apply(this.buffer);
this.isTempBuffer = true;
}
const target = insertionPoint + pointsRequired;
let start = insertionPoint;
for (; start < target; start++) {
this.buffer.splice(start, 0, 0);
}
}
};
MCTChartSeriesElement.prototype.reset = function () {
this.buffer = new Float32Array(20000);
this.count = 0;
if (this.offset.x) {
this.series.data.forEach(function (point, index) {
this.append(point, index, this.series);
}, this);
}
};
MCTChartSeriesElement.prototype.growIfNeeded = function (pointsRequired) {
const remainingPoints = this.buffer.length - this.count * 2;
let temp;
if (remainingPoints <= pointsRequired) {
temp = new Float32Array(this.buffer.length + 20000);
temp.set(this.buffer);
this.buffer = temp;
}
};
MCTChartSeriesElement.prototype.destroy = function () {
this.stopListening();
};
return MCTChartSeriesElement;
});

View File

@@ -0,0 +1,130 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'EventEmitter',
'./Model',
'../lib/extend',
'../lib/eventHelpers'
], function (
EventEmitter,
Model,
extend,
eventHelpers
) {
function Collection(options) {
if (options.models) {
this.models = options.models.map(this.modelFn, this);
} else {
this.models = [];
}
this.initialize(options);
}
Object.assign(Collection.prototype, EventEmitter.prototype);
eventHelpers.extend(Collection.prototype);
Collection.extend = extend;
Collection.prototype.initialize = function (options) {
};
Collection.prototype.modelClass = Model;
Collection.prototype.modelFn = function (model) {
if (model instanceof this.modelClass) {
model.collection = this;
return model;
}
return new this.modelClass({
collection: this,
model: model
});
};
Collection.prototype.first = function () {
return this.at(0);
};
Collection.prototype.forEach = function (iteree, context) {
this.models.forEach(iteree, context);
};
Collection.prototype.map = function (iteree, context) {
return this.models.map(iteree, context);
};
Collection.prototype.filter = function (iteree, context) {
return this.models.filter(iteree, context);
};
Collection.prototype.size = function () {
return this.models.length;
};
Collection.prototype.at = function (index) {
return this.models[index];
};
Collection.prototype.add = function (model) {
model = this.modelFn(model);
const index = this.models.length;
this.models.push(model);
this.emit('add', model, index);
};
Collection.prototype.insert = function (model, index) {
model = this.modelFn(model);
this.models.splice(index, 0, model);
this.emit('add', model, index + 1);
};
Collection.prototype.indexOf = function (model) {
return this.models.findIndex(m => m === model);
};
Collection.prototype.remove = function (model) {
const index = this.indexOf(model);
if (index === -1) {
throw new Error('model not found in collection.');
}
this.emit('remove', model, index);
this.models.splice(index, 1);
};
Collection.prototype.destroy = function (model) {
this.forEach(function (m) {
m.destroy();
});
this.stopListening();
};
return Collection;
});

View File

@@ -0,0 +1,63 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./Model'
], function (
Model
) {
/**
* TODO: doc strings.
*/
const LegendModel = Model.extend({
listenToSeriesCollection: function (seriesCollection) {
this.seriesCollection = seriesCollection;
this.listenTo(this.seriesCollection, 'add', this.setHeight, this);
this.listenTo(this.seriesCollection, 'remove', this.setHeight, this);
this.listenTo(this, 'change:expanded', this.setHeight, this);
this.set('expanded', this.get('expandByDefault'));
},
setHeight: function () {
const expanded = this.get('expanded');
if (this.get('position') !== 'top') {
this.set('height', '0px');
} else {
this.set('height', expanded ? (20 * (this.seriesCollection.size() + 1) + 40) + 'px' : '21px');
}
},
defaults: function (options) {
return {
position: 'top',
expandByDefault: false,
hideLegendWhenSmall: false,
valueToShowWhenCollapsed: 'nearestValue',
showTimestampWhenExpanded: true,
showValueWhenExpanded: true,
showMaximumWhenExpanded: true,
showMinimumWhenExpanded: true,
showUnitsWhenExpanded: true
};
}
});
return LegendModel;
});

View File

@@ -0,0 +1,103 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'lodash',
'EventEmitter',
'../lib/extend',
'../lib/eventHelpers'
], function (
_,
EventEmitter,
extend,
eventHelpers
) {
function Model(options) {
if (!options) {
options = {};
}
this.id = options.id;
this.model = options.model;
this.collection = options.collection;
const defaults = this.defaults(options);
if (!this.model) {
this.model = options.model = defaults;
} else {
_.defaultsDeep(this.model, defaults);
}
this.initialize(options);
}
Object.assign(Model.prototype, EventEmitter.prototype);
eventHelpers.extend(Model.prototype);
Model.extend = extend;
Model.prototype.idAttr = 'id';
Model.prototype.defaults = function (options) {
return {};
};
Model.prototype.initialize = function (model) {
};
/**
* Destroy the model, removing all listeners and subscriptions.
*/
Model.prototype.destroy = function () {
this.emit('destroy');
this.removeAllListeners();
};
Model.prototype.id = function () {
return this.get(this.idAttr);
};
Model.prototype.get = function (attribute) {
return this.model[attribute];
};
Model.prototype.has = function (attribute) {
return _.has(this.model, attribute);
};
Model.prototype.set = function (attribute, value) {
const oldValue = this.model[attribute];
this.model[attribute] = value;
this.emit('change', attribute, value, oldValue, this);
this.emit('change:' + attribute, value, oldValue, this);
};
Model.prototype.unset = function (attribute) {
const oldValue = this.model[attribute];
delete this.model[attribute];
this.emit('change', attribute, undefined, oldValue, this);
this.emit('change:' + attribute, undefined, oldValue, this);
};
return Model;
});

View File

@@ -0,0 +1,152 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./Collection',
'./Model',
'./SeriesCollection',
'./XAxisModel',
'./YAxisModel',
'./LegendModel',
'lodash'
], function (
Collection,
Model,
SeriesCollection,
XAxisModel,
YAxisModel,
LegendModel,
_
) {
/**
* PlotConfiguration model stores the configuration of a plot and some
* limited state. The indiidual parts of the plot configuration model
* handle setting defaults and updating in response to various changes.
*
*/
const PlotConfigurationModel = Model.extend({
/**
* Initializes all sub models and then passes references to submodels
* to those that need it.
*/
initialize: function (options) {
this.openmct = options.openmct;
this.xAxis = new XAxisModel({
model: options.model.xAxis,
plot: this,
openmct: options.openmct
});
this.yAxis = new YAxisModel({
model: options.model.yAxis,
plot: this,
openmct: options.openmct
});
this.legend = new LegendModel({
model: options.model.legend,
plot: this,
openmct: options.openmct
});
this.series = new SeriesCollection({
models: options.model.series,
plot: this,
openmct: options.openmct
});
if (this.get('domainObject').type === 'telemetry.plot.overlay') {
this.removeMutationListener = this.openmct.objects.observe(
this.get('domainObject'),
'*',
this.updateDomainObject.bind(this)
);
}
this.yAxis.listenToSeriesCollection(this.series);
this.legend.listenToSeriesCollection(this.series);
this.listenTo(this, 'destroy', this.onDestroy, this);
},
/**
* Retrieve the persisted series config for a given identifier.
*/
getPersistedSeriesConfig: function (identifier) {
const domainObject = this.get('domainObject');
if (!domainObject.configuration || !domainObject.configuration.series) {
return;
}
return domainObject.configuration.series.filter(function (seriesConfig) {
return seriesConfig.identifier.key === identifier.key
&& seriesConfig.identifier.namespace === identifier.namespace;
})[0];
},
/**
* Retrieve the persisted filters for a given identifier.
*/
getPersistedFilters: function (identifier) {
const domainObject = this.get('domainObject');
const keystring = this.openmct.objects.makeKeyString(identifier);
if (!domainObject.configuration || !domainObject.configuration.filters) {
return;
}
return domainObject.configuration.filters[keystring];
},
/**
* Update the domain object with the given value.
*/
updateDomainObject: function (domainObject) {
this.set('domainObject', domainObject);
},
/**
* Clean up all objects and remove all listeners.
*/
onDestroy: function () {
this.xAxis.destroy();
this.yAxis.destroy();
this.series.destroy();
this.legend.destroy();
if (this.removeMutationListener) {
this.removeMutationListener();
}
},
/**
* Return defaults, which are extracted from the passed in domain
* object.
*/
defaults: function (options) {
return {
series: [],
domainObject: options.domainObject,
xAxis: {
},
yAxis: _.cloneDeep(_.get(options.domainObject, 'configuration.yAxis', {})),
legend: _.cloneDeep(_.get(options.domainObject, 'configuration.legend', {}))
};
}
});
return PlotConfigurationModel;
});

View File

@@ -0,0 +1,465 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'lodash',
'../configuration/Model',
'../lib/extend',
'EventEmitter',
'../draw/MarkerShapes'
], function (
_,
Model,
extend,
EventEmitter,
MARKER_SHAPES
) {
/**
* Plot series handle interpreting telemetry metadata for a single telemetry
* object, querying for that data, and formatting it for display purposes.
*
* Plot series emit both collection events and model events:
* `change` when any property changes
* `change:<prop_name>` when a specific property changes.
* `destroy`: when series is destroyed
* `add`: whenever a data point is added to a series
* `remove`: whenever a data point is removed from a series.
* `reset`: whenever the collection is emptied.
*
* Plot series have the following Model properties:
*
* `name`: name of series.
* `identifier`: the Open MCT identifier for the telemetry source for this
* series.
* `xKey`: the telemetry value key for x values fetched from this series.
* `yKey`: the telemetry value key for y values fetched from this series.
* `interpolate`: interpolate method, either `undefined` (no interpolation),
* `linear` (points are connected via straight lines), or
* `stepAfter` (points are connected by steps).
* `markers`: boolean, whether or not this series should render with markers.
* `markerShape`: string, shape of markers.
* `markerSize`: number, size in pixels of markers for this series.
* `alarmMarkers`: whether or not to display alarm markers for this series.
* `stats`: An object that tracks the min and max y values observed in this
* series. This property is checked and updated whenever data is
* added.
*
* Plot series have the following instance properties:
*
* `metadata`: the Open MCT Telemetry Metadata Manager for the associated
* telemetry point.
* `formats`: the Open MCT format map for this telemetry point.
*/
const PlotSeries = Model.extend({
constructor: function (options) {
this.metadata = options
.openmct
.telemetry
.getMetadata(options.domainObject);
this.formats = options
.openmct
.telemetry
.getFormatMap(this.metadata);
this.data = [];
this.listenTo(this, 'change:xKey', this.onXKeyChange, this);
this.listenTo(this, 'change:yKey', this.onYKeyChange, this);
this.persistedConfig = options.persistedConfig;
this.filters = options.filters;
Model.apply(this, arguments);
this.onXKeyChange(this.get('xKey'));
this.onYKeyChange(this.get('yKey'));
},
/**
* Set defaults for telemetry series.
*/
defaults: function (options) {
const range = this.metadata.valuesForHints(['range'])[0];
return {
name: options.domainObject.name,
unit: range.unit,
xKey: options.collection.plot.xAxis.get('key'),
yKey: range.key,
markers: true,
markerShape: 'point',
markerSize: 2.0,
alarmMarkers: true
};
},
/**
* Remove real-time subscription when destroyed.
*/
onDestroy: function (model) {
if (this.unsubscribe) {
this.unsubscribe();
}
},
initialize: function (options) {
this.openmct = options.openmct;
this.domainObject = options.domainObject;
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(options.domainObject);
this.on('destroy', this.onDestroy, this);
},
locateOldObject: function (oldStyleParent) {
return oldStyleParent.useCapability('composition')
.then(function (children) {
this.oldObject = children
.filter(function (child) {
return child.getId() === this.keyString;
}, this)[0];
}.bind(this));
},
/**
* Fetch historical data and establish a realtime subscription. Returns
* a promise that is resolved when all connections have been successfully
* established.
*
* @returns {Promise}
*/
fetch: function (options) {
let strategy;
if (this.model.interpolate !== 'none') {
strategy = 'minmax';
}
options = Object.assign({}, {
size: 1000,
strategy,
filters: this.filters
}, options || {});
if (!this.unsubscribe) {
this.unsubscribe = this.openmct
.telemetry
.subscribe(
this.domainObject,
this.add.bind(this),
{
filters: this.filters
}
);
}
/* eslint-disable you-dont-need-lodash-underscore/concat */
return this.openmct
.telemetry
.request(this.domainObject, options)
.then(function (points) {
const newPoints = _(this.data)
.concat(points)
.sortBy(this.getXVal)
.uniq(true, point => [this.getXVal(point), this.getYVal(point)].join())
.value();
this.reset(newPoints);
}.bind(this));
/* eslint-enable you-dont-need-lodash-underscore/concat */
},
/**
* Update x formatter on x change.
*/
onXKeyChange: function (xKey) {
const format = this.formats[xKey];
this.getXVal = format.parse.bind(format);
},
/**
* Update y formatter on change, default to stepAfter interpolation if
* y range is an enumeration.
*/
onYKeyChange: function (newKey, oldKey) {
if (newKey === oldKey) {
return;
}
const valueMetadata = this.metadata.value(newKey);
if (!this.persistedConfig || !this.persistedConfig.interpolate) {
if (valueMetadata.format === 'enum') {
this.set('interpolate', 'stepAfter');
} else {
this.set('interpolate', 'linear');
}
}
this.evaluate = function (datum) {
return this.limitEvaluator.evaluate(datum, valueMetadata);
}.bind(this);
const format = this.formats[newKey];
this.getYVal = format.parse.bind(format);
},
formatX: function (point) {
return this.formats[this.get('xKey')].format(point);
},
formatY: function (point) {
return this.formats[this.get('yKey')].format(point);
},
/**
* Clear stats and recalculate from existing data.
*/
resetStats: function () {
this.unset('stats');
this.data.forEach(this.updateStats, this);
},
/**
* Reset plot series. If new data is provided, will add that
* data to series after reset.
*/
reset: function (newData) {
this.data = [];
this.resetStats();
this.emit('reset');
if (newData) {
newData.forEach(function (point) {
this.add(point, true);
}, this);
}
},
/**
* Return the point closest to a given x value.
*/
nearestPoint: function (xValue) {
const insertIndex = this.sortedIndex(xValue);
const lowPoint = this.data[insertIndex - 1];
const highPoint = this.data[insertIndex];
const indexVal = this.getXVal(xValue);
const lowDistance = lowPoint
? indexVal - this.getXVal(lowPoint)
: Number.POSITIVE_INFINITY;
const highDistance = highPoint
? this.getXVal(highPoint) - indexVal
: Number.POSITIVE_INFINITY;
const nearestPoint = highDistance < lowDistance ? highPoint : lowPoint;
return nearestPoint;
},
/**
* Override this to implement plot series loading functionality. Must return
* a promise that is resolved when loading is completed.
*
* @private
* @returns {Promise}
*/
load: function (options) {
return this.fetch(options)
.then(function (res) {
this.emit('load');
return res;
}.bind(this));
},
/**
* Find the insert index for a given point to maintain sort order.
* @private
*/
sortedIndex: function (point) {
return _.sortedIndexBy(this.data, point, this.getXVal);
},
/**
* Update min/max stats for the series.
* @private
*/
updateStats: function (point) {
const value = this.getYVal(point);
let stats = this.get('stats');
let changed = false;
if (!stats) {
stats = {
minValue: value,
minPoint: point,
maxValue: value,
maxPoint: point
};
changed = true;
} else {
if (stats.maxValue < value) {
stats.maxValue = value;
stats.maxPoint = point;
changed = true;
}
if (stats.minValue > value) {
stats.minValue = value;
stats.minPoint = point;
changed = true;
}
}
if (changed) {
this.set('stats', {
minValue: stats.minValue,
minPoint: stats.minPoint,
maxValue: stats.maxValue,
maxPoint: stats.maxPoint
});
}
},
/**
* Add a point to the data array while maintaining the sort order of
* the array and preventing insertion of points with a duplicate x
* value. Can provide an optional argument to append a point without
* maintaining sort order and dupe checks, which improves performance
* when adding an array of points that are already properly sorted.
*
* @private
* @param {Object} point a telemetry datum.
* @param {Boolean} [appendOnly] default false, if true will append
* a point to the end without dupe checking.
*/
add: function (point, appendOnly) {
let insertIndex = this.data.length;
const currentYVal = this.getYVal(point);
const lastYVal = this.getYVal(this.data[insertIndex - 1]);
if (this.isValueInvalid(currentYVal) && this.isValueInvalid(lastYVal)) {
console.warn('[Plot] Invalid Y Values detected');
return;
}
if (!appendOnly) {
insertIndex = this.sortedIndex(point);
if (this.getXVal(this.data[insertIndex]) === this.getXVal(point)) {
return;
}
if (this.getXVal(this.data[insertIndex - 1]) === this.getXVal(point)) {
return;
}
}
this.updateStats(point);
point.mctLimitState = this.evaluate(point);
this.data.splice(insertIndex, 0, point);
this.emit('add', point, insertIndex, this);
},
/**
*
* @private
*/
isValueInvalid: function (val) {
return Number.isNaN(val) || val === undefined;
},
/**
* Remove a point from the data array and notify listeners.
* @private
*/
remove: function (point) {
const index = this.data.indexOf(point);
this.data.splice(index, 1);
this.emit('remove', point, index, this);
},
/**
* Purges records outside a given x range. Changes removal method based
* on number of records to remove: for large purge, reset data and
* rebuild array. for small purge, removes points and emits updates.
*
* @public
* @param {Object} range
* @param {number} range.min minimum x value to keep
* @param {number} range.max maximum x value to keep.
*/
purgeRecordsOutsideRange: function (range) {
const startIndex = this.sortedIndex(range.min);
const endIndex = this.sortedIndex(range.max) + 1;
const pointsToRemove = startIndex + (this.data.length - endIndex + 1);
if (pointsToRemove > 0) {
if (pointsToRemove < 1000) {
this.data.slice(0, startIndex).forEach(this.remove, this);
this.data.slice(endIndex, this.data.length).forEach(this.remove, this);
this.resetStats();
} else {
const newData = this.data.slice(startIndex, endIndex);
this.reset(newData);
}
}
},
/**
* Updates filters, clears the plot series, unsubscribes and resubscribes
* @public
*/
updateFiltersAndRefresh: function (updatedFilters) {
let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters));
if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) {
this.filters = deepCopiedFilters;
this.reset();
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
this.fetch();
} else {
this.filters = deepCopiedFilters;
}
},
getDisplayRange: function (xKey) {
const unsortedData = this.data;
this.data = [];
unsortedData.forEach(point => this.add(point, false));
const minValue = this.getXVal(this.data[0]);
const maxValue = this.getXVal(this.data[this.data.length - 1]);
return {
min: minValue,
max: maxValue
};
},
markerOptionsDisplayText: function () {
const showMarkers = this.get('markers');
if (!showMarkers) {
return "Disabled";
}
const markerShapeKey = this.get('markerShape');
const markerShape = MARKER_SHAPES[markerShapeKey].label;
const markerSize = this.get('markerSize');
return `${markerShape}: ${markerSize}px`;
},
nameWithUnit: function () {
let unit = this.get('unit');
return this.get('name') + (unit ? ' ' + unit : '');
}
});
return PlotSeries;
});

View File

@@ -0,0 +1,174 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./PlotSeries',
'./Collection',
'./Model',
'../lib/color',
'lodash'
], function (
PlotSeries,
Collection,
Model,
color,
_
) {
const SeriesCollection = Collection.extend({
modelClass: PlotSeries,
initialize: function (options) {
this.plot = options.plot;
this.openmct = options.openmct;
this.palette = new color.ColorPalette();
this.listenTo(this, 'add', this.onSeriesAdd, this);
this.listenTo(this, 'remove', this.onSeriesRemove, this);
this.listenTo(this.plot, 'change:domainObject', this.trackPersistedConfig, this);
const domainObject = this.plot.get('domainObject');
if (domainObject.telemetry) {
this.addTelemetryObject(domainObject);
} else {
this.watchTelemetryContainer(domainObject);
}
},
trackPersistedConfig: function (domainObject) {
domainObject.configuration.series.forEach(function (seriesConfig) {
const series = this.byIdentifier(seriesConfig.identifier);
if (series) {
series.persistedConfig = seriesConfig;
}
}, this);
},
watchTelemetryContainer: function (domainObject) {
const composition = this.openmct.composition.get(domainObject);
this.listenTo(composition, 'add', this.addTelemetryObject, this);
this.listenTo(composition, 'remove', this.removeTelemetryObject, this);
composition.load();
},
addTelemetryObject: function (domainObject, index) {
let seriesConfig = this.plot.getPersistedSeriesConfig(domainObject.identifier);
const filters = this.plot.getPersistedFilters(domainObject.identifier);
const plotObject = this.plot.get('domainObject');
if (!seriesConfig) {
seriesConfig = {
identifier: domainObject.identifier
};
if (plotObject.type === 'telemetry.plot.overlay') {
this.openmct.objects.mutate(
plotObject,
'configuration.series[' + this.size() + ']',
seriesConfig
);
seriesConfig = this.plot
.getPersistedSeriesConfig(domainObject.identifier);
}
}
// Clone to prevent accidental mutation by ref.
seriesConfig = JSON.parse(JSON.stringify(seriesConfig));
this.add(new PlotSeries({
model: seriesConfig,
domainObject: domainObject,
collection: this,
openmct: this.openmct,
persistedConfig: this.plot
.getPersistedSeriesConfig(domainObject.identifier),
filters: filters
}));
},
removeTelemetryObject: function (identifier) {
const plotObject = this.plot.get('domainObject');
if (plotObject.type === 'telemetry.plot.overlay') {
const persistedIndex = plotObject.configuration.series.findIndex(s => {
return _.isEqual(identifier, s.identifier);
});
const configIndex = this.models.findIndex(m => {
return _.isEqual(m.domainObject.identifier, identifier);
});
/*
when cancelling out of edit mode, the config store and domain object are out of sync
thus it is necesarry to check both and remove the models that are no longer in composition
*/
if (persistedIndex === -1) {
this.remove(this.at(configIndex));
} else {
this.remove(this.at(persistedIndex));
// Because this is triggered by a composition change, we have
// to defer mutation of our plot object, otherwise we might
// mutate an outdated version of the plotObject.
setTimeout(function () {
const newPlotObject = this.plot.get('domainObject');
const cSeries = newPlotObject.configuration.series.slice();
cSeries.splice(persistedIndex, 1);
this.openmct.objects.mutate(newPlotObject, 'configuration.series', cSeries);
}.bind(this));
}
}
},
onSeriesAdd: function (series) {
let seriesColor = series.get('color');
if (seriesColor) {
if (!(seriesColor instanceof color.Color)) {
seriesColor = color.Color.fromHexString(seriesColor);
series.set('color', seriesColor);
}
this.palette.remove(seriesColor);
} else {
series.set('color', this.palette.getNextColor());
}
this.listenTo(series, 'change:color', this.updateColorPalette, this);
},
onSeriesRemove: function (series) {
this.palette.return(series.get('color'));
this.stopListening(series);
series.destroy();
},
updateColorPalette: function (newColor, oldColor) {
this.palette.remove(newColor);
const seriesWithColor = this.filter(function (series) {
return series.get('color') === newColor;
})[0];
if (!seriesWithColor) {
this.palette.return(oldColor);
}
},
byIdentifier: function (identifier) {
return this.filter(function (series) {
const seriesIdentifier = series.get('identifier');
return seriesIdentifier.namespace === identifier.namespace
&& seriesIdentifier.key === identifier.key;
})[0];
}
});
return SeriesCollection;
});

View File

@@ -0,0 +1,97 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./Model'
], function (
Model
) {
/**
* TODO: doc strings.
*/
const XAxisModel = Model.extend({
initialize: function (options) {
this.plot = options.plot;
this.set('label', options.model.name || '');
this.on('change:range', function (newValue, oldValue, model) {
if (!model.get('frozen')) {
model.set('displayRange', newValue);
}
});
this.on('change:frozen', ((frozen, oldValue, model) => {
if (!frozen) {
model.set('range', this.get('range'));
}
}));
if (this.get('range')) {
this.set('range', this.get('range'));
}
this.listenTo(this, 'change:key', this.changeKey, this);
this.listenTo(this, 'resetSeries', this.resetSeries, this);
},
changeKey: function (newKey) {
const series = this.plot.series.first();
if (series) {
const xMetadata = series.metadata.value(newKey);
const xFormat = series.formats[newKey];
this.set('label', xMetadata.name);
this.set('format', xFormat.format.bind(xFormat));
} else {
this.set('format', function (x) {
return x;
});
this.set('label', newKey);
}
this.plot.series.forEach(function (plotSeries) {
plotSeries.set('xKey', newKey);
});
},
resetSeries: function () {
this.plot.series.forEach(function (plotSeries) {
plotSeries.reset();
});
},
defaults: function (options) {
const bounds = options.openmct.time.bounds();
const timeSystem = options.openmct.time.timeSystem();
const format = options.openmct.$injector.get('formatService')
.getFormat(timeSystem.timeFormat);
return {
name: timeSystem.name,
key: timeSystem.key,
format: format.format.bind(format),
range: {
min: bounds.start,
max: bounds.end
},
frozen: false
};
}
});
return XAxisModel;
});

View File

@@ -0,0 +1,244 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./Model',
'lodash'
], function (
Model,
_
) {
/**
* YAxis model
*
* TODO: docstrings.
*
* has the following Model properties:
*
* `autoscale`: boolean, whether or not to autoscale.
* `autoscalePadding`: float, percent of padding to display in plots.
* `displayRange`: the current display range for the x Axis.
* `format`: the formatter for the axis.
* `frozen`: boolean, if true, displayRange will not be updated automatically.
* Used to temporarily disable automatic updates during user interaction.
* `label`: label to display on axis.
* `stats`: Min and Max Values of data, automatically updated by observing
* plot series.
* `values`: for enumerated types, an array of possible display values.
* `range`: the user-configured range to use for display, when autoscale is
* disabled.
*
*/
const YAxisModel = Model.extend({
initialize: function (options) {
this.plot = options.plot;
this.listenTo(this, 'change:stats', this.calculateAutoscaleExtents, this);
this.listenTo(this, 'change:autoscale', this.toggleAutoscale, this);
this.listenTo(this, 'change:autoscalePadding', this.updatePadding, this);
this.listenTo(this, 'change:frozen', this.toggleFreeze, this);
this.listenTo(this, 'change:range', this.updateDisplayRange, this);
this.updateDisplayRange(this.get('range'));
},
listenToSeriesCollection: function (seriesCollection) {
this.seriesCollection = seriesCollection;
this.listenTo(this.seriesCollection, 'add', (series => {
this.trackSeries(series);
this.updateFromSeries(this.seriesCollection);
}), this);
this.listenTo(this.seriesCollection, 'remove', (series => {
this.untrackSeries(series);
this.updateFromSeries(this.seriesCollection);
}), this);
this.seriesCollection.forEach(this.trackSeries, this);
this.updateFromSeries(this.seriesCollection);
},
updateDisplayRange: function (range) {
if (!this.get('autoscale')) {
this.set('displayRange', range);
}
},
toggleFreeze: function (frozen) {
if (!frozen) {
this.toggleAutoscale(this.get('autoscale'));
}
},
applyPadding: function (range) {
let padding = Math.abs(range.max - range.min) * this.get('autoscalePadding');
if (padding === 0) {
padding = 1;
}
return {
min: range.min - padding,
max: range.max + padding
};
},
updatePadding: function (newPadding) {
if (this.get('autoscale') && !this.get('frozen') && this.has('stats')) {
this.set('displayRange', this.applyPadding(this.get('stats')));
}
},
calculateAutoscaleExtents: function (newStats) {
if (this.get('autoscale') && !this.get('frozen')) {
if (!newStats) {
this.unset('displayRange');
} else {
this.set('displayRange', this.applyPadding(newStats));
}
}
},
updateStats: function (seriesStats) {
if (!this.has('stats')) {
this.set('stats', {
min: seriesStats.minValue,
max: seriesStats.maxValue
});
return;
}
const stats = this.get('stats');
let changed = false;
if (stats.min > seriesStats.minValue) {
changed = true;
stats.min = seriesStats.minValue;
}
if (stats.max < seriesStats.maxValue) {
changed = true;
stats.max = seriesStats.maxValue;
}
if (changed) {
this.set('stats', {
min: stats.min,
max: stats.max
});
}
},
resetStats: function () {
this.unset('stats');
this.seriesCollection.forEach(function (series) {
if (series.has('stats')) {
this.updateStats(series.get('stats'));
}
}, this);
},
trackSeries: function (series) {
this.listenTo(series, 'change:stats', seriesStats => {
if (!seriesStats) {
this.resetStats();
} else {
this.updateStats(seriesStats);
}
});
this.listenTo(series, 'change:yKey', () => {
this.updateFromSeries(this.seriesCollection);
});
},
untrackSeries: function (series) {
this.stopListening(series);
this.resetStats();
this.updateFromSeries(this.seriesCollection);
},
toggleAutoscale: function (autoscale) {
if (autoscale && this.has('stats')) {
this.set('displayRange', this.applyPadding(this.get('stats')));
} else {
this.set('displayRange', this.get('range'));
}
},
/**
* Update yAxis format, values, and label from known series.
*/
updateFromSeries: function (series) {
this.unset('displayRange');
const plotModel = this.plot.get('domainObject');
const label = _.get(plotModel, 'configuration.yAxis.label');
const sampleSeries = series.first();
if (!sampleSeries) {
if (!label) {
this.unset('label');
}
return;
}
const yKey = sampleSeries.get('yKey');
const yMetadata = sampleSeries.metadata.value(yKey);
const yFormat = sampleSeries.formats[yKey];
this.set('format', yFormat.format.bind(yFormat));
this.set('values', yMetadata.values);
if (!label) {
const labelName = series.map(function (s) {
return s.metadata.value(s.get('yKey')).name;
}).reduce(function (a, b) {
if (a === undefined) {
return b;
}
if (a === b) {
return a;
}
return '';
}, undefined);
if (labelName) {
this.set('label', labelName);
return;
}
const labelUnits = series.map(function (s) {
return s.metadata.value(s.get('yKey')).units;
}).reduce(function (a, b) {
if (a === undefined) {
return b;
}
if (a === b) {
return a;
}
return '';
}, undefined);
if (labelUnits) {
this.set('label', labelUnits);
return;
}
}
},
defaults: function (options) {
return {
frozen: false,
autoscale: true,
autoscalePadding: 0.1
};
}
});
return YAxisModel;
});

View File

@@ -0,0 +1,48 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
], function (
) {
function ConfigStore() {
this.store = {};
}
ConfigStore.prototype.deleteStore = function (id) {
if (this.store[id]) {
this.store[id].destroy();
delete this.store[id];
}
};
ConfigStore.prototype.add = function (id, config) {
this.store[id] = config;
};
ConfigStore.prototype.get = function (id) {
return this.store[id];
};
const STORE = new ConfigStore();
return STORE;
});

View File

@@ -0,0 +1,163 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'EventEmitter',
'../lib/eventHelpers',
'./MarkerShapes'
], function (
EventEmitter,
eventHelpers,
MARKER_SHAPES
) {
/**
* Create a new draw API utilizing the Canvas's 2D API for rendering.
*
* @constructor
* @param {CanvasElement} canvas the canvas object to render upon
* @throws {Error} an error is thrown if Canvas's 2D API is unavailab
*/
function Draw2D(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.");
}
}
Object.assign(Draw2D.prototype, EventEmitter.prototype);
eventHelpers.extend(Draw2D.prototype);
// Convert from logical to physical x coordinates
Draw2D.prototype.x = function (v) {
return ((v - this.origin[0]) / this.dimensions[0]) * this.width;
};
// Convert from logical to physical y coordinates
Draw2D.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
Draw2D.prototype.setColor = function (color) {
const 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 + ")";
};
Draw2D.prototype.clear = function () {
this.width = this.canvas.width = this.canvas.offsetWidth;
this.height = this.canvas.height = this.canvas.offsetHeight;
this.c2d.clearRect(0, 0, this.width, this.height);
};
Draw2D.prototype.setDimensions = function (newDimensions, newOrigin) {
this.dimensions = newDimensions;
this.origin = newOrigin;
};
Draw2D.prototype.drawLine = function (buf, color, points) {
let i;
this.setColor(color);
// Configure context to draw two-pixel-thick lines
this.c2d.lineWidth = 1;
// 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();
};
Draw2D.prototype.drawSquare = function (min, max, color) {
const x1 = this.x(min[0]);
const y1 = this.y(min[1]);
const w = this.x(max[0]) - x1;
const h = this.y(max[1]) - y1;
this.setColor(color);
this.c2d.fillRect(x1, y1, w, h);
};
Draw2D.prototype.drawPoints = function (
buf,
color,
points,
pointSize,
shape
) {
const drawC2DShape = MARKER_SHAPES[shape].drawC2D.bind(this);
this.setColor(color);
for (let i = 0; i < points; i++) {
drawC2DShape(
this.x(buf[i * 2]),
this.y(buf[i * 2 + 1]),
pointSize
);
}
};
Draw2D.prototype.drawLimitPoint = function (x, y, size) {
this.c2d.fillRect(x + size, y, size, size);
this.c2d.fillRect(x, y + size, size, size);
this.c2d.fillRect(x - size, y, size, size);
this.c2d.fillRect(x, y - size, size, size);
};
Draw2D.prototype.drawLimitPoints = function (points, color, pointSize) {
const limitSize = pointSize * 2;
const offset = limitSize / 2;
this.setColor(color);
for (let i = 0; i < points.length; i++) {
this.drawLimitPoint(
this.x(points[i].x) - offset,
this.y(points[i].y) - offset,
limitSize
);
}
};
return Draw2D;
});

View File

@@ -0,0 +1,110 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
'./DrawWebGL',
'./Draw2D'
],
function (DrawWebGL, Draw2D) {
const CHARTS = [
{
MAX_INSTANCES: 16,
API: DrawWebGL,
ALLOCATIONS: []
},
{
MAX_INSTANCES: Number.POSITIVE_INFINITY,
API: Draw2D,
ALLOCATIONS: []
}
];
/**
* Draw loader attaches a draw API to a canvas element and returns the
* draw API.
*/
return {
/**
* Return the first draw API available. Returns
* `undefined` if a draw API could not be constructed.
*.
* @param {CanvasElement} canvas - The canvas eelement to attach
the draw API to.
*/
getDrawAPI: function (canvas, overlay) {
let api;
CHARTS.forEach(function (CHART_TYPE) {
if (api) {
return;
}
if (CHART_TYPE.ALLOCATIONS.length
>= CHART_TYPE.MAX_INSTANCES) {
return;
}
try {
api = new CHART_TYPE.API(canvas, overlay);
CHART_TYPE.ALLOCATIONS.push(api);
} catch (e) {
console.warn([
"Could not instantiate chart",
CHART_TYPE.API.name,
";",
e.message
].join(" "));
}
});
if (!api) {
console.warn("Cannot initialize mct-chart.");
}
return api;
},
/**
* Returns a fallback draw api.
*/
getFallbackDrawAPI: function (canvas, overlay) {
const api = new CHARTS[1].API(canvas, overlay);
CHARTS[1].ALLOCATIONS.push(api);
return api;
},
releaseDrawAPI: function (api) {
CHARTS.forEach(function (CHART_TYPE) {
if (api instanceof CHART_TYPE.API) {
CHART_TYPE.ALLOCATIONS.splice(CHART_TYPE.ALLOCATIONS.indexOf(api), 1);
}
});
if (api.destroy) {
api.destroy();
}
}
};
}
);

View File

@@ -0,0 +1,307 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'EventEmitter',
'../lib/eventHelpers',
'./MarkerShapes'
], function (
EventEmitter,
eventHelpers,
MARKER_SHAPES
) {
// WebGL shader sources (for drawing plain colors)
const FRAGMENT_SHADER = `
precision mediump float;
uniform vec4 uColor;
uniform int uMarkerShape;
void main(void) {
gl_FragColor = uColor;
if (uMarkerShape > 1) {
vec2 clipSpacePointCoord = 2.0 * gl_PointCoord - 1.0;
if (uMarkerShape == 2) { // circle
float distance = length(clipSpacePointCoord);
if (distance > 1.0) {
discard;
}
} else if (uMarkerShape == 3) { // diamond
float distance = abs(clipSpacePointCoord.x) + abs(clipSpacePointCoord.y);
if (distance > 1.0) {
discard;
}
} else if (uMarkerShape == 4) { // triangle
float x = clipSpacePointCoord.x;
float y = clipSpacePointCoord.y;
float distance = 2.0 * x - 1.0;
float distance2 = -2.0 * x - 1.0;
if (distance > y || distance2 > y) {
discard;
}
}
}
}
`;
const VERTEX_SHADER = `
attribute vec2 aVertexPosition;
uniform vec2 uDimensions;
uniform vec2 uOrigin;
uniform float uPointSize;
void main(void) {
gl_Position = vec4(2.0 * ((aVertexPosition - uOrigin) / uDimensions) - vec2(1,1), 0, 1);
gl_PointSize = uPointSize;
}
`;
/**
* Create a draw api utilizing WebGL.
*
* @constructor
* @param {CanvasElement} canvas the canvas object to render upon
* @throws {Error} an error is thrown if WebGL is unavailable.
*/
function DrawWebGL(canvas, overlay) {
this.canvas = canvas;
this.gl = this.canvas.getContext("webgl", { preserveDrawingBuffer: true })
|| this.canvas.getContext("experimental-webgl", { preserveDrawingBuffer: true });
this.overlay = overlay;
this.c2d = overlay.getContext('2d');
if (!this.c2d) {
throw new Error("No canvas 2d!");
}
// Ensure a context was actually available before proceeding
if (!this.gl) {
throw new Error("WebGL unavailable.");
}
this.initContext();
this.listenTo(this.canvas, "webglcontextlost", this.onContextLost, this);
}
Object.assign(DrawWebGL.prototype, EventEmitter.prototype);
eventHelpers.extend(DrawWebGL.prototype);
DrawWebGL.prototype.onContextLost = function (event) {
this.emit('error');
this.isContextLost = true;
this.destroy();
};
DrawWebGL.prototype.initContext = function () {
// Initialize shaders
this.vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER);
this.gl.shaderSource(this.vertexShader, VERTEX_SHADER);
this.gl.compileShader(this.vertexShader);
this.fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER);
this.gl.shaderSource(this.fragmentShader, FRAGMENT_SHADER);
this.gl.compileShader(this.fragmentShader);
// Assemble vertex/fragment shaders into programs
this.program = this.gl.createProgram();
this.gl.attachShader(this.program, this.vertexShader);
this.gl.attachShader(this.program, this.fragmentShader);
this.gl.linkProgram(this.program);
this.gl.useProgram(this.program);
// Get locations for attribs/uniforms from the
// shader programs (to pass values into shaders at draw-time)
this.aVertexPosition = this.gl.getAttribLocation(this.program, "aVertexPosition");
this.uColor = this.gl.getUniformLocation(this.program, "uColor");
this.uMarkerShape = this.gl.getUniformLocation(this.program, "uMarkerShape");
this.uDimensions = this.gl.getUniformLocation(this.program, "uDimensions");
this.uOrigin = this.gl.getUniformLocation(this.program, "uOrigin");
this.uPointSize = this.gl.getUniformLocation(this.program, "uPointSize");
this.gl.enableVertexAttribArray(this.aVertexPosition);
// Create a buffer to holds points which will be drawn
this.buffer = this.gl.createBuffer();
// Enable blending, for smoothness
this.gl.enable(this.gl.BLEND);
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
};
DrawWebGL.prototype.destroy = function () {
this.stopListening();
};
// Convert from logical to physical x coordinates
DrawWebGL.prototype.x = function (v) {
return ((v - this.origin[0]) / this.dimensions[0]) * this.width;
};
// Convert from logical to physical y coordinates
DrawWebGL.prototype.y = function (v) {
return this.height
- ((v - this.origin[1]) / this.dimensions[1]) * this.height;
};
DrawWebGL.prototype.doDraw = function (drawType, buf, color, points, shape) {
if (this.isContextLost) {
return;
}
const shapeCode = MARKER_SHAPES[shape] ? MARKER_SHAPES[shape].drawWebGL : 0;
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, buf, this.gl.DYNAMIC_DRAW);
this.gl.vertexAttribPointer(this.aVertexPosition, 2, this.gl.FLOAT, false, 0, 0);
this.gl.uniform4fv(this.uColor, color);
this.gl.uniform1i(this.uMarkerShape, shapeCode);
this.gl.drawArrays(drawType, 0, points);
};
DrawWebGL.prototype.clear = function () {
if (this.isContextLost) {
return;
}
this.height = this.canvas.height = this.canvas.offsetHeight;
this.width = this.canvas.width = this.canvas.offsetWidth;
this.overlay.height = this.overlay.offsetHeight;
this.overlay.width = this.overlay.offsetWidth;
// 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.
this.gl.viewport(
0,
0,
this.gl.drawingBufferWidth,
this.gl.drawingBufferHeight
);
this.gl.clear(this.gl.COLOR_BUFFER_BIT + this.gl.DEPTH_BUFFER_BIT);
};
/**
* 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
*/
DrawWebGL.prototype.setDimensions = function (dimensions, origin) {
this.dimensions = dimensions;
this.origin = origin;
if (this.isContextLost) {
return;
}
if (dimensions && dimensions.length > 0
&& origin && origin.length > 0) {
this.gl.uniform2fv(this.uDimensions, dimensions);
this.gl.uniform2fv(this.uOrigin, origin);
}
};
/**
* 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
*/
DrawWebGL.prototype.drawLine = function (buf, color, points) {
if (this.isContextLost) {
return;
}
this.doDraw(this.gl.LINE_STRIP, buf, color, points);
};
/**
* Draw the buffer as points.
*
*/
DrawWebGL.prototype.drawPoints = function (buf, color, points, pointSize, shape) {
if (this.isContextLost) {
return;
}
this.gl.uniform1f(this.uPointSize, pointSize);
this.doDraw(this.gl.POINTS, buf, color, points, shape);
};
/**
* 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
*/
DrawWebGL.prototype.drawSquare = function (min, max, color) {
if (this.isContextLost) {
return;
}
this.doDraw(this.gl.TRIANGLE_FAN, new Float32Array(
min.concat([min[0], max[1]]).concat(max).concat([max[0], min[1]])
), color, 4);
};
DrawWebGL.prototype.drawLimitPoint = function (x, y, size) {
this.c2d.fillRect(x + size, y, size, size);
this.c2d.fillRect(x, y + size, size, size);
this.c2d.fillRect(x - size, y, size, size);
this.c2d.fillRect(x, y - size, size, size);
};
DrawWebGL.prototype.drawLimitPoints = function (points, color, pointSize) {
const limitSize = pointSize * 2;
const offset = limitSize / 2;
const 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 + ")";
for (let i = 0; i < points.length; i++) {
this.drawLimitPoint(
this.x(points[i].x) - offset,
this.y(points[i].y) - offset,
limitSize
);
}
};
return DrawWebGL;
});

View File

@@ -0,0 +1,90 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([], function () {
/**
* @label string (required) display name of shape
* @drawWebGL integer (unique, required) index provided to WebGL Fragment Shader
* @drawC2D function (required) canvas2d draw function
*/
const MARKER_SHAPES = {
point: {
label: 'Point',
drawWebGL: 1,
drawC2D: function (x, y, size) {
const offset = size / 2;
this.c2d.fillRect(x - offset, y - offset, size, size);
}
},
circle: {
label: 'Circle',
drawWebGL: 2,
drawC2D: function (x, y, size) {
const radius = size / 2;
this.c2d.beginPath();
this.c2d.arc(x, y, radius, 0, 2 * Math.PI, false);
this.c2d.closePath();
this.c2d.fill();
}
},
diamond: {
label: 'Diamond',
drawWebGL: 3,
drawC2D: function (x, y, size) {
const offset = size / 2;
const top = [x, y + offset];
const right = [x + offset, y];
const bottom = [x, y - offset];
const left = [x - offset, y];
this.c2d.beginPath();
this.c2d.moveTo(...top);
this.c2d.lineTo(...right);
this.c2d.lineTo(...bottom);
this.c2d.lineTo(...left);
this.c2d.closePath();
this.c2d.fill();
}
},
triangle: {
label: 'Triangle',
drawWebGL: 4,
drawC2D: function (x, y, size) {
const offset = size / 2;
const v1 = [x, y - offset];
const v2 = [x - offset, y + offset];
const v3 = [x + offset, y + offset];
this.c2d.beginPath();
this.c2d.moveTo(...v1);
this.c2d.lineTo(...v2);
this.c2d.lineTo(...v3);
this.c2d.closePath();
this.c2d.fill();
}
}
};
return MARKER_SHAPES;
});

View File

@@ -0,0 +1,55 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(function () {
/**
* Simple directive that removes the elements pool when used in the
* inspector region. Workaround until we have better control of screen
* regions.
*/
return function HideElementPoolDirective() {
return {
restrict: "A",
link: function ($scope, $element) {
let splitter = $element.parent();
while (splitter[0].tagName !== 'MCT-SPLIT-PANE') {
splitter = splitter.parent();
}
[
'.split-pane-component.pane.bottom',
'mct-splitter'
].forEach(function (selector) {
const element = splitter[0].querySelectorAll(selector)[0];
element.style.maxHeight = '0px';
element.style.minHeight = '0px';
});
splitter[0]
.querySelectorAll('.split-pane-component.pane.top')[0]
.style
.bottom = '0px';
}
};
};
});

View File

@@ -0,0 +1,67 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
'./Region'
],
function (Region) {
/**
* Defines the a default Inspector region. Captured in a class to
* allow for modular extension and customization of regions based on
* the typical case.
* @memberOf platform/commonUI/regions
* @constructor
*/
function InspectorRegion() {
Region.call(this, {'name': 'Inspector'});
this.buildRegion();
}
InspectorRegion.prototype = Object.create(Region.prototype);
InspectorRegion.prototype.constructor = Region;
/**
* @private
*/
InspectorRegion.prototype.buildRegion = function () {
const metadataRegion = {
name: 'metadata',
title: 'Metadata Region',
// Which modes should the region part be visible in? If
// nothing provided here, then assumed that part is visible
// in both. The visibility or otherwise of a region part
// should be decided by a policy. In this case, 'modes' is a
// shortcut that is used by the EditableRegionPolicy.
modes: ['browse', 'edit'],
content: {
key: 'object-properties'
}
};
this.addRegion(new Region(metadataRegion), 0);
};
return InspectorRegion;
}
);

View File

@@ -0,0 +1,41 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./Region'
], function (
Region
) {
const PlotBrowseRegion = new Region({
name: "plot-options",
title: "Plot Options",
modes: ['browse'],
content: {
key: "plot-options-browse"
}
});
return PlotBrowseRegion;
});

View File

@@ -0,0 +1,39 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open openmct 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 openmct 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([
'./Region'
], function (
Region
) {
const PlotEditRegion = new Region({
name: "plot-options",
title: "Plot Options",
modes: ['edit'],
content: {
key: "plot-options-edit"
}
});
return PlotEditRegion;
});

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