Compare commits

...

18 Commits

Author SHA1 Message Date
6cfee100d7 Merge branch 'master' into handle-invalid-time-url 2017-08-02 15:44:18 -07:00
eeb214204d Merge pull request #1662 from nasa/revert-1659-master
Revert "persist user preference width for MCTSplitPanes issue #1646"
2017-08-02 10:53:07 -07:00
b41ceab51e Revert "persist user preference width for MCTSplitPanes issue #1646" 2017-08-02 10:48:57 -07:00
10b0f43fc1 Merge pull request #1659 from dtailor90/master
persist user preference width for MCTSplitPanes issue #1646, updated MCTSplitPane to handle logic regarding localStorage
2017-08-02 10:48:51 -07:00
593c1adf56 updated mctSplitPane to handle all logic regarding persisting userPreferenceWidth to local storage 2017-08-02 10:46:14 -07:00
5a7fdf82ac persist user preference width for MCTSplitPanes 2017-08-01 12:00:21 -07:00
4571205871 [Time] Handle missing time system gracefully
When loading the application and no time system is set,
the url handler should not cause multiple errors to be
logged.

This adds a test for the case but does not specify the
expected behavior in detail, other than "it should not
crash."

Fixes bugs as reported here.
2017-07-27 15:38:42 -07:00
34c3763421 Merge pull request #1635 from nasa/historical-imagery
[Historical imagery] [WIP]: Add historical view for image telemetry
2017-07-26 11:36:21 -07:00
ed6ae23dc0 [Historical Imagery] JSDoc, code review style changes
Added $element dependency and JSDoc for private methods. Autoscroll is now enabled by default when there is an active clock. Inline comments removed.
2017-07-14 13:05:59 -07:00
23839b05b0 Merge pull request #1643 from nasa/open1641
[Open 1641] Change warnings to info messages
2017-07-13 17:23:10 -07:00
6aed3bb0b5 Merge pull request #1638 from nasa/only-time-change-when-changed-1636
[Time API] Only change time when changed
2017-07-13 17:11:59 -07:00
1ae62cde05 [Browse] Don't klobber params when preventing default
When browse controller is hijacking a browser navigation event,
it calls preventDefault on the route change.  This has the effect
of preventing all changes in the location (including search changes).

This change checks if other changes were made in the route change and
re-applies them after the navigation has completed.

Fixes a bug where navigating via a link that contained additional
search paramterers would have the effect of navigating but not
keeping the parameters in tact.

Discovered in the course of fixing #1636
2017-07-13 16:48:38 -07:00
4e7e5bb783 [Time] Conductor changes based on click not scope
Update time conductor so that it triggers changes when the user
selects them instead of when the scope is updated.  Prevents spurious
changes from being triggered by the conductor when it updates
in response to a time API change.

Fixes #1636.
2017-07-13 16:48:38 -07:00
efc46613bb Updated TelemetryCapability.js
removed outdated comment.
2017-07-13 16:47:24 -07:00
218ef16160 [Imagery] Implemented historical view for imagery
Implemented auto-scrolling historical imagery view in ImageryController. Imagery domain objects now request historical data on each manual bounds change. Added new specs for ensuring that historical data is requested on bounds change and duplicate bounds / datum are ignored.
2017-07-12 12:13:57 -07:00
fb0a577d16 [Open1641] Updated the spec files to check for info messages instead of warning messages 2017-07-10 09:46:51 -07:00
19b5e7c781 [Open 1641] Change warnings to info messages 2017-07-06 15:55:29 -07:00
cb242d8efb [TimeAPI] check for change before triggering
Update the TimeSettingsURLHandler to check for changes to the search
parameters and only trigger a bounds change when there is a change.

Added integration tests between Time API and settings url handler.

Prevents extraneous bounds events.

Fixes https://github.com/nasa/openmct/issues/1636
2017-07-04 15:09:56 -07:00
17 changed files with 876 additions and 244 deletions

View File

@ -25,8 +25,8 @@
* @namespace platform/commonUI/browse
*/
define(
[],
function () {
['lodash'],
function (_) {
/**
* The BrowseController is used to populate the initial scope in Browse
@ -157,12 +157,28 @@ define(
// (e.g. bookmarks to pages in OpenMCT) and prevent them. Instead,
// navigate to the path ourselves, which results in it being
// properly set.
$scope.$on('$routeChangeStart', function (event, route) {
$scope.$on('$routeChangeStart', function (event, route, oldRoute) {
if (route.$$route === $route.current.$$route) {
if (route.pathParams.ids &&
route.pathParams.ids !== $route.current.pathParams.ids) {
var otherParams = _.omit(route.params, 'ids');
var oldOtherParams = _.omit(oldRoute.params, 'ids');
var deletedParams = _.omit(oldOtherParams, _.keys(otherParams));
event.preventDefault();
navigateToPath(route.pathParams.ids.split('/'));
navigateToPath(route.pathParams.ids.split('/'))
.then(function () {
if (!_.isEqual(otherParams, oldOtherParams)) {
_.forEach(otherParams, function (v, k) {
$location.search(k, v);
});
_.forEach(deletedParams, function (k) {
$location.search(k, null);
});
}
});
} else {
navigateToPath([]);
}

View File

@ -83,12 +83,10 @@
.l-image-thumbs-wrapper {
//@include test(green);
direction: rtl;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: $interiorMargin;
white-space: nowrap;
z-index: 70;
}
.l-image-thumb-item {
@ -114,7 +112,7 @@
width: $imageThumbsD + $imageThumbPad*2;
white-space: normal;
&:hover {
background: rgba(#fff, 0.2);
background: $colorThumbHoverBg;
.l-date,
.l-time {
color: #fff;

View File

@ -191,6 +191,9 @@ $colorItemTreeVCHover: pullForward($colorItemTreeVC, 20%);
$colorItemTreeSelectedVC: $colorItemTreeVC;
$shdwItemTreeIcon: 0.6;
// Images
$colorThumbHoverBg: $colorItemTreeHoverBg;
// Scrollbar
$scrollbarTrackSize: 10px;
$scrollbarTrackShdw: rgba(#000, 0.7) 0 1px 5px;

View File

@ -191,6 +191,9 @@ $colorItemTreeVCHover: $colorKey;
$colorItemTreeSelectedVC: $colorBodyBg;
$shdwItemTreeIcon: none;
// Images
$colorThumbHoverBg: $colorItemTreeHoverBg;
// Scrollbar
$scrollbarTrackSize: 10px;
$scrollbarTrackShdw: rgba(#000, 0.2) 0 1px 2px;

View File

@ -23,7 +23,7 @@
<div class="pane left menu-items">
<ul>
<li ng-repeat="metadata in ngModel.options"
ng-click="ngModel.selected = metadata">
ng-click="ngModel.select(metadata)">
<a ng-mouseover="ngModel.activeMetadata = metadata"
ng-mouseleave="ngModel.activeMetadata = undefined"
class="menu-item-a {{metadata.cssClass}}">

View File

@ -111,7 +111,8 @@ define(
var options = this.optionsFromConfig(config);
this.menu = {
selected: undefined,
options: options
options: options,
select: this.selectMenuOption
};
//Set the initial state of the UI from the conductor state
@ -132,8 +133,6 @@ define(
this.setViewFromBounds(bounds);
}
this.$scope.$watch("tcController.menu.selected", this.selectMenuOption);
this.conductorViewService.on('pan', this.onPan);
this.conductorViewService.on('pan-stop', this.onPanStop);
@ -164,26 +163,28 @@ define(
*
* @private
* @param newOption
* @param oldOption
*/
TimeConductorController.prototype.selectMenuOption = function (newOption, oldOption) {
if (newOption !== oldOption) {
var config = this.getConfig(this.timeAPI.timeSystem(), newOption.clock);
if (!config) {
// Clock does not support this timeSystem, fallback to first
// option provided for clock.
config = this.config.menuOptions.filter(function (menuOption) {
return menuOption.clock === (newOption.clock && newOption.clock.key);
})[0];
}
TimeConductorController.prototype.selectMenuOption = function (newOption) {
if (this.menu.selected.key === newOption.key) {
return;
}
this.menu.selected = newOption;
if (config.clock) {
this.timeAPI.clock(config.clock, config.clockOffsets);
this.timeAPI.timeSystem(config.timeSystem);
} else {
this.timeAPI.stopClock();
this.timeAPI.timeSystem(config.timeSystem, config.bounds);
}
var config = this.getConfig(this.timeAPI.timeSystem(), newOption.clock);
if (!config) {
// Clock does not support this timeSystem, fallback to first
// option provided for clock.
config = this.config.menuOptions.filter(function (menuOption) {
return menuOption.clock === (newOption.clock && newOption.clock.key);
})[0];
}
if (config.clock) {
this.timeAPI.clock(config.clock, config.clockOffsets);
this.timeAPI.timeSystem(config.timeSystem);
} else {
this.timeAPI.stopClock();
this.timeAPI.timeSystem(config.timeSystem, config.bounds);
}
};

View File

@ -25,12 +25,14 @@ define([
"./src/controllers/ImageryController",
"./src/directives/MCTBackgroundImage",
"text!./res/templates/imagery.html",
"text!./res/templates/imageryTimeline.html",
'legacyRegistry'
], function (
ImageryViewPolicy,
ImageryController,
MCTBackgroundImage,
imageryTemplate,
imageryTimelineTemplate,
legacyRegistry
) {
@ -48,6 +50,17 @@ define([
"telemetry"
],
"editable": false
},
{
"name": "Historical Imagery",
"key": "historical-imagery",
"cssClass": "icon-image",
"template": imageryTimelineTemplate,
"priority": "preferred",
"needs": [
"telemetry"
],
"editable": false
}
],
"policies": [
@ -65,6 +78,8 @@ define([
"implementation": ImageryController,
"depends": [
"$scope",
"$window",
"$element",
"openmct"
]
}

View File

@ -0,0 +1,8 @@
<div class="l-image-thumbs-wrapper" ng-controller="ImageryController as imagery">
<div class="l-image-thumb-item" ng-repeat="image in imageHistory track by $index">
<img class="l-thumb" ng-init="imagery.scrollToRight()"
ng-src={{imagery.getImageUrl(image)}} >
<div class="l-time">{{imagery.getTime(image)}}</div>
</div>
</div>

View File

@ -24,9 +24,13 @@
* This bundle implements views of image telemetry.
* @namespace platform/features/imagery
*/
define(
['moment'],
function (moment) {
[
'zepto',
'lodash'
],
function ($, _) {
/**
* Controller for the "Imagery" view of a domain object which
@ -34,14 +38,20 @@ define(
* @constructor
* @memberof platform/features/imagery
*/
function ImageryController($scope, openmct) {
function ImageryController($scope, $window, element, openmct) {
this.$scope = $scope;
this.$window = $window;
this.openmct = openmct;
this.date = "";
this.time = "";
this.zone = "";
this.imageUrl = "";
this.requestCount = 0;
this.scrollable = $(element[0]);
this.autoScroll = openmct.time.clock() ? true : false;
this.$scope.imageHistory = [];
this.$scope.filters = {
brightness: 100,
contrast: 100
@ -50,12 +60,15 @@ define(
this.subscribe = this.subscribe.bind(this);
this.stopListening = this.stopListening.bind(this);
this.updateValues = this.updateValues.bind(this);
this.updateHistory = this.updateHistory.bind(this);
this.onBoundsChange = this.onBoundsChange.bind(this);
this.onScroll = this.onScroll.bind(this);
// Subscribe to telemetry when a domain object becomes available
this.subscribe(this.$scope.domainObject);
// Unsubscribe when the plot is destroyed
this.$scope.$on("$destroy", this.stopListening);
this.$scope.$on('$destroy', this.stopListening);
this.openmct.time.on('bounds', this.onBoundsChange);
this.scrollable.on('scroll', this.onScroll);
}
ImageryController.prototype.subscribe = function (domainObject) {
@ -75,50 +88,152 @@ define(
.telemetry
.getValueFormatter(metadata.valuesForHints(['image'])[0]);
this.unsubscribe = this.openmct.telemetry
.subscribe(this.domainObject, this.updateValues);
this.openmct.telemetry
.request(this.domainObject, {
strategy: 'latest',
size: 1
})
.then(function (values) {
this.updateValues(values[0]);
.subscribe(this.domainObject, function (datum) {
this.updateHistory(datum);
this.updateValues(datum);
}.bind(this));
this.requestLad(false);
this.requestHistory(this.openmct.time.bounds());
}.bind(this));
};
ImageryController.prototype.requestHistory = function (bounds) {
this.requestCount++;
this.$scope.imageHistory = [];
var requestId = this.requestCount;
this.openmct.telemetry
.request(this.domainObject, bounds)
.then(function (values) {
if (this.requestCount > requestId) {
return Promise.resolve('Stale request');
}
values.forEach(function (datum) {
this.updateHistory(datum);
}, this);
this.requestLad(true);
}.bind(this));
};
/**
* Makes a request for the most recent datum in the
* telelmetry store. Optional addToHistory argument
* determines whether the requested telemetry should
* be added to history or only used to update the current
* image url and timestamp.
* @private
* @param {boolean} [addToHistory] if true, adds to history
*/
ImageryController.prototype.requestLad = function (addToHistory) {
this.openmct.telemetry
.request(this.domainObject, {
strategy: 'latest',
size: 1
})
.then(function (values) {
this.updateValues(values[0]);
if (addToHistory !== false) {
this.updateHistory(values[0]);
}
}.bind(this));
};
ImageryController.prototype.stopListening = function () {
this.openmct.time.off('bounds', this.onBoundsChange);
this.scrollable.off('scroll', this.onScroll);
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
};
// Update displayable values to reflect latest image telemetry
/**
* Responds to bound change event be requesting new
* historical data if the bound change was manual.
* @private
* @param {object} [newBounds] new bounds object
* @param {boolean} [tick] true when change is automatic
*/
ImageryController.prototype.onBoundsChange = function (newBounds, tick) {
if (this.domainObject && !tick) {
this.requestHistory(newBounds);
}
};
/**
* Updates displayable values to match those of the most
* recently recieved datum.
* @param {object} [datum] the datum
* @private
*/
ImageryController.prototype.updateValues = function (datum) {
if (this.isPaused) {
this.nextDatum = datum;
return;
}
this.time = this.timeFormat.format(datum);
this.imageUrl = this.imageFormat.format(datum);
};
/**
* Appends given imagery datum to running history.
* @private
* @param {object} [datum] target telemetry datum
* @returns {boolean} falsy when a duplicate datum is given
*/
ImageryController.prototype.updateHistory = function (datum) {
if (this.$scope.imageHistory.length === 0 ||
!_.isEqual(this.$scope.imageHistory.slice(-1)[0], datum)) {
var index = _.sortedIndex(this.$scope.imageHistory, datum, 'utc');
this.$scope.imageHistory.splice(index, 0, datum);
return true;
}
return false;
};
ImageryController.prototype.onScroll = function (event) {
this.$window.requestAnimationFrame(function () {
if (this.scrollable[0].scrollLeft <
(this.scrollable[0].scrollWidth - this.scrollable[0].clientWidth) - 20) {
this.autoScroll = false;
} else {
this.autoScroll = true;
}
}.bind(this));
};
ImageryController.prototype.scrollToRight = function () {
if (this.autoScroll) {
this.scrollable[0].scrollLeft = this.scrollable[0].scrollWidth;
}
};
/**
* Get the time portion (hours, minutes, seconds) of the
* timestamp associated with the incoming image telemetry.
* timestamp associated with the incoming image telemetry
* if no parameter is given, or of a provided datum.
* @param {object} [datum] target telemetry datum
* @returns {string} the time
*/
ImageryController.prototype.getTime = function () {
return this.time;
ImageryController.prototype.getTime = function (datum) {
return datum ?
this.timeFormat.format(datum) :
this.time;
};
/**
* Get the URL of the image telemetry to display.
* Get the URL of the most recent image telemetry if no
* parameter is given, or of a provided datum.
* @param {object} [datum] target telemetry datum
* @returns {string} URL for telemetry image
*/
ImageryController.prototype.getImageUrl = function () {
return this.imageUrl;
ImageryController.prototype.getImageUrl = function (datum) {
return datum ?
this.imageFormat.format(datum) :
this.imageUrl;
};
/**
@ -128,15 +243,15 @@ define(
* @returns {boolean} the current state
*/
ImageryController.prototype.paused = function (state) {
if (arguments.length > 0 && state !== this.isPaused) {
this.isPaused = state;
if (this.nextDatum) {
this.updateValues(this.nextDatum);
delete this.nextDatum;
if (arguments.length > 0 && state !== this.isPaused) {
this.isPaused = state;
if (this.nextDatum) {
this.updateValues(this.nextDatum);
delete this.nextDatum;
}
}
}
return this.isPaused;
};
return this.isPaused;
};
return ImageryController;
}

View File

@ -47,7 +47,7 @@ define([
};
ImageryViewPolicy.prototype.allow = function (view, domainObject) {
if (view.key === 'imagery') {
if (view.key === 'imagery' || view.key === 'historical-imagery') {
return this.hasImageTelemetry(domainObject);
}

View File

@ -21,8 +21,14 @@
*****************************************************************************/
define(
["../../src/controllers/ImageryController"],
function (ImageryController) {
[
"zepto",
"../../src/controllers/ImageryController"
],
function ($, ImageryController) {
var MOCK_ELEMENT_TEMPLATE =
'<div class="l-image-thumbs-wrapper"></div>';
describe("The Imagery controller", function () {
var $scope,
@ -33,7 +39,9 @@ define(
metadata,
prefix,
controller,
hasLoaded;
hasLoaded,
mockWindow,
mockElement;
beforeEach(function () {
$scope = jasmine.createSpyObj('$scope', ['$on', '$watch']);
@ -42,14 +50,16 @@ define(
['getId']
);
newDomainObject = { name: 'foo' };
oldDomainObject.getId.andReturn('testID');
openmct = {
objects: jasmine.createSpyObj('objectAPI', [
'get'
]),
time: jasmine.createSpyObj('timeAPI', [
'timeSystem'
'timeSystem',
'clock',
'on',
'off'
]),
telemetry: jasmine.createSpyObj('telemetryAPI', [
'subscribe',
@ -92,13 +102,24 @@ define(
});
metadata.value.andReturn("timestamp");
metadata.valuesForHints.andReturn(["value"]);
mockElement = $(MOCK_ELEMENT_TEMPLATE);
mockWindow = jasmine.createSpyObj('$window', ['requestAnimationFrame']);
mockWindow.requestAnimationFrame.andCallFake(function (f) {
return f();
});
controller = new ImageryController($scope, openmct);
controller = new ImageryController(
$scope,
mockWindow,
mockElement,
openmct
);
});
describe("when loaded", function () {
var callback;
var callback,
boundsListener;
beforeEach(function () {
waitsFor(function () {
return hasLoaded;
@ -106,12 +127,16 @@ define(
runs(function () {
openmct.time.on.calls.forEach(function (call) {
if (call.args[0] === "bounds") {
boundsListener = call.args[1];
}
});
callback =
openmct.telemetry.subscribe.mostRecentCall.args[1];
});
});
it("uses LAD telemetry", function () {
expect(openmct.telemetry.request).toHaveBeenCalledWith(
newDomainObject,
@ -165,7 +190,14 @@ define(
);
});
it("unsubscribes when scope is destroyed", function () {
it("requests telemetry", function () {
expect(openmct.telemetry.request).toHaveBeenCalledWith(
newDomainObject,
jasmine.any(Object)
);
});
it("unsubscribes and unlistens when scope is destroyed", function () {
expect(unsubscribe).not.toHaveBeenCalled();
$scope.$on.calls.forEach(function (call) {
@ -174,6 +206,25 @@ define(
}
});
expect(unsubscribe).toHaveBeenCalled();
expect(openmct.time.off)
.toHaveBeenCalledWith('bounds', jasmine.any(Function));
});
it("listens for bounds event and responds to tick and manual change", function () {
var mockBounds = {start: 1434600000000, end: 1434600500000};
expect(openmct.time.on).toHaveBeenCalled();
openmct.telemetry.request.reset();
boundsListener(mockBounds, true);
expect(openmct.telemetry.request).not.toHaveBeenCalled();
boundsListener(mockBounds, false);
expect(openmct.telemetry.request).toHaveBeenCalledWith(newDomainObject, mockBounds);
});
it ("doesnt append duplicate datum", function () {
var mockDatum = {url: 'image/url', utc: 1434600000000};
expect(controller.updateHistory(mockDatum)).toBe(true);
expect(controller.updateHistory(mockDatum)).toBe(false);
expect(controller.updateHistory(mockDatum)).toBe(false);
});
});

View File

@ -75,6 +75,21 @@ define(
].join(""));
}
//Log an info: defaults to "no service provide by"
function info(extension, category, message) {
var msg = message || "No service provided by";
$log.info([
msg,
" ",
category,
" ",
extension.key,
" from bundle ",
(extension.bundle || { path: "unknown bundle" }).path,
"; skipping."
].join(""));
}
// Echo arguments; used to represent groups of non-built-in
// extensions as a single dependency.
function echoMany() {
@ -161,13 +176,13 @@ define(
name = makeName("aggregator", service, index);
if (!service) {
return warn(aggregator, "aggregator");
return info(aggregator, "aggregator");
}
// Aggregators need other services to aggregate, otherwise they
// do nothing.
if (!latest[service]) {
return warn(
return info(
aggregator,
"aggregator",
"No services to aggregate for"

View File

@ -195,7 +195,8 @@ define(
expect(mockApp.service).not.toHaveBeenCalled();
// Should have gotten one warning for each skipped component
expect(mockLog.warn.calls.length).toEqual(3);
expect(mockLog.warn.calls.length).toEqual(2);
expect(mockLog.info.calls.length).toEqual(1);
});
it("warns about and skips aggregators with zero providers", function () {
@ -217,7 +218,7 @@ define(
expect(mockApp.service).not.toHaveBeenCalled();
// Should have gotten a warning
expect(mockLog.warn).toHaveBeenCalled();
expect(mockLog.info).toHaveBeenCalled();
});
it("warns about and skips decorators with nothing to decorate", function () {

View File

@ -115,9 +115,7 @@ define(
return (this.telemetryService =
$injector.get("telemetryService"));
} catch (e) {
// $injector should throw if telemetryService
// is unavailable or unsatisfiable.
$log.warn("Telemetry service unavailable");
$log.info("Telemetry service unavailable");
return (this.telemetryService = null);
}
};
@ -314,4 +312,3 @@ define(
return TelemetryCapability;
}
);

View File

@ -199,7 +199,7 @@ define(
telemetry.requestData();
expect(mockLog.warn).toHaveBeenCalled();
expect(mockLog.info).toHaveBeenCalled();
});
it("if a new style telemetry source is available, use it", function () {

View File

@ -20,7 +20,11 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([], function () {
define([
'lodash'
], function (
_
) {
// Parameter names in query string
var SEARCH = {
MODE: 'tc.mode',
@ -73,43 +77,72 @@ define([], function () {
this.$location.search(SEARCH.END_DELTA, deltas.end);
};
TimeSettingsURLHandler.prototype.parseQueryParams = function () {
var searchParams = _.pick(this.$location.search(), _.values(SEARCH));
var parsedParams = {
clock: searchParams[SEARCH.MODE],
timeSystem: searchParams[SEARCH.TIME_SYSTEM]
};
if (!isNaN(parseInt(searchParams[SEARCH.START_DELTA], 0xA)) &&
!isNaN(parseInt(searchParams[SEARCH.END_DELTA], 0xA))) {
parsedParams.clockOffsets = {
start: -searchParams[SEARCH.START_DELTA],
end: +searchParams[SEARCH.END_DELTA]
};
}
if (!isNaN(parseInt(searchParams[SEARCH.START_BOUND], 0xA)) &&
!isNaN(parseInt(searchParams[SEARCH.END_BOUND], 0xA))) {
parsedParams.bounds = {
start: +searchParams[SEARCH.START_BOUND],
end: +searchParams[SEARCH.END_BOUND]
};
}
return parsedParams;
};
TimeSettingsURLHandler.prototype.updateTime = function () {
var searchParams = this.$location.search();
var mode = searchParams[SEARCH.MODE];
var timeSystem = searchParams[SEARCH.TIME_SYSTEM];
var clockOffsets = {
start: -searchParams[SEARCH.START_DELTA],
end: +searchParams[SEARCH.END_DELTA]
};
var bounds = {
start: +searchParams[SEARCH.START_BOUND],
end: +searchParams[SEARCH.END_BOUND]
};
var fixed = (mode === 'fixed');
var clock = fixed ? undefined : mode;
var hasDeltas =
!isNaN(parseInt(searchParams[SEARCH.START_DELTA], 0xA)) &&
!isNaN(parseInt(searchParams[SEARCH.END_DELTA], 0xA));
var hasBounds =
!isNaN(parseInt(searchParams[SEARCH.START_BOUND], 0xA)) &&
!isNaN(parseInt(searchParams[SEARCH.END_BOUND], 0xA));
if (fixed && timeSystem && hasBounds) {
this.time.timeSystem(timeSystem, bounds);
this.time.stopClock();
var params = this.parseQueryParams();
if (_.isEqual(params, this.last)) {
return; // Do nothing;
}
this.last = params;
if (!fixed && clock && hasDeltas) {
this.time.clock(clock, clockOffsets);
this.time.timeSystem(timeSystem);
}
if (!params.timeSystem) {
this.updateQueryParams();
} else if (params.clock === 'fixed' && params.bounds) {
if (!this.time.timeSystem() ||
this.time.timeSystem().key !== params.timeSystem) {
if (hasDeltas && !fixed) {
this.time.clockOffsets(clockOffsets);
}
this.time.timeSystem(
params.timeSystem,
params.bounds
);
} else if (!_.isEqual(this.time.bounds(), params.bounds)) {
this.time.bounds(params.bounds);
}
if (this.time.clock()) {
this.time.stopClock();
}
} else if (params.clockOffsets) {
if (params.clock === 'fixed') {
this.time.stopClock();
return;
}
if (!this.time.clock() ||
this.time.clock().key !== params.clock) {
if (hasBounds && fixed) {
this.time.bounds(bounds);
this.time.clock(params.clock, params.clockOffsets);
} else if (!_.isEqual(this.time.clockOffsets(), params.clockOffsets)) {
this.time.clockOffsets(params.clockOffsets);
}
if (!this.time.timeSystem() ||
this.time.timeSystem().key !== params.timeSystem) {
this.time.timeSystem(params.timeSystem);
}
} else {
// Neither found, update from timeSystem.
this.updateQueryParams();
}
};

View File

@ -20,23 +20,78 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(['./TimeSettingsURLHandler'], function (TimeSettingsURLHandler) {
define([
'./TimeSettingsURLHandler',
'../../api/time/TimeAPI'
], function (
TimeSettingsURLHandler,
TimeAPI
) {
describe("TimeSettingsURLHandler", function () {
var time;
var $location;
var $rootScope;
var search;
var handler;
var clockA;
var clockB;
var timeSystemA;
var timeSystemB;
var boundsA;
var boundsB;
var offsetsA;
var offsetsB;
var initialize;
var triggerLocationChange;
beforeEach(function () {
time = jasmine.createSpyObj('time', [
clockA = jasmine.createSpyObj('clockA', ['on', 'off']);
clockA.key = 'clockA';
clockA.currentValue = function () {
return 1000;
};
clockB = jasmine.createSpyObj('clockB', ['on', 'off']);
clockB.key = 'clockB';
clockB.currentValue = function () {
return 2000;
};
timeSystemA = {key: 'timeSystemA'};
timeSystemB = {key: 'timeSystemB'};
boundsA = {
start: 10,
end: 20
};
boundsB = {
start: 120,
end: 360
};
offsetsA = {
start: -100,
end: 0
};
offsetsB = {
start: -50,
end: 50
};
time = new TimeAPI();
[
'on',
'bounds',
'clockOffsets',
'timeSystem',
'clock',
'stopClock'
]);
].forEach(function (method) {
spyOn(time, method).andCallThrough();
});
time.addTimeSystem(timeSystemA);
time.addTimeSystem(timeSystemB);
time.addClock(clockA);
time.addClock(clockB);
$location = jasmine.createSpyObj('$location', [
'search'
]);
@ -44,8 +99,6 @@ define(['./TimeSettingsURLHandler'], function (TimeSettingsURLHandler) {
'$on'
]);
time.timeSystem.andReturn({ key: 'test-time-system' });
search = {};
$location.search.andCallFake(function (key, value) {
if (arguments.length === 0) {
@ -59,142 +112,465 @@ define(['./TimeSettingsURLHandler'], function (TimeSettingsURLHandler) {
return this;
});
handler = new TimeSettingsURLHandler(
time,
$location,
$rootScope
expect(time.timeSystem()).toBeUndefined();
expect(time.bounds()).toEqual({});
expect(time.clockOffsets()).toBeUndefined();
expect(time.clock()).toBeUndefined();
initialize = function () {
handler = new TimeSettingsURLHandler(
time,
$location,
$rootScope
);
expect($rootScope.$on).toHaveBeenCalledWith(
'$locationChangeSuccess',
jasmine.any(Function)
);
triggerLocationChange = $rootScope.$on.mostRecentCall.args[1];
};
});
it("initializes with missing time system", function () {
// This handles an odd transitory case where a url does not include
// a timeSystem. It's generally only experienced by those who
// based their code on the tutorial before it specified a time
// system.
search['tc.mode'] = 'clockA';
search['tc.timeSystem'] = undefined;
search['tc.startDelta'] = '123';
search['tc.endDelta'] = '456';
// We don't specify behavior right now other than "don't break."
expect(initialize).not.toThrow();
});
it("can initalize fixed mode from location", function () {
search['tc.mode'] = 'fixed';
search['tc.timeSystem'] = 'timeSystemA';
search['tc.startBound'] = '123';
search['tc.endBound'] = '456';
initialize();
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemA',
{
start: 123,
end: 456
}
);
});
['bounds', 'timeSystem', 'clock', 'clockOffsets'].forEach(function (event) {
it("listens for " + event + " time events", function () {
expect(time.on)
.toHaveBeenCalledWith(event, jasmine.any(Function));
});
it("can initialize clock mode from location", function () {
search['tc.mode'] = 'clockA';
search['tc.timeSystem'] = 'timeSystemA';
search['tc.startDelta'] = '123';
search['tc.endDelta'] = '456';
describe("when " + event + " time event occurs with no clock", function () {
var expected;
initialize();
beforeEach(function () {
expected = {
'tc.mode': 'fixed',
'tc.timeSystem': 'test-time-system',
'tc.startBound': '123',
'tc.endBound': '456'
};
time.clock.andReturn(undefined);
time.bounds.andReturn({ start: 123, end: 456 });
time.on.calls.forEach(function (call) {
if (call.args[0] === event) {
call.args[1]();
}
});
});
it("updates query parameters for fixed mode", function () {
expect(search).toEqual(expected);
});
});
describe("when " + event + " time event occurs with no time system", function () {
beforeEach(function () {
time.timeSystem.andReturn(undefined);
time.on.calls.forEach(function (call) {
if (call.args[0] === event) {
call.args[1]();
}
});
});
it("clears the time system from the URL", function () {
expect(search['tc.timeSystem']).toBeUndefined();
});
});
describe("when " + event + " time event occurs with a clock", function () {
var expected;
beforeEach(function () {
expected = {
'tc.mode': 'clocky',
'tc.timeSystem': 'test-time-system',
'tc.startDelta': '123',
'tc.endDelta': '456'
};
time.clock.andReturn({ key: 'clocky' });
time.clockOffsets.andReturn({ start: -123, end: 456 });
time.on.calls.forEach(function (call) {
if (call.args[0] === event) {
call.args[1]();
}
});
});
it("updates query parameters for realtime mode", function () {
expect(search).toEqual(expected);
});
});
});
it("listens for location changes", function () {
expect($rootScope.$on)
.toHaveBeenCalledWith('$locationChangeSuccess', jasmine.any(Function));
});
[false, true].forEach(function (fixed) {
var name = fixed ? "fixed-time" : "real-time";
var suffix = fixed ? 'Bound' : 'Delta';
describe("when " + name + " location changes occur", function () {
beforeEach(function () {
search['tc.mode'] = fixed ? 'fixed' : 'clocky';
search['tc.timeSystem'] = 'some-time-system';
search['tc.start' + suffix] = '12321';
search['tc.end' + suffix] = '32123';
$rootScope.$on.mostRecentCall.args[1]();
});
if (fixed) {
var bounds = { start: 12321, end: 32123 };
it("stops the clock", function () {
expect(time.stopClock).toHaveBeenCalled();
});
it("sets the bounds", function () {
expect(time.bounds).toHaveBeenCalledWith(bounds);
});
it("sets the time system with bounds", function () {
expect(time.timeSystem).toHaveBeenCalledWith(
search['tc.timeSystem'],
bounds
);
});
} else {
var clockOffsets = { start: -12321, end: 32123 };
it("sets the clock", function () {
expect(time.stopClock).not.toHaveBeenCalled();
expect(time.clock).toHaveBeenCalledWith(
search['tc.mode'],
clockOffsets
);
});
it("sets clock offsets", function () {
expect(time.clockOffsets)
.toHaveBeenCalledWith(clockOffsets);
});
it("sets the time system without bounds", function () {
expect(time.timeSystem).toHaveBeenCalledWith(
search['tc.timeSystem']
);
});
expect(time.clock).toHaveBeenCalledWith(
'clockA',
{
start: -123,
end: 456
}
);
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemA'
);
});
it("can initialize fixed mode from time API", function () {
time.timeSystem(timeSystemA.key, boundsA);
initialize();
expect($location.search)
.toHaveBeenCalledWith('tc.mode', 'fixed');
expect($location.search)
.toHaveBeenCalledWith('tc.timeSystem', 'timeSystemA');
expect($location.search)
.toHaveBeenCalledWith('tc.startBound', 10);
expect($location.search)
.toHaveBeenCalledWith('tc.endBound', 20);
expect($location.search)
.toHaveBeenCalledWith('tc.startDelta', null);
expect($location.search)
.toHaveBeenCalledWith('tc.endDelta', null);
});
it("can initialize clock mode from time API", function () {
time.clock(clockA.key, offsetsA);
time.timeSystem(timeSystemA.key);
initialize();
expect($location.search)
.toHaveBeenCalledWith('tc.mode', 'clockA');
expect($location.search)
.toHaveBeenCalledWith('tc.timeSystem', 'timeSystemA');
expect($location.search)
.toHaveBeenCalledWith('tc.startBound', null);
expect($location.search)
.toHaveBeenCalledWith('tc.endBound', null);
expect($location.search)
.toHaveBeenCalledWith('tc.startDelta', 100);
expect($location.search)
.toHaveBeenCalledWith('tc.endDelta', 0);
});
describe('location changes in fixed mode', function () {
beforeEach(function () {
time.timeSystem(timeSystemA.key, boundsA);
initialize();
time.timeSystem.reset();
time.bounds.reset();
time.clock.reset();
time.stopClock.reset();
});
it("does not change on spurious location change", function () {
triggerLocationChange();
expect(time.timeSystem).not.toHaveBeenCalledWith(
'timeSystemA',
jasmine.any(Object)
);
expect(time.bounds).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
expect(time.stopClock).not.toHaveBeenCalled();
});
it("updates timeSystem changes", function () {
search['tc.timeSystem'] = 'timeSystemB';
triggerLocationChange();
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemB',
{
start: 10,
end: 20
}
);
});
it("updates bounds changes", function () {
search['tc.startBound'] = '100';
search['tc.endBound'] = '200';
triggerLocationChange();
expect(time.timeSystem).not.toHaveBeenCalledWith(
jasmine.any(), jasmine.any()
);
expect(time.bounds).toHaveBeenCalledWith({
start: 100,
end: 200
});
search['tc.endBound'] = '300';
triggerLocationChange();
expect(time.timeSystem).not.toHaveBeenCalledWith(
jasmine.any(), jasmine.any()
);
expect(time.bounds).toHaveBeenCalledWith({
start: 100,
end: 300
});
});
it("updates clock mode w/o timeSystem change", function () {
search['tc.mode'] = 'clockA';
search['tc.startDelta'] = '50';
search['tc.endDelta'] = '50';
delete search['tc.endBound'];
delete search['tc.startBound'];
triggerLocationChange();
expect(time.clock).toHaveBeenCalledWith(
'clockA',
{
start: -50,
end: 50
}
);
expect(time.timeSystem).not.toHaveBeenCalledWith(
jasmine.any(), jasmine.any()
);
});
it("updates clock mode and timeSystem", function () {
search['tc.mode'] = 'clockA';
search['tc.startDelta'] = '50';
search['tc.endDelta'] = '50';
search['tc.timeSystem'] = 'timeSystemB';
delete search['tc.endBound'];
delete search['tc.startBound'];
triggerLocationChange();
expect(time.clock).toHaveBeenCalledWith(
'clockA',
{
start: -50,
end: 50
}
);
expect(time.timeSystem).toHaveBeenCalledWith('timeSystemB');
});
});
describe('location changes in clock mode', function () {
beforeEach(function () {
time.clock(clockA.key, offsetsA);
time.timeSystem(timeSystemA.key);
initialize();
time.timeSystem.reset();
time.bounds.reset();
time.clock.reset();
time.clockOffsets.reset();
time.stopClock.reset();
});
it("does not change on spurious location change", function () {
triggerLocationChange();
expect(time.timeSystem).not.toHaveBeenCalledWith(
'timeSystemA',
jasmine.any(Object)
);
expect(time.clockOffsets).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
expect(time.clock).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
expect(time.bounds).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
});
it("changes time system", function () {
search['tc.timeSystem'] = 'timeSystemB';
triggerLocationChange();
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemB'
);
expect(time.clockOffsets).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
expect(time.clock).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
expect(time.stopClock).not.toHaveBeenCalled();
expect(time.bounds).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
});
it("changes offsets", function () {
search['tc.startDelta'] = '50';
search['tc.endDelta'] = '50';
triggerLocationChange();
expect(time.timeSystem).not.toHaveBeenCalledWith(
'timeSystemA',
jasmine.any(Object)
);
expect(time.clockOffsets).toHaveBeenCalledWith(
{
start: -50,
end: 50
}
);
expect(time.clock).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
});
it("updates to fixed w/o timeSystem change", function () {
search['tc.mode'] = 'fixed';
search['tc.startBound'] = '234';
search['tc.endBound'] = '567';
delete search['tc.endDelta'];
delete search['tc.startDelta'];
triggerLocationChange();
expect(time.stopClock).toHaveBeenCalled();
expect(time.bounds).toHaveBeenCalledWith({
start: 234,
end: 567
});
expect(time.timeSystem).not.toHaveBeenCalledWith(
jasmine.any(), jasmine.any()
);
});
it("updates fixed and timeSystem", function () {
search['tc.mode'] = 'fixed';
search['tc.startBound'] = '234';
search['tc.endBound'] = '567';
search['tc.timeSystem'] = 'timeSystemB';
delete search['tc.endDelta'];
delete search['tc.startDelta'];
triggerLocationChange();
expect(time.stopClock).toHaveBeenCalled();
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemB',
{
start: 234,
end: 567
}
);
});
it("updates clock", function () {
search['tc.mode'] = 'clockB';
triggerLocationChange();
expect(time.clock).toHaveBeenCalledWith(
'clockB',
{
start: -100,
end: 0
}
);
expect(time.timeSystem).not.toHaveBeenCalledWith(jasmine.any());
});
it("updates clock and timeSystem", function () {
search['tc.mode'] = 'clockB';
search['tc.timeSystem'] = 'timeSystemB';
triggerLocationChange();
expect(time.clock).toHaveBeenCalledWith(
'clockB',
{
start: -100,
end: 0
}
);
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemB'
);
});
it("updates clock and timeSystem and offsets", function () {
search['tc.mode'] = 'clockB';
search['tc.timeSystem'] = 'timeSystemB';
search['tc.startDelta'] = '50';
search['tc.endDelta'] = '50';
triggerLocationChange();
expect(time.clock).toHaveBeenCalledWith(
'clockB',
{
start: -50,
end: 50
}
);
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemB'
);
});
it("stops the clock", function () {
// this is a robustness test, unsure if desired, requires
// user to be manually editing location strings.
search['tc.mode'] = 'fixed';
triggerLocationChange();
expect(time.stopClock).toHaveBeenCalled();
});
});
describe("location updates from time API in fixed", function () {
beforeEach(function () {
time.timeSystem(timeSystemA.key, boundsA);
initialize();
});
it("updates on bounds change", function () {
time.bounds(boundsB);
expect(search).toEqual({
'tc.mode': 'fixed',
'tc.startBound': '120',
'tc.endBound': '360',
'tc.timeSystem': 'timeSystemA'
});
});
it("updates on timeSystem change", function () {
time.timeSystem(timeSystemB, boundsA);
expect(search).toEqual({
'tc.mode': 'fixed',
'tc.startBound': '10',
'tc.endBound': '20',
'tc.timeSystem': 'timeSystemB'
});
time.timeSystem(timeSystemA, boundsB);
expect(search).toEqual({
'tc.mode': 'fixed',
'tc.startBound': '120',
'tc.endBound': '360',
'tc.timeSystem': 'timeSystemA'
});
});
it("Updates to clock", function () {
time.clock(clockA, offsetsA);
expect(search).toEqual({
'tc.mode': 'clockA',
'tc.startDelta': '100',
'tc.endDelta': '0',
'tc.timeSystem': 'timeSystemA'
});
});
});
describe("location updates from time API in fixed", function () {
beforeEach(function () {
time.clock(clockA.key, offsetsA);
time.timeSystem(timeSystemA.key);
initialize();
});
it("updates offsets", function () {
time.clockOffsets(offsetsB);
expect(search).toEqual({
'tc.mode': 'clockA',
'tc.startDelta': '50',
'tc.endDelta': '50',
'tc.timeSystem': 'timeSystemA'
});
});
it("updates clocks", function () {
time.clock(clockB, offsetsA);
expect(search).toEqual({
'tc.mode': 'clockB',
'tc.startDelta': '100',
'tc.endDelta': '0',
'tc.timeSystem': 'timeSystemA'
});
time.clock(clockA, offsetsB);
expect(search).toEqual({
'tc.mode': 'clockA',
'tc.startDelta': '50',
'tc.endDelta': '50',
'tc.timeSystem': 'timeSystemA'
});
});
it("updates timesystems", function () {
time.timeSystem(timeSystemB);
expect(search).toEqual({
'tc.mode': 'clockA',
'tc.startDelta': '100',
'tc.endDelta': '0',
'tc.timeSystem': 'timeSystemB'
});
});
it("stops the clock", function () {
time.stopClock();
expect(search).toEqual({
'tc.mode': 'fixed',
'tc.startBound': '900',
'tc.endBound': '1000',
'tc.timeSystem': 'timeSystemA'
});
});
});
});
});