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

Merge latest from master branch to reconcile conflicts
for https://github.com/nasa/openmctweb/pull/469

Conflicts:
	platform/commonUI/general/bundle.json
This commit is contained in:
Victor Woeltjen
2016-01-08 15:36:10 -08:00
28 changed files with 2939 additions and 413 deletions

View File

@@ -37,7 +37,6 @@ define([
"./src/controllers/ViewSwitcherController",
"./src/controllers/BottomBarController",
"./src/controllers/GetterSetterController",
"./src/controllers/SplitPaneController",
"./src/controllers/SelectorController",
"./src/controllers/ObjectInspectorController",
"./src/controllers/BannerController",
@@ -66,7 +65,6 @@ define([
ViewSwitcherController,
BottomBarController,
GetterSetterController,
SplitPaneController,
SelectorController,
ObjectInspectorController,
BannerController,
@@ -241,10 +239,6 @@ define([
"$scope"
]
},
{
"key": "SplitPaneController",
"implementation": SplitPaneController
},
{
"key": "SelectorController",
"implementation": SelectorController,

View File

@@ -1,30 +0,0 @@
<!--
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.
-->
<span ng-controller="SplitPaneController as splitter">
<div class="splitter" ng-style="splitter.style()"
mct-drag="splitter.move(delta.x)">
</div>
<div class='split-pane-component items pane' style="right:0;"
ng-style="splitter.style()"
ng-transclude>
</div>
</span>

View File

@@ -1,89 +0,0 @@
/*****************************************************************************
* 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 DEFAULT_MAXIMUM = 1000,
DEFAULT_MINIMUM = 120;
/**
* Controller for the splitter in Browse mode. Current implementation
* uses many hard-coded constants; this could be generalized.
* @memberof platform/commonUI/general
* @constructor
*/
function SplitPaneController() {
this.current = 200;
this.start = 200;
this.assigned = false;
}
/**
* Get the current position of the splitter, in pixels
* from the left edge.
* @returns {number} position of the splitter, in pixels
*/
SplitPaneController.prototype.state = function (defaultState) {
// Set the state to the desired default, if we don't have a
// "real" current state yet.
if (arguments.length > 0 && !this.assigned) {
this.current = defaultState;
this.assigned = true;
}
return this.current;
};
/**
* Begin moving the splitter; this will note the splitter's
* current position, which is necessary for correct
* interpretation of deltas provided by mct-drag.
*/
SplitPaneController.prototype.startMove = function () {
this.start = this.current;
};
/**
* Move the splitter a number of pixels to the right
* (negative numbers move the splitter to the left.)
* This movement is relative to the position of the
* splitter when startMove was last invoked.
* @param {number} delta number of pixels to move
*/
SplitPaneController.prototype.move = function (delta, minimum, maximum) {
// Ensure defaults for minimum/maximum
maximum = isNaN(maximum) ? DEFAULT_MAXIMUM : maximum;
minimum = isNaN(minimum) ? DEFAULT_MINIMUM : minimum;
// Update current splitter state
this.current = Math.min(
maximum,
Math.max(minimum, this.start + delta)
);
};
return SplitPaneController;
}
);

View File

@@ -22,8 +22,8 @@
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../../src/controllers/DateTimePickerController"],
function (DateTimePickerController) {
["../../src/controllers/DateTimePickerController", "moment"],
function (DateTimePickerController, moment) {
"use strict";
describe("The DateTimePickerController", function () {
@@ -39,6 +39,14 @@ define(
});
}
function fireWatchCollection(expr, value) {
mockScope.$watchCollection.calls.forEach(function (call) {
if (call.args[0] === expr) {
call.args[1](value);
}
});
}
beforeEach(function () {
mockScope = jasmine.createSpyObj(
"$scope",
@@ -57,6 +65,131 @@ define(
);
});
it("updates value in model when values in scope change", function () {
mockScope.date = {
year: 1998,
month: 0,
day: 6
};
mockScope.time = {
hours: 12,
minutes: 34,
seconds: 56
};
fireWatchCollection("date", mockScope.date);
expect(mockScope.ngModel[mockScope.field])
.toEqual(moment.utc("1998-01-06 12:34:56").valueOf());
});
describe("once initialized with model state", function () {
var testTime = moment.utc("1998-01-06 12:34:56").valueOf();
beforeEach(function () {
fireWatch("ngModel[field]", testTime);
});
it("exposes date/time values in scope", function () {
expect(mockScope.date.year).toEqual(1998);
expect(mockScope.date.month).toEqual(0); // Months are zero-indexed
expect(mockScope.date.day).toEqual(6);
expect(mockScope.time.hours).toEqual(12);
expect(mockScope.time.minutes).toEqual(34);
expect(mockScope.time.seconds).toEqual(56);
});
it("provides names for time properties", function () {
Object.keys(mockScope.time).forEach(function (key) {
expect(mockScope.nameFor(key))
.toEqual(jasmine.any(String));
});
});
it("provides options for time properties", function () {
Object.keys(mockScope.time).forEach(function (key) {
expect(mockScope.optionsFor(key))
.toEqual(jasmine.any(Array));
});
});
it("exposes times to populate calendar as a table", function () {
// Verify that data structure is as expected by template
expect(mockScope.table).toEqual(jasmine.any(Array));
expect(mockScope.table[0]).toEqual(jasmine.any(Array));
expect(mockScope.table[0][0]).toEqual({
year: jasmine.any(Number),
month: jasmine.any(Number),
day: jasmine.any(Number),
dayOfYear: jasmine.any(Number)
});
});
it("contains the current date in its initial table", function () {
var matchingCell;
// Should be able to find the selected date
mockScope.table.forEach(function (row) {
row.forEach(function (cell) {
if (cell.dayOfYear === 6) {
matchingCell = cell;
}
});
});
expect(matchingCell).toEqual({
year: 1998,
month: 0,
day: 6,
dayOfYear: 6
});
});
it("allows the displayed month to be advanced", function () {
// Around the edges of the displayed calendar we
// may be in previous or subsequent month, so
// test around the middle.
var i, originalMonth = mockScope.table[2][0].month;
function mod12(month) {
return ((month % 12) + 12) % 12;
}
for (i = 1; i <= 12; i += 1) {
mockScope.changeMonth(1);
expect(mockScope.table[2][0].month)
.toEqual(mod12(originalMonth + i));
}
for (i = 11; i >= -12; i -= 1) {
mockScope.changeMonth(-1);
expect(mockScope.table[2][0].month)
.toEqual(mod12(originalMonth + i));
}
});
it("allows checking if a cell is in the current month", function () {
expect(mockScope.isInCurrentMonth(mockScope.table[2][0]))
.toBe(true);
});
it("allows cells to be selected", function () {
mockScope.select(mockScope.table[2][0]);
expect(mockScope.isSelected(mockScope.table[2][0]))
.toBe(true);
mockScope.select(mockScope.table[2][1]);
expect(mockScope.isSelected(mockScope.table[2][0]))
.toBe(false);
expect(mockScope.isSelected(mockScope.table[2][1]))
.toBe(true);
});
it("allows cells to be compared", function () {
var table = mockScope.table;
expect(mockScope.dateEquals(table[2][0], table[2][1]))
.toBe(false);
expect(mockScope.dateEquals(table[2][1], table[2][1]))
.toBe(true);
});
});
});
}

View File

@@ -1,74 +0,0 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../../src/controllers/SplitPaneController"],
function (SplitPaneController) {
"use strict";
describe("The split pane controller", function () {
var controller;
beforeEach(function () {
controller = new SplitPaneController();
});
it("has an initial position", function () {
expect(controller.state() > 0).toBeTruthy();
});
it("can be moved", function () {
var initialState = controller.state();
controller.startMove();
controller.move(50);
expect(controller.state()).toEqual(initialState + 50);
});
it("clamps its position", function () {
var initialState = controller.state();
controller.startMove();
// Move some really extreme number
controller.move(-100000);
// Shouldn't have moved below 0...
expect(controller.state() > 0).toBeTruthy();
// ...but should have moved left somewhere
expect(controller.state() < initialState).toBeTruthy();
// Then do the same to the right
controller.move(100000);
// Shouldn't have moved below 0...
expect(controller.state() < 100000).toBeTruthy();
// ...but should have moved left somewhere
expect(controller.state() > initialState).toBeTruthy();
});
it("accepts a default state", function () {
// Should use default state the first time...
expect(controller.state(12321)).toEqual(12321);
// ...but not after it's been initialized
expect(controller.state(42)).toEqual(12321);
});
});
}
);

View File

@@ -34,14 +34,14 @@ define(
mockElement,
testAttrs,
mockBody,
mockParentEl,
mockPlainEl,
testRect,
mctClickElsewhere;
function testEvent(x, y) {
return {
pageX: x,
pageY: y,
clientX: x,
clientY: y,
preventDefault: jasmine.createSpy("preventDefault")
};
}
@@ -55,8 +55,8 @@ define(
jasmine.createSpyObj("element", JQLITE_METHODS);
mockBody =
jasmine.createSpyObj("body", JQLITE_METHODS);
mockParentEl =
jasmine.createSpyObj("parent", ["getBoundingClientRect"]);
mockPlainEl =
jasmine.createSpyObj("htmlElement", ["getBoundingClientRect"]);
testAttrs = {
mctClickElsewhere: "some Angular expression"
@@ -67,6 +67,8 @@ define(
width: 60,
height: 75
};
mockElement[0] = mockPlainEl;
mockPlainEl.getBoundingClientRect.andReturn(testRect);
mockDocument.find.andReturn(mockBody);
@@ -78,6 +80,49 @@ define(
expect(mctClickElsewhere.restrict).toEqual("A");
});
it("detaches listeners when destroyed", function () {
expect(mockBody.off).not.toHaveBeenCalled();
mockScope.$on.calls.forEach(function (call) {
if (call.args[0] === '$destroy') {
call.args[1]();
}
});
expect(mockBody.off).toHaveBeenCalled();
expect(mockBody.off.mostRecentCall.args)
.toEqual(mockBody.on.mostRecentCall.args);
});
it("listens for mousedown on the document's body", function () {
expect(mockBody.on)
.toHaveBeenCalledWith('mousedown', jasmine.any(Function));
});
describe("when a click occurs outside the element's bounds", function () {
beforeEach(function () {
mockBody.on.mostRecentCall.args[1](testEvent(
testRect.left + testRect.width + 10,
testRect.top + testRect.height + 10
));
});
it("triggers an evaluation of its related Angular expression", function () {
expect(mockScope.$eval)
.toHaveBeenCalledWith(testAttrs.mctClickElsewhere);
});
});
describe("when a click occurs within the element's bounds", function () {
beforeEach(function () {
mockBody.on.mostRecentCall.args[1](testEvent(
testRect.left + testRect.width / 2,
testRect.top + testRect.height / 2
));
});
it("triggers no evaluation", function () {
expect(mockScope.$eval).not.toHaveBeenCalled();
});
});
});
}

View File

@@ -30,13 +30,16 @@ define(
'on',
'addClass',
'children',
'eq'
'eq',
'toggleClass',
'css'
];
describe("The mct-split-pane directive", function () {
var mockParse,
mockLog,
mockInterval,
mockParsed,
mctSplitPane;
beforeEach(function () {
@@ -45,6 +48,11 @@ define(
jasmine.createSpyObj('$log', ['warn', 'info', 'debug']);
mockInterval = jasmine.createSpy('$interval');
mockInterval.cancel = jasmine.createSpy('mockCancel');
mockParsed = jasmine.createSpy('parsed');
mockParsed.assign = jasmine.createSpy('assign');
mockParse.andReturn(mockParsed);
mctSplitPane = new MCTSplitPane(
mockParse,
mockLog,
@@ -61,8 +69,19 @@ define(
mockElement,
testAttrs,
mockChildren,
mockFirstPane,
mockSplitter,
mockSecondPane,
controller;
function fireOn(eventType) {
mockScope.$on.calls.forEach(function (call) {
if (call.args[0] === eventType) {
call.args[1]();
}
});
}
beforeEach(function () {
mockScope =
jasmine.createSpyObj('$scope', ['$apply', '$watch', '$on']);
@@ -71,10 +90,33 @@ define(
testAttrs = {};
mockChildren =
jasmine.createSpyObj('children', JQLITE_METHODS);
mockFirstPane =
jasmine.createSpyObj('firstPane', JQLITE_METHODS);
mockSplitter =
jasmine.createSpyObj('splitter', JQLITE_METHODS);
mockSecondPane =
jasmine.createSpyObj('secondPane', JQLITE_METHODS);
mockElement.children.andReturn(mockChildren);
mockChildren.eq.andReturn(mockChildren);
mockChildren[0] = {};
mockElement[0] = {
offsetWidth: 12321,
offsetHeight: 45654
};
mockChildren.eq.andCallFake(function (i) {
return [mockFirstPane, mockSplitter, mockSecondPane][i];
});
mockFirstPane[0] = { offsetWidth: 123, offsetHeight: 456 };
mockSplitter[0] = {
nodeName: 'mct-splitter',
offsetWidth: 10,
offsetHeight: 456
};
mockSecondPane[0] = { offsetWidth: 10, offsetHeight: 456 };
mockChildren[0] = mockFirstPane[0];
mockChildren[1] = mockSplitter[0];
mockChildren[3] = mockSecondPane[0];
mockChildren.length = 3;
controller = mctSplitPane.controller[3](
mockScope,
@@ -87,6 +129,77 @@ define(
expect(mockInterval.mostRecentCall.args[3]).toBe(false);
});
it("exposes its splitter's initial position", function () {
expect(controller.position()).toEqual(
mockFirstPane[0].offsetWidth + mockSplitter[0].offsetWidth
);
});
it("exposes the current anchoring mode", function () {
expect(controller.anchor()).toEqual({
edge : 'left',
opposite : 'right',
dimension : 'width',
orientation : 'vertical'
});
});
it("allows classes to be toggled on contained elements", function () {
controller.toggleClass('resizing');
expect(mockChildren.toggleClass)
.toHaveBeenCalledWith('resizing');
});
it("allows positions to be set", function () {
var testValue = mockChildren[0].offsetWidth + 50;
controller.position(testValue);
expect(mockFirstPane.css).toHaveBeenCalledWith(
'width',
(testValue - mockSplitter[0].offsetWidth) + 'px'
);
});
it("issues no warnings under nominal usage", function () {
expect(mockLog.warn).not.toHaveBeenCalled();
});
it("warns if no mct-splitter is present", function () {
mockSplitter[0].nodeName = "not-mct-splitter";
controller = mctSplitPane.controller[3](
mockScope,
mockElement,
testAttrs
);
expect(mockLog.warn).toHaveBeenCalled();
});
it("warns if an unknown anchor key is given", function () {
testAttrs.anchor = "middle";
controller = mctSplitPane.controller[3](
mockScope,
mockElement,
testAttrs
);
expect(mockLog.warn).toHaveBeenCalled();
});
it("updates positions on a timer", function () {
mockFirstPane[0].offsetWidth += 100;
// Should not reflect the change yet
expect(controller.position()).not.toEqual(
mockFirstPane[0].offsetWidth + mockSplitter[0].offsetWidth
);
mockInterval.mostRecentCall.args[0]();
expect(controller.position()).toEqual(
mockFirstPane[0].offsetWidth + mockSplitter[0].offsetWidth
);
});
it("cancels the active interval when scope is destroyed", function () {
expect(mockInterval.cancel).not.toHaveBeenCalled();
fireOn('$destroy');
expect(mockInterval.cancel).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,113 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../../src/directives/MCTSplitter"],
function (MCTSplitter) {
'use strict';
describe("The mct-splitter directive", function () {
var mctSplitter;
beforeEach(function () {
mctSplitter = new MCTSplitter();
});
it("is applicable to elements", function () {
expect(mctSplitter.restrict).toEqual("E");
});
it("depends on the mct-split-pane controller", function () {
expect(mctSplitter.require).toEqual("^mctSplitPane");
});
describe("when linked", function () {
var mockScope,
mockElement,
testAttrs,
mockSplitPane;
beforeEach(function () {
mockScope = jasmine.createSpyObj(
'$scope',
[ '$on', '$watch' ]
);
mockElement = jasmine.createSpyObj(
'element',
[ 'addClass' ]
);
testAttrs = {};
mockSplitPane = jasmine.createSpyObj(
'mctSplitPane',
[ 'position', 'toggleClass', 'anchor' ]
);
mctSplitter.link(
mockScope,
mockElement,
testAttrs,
mockSplitPane
);
});
it("adds a splitter class", function () {
expect(mockElement.addClass)
.toHaveBeenCalledWith('splitter');
});
describe("and then manipulated", function () {
var testPosition;
beforeEach(function () {
testPosition = 12321;
mockSplitPane.position.andReturn(testPosition);
mockSplitPane.anchor.andReturn({
orientation: 'vertical',
reversed: false
});
mockScope.splitter.startMove();
});
it("adds a 'resizing' class", function () {
expect(mockSplitPane.toggleClass)
.toHaveBeenCalledWith('resizing');
});
it("repositions during drag", function () {
mockScope.splitter.move([ 10, 0 ]);
expect(mockSplitPane.position)
.toHaveBeenCalledWith(testPosition + 10);
});
it("removes the 'resizing' class when finished", function () {
mockSplitPane.toggleClass.reset();
mockScope.splitter.endMove();
expect(mockSplitPane.toggleClass)
.toHaveBeenCalledWith('resizing');
});
});
});
});
}
);

View File

@@ -8,7 +8,6 @@
"controllers/GetterSetterController",
"controllers/ObjectInspectorController",
"controllers/SelectorController",
"controllers/SplitPaneController",
"controllers/TimeRangeController",
"controllers/ToggleController",
"controllers/TreeNodeController",
@@ -20,6 +19,7 @@
"directives/MCTResize",
"directives/MCTScroll",
"directives/MCTSplitPane",
"directives/MCTSplitter",
"services/Popup",
"services/PopupService",
"services/UrlService",

View File

@@ -35,10 +35,21 @@ define(
* Both "Start" and "Restart" share this implementation, but
* control their visibility with different `appliesTo` behavior.
*
* @implements Action
* @implements {Action}
* @memberof platform/features/clock
* @constructor
* @param {Function} now a function which returns the current
* time (typically wrapping `Date.now`)
* @param {ActionContext} context the context for this action
*/
function AbstractStartTimerAction(now, context) {
var domainObject = context.domainObject;
this.domainObject = context.domainObject;
this.now = now;
}
AbstractStartTimerAction.prototype.perform = function () {
var domainObject = this.domainObject,
now = this.now;
function doPersist() {
var persistence = domainObject.getCapability('persistence');
@@ -49,13 +60,9 @@ define(
model.timestamp = now();
}
return {
perform: function () {
return domainObject.useCapability('mutation', setTimestamp)
.then(doPersist);
}
};
}
return domainObject.useCapability('mutation', setTimestamp)
.then(doPersist);
};
return AbstractStartTimerAction;
}

View File

@@ -31,12 +31,22 @@ define(
*
* Behaves the same as (and delegates functionality to)
* the "Start" action.
* @implements Action
*
* @extends {platform/features/clock.AbstractTimerAction}
* @implements {Action}
* @memberof platform/features/clock
* @constructor
* @param {Function} now a function which returns the current
* time (typically wrapping `Date.now`)
* @param {ActionContext} context the context for this action
*/
function RestartTimerAction(now, context) {
return new AbstractStartTimerAction(now, context);
AbstractStartTimerAction.apply(this, [ now, context ]);
}
RestartTimerAction.prototype =
Object.create(AbstractStartTimerAction.prototype);
RestartTimerAction.appliesTo = function (context) {
var model =
(context.domainObject && context.domainObject.getModel())

View File

@@ -32,12 +32,21 @@ define(
* Sets the reference timestamp in a timer to the current
* time, such that it begins counting up.
*
* @implements Action
* @extends {platform/features/clock.AbstractTimerAction}
* @implements {Action}
* @memberof platform/features/clock
* @constructor
* @param {Function} now a function which returns the current
* time (typically wrapping `Date.now`)
* @param {ActionContext} context the context for this action
*/
function StartTimerAction(now, context) {
return new AbstractStartTimerAction(now, context);
AbstractStartTimerAction.apply(this, [ now, context ]);
}
StartTimerAction.prototype =
Object.create(AbstractStartTimerAction.prototype);
StartTimerAction.appliesTo = function (context) {
var model =
(context.domainObject && context.domainObject.getModel())

View File

@@ -30,19 +30,21 @@ define(
* Controller for views of a Clock domain object.
*
* @constructor
* @memberof platform/features/clock
* @param {angular.Scope} $scope the Angular scope
* @param {platform/features/clock.TickerService} tickerService
* a service used to align behavior with clock ticks
*/
function ClockController($scope, tickerService) {
var text,
ampm,
use24,
lastTimestamp,
var lastTimestamp,
unlisten,
timeFormat;
timeFormat,
self = this;
function update() {
var m = moment.utc(lastTimestamp);
text = timeFormat && m.format(timeFormat);
ampm = m.format("A"); // Just the AM or PM part
self.textValue = timeFormat && m.format(timeFormat);
self.ampmValue = m.format("A"); // Just the AM or PM part
}
function tick(timestamp) {
@@ -56,8 +58,8 @@ define(
if (clockFormat !== undefined) {
baseFormat = clockFormat[0];
use24 = clockFormat[1] === 'clock24';
timeFormat = use24 ?
self.use24 = clockFormat[1] === 'clock24';
timeFormat = self.use24 ?
baseFormat.replace('hh', "HH") : baseFormat;
update();
@@ -69,32 +71,32 @@ define(
// Listen for clock ticks ... and stop listening on destroy
unlisten = tickerService.listen(tick);
$scope.$on('$destroy', unlisten);
return {
/**
* Get the clock's time zone, as displayable text.
* @returns {string}
*/
zone: function () {
return "UTC";
},
/**
* Get the current time, as displayable text.
* @returns {string}
*/
text: function () {
return text;
},
/**
* Get the text to display to qualify a time as AM or PM.
* @returns {string}
*/
ampm: function () {
return use24 ? '' : ampm;
}
};
}
/**
* Get the clock's time zone, as displayable text.
* @returns {string}
*/
ClockController.prototype.zone = function () {
return "UTC";
};
/**
* Get the current time, as displayable text.
* @returns {string}
*/
ClockController.prototype.text = function () {
return this.textValue;
};
/**
* Get the text to display to qualify a time as AM or PM.
* @returns {string}
*/
ClockController.prototype.ampm = function () {
return this.use24 ? '' : this.ampmValue;
};
return ClockController;
}
);

View File

@@ -31,6 +31,12 @@ define(
*
* This is a short-term workaround to assure Timer views stay
* up-to-date; should be replaced by a global auto-refresh.
*
* @constructor
* @memberof platform/features/clock
* @param {angular.Scope} $scope the Angular scope
* @param {platform/features/clock.TickerService} tickerService
* a service used to align behavior with clock ticks
*/
function RefreshingController($scope, tickerService) {
var unlisten;

View File

@@ -33,26 +33,30 @@ define(
* Controller for views of a Timer domain object.
*
* @constructor
* @memberof platform/features/clock
* @param {angular.Scope} $scope the Angular scope
* @param $window Angular-provided window object
* @param {Function} now a function which returns the current
* time (typically wrapping `Date.now`)
*/
function TimerController($scope, $window, now) {
var timerObject,
relevantAction,
sign = '',
text = '',
formatter,
active = true,
relativeTimestamp,
lastTimestamp;
lastTimestamp,
self = this;
function update() {
var timeDelta = lastTimestamp - relativeTimestamp;
if (formatter && !isNaN(timeDelta)) {
text = formatter(timeDelta);
sign = timeDelta < 0 ? "-" : timeDelta >= 1000 ? "+" : "";
self.textValue = formatter(timeDelta);
self.signValue = timeDelta < 0 ? "-" :
timeDelta >= 1000 ? "+" : "";
} else {
text = "";
sign = "";
self.textValue = "";
self.signValue = "";
}
}
@@ -75,7 +79,7 @@ define(
updateFormat(formatKey);
updateTimestamp(timestamp);
relevantAction = actionCapability &&
self.relevantAction = actionCapability &&
actionCapability.getActions(actionKey)[0];
update();
@@ -92,13 +96,14 @@ define(
}
function tick() {
var lastSign = sign, lastText = text;
var lastSign = self.signValue,
lastText = self.textValue;
lastTimestamp = now();
update();
// We're running in an animation frame, not in a digest cycle.
// We need to trigger a digest cycle if our displayable data
// changes.
if (lastSign !== sign || lastText !== text) {
if (lastSign !== self.signValue || lastText !== self.textValue) {
$scope.$apply();
}
if (active) {
@@ -117,51 +122,59 @@ define(
active = false;
});
return {
/**
* Get the glyph to display for the start/restart button.
* @returns {string} glyph to display
*/
buttonGlyph: function () {
return relevantAction ?
relevantAction.getMetadata().glyph : "";
},
/**
* Get the text to show for the start/restart button
* (e.g. in a tooltip)
* @returns {string} name of the action
*/
buttonText: function () {
return relevantAction ?
relevantAction.getMetadata().name : "";
},
/**
* Perform the action associated with the start/restart button.
*/
clickButton: function () {
if (relevantAction) {
relevantAction.perform();
updateObject($scope.domainObject);
}
},
/**
* Get the sign (+ or -) of the current timer value, as
* displayable text.
* @returns {string} sign of the current timer value
*/
sign: function () {
return sign;
},
/**
* Get the text to display for the current timer value.
* @returns {string} current timer value
*/
text: function () {
return text;
}
};
this.$scope = $scope;
this.signValue = '';
this.textValue = '';
this.updateObject = updateObject;
}
/**
* Get the glyph to display for the start/restart button.
* @returns {string} glyph to display
*/
TimerController.prototype.buttonGlyph = function () {
return this.relevantAction ?
this.relevantAction.getMetadata().glyph : "";
};
/**
* Get the text to show for the start/restart button
* (e.g. in a tooltip)
* @returns {string} name of the action
*/
TimerController.prototype.buttonText = function () {
return this.relevantAction ?
this.relevantAction.getMetadata().name : "";
};
/**
* Perform the action associated with the start/restart button.
*/
TimerController.prototype.clickButton = function () {
if (this.relevantAction) {
this.relevantAction.perform();
this.updateObject(this.$scope.domainObject);
}
};
/**
* Get the sign (+ or -) of the current timer value, as
* displayable text.
* @returns {string} sign of the current timer value
*/
TimerController.prototype.sign = function () {
return this.signValue;
};
/**
* Get the text to display for the current timer value.
* @returns {string} current timer value
*/
TimerController.prototype.text = function () {
return this.textValue;
};
return TimerController;
}
);

View File

@@ -37,45 +37,39 @@ define(
* supports `TimerController`.
*
* @constructor
* @memberof platform/features/clock
*/
function TimerFormatter() {
// Round this timestamp down to the second boundary
// (e.g. 1124ms goes down to 1000ms, -2400ms goes down to -3000ms)
function toWholeSeconds(duration) {
return Math.abs(Math.floor(duration / 1000) * 1000);
}
// Short-form format, e.g. 02:22:11
function short(duration) {
return moment.duration(toWholeSeconds(duration), 'ms')
.format(SHORT_FORMAT, { trim: false });
}
// Long-form format, e.g. 3d 02:22:11
function long(duration) {
return moment.duration(toWholeSeconds(duration), 'ms')
.format(LONG_FORMAT, { trim: false });
}
return {
/**
* Format a duration for display, using the short form.
* (e.g. 03:33:11)
* @param {number} duration the duration, in milliseconds
* @param {boolean} sign true if positive
*/
short: short,
/**
* Format a duration for display, using the long form.
* (e.g. 0d 03:33:11)
* @param {number} duration the duration, in milliseconds
* @param {boolean} sign true if positive
*/
long: long
};
}
// Round this timestamp down to the second boundary
// (e.g. 1124ms goes down to 1000ms, -2400ms goes down to -3000ms)
function toWholeSeconds(duration) {
return Math.abs(Math.floor(duration / 1000) * 1000);
}
/**
* Format a duration for display, using the short form.
* (e.g. 03:33:11)
* @param {number} duration the duration, in milliseconds
* @param {boolean} sign true if positive
*/
TimerFormatter.prototype.short = function (duration) {
return moment.duration(toWholeSeconds(duration), 'ms')
.format(SHORT_FORMAT, { trim: false });
};
/**
* Format a duration for display, using the long form.
* (e.g. 0d 03:33:11)
* @param {number} duration the duration, in milliseconds
* @param {boolean} sign true if positive
*/
TimerFormatter.prototype.long = function (duration) {
return moment.duration(toWholeSeconds(duration), 'ms')
.format(LONG_FORMAT, { trim: false });
};
return TimerFormatter;
}
);

View File

@@ -28,32 +28,40 @@ define(
/**
* Indicator that displays the current UTC time in the status area.
* @implements Indicator
* @implements {Indicator}
* @memberof platform/features/clock
* @param {platform/features/clock.TickerService} tickerService
* a service used to align behavior with clock ticks
* @param {string} indicatorFormat format string for timestamps
* shown in this indicator
*/
function ClockIndicator(tickerService, CLOCK_INDICATOR_FORMAT) {
var text = "";
function ClockIndicator(tickerService, indicatorFormat) {
var self = this;
this.text = "";
tickerService.listen(function (timestamp) {
text = moment.utc(timestamp).format(CLOCK_INDICATOR_FORMAT) + " UTC";
self.text = moment.utc(timestamp)
.format(indicatorFormat) + " UTC";
});
return {
getGlyph: function () {
return "C";
},
getGlyphClass: function () {
return "no-icon no-collapse float-right subtle";
},
getText: function () {
return text;
},
getDescription: function () {
return "";
}
};
}
ClockIndicator.prototype.getGlyph = function () {
return "C";
};
ClockIndicator.prototype.getGlyphClass = function () {
return "no-icon no-collapse float-right subtle";
};
ClockIndicator.prototype.getText = function () {
return this.text;
};
ClockIndicator.prototype.getDescription = function () {
return "";
};
return ClockIndicator;
}
);

View File

@@ -30,23 +30,23 @@ define(
* Calls functions every second, as close to the actual second
* tick as is feasible.
* @constructor
* @memberof platform/features/clock
* @param $timeout Angular's $timeout
* @param {Function} now function to provide the current time in ms
*/
function TickerService($timeout, now) {
var callbacks = [],
last = now() - 1000;
var self = this;
function tick() {
var timestamp = now(),
millis = timestamp % 1000;
// Only update callbacks if a second has actually passed.
if (timestamp >= last + 1000) {
callbacks.forEach(function (callback) {
if (timestamp >= self.last + 1000) {
self.callbacks.forEach(function (callback) {
callback(timestamp);
});
last = timestamp - millis;
self.last = timestamp - millis;
}
// Try to update at exactly the next second
@@ -55,35 +55,35 @@ define(
tick();
return {
/**
* Listen for clock ticks. The provided callback will
* be invoked with the current timestamp (in milliseconds
* since Jan 1 1970) at regular intervals, as near to the
* second boundary as possible.
*
* @method listen
* @name TickerService#listen
* @param {Function} callback callback to invoke
* @returns {Function} a function to unregister this listener
*/
listen: function (callback) {
callbacks.push(callback);
// Provide immediate feedback
callback(last);
// Provide a deregistration function
return function () {
callbacks = callbacks.filter(function (cb) {
return cb !== callback;
});
};
}
};
this.callbacks = [];
this.last = now() - 1000;
}
/**
* Listen for clock ticks. The provided callback will
* be invoked with the current timestamp (in milliseconds
* since Jan 1 1970) at regular intervals, as near to the
* second boundary as possible.
*
* @param {Function} callback callback to invoke
* @returns {Function} a function to unregister this listener
*/
TickerService.prototype.listen = function (callback) {
var self = this;
self.callbacks.push(callback);
// Provide immediate feedback
callback(this.last);
// Provide a deregistration function
return function () {
self.callbacks = self.callbacks.filter(function (cb) {
return cb !== callback;
});
};
};
return TickerService;
}
);

View File

@@ -424,6 +424,58 @@ define(
expect(controller.selected().style).not.toEqual(oldStyle);
});
describe("on display bounds changes", function () {
var testBounds;
beforeEach(function () {
testBounds = { start: 123, end: 321 };
mockScope.domainObject = mockDomainObject;
mockScope.model = testModel;
findWatch("domainObject")(mockDomainObject);
findWatch("model.modified")(testModel.modified);
findWatch("model.composition")(mockScope.model.composition);
findOn('telemetry:display:bounds')({}, testBounds);
});
it("issues new requests", function () {
expect(mockHandle.request).toHaveBeenCalled();
});
it("requests only a single point", function () {
expect(mockHandle.request.mostRecentCall.args[0].size)
.toEqual(1);
});
describe("and after data has been received", function () {
var mockSeries,
testValue;
beforeEach(function () {
testValue = 12321;
mockSeries = jasmine.createSpyObj('series', [
'getPointCount',
'getDomainValue',
'getRangeValue'
]);
mockSeries.getPointCount.andReturn(1);
mockSeries.getRangeValue.andReturn(testValue);
// Fire the callback associated with the request
mockHandle.request.mostRecentCall.args[1](
mockHandle.getTelemetryObjects()[0],
mockSeries
);
});
it("updates displayed values", function () {
expect(controller.getElements()[0].value)
.toEqual("Formatted " + testValue);
});
});
});
it("reflects limit status", function () {
var elements;
@@ -459,6 +511,7 @@ define(
expect(elements[1].cssClass).toEqual("alarm-b");
expect(elements[2].cssClass).toEqual("alarm-c");
});
});
}
);

View File

@@ -286,6 +286,34 @@ define(
expect(mockHandle.request.calls.length).toEqual(2);
});
it("maintains externally-provided domain axis bounds after data is received", function () {
mockSeries.getPointCount.andReturn(3);
mockSeries.getRangeValue.andReturn(42);
mockSeries.getDomainValue.andCallFake(function (i) {
return 2500 + i * 2500;
});
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
fireEvent("telemetry:display:bounds", [
{},
{start: 0, end: 10000}
]);
mockHandle.request.mostRecentCall.args[1](
mockDomainObject,
mockSeries
);
// Pan-zoom state should reflect bounds set externally;
// domain axis should not have shrunk to fit data.
expect(
controller.getSubPlots()[0].panZoomStack.getOrigin()[0]
).toEqual(0);
expect(
controller.getSubPlots()[0].panZoomStack.getDimensions()[0]
).toEqual(10000);
});
it("provides classes for legends based on limit state", function () {
var mockTelemetryObjects = mockHandle.getTelemetryObjects();

View File

@@ -32,16 +32,14 @@ define(
var TEST_RANGE_VALUE = "some formatted range value";
describe("A range column", function () {
var mockDataSet,
var testDatum,
testMetadata,
mockFormatter,
mockDomainObject,
column;
beforeEach(function () {
mockDataSet = jasmine.createSpyObj(
"data",
[ "getRangeValue" ]
);
testDatum = { testKey: 123, otherKey: 456 };
mockFormatter = jasmine.createSpyObj(
"formatter",
[ "formatDomainValue", "formatRangeValue" ]
@@ -50,6 +48,10 @@ define(
key: "testKey",
name: "Test Name"
};
mockDomainObject = jasmine.createSpyObj(
"domainObject",
[ "getModel", "getCapability" ]
);
mockFormatter.formatRangeValue.andReturn(TEST_RANGE_VALUE);
column = new RangeColumn(testMetadata, mockFormatter);
@@ -59,20 +61,13 @@ define(
expect(column.getTitle()).toEqual("Test Name");
});
xit("looks up data from a data set", function () {
column.getValue(undefined, mockDataSet, 42);
expect(mockDataSet.getRangeValue)
.toHaveBeenCalledWith(42, "testKey");
});
xit("formats range values as numbers", function () {
mockDataSet.getRangeValue.andReturn(123.45678);
expect(column.getValue(undefined, mockDataSet, 42).text)
it("formats range values as numbers", function () {
expect(column.getValue(mockDomainObject, testDatum).text)
.toEqual(TEST_RANGE_VALUE);
// Make sure that service interactions were as expected
expect(mockFormatter.formatRangeValue)
.toHaveBeenCalledWith(123.45678);
.toHaveBeenCalledWith(testDatum.testKey);
expect(mockFormatter.formatDomainValue)
.not.toHaveBeenCalled();
});

View File

@@ -47,7 +47,13 @@ define(
mockQ = jasmine.createSpyObj('$q', ['when', 'all']);
mockSubscription = jasmine.createSpyObj(
'subscription',
['unsubscribe', 'getTelemetryObjects', 'promiseTelemetryObjects']
[
'makeDatum',
'getDatum',
'unsubscribe',
'getTelemetryObjects',
'promiseTelemetryObjects'
]
);
mockDomainObject = jasmine.createSpyObj(
'domainObject',
@@ -112,6 +118,20 @@ define(
expect(handle.getSeries(mockDomainObject))
.toEqual(mockSeries);
});
it("provides access to the datum objects by index", function () {
var testDatum = { a: 1, b: 2 }, testIndex = 42;
mockSubscription.makeDatum.andReturn(testDatum);
handle.request({});
expect(handle.getDatum(mockDomainObject, testIndex))
.toEqual(testDatum);
expect(mockSubscription.makeDatum)
.toHaveBeenCalledWith(
mockDomainObject,
mockSeries,
testIndex
);
});
});
}
);