diff --git a/platform/commonUI/formats/src/UTCTimeFormat.js b/platform/commonUI/formats/src/UTCTimeFormat.js index 3faab7a620..26919e3160 100644 --- a/platform/commonUI/formats/src/UTCTimeFormat.js +++ b/platform/commonUI/formats/src/UTCTimeFormat.js @@ -95,6 +95,38 @@ define([ })[0][0]; } + UTCTimeFormat.prototype.timeUnits = function (timeRange) { + var momentified = moment.duration(timeRange); + return [ + ["Decades", function (r) { + return r.years() > 15; + }], + ["Years", function (r) { + return r.years() > 1; + }], + ["Months", function (r) { + return r.years() === 1 || r.months() > 1; + }], + ["Days", function (r) { + return r.months() === 1 || r.days() > 1; + }], + ["Hours", function (r) { + return r.days() === 1 || r.hours() > 1; + }], + ["Minutes", function (r) { + return r.hours() === 1 || r.minutes() > 1; + }], + ["Seconds", function (r) { + return r.minutes() === 1 || r.seconds() > 1; + }], + ["Milliseconds", function (r) { + return true; + }] + ].filter(function (row){ + return row[1](momentified); + })[0][0]; + }; + /** * * @param value diff --git a/platform/features/conductor-v2/compatibility/src/ConductorRepresenter.js b/platform/features/conductor-v2/compatibility/src/ConductorRepresenter.js index fe19bbc66a..7013906ef9 100644 --- a/platform/features/conductor-v2/compatibility/src/ConductorRepresenter.js +++ b/platform/features/conductor-v2/compatibility/src/ConductorRepresenter.js @@ -85,6 +85,7 @@ define( ConductorRepresenter.prototype.destroy = function destroy() { this.conductor.off("bounds", this.boundsListener); this.conductor.off("timeSystem", this.timeSystemListener); + this.conductor.off("follow", this.followListener); }; return ConductorRepresenter; diff --git a/platform/features/conductor-v2/conductor/bundle.js b/platform/features/conductor-v2/conductor/bundle.js index 1fbeebdea7..c9520de1bb 100644 --- a/platform/features/conductor-v2/conductor/bundle.js +++ b/platform/features/conductor-v2/conductor/bundle.js @@ -69,7 +69,17 @@ define([ "$window", "timeConductor", "timeConductorViewService", - "timeSystems[]" + "timeSystems[]", + "formatService" + ] + }, + { + "key": "ConductorAxisController", + "implementation": ConductorAxisController, + "depends": [ + "timeConductor", + "formatService", + "timeConductorViewService" ] }, { diff --git a/platform/features/conductor-v2/conductor/res/sass/_time-conductor-base.scss b/platform/features/conductor-v2/conductor/res/sass/_time-conductor-base.scss index b48c3177e6..722adbdb8c 100644 --- a/platform/features/conductor-v2/conductor/res/sass/_time-conductor-base.scss +++ b/platform/features/conductor-v2/conductor/res/sass/_time-conductor-base.scss @@ -312,13 +312,13 @@ .l-time-conductor-zoom-w { @include justify-content(flex-end); .time-conductor-zoom { - display: none; // TEMP per request from Andrew 8/1/16 + //display: none; // TEMP per request from Andrew 8/1/16 height: $r3H; min-width: 100px; width: 20%; } .time-conductor-zoom-current-range { - display: none; // TEMP per request from Andrew 8/1/16 + //display: none; // TEMP per request from Andrew 8/1/16 color: $colorTick; } } 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 5dc88c8b1d..2a385bf3ed 100644 --- a/platform/features/conductor-v2/conductor/res/templates/time-conductor.html +++ b/platform/features/conductor-v2/conductor/res/templates/time-conductor.html @@ -131,8 +131,15 @@
- - + {{timeUnits}} +
diff --git a/platform/features/conductor-v2/conductor/src/timeSystems/TimeSystem.js b/platform/features/conductor-v2/conductor/src/timeSystems/TimeSystem.js index 652ea9ed0f..51987b39c8 100644 --- a/platform/features/conductor-v2/conductor/src/timeSystems/TimeSystem.js +++ b/platform/features/conductor-v2/conductor/src/timeSystems/TimeSystem.js @@ -73,8 +73,20 @@ define([], function () { throw new Error('Not implemented'); }; - /** + /*** * + * @typedef {object} TimeConductorZoom + * @property {number} min The largest time span that the time + * conductor can display in this time system + * @property {number} max The smallest time span that the time + * conductor can display in this time system + * + * @typedef {object} TimeSystemDefault + * @property {TimeConductorDeltas} deltas The deltas to apply by default + * when this time system is active. Applies to real-time modes only + * @property {TimeConductorBounds} bounds The bounds to apply by default + * when this time system is active + * @property {TimeConductorZoom} zoom Default min and max zoom levels * @returns {TimeSystemDefault[]} At least one set of default values for * this time system. */ diff --git a/platform/features/conductor-v2/conductor/src/ui/ConductorAxisController.js b/platform/features/conductor-v2/conductor/src/ui/ConductorAxisController.js index b88177181d..2fbe958e45 100644 --- a/platform/features/conductor-v2/conductor/src/ui/ConductorAxisController.js +++ b/platform/features/conductor-v2/conductor/src/ui/ConductorAxisController.js @@ -32,11 +32,12 @@ define( * labelled 'ticks'. It requires 'start' and 'end' integer values to * be specified as attributes. */ - function ConductorAxisController(conductor, formatService) { + function ConductorAxisController(conductor, formatService, conductorViewService) { // Dependencies this.d3 = d3; this.formatService = formatService; this.conductor = conductor; + this.conductorViewService = conductorViewService; // Runtime properties (set by 'link' function) this.target = undefined; @@ -46,14 +47,22 @@ define( this.initialized = false; this.msPerPixel = undefined; - this.setScale = this.setScale.bind(this); - this.changeBounds = this.changeBounds.bind(this); - this.changeTimeSystem = this.changeTimeSystem.bind(this); - this.bounds = conductor.bounds(); this.timeSystem = conductor.timeSystem(); + + //Bind all class functions to 'this' + Object.keys(ConductorAxisController.prototype).filter(function (key) { + return typeof ConductorAxisController.prototype[key] === 'function'; + }).forEach(function (key) { + this[key] = ConductorAxisController.prototype[key].bind(this); + }.bind(this)); } + ConductorAxisController.prototype.destroy = function () { + this.conductor.off('timeSystem', this.changeTimeSystem); + this.conductor.off('bounds', this.setScale); + }; + ConductorAxisController.prototype.changeBounds = function (bounds) { this.bounds = bounds; if (this.initialized) { @@ -129,18 +138,26 @@ define( if (this.timeSystem !== undefined) { this.changeTimeSystem(this.timeSystem); - this.setScale(this.bounds); + this.setScale(); } //Respond to changes in conductor this.conductor.on("timeSystem", this.changeTimeSystem); this.conductor.on("bounds", this.changeBounds); + + this.scope.$on("$destroy", this.destroy); + + this.conductorViewService.on("zoom", this.zoom); }; - ConductorAxisController.prototype.panEnd = function () { + ConductorAxisController.prototype.panStop = function () { //resync view bounds with time conductor bounds this.conductor.bounds(this.bounds); - this.scope.$emit("pan-stop"); + this.conductorViewService.emit("pan-stop"); + }; + + ConductorAxisController.prototype.zoom = function (bounds) { + this.changeBounds(bounds); }; ConductorAxisController.prototype.pan = function (delta) { @@ -154,7 +171,7 @@ define( end: end }; this.setScale(); - this.scope.$emit("pan", this.bounds); + this.conductorViewService.emit("pan", this.bounds); } }; diff --git a/platform/features/conductor-v2/conductor/src/ui/MctConductorAxis.js b/platform/features/conductor-v2/conductor/src/ui/MctConductorAxis.js index 8b874badf2..d664057af6 100644 --- a/platform/features/conductor-v2/conductor/src/ui/MctConductorAxis.js +++ b/platform/features/conductor-v2/conductor/src/ui/MctConductorAxis.js @@ -41,7 +41,7 @@ define([], function () { template: '
' } diff --git a/platform/features/conductor-v2/conductor/src/ui/MctConductorAxisSpec.js b/platform/features/conductor-v2/conductor/src/ui/MctConductorAxisSpec.js index 5c45521ea4..0fe3c4cf3f 100644 --- a/platform/features/conductor-v2/conductor/src/ui/MctConductorAxisSpec.js +++ b/platform/features/conductor-v2/conductor/src/ui/MctConductorAxisSpec.js @@ -32,7 +32,9 @@ define(['./MctConductorAxis'], function (MctConductorAxis) { d3; beforeEach(function () { - mockScope = {}; + mockScope = jasmine.createSpyObj("scope", [ + "$on" + ]); //Add some HTML elements mockTarget = { @@ -49,7 +51,8 @@ define(['./MctConductorAxis'], function (MctConductorAxis) { mockConductor = jasmine.createSpyObj("conductor", [ "timeSystem", "bounds", - "on" + "on", + "off" ]); mockConductor.bounds.andReturn(mockBounds); @@ -85,6 +88,13 @@ define(['./MctConductorAxis'], function (MctConductorAxis) { expect(mockConductor.on).toHaveBeenCalledWith("bounds", directive.setScale); }); + it("on scope destruction, deregisters listeners", function () { + expect(mockScope.$on).toHaveBeenCalledWith("$destroy", directive.destroy); + directive.destroy(); + expect(mockConductor.off).toHaveBeenCalledWith("timeSystem", directive.changeTimeSystem); + expect(mockConductor.off).toHaveBeenCalledWith("bounds", directive.setScale); + }); + describe("when the time system changes", function () { var mockTimeSystem; var mockFormat; diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js index 3026232f18..176d1bbb2f 100644 --- a/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js @@ -26,7 +26,7 @@ define( ], function (TimeConductorValidation) { - function TimeConductorController($scope, $window, timeConductor, conductorViewService, timeSystems) { + function TimeConductorController($scope, $window, timeConductor, conductorViewService, timeSystems, formatService) { var self = this; @@ -43,6 +43,7 @@ define( this.conductor = timeConductor; this.modes = conductorViewService.availableModes(); this.validation = new TimeConductorValidation(this.conductor); + this.formatService = formatService; // Construct the provided time system definitions this.timeSystems = timeSystems.map(function (timeSystemConstructor) { @@ -53,9 +54,6 @@ define( this.initializeScope(); this.conductor.on('bounds', this.setFormFromBounds); - this.conductor.on('follow', function (follow) { - $scope.followMode = follow; - }); this.conductor.on('timeSystem', this.changeTimeSystem); // If no mode selected, select fixed as the default @@ -100,14 +98,25 @@ define( // Watch scope for selection of mode or time system by user this.$scope.$watch('modeModel.selectedKey', this.setMode); - this.$scope.$on('pan', function (e, bounds) { - this.$scope.panning = true; - this.setFormFromBounds(bounds); - }.bind(this)); + this.conductorViewService.on('pan', this.pan); - this.$scope.$on('pan-stop', function () { - this.$scope.panning = false; - }.bind(this)); + this.conductorViewService.on('pan-stop', this.panStop); + + this.$scope.$on('$destroy', this.destroy); + }; + + TimeConductorController.prototype.destroy = function () { + this.conductor.off('bounds', this.setFormFromBounds); + this.conductor.off('timeSystem', this.changeTimeSystem); + }; + + TimeConductorController.prototype.pan = function (bounds) { + this.$scope.panning = true; + this.setFormFromBounds(bounds); + }; + + TimeConductorController.prototype.panStop = function () { + this.$scope.panning = false; }; /** @@ -119,6 +128,10 @@ define( TimeConductorController.prototype.setFormFromBounds = function (bounds) { this.$scope.boundsModel.start = bounds.start; this.$scope.boundsModel.end = bounds.end; + + this.$scope.currentZoom = this.toSliderValue(bounds.end - bounds.start); + this.toTimeUnits(bounds.end - bounds.start); + if (!this.pendingUpdate) { this.pendingUpdate = true; this.$window.requestAnimationFrame(function () { @@ -153,9 +166,12 @@ define( * @private */ TimeConductorController.prototype.setFormFromTimeSystem = function (timeSystem) { - this.$scope.timeSystemModel.selected = timeSystem; - this.$scope.timeSystemModel.format = timeSystem.formats()[0]; - this.$scope.timeSystemModel.deltaFormat = timeSystem.deltaFormat(); + var timeSystemModel = this.$scope.timeSystemModel; + timeSystemModel.selected = timeSystem; + timeSystemModel.format = timeSystem.formats()[0]; + timeSystemModel.deltaFormat = timeSystem.deltaFormat(); + timeSystemModel.minZoom = timeSystem.defaults().zoom.min; + timeSystemModel.maxZoom = timeSystem.defaults().zoom.max; }; @@ -242,6 +258,41 @@ define( } }; + TimeConductorController.prototype.toSliderValue = function (timeSpan) { + var timeSystem = this.conductor.timeSystem(); + if (timeSystem) { + var zoomDefaults = this.conductor.timeSystem().defaults().zoom; + var perc = timeSpan / (zoomDefaults.min - zoomDefaults.max); + return 1 - Math.pow(perc, 1 / 4); + } + }; + + TimeConductorController.prototype.toTimeSpan = function (sliderValue) { + var center = this.$scope.boundsModel.start + + ((this.$scope.boundsModel.end - this.$scope.boundsModel.start) / 2); + var zoomDefaults = this.conductor.timeSystem().defaults().zoom; + var timeSpan = Math.pow((1 - sliderValue), 4) * (zoomDefaults.min - zoomDefaults.max); + return {start: center - timeSpan / 2, end: center + timeSpan / 2}; + }; + + TimeConductorController.prototype.toTimeUnits = function (timeSpan) { + if (this.conductor.timeSystem()) { + var timeFormat = this.formatService.getFormat(this.conductor.timeSystem().formats()[0]); + this.$scope.timeUnits = timeFormat.timeUnits && timeFormat.timeUnits(timeSpan); + } + } + + TimeConductorController.prototype.zoom = function(sliderValue) { + var bounds = this.toTimeSpan(sliderValue); + this.setFormFromBounds(bounds); + this.conductorViewService.emit("zoom", bounds); + }; + + TimeConductorController.prototype.zoomStop = function (sliderValue) { + var bounds = this.toTimeSpan(sliderValue); + this.conductor.bounds(bounds); + }; + return TimeConductorController; } ); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js index 7d1457eafe..39759c60f5 100644 --- a/platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js @@ -30,14 +30,18 @@ define(['./TimeConductorController'], function (TimeConductorController) { var controller; beforeEach(function () { - mockScope = jasmine.createSpyObj("$scope", ["$watch"]); + mockScope = jasmine.createSpyObj("$scope", [ + "$watch", + "$on" + ]); mockWindow = jasmine.createSpyObj("$window", ["requestAnimationFrame"]); mockTimeConductor = jasmine.createSpyObj( "TimeConductor", [ "bounds", "timeSystem", - "on" + "on", + "off" ] ); mockTimeConductor.bounds.andReturn({start: undefined, end: undefined}); @@ -124,9 +128,16 @@ define(['./TimeConductorController'], function (TimeConductorController) { }); it("listens for changes to conductor state", function () { - expect(mockTimeConductor.on).toHaveBeenCalledWith("timeSystem", jasmine.any(Function)); - expect(mockTimeConductor.on).toHaveBeenCalledWith("bounds", jasmine.any(Function)); - expect(mockTimeConductor.on).toHaveBeenCalledWith("follow", jasmine.any(Function)); + expect(mockTimeConductor.on).toHaveBeenCalledWith("timeSystem", controller.changeTimeSystem); + expect(mockTimeConductor.on).toHaveBeenCalledWith("bounds", controller.setFormFromBounds); + }); + + it("deregisters conductor listens when scope is destroyed", function () { + expect(mockScope.$on).toHaveBeenCalledWith("$destroy", controller.destroy); + + controller.destroy(); + expect(mockTimeConductor.off).toHaveBeenCalledWith("timeSystem", controller.changeTimeSystem); + expect(mockTimeConductor.off).toHaveBeenCalledWith("bounds", controller.setFormFromBounds); }); it("when time system changes, sets time system on scope", function () { @@ -164,17 +175,6 @@ define(['./TimeConductorController'], function (TimeConductorController) { expect(mockScope.boundsModel.start).toEqual(bounds.start); expect(mockScope.boundsModel.end).toEqual(bounds.end); }); - - it("responds to a change in 'follow' state of the time conductor", function () { - var followListener = getListener("follow"); - expect(followListener).toBeDefined(); - - followListener(true); - expect(mockScope.followMode).toEqual(true); - - followListener(false); - expect(mockScope.followMode).toEqual(false); - }); }); describe("when user makes changes from UI", function () { diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewService.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewService.js index 8cbc349520..9796e2bc3b 100644 --- a/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewService.js +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewService.js @@ -22,9 +22,10 @@ define( [ + 'EventEmitter', './TimeConductorMode' ], - function (TimeConductorMode) { + function (EventEmitter, TimeConductorMode) { /** * A class representing the state of the time conductor view. This @@ -36,6 +37,9 @@ define( * @constructor */ function TimeConductorViewService(conductor, timeSystems) { + + EventEmitter.call(this); + this.systems = timeSystems.map(function (timeSystemConstructor) { return timeSystemConstructor(); }); @@ -97,6 +101,8 @@ define( } } + TimeConductorViewService.prototype = Object.create(EventEmitter.prototype); + /** * Getter/Setter for the Time Conductor Mode. Modes determine the * behavior of the time conductor, especially with regards to the diff --git a/platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystem.js b/platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystem.js index b6e969c3eb..780bfa8db1 100644 --- a/platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystem.js +++ b/platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystem.js @@ -64,13 +64,17 @@ define([ return this.sources; }; - UTCTimeSystem.prototype.defaults = function (key) { + UTCTimeSystem.prototype.defaults = function () { var now = Math.ceil(Date.now() / 1000) * 1000; + var ONE_MINUTE = 60 * 1 * 1000; + var FIFTY_YEARS = 50 * 365 * 24 * 60 * 60 * 1000; + return { key: 'utc-default', name: 'UTC time system defaults', deltas: {start: FIFTEEN_MINUTES, end: 0}, - bounds: {start: now - FIFTEEN_MINUTES, end: now} + bounds: {start: now - FIFTEEN_MINUTES, end: now}, + zoom: {min: FIFTY_YEARS, max: ONE_MINUTE} }; };