diff --git a/main.js b/main.js
index ab911921ec..bf7cbbd90d 100644
--- a/main.js
+++ b/main.js
@@ -74,6 +74,7 @@ define([
'./platform/features/plot/bundle',
'./platform/features/scrolling/bundle',
'./platform/features/timeline/bundle',
+ './platform/features/table/bundle',
'./platform/forms/bundle',
'./platform/identity/bundle',
'./platform/persistence/aggregator/bundle',
diff --git a/platform/features/table/bundle.js b/platform/features/table/bundle.js
new file mode 100644
index 0000000000..b598ec1200
--- /dev/null
+++ b/platform/features/table/bundle.js
@@ -0,0 +1,132 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web 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 Web 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.
+ *****************************************************************************/
+/*global define*/
+
+define([
+ "./src/directives/MCTTable",
+ "./src/controllers/TableController",
+ "./src/controllers/TableOptionsController",
+ "./src/controllers/MCTTableController",
+ '../../commonUI/regions/src/Region',
+ '../../commonUI/browse/src/InspectorRegion',
+ "legacyRegistry"
+], function (
+ MCTTable,
+ TableController,
+ TableOptionsController,
+ MCTTableController,
+ Region,
+ InspectorRegion,
+ legacyRegistry
+) {
+ "use strict";
+ /**
+ * Two region parts are defined here. One that appears only in browse
+ * mode, and one that appears only in edit mode. For not they both point
+ * to the same representation, but a different key could be used here to
+ * include a customized representation for edit mode.
+ */
+ var tableInspector = new InspectorRegion(),
+ tableOptionsEditRegion = new Region({
+ name: "table-options",
+ title: "Table Options",
+ modes: ['edit'],
+ content: {
+ key: "table-options-edit"
+ }
+ });
+ tableInspector.addRegion(tableOptionsEditRegion);
+
+ legacyRegistry.register("platform/features/table", {
+ "extensions": {
+ "types": [
+ {
+ "key": "table",
+ "name": "Table",
+ "glyph": "\ue605",
+ "description": "A table for displaying telemetry data",
+ "features": "creation",
+ "delegates": [
+ "telemetry"
+ ],
+ "inspector": tableInspector,
+ "contains": [
+ {
+ "has": "telemetry"
+ }
+ ],
+ "model": {
+ "composition": []
+ },
+ "views": [
+ "table"
+ ]
+ }
+ ],
+ "controllers": [
+ {
+ "key": "TableController",
+ "implementation": TableController,
+ "depends": ["$scope", "telemetryHandler", "telemetryFormatter"]
+ },
+ {
+ "key": "TableOptionsController",
+ "implementation": TableOptionsController,
+ "depends": ["$scope"]
+ },
+ {
+ "key": "MCTTableController",
+ "implementation": MCTTableController,
+ "depends": ["$scope", "$timeout", "$element"]
+ }
+
+ ],
+ "views": [
+ {
+ "name": "Table",
+ "key": "table",
+ "glyph": "\ue605",
+ "templateUrl": "templates/table.html",
+ "needs": [
+ "telemetry"
+ ],
+ "delegation": true,
+ "editable": true
+ }
+ ],
+ "directives": [
+ {
+ "key": "mctTable",
+ "implementation": MCTTable,
+ "depends": ["$timeout"]
+ }
+ ],
+ "representations": [
+ {
+ "key": "table-options-edit",
+ "templateUrl": "templates/table-options-edit.html"
+ }
+ ]
+ }
+ });
+
+});
diff --git a/platform/features/table/res/templates/mct-data-table.html b/platform/features/table/res/templates/mct-data-table.html
new file mode 100644
index 0000000000..42753a8a7b
--- /dev/null
+++ b/platform/features/table/res/templates/mct-data-table.html
@@ -0,0 +1,67 @@
+
diff --git a/platform/features/table/res/templates/table-options-edit.html b/platform/features/table/res/templates/table-options-edit.html
new file mode 100644
index 0000000000..39884b18b1
--- /dev/null
+++ b/platform/features/table/res/templates/table-options-edit.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/features/table/res/templates/table.html b/platform/features/table/res/templates/table.html
new file mode 100644
index 0000000000..12faebe40b
--- /dev/null
+++ b/platform/features/table/res/templates/table.html
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/platform/features/table/src/DomainColumn.js b/platform/features/table/src/DomainColumn.js
new file mode 100644
index 0000000000..1dbe0ab73d
--- /dev/null
+++ b/platform/features/table/src/DomainColumn.js
@@ -0,0 +1,64 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web 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 Web 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.
+ *****************************************************************************/
+/*global define,moment*/
+
+/**
+ * Module defining DomainColumn.
+ */
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * A column which will report telemetry domain values
+ * (typically, timestamps.) Used by the ScrollingListController.
+ *
+ * @memberof platform/features/table
+ * @constructor
+ * @param domainMetadata an object with the machine- and human-
+ * readable names for this domain (in `key` and `name`
+ * fields, respectively.)
+ * @param {TelemetryFormatter} telemetryFormatter the telemetry
+ * formatting service, for making values human-readable.
+ */
+ function DomainColumn(domainMetadata, telemetryFormatter) {
+ this.domainMetadata = domainMetadata;
+ this.telemetryFormatter = telemetryFormatter;
+ }
+
+ DomainColumn.prototype.getTitle = function () {
+ return this.domainMetadata.name;
+ };
+
+ DomainColumn.prototype.getValue = function (domainObject, datum) {
+ return {
+ text: this.telemetryFormatter.formatDomainValue(
+ datum[this.domainMetadata.key],
+ this.domainMetadata.format
+ )
+ };
+ };
+
+ return DomainColumn;
+ }
+);
diff --git a/platform/features/table/src/NameColumn.js b/platform/features/table/src/NameColumn.js
new file mode 100644
index 0000000000..72ace365e4
--- /dev/null
+++ b/platform/features/table/src/NameColumn.js
@@ -0,0 +1,54 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web 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 Web 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.
+ *****************************************************************************/
+/*global define,Promise*/
+
+/**
+ * Module defining NameColumn. Created by vwoeltje on 11/18/14.
+ */
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * A column which will report the name of the domain object
+ * which exposed specific telemetry values.
+ *
+ * @memberof platform/features/table
+ * @constructor
+ */
+ function NameColumn() {
+ }
+
+ NameColumn.prototype.getTitle = function () {
+ return "Name";
+ };
+
+ NameColumn.prototype.getValue = function (domainObject) {
+ return {
+ text: domainObject.getModel().name
+ };
+ };
+
+ return NameColumn;
+ }
+);
diff --git a/platform/features/table/src/RangeColumn.js b/platform/features/table/src/RangeColumn.js
new file mode 100644
index 0000000000..0dfe964dc8
--- /dev/null
+++ b/platform/features/table/src/RangeColumn.js
@@ -0,0 +1,67 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web 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 Web 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.
+ *****************************************************************************/
+/*global define,Promise*/
+
+/**
+ * Module defining DomainColumn. Created by vwoeltje on 11/18/14.
+ */
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * A column which will report telemetry range values
+ * (typically, measurements.) Used by the ScrollingListController.
+ *
+ * @memberof platform/features/table
+ * @constructor
+ * @param rangeMetadata an object with the machine- and human-
+ * readable names for this range (in `key` and `name`
+ * fields, respectively.)
+ * @param {TelemetryFormatter} telemetryFormatter the telemetry
+ * formatting service, for making values human-readable.
+ */
+ function RangeColumn(rangeMetadata, telemetryFormatter) {
+ this.rangeMetadata = rangeMetadata;
+ this.telemetryFormatter = telemetryFormatter;
+ }
+
+ RangeColumn.prototype.getTitle = function () {
+ return this.rangeMetadata.name;
+ };
+
+ RangeColumn.prototype.getValue = function (domainObject, datum) {
+ var range = this.rangeMetadata.key,
+ limit = domainObject.getCapability('limit'),
+ value = isNaN(datum[range]) ? datum[range] : parseFloat(datum[range]),
+ alarm = limit && limit.evaluate(datum, range);
+
+ return {
+ cssClass: alarm && alarm.cssClass,
+ text: typeof(value) === 'undefined' ? undefined : this.telemetryFormatter.formatRangeValue(value)
+ };
+ };
+
+ return RangeColumn;
+ }
+);
diff --git a/platform/features/table/src/Table.js b/platform/features/table/src/Table.js
new file mode 100644
index 0000000000..a12c9789b1
--- /dev/null
+++ b/platform/features/table/src/Table.js
@@ -0,0 +1,163 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web 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 Web 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.
+ *****************************************************************************/
+/*global define,moment*/
+
+define(
+ [
+ './DomainColumn',
+ './RangeColumn',
+ './NameColumn'
+ ],
+ function (DomainColumn, RangeColumn, NameColumn) {
+ "use strict";
+
+ /**
+ * Class that manages table metadata, state, and contents.
+ * @memberof platform/features/table
+ * @param domainObject
+ * @constructor
+ */
+ function Table(domainObject, telemetryFormatter) {
+ this.domainObject = domainObject;
+ this.columns = [];
+ this.telemetryFormatter = telemetryFormatter;
+ }
+
+ /**
+ * Build column definitions based on supplied telemetry metadata
+ * @param metadata Metadata describing the domains and ranges available
+ * @returns {Table} This object
+ */
+ Table.prototype.buildColumns = function(metadata) {
+ var self = this;
+
+ this.columns = [];
+
+ if (metadata) {
+ metadata.forEach(function (metadatum) {
+ //Push domains first
+ metadatum.domains.forEach(function (domainMetadata) {
+ self.addColumn(new DomainColumn(domainMetadata, self.telemetryFormatter));
+ });
+ metadatum.ranges.forEach(function (rangeMetadata) {
+ self.addColumn(new RangeColumn(rangeMetadata, self.telemetryFormatter));
+ });
+ });
+ }
+ return this;
+ };
+
+ /**
+ * Add a column definition to this Table
+ * @param {RangeColumn | DomainColumn | NameColumn} column
+ * @param {Number} [index] Where the column should appear (will be
+ * affected by column filtering)
+ */
+ Table.prototype.addColumn = function (column, index) {
+ if (typeof index === 'undefined') {
+ this.columns.push(column);
+ } else {
+ this.columns.splice(index, 0, column);
+ }
+ };
+
+ /**
+ * @private
+ * @param column
+ * @returns {*|string}
+ */
+ Table.prototype.getColumnTitle = function (column) {
+ return column.getTitle();
+ };
+
+ /**
+ * Get a simple list of column titles
+ * @returns {Array} The titles of the columns
+ */
+ Table.prototype.getHeaders = function() {
+ var self = this;
+ return this.columns.map(function (column){
+ return self.getColumnTitle(column);
+ });
+ };
+
+ /**
+ * Retrieve and format values for a given telemetry datum.
+ * @param telemetryObject The object that the telemetry data is
+ * associated with
+ * @param datum The telemetry datum to retrieve values from
+ * @returns {Object} Key value pairs where the key is the column
+ * title, and the value is the formatted value from the provided datum.
+ */
+ Table.prototype.getRowValues = function(telemetryObject, datum) {
+ var self = this;
+ return this.columns.reduce(function(rowObject, column){
+ var columnTitle = self.getColumnTitle(column),
+ columnValue = column.getValue(telemetryObject, datum);
+
+ if (columnValue !== undefined && columnValue.text === undefined){
+ columnValue.text = '';
+ }
+ // Don't replace something with nothing.
+ // This occurs when there are multiple columns with the
+ // column title
+ if (rowObject[columnTitle] === undefined || rowObject[columnTitle].text === undefined || rowObject[columnTitle].text.length === 0) {
+ rowObject[columnTitle] = columnValue;
+ }
+ return rowObject;
+ }, {});
+ };
+
+ /**
+ * @private
+ */
+ Table.prototype.defaultColumnConfiguration = function () {
+ return ((this.domainObject.getModel().configuration || {}).table || {}).columns || {};
+ };
+
+ /**
+ * As part of the process of building the table definition, extract
+ * configuration from column definitions.
+ * @returns {Object} A configuration object consisting of key-value
+ * pairs where the key is the column title, and the value is a
+ * boolean indicating whether the column should be shown.
+ */
+ Table.prototype.getColumnConfiguration = function() {
+ var configuration = {},
+ //Use existing persisted config, or default it
+ defaultConfig = this.defaultColumnConfiguration();
+
+ /**
+ * For each column header, define a configuration value
+ * specifying whether the column is visible or not. Default to
+ * existing (persisted) configuration if available
+ */
+ this.getHeaders().forEach(function(columnTitle) {
+ configuration[columnTitle] = typeof defaultConfig[columnTitle] === 'undefined' ? true : defaultConfig[columnTitle];
+ });
+
+ return configuration;
+ };
+
+ return Table;
+ }
+);
diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js
new file mode 100644
index 0000000000..63a5aa5036
--- /dev/null
+++ b/platform/features/table/src/controllers/MCTTableController.js
@@ -0,0 +1,354 @@
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ function MCTTableController($scope, $timeout, element) {
+ var self = this;
+
+ this.$scope = $scope;
+ this.element = element;
+ this.$timeout = $timeout;
+ this.maxDisplayRows = 50;
+
+ $scope.visibleRows = [];
+ $scope.overrideRowPositioning = false;
+
+ /**
+ * Set default values for optional parameters on a given scope
+ */
+ function setDefaults($scope) {
+ if (typeof $scope.enableFilter === 'undefined') {
+ $scope.enableFilter = true;
+ $scope.filters = {};
+ }
+ if (typeof $scope.enableSort === 'undefined') {
+ $scope.enableSort = true;
+ $scope.sortColumn = undefined;
+ $scope.sortDirection = undefined;
+ }
+ }
+
+ setDefaults($scope);
+
+ element.find('div').on('scroll', this.onScroll.bind(this));
+
+ $scope.toggleSort = function (key) {
+ if (!$scope.enableSort) {
+ return;
+ }
+ if ($scope.sortColumn !== key) {
+ $scope.sortColumn = key;
+ $scope.sortDirection = 'asc';
+ } else if ($scope.sortDirection === 'asc') {
+ $scope.sortDirection = 'desc';
+ } else if ($scope.sortDirection === 'desc') {
+ $scope.sortColumn = undefined;
+ $scope.sortDirection = undefined;
+ }
+ self.updateRows($scope.rows);
+ };
+
+ $scope.$watchCollection('filters', function () {
+ self.updateRows(self.$scope.rows);
+ });
+ $scope.$watchCollection('headers', this.updateHeaders.bind(this));
+ $scope.$watchCollection('rows', this.updateRows.bind(this));
+ }
+
+ /**
+ * On scroll, calculate which rows indexes are visible and
+ * ensure that an equal number of rows are preloaded for
+ * scrolling in either direction.
+ */
+ MCTTableController.prototype.onScroll = function (event) {
+ var self = this,
+ topScroll = event.target.scrollTop,
+ bottomScroll = topScroll + event.target.offsetHeight,
+ firstVisible,
+ lastVisible,
+ totalVisible,
+ numberOffscreen,
+ start,
+ end;
+
+ if (this.$scope.displayRows.length < this.maxDisplayRows) {
+ return;
+ }
+
+ if (topScroll < this.$scope.headerHeight) {
+ firstVisible = 0;
+ } else {
+ firstVisible = Math.floor(
+ (topScroll - this.$scope.headerHeight) / this.$scope.rowHeight
+ );
+ }
+ lastVisible = Math.ceil(
+ (bottomScroll - this.$scope.headerHeight) / this.$scope.rowHeight
+ );
+
+ totalVisible = lastVisible - firstVisible;
+ numberOffscreen = this.maxDisplayRows - totalVisible;
+ start = firstVisible - Math.floor(numberOffscreen / 2);
+ end = lastVisible + Math.ceil(numberOffscreen / 2);
+
+ if (start < 0) {
+ start = 0;
+ end = this.$scope.visibleRows.length - 1;
+ } else if (end >= this.$scope.displayRows.length) {
+ end = this.$scope.displayRows.length - 1;
+ start = end - this.maxDisplayRows + 1;
+ }
+ if (this.$scope.visibleRows[0].rowIndex === start &&
+ this.$scope.visibleRows[this.$scope.visibleRows.length-1]
+ .rowIndex === end) {
+
+ return; // don't update if no changes are required.
+ }
+
+ this.$scope.visibleRows = this.$scope.displayRows.slice(start, end)
+ .map(function(row, i) {
+ return {
+ rowIndex: start + i,
+ offsetY: ((start + i) * self.$scope.rowHeight) +
+ self.$scope.headerHeight,
+ contents: row
+ };
+ });
+
+ this.$scope.$digest();
+ };
+
+ /**
+ * Update table headers with new headers. If filtering is
+ * enabled, reset filters. If sorting is enabled, reset
+ * sorting.
+ */
+ MCTTableController.prototype.updateHeaders = function (newHeaders) {
+ if (!newHeaders){
+ return;
+ }
+
+ this.$scope.displayHeaders = newHeaders;
+ if (this.$scope.enableFilter) {
+ this.$scope.filters = {};
+ }
+ // Reset column sort information unless the new headers
+ // contain the column current sorted on.
+ if (this.$scope.enableSort && newHeaders.indexOf(this.$scope.sortColumn) === -1) {
+ this.$scope.sortColumn = undefined;
+ this.$scope.sortDirection = undefined;
+ }
+ this.updateRows(this.$scope.rows);
+ };
+
+ /**
+ * Read styles from the DOM and use them to calculate offsets
+ * for individual rows.
+ */
+ MCTTableController.prototype.setElementSizes = function () {
+ var self = this,
+ thead = this.element.find('thead'),
+ tbody = this.element.find('tbody'),
+ firstRow = tbody.find('tr'),
+ column = firstRow.find('td'),
+ headerHeight = thead.prop('offsetHeight'),
+ //row height is hard-coded for now.
+ rowHeight = 20,
+ overallHeight = headerHeight + (rowHeight * (this.$scope.displayRows ? this.$scope.displayRows.length - 1 : 0));
+
+ this.$scope.columnWidths = [];
+
+ while (column.length) {
+ this.$scope.columnWidths.push(column.prop('offsetWidth'));
+ column = column.next();
+ }
+ this.$scope.headerHeight = headerHeight;
+ this.$scope.rowHeight = rowHeight;
+ this.$scope.totalHeight = overallHeight;
+
+ this.$scope.visibleRows = this.$scope.displayRows.slice(0, this.maxDisplayRows).map(function(row, i) {
+ return {
+ rowIndex: i,
+ offsetY: (i * self.$scope.rowHeight) + self.$scope.headerHeight,
+ contents: row
+ };
+ });
+
+ this.$scope.overrideRowPositioning = true;
+ };
+
+ /**
+ * Returns a new array which is a result of applying the sort
+ * criteria defined in $scope.
+ *
+ * Does not modify the array that was passed in.
+ */
+ MCTTableController.prototype.sortRows = function(rowsToSort) {
+ /**
+ * Compare two variables, returning a number that represents
+ * which is larger. Similar to the default array sort
+ * comparator, but does not coerce all values to string before
+ * conversion. Strings are lowercased before comparison.
+ */
+ function genericComparator(a, b) {
+ if (typeof a === "string" && typeof b === "string") {
+ a = a.toLowerCase();
+ b = b.toLowerCase();
+ }
+
+ if (a < b) {
+ return -1;
+ }
+ if (a > b) {
+ return 1;
+ }
+ return 0;
+ }
+
+ if (!this.$scope.sortColumn || !this.$scope.sortDirection) {
+ return rowsToSort;
+ }
+ var sortKey = this.$scope.sortColumn,
+ sortDirectionMultiplier;
+
+ if (this.$scope.sortDirection === 'asc') {
+ sortDirectionMultiplier = 1;
+ } else if (this.$scope.sortDirection === 'desc') {
+ sortDirectionMultiplier = -1;
+ }
+
+ return rowsToSort.slice(0).sort(function(a, b) {
+ //If the values to compare can be compared as
+ // numbers, do so. String comparison of number
+ // values can cause inconsistencies
+ var valA = isNaN(a[sortKey].text) ? a[sortKey].text : parseFloat(a[sortKey].text),
+ valB = isNaN(b[sortKey].text) ? b[sortKey].text : parseFloat(b[sortKey].text);
+
+ return genericComparator(valA, valB) *
+ sortDirectionMultiplier;
+ });
+ };
+
+ /**
+ * Returns an object which contains the largest values
+ * for each key in the given set of rows. This is used to
+ * pre-calculate optimal column sizes without having to render
+ * every row.
+ */
+ MCTTableController.prototype.findLargestRow = function(rows) {
+ var largestRow = rows.reduce(function (largestRow, row) {
+ Object.keys(row).forEach(function (key) {
+ var currentColumn = row[key].text,
+ currentColumnLength =
+ (currentColumn && currentColumn.length) ?
+ currentColumn.length :
+ currentColumn,
+ largestColumn = largestRow[key].text,
+ largestColumnLength =
+ (largestColumn && largestColumn.length) ?
+ largestColumn.length :
+ largestColumn;
+
+ if (currentColumnLength > largestColumnLength) {
+ largestRow[key] = JSON.parse(JSON.stringify(row[key]));
+ }
+ });
+ return largestRow;
+ }, JSON.parse(JSON.stringify(rows[0] || {})));
+
+ // Pad with characters to accomodate variable-width fonts,
+ // and remove characters that would allow word-wrapping.
+ largestRow = JSON.parse(JSON.stringify(largestRow));
+ Object.keys(largestRow).forEach(function(key) {
+ var padCharacters,
+ i;
+
+ largestRow[key].text = String(largestRow[key].text);
+ padCharacters = largestRow[key].text.length / 10;
+ for (i = 0; i < padCharacters; i++) {
+ largestRow[key].text = largestRow[key].text + 'W';
+ }
+ largestRow[key].text = largestRow[key].text
+ .replace(/[ \-_]/g, 'W');
+ });
+ return largestRow;
+ };
+
+ MCTTableController.prototype.resize = function (){
+ var largestRow = this.findLargestRow(this.$scope.displayRows);
+ this.$scope.visibleRows = [
+ {
+ rowIndex: 0,
+ offsetY: undefined,
+ contents: largestRow
+ }
+ ];
+
+ this.$timeout(this.setElementSizes.bind(this), 0);
+ };
+
+ /**
+ * Update rows with new data. If filtering is enabled, rows
+ * will be sorted before display.
+ */
+ MCTTableController.prototype.updateRows = function (newRows) {
+ var displayRows = newRows;
+ this.$scope.visibleRows = [];
+ this.$scope.overrideRowPositioning = false;
+
+ if (!this.$scope.displayHeaders) {
+ return;
+ }
+
+ if (this.$scope.enableFilter) {
+ displayRows = this.filterRows(displayRows);
+ }
+
+ if (this.$scope.enableSort) {
+ displayRows = this.sortRows(displayRows);
+ }
+ this.$scope.displayRows = displayRows;
+ this.resize();
+ };
+
+ /**
+ * Filter rows.
+ */
+ MCTTableController.prototype.filterRows = function(rowsToFilter) {
+ var filters = {},
+ self = this;
+
+ /**
+ * Returns true if row matches all filters.
+ */
+ function matchRow(filters, row) {
+ return Object.keys(filters).every(function(key) {
+ if (!row[key]) {
+ return false;
+ }
+ var testVal = String(row[key].text).toLowerCase();
+ return testVal.indexOf(filters[key]) !== -1;
+ });
+ }
+
+ if (!Object.keys(this.$scope.filters).length) {
+ return rowsToFilter;
+ }
+
+ Object.keys(this.$scope.filters).forEach(function(key) {
+ if (!self.$scope.filters[key]) {
+ return;
+ }
+ filters[key] = self.$scope.filters[key].toLowerCase();
+ });
+
+ return rowsToFilter.filter(matchRow.bind(null, filters));
+ };
+
+
+ return MCTTableController;
+ }
+);
diff --git a/platform/features/table/src/controllers/TableController.js b/platform/features/table/src/controllers/TableController.js
new file mode 100644
index 0000000000..3b723836ae
--- /dev/null
+++ b/platform/features/table/src/controllers/TableController.js
@@ -0,0 +1,214 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web 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 Web 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.
+ *****************************************************************************/
+/*global define*/
+
+/**
+ * This bundle adds a table view for displaying telemetry data.
+ * @namespace platform/features/table
+ */
+define(
+ [
+ '../Table',
+ '../NameColumn'
+ ],
+ function (Table, NameColumn) {
+ "use strict";
+
+ /**
+ * The TableController is responsible for getting data onto the page
+ * in the table widget. This includes handling composition,
+ * configuration, and telemetry subscriptions.
+ * @memberof platform/features/table
+ * @param $scope
+ * @param telemetryHandler
+ * @param telemetryFormatter
+ * @constructor
+ */
+ function TableController(
+ $scope,
+ telemetryHandler,
+ telemetryFormatter
+ ) {
+ var self = this;
+
+ this.$scope = $scope;
+ this.columns = {}; //Range and Domain columns
+ this.handle = undefined;
+ //this.pending = false;
+ this.telemetryHandler = telemetryHandler;
+ this.table = new Table($scope.domainObject, telemetryFormatter);
+ this.changeListeners = [];
+
+ $scope.rows = [];
+
+ // Subscribe to telemetry when a domain object becomes available
+ this.$scope.$watch('domainObject', function(domainObject){
+ if (!domainObject)
+ return;
+
+ self.subscribe();
+ self.registerChangeListeners();
+ });
+
+ // Unsubscribe when the plot is destroyed
+ this.$scope.$on("$destroy", this.destroy.bind(this));
+ }
+
+ TableController.prototype.registerChangeListeners = function() {
+ //Defer registration of change listeners until domain object is
+ // available in order to avoid race conditions
+
+ this.changeListeners.forEach(function (listener) {
+ return listener && listener();
+ });
+ this.changeListeners = [];
+ // When composition changes, re-subscribe to the various
+ // telemetry subscriptions
+ this.changeListeners.push(this.$scope.$watchCollection('domainObject.getModel().composition', this.subscribe.bind(this)));
+
+ //Change of bounds in time conductor
+ this.changeListeners.push(this.$scope.$on('telemetry:display:bounds', this.subscribe.bind(this)));
+
+ };
+
+ /**
+ * Release the current subscription (called when scope is destroyed)
+ */
+ TableController.prototype.destroy = function () {
+ if (this.handle) {
+ this.handle.unsubscribe();
+ this.handle = undefined;
+ }
+ };
+
+ /**
+ Create a new subscription. This is called when
+ */
+ TableController.prototype.subscribe = function() {
+ var self = this;
+
+ /*if (this.pending){
+ return;
+ }*/
+ //this.pending = true;
+
+ if (this.handle) {
+ this.handle.unsubscribe();
+ }
+
+ this.$scope.rows = [];
+
+ //Noop because not supporting realtime data right now
+ function noop(){
+ //self.pending = false;
+ }
+
+ this.handle = this.$scope.domainObject && this.telemetryHandler.handle(
+ this.$scope.domainObject,
+ noop,
+ true // Lossless
+ );
+
+ this.handle.request({}, this.addHistoricalData.bind(this));
+
+ this.setup();
+ };
+
+ /**
+ * Add any historical data available
+ */
+ TableController.prototype.addHistoricalData = function(domainObject, series) {
+ var i;
+ //this.pending = false;
+ for (i=0; i < series.getPointCount(); i++) {
+ this.updateRows(domainObject, this.handle.makeDatum(domainObject, series, i));
+ }
+ };
+
+ /**
+ * Set the established configuration on the domain object
+ * @private
+ */
+ TableController.prototype.writeConfigToModel = function (configuration) {
+ this.$scope.domainObject.useCapability('mutation', function (model) {
+ model.configuration = model.configuration || {};
+ model.configuration.table = model.configuration.table || {};
+ model.configuration.table.columns = configuration;
+ });
+ };
+
+ /**
+ * Setup table columns based on domain object metadata
+ */
+ TableController.prototype.setup = function() {
+ var handle = this.handle,
+ table = this.table,
+ self = this,
+ configuration;
+
+ if (handle) {
+ handle.promiseTelemetryObjects().then(function () {
+ table.buildColumns(handle.getMetadata());
+
+ if (table.columns.length > 0){
+ table.addColumn(new NameColumn(), 0);
+ }
+
+ self.filterColumns();
+
+ // When table column configuration changes, (due to being
+ // selected or deselected), filter columns appropriately.
+ self.changeListeners.push(self.$scope.$watchCollection(
+ 'domainObject.getModel().configuration.table.columns',
+ self.filterColumns.bind(self)
+ ));
+ });
+ }
+ };
+
+ /**
+ * Add data to rows
+ * @param object The object for which data is available (table may
+ * be composed of multiple objects)
+ * @param datum The data received from the telemetry source
+ */
+ TableController.prototype.updateRows = function (object, datum) {
+ this.$scope.rows.push(this.table.getRowValues(object, datum));
+ };
+
+ /**
+ * When column configuration changes, update the visible headers
+ * accordingly.
+ */
+ TableController.prototype.filterColumns = function () {
+ var config = this.table.getColumnConfiguration();
+
+ this.writeConfigToModel(config);
+ //Populate headers with visible columns (determined by configuration)
+ this.$scope.headers = Object.keys(config).filter(function(column) {
+ return config[column];
+ });
+ };
+
+ return TableController;
+ }
+);
diff --git a/platform/features/table/src/controllers/TableOptionsController.js b/platform/features/table/src/controllers/TableOptionsController.js
new file mode 100644
index 0000000000..eb6ef2ef9c
--- /dev/null
+++ b/platform/features/table/src/controllers/TableOptionsController.js
@@ -0,0 +1,94 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web 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 Web 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.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * Notes on implementation of plot options
+ *
+ * Multiple y-axes will have to be handled with multiple forms as
+ * they will need to be stored on distinct model object
+ *
+ * Likewise plot series options per-child will need to be separate
+ * forms.
+ */
+
+ /**
+ * The LayoutController is responsible for supporting the
+ * Layout view. It arranges frames according to saved configuration
+ * and provides methods for updating these based on mouse
+ * movement.
+ * @memberof platform/features/plot
+ * @constructor
+ * @param {Scope} $scope the controller's Angular scope
+ */
+ function TableOptionsController($scope) {
+
+ var self = this;
+
+ this.$scope = $scope;
+ this.domainObject = $scope.domainObject;
+
+ $scope.columnsForm = {};
+
+ this.domainObject.getCapability('mutation').listen(function (model) {
+ self.populateForm(model);
+ });
+
+ $scope.$watchCollection('configuration.table.columns', function(columns){
+ if (columns){
+ self.domainObject.useCapability('mutation', function(model) {
+ model.configuration.table.columns = columns;
+ });
+ self.domainObject.getCapability('persistence').persist();
+ }
+ });
+
+ }
+
+ TableOptionsController.prototype.populateForm = function (model) {
+ var columnsDefinition = (((model.configuration || {}).table || {}).columns || {}),
+ rows = [];
+ this.$scope.columnsForm = {
+ 'name':'Columns',
+ 'sections': [{
+ 'name': 'Columns',
+ 'rows': rows
+ }]};
+
+ Object.keys(columnsDefinition).forEach(function (key){
+ rows.push({
+ 'name': key,
+ 'control': 'checkbox',
+ 'key': key
+ });
+ });
+ this.$scope.configuration = JSON.parse(JSON.stringify(model.configuration));
+ };
+
+ return TableOptionsController;
+ }
+);
diff --git a/platform/features/table/src/directives/MCTTable.js b/platform/features/table/src/directives/MCTTable.js
new file mode 100644
index 0000000000..1e3eec6111
--- /dev/null
+++ b/platform/features/table/src/directives/MCTTable.js
@@ -0,0 +1,24 @@
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ function MCTTable($timeout) {
+ return {
+ restrict: "E",
+ templateUrl: "platform/features/table/res/templates/mct-data-table.html",
+ controller: 'MCTTableController',
+ scope: {
+ headers: "=",
+ rows: "=",
+ enableFilter: "=?",
+ enableSort: "=?"
+ }
+ };
+ }
+
+ return MCTTable;
+ }
+);
diff --git a/platform/features/table/test/DomainColumnSpec.js b/platform/features/table/test/DomainColumnSpec.js
new file mode 100644
index 0000000000..bb15f9d55e
--- /dev/null
+++ b/platform/features/table/test/DomainColumnSpec.js
@@ -0,0 +1,84 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web 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 Web 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.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,xit*/
+
+/**
+ * MergeModelsSpec. Created by vwoeltje on 11/6/14.
+ */
+define(
+ ["../src/DomainColumn"],
+ function (DomainColumn) {
+ "use strict";
+
+ var TEST_DOMAIN_VALUE = "some formatted domain value";
+
+ describe("A domain column", function () {
+ var mockDataSet,
+ testMetadata,
+ mockFormatter,
+ column;
+
+ beforeEach(function () {
+ mockDataSet = jasmine.createSpyObj(
+ "data",
+ [ "getDomainValue" ]
+ );
+ mockFormatter = jasmine.createSpyObj(
+ "formatter",
+ [ "formatDomainValue", "formatRangeValue" ]
+ );
+ testMetadata = {
+ key: "testKey",
+ name: "Test Name"
+ };
+ mockFormatter.formatDomainValue.andReturn(TEST_DOMAIN_VALUE);
+
+ column = new DomainColumn(testMetadata, mockFormatter);
+ });
+
+ it("reports a column header from domain metadata", function () {
+ expect(column.getTitle()).toEqual("Test Name");
+ });
+
+ xit("looks up data from a data set", function () {
+ column.getValue(undefined, mockDataSet, 42);
+ expect(mockDataSet.getDomainValue)
+ .toHaveBeenCalledWith(42, "testKey");
+ });
+
+ xit("formats domain values as time", function () {
+ mockDataSet.getDomainValue.andReturn(402513731000);
+
+ // Should have just given the value the formatter gave
+ expect(column.getValue(undefined, mockDataSet, 42).text)
+ .toEqual(TEST_DOMAIN_VALUE);
+
+ // Make sure that service interactions were as expected
+ expect(mockFormatter.formatDomainValue)
+ .toHaveBeenCalledWith(402513731000);
+ expect(mockFormatter.formatRangeValue)
+ .not.toHaveBeenCalled();
+ });
+
+ });
+ }
+);
diff --git a/platform/features/table/test/NameColumnSpec.js b/platform/features/table/test/NameColumnSpec.js
new file mode 100644
index 0000000000..355ebef545
--- /dev/null
+++ b/platform/features/table/test/NameColumnSpec.js
@@ -0,0 +1,58 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web 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 Web 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.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/
+
+/**
+ * MergeModelsSpec. Created by vwoeltje on 11/6/14.
+ */
+define(
+ ["../src/NameColumn"],
+ function (NameColumn) {
+ "use strict";
+
+ describe("A name column", function () {
+ var mockDomainObject,
+ column;
+
+ beforeEach(function () {
+ mockDomainObject = jasmine.createSpyObj(
+ "domainObject",
+ [ "getModel" ]
+ );
+ mockDomainObject.getModel.andReturn({
+ name: "Test object name"
+ });
+ column = new NameColumn();
+ });
+
+ it("reports a column header", function () {
+ expect(column.getTitle()).toEqual("Name");
+ });
+
+ it("looks up name from an object's model", function () {
+ expect(column.getValue(mockDomainObject).text)
+ .toEqual("Test object name");
+ });
+
+ });
+ }
+);
diff --git a/platform/features/table/test/RangeColumnSpec.js b/platform/features/table/test/RangeColumnSpec.js
new file mode 100644
index 0000000000..b77245bb82
--- /dev/null
+++ b/platform/features/table/test/RangeColumnSpec.js
@@ -0,0 +1,76 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web 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 Web 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.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,xit*/
+
+/**
+ * MergeModelsSpec. Created by vwoeltje on 11/6/14.
+ */
+define(
+ ["../src/RangeColumn"],
+ function (RangeColumn) {
+ "use strict";
+
+ var TEST_RANGE_VALUE = "some formatted range value";
+
+ describe("A range column", function () {
+ var testDatum,
+ testMetadata,
+ mockFormatter,
+ mockDomainObject,
+ column;
+
+ beforeEach(function () {
+ testDatum = { testKey: 123, otherKey: 456 };
+ mockFormatter = jasmine.createSpyObj(
+ "formatter",
+ [ "formatDomainValue", "formatRangeValue" ]
+ );
+ testMetadata = {
+ key: "testKey",
+ name: "Test Name"
+ };
+ mockDomainObject = jasmine.createSpyObj(
+ "domainObject",
+ [ "getModel", "getCapability" ]
+ );
+ mockFormatter.formatRangeValue.andReturn(TEST_RANGE_VALUE);
+
+ column = new RangeColumn(testMetadata, mockFormatter);
+ });
+
+ it("reports a column header from range metadata", function () {
+ expect(column.getTitle()).toEqual("Test Name");
+ });
+
+ it("formats range values as numbers", function () {
+ expect(column.getValue(mockDomainObject, testDatum).text)
+ .toEqual(TEST_RANGE_VALUE);
+
+ // Make sure that service interactions were as expected
+ expect(mockFormatter.formatRangeValue)
+ .toHaveBeenCalledWith(testDatum.testKey);
+ expect(mockFormatter.formatDomainValue)
+ .not.toHaveBeenCalled();
+ });
+ });
+ }
+);
diff --git a/platform/features/table/test/TableSpec.js b/platform/features/table/test/TableSpec.js
new file mode 100644
index 0000000000..f042ec6c1d
--- /dev/null
+++ b/platform/features/table/test/TableSpec.js
@@ -0,0 +1,197 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web 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 Web 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.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,xit*/
+
+define(
+ [
+ "../src/Table",
+ "../src/DomainColumn"
+ ],
+ function (Table, DomainColumn) {
+ "use strict";
+
+ describe("A table", function () {
+ var mockDomainObject,
+ mockTelemetryFormatter,
+ table,
+ mockModel;
+
+ beforeEach(function () {
+ mockDomainObject = jasmine.createSpyObj('domainObject',
+ ['getModel', 'useCapability', 'getCapability']
+ );
+ mockModel = {};
+ mockDomainObject.getModel.andReturn(mockModel);
+ mockTelemetryFormatter = jasmine.createSpyObj('telemetryFormatter',
+ [
+ 'formatDomainValue',
+ 'formatRangeValue'
+ ]);
+ mockTelemetryFormatter.formatDomainValue.andCallFake(function(valueIn){
+ return valueIn;
+ });
+ mockTelemetryFormatter.formatRangeValue.andCallFake(function(valueIn){
+ return valueIn;
+ });
+
+ table = new Table(mockDomainObject, mockTelemetryFormatter);
+ });
+
+ it("Add column with no index adds new column to the end", function () {
+ var firstColumn = {title: 'First Column'},
+ secondColumn = {title: 'Second Column'},
+ thirdColumn = {title: 'Third Column'};
+
+ table.addColumn(firstColumn);
+ table.addColumn(secondColumn);
+ table.addColumn(thirdColumn);
+
+ expect(table.columns).toBeDefined();
+ expect(table.columns.length).toBe(3);
+ expect(table.columns[0]).toBe(firstColumn);
+ expect(table.columns[1]).toBe(secondColumn);
+ expect(table.columns[2]).toBe(thirdColumn);
+ });
+
+ it("Add column with index adds new column at the specified" +
+ " position", function () {
+ var firstColumn = {title: 'First Column'},
+ secondColumn = {title: 'Second Column'},
+ thirdColumn = {title: 'Third Column'};
+
+ table.addColumn(firstColumn);
+ table.addColumn(thirdColumn);
+ table.addColumn(secondColumn, 1);
+
+ expect(table.columns).toBeDefined();
+ expect(table.columns.length).toBe(3);
+ expect(table.columns[0]).toBe(firstColumn);
+ expect(table.columns[1]).toBe(secondColumn);
+ expect(table.columns[2]).toBe(thirdColumn);
+ });
+
+ describe("Building columns from telemetry metadata", function() {
+ var metadata = [{
+ ranges: [
+ {
+ name: 'Range 1',
+ key: 'range1'
+ },
+ {
+ name: 'Range 2',
+ key: 'range2'
+ }
+ ],
+ domains: [
+ {
+ name: 'Domain 1',
+ key: 'domain1',
+ format: 'utc'
+ },
+ {
+ name: 'Domain 2',
+ key: 'domain2',
+ format: 'utc'
+ }
+ ]
+ }];
+
+ beforeEach(function() {
+ table.buildColumns(metadata);
+ });
+
+ it("populates the columns attribute", function() {
+ expect(table.columns.length).toBe(4);
+ });
+
+ it("Build columns populates columns with domains to the left", function() {
+ expect(table.columns[0] instanceof DomainColumn).toBeTruthy();
+ expect(table.columns[1] instanceof DomainColumn).toBeTruthy();
+ expect(table.columns[2] instanceof DomainColumn).toBeFalsy();
+ });
+
+ it("Produces headers for each column based on title", function() {
+ var headers,
+ firstColumn = table.columns[0];
+
+ spyOn(firstColumn, 'getTitle');
+ headers = table.getHeaders();
+ expect(headers.length).toBe(4);
+ expect(firstColumn.getTitle).toHaveBeenCalled();
+ });
+
+ it("Provides a default configuration with all columns" +
+ " visible", function() {
+ var configuration = table.getColumnConfiguration();
+
+ expect(configuration).toBeDefined();
+ expect(Object.keys(configuration).every(function(key){
+ return configuration[key];
+ }));
+ });
+
+ it("Column configuration exposes persisted configuration", function() {
+ var tableConfig,
+ modelConfig = {
+ table: {
+ columns : {
+ 'Range 1': false
+ }
+ }
+ };
+ mockModel.configuration = modelConfig;
+
+ tableConfig = table.getColumnConfiguration();
+
+ expect(tableConfig).toBeDefined();
+ expect(tableConfig['Range 1']).toBe(false);
+ });
+
+ describe('retrieving row values', function () {
+ var datum,
+ rowValues;
+
+ beforeEach(function() {
+ datum = {
+ 'range1': 'range 1 value',
+ 'range2': 'range 2 value',
+ 'domain1': 0,
+ 'domain2': 1
+ };
+ rowValues = table.getRowValues(mockDomainObject, datum);
+ });
+
+ it("Returns a value for every column", function() {
+ expect(rowValues['Range 1'].text).toBeDefined();
+ expect(rowValues['Range 1'].text).toEqual('range 1' +
+ ' value');
+ });
+
+ it("Uses the telemetry formatter to appropriately format" +
+ " telemetry values", function() {
+ expect(mockTelemetryFormatter.formatRangeValue).toHaveBeenCalled();
+ });
+ });
+ });
+ });
+ }
+);
diff --git a/platform/features/table/test/controllers/MCTTableControllerSpec.js b/platform/features/table/test/controllers/MCTTableControllerSpec.js
new file mode 100644
index 0000000000..e4f8d170d7
--- /dev/null
+++ b/platform/features/table/test/controllers/MCTTableControllerSpec.js
@@ -0,0 +1,155 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web 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 Web 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.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,xit*/
+
+define(
+ [
+ "../../src/controllers/MCTTableController"
+ ],
+ function (MCTTableController) {
+ "use strict";
+
+ describe('The MCTTable Controller', function() {
+
+ var controller,
+ mockScope,
+ watches,
+ mockTimeout,
+ mockElement;
+
+ function promise(value) {
+ return {
+ then: function (callback){
+ return promise(callback(value));
+ }
+ };
+ }
+
+ beforeEach(function() {
+ watches = {};
+
+ mockScope = jasmine.createSpyObj('scope', [
+ '$watchCollection'
+ ]);
+ mockScope.$watchCollection.andCallFake(function(event, callback) {
+ watches[event] = callback;
+ });
+
+ mockElement = jasmine.createSpyObj('element', [
+ 'find',
+ 'on'
+ ]);
+ mockElement.find.andReturn(mockElement);
+
+ mockScope.displayHeaders = true;
+ mockTimeout = jasmine.createSpy('$timeout');
+
+ controller = new MCTTableController(mockScope, mockTimeout, mockElement);
+ });
+
+ it('Reacts to changes to filters, headers, and rows', function() {
+ expect(mockScope.$watchCollection).toHaveBeenCalledWith('filters', jasmine.any(Function));
+ expect(mockScope.$watchCollection).toHaveBeenCalledWith('headers', jasmine.any(Function));
+ expect(mockScope.$watchCollection).toHaveBeenCalledWith('rows', jasmine.any(Function));
+ });
+
+ describe('rows', function() {
+ var testRows = [];
+ beforeEach(function() {
+ testRows = [
+ {
+ 'col1': {'text': 'row1 col1 match'},
+ 'col2': {'text': 'def'},
+ 'col3': {'text': 'row1 col3'}
+ },
+ {
+ 'col1': {'text': 'row2 col1 match'},
+ 'col2': {'text': 'abc'},
+ 'col3': {'text': 'row2 col3'}
+ },
+ {
+ 'col1': {'text': 'row3 col1'},
+ 'col2': {'text': 'ghi'},
+ 'col3': {'text': 'row3 col3'}
+ }
+ ];
+ });
+
+ it('Filters results based on filter input', function() {
+ var filters = {},
+ filteredRows;
+
+ mockScope.filters = filters;
+
+ filteredRows = controller.filterRows(testRows);
+ expect(filteredRows.length).toBe(3);
+ filters.col1 = 'row1';
+ filteredRows = controller.filterRows(testRows);
+ expect(filteredRows.length).toBe(1);
+ filters.col1 = 'match';
+ filteredRows = controller.filterRows(testRows);
+ expect(filteredRows.length).toBe(2);
+ });
+
+ it('Sets rows on scope when rows change', function() {
+ controller.updateRows(testRows);
+ expect(mockScope.displayRows.length).toBe(3);
+ expect(mockScope.displayRows).toEqual(testRows);
+ });
+
+ describe('sorting', function() {
+ var sortedRows;
+
+ it('Sorts rows ascending', function() {
+ mockScope.sortColumn = 'col1';
+ mockScope.sortDirection = 'asc';
+
+ sortedRows = controller.sortRows(testRows);
+ expect(sortedRows[0].col1.text).toEqual('row1 col1 match');
+ expect(sortedRows[1].col1.text).toEqual('row2 col1' +
+ ' match');
+ expect(sortedRows[2].col1.text).toEqual('row3 col1');
+
+ });
+
+ it('Sorts rows descending', function() {
+ mockScope.sortColumn = 'col1';
+ mockScope.sortDirection = 'desc';
+
+ sortedRows = controller.sortRows(testRows);
+ expect(sortedRows[0].col1.text).toEqual('row3 col1');
+ expect(sortedRows[1].col1.text).toEqual('row2 col1 match');
+ expect(sortedRows[2].col1.text).toEqual('row1 col1 match');
+ });
+ it('Sorts rows descending based on selected sort column', function() {
+ mockScope.sortColumn = 'col2';
+ mockScope.sortDirection = 'desc';
+
+ sortedRows = controller.sortRows(testRows);
+ expect(sortedRows[0].col2.text).toEqual('ghi');
+ expect(sortedRows[1].col2.text).toEqual('def');
+ expect(sortedRows[2].col2.text).toEqual('abc');
+ });
+ });
+ });
+ });
+ });
diff --git a/platform/features/table/test/controllers/TableControllerSpec.js b/platform/features/table/test/controllers/TableControllerSpec.js
new file mode 100644
index 0000000000..f87789eccf
--- /dev/null
+++ b/platform/features/table/test/controllers/TableControllerSpec.js
@@ -0,0 +1,224 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web 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 Web 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.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,xit*/
+
+define(
+ [
+ "../../src/controllers/TableController"
+ ],
+ function (TableController) {
+ "use strict";
+
+ describe('The Table Controller', function() {
+ var mockScope,
+ mockTelemetryHandler,
+ mockTelemetryHandle,
+ mockTelemetryFormatter,
+ mockDomainObject,
+ mockTable,
+ mockConfiguration,
+ watches,
+ controller;
+
+ function promise(value) {
+ return {
+ then: function (callback){
+ return promise(callback(value));
+ }
+ };
+ }
+
+ beforeEach(function() {
+ watches = {};
+ mockScope = jasmine.createSpyObj('scope', [
+ '$on',
+ '$watch',
+ '$watchCollection'
+ ]);
+
+ mockScope.$on.andCallFake(function(expression, callback){
+ watches[expression] = callback;
+ });
+ mockScope.$watch.andCallFake(function(expression, callback){
+ watches[expression] = callback;
+ });
+ mockScope.$watchCollection.andCallFake(function(expression, callback){
+ watches[expression] = callback;
+ });
+
+ mockConfiguration = {
+ 'range1': true,
+ 'range2': true,
+ 'domain1': true
+ };
+
+ mockTable = jasmine.createSpyObj('table',
+ [
+ 'buildColumns',
+ 'getColumnConfiguration',
+ 'getRowValues'
+ ]
+ );
+ mockTable.columns = [];
+ mockTable.getColumnConfiguration.andReturn(mockConfiguration);
+
+ mockDomainObject= jasmine.createSpyObj('domainObject', [
+ 'getCapability',
+ 'useCapability',
+ 'getModel'
+ ]);
+ mockDomainObject.getModel.andReturn({});
+
+ mockScope.domainObject = mockDomainObject;
+
+ mockTelemetryHandle = jasmine.createSpyObj('telemetryHandle', [
+ 'request',
+ 'promiseTelemetryObjects',
+ 'getMetadata',
+ 'unsubscribe',
+ 'makeDatum'
+ ]);
+ mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined));
+
+ mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [
+ 'handle'
+ ]);
+ mockTelemetryHandler.handle.andReturn(mockTelemetryHandle);
+
+ controller = new TableController(mockScope, mockTelemetryHandler, mockTelemetryFormatter);
+ controller.table = mockTable;
+ controller.handle = mockTelemetryHandle;
+ });
+
+ it('subscribes to telemetry handler for telemetry updates', function() {
+ controller.subscribe();
+ expect(mockTelemetryHandler.handle).toHaveBeenCalled();
+ expect(mockTelemetryHandle.request).toHaveBeenCalled();
+ });
+
+ it('Unsubscribes from telemetry when scope is destroyed',function() {
+ controller.handle = mockTelemetryHandle;
+ watches.$destroy();
+ expect(mockTelemetryHandle.unsubscribe).toHaveBeenCalled();
+ });
+
+ describe('the controller makes use of the table', function() {
+
+ it('to create column definitions from telemetry' +
+ ' metadata', function() {
+ controller.setup();
+ expect(mockTable.buildColumns).toHaveBeenCalled();
+ });
+
+ it('to create column configuration, which is written to the' +
+ ' object model', function() {
+ var mockModel = {};
+
+ controller.setup();
+ expect(mockTable.getColumnConfiguration).toHaveBeenCalled();
+ expect(mockDomainObject.useCapability).toHaveBeenCalledWith('mutation', jasmine.any(Function));
+
+ mockDomainObject.useCapability.mostRecentCall.args[1](mockModel);
+ expect(mockModel.configuration).toBeDefined();
+ });
+ });
+
+ it('updates the rows on scope when historical telemetry is received', function(){
+ var mockSeries = {
+ getPointCount: function() {
+ return 5;
+ },
+ getDomainValue: function() {
+ return 'Domain Value';
+ },
+ getRangeValue: function() {
+ return 'Range Value';
+ }
+ },
+ mockRow = {'domain': 'Domain Value', 'range': 'Range' +
+ ' Value'};
+
+ mockTelemetryHandle.makeDatum.andCallFake(function(){
+ return mockRow;
+ });
+ mockTable.getRowValues.andReturn(mockRow);
+ controller.addHistoricalData(mockDomainObject, mockSeries);
+
+ expect(controller.$scope.rows.length).toBe(5);
+ expect(controller.$scope.rows[0]).toBe(mockRow);
+ });
+
+ it('filters the visible columns based on configuration', function(){
+ controller.filterColumns();
+ expect(controller.$scope.headers.length).toBe(3);
+ expect(controller.$scope.headers[2]).toEqual('domain1');
+
+ mockConfiguration.domain1 = false;
+ controller.filterColumns();
+ expect(controller.$scope.headers.length).toBe(2);
+ expect(controller.$scope.headers[2]).toBeUndefined();
+ });
+
+ describe('creates event listeners', function(){
+ beforeEach(function() {
+ spyOn(controller,'subscribe');
+ spyOn(controller, 'filterColumns');
+ });
+
+ it('triggers telemetry subscription update when domain' +
+ ' object changes', function() {
+ controller.registerChangeListeners();
+ //'watches' object is populated by fake scope watch and
+ // watchCollection functions defined above
+ expect(watches.domainObject).toBeDefined();
+ watches.domainObject(mockDomainObject);
+ expect(controller.subscribe).toHaveBeenCalled();
+ });
+
+ it('triggers telemetry subscription update when domain' +
+ ' object composition changes', function() {
+ controller.registerChangeListeners();
+ expect(watches['domainObject.getModel().composition']).toBeDefined();
+ watches['domainObject.getModel().composition']();
+ expect(controller.subscribe).toHaveBeenCalled();
+ });
+
+ it('triggers telemetry subscription update when time' +
+ ' conductor bounds change', function() {
+ controller.registerChangeListeners();
+ expect(watches['telemetry:display:bounds']).toBeDefined();
+ watches['telemetry:display:bounds']();
+ expect(controller.subscribe).toHaveBeenCalled();
+ });
+
+ it('triggers refiltering of the columns when configuration' +
+ ' changes', function() {
+ controller.setup();
+ expect(watches['domainObject.getModel().configuration.table.columns']).toBeDefined();
+ watches['domainObject.getModel().configuration.table.columns']();
+ expect(controller.filterColumns).toHaveBeenCalled();
+ });
+
+ });
+ });
+ }
+);
diff --git a/platform/features/table/test/controllers/TableOptionsControllerSpec.js b/platform/features/table/test/controllers/TableOptionsControllerSpec.js
new file mode 100644
index 0000000000..9de96b5f52
--- /dev/null
+++ b/platform/features/table/test/controllers/TableOptionsControllerSpec.js
@@ -0,0 +1,105 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web 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 Web 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.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,xit*/
+
+define(
+ [
+ "../../src/controllers/TableOptionsController"
+ ],
+ function (TableOptionsController) {
+ "use strict";
+
+ describe('The Table Options Controller', function() {
+ var mockDomainObject,
+ mockCapability,
+ controller,
+ mockScope;
+
+ function promise(value) {
+ return {
+ then: function (callback){
+ return promise(callback(value));
+ }
+ };
+ }
+
+ beforeEach(function() {
+ mockCapability = jasmine.createSpyObj('mutationCapability', [
+ 'listen'
+ ]);
+ mockDomainObject = jasmine.createSpyObj('domainObject', [
+ 'getCapability'
+ ]);
+ mockDomainObject.getCapability.andReturn(mockCapability);
+ mockScope = jasmine.createSpyObj('scope', [
+ '$watchCollection'
+ ]);
+ mockScope.domainObject = mockDomainObject;
+
+ controller = new TableOptionsController(mockScope);
+ });
+
+ it('Registers a listener for mutation events on the object', function() {
+ expect(mockCapability.listen).toHaveBeenCalled();
+ });
+
+ it('Listens for changes to object composition and updates' +
+ ' options accordingly', function() {
+ expect(mockScope.$watchCollection).toHaveBeenCalledWith('configuration.table.columns', jasmine.any(Function));
+ });
+
+ describe('Populates scope with a form definition based on provided' +
+ ' column configuration', function() {
+ var mockModel;
+
+ beforeEach(function() {
+ mockModel = {
+ configuration: {
+ table: {
+ columns: {
+ 'column1': true,
+ 'column2': true,
+ 'column3': false,
+ 'column4': true,
+ }
+ }
+ }
+ };
+ controller.populateForm(mockModel);
+ });
+
+ it('creates form on scope', function() {
+ expect(mockScope.columnsForm).toBeDefined();
+ expect(mockScope.columnsForm.sections[0]).toBeDefined();
+ expect(mockScope.columnsForm.sections[0].rows).toBeDefined();
+ expect(mockScope.columnsForm.sections[0].rows.length).toBe(4);
+ });
+
+ it('presents columns as checkboxes', function() {
+ expect(mockScope.columnsForm.sections[0].rows.every(function(row){
+ return row.control === 'checkbox';
+ })).toBe(true);
+ });
+ });
+ });
+
+ });
\ No newline at end of file