Merge remote-tracking branch 'github/master' into open1239

This commit is contained in:
Victor Woeltjen
2015-10-23 13:39:09 -07:00
261 changed files with 28501 additions and 10671 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,46 @@
{
"extensions": {
"representers": [
{
"implementation": "ConductorRepresenter.js",
"depends": [
"throttle",
"conductorService",
"$compile",
"views[]"
]
}
],
"components": [
{
"type": "decorator",
"provides": "telemetryService",
"implementation": "ConductorTelemetryDecorator.js",
"depends": [ "conductorService" ]
}
],
"services": [
{
"key": "conductorService",
"implementation": "ConductorService.js",
"depends": [ "now", "TIME_CONDUCTOR_DOMAINS" ]
}
],
"templates": [
{
"key": "time-conductor",
"templateUrl": "templates/time-conductor.html"
}
],
"constants": [
{
"key": "TIME_CONDUCTOR_DOMAINS",
"value": [
{ "key": "time", "name": "Time" },
{ "key": "yesterday", "name": "Yesterday" }
],
"comment": "Placeholder; to be replaced by inspection of available domains."
}
]
}
}

View File

@@ -0,0 +1,10 @@
<mct-include key="'time-controller'"
ng-model='ngModel.conductor'>
</mct-include>
<mct-control key="'select'"
ng-model='ngModel'
field="'domain'"
options="ngModel.options"
style="position: absolute; right: 0px; bottom: 46px;"
>
</mct-control>

View File

@@ -0,0 +1,201 @@
/*****************************************************************************
* 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 TEMPLATE = [
"<mct-include key=\"'time-conductor'\" ng-model='ngModel' class='l-time-controller'>",
"</mct-include>"
].join(''),
THROTTLE_MS = 200,
GLOBAL_SHOWING = false;
/**
* The ConductorRepresenter attaches the universal time conductor
* to views.
*
* @implements {Representer}
* @constructor
* @memberof platform/features/conductor
* @param {Function} throttle a function used to reduce the frequency
* of function invocations
* @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(
throttle,
conductorService,
$compile,
views,
scope,
element
) {
this.throttle = throttle;
this.scope = scope;
this.conductorService = conductorService;
this.element = element;
this.views = views;
this.$compile = $compile;
}
// Update the time conductor from the scope
ConductorRepresenter.prototype.wireScope = function () {
var conductor = this.conductorService.getConductor(),
conductorScope = this.conductorScope(),
repScope = this.scope,
lastObservedBounds,
broadcastBounds;
// Combine start/end times into a single object
function bounds(start, end) {
return {
start: conductor.displayStart(),
end: conductor.displayEnd(),
domain: conductor.domain()
};
}
function boundsAreStable(newlyObservedBounds) {
return !lastObservedBounds ||
(lastObservedBounds.start === newlyObservedBounds.start &&
lastObservedBounds.end === newlyObservedBounds.end);
}
function updateConductorInner() {
var innerBounds = conductorScope.ngModel.conductor.inner;
conductor.displayStart(innerBounds.start);
conductor.displayEnd(innerBounds.end);
lastObservedBounds = lastObservedBounds || bounds();
broadcastBounds();
}
function updateDomain(value) {
conductor.domain(value);
repScope.$broadcast('telemetry:display:bounds', bounds(
conductor.displayStart(),
conductor.displayEnd(),
conductor.domain()
));
}
// telemetry domain metadata -> option for a select control
function makeOption(domainOption) {
return {
name: domainOption.name,
value: domainOption.key
};
}
broadcastBounds = this.throttle(function () {
var newlyObservedBounds = bounds();
if (boundsAreStable(newlyObservedBounds)) {
repScope.$broadcast('telemetry:display:bounds', bounds());
lastObservedBounds = undefined;
} else {
lastObservedBounds = newlyObservedBounds;
broadcastBounds();
}
}, THROTTLE_MS);
conductorScope.ngModel = {};
conductorScope.ngModel.conductor =
{ outer: bounds(), inner: bounds() };
conductorScope.ngModel.options =
conductor.domainOptions().map(makeOption);
conductorScope.ngModel.domain = conductor.domain();
conductorScope
.$watch('ngModel.conductor.inner.start', updateConductorInner);
conductorScope
.$watch('ngModel.conductor.inner.end', updateConductorInner);
conductorScope
.$watch('ngModel.domain', updateDomain);
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());
this.wireScope();
this.conductorElement =
this.$compile(TEMPLATE)(this.conductorScope());
this.element.after(this.conductorElement[0]);
this.element.addClass('l-controls-visible l-time-controller-visible');
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,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*/
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, domains) {
var initialEnd =
Math.ceil(now() / SIX_HOURS_IN_MS) * SIX_HOURS_IN_MS;
this.conductor = new TimeConductor(
initialEnd - ONE_DAY_IN_MS,
initialEnd,
domains
);
}
/**
* 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,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*/
define(
function () {
'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;
}
ConductorTelemetryDecorator.prototype.amendRequests = function (requests) {
var conductor = this.conductorService.getConductor(),
start = conductor.displayStart(),
end = conductor.displayEnd(),
domain = conductor.domain();
function amendRequest(request) {
request = request || {};
request.start = start;
request.end = end;
request.domain = domain;
return request;
}
return (requests || []).map(amendRequest);
};
ConductorTelemetryDecorator.prototype.requestTelemetry = function (requests) {
var self = this;
return this.telemetryService
.requestTelemetry(this.amendRequests(requests));
};
ConductorTelemetryDecorator.prototype.subscribe = function (callback, requests) {
var self = this;
return this.telemetryService
.subscribe(callback, this.amendRequests(requests));
};
return ConductorTelemetryDecorator;
}
);

View File

@@ -0,0 +1,103 @@
/*****************************************************************************
* 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, domains) {
this.range = { start: start, end: end };
this.domains = domains;
this.activeDomain = domains[0].key;
}
/**
* 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.range.start = value;
}
return this.range.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.range.end = value;
}
return this.range.end;
};
/**
* Get available domain options which can be used to bound time
* selection.
* @returns {TelemetryDomain[]} available domains
*/
TimeConductor.prototype.domainOptions = function () {
return this.domains;
};
/**
* Get or set (if called with an argument) the active domain.
* @param {string} [key] the key identifying the domain choice
* @returns {TelemetryDomain} the active telemetry domain
*/
TimeConductor.prototype.domain = function (key) {
function matchesKey(domain) {
return domain.key === key;
}
if (arguments.length > 0) {
if (!this.domains.some(matchesKey)) {
throw new Error("Unknown domain " + key);
}
this.activeDomain = key;
}
return this.activeDomain;
};
return TimeConductor;
}
);

View File

@@ -0,0 +1,259 @@
/*****************************************************************************
* 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*/
define(
["../src/ConductorRepresenter", "./TestTimeConductor"],
function (ConductorRepresenter, TestTimeConductor) {
"use strict";
var SCOPE_METHODS = [
'$on',
'$watch',
'$broadcast',
'$emit',
'$new',
'$destroy'
],
ELEMENT_METHODS = [
'hasClass',
'addClass',
'removeClass',
'css',
'after',
'remove'
];
describe("ConductorRepresenter", function () {
var mockThrottle,
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 () {
mockThrottle = jasmine.createSpy('throttle');
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 = new TestTimeConductor();
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);
mockThrottle.andCallFake(function (fn) {
return fn;
});
representer = new ConductorRepresenter(
mockThrottle,
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.displayStart.andReturn(1977);
mockConductor.displayEnd.andReturn(1984);
mockConductor.domain.andReturn('d');
representer.represent(testViews[0], {});
expect(mockNewScope.ngModel.conductor).toEqual({
inner: { start: 1977, end: 1984, domain: 'd' },
outer: { start: 1977, end: 1984, domain: 'd' }
});
});
it("updates conductor state from scope", function () {
var testState = {
inner: { start: 42, end: 1984 },
outer: { start: -1977, end: 12321 }
};
representer.represent(testViews[0], {});
mockNewScope.ngModel.conductor = testState;
fireWatch(
mockNewScope,
'ngModel.conductor.inner.start',
testState.inner.start
);
expect(mockConductor.displayStart).toHaveBeenCalledWith(42);
fireWatch(
mockNewScope,
'ngModel.conductor.inner.end',
testState.inner.end
);
expect(mockConductor.displayEnd).toHaveBeenCalledWith(1984);
});
describe("when bounds are changing", function () {
var startWatch = "ngModel.conductor.inner.start",
endWatch = "ngModel.conductor.inner.end",
mockThrottledFn = jasmine.createSpy('throttledFn'),
testBounds;
function fireThrottledFn() {
mockThrottle.mostRecentCall.args[0]();
}
beforeEach(function () {
mockThrottle.andReturn(mockThrottledFn);
representer.represent(testViews[0], {});
testBounds = { start: 0, end: 1000 };
mockNewScope.ngModel.conductor.inner = testBounds;
mockConductor.displayStart.andCallFake(function () {
return testBounds.start;
});
mockConductor.displayEnd.andCallFake(function () {
return testBounds.end;
});
});
it("does not broadcast while bounds are changing", function () {
expect(mockScope.$broadcast).not.toHaveBeenCalled();
testBounds.start = 100;
fireWatch(mockNewScope, startWatch, testBounds.start);
testBounds.end = 500;
fireWatch(mockNewScope, endWatch, testBounds.end);
fireThrottledFn();
testBounds.start = 200;
fireWatch(mockNewScope, startWatch, testBounds.start);
testBounds.end = 400;
fireWatch(mockNewScope, endWatch, testBounds.end);
fireThrottledFn();
expect(mockScope.$broadcast).not.toHaveBeenCalled();
});
it("does broadcast when bounds have stabilized", function () {
expect(mockScope.$broadcast).not.toHaveBeenCalled();
testBounds.start = 100;
fireWatch(mockNewScope, startWatch, testBounds.start);
testBounds.end = 500;
fireWatch(mockNewScope, endWatch, testBounds.end);
fireThrottledFn();
fireWatch(mockNewScope, startWatch, testBounds.start);
fireWatch(mockNewScope, endWatch, testBounds.end);
fireThrottledFn();
expect(mockScope.$broadcast).toHaveBeenCalled();
});
});
it("exposes domain selection in scope", function () {
representer.represent(testViews[0], null);
expect(mockNewScope.ngModel.domain)
.toEqual(mockConductor.domain());
});
it("exposes domain options in scope", function () {
representer.represent(testViews[0], null);
mockConductor.domainOptions().forEach(function (option, i) {
expect(mockNewScope.ngModel.options[i].value)
.toEqual(option.key);
expect(mockNewScope.ngModel.options[i].name)
.toEqual(option.name);
});
});
it("updates domain selection from scope", function () {
var choice;
representer.represent(testViews[0], null);
// Choose a domain that isn't currently selected
mockNewScope.ngModel.options.forEach(function (option) {
if (option.value !== mockNewScope.ngModel.domain) {
choice = option.value;
}
});
expect(mockConductor.domain)
.not.toHaveBeenCalledWith(choice);
mockNewScope.ngModel.domain = choice;
fireWatch(mockNewScope, "ngModel.domain", choice);
expect(mockConductor.domain)
.toHaveBeenCalledWith(choice);
});
});
}
);

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*/
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, [
{ key: "d1", name: "Domain #1" },
{ key: "d2", name: "Domain #2" }
]);
});
it("initializes a time conductor around the current time", function () {
var conductor = conductorService.getConductor();
expect(conductor.displayStart() <= TEST_NOW).toBeTruthy();
expect(conductor.displayEnd() >= TEST_NOW).toBeTruthy();
expect(conductor.displayEnd() > conductor.displayStart())
.toBeTruthy();
});
it("provides a single shared time conductor instance", function () {
expect(conductorService.getConductor())
.toBe(conductorService.getConductor());
});
});
}
);

View File

@@ -0,0 +1,160 @@
/*****************************************************************************
* 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", "./TestTimeConductor"],
function (ConductorTelemetryDecorator, TestTimeConductor) {
"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 = new TestTimeConductor();
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.displayStart.andReturn(42);
mockConductor.displayEnd.andReturn(1977);
mockConductor.domain.andReturn("testDomain");
decorator = new ConductorTelemetryDecorator(
mockConductorService,
mockTelemetryService
);
});
describe("decorates historical requests", function () {
var request;
beforeEach(function () {
decorator.requestTelemetry([{ someKey: "some value" }]);
request = mockTelemetryService.requestTelemetry
.mostRecentCall.args[0][0];
});
it("with start times", function () {
expect(request.start).toEqual(mockConductor.displayStart());
});
it("with end times", function () {
expect(request.end).toEqual(mockConductor.displayEnd());
});
it("with domain selection", function () {
expect(request.domain).toEqual(mockConductor.domain());
});
});
describe("decorates subscription requests", function () {
var request;
beforeEach(function () {
var mockCallback = jasmine.createSpy('callback');
decorator.subscribe(mockCallback, [{ someKey: "some value" }]);
request = mockTelemetryService.subscribe
.mostRecentCall.args[1][0];
});
it("with start times", function () {
expect(request.start).toEqual(mockConductor.displayStart());
});
it("with end times", function () {
expect(request.end).toEqual(mockConductor.displayEnd());
});
it("with domain selection", function () {
expect(request.domain).toEqual(mockConductor.domain());
});
});
it("adds display start/end times & domain selection to historical requests", function () {
decorator.requestTelemetry([{ someKey: "some value" }]);
expect(mockTelemetryService.requestTelemetry)
.toHaveBeenCalledWith([{
someKey: "some value",
start: mockConductor.displayStart(),
end: mockConductor.displayEnd(),
domain: jasmine.any(String)
}]);
});
it("adds display start/end times & domain selection 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(),
domain: jasmine.any(String)
}]);
});
});
}
);

View File

@@ -0,0 +1,50 @@
/*****************************************************************************
* 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,spyOn*/
define(
["../src/TimeConductor"],
function (TimeConductor) {
'use strict';
function TestTimeConductor() {
var self = this;
TimeConductor.apply(this, [
402514200000,
444546000000,
[
{ key: "domain0", name: "Domain #1" },
{ key: "domain1", name: "Domain #2" }
]
]);
Object.keys(TimeConductor.prototype).forEach(function (method) {
spyOn(self, method).andCallThrough();
});
}
TestTimeConductor.prototype = TimeConductor.prototype;
return TestTimeConductor;
}
);

View File

@@ -0,0 +1,78 @@
/*****************************************************************************
* 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/TimeConductor"],
function (TimeConductor) {
"use strict";
describe("TimeConductor", function () {
var testStart,
testEnd,
testDomains,
conductor;
beforeEach(function () {
testStart = 42;
testEnd = 12321;
testDomains = [
{ key: "d1", name: "Domain #1" },
{ key: "d2", name: "Domain #2" }
];
conductor = new TimeConductor(testStart, testEnd, testDomains);
});
it("provides accessors for query/display start/end times", function () {
expect(conductor.displayStart()).toEqual(testStart);
expect(conductor.displayEnd()).toEqual(testEnd);
});
it("provides setters for query/display start/end times", function () {
expect(conductor.displayStart(3)).toEqual(3);
expect(conductor.displayEnd(4)).toEqual(4);
expect(conductor.displayStart()).toEqual(3);
expect(conductor.displayEnd()).toEqual(4);
});
it("exposes domain options", function () {
expect(conductor.domainOptions()).toEqual(testDomains);
});
it("exposes the current domain choice", function () {
expect(conductor.domain()).toEqual(testDomains[0].key);
});
it("allows the domain choice to be changed", function () {
conductor.domain(testDomains[1].key);
expect(conductor.domain()).toEqual(testDomains[1].key);
});
it("throws an error on attempts to set an invalid domain", function () {
expect(function () {
conductor.domain("invalid-domain");
}).toThrow();
});
});
}
);

View File

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

View File

@@ -10,7 +10,7 @@
ng-show="false && showLocalControls"
>
<a
class="t-btn l-btn s-btn s-icon-btn s-very-subtle"
class="s-btn"
ng-click="plot.stepBackPanZoom()"
ng-show="1"
title="Restore previous pan/zoom">
@@ -18,7 +18,7 @@
</a>
<a
class="t-btn l-btn s-btn s-icon-btn s-very-subtle"
class="s-btn"
ng-click="plot.unzoom()"
ng-show="1"
title="Reset pan/zoom">
@@ -33,10 +33,10 @@
>
</div>
<div class="l-image-main-controlbar bar">
<div class="l-image-main-controlbar l-flex bar">
<div class="left">
<a
class="t-btn l-btn s-btn s-icon-btn s-very-subtle show-thumbs sm"
class="s-btn show-thumbs sm hidden"
ng-click="showThumbsBubble = (showThumbsBubble)? false:true"
><span class="ui-symbol icon"></span></a>
<span class="l-timezone">{{imagery.getZone()}}</span>
@@ -45,7 +45,7 @@
</div>
<div class="right">
<a
class="t-btn l-btn s-btn s-icon-btn s-very-subtle pause-play sm"
class="s-btn pause-play"
ng-click="imagery.paused(!imagery.paused())"
ng-class="{ paused: imagery.paused() }"
><span class="ui-symbol icon"></span></a>
@@ -53,10 +53,10 @@
ng-if="imagery.getImageUrl()"
target="_blank"
title="Open image in new tab."
class="t-btn l-btn s-btn s-icon-btn s-very-subtle sm">
class="s-btn">
<span class="ui-symbol icon">y</span></a>
<a href=""
class="l-btn s-btn s-icon-btn l-mag s-mag ui-symbol vsm"
class="s-btn l-mag s-mag ui-symbol vsm"
ng-click="clipped = false"
ng-show="clipped === true"
title="Not all of image is visible; click to reset."

View File

@@ -1,6 +1,6 @@
{
"name": "Layout components.",
"description": "Plug in adding Layout capabiltiies.",
"description": "Plug in adding Layout capabilities.",
"extensions": {
"views": [
{
@@ -85,7 +85,7 @@
},
{
"property": "fill",
"glyph": "\u00E0",
"glyph": "\ue606",
"control": "color"
},
{
@@ -167,8 +167,9 @@
"$scope",
"$q",
"dialogService",
"telemetrySubscriber",
"telemetryFormatter"
"telemetryHandler",
"telemetryFormatter",
"throttle"
]
}
],
@@ -258,16 +259,6 @@
"pattern": "^(\\d*[1-9]\\d*)?$",
"property": "layoutGrid",
"conversion": "number[]"
},
{
"name": "Default View",
"control": "select",
"options": [
{ "name": "Plot", "value": "plot" },
{ "name": "Scrolling", "value": "scrolling" }
],
"comment": "TODO: Infer values from type",
"key": "defaultView"
}
]
}

View File

@@ -20,13 +20,12 @@
at runtime from the About dialog for additional information.
-->
<div class="frame frame-template abs">
<div class="bar abs object-header object-top-bar">
<div class="title left abs">
<mct-representation key="'node'"
mct-object="domainObject">
<div class="bar abs l-flex object-header object-top-bar">
<div class="left">
<mct-representation key="'object-header'" mct-object="domainObject">
</mct-representation>
</div>
<div class="btn-bar right abs">
<div class="btn-bar right">
<mct-representation key="'switcher'"
ng-model="representation"
mct-object="domainObject">

View File

@@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div style="width: 100%; height: 100%;"
<div class="l-layout"
ng-controller="LayoutController as controller">
<div class='frame child-frame panel abs'

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,52 @@ 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;
});
if (index >= 0) {
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 +143,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 +206,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 +239,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
@@ -229,6 +275,7 @@ define(
// Position a panel after a drop event
function handleDrop(e, id, position) {
// Don't handle this event if it has already been handled
// color is set to "" to let the CSS theme determine the default color
if (e.defaultPrevented) {
return;
}
@@ -240,7 +287,7 @@ define(
y: Math.floor(position.y / self.gridSize[1]),
id: id,
stroke: "transparent",
color: "#cccccc",
color: "",
titled: true,
width: DEFAULT_DIMENSIONS[0],
height: DEFAULT_DIMENSIONS[1]
@@ -277,6 +324,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

@@ -44,8 +44,7 @@ define(
},
"fixed.text": {
fill: "transparent",
stroke: "transparent",
color: "#cccccc"
stroke: "transparent"
}
},
DIALOGS = {

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

@@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div class="abs l-iframe">
<div class="l-iframe abs">
<iframe ng-controller="EmbeddedPageController as ctl"
ng-src="{{ctl.trust(model.url)}}">
</iframe>

View File

@@ -105,23 +105,21 @@
ng-show="representation.showControls"
style="position: absolute; top: 8px; right: 8px;">
<a href=""
class="t-btn l-btn s-btn s-icon-btn s-very-subtle"
<a class="s-btn"
ng-click="plot.stepBackPanZoom()"
ng-show="plot.isZoomed()"
title="Restore previous pan/zoom">
<span class="ui-symbol icon">&lt;</span>
</a>
<a href=""
class="t-btn l-btn s-btn s-icon-btn s-very-subtle"
<a class="s-btn"
ng-click="plot.unzoom()"
ng-show="plot.isZoomed()"
title="Reset pan/zoom">
<span class="ui-symbol icon">I</span>
</a>
<div class="menu-element btn s-very-subtle btn-menu dropdown menus-to-left"
<div class="menu-element s-menu-btn menus-to-left"
ng-if="plot.getModeOptions().length > 1"
ng-controller="ClickAwayController as toggle">
@@ -129,9 +127,8 @@
<span class="ui-symbol icon type-icon">{{plot.getMode().glyph}}</span>
<span>{{plot.getMode().name}}</span>
<span class='ui-symbol invoke-menu'>v</span>
<div class="menu dropdown" ng-show="toggle.isActive()">
<div class="menu" ng-show="toggle.isActive()">
<ul>
<li ng-repeat="option in plot.getModeOptions()">
<a href="" ng-click="plot.setMode(option); toggle.setState(false)">

View File

@@ -65,6 +65,7 @@ define(
subPlotFactory = new SubPlotFactory(telemetryFormatter),
cachedObjects = [],
updater,
lastBounds,
handle;
// Populate the scope with axis information (specifically, options
@@ -94,6 +95,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 +119,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 +143,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 +183,14 @@ define(
}
}
// Respond to a display bounds change (requery for data)
function changeDisplayBounds(event, bounds) {
self.pending = true;
releaseSubscription();
subscribe($scope.domainObject);
setBasePanZoom(bounds);
}
this.modeOptions = new PlotModeOptions([], subPlotFactory);
this.updateValues = updateValues;
@@ -174,12 +200,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 +308,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

@@ -64,6 +64,16 @@ define(
this.updateTicks();
}
/**
* Tests whether this subplot has domain data to show for the current pan/zoom level. Absence of domain data
* implies that there is no range data displayed either
* @returns {boolean} true if domain data exists for the current pan/zoom level
*/
SubPlot.prototype.hasDomainData = function() {
return this.panZoomStack
&& this.panZoomStack.getDimensions()[0] > 0;
};
// Utility function for filtering out empty strings.
function isNonEmpty(v) {
return typeof v === 'string' && v !== "";
@@ -253,7 +263,10 @@ define(
this.hovering = true;
this.subPlotBounds = $event.target.getBoundingClientRect();
this.mousePosition = this.toMousePosition($event);
this.updateHoverCoordinates();
//If there is a domain to display, show hover coordinates, otherwise hover coordinates are meaningless
if (this.hasDomainData()) {
this.updateHoverCoordinates();
}
if (this.marqueeStart) {
this.updateMarqueeBox();
}

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

@@ -53,7 +53,8 @@ define(
for (i = 0; i < count; i += 1) {
result.push({
label: format(i * step + start)
//If data to show, display label for each tick line, otherwise show lines but suppress labels.
label: span > 0 ? format(i * step + start) : ''
});
}

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

@@ -157,6 +157,15 @@ define(
);
});
it ("indicates when there is domain data shown", function () {
expect(subplot.hasDomainData()).toEqual(true);
});
it ("indicates when there is no domain data shown", function () {
mockPanZoomStack.getDimensions.andReturn([0,0]);
expect(subplot.hasDomainData()).toEqual(false);
});
it("disallows marquee zoom when start and end Marquee is at the same position", function () {
expect(mockPanZoomStack.pushPanZoom).not.toHaveBeenCalled();

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,