diff --git a/platform/features/conductor-v2/conductor/bundle.js b/platform/features/conductor-v2/conductor/bundle.js index 506c0f52ba..d701142fdd 100644 --- a/platform/features/conductor-v2/conductor/bundle.js +++ b/platform/features/conductor-v2/conductor/bundle.js @@ -67,6 +67,7 @@ define([ "depends": [ "$scope", "$window", + "$location", "openmct", "timeConductorViewService", "timeSystems[]", diff --git a/platform/features/conductor-v2/conductor/res/templates/time-conductor.html b/platform/features/conductor-v2/conductor/res/templates/time-conductor.html index 0556584760..de1b5c2326 100644 --- a/platform/features/conductor-v2/conductor/res/templates/time-conductor.html +++ b/platform/features/conductor-v2/conductor/res/templates/time-conductor.html @@ -12,7 +12,7 @@
+ ng-submit="tcController.setBounds(boundsModel)"> @@ -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"> @@ -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"> @@ -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"> diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js index d95632391c..10ede90d03 100644 --- a/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js @@ -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'); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js index 8495336972..ef3a25a163 100644 --- a/platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js @@ -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"); + }); + }); + }); }); }); diff --git a/platform/features/plot/src/PlotController.js b/platform/features/plot/src/PlotController.js index 6089358684..e4be264ea9 100644 --- a/platform/features/plot/src/PlotController.js +++ b/platform/features/plot/src/PlotController.js @@ -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); diff --git a/platform/features/plot/test/PlotControllerSpec.js b/platform/features/plot/test/PlotControllerSpec.js index 46d906cb41..e0ad612f60 100644 --- a/platform/features/plot/test/PlotControllerSpec.js +++ b/platform/features/plot/test/PlotControllerSpec.js @@ -126,9 +126,12 @@ define( 'off', 'bounds', 'timeSystem', - 'timeOfInterest' + 'timeOfInterest', + 'follow' ]); + mockConductor.bounds.andReturn({}); + controller = new PlotController( mockScope, mockElement,