diff --git a/platform/features/scrolling/src/ScrollingListController.js b/platform/features/scrolling/src/ScrollingListController.js index 3df32c4d71..8ef0bb4a4a 100644 --- a/platform/features/scrolling/src/ScrollingListController.js +++ b/platform/features/scrolling/src/ScrollingListController.js @@ -4,8 +4,8 @@ * Module defining ListController. Created by vwoeltje on 11/18/14. */ define( - ["./NameColumn", "./DomainColumn", "./RangeColumn"], - function (NameColumn, DomainColumn, RangeColumn) { + ["./NameColumn", "./DomainColumn", "./RangeColumn", "./ScrollingListPopulator"], + function (NameColumn, DomainColumn, RangeColumn, ScrollingListPopulator) { "use strict"; var ROW_COUNT = 18; @@ -16,104 +16,16 @@ define( * @constructor */ function ScrollingListController($scope) { - var columns = []; // Domain used + var populator; - /** - * Look up the most recent values from a set of data objects. - * Returns an array of objects in the order in which data - * should be displayed; each element is an object with - * two properties: - * - * * objectIndex: The index of the domain object associated - * with the data point to be displayed in that - * row. - * * pointIndex: The index of the data point itself, within - * its data set. - * - * @param {Array} datas an array of the most recent - * data objects; expected to be in the same order - * as the domain objects provided at constructor - * @param {Array= 0) { - nextTime = data.getDomainValue(pointIndex); - if (nextTime > candidateTime) { - candidateTime = nextTime; - candidate = { - objectIndex: i, - pointIndex: pointIndex - }; - } - } - } - - // Assemble a list of the most recent data points - while (latest.length < ROW_COUNT) { - // Reset variables pre-search - candidateTime = Number.NEGATIVE_INFINITY; - candidate = undefined; - - // Linear search for most recent - datas.forEach(findCandidate); - - if (candidate) { - // Record this data point - it is the most recent - latest.push(candidate); - - // Track the data points used so we can look farther back - // in the data set on the next iteration - used[candidate.objectIndex] = used[candidate.objectIndex] + 1; - } else { - // Ran out of candidates; not enough data points - // available to fill all rows. - break; - } - } - - return latest; - } // Get a set of populated, ready-to-display rows for the // latest data values. function getRows(telemetry) { var datas = telemetry.getResponse(), - objects = telemetry.getTelemetryObjects(), - values = getLatestDataValues(datas); + objects = telemetry.getTelemetryObjects(); - // Each value will become a row, which will contain - // some value in each column (rendering by the - // column object itself) - return values.map(function (value) { - return columns.map(function (column) { - return column.getValue( - objects[value.objectIndex], - datas[value.objectIndex], - value.pointIndex - ); - }); - }); + return populator.getRows(datas, objects, ROW_COUNT); } // Update the contents @@ -128,6 +40,7 @@ define( function setupColumns(telemetry) { var domainKeys = {}, rangeKeys = {}, + columns = [], metadata; // Add a domain to the set of columns, if a domain @@ -135,6 +48,7 @@ define( function addDomain(domain) { var key = domain.key; if (key && !domainKeys[key]) { + domainKeys[key] = true; columns.push(new DomainColumn(domain)); } } @@ -144,6 +58,7 @@ define( function addRange(range) { var key = range.key; if (key && !rangeKeys[key]) { + rangeKeys[key] = true; columns.push(new RangeColumn(range)); } } @@ -163,6 +78,8 @@ define( metadata = telemetry.getMetadata(); (metadata || []).forEach(function (metadata) { (metadata.domains || []).forEach(addDomain); + }); + (metadata || []).forEach(function (metadata) { (metadata.ranges || []).forEach(addRange); }); @@ -175,11 +92,13 @@ define( columns.push(new RangeColumn({ name: "Value" })); } - // We have all columns now, so populate the headers - // for these columns in the scope. - $scope.headers = columns.map(function (column) { - return column.getTitle(); - }); + // We have all columns now; use them to initializer + // the populator, which will use them to generate + // actual rows and headers. + populator = new ScrollingListPopulator(columns); + + // Initialize headers + $scope.headers = populator.getHeaders(); // Fill in the contents of the rows. updateRows(); diff --git a/platform/features/scrolling/src/ScrollingListPopulator.js b/platform/features/scrolling/src/ScrollingListPopulator.js new file mode 100644 index 0000000000..b2d1a5da72 --- /dev/null +++ b/platform/features/scrolling/src/ScrollingListPopulator.js @@ -0,0 +1,115 @@ +/*global define*/ + +define( + [], + function () { + "use strict"; + + function ScrollingListPopulator(columns) { + /** + * Look up the most recent values from a set of data objects. + * Returns an array of objects in the order in which data + * should be displayed; each element is an object with + * two properties: + * + * * objectIndex: The index of the domain object associated + * with the data point to be displayed in that + * row. + * * pointIndex: The index of the data point itself, within + * its data set. + * + * @param {Array} datas an array of the most recent + * data objects; expected to be in the same order + * as the domain objects provided at constructor + * @param {Array= 0) { + nextTime = data.getDomainValue(pointIndex); + if (nextTime > candidateTime) { + candidateTime = nextTime; + candidate = { + objectIndex: i, + pointIndex: pointIndex + }; + } + } + } + + // Assemble a list of the most recent data points + while (latest.length < count) { + // Reset variables pre-search + candidateTime = Number.NEGATIVE_INFINITY; + candidate = undefined; + + // Linear search for most recent + datas.forEach(findCandidate); + + if (candidate) { + // Record this data point - it is the most recent + latest.push(candidate); + + // Track the data points used so we can look farther back + // in the data set on the next iteration + used[candidate.objectIndex] = used[candidate.objectIndex] + 1; + } else { + // Ran out of candidates; not enough data points + // available to fill all rows. + break; + } + } + + return latest; + } + + + return { + getHeaders: function () { + return columns.map(function (column) { + return column.getTitle(); + }); + }, + getRows: function (datas, objects, count) { + var values = getLatestDataValues(datas, count); + + // Each value will become a row, which will contain + // some value in each column (rendering by the + // column object itself) + return values.map(function (value) { + return columns.map(function (column) { + return column.getValue( + objects[value.objectIndex], + datas[value.objectIndex], + value.pointIndex + ); + }); + }); + } + }; + } + + return ScrollingListPopulator; + + } +); \ No newline at end of file diff --git a/platform/features/scrolling/test/DomainColumnSpec.js b/platform/features/scrolling/test/DomainColumnSpec.js index d2f1629c55..9fe38779e1 100644 --- a/platform/features/scrolling/test/DomainColumnSpec.js +++ b/platform/features/scrolling/test/DomainColumnSpec.js @@ -9,6 +9,38 @@ define( "use strict"; describe("A domain column", function () { + var mockDataSet, + testMetadata, + column; + + beforeEach(function () { + mockDataSet = jasmine.createSpyObj( + "data", + [ "getDomainValue" ] + ); + testMetadata = { + key: "testKey", + name: "Test Name" + }; + column = new DomainColumn(testMetadata); + }); + + it("reports a column header from domain metadata", function () { + expect(column.getTitle()).toEqual("Test Name"); + }); + + it("looks up data from a data set", function () { + column.getValue(undefined, mockDataSet, 42); + expect(mockDataSet.getDomainValue) + .toHaveBeenCalledWith(42, "testKey"); + }); + + it("formats domain values as time", function () { + mockDataSet.getDomainValue.andReturn(402513731000); + expect(column.getValue(undefined, mockDataSet, 42)) + .toEqual("1982-276 17:22:11"); + }); + }); } ); \ No newline at end of file diff --git a/platform/features/scrolling/test/NameColumnSpec.js b/platform/features/scrolling/test/NameColumnSpec.js index 5763bd6021..696cc07707 100644 --- a/platform/features/scrolling/test/NameColumnSpec.js +++ b/platform/features/scrolling/test/NameColumnSpec.js @@ -9,6 +9,29 @@ define( "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)) + .toEqual("Test object name"); + }); + }); } ); \ No newline at end of file diff --git a/platform/features/scrolling/test/RangeColumnSpec.js b/platform/features/scrolling/test/RangeColumnSpec.js index 39983bc153..efb262420a 100644 --- a/platform/features/scrolling/test/RangeColumnSpec.js +++ b/platform/features/scrolling/test/RangeColumnSpec.js @@ -9,6 +9,37 @@ define( "use strict"; describe("A range column", function () { + var mockDataSet, + testMetadata, + column; + + beforeEach(function () { + mockDataSet = jasmine.createSpyObj( + "data", + [ "getRangeValue" ] + ); + testMetadata = { + key: "testKey", + name: "Test Name" + }; + column = new RangeColumn(testMetadata); + }); + + it("reports a column header from range metadata", function () { + expect(column.getTitle()).toEqual("Test Name"); + }); + + it("looks up data from a data set", function () { + column.getValue(undefined, mockDataSet, 42); + expect(mockDataSet.getRangeValue) + .toHaveBeenCalledWith(42, "testKey"); + }); + + it("formats range values as time", function () { + mockDataSet.getRangeValue.andReturn(123.45678); + expect(column.getValue(undefined, mockDataSet, 42)) + .toEqual("123.457"); + }); }); } ); \ No newline at end of file diff --git a/platform/features/scrolling/test/ScrollingListControllerSpec.js b/platform/features/scrolling/test/ScrollingListControllerSpec.js index 25922d442a..7aec4a7e40 100644 --- a/platform/features/scrolling/test/ScrollingListControllerSpec.js +++ b/platform/features/scrolling/test/ScrollingListControllerSpec.js @@ -9,6 +9,81 @@ define( "use strict"; describe("The scrolling list controller", function () { + var mockScope, + mockTelemetry, + testMetadata, + controller; + + beforeEach(function () { + mockScope = jasmine.createSpyObj( + "$scope", + [ "$on", "$watch" ] + ); + mockTelemetry = jasmine.createSpyObj( + "telemetryController", + [ "getResponse", "getMetadata", "getTelemetryObjects" ] + ); + testMetadata = [ + { + domains: [ + { key: "d0", name: "D0" }, + { key: "d1", name: "D1" } + ], + ranges: [ + { key: "r0", name: "R0" }, + { key: "r1", name: "R1" } + ] + }, + { + domains: [ + { key: "d0", name: "D0" }, + { key: "d2", name: "D2" } + ], + ranges: [ + { key: "r0", name: "R0" } + ] + } + ]; + mockTelemetry.getMetadata.andReturn(testMetadata); + mockTelemetry.getResponse.andReturn([]); + mockTelemetry.getTelemetryObjects.andReturn([]); + mockScope.telemetry = mockTelemetry; + controller = new ScrollingListController(mockScope); + }); + + it("listens for telemetry data updates", function () { + expect(mockScope.$on).toHaveBeenCalledWith( + "telemetryUpdate", + jasmine.any(Function) + ); + }); + + it("watches for telemetry controller changes", function () { + expect(mockScope.$watch).toHaveBeenCalledWith( + "telemetry", + jasmine.any(Function) + ); + }); + + it("provides a column for each name and each unique domain, range", function () { + // Should have six columns based on metadata above, + // (name, d0, d1, d2, r0, r1) + mockScope.$watch.mostRecentCall.args[1](mockTelemetry); + expect(mockScope.headers).toEqual(["Name", "D0", "D1", "D2", "R0", "R1"]); + }); + + it("does not throw if telemetry controller is undefined", function () { + // Just a general robustness check + mockScope.telemetry = undefined; + expect(mockScope.$watch.mostRecentCall.args[1]) + .not.toThrow(); + }); + + it("provides default columns if domain/range metadata is unavailable", function () { + mockTelemetry.getMetadata.andReturn([]); + mockScope.$watch.mostRecentCall.args[1](mockTelemetry); + expect(mockScope.headers).toEqual(["Name", "Time", "Value"]); + }); }); } ); \ No newline at end of file