Merge branch 'open1515' into open115

This commit is contained in:
Victor Woeltjen
2015-09-23 16:05:09 -07:00
42 changed files with 2477 additions and 177 deletions

View File

@@ -0,0 +1,9 @@
Provides the time conductor, a control which appears at the
bottom of the screen allowing telemetry start and end times
to be modified.
Note that the term "time controller" is generally preferred
outside of the code base (e.g. in UI documents, issues, etc.);
the term "time conductor" is being used in code to avoid
confusion with "controllers" in the Model-View-Controller
sense.

View File

@@ -0,0 +1,25 @@
{
"extensions": {
"representers": [
{
"implementation": "ConductorRepresenter.js",
"depends": [ "conductorService", "$compile", "views[]" ]
}
],
"components": [
{
"type": "decorator",
"provides": "telemetryService",
"implementation": "ConductorTelemetryDecorator.js",
"depends": [ "conductorService" ]
}
],
"services": [
{
"key": "conductorService",
"implementation": "ConductorService.js",
"depends": [ "now" ]
}
]
}
}

View File

@@ -0,0 +1,167 @@
/*****************************************************************************
* 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";
var CONDUCTOR_HEIGHT = "100px",
TEMPLATE = [
'<div style=',
'"position: absolute; bottom: 0; width: 100%; ',
'overflow: hidden; ',
'height: ' + CONDUCTOR_HEIGHT + '">',
"<mct-include key=\"'time-controller'\" ng-model='conductor'>",
"</mct-include>",
'</div>'
].join(''),
GLOBAL_SHOWING = false;
/**
* The ConductorRepresenter attaches the universal time conductor
* to views.
*
* @implements {Representer}
* @constructor
* @memberof platform/features/conductor
* @param {platform/features/conductor.ConductorService} conductorService
* service which provides the active time conductor
* @param $compile Angular's $compile
* @param {ViewDefinition[]} views all defined views
* @param {Scope} the scope of the representation
* @param element the jqLite-wrapped representation element
*/
function ConductorRepresenter(conductorService, $compile, views, scope, element) {
this.scope = scope;
this.conductorService = conductorService;
this.element = element;
this.views = views;
this.$compile = $compile;
}
// Combine start/end times into a single object
function bounds(start, end) {
return { start: start, end: end };
}
// Update the time conductor from the scope
function wireScope(conductor, conductorScope, repScope) {
function updateConductorOuter() {
conductor.queryStart(conductorScope.conductor.outer.start);
conductor.queryEnd(conductorScope.conductor.outer.end);
repScope.$broadcast(
'telemetry:query:bounds',
bounds(conductor.queryStart(), conductor.queryEnd())
);
}
function updateConductorInner() {
conductor.displayStart(conductorScope.conductor.inner.start);
conductor.displayEnd(conductorScope.conductor.inner.end);
repScope.$broadcast(
'telemetry:display:bounds',
bounds(conductor.displayStart(), conductor.displayEnd())
);
}
conductorScope.conductor = {
outer: bounds(conductor.queryStart(), conductor.queryEnd()),
inner: bounds(conductor.displayStart(), conductor.displayEnd())
};
conductorScope
.$watch('conductor.outer.start', updateConductorOuter);
conductorScope
.$watch('conductor.outer.end', updateConductorOuter);
conductorScope
.$watch('conductor.inner.start', updateConductorInner);
conductorScope
.$watch('conductor.inner.end', updateConductorInner);
repScope.$on('telemetry:view', updateConductorInner);
}
ConductorRepresenter.prototype.conductorScope = function (s) {
return (this.cScope = arguments.length > 0 ?
s : this.cScope);
};
// Handle a specific representation of a specific domain object
ConductorRepresenter.prototype.represent = function represent(representation, representedObject) {
this.destroy();
if (this.views.indexOf(representation) !== -1 && !GLOBAL_SHOWING) {
// Track original states
this.originalHeight = this.element.css('height');
this.hadAbs = this.element.hasClass('abs');
// Create a new scope for the conductor
this.conductorScope(this.scope.$new());
wireScope(
this.conductorService.getConductor(),
this.conductorScope(),
this.scope
);
this.conductorElement =
this.$compile(TEMPLATE)(this.conductorScope());
this.element.after(this.conductorElement[0]);
this.element.addClass('abs');
this.element.css('bottom', CONDUCTOR_HEIGHT);
GLOBAL_SHOWING = true;
}
};
// Respond to the destruction of the current representation.
ConductorRepresenter.prototype.destroy = function destroy() {
// We may not have decided to show in the first place,
// so circumvent any unnecessary cleanup
if (!this.conductorElement) {
return;
}
// Restore the original size of the mct-representation
if (!this.hadAbs) {
this.element.removeClass('abs');
}
this.element.css('height', this.originalHeight);
// ...and remove the conductor
if (this.conductorElement) {
this.conductorElement.remove();
this.conductorElement = undefined;
}
// Finally, destroy its scope
if (this.conductorScope()) {
this.conductorScope().$destroy();
this.conductorScope(undefined);
}
GLOBAL_SHOWING = false;
};
return ConductorRepresenter;
}
);

View File

@@ -0,0 +1,61 @@
/*****************************************************************************
* 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(
['./TimeConductor'],
function (TimeConductor) {
'use strict';
var ONE_DAY_IN_MS = 1000 * 60 * 60 * 24,
SIX_HOURS_IN_MS = ONE_DAY_IN_MS / 4;
/**
* Provides a single global instance of the time conductor, which
* controls both query ranges and displayed ranges for telemetry
* data.
*
* @constructor
* @memberof platform/features/conductor
* @param {Function} now a function which returns the current time
* as a UNIX timestamp, in milliseconds
*/
function ConductorService(now) {
var initialEnd =
Math.ceil(now() / SIX_HOURS_IN_MS) * SIX_HOURS_IN_MS;
this.conductor =
new TimeConductor(initialEnd - ONE_DAY_IN_MS, initialEnd);
}
/**
* Get the global instance of the time conductor.
* @returns {platform/features/conductor.TimeConductor} the
* time conductor
*/
ConductorService.prototype.getConductor = function () {
return this.conductor;
};
return ConductorService;
}
);

View File

@@ -0,0 +1,108 @@
/*****************************************************************************
* 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(
['./ConductorTelemetrySeries'],
function (ConductorTelemetrySeries) {
'use strict';
/**
* Decorates the `telemetryService` such that requests are
* mediated by the time conductor.
*
* @constructor
* @memberof platform/features/conductor
* @implements {TelemetryService}
* @param {platform/features/conductor.ConductorService} conductorServe
* the service which exposes the global time conductor
* @param {TelemetryService} telemetryService the decorated service
*/
function ConductorTelemetryDecorator(conductorService, telemetryService) {
this.conductorService = conductorService;
this.telemetryService = telemetryService;
}
// Strip out any realtime data series that is outside of the conductor's
// bounds.
ConductorTelemetryDecorator.prototype.pruneNonDisplayable = function (packaged) {
var conductor = this.conductorService.getConductor(),
repackaged = {};
function filterSource(packagedBySource) {
var repackagedBySource = {};
Object.keys(packagedBySource).forEach(function (k) {
repackagedBySource[k] = new ConductorTelemetrySeries(
packagedBySource[k],
conductor
);
});
return repackagedBySource;
}
Object.keys(packaged).forEach(function (source) {
repackaged[source] = filterSource(packaged[source]);
});
return repackaged;
};
ConductorTelemetryDecorator.prototype.amendRequests = function (requests) {
var conductor = this.conductorService.getConductor(),
start = conductor.displayStart(),
end = conductor.displayEnd();
function amendRequest(request) {
request = request || {};
request.start = start;
request.end = end;
return request;
}
return (requests || []).map(amendRequest);
};
ConductorTelemetryDecorator.prototype.requestTelemetry = function (requests) {
var self = this;
return this.telemetryService
.requestTelemetry(this.amendRequests(requests))
.then(function (packaged) {
return self.pruneNonDisplayable(packaged);
});
};
ConductorTelemetryDecorator.prototype.subscribe = function (callback, requests) {
var self = this;
function internalCallback(packagedSeries) {
return callback(self.pruneNonDisplayable(packagedSeries));
}
return this.telemetryService
.subscribe(internalCallback, this.amendRequests(requests));
};
return ConductorTelemetryDecorator;
}
);

View File

@@ -0,0 +1,71 @@
/*****************************************************************************
* 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';
/**
* Bound a series of telemetry such that it only includes
* points from within the time conductor's displayable window.
*
* @param {TelemetrySeries} series the telemetry series
* @param {platform/features/conductor.TimeConductor} the
* time conductor instance which bounds this series
* @constructor
* @implements {TelemetrySeries}
*/
function ConductorTelemetrySeries(series, conductor) {
var max = series.getPointCount() - 1;
function binSearch(min, max, value) {
var mid = Math.floor((min + max) / 2);
return min > max ? min :
series.getDomainValue(mid) < value ?
binSearch(mid + 1, max, value) :
binSearch(min, mid - 1, value);
}
this.startIndex = binSearch(0, max, conductor.displayStart());
this.endIndex = binSearch(0, max, conductor.displayEnd());
this.series = series;
}
ConductorTelemetrySeries.prototype.getPointCount = function () {
return Math.max(0, this.endIndex - this.startIndex);
};
ConductorTelemetrySeries.prototype.getDomainValue = function (i, d) {
return this.series.getDomainValue(i + this.startIndex, d);
};
ConductorTelemetrySeries.prototype.getRangeValue = function (i, r) {
return this.series.getRangeValue(i + this.startIndex, r);
};
return ConductorTelemetrySeries;
}
);

View File

@@ -0,0 +1,99 @@
/*****************************************************************************
* 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*/
/**
* The time conductor bundle adds a global control to the bottom of the
* outermost viewing area. This controls both the range for time-based
* queries and for time-based displays.
*
* @namespace platform/features/conductor
*/
define(
function () {
'use strict';
/**
* Tracks the current state of the time conductor.
*
* @memberof platform/features/conductor
* @constructor
* @param {number} start the initial start time
* @param {number} end the initial end time
*/
function TimeConductor(start, end) {
this.inner = { start: start, end: end };
this.outer = { start: start, end: end };
}
/**
* Get or set (if called with an argument) the start time for queries.
* @param {number} [value] the start time to set
* @returns {number} the start time
*/
TimeConductor.prototype.queryStart = function (value) {
if (arguments.length > 0) {
this.outer.start = value;
}
return this.outer.start;
};
/**
* Get or set (if called with an argument) the end time for queries.
* @param {number} [value] the end time to set
* @returns {number} the end time
*/
TimeConductor.prototype.queryEnd = function (value) {
if (arguments.length > 0) {
this.outer.end = value;
}
return this.outer.end;
};
/**
* Get or set (if called with an argument) the start time for displays.
* @param {number} [value] the start time to set
* @returns {number} the start time
*/
TimeConductor.prototype.displayStart = function (value) {
if (arguments.length > 0) {
this.inner.start = value;
}
return this.inner.start;
};
/**
* Get or set (if called with an argument) the end time for displays.
* @param {number} [value] the end time to set
* @returns {number} the end time
*/
TimeConductor.prototype.displayEnd = function (value) {
if (arguments.length > 0) {
this.inner.end = value;
}
return this.inner.end;
};
return TimeConductor;
}
);

View File

@@ -0,0 +1,167 @@
/*****************************************************************************
* 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,afterEach,jasmine*/
/**
* EventSpec. Created by vwoeltje on 11/6/14. Modified by shale on 06/23/2015.
*/
define(
["../src/ConductorRepresenter"],
function (ConductorRepresenter) {
"use strict";
var SCOPE_METHODS = [
'$on',
'$watch',
'$broadcast',
'$emit',
'$new',
'$destroy'
],
ELEMENT_METHODS = [
'hasClass',
'addClass',
'removeClass',
'css',
'after',
'remove'
];
describe("ConductorRepresenter", function () {
var mockConductorService,
mockCompile,
testViews,
mockScope,
mockElement,
mockConductor,
mockCompiledTemplate,
mockNewScope,
mockNewElement,
representer;
function fireWatch(scope, watch, value) {
scope.$watch.calls.forEach(function (call) {
if (call.args[0] === watch) {
call.args[1](value);
}
});
}
beforeEach(function () {
mockConductorService = jasmine.createSpyObj(
'conductorService',
['getConductor']
);
mockCompile = jasmine.createSpy('$compile');
testViews = [ { someKey: "some value" } ];
mockScope = jasmine.createSpyObj('scope', SCOPE_METHODS);
mockElement = jasmine.createSpyObj('element', ELEMENT_METHODS);
mockConductor = jasmine.createSpyObj(
'conductor',
[ 'queryStart', 'queryEnd', 'displayStart', 'displayEnd' ]
);
mockCompiledTemplate = jasmine.createSpy('template');
mockNewScope = jasmine.createSpyObj('newScope', SCOPE_METHODS);
mockNewElement = jasmine.createSpyObj('newElement', ELEMENT_METHODS);
mockNewElement[0] = mockNewElement;
mockConductorService.getConductor.andReturn(mockConductor);
mockCompile.andReturn(mockCompiledTemplate);
mockCompiledTemplate.andReturn(mockNewElement);
mockScope.$new.andReturn(mockNewScope);
representer = new ConductorRepresenter(
mockConductorService,
mockCompile,
testViews,
mockScope,
mockElement
);
});
afterEach(function () {
representer.destroy();
});
it("adds a conductor to views", function () {
representer.represent(testViews[0], {});
expect(mockElement.after).toHaveBeenCalledWith(mockNewElement);
});
it("adds nothing to non-view representations", function () {
representer.represent({ someKey: "something else" }, {});
expect(mockElement.after).not.toHaveBeenCalled();
});
it("removes the conductor when destroyed", function () {
representer.represent(testViews[0], {});
expect(mockNewElement.remove).not.toHaveBeenCalled();
representer.destroy();
expect(mockNewElement.remove).toHaveBeenCalled();
});
it("destroys any new scope created", function () {
representer.represent(testViews[0], {});
representer.destroy();
expect(mockNewScope.$destroy.calls.length)
.toEqual(mockScope.$new.calls.length);
});
it("exposes conductor state in scope", function () {
mockConductor.queryStart.andReturn(42);
mockConductor.queryEnd.andReturn(12321);
mockConductor.displayStart.andReturn(1977);
mockConductor.displayEnd.andReturn(1984);
representer.represent(testViews[0], {});
expect(mockNewScope.conductor).toEqual({
inner: { start: 1977, end: 1984 },
outer: { start: 42, end: 12321 }
});
});
it("updates conductor state from scope", function () {
var testState = {
inner: { start: 42, end: 1984 },
outer: { start: -1977, end: 12321 }
};
representer.represent(testViews[0], {});
mockNewScope.conductor = testState;
fireWatch(mockNewScope, 'conductor.inner.start', testState.inner.start);
expect(mockConductor.displayStart).toHaveBeenCalledWith(42);
fireWatch(mockNewScope, 'conductor.inner.end', testState.inner.end);
expect(mockConductor.displayEnd).toHaveBeenCalledWith(1984);
fireWatch(mockNewScope, 'conductor.outer.start', testState.outer.start);
expect(mockConductor.queryStart).toHaveBeenCalledWith(-1977);
fireWatch(mockNewScope, 'conductor.outer.end', testState.outer.end);
expect(mockConductor.queryEnd).toHaveBeenCalledWith(12321);
});
});
}
);

View File

@@ -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*/
/**
* EventSpec. Created by vwoeltje on 11/6/14. Modified by shale on 06/23/2015.
*/
define(
["../src/ConductorService"],
function (ConductorService) {
"use strict";
var TEST_NOW = 1020304050;
describe("ConductorService", function () {
var mockNow,
conductorService;
beforeEach(function () {
mockNow = jasmine.createSpy('now');
mockNow.andReturn(TEST_NOW);
conductorService = new ConductorService(mockNow);
});
it("initializes a time conductor around the current time", function () {
var conductor = conductorService.getConductor();
expect(conductor.queryStart() <= TEST_NOW).toBeTruthy();
expect(conductor.queryEnd() >= TEST_NOW).toBeTruthy();
expect(conductor.queryEnd() > conductor.queryStart())
.toBeTruthy();
});
it("provides a single shared time conductor instance", function () {
expect(conductorService.getConductor())
.toBe(conductorService.getConductor());
});
});
}
);

View File

@@ -0,0 +1,137 @@
/*****************************************************************************
* 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*/
define(
["../src/ConductorTelemetryDecorator"],
function (ConductorTelemetryDecorator) {
"use strict";
describe("ConductorTelemetryDecorator", function () {
var mockTelemetryService,
mockConductorService,
mockConductor,
mockPromise,
mockSeries,
decorator;
function seriesIsInWindow(series) {
var i, v, inWindow = true;
for (i = 0; i < series.getPointCount(); i += 1) {
v = series.getDomainValue(i);
inWindow = inWindow && (v >= mockConductor.displayStart());
inWindow = inWindow && (v <= mockConductor.displayEnd());
}
return inWindow;
}
beforeEach(function () {
mockTelemetryService = jasmine.createSpyObj(
'telemetryService',
[ 'requestTelemetry', 'subscribe' ]
);
mockConductorService = jasmine.createSpyObj(
'conductorService',
['getConductor']
);
mockConductor = jasmine.createSpyObj(
'conductor',
[ 'queryStart', 'queryEnd', 'displayStart', 'displayEnd' ]
);
mockPromise = jasmine.createSpyObj(
'promise',
['then']
);
mockSeries = jasmine.createSpyObj(
'series',
[ 'getPointCount', 'getDomainValue', 'getRangeValue' ]
);
mockTelemetryService.requestTelemetry.andReturn(mockPromise);
mockConductorService.getConductor.andReturn(mockConductor);
// Prepare test series; make sure it has a broad range of
// domain values, with at least some in the query-able range
mockSeries.getPointCount.andReturn(1000);
mockSeries.getDomainValue.andCallFake(function (i) {
var j = i - 500;
return j * j * j;
});
mockConductor.queryStart.andReturn(-12321);
mockConductor.queryEnd.andReturn(-12321);
mockConductor.displayStart.andReturn(42);
mockConductor.displayEnd.andReturn(1977);
decorator = new ConductorTelemetryDecorator(
mockConductorService,
mockTelemetryService
);
});
it("adds display start/end times to historical requests", function () {
decorator.requestTelemetry([{ someKey: "some value" }]);
expect(mockTelemetryService.requestTelemetry)
.toHaveBeenCalledWith([{
someKey: "some value",
start: mockConductor.displayStart(),
end: mockConductor.displayEnd()
}]);
});
it("adds display start/end times to subscription requests", function () {
var mockCallback = jasmine.createSpy('callback');
decorator.subscribe(mockCallback, [{ someKey: "some value" }]);
expect(mockTelemetryService.subscribe)
.toHaveBeenCalledWith(jasmine.any(Function), [{
someKey: "some value",
start: mockConductor.displayStart(),
end: mockConductor.displayEnd()
}]);
});
it("prunes historical values to the displayable range", function () {
var packagedTelemetry;
decorator.requestTelemetry([{ source: "abc", key: "xyz" }]);
packagedTelemetry = mockPromise.then.mostRecentCall.args[0]({
"abc": { "xyz": mockSeries }
});
expect(seriesIsInWindow(packagedTelemetry.abc.xyz))
.toBeTruthy();
});
it("prunes subscribed values to the displayable range", function () {
var mockCallback = jasmine.createSpy('callback'),
packagedTelemetry;
decorator.subscribe(mockCallback, [{ source: "abc", key: "xyz" }]);
mockTelemetryService.subscribe.mostRecentCall.args[0]({
"abc": { "xyz": mockSeries }
});
packagedTelemetry = mockCallback.mostRecentCall.args[0];
expect(seriesIsInWindow(packagedTelemetry.abc.xyz))
.toBeTruthy();
});
});
}
);

View File

@@ -0,0 +1,86 @@
/*****************************************************************************
* 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*/
define(
["../src/ConductorTelemetrySeries"],
function (ConductorTelemetrySeries) {
"use strict";
describe("ConductorTelemetrySeries", function () {
var mockSeries,
mockConductor,
testArray,
series;
beforeEach(function () {
testArray = [ -10, 0, 42, 1977, 12321 ];
mockSeries = jasmine.createSpyObj(
'series',
[ 'getPointCount', 'getDomainValue', 'getRangeValue' ]
);
mockConductor = jasmine.createSpyObj(
'conductor',
[ 'queryStart', 'queryEnd', 'displayStart', 'displayEnd' ]
);
mockSeries.getPointCount.andCallFake(function () {
return testArray.length;
});
mockSeries.getDomainValue.andCallFake(function (i) {
return testArray[i];
});
mockSeries.getRangeValue.andCallFake(function (i) {
return testArray[i] * 2;
});
mockConductor.displayStart.andReturn(0);
mockConductor.displayEnd.andReturn(2000);
series = new ConductorTelemetrySeries(
mockSeries,
mockConductor
);
});
it("reduces the apparent size of a series", function () {
expect(series.getPointCount()).toEqual(3);
});
it("maps domain value indexes to the displayable range", function () {
[0, 1, 2].forEach(function (i) {
expect(series.getDomainValue(i))
.toEqual(mockSeries.getDomainValue(i + 1));
});
});
it("maps range value indexes to the displayable range", function () {
[0, 1, 2].forEach(function (i) {
expect(series.getRangeValue(i))
.toEqual(mockSeries.getRangeValue(i + 1));
});
});
});
}
);

View File

@@ -0,0 +1,63 @@
/*****************************************************************************
* 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*/
/**
* EventSpec. Created by vwoeltje on 11/6/14. Modified by shale on 06/23/2015.
*/
define(
["../src/TimeConductor"],
function (TimeConductor) {
"use strict";
describe("TimeConductor", function () {
var testStart,
testEnd,
conductor;
beforeEach(function () {
testStart = 42;
testEnd = 12321;
conductor = new TimeConductor(testStart, testEnd);
});
it("provides accessors for query/display start/end times", function () {
expect(conductor.queryStart()).toEqual(testStart);
expect(conductor.queryEnd()).toEqual(testEnd);
expect(conductor.displayStart()).toEqual(testStart);
expect(conductor.displayEnd()).toEqual(testEnd);
});
it("provides setters for query/display start/end times", function () {
expect(conductor.queryStart(1)).toEqual(1);
expect(conductor.queryEnd(2)).toEqual(2);
expect(conductor.displayStart(3)).toEqual(3);
expect(conductor.displayEnd(4)).toEqual(4);
expect(conductor.queryStart()).toEqual(1);
expect(conductor.queryEnd()).toEqual(2);
expect(conductor.displayStart()).toEqual(3);
expect(conductor.displayEnd()).toEqual(4);
});
});
}
);

View File

@@ -0,0 +1,7 @@
[
"ConductorRepresenter",
"ConductorService",
"ConductorTelemetryDecorator",
"ConductorTelemetrySeries",
"TimeConductor"
]

View File

@@ -167,8 +167,9 @@
"$scope",
"$q",
"dialogService",
"telemetrySubscriber",
"telemetryFormatter"
"telemetryHandler",
"telemetryFormatter",
"throttle"
]
}
],

View File

@@ -38,12 +38,13 @@ define(
* @constructor
* @param {Scope} $scope the controller's Angular scope
*/
function FixedController($scope, $q, dialogService, telemetrySubscriber, telemetryFormatter) {
function FixedController($scope, $q, dialogService, telemetryHandler, telemetryFormatter, throttle) {
var self = this,
subscription,
handle,
names = {}, // Cache names by ID
values = {}, // Cache values by ID
elementProxiesById = {};
elementProxiesById = {},
maxDomainValue = Number.POSITIVE_INFINITY;
// Convert from element x/y/width/height to an
// appropriate ng-style argument, to position elements.
@@ -81,25 +82,50 @@ define(
return element.handles().map(generateDragHandle);
}
// Update the displayed value for this object
function updateValue(telemetryObject) {
var id = telemetryObject && telemetryObject.getId(),
// Update the value displayed in elements of this telemetry object
function setDisplayedValue(telemetryObject, value, alarm) {
var id = telemetryObject.getId();
(elementProxiesById[id] || []).forEach(function (element) {
names[id] = telemetryObject.getModel().name;
values[id] = telemetryFormatter.formatRangeValue(value);
element.name = names[id];
element.value = values[id];
element.cssClass = alarm && alarm.cssClass;
});
}
// Update the displayed value for this object, from a specific
// telemetry series
function updateValueFromSeries(telemetryObject, telemetrySeries) {
var index = telemetrySeries.getPointCount() - 1,
limit = telemetryObject &&
telemetryObject.getCapability('limit'),
datum = telemetryObject &&
subscription.getDatum(telemetryObject),
alarm = limit && datum && limit.evaluate(datum);
datum = telemetryObject && handle.getDatum(
telemetryObject,
index
);
if (id) {
(elementProxiesById[id] || []).forEach(function (element) {
names[id] = telemetryObject.getModel().name;
values[id] = telemetryFormatter.formatRangeValue(
subscription.getRangeValue(telemetryObject)
);
element.name = names[id];
element.value = values[id];
element.cssClass = alarm && alarm.cssClass;
});
setDisplayedValue(
telemetryObject,
telemetrySeries.getRangeValue(index),
limit && datum && limit.evaluate(datum)
);
}
// Update the displayed value for this object
function updateValue(telemetryObject) {
var limit = telemetryObject &&
telemetryObject.getCapability('limit'),
datum = telemetryObject &&
handle.getDatum(telemetryObject);
if (telemetryObject &&
(handle.getDomainValue(telemetryObject) < maxDomainValue)) {
setDisplayedValue(
telemetryObject,
handle.getRangeValue(telemetryObject),
limit && datum && limit.evaluate(datum)
);
}
}
@@ -115,8 +141,8 @@ define(
// Update telemetry values based on new data available
function updateValues() {
if (subscription) {
subscription.getTelemetryObjects().forEach(updateValue);
if (handle) {
handle.getTelemetryObjects().forEach(updateValue);
}
}
@@ -178,22 +204,29 @@ define(
// Free up subscription to telemetry
function releaseSubscription() {
if (subscription) {
subscription.unsubscribe();
subscription = undefined;
if (handle) {
handle.unsubscribe();
handle = undefined;
}
}
// Subscribe to telemetry updates for this domain object
function subscribe(domainObject) {
// Release existing subscription (if any)
if (subscription) {
subscription.unsubscribe();
if (handle) {
handle.unsubscribe();
}
// Make a new subscription
subscription = domainObject &&
telemetrySubscriber.subscribe(domainObject, updateValues);
handle = domainObject && telemetryHandler.handle(
domainObject,
updateValues
);
// Request an initial historical telemetry value
handle.request(
{ size: 1 }, // Only need a single data point
updateValueFromSeries
);
}
// Handle changes in the object's composition
@@ -204,6 +237,17 @@ define(
subscribe($scope.domainObject);
}
// Trigger a new query for telemetry data
function updateDisplayBounds(event, bounds) {
maxDomainValue = bounds.end;
if (handle) {
handle.request(
{ size: 1 }, // Only need a single data point
updateValueFromSeries
);
}
}
// Add an element to this view
function addElement(element) {
// Ensure that configuration field is populated
@@ -278,6 +322,9 @@ define(
// Position panes where they are dropped
$scope.$on("mctDrop", handleDrop);
// Respond to external bounds changes
$scope.$on("telemetry:display:bounds", updateDisplayBounds);
}
/**

View File

@@ -30,10 +30,10 @@ define(
var mockScope,
mockQ,
mockDialogService,
mockSubscriber,
mockHandler,
mockFormatter,
mockDomainObject,
mockSubscription,
mockHandle,
mockEvent,
testGrid,
testModel,
@@ -78,9 +78,9 @@ define(
'$scope',
[ "$on", "$watch", "commit" ]
);
mockSubscriber = jasmine.createSpyObj(
'telemetrySubscriber',
[ 'subscribe' ]
mockHandler = jasmine.createSpyObj(
'telemetryHandler',
[ 'handle' ]
);
mockQ = jasmine.createSpyObj('$q', ['when']);
mockDialogService = jasmine.createSpyObj(
@@ -95,9 +95,16 @@ define(
'domainObject',
[ 'getId', 'getModel', 'getCapability' ]
);
mockSubscription = jasmine.createSpyObj(
mockHandle = jasmine.createSpyObj(
'subscription',
[ 'unsubscribe', 'getTelemetryObjects', 'getRangeValue', 'getDatum' ]
[
'unsubscribe',
'getDomainValue',
'getTelemetryObjects',
'getRangeValue',
'getDatum',
'request'
]
);
mockEvent = jasmine.createSpyObj(
'event',
@@ -116,13 +123,14 @@ define(
{ type: "fixed.telemetry", id: 'c', x: 1, y: 1 }
]};
mockSubscriber.subscribe.andReturn(mockSubscription);
mockSubscription.getTelemetryObjects.andReturn(
mockHandler.handle.andReturn(mockHandle);
mockHandle.getTelemetryObjects.andReturn(
testModel.composition.map(makeMockDomainObject)
);
mockSubscription.getRangeValue.andCallFake(function (o) {
mockHandle.getRangeValue.andCallFake(function (o) {
return testValues[o.getId()];
});
mockHandle.getDomainValue.andReturn(12321);
mockFormatter.formatRangeValue.andCallFake(function (v) {
return "Formatted " + v;
});
@@ -137,7 +145,7 @@ define(
mockScope,
mockQ,
mockDialogService,
mockSubscriber,
mockHandler,
mockFormatter
);
});
@@ -145,7 +153,7 @@ define(
it("subscribes when a domain object is available", function () {
mockScope.domainObject = mockDomainObject;
findWatch("domainObject")(mockDomainObject);
expect(mockSubscriber.subscribe).toHaveBeenCalledWith(
expect(mockHandler.handle).toHaveBeenCalledWith(
mockDomainObject,
jasmine.any(Function)
);
@@ -156,13 +164,13 @@ define(
// First pass - should simply should subscribe
findWatch("domainObject")(mockDomainObject);
expect(mockSubscription.unsubscribe).not.toHaveBeenCalled();
expect(mockSubscriber.subscribe.calls.length).toEqual(1);
expect(mockHandle.unsubscribe).not.toHaveBeenCalled();
expect(mockHandler.handle.calls.length).toEqual(1);
// Object changes - should unsubscribe then resubscribe
findWatch("domainObject")(mockDomainObject);
expect(mockSubscription.unsubscribe).toHaveBeenCalled();
expect(mockSubscriber.subscribe.calls.length).toEqual(2);
expect(mockHandle.unsubscribe).toHaveBeenCalled();
expect(mockHandler.handle.calls.length).toEqual(2);
});
it("exposes visible elements based on configuration", function () {
@@ -255,7 +263,7 @@ define(
findWatch("model.composition")(mockScope.model.composition);
// Invoke the subscription callback
mockSubscriber.subscribe.mostRecentCall.args[1]();
mockHandler.handle.mostRecentCall.args[1]();
// Get elements that controller is now exposing
elements = controller.getElements();
@@ -333,11 +341,11 @@ define(
// Make an object available
findWatch('domainObject')(mockDomainObject);
// Also verify precondition
expect(mockSubscription.unsubscribe).not.toHaveBeenCalled();
expect(mockHandle.unsubscribe).not.toHaveBeenCalled();
// Destroy the scope
findOn('$destroy')();
// Should have unsubscribed
expect(mockSubscription.unsubscribe).toHaveBeenCalled();
expect(mockHandle.unsubscribe).toHaveBeenCalled();
});
it("exposes its grid size", function () {

View File

@@ -65,6 +65,8 @@ define(
subPlotFactory = new SubPlotFactory(telemetryFormatter),
cachedObjects = [],
updater,
lastBounds,
throttledRequery,
handle;
// Populate the scope with axis information (specifically, options
@@ -94,6 +96,17 @@ define(
}
}
// Change the displayable bounds
function setBasePanZoom(bounds) {
var start = bounds.start,
end = bounds.end;
if (updater) {
updater.setDomainBounds(start, end);
self.update();
}
lastBounds = bounds;
}
// Reinstantiate the plot updater (e.g. because we have a
// new subscription.) This will clear the plot.
function recreateUpdater() {
@@ -107,10 +120,15 @@ define(
handle,
($scope.axes[1].active || {}).key
);
// Keep any externally-provided bounds
if (lastBounds) {
setBasePanZoom(lastBounds);
}
}
// Handle new telemetry data in this plot
function updateValues() {
self.pending = false;
if (handle) {
setupModes(handle.getTelemetryObjects());
}
@@ -126,6 +144,7 @@ define(
// Display new historical data as it becomes available
function addHistoricalData(domainObject, series) {
self.pending = false;
updater.addHistorical(domainObject, series);
self.modeOptions.getModeHandler().plotTelemetry(updater);
self.update();
@@ -165,6 +184,19 @@ define(
}
}
// Respond to a display bounds change (requery for data)
function changeDisplayBounds(event, bounds) {
self.pending = true;
releaseSubscription();
throttledRequery();
setBasePanZoom(bounds);
}
// Reestablish/reissue request for telemetry
throttledRequery = throttle(function () {
subscribe($scope.domainObject);
}, 250);
this.modeOptions = new PlotModeOptions([], subPlotFactory);
this.updateValues = updateValues;
@@ -174,12 +206,19 @@ define(
.forEach(updateSubplot);
});
self.pending = true;
// Subscribe to telemetry when a domain object becomes available
$scope.$watch('domainObject', subscribe);
// Respond to external bounds changes
$scope.$on("telemetry:display:bounds", changeDisplayBounds);
// Unsubscribe when the plot is destroyed
$scope.$on("$destroy", releaseSubscription);
// Notify any external observers that a new telemetry view is here
$scope.$emit("telemetry:view");
}
/**
@@ -275,7 +314,7 @@ define(
PlotController.prototype.isRequestPending = function () {
// Placeholder; this should reflect request state
// when requesting historical telemetry
return false;
return this.pending;
};
return PlotController;

View File

@@ -143,8 +143,7 @@ define(
PlotPanZoomStackGroup.prototype.getDepth = function () {
// All stacks are kept in sync, so look up depth
// from the first one.
return this.stacks.length > 0 ?
this.stacks[0].getDepth() : 0;
return this.stacks.length > 0 ? this.stacks[0].getDepth() : 0;
};
/**

View File

@@ -141,10 +141,10 @@ define(
PlotUpdater.prototype.initializeDomainOffset = function (values) {
this.domainOffset =
((this.domainOffset === undefined) && (values.length > 0)) ?
(values.reduce(function (a, b) {
return (a || 0) + (b || 0);
}, 0) / values.length) :
this.domainOffset;
(values.reduce(function (a, b) {
return (a || 0) + (b || 0);
}, 0) / values.length) :
this.domainOffset;
};
// Expand range slightly so points near edges are visible
@@ -159,7 +159,10 @@ define(
// Update dimensions and origin based on extrema of plots
PlotUpdater.prototype.updateBounds = function () {
var bufferArray = this.bufferArray;
var bufferArray = this.bufferArray,
priorDomainOrigin = this.origin[0],
priorDomainDimensions = this.dimensions[0];
if (bufferArray.length > 0) {
this.domainExtrema = bufferArray.map(function (lineBuffer) {
return lineBuffer.getDomainExtrema();
@@ -178,6 +181,18 @@ define(
// Enforce some minimum visible area
this.expandRange();
// Suppress domain changes when pinned
if (this.hasSpecificDomainBounds) {
this.origin[0] = priorDomainOrigin;
this.dimensions[0] = priorDomainDimensions;
if (this.following) {
this.origin[0] = Math.max(
this.domainExtrema[1] - this.dimensions[0],
this.origin[0]
);
}
}
// ...then enforce a fixed duration if needed
if (this.fixedDuration !== undefined) {
this.origin[0] = this.origin[0] + this.dimensions[0] -
@@ -281,6 +296,21 @@ define(
return this.bufferArray;
};
/**
* Set the start and end boundaries (usually time) for the
* domain axis of this updater.
*/
PlotUpdater.prototype.setDomainBounds = function (start, end) {
this.fixedDuration = end - start;
this.origin[0] = start;
this.dimensions[0] = this.fixedDuration;
// Suppress follow behavior if we have windowed in on the past
this.hasSpecificDomainBounds = true;
this.following =
!this.domainExtrema || (end >= this.domainExtrema[1]);
};
/**
* Fill in historical data.
*/

View File

@@ -45,11 +45,19 @@ define(
};
}
function fireEvent(name, args) {
mockScope.$on.calls.forEach(function (call) {
if (call.args[0] === name) {
call.args[1].apply(null, args || []);
}
});
}
beforeEach(function () {
mockScope = jasmine.createSpyObj(
"$scope",
[ "$watch", "$on" ]
[ "$watch", "$on", "$emit" ]
);
mockFormatter = jasmine.createSpyObj(
"formatter",
@@ -87,6 +95,7 @@ define(
mockHandle.getMetadata.andReturn([{}]);
mockHandle.getDomainValue.andReturn(123);
mockHandle.getRangeValue.andReturn(42);
mockScope.domainObject = mockDomainObject;
controller = new PlotController(
mockScope,
@@ -212,7 +221,12 @@ define(
});
it("indicates if a request is pending", function () {
// Placeholder; need to support requesting telemetry
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(controller.isRequestPending()).toBeTruthy();
mockHandle.request.mostRecentCall.args[1](
mockDomainObject,
mockSeries
);
expect(controller.isRequestPending()).toBeFalsy();
});
@@ -233,10 +247,20 @@ define(
// Also verify precondition
expect(mockHandle.unsubscribe).not.toHaveBeenCalled();
// Destroy the scope
mockScope.$on.mostRecentCall.args[1]();
fireEvent("$destroy");
// Should have unsubscribed
expect(mockHandle.unsubscribe).toHaveBeenCalled();
});
it("requeries when displayable bounds change", function () {
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(mockHandle.request.calls.length).toEqual(1);
fireEvent("telemetry:display:bounds", [
{},
{ start: 10, end: 100 }
]);
expect(mockHandle.request.calls.length).toEqual(2);
});
});
}
);

View File

@@ -55,7 +55,7 @@ define(
var range = this.rangeMetadata.key,
limit = domainObject.getCapability('limit'),
value = datum[range],
alarm = limit.evaluate(datum, range);
alarm = limit && limit.evaluate(datum, range);
return {
cssClass: alarm && alarm.cssClass,