diff --git a/platform/features/timeline/bundle.js b/platform/features/timeline/bundle.js index eb66456ce4..ae7a283fc5 100644 --- a/platform/features/timeline/bundle.js +++ b/platform/features/timeline/bundle.js @@ -91,7 +91,12 @@ define([ "name": "Export Timeline as CSV", "category": "contextual", "implementation": ExportTimelineAsCSVAction, - "depends": ["exportService", "notificationService"] + "depends": [ + "$log", + "exportService", + "notificationService", + "resources[]" + ] } ], "constants": [ diff --git a/platform/features/timeline/src/actions/CompositionColumn.js b/platform/features/timeline/src/actions/CompositionColumn.js index f9bede9983..e33f028f99 100644 --- a/platform/features/timeline/src/actions/CompositionColumn.js +++ b/platform/features/timeline/src/actions/CompositionColumn.js @@ -27,11 +27,15 @@ define([], function () { * in a domain object's composition. * @param {number} index the zero-based index of the composition * element associated with this column + * @param idMap an object containing key value pairs, where keys + * are domain object identifiers and values are whatever + * should appear in CSV output in their place * @constructor * @implements {platform/features/timeline.TimelineCSVColumn} */ - function CompositionColumn(index) { + function CompositionColumn(index, idMap) { this.index = index; + this.idMap = idMap; } CompositionColumn.prototype.name = function () { @@ -41,7 +45,9 @@ define([], function () { CompositionColumn.prototype.value = function (domainObject) { var model = domainObject.getModel(), composition = model.composition || []; - return (composition[this.index]) || ""; + + return composition.length > this.index ? + this.idMap[composition[this.index]] : ""; }; return CompositionColumn; diff --git a/platform/features/timeline/src/actions/ExportTimelineAsCSVAction.js b/platform/features/timeline/src/actions/ExportTimelineAsCSVAction.js index f71422358c..5b0e007e23 100644 --- a/platform/features/timeline/src/actions/ExportTimelineAsCSVAction.js +++ b/platform/features/timeline/src/actions/ExportTimelineAsCSVAction.js @@ -27,14 +27,23 @@ define(["./ExportTimelineAsCSVTask"], function (ExportTimelineAsCSVTask) { * * @param exportService the service used to perform the CSV export * @param notificationService the service used to show notifications + * @param {Array} resources an array of `resources` extensions * @param context the Action's context * @implements {Action} * @constructor * @memberof {platform/features/timeline} */ - function ExportTimelineAsCSVAction(exportService, notificationService, context) { + function ExportTimelineAsCSVAction( + $log, + exportService, + notificationService, + resources, + context + ) { + this.$log = $log; this.task = new ExportTimelineAsCSVTask( exportService, + resources, context.domainObject ); this.notificationService = notificationService; @@ -45,13 +54,15 @@ define(["./ExportTimelineAsCSVTask"], function (ExportTimelineAsCSVTask) { notification = notificationService.notify({ title: "Exporting CSV", unknownProgress: true - }); + }), + $log = this.$log; return this.task.run() .then(function () { notification.dismiss(); }) - .catch(function () { + .catch(function (err) { + $log.warn(err); notification.dismiss(); notificationService.error("Error exporting CSV"); }); diff --git a/platform/features/timeline/src/actions/ExportTimelineAsCSVTask.js b/platform/features/timeline/src/actions/ExportTimelineAsCSVTask.js index b8d796b3c4..d026edff3a 100644 --- a/platform/features/timeline/src/actions/ExportTimelineAsCSVTask.js +++ b/platform/features/timeline/src/actions/ExportTimelineAsCSVTask.js @@ -35,11 +35,13 @@ define([ * @constructor * @memberof {platform/features/timeline} * @param exportService the service used to export as CSV + * @param resources the `resources` extension category * @param {DomainObject} domainObject the timeline being exported */ - function ExportTimelineAsCSVTask(exportService, domainObject) { + function ExportTimelineAsCSVTask(exportService, resources, domainObject) { this.domainObject = domainObject; this.exportService = exportService; + this.resources = resources; } /** @@ -50,9 +52,10 @@ define([ */ ExportTimelineAsCSVTask.prototype.run = function () { var exportService = this.exportService; + var resources = this.resources; function doExport(objects) { - var exporter = new TimelineColumnizer(objects), + var exporter = new TimelineColumnizer(objects, resources), options = { headers: exporter.headers() }; return exporter.rows().then(function (rows) { return exportService.exportCSV(rows, options); diff --git a/platform/features/timeline/src/actions/IdColumn.js b/platform/features/timeline/src/actions/IdColumn.js index 38c8b9264e..9148ef6a8b 100644 --- a/platform/features/timeline/src/actions/IdColumn.js +++ b/platform/features/timeline/src/actions/IdColumn.js @@ -23,19 +23,23 @@ define([], function () { /** - * A column showing domain object identifiers. + * A column showing identifying domain objects. * @constructor + * @param idMap an object containing key value pairs, where keys + * are domain object identifiers and values are whatever + * should appear in CSV output in their place * @implements {platform/features/timeline.TimelineCSVColumn} */ - function IdColumn() { + function IdColumn(idMap) { + this.idMap = idMap; } IdColumn.prototype.name = function () { - return "Identifier"; + return "Index"; }; IdColumn.prototype.value = function (domainObject) { - return domainObject.getId(); + return this.idMap[domainObject.getId()]; }; return IdColumn; diff --git a/platform/features/timeline/src/actions/ModeColumn.js b/platform/features/timeline/src/actions/ModeColumn.js index fe2063566d..05eec7a30a 100644 --- a/platform/features/timeline/src/actions/ModeColumn.js +++ b/platform/features/timeline/src/actions/ModeColumn.js @@ -27,10 +27,14 @@ define([], function () { * @constructor * @param {number} index the zero-based index of the composition * element associated with this column + * @param idMap an object containing key value pairs, where keys + * are domain object identifiers and values are whatever + * should appear in CSV output in their place * @implements {platform/features/timeline.TimelineCSVColumn} */ - function ModeColumn(index) { + function ModeColumn(index, idMap) { this.index = index; + this.idMap = idMap; } ModeColumn.prototype.name = function () { @@ -39,8 +43,9 @@ define([], function () { ModeColumn.prototype.value = function (domainObject) { var model = domainObject.getModel(), - composition = (model.relationships || {}).modes || []; - return (composition[this.index]) || ""; + modes = (model.relationships || {}).modes || []; + return modes.length > this.index ? + this.idMap[modes[this.index]] : ""; }; return ModeColumn; diff --git a/platform/features/timeline/src/actions/TimelineColumnizer.js b/platform/features/timeline/src/actions/TimelineColumnizer.js index f24fa20eee..92e54b1860 100644 --- a/platform/features/timeline/src/actions/TimelineColumnizer.js +++ b/platform/features/timeline/src/actions/TimelineColumnizer.js @@ -25,13 +25,15 @@ define([ "./ModeColumn", "./CompositionColumn", "./MetadataColumn", - "./TimespanColumn" + "./TimespanColumn", + "./UtilizationColumn" ], function ( IdColumn, ModeColumn, CompositionColumn, MetadataColumn, - TimespanColumn + TimespanColumn, + UtilizationColumn ) { /** @@ -63,15 +65,17 @@ define([ * * @param {DomainObject[]} domainObjects the objects to include * in the exported data + * @param {Array} resources an array of `resources` extensions * @constructor * @memberof {platform/features/timeline} */ - function TimelineColumnizer(domainObjects) { + function TimelineColumnizer(domainObjects, resources) { var maxComposition = 0, maxRelationships = 0, columnNames = {}, columns = [], foundTimespan = false, + idMap, i; function addMetadataProperty(property) { @@ -82,7 +86,12 @@ define([ } } - columns.push(new IdColumn()); + idMap = domainObjects.reduce(function (map, domainObject, index) { + map[domainObject.getId()] = index + 1; + return map; + }, {}); + + columns.push(new IdColumn(idMap)); domainObjects.forEach(function (domainObject) { var model = domainObject.getModel(), @@ -113,12 +122,16 @@ define([ columns.push(new TimespanColumn(false)); } + resources.forEach(function (resource) { + columns.push(new UtilizationColumn(resource)); + }); + for (i = 0; i < maxComposition; i += 1) { - columns.push(new CompositionColumn(i)); + columns.push(new CompositionColumn(i, idMap)); } for (i = 0; i < maxRelationships; i += 1) { - columns.push(new ModeColumn(i)); + columns.push(new ModeColumn(i, idMap)); } this.domainObjects = domainObjects; diff --git a/platform/features/timeline/src/actions/UtilizationColumn.js b/platform/features/timeline/src/actions/UtilizationColumn.js new file mode 100644 index 0000000000..7a92ce668e --- /dev/null +++ b/platform/features/timeline/src/actions/UtilizationColumn.js @@ -0,0 +1,72 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2009-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. + *****************************************************************************/ + +define([], function () { + /** + * A column showing utilization costs associated with activities. + * @constructor + * @param {string} key the key for the particular cost + * @implements {platform/features/timeline.TimelineCSVColumn} + */ + function UtilizationColumn(resource) { + this.resource = resource; + } + + UtilizationColumn.prototype.name = function () { + var units = { + "Kbps": "Kb", + "watts": "watt-seconds" + }[this.resource.units] || "unknown units"; + + return this.resource.name + " (" + units + ")"; + }; + + UtilizationColumn.prototype.value = function (domainObject) { + var resource = this.resource; + + function getCost(utilization) { + var seconds = (utilization.end - utilization.start) / 1000; + return seconds * utilization.value; + } + + function getUtilizationValue(utilizations) { + utilizations = utilizations.filter(function (utilization) { + return utilization.key === resource.key; + }); + + if (utilizations.length === 0) { + return ""; + } + + return utilizations.map(getCost).reduce(function (a, b) { + return a + b; + }, 0); + } + + return domainObject.hasCapability('utilization') ? + domainObject.getCapability('utilization').internal() + .then(getUtilizationValue) : + ""; + }; + + return UtilizationColumn; +}); diff --git a/platform/features/timeline/src/capabilities/UtilizationCapability.js b/platform/features/timeline/src/capabilities/UtilizationCapability.js index 2744976615..55976f3ab5 100644 --- a/platform/features/timeline/src/capabilities/UtilizationCapability.js +++ b/platform/features/timeline/src/capabilities/UtilizationCapability.js @@ -193,6 +193,13 @@ define( * @returns {Promise.} a promise for resource identifiers */ resources: promiseResourceKeys, + /** + * Get the resource utilization associated with this object + * directly, not including any resource utilization associated + * with contained objects. + * @returns {Promise.} + */ + internal: promiseInternalUtilization, /** * Get the resource utilization associated with this * object. Results are not sorted. This requires looking diff --git a/platform/features/timeline/test/actions/CompositionColumnSpec.js b/platform/features/timeline/test/actions/CompositionColumnSpec.js index 8cf566a080..df52d08db5 100644 --- a/platform/features/timeline/test/actions/CompositionColumnSpec.js +++ b/platform/features/timeline/test/actions/CompositionColumnSpec.js @@ -23,13 +23,20 @@ define( ['../../src/actions/CompositionColumn'], function (CompositionColumn) { + var TEST_IDS = ['a', 'b', 'c', 'd', 'e', 'f']; + describe("CompositionColumn", function () { var testIndex, + testIdMap, column; beforeEach(function () { testIndex = 3; - column = new CompositionColumn(testIndex); + testIdMap = TEST_IDS.reduce(function (map, id, index) { + map[id] = index; + return map; + }, {}); + column = new CompositionColumn(testIndex, testIdMap); }); it("includes a one-based index in its name", function () { @@ -46,15 +53,13 @@ define( 'domainObject', ['getId', 'getModel', 'getCapability'] ); - testModel = { - composition: ['a', 'b', 'c', 'd', 'e', 'f'] - }; + testModel = { composition: TEST_IDS }; mockDomainObject.getModel.andReturn(testModel); }); - it("returns a corresponding identifier", function () { + it("returns a corresponding value from the map", function () { expect(column.value(mockDomainObject)) - .toEqual(testModel.composition[testIndex]); + .toEqual(testIdMap[testModel.composition[testIndex]]); }); it("returns nothing when composition is exceeded", function () { diff --git a/platform/features/timeline/test/actions/ExportTimelineAsCSVActionSpec.js b/platform/features/timeline/test/actions/ExportTimelineAsCSVActionSpec.js index e0f09c3ae6..e31f25b074 100644 --- a/platform/features/timeline/test/actions/ExportTimelineAsCSVActionSpec.js +++ b/platform/features/timeline/test/actions/ExportTimelineAsCSVActionSpec.js @@ -24,7 +24,8 @@ define( ['../../src/actions/ExportTimelineAsCSVAction'], function (ExportTimelineAsCSVAction) { describe("ExportTimelineAsCSVAction", function () { - var mockExportService, + var mockLog, + mockExportService, mockNotificationService, mockNotification, mockDomainObject, @@ -39,6 +40,13 @@ define( ['getId', 'getModel', 'getCapability', 'hasCapability'] ); mockType = jasmine.createSpyObj('type', ['instanceOf']); + + mockLog = jasmine.createSpyObj('$log', [ + 'warn', + 'error', + 'info', + 'debug' + ]); mockExportService = jasmine.createSpyObj( 'exportService', ['exportCSV'] @@ -63,8 +71,10 @@ define( testContext = { domainObject: mockDomainObject }; action = new ExportTimelineAsCSVAction( + mockLog, mockExportService, mockNotificationService, + [], testContext ); }); @@ -129,8 +139,11 @@ define( }); describe("and an error occurs", function () { + var testError; + beforeEach(function () { - testPromise.reject(); + testError = { someProperty: "some value" }; + testPromise.reject(testError); waitsFor(function () { return mockCallback.calls.length > 0; }); @@ -145,6 +158,10 @@ define( expect(mockNotificationService.error) .toHaveBeenCalledWith(jasmine.any(String)); }); + + it("logs the root cause", function () { + expect(mockLog.warn).toHaveBeenCalledWith(testError); + }); }); }); }); diff --git a/platform/features/timeline/test/actions/ExportTimelineAsCSVTaskSpec.js b/platform/features/timeline/test/actions/ExportTimelineAsCSVTaskSpec.js index 0330e86397..4deab99801 100644 --- a/platform/features/timeline/test/actions/ExportTimelineAsCSVTaskSpec.js +++ b/platform/features/timeline/test/actions/ExportTimelineAsCSVTaskSpec.js @@ -52,6 +52,7 @@ define( task = new ExportTimelineAsCSVTask( mockExportService, + [], mockDomainObject ); }); diff --git a/platform/features/timeline/test/actions/IdColumnSpec.js b/platform/features/timeline/test/actions/IdColumnSpec.js index f44d255255..80b84680a4 100644 --- a/platform/features/timeline/test/actions/IdColumnSpec.js +++ b/platform/features/timeline/test/actions/IdColumnSpec.js @@ -24,10 +24,12 @@ define( ['../../src/actions/IdColumn'], function (IdColumn) { describe("IdColumn", function () { - var column; + var testIdMap, + column; beforeEach(function () { - column = new IdColumn(); + testIdMap = { "foo": "bar" }; + column = new IdColumn(testIdMap); }); it("has a name", function () { @@ -47,9 +49,9 @@ define( mockDomainObject.getId.andReturn(testId); }); - it("provides a domain object's identifier", function () { + it("provides a value mapped from domain object's identifier", function () { expect(column.value(mockDomainObject)) - .toEqual(testId); + .toEqual(testIdMap[testId]); }); }); diff --git a/platform/features/timeline/test/actions/ModeColumnSpec.js b/platform/features/timeline/test/actions/ModeColumnSpec.js index 446e3b1030..037aa5c34f 100644 --- a/platform/features/timeline/test/actions/ModeColumnSpec.js +++ b/platform/features/timeline/test/actions/ModeColumnSpec.js @@ -23,13 +23,20 @@ define( ['../../src/actions/ModeColumn'], function (ModeColumn) { + var TEST_IDS = ['a', 'b', 'c', 'd', 'e', 'f']; + describe("ModeColumn", function () { var testIndex, + testIdMap, column; beforeEach(function () { testIndex = 3; - column = new ModeColumn(testIndex); + testIdMap = TEST_IDS.reduce(function (map, id, index) { + map[id] = index; + return map; + }, {}); + column = new ModeColumn(testIndex, testIdMap); }); it("includes a one-based index in its name", function () { @@ -48,15 +55,15 @@ define( ); testModel = { relationships: { - modes: ['a', 'b', 'c', 'd', 'e', 'f'] + modes: TEST_IDS } }; mockDomainObject.getModel.andReturn(testModel); }); - it("returns a corresponding identifier", function () { + it("returns a corresponding value from the map", function () { expect(column.value(mockDomainObject)) - .toEqual(testModel.relationships.modes[testIndex]); + .toEqual(testIdMap[testModel.relationships.modes[testIndex]]); }); it("returns nothing when relationships are exceeded", function () { diff --git a/platform/features/timeline/test/actions/TimelineColumnizerSpec.js b/platform/features/timeline/test/actions/TimelineColumnizerSpec.js index d29bb14278..980ed1e6c3 100644 --- a/platform/features/timeline/test/actions/TimelineColumnizerSpec.js +++ b/platform/features/timeline/test/actions/TimelineColumnizerSpec.js @@ -75,7 +75,7 @@ define( return c === 'metadata' && testMetadata; }); - exporter = new TimelineColumnizer(mockDomainObjects); + exporter = new TimelineColumnizer(mockDomainObjects, []); }); describe("rows", function () { @@ -94,13 +94,6 @@ define( it("include one row per domain object", function () { expect(rows.length).toEqual(mockDomainObjects.length); }); - - it("includes identifiers for each domain object", function () { - rows.forEach(function (row, index) { - var id = mockDomainObjects[index].getId(); - expect(row.indexOf(id)).not.toEqual(-1); - }); - }); }); describe("headers", function () {