Merge pull request #1325 from nasa/open1231

[Time Conductor] Persist Time Conductor state in URL
This commit is contained in:
Victor Woeltjen 2016-11-30 13:17:33 -08:00 committed by GitHub
commit f85595665f
6 changed files with 290 additions and 41 deletions

View File

@ -67,6 +67,7 @@ define([
"depends": [
"$scope",
"$window",
"$location",
"openmct",
"timeConductorViewService",
"timeSystems[]",

View File

@ -12,7 +12,7 @@
<!-- Holds inputs and ticks -->
<div class="l-time-conductor-inputs-and-ticks l-row-elem flex-elem no-margin">
<form class="l-time-conductor-inputs-holder"
ng-submit="tcController.updateBoundsFromForm(boundsModel)">
ng-submit="tcController.setBounds(boundsModel)">
<span class="l-time-range-w start-w">
<span class="l-time-conductor-inputs">
<span class="l-time-range-input-w start-date">
@ -23,7 +23,7 @@
validate: tcController.validation.validateStart
}"
ng-model="boundsModel"
ng-blur="tcController.updateBoundsFromForm(boundsModel)"
ng-blur="tcController.setBounds(boundsModel)"
field="'start'"
class="time-range-input">
</mct-control>
@ -37,7 +37,7 @@
validate: tcController.validation.validateStartDelta
}"
ng-model="boundsModel"
ng-blur="tcController.updateDeltasFromForm(boundsModel)"
ng-blur="tcController.setDeltas(boundsModel)"
field="'startDelta'"
class="hrs-min-input">
</mct-control>
@ -55,7 +55,7 @@
validate: tcController.validation.validateEnd
}"
ng-model="boundsModel"
ng-blur="tcController.updateBoundsFromForm(boundsModel)"
ng-blur="tcController.setBounds(boundsModel)"
ng-disabled="modeModel.selectedKey !== 'fixed'"
field="'end'"
class="time-range-input">
@ -70,7 +70,7 @@
validate: tcController.validation.validateEndDelta
}"
ng-model="boundsModel"
ng-blur="tcController.updateDeltasFromForm(boundsModel)"
ng-blur="tcController.setDeltas(boundsModel)"
field="'endDelta'"
class="hrs-min-input">
</mct-control>

View File

@ -25,6 +25,14 @@ define(
'./TimeConductorValidation'
],
function (TimeConductorValidation) {
var SEARCH = {
MODE: 'tc.mode',
TIME_SYSTEM: 'tc.timeSystem',
START_BOUND: 'tc.startBound',
END_BOUND: 'tc.endBound',
START_DELTA: 'tc.startDelta',
END_DELTA: 'tc.endDelta'
};
/**
* Controller for the Time Conductor UI element. The Time Conductor includes form fields for specifying time
@ -32,7 +40,7 @@ define(
* @memberof platform.features.conductor
* @constructor
*/
function TimeConductorController($scope, $window, openmct, conductorViewService, timeSystems, formatService) {
function TimeConductorController($scope, $window, $location, openmct, conductorViewService, timeSystems, formatService) {
var self = this;
@ -45,6 +53,7 @@ define(
this.$scope = $scope;
this.$window = $window;
this.$location = $location;
this.conductorViewService = conductorViewService;
this.conductor = openmct.conductor;
this.modes = conductorViewService.availableModes();
@ -56,16 +65,35 @@ define(
return timeSystemConstructor();
});
//Set the initial state of the view based on current time conductor
this.initializeScope();
var searchParams = JSON.parse(JSON.stringify(this.$location.search()));
//Set bounds, time systems, deltas, on conductor from URL
this.setStateFromSearchParams(searchParams);
//Set the initial state of the UI from the conductor state
var timeSystem = this.conductor.timeSystem();
if (timeSystem) {
this.changeTimeSystem(this.conductor.timeSystem());
}
var deltas = this.conductorViewService.deltas();
if (deltas) {
this.setFormFromDeltas(deltas);
}
var bounds = this.conductor.bounds();
if (bounds && bounds.start !== undefined && bounds.end !== undefined) {
this.changeBounds(bounds);
}
//Listen for changes to URL and update state if necessary
this.$scope.$on('$routeUpdate', function () {
this.setStateFromSearchParams(this.$location.search());
}.bind(this));
//Respond to any subsequent conductor changes
this.conductor.on('bounds', this.changeBounds);
this.conductor.on('timeSystem', this.changeTimeSystem);
// If no mode selected, select fixed as the default
if (!this.conductorViewService.mode()) {
this.setMode('fixed');
}
}
/**
@ -78,10 +106,6 @@ define(
//If conductor has a time system selected already, populate the
//form from it
this.$scope.timeSystemModel = {};
var timeSystem = this.conductor.timeSystem();
if (timeSystem) {
this.setFormFromTimeSystem(timeSystem);
}
//Represents the various modes, and the currently selected mode
//in the view
@ -89,20 +113,6 @@ define(
options: this.conductorViewService.availableModes()
};
var mode = this.conductorViewService.mode();
if (mode) {
//If view already defines a mode (eg. controller is being
// initialized after navigation), then pre-populate form.
this.setFormFromMode(mode);
var deltas = this.conductorViewService.deltas();
if (deltas) {
this.setFormFromDeltas(deltas);
}
}
this.setFormFromBounds(this.conductor.bounds());
// Watch scope for selection of mode or time system by user
this.$scope.$watch('modeModel.selectedKey', this.setMode);
@ -112,6 +122,47 @@ define(
this.$scope.$on('$destroy', this.destroy);
};
TimeConductorController.prototype.setStateFromSearchParams = function (searchParams) {
//Set mode from url if changed
if (searchParams[SEARCH.MODE] === undefined ||
searchParams[SEARCH.MODE] !== this.$scope.modeModel.selectedKey) {
this.setMode(searchParams[SEARCH.MODE] || "fixed");
}
if (searchParams[SEARCH.TIME_SYSTEM] &&
searchParams[SEARCH.TIME_SYSTEM] !== this.conductor.timeSystem().metadata.key) {
//Will select the specified time system on the conductor
this.selectTimeSystemByKey(searchParams[SEARCH.TIME_SYSTEM]);
}
var validDeltas = searchParams[SEARCH.MODE] !== 'fixed' &&
searchParams[SEARCH.START_DELTA] &&
searchParams[SEARCH.END_DELTA] &&
!isNaN(searchParams[SEARCH.START_DELTA]) &&
!isNaN(searchParams[SEARCH.END_DELTA]);
if (validDeltas) {
//Sets deltas from some form model
this.setDeltas({
startDelta: parseInt(searchParams[SEARCH.START_DELTA]),
endDelta: parseInt(searchParams[SEARCH.END_DELTA])
});
}
var validBounds = searchParams[SEARCH.MODE] === 'fixed' &&
searchParams[SEARCH.START_BOUND] &&
searchParams[SEARCH.END_BOUND] &&
!isNaN(searchParams[SEARCH.START_BOUND]) &&
!isNaN(searchParams[SEARCH.END_BOUND]);
if (validBounds) {
this.conductor.bounds({
start: parseInt(searchParams[SEARCH.START_BOUND]),
end: parseInt(searchParams[SEARCH.END_BOUND])
});
}
};
/**
* @private
*/
@ -132,6 +183,11 @@ define(
//If a zoom or pan is currently in progress, do not override form values.
if (!this.zooming && !this.panning) {
this.setFormFromBounds(bounds);
if (this.conductorViewService.mode() === 'fixed') {
//Set bounds in URL on change
this.$location.search(SEARCH.START_BOUND, bounds.start);
this.$location.search(SEARCH.END_BOUND, bounds.end);
}
}
};
@ -217,11 +273,10 @@ define(
};
/**
* Called when form values are changed. Synchronizes the form with
* the time conductor
* Called when form values are changed.
* @param formModel
*/
TimeConductorController.prototype.updateBoundsFromForm = function (boundsModel) {
TimeConductorController.prototype.setBounds = function (boundsModel) {
this.conductor.bounds({
start: boundsModel.start,
end: boundsModel.end
@ -234,7 +289,7 @@ define(
* @param boundsModel
* @see TimeConductorMode
*/
TimeConductorController.prototype.updateDeltasFromForm = function (boundsFormModel) {
TimeConductorController.prototype.setDeltas = function (boundsFormModel) {
var deltas = {
start: boundsFormModel.startDelta,
end: boundsFormModel.endDelta
@ -242,6 +297,10 @@ define(
if (this.validation.validateStartDelta(deltas.start) && this.validation.validateEndDelta(deltas.end)) {
//Sychronize deltas between form and mode
this.conductorViewService.deltas(deltas);
//Set Deltas in URL on change
this.$location.search(SEARCH.START_DELTA, deltas.start);
this.$location.search(SEARCH.END_DELTA, deltas.end);
}
};
@ -255,9 +314,26 @@ define(
* @param oldModeKey
*/
TimeConductorController.prototype.setMode = function (newModeKey, oldModeKey) {
//Set mode in URL on change
this.$location.search(SEARCH.MODE, newModeKey);
if (newModeKey !== oldModeKey) {
this.conductorViewService.mode(newModeKey);
this.setFormFromMode(newModeKey);
if (newModeKey === "fixed") {
this.$location.search(SEARCH.START_DELTA, null);
this.$location.search(SEARCH.END_DELTA, null);
} else {
this.$location.search(SEARCH.START_BOUND, null);
this.$location.search(SEARCH.END_BOUND, null);
var deltas = this.conductorViewService.deltas();
if (deltas) {
this.$location.search(SEARCH.START_DELTA, deltas.start);
this.$location.search(SEARCH.END_DELTA, deltas.end);
}
}
}
};
@ -286,8 +362,12 @@ define(
* @param newTimeSystem
*/
TimeConductorController.prototype.changeTimeSystem = function (newTimeSystem) {
//Set time system in URL on change
this.$location.search(SEARCH.TIME_SYSTEM, newTimeSystem.metadata.key);
if (newTimeSystem && (newTimeSystem !== this.$scope.timeSystemModel.selected)) {
this.setFormFromTimeSystem(newTimeSystem);
if (newTimeSystem.defaults()) {
var deltas = newTimeSystem.defaults().deltas || {start: 0, end: 0};
var bounds = newTimeSystem.defaults().bounds || {start: 0, end: 0};
@ -357,8 +437,8 @@ define(
* @fires platform.features.conductor.TimeConductorController~zoomStop
*/
TimeConductorController.prototype.onZoomStop = function () {
this.updateBoundsFromForm(this.$scope.boundsModel);
this.updateDeltasFromForm(this.$scope.boundsModel);
this.setBounds(this.$scope.boundsModel);
this.setDeltas(this.$scope.boundsModel);
this.zooming = false;
this.conductorViewService.emit('zoom-stop');

View File

@ -30,6 +30,7 @@ define(['./TimeConductorController'], function (TimeConductorController) {
var controller;
var mockFormatService;
var mockFormat;
var mockLocation;
beforeEach(function () {
mockScope = jasmine.createSpyObj("$scope", [
@ -70,6 +71,10 @@ define(['./TimeConductorController'], function (TimeConductorController) {
'format'
]);
mockFormatService.getFormat.andReturn(mockFormat);
mockLocation = jasmine.createSpyObj('location', [
'search'
]);
mockLocation.search.andReturn({});
mockTimeSystems = [];
});
@ -104,6 +109,9 @@ define(['./TimeConductorController'], function (TimeConductorController) {
bounds: defaultBounds
};
timeSystem = {
metadata: {
key: 'mock'
},
formats: function () {
return [mockFormat];
},
@ -118,6 +126,7 @@ define(['./TimeConductorController'], function (TimeConductorController) {
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
mockTimeSystems,
@ -255,6 +264,7 @@ define(['./TimeConductorController'], function (TimeConductorController) {
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
mockTimeSystemConstructors,
@ -271,6 +281,7 @@ define(['./TimeConductorController'], function (TimeConductorController) {
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
mockTimeSystemConstructors,
@ -292,17 +303,17 @@ define(['./TimeConductorController'], function (TimeConductorController) {
end: 10
};
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
mockTimeSystemConstructors,
mockFormatService
);
controller.updateBoundsFromForm(formModel);
controller.setBounds(formModel);
expect(mockTimeConductor.bounds).toHaveBeenCalledWith(formModel);
});
@ -319,13 +330,14 @@ define(['./TimeConductorController'], function (TimeConductorController) {
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
mockTimeSystemConstructors,
mockFormatService
);
controller.updateDeltasFromForm(formModel);
controller.setDeltas(formModel);
expect(mockConductorViewService.deltas).toHaveBeenCalledWith(deltas);
});
@ -355,6 +367,7 @@ define(['./TimeConductorController'], function (TimeConductorController) {
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
mockTimeSystems,
@ -384,5 +397,147 @@ define(['./TimeConductorController'], function (TimeConductorController) {
});
});
describe("when the URL defines conductor state", function () {
var urlBounds;
var urlTimeSystem;
var urlMode;
var urlDeltas;
var mockDeltaFormat;
var defaultBounds;
var defaultDeltas;
var mockDefaults;
var timeSystem;
var otherTimeSystem;
var mockSearchObject;
beforeEach(function () {
mockFormat = {};
mockDeltaFormat = {};
defaultBounds = {
start: 2,
end: 3
};
defaultDeltas = {
start: 10,
end: 20
};
mockDefaults = {
deltas: defaultDeltas,
bounds: defaultBounds
};
timeSystem = {
metadata: {
key: 'mockTimeSystem'
},
formats: function () {
return [mockFormat];
},
deltaFormat: function () {
return mockDeltaFormat;
},
defaults: function () {
return mockDefaults;
}
};
otherTimeSystem = {
metadata: {
key: 'otherTimeSystem'
},
formats: function () {
return [mockFormat];
},
deltaFormat: function () {
return mockDeltaFormat;
},
defaults: function () {
return mockDefaults;
}
};
mockTimeSystems.push(function () {
return timeSystem;
});
mockTimeSystems.push(function () {
return otherTimeSystem;
});
urlBounds = {
start: 100,
end: 200
};
urlTimeSystem = "otherTimeSystem";
urlMode = "realtime";
urlDeltas = {
start: 300,
end: 400
};
mockSearchObject = {
"tc.startBound": urlBounds.start,
"tc.endBound": urlBounds.end,
"tc.startDelta": urlDeltas.start,
"tc.endDelta": urlDeltas.end,
"tc.timeSystem": urlTimeSystem
};
mockLocation.search.andReturn(mockSearchObject);
mockTimeConductor.timeSystem.andReturn(timeSystem);
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
mockTimeSystems,
mockFormatService
);
spyOn(controller, "setMode");
spyOn(controller, "selectTimeSystemByKey");
});
it("sets conductor state from URL", function () {
mockSearchObject["tc.mode"] = "fixed";
controller.setStateFromSearchParams(mockSearchObject);
expect(controller.selectTimeSystemByKey).toHaveBeenCalledWith("otherTimeSystem");
expect(mockTimeConductor.bounds).toHaveBeenCalledWith(urlBounds);
});
it("sets mode from URL", function () {
mockTimeConductor.bounds.reset();
mockSearchObject["tc.mode"] = "realtime";
controller.setStateFromSearchParams(mockSearchObject);
expect(controller.setMode).toHaveBeenCalledWith("realtime");
expect(controller.selectTimeSystemByKey).toHaveBeenCalledWith("otherTimeSystem");
expect(mockConductorViewService.deltas).toHaveBeenCalledWith(urlDeltas);
expect(mockTimeConductor.bounds).not.toHaveBeenCalled();
});
describe("when conductor state changes", function () {
it("updates the URL with the mode", function () {
controller.setMode("realtime", "fixed");
expect(mockLocation.search).toHaveBeenCalledWith("tc.mode", "fixed");
});
it("updates the URL with the bounds", function () {
mockConductorViewService.mode.andReturn("fixed");
controller.changeBounds({start: 500, end: 600});
expect(mockLocation.search).toHaveBeenCalledWith("tc.startBound", 500);
expect(mockLocation.search).toHaveBeenCalledWith("tc.endBound", 600);
});
it("updates the URL with the deltas", function () {
controller.setDeltas({startDelta: 700, endDelta: 800});
expect(mockLocation.search).toHaveBeenCalledWith("tc.startDelta", 700);
expect(mockLocation.search).toHaveBeenCalledWith("tc.endDelta", 800);
});
it("updates the URL with the time system", function () {
controller.changeTimeSystem(otherTimeSystem);
expect(mockLocation.search).toHaveBeenCalledWith("tc.timeSystem", "otherTimeSystem");
});
});
});
});
});

View File

@ -254,7 +254,9 @@ define(
} else {
var domainAxis = $scope.axes[0];
domainAxis.chooseOption(bounds.domain);
if (bounds.domain) {
domainAxis.chooseOption(bounds.domain);
}
updateDomainFormat();
setBasePanZoom(bounds);
requery();
@ -283,6 +285,14 @@ define(
new PlotAxis("ranges", [], AXIS_DEFAULTS[1])
];
//Are some initialized bounds defined?
var bounds = conductor.bounds();
if (bounds &&
bounds.start !== undefined &&
bounds.end !== undefined) {
changeDisplayBounds(undefined, conductor.bounds(), conductor.follow());
}
// Watch for changes to the selected axis
$scope.$watch("axes[0].active.key", domainRequery);
$scope.$watch("axes[1].active.key", rangeRequery);

View File

@ -126,9 +126,12 @@ define(
'off',
'bounds',
'timeSystem',
'timeOfInterest'
'timeOfInterest',
'follow'
]);
mockConductor.bounds.andReturn({});
controller = new PlotController(
mockScope,
mockElement,