+ class="holder grows flex-elem l-flex-row l-time-conductor {{modeModel.selectedKey}}-mode {{timeSystemModel.selected.metadata.key}}-time-system"
+ ng-class="{'status-panning': tcController.panning}">
@@ -98,9 +113,18 @@
}">
-
diff --git a/platform/features/conductor-v2/conductor/res/templates/time-of-interest.html b/platform/features/conductor-v2/conductor/res/templates/time-of-interest.html
new file mode 100644
index 0000000000..6335b7a103
--- /dev/null
+++ b/platform/features/conductor-v2/conductor/res/templates/time-of-interest.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
{{toi.toiText}}
+
+
\ No newline at end of file
diff --git a/platform/features/conductor-v2/conductor/src/TimeConductor.js b/platform/features/conductor-v2/conductor/src/TimeConductor.js
deleted file mode 100644
index 2c2194b5a0..0000000000
--- a/platform/features/conductor-v2/conductor/src/TimeConductor.js
+++ /dev/null
@@ -1,179 +0,0 @@
-/*****************************************************************************
- * Open MCT Web, Copyright (c) 2014-2015, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT Web is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT Web includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-define(['EventEmitter'], function (EventEmitter) {
-
- /**
- * The public API for setting and querying time conductor state. The
- * time conductor is the means by which the temporal bounds of a view
- * are controlled. Time-sensitive views will typically respond to
- * changes to bounds or other properties of the time conductor and
- * update the data displayed based on the time conductor state.
- *
- * The TimeConductor extends the EventEmitter class. A number of events are
- * fired when properties of the time conductor change, which are
- * documented below.
- * @constructor
- */
- function TimeConductor() {
- EventEmitter.call(this);
-
- //The Time System
- this.system = undefined;
- //The Time Of Interest
- this.toi = undefined;
-
- this.boundsVal = {
- start: undefined,
- end: undefined
- };
-
- //Default to fixed mode
- this.followMode = false;
- }
-
- TimeConductor.prototype = Object.create(EventEmitter.prototype);
-
- /**
- * Validate the given bounds. This can be used for pre-validation of
- * bounds, for example by views validating user inputs.
- * @param bounds The start and end time of the conductor.
- * @returns {string | true} A validation error, or true if valid
- */
- TimeConductor.prototype.validateBounds = function (bounds) {
- if ((bounds.start === undefined) ||
- (bounds.end === undefined) ||
- isNaN(bounds.start) ||
- isNaN(bounds.end)
- ) {
- return "Start and end must be specified as integer values";
- } else if (bounds.start > bounds.end) {
- return "Specified start date exceeds end bound";
- }
- return true;
- };
-
- /**
- * Get or set the follow mode of the time conductor. In follow mode the
- * time conductor ticks, regularly updating the bounds from a timing
- * source appropriate to the selected time system and mode of the time
- * conductor.
- * @fires TimeConductor#follow
- * @param {boolean} followMode
- * @returns {boolean}
- */
- TimeConductor.prototype.follow = function (followMode) {
- if (arguments.length > 0) {
- this.followMode = followMode;
- /**
- * @event TimeConductor#follow The TimeConductor has toggled
- * into or out of follow mode.
- * @property {boolean} followMode true if follow mode is
- * enabled, otherwise false.
- */
- this.emit('follow', this.followMode);
- }
- return this.followMode;
- };
-
- /**
- * @typedef {Object} TimeConductorBounds
- * @property {number} start The start time displayed by the time conductor in ms since epoch. Epoch determined by current time system
- * @property {number} end The end time displayed by the time conductor in ms since epoch.
- */
- /**
- * Get or set the start and end time of the time conductor. Basic validation
- * of bounds is performed.
- *
- * @param {TimeConductorBounds} newBounds
- * @throws {Error} Validation error
- * @fires TimeConductor#bounds
- * @returns {TimeConductorBounds}
- */
- TimeConductor.prototype.bounds = function (newBounds) {
- if (arguments.length > 0) {
- var validationResult = this.validateBounds(newBounds);
- if (validationResult !== true) {
- throw new Error(validationResult);
- }
- //Create a copy to avoid direct mutation of conductor bounds
- this.boundsVal = JSON.parse(JSON.stringify(newBounds));
- /**
- * @event TimeConductor#bounds The start time, end time, or
- * both have been updated
- * @property {TimeConductorBounds} bounds
- */
- this.emit('bounds', this.boundsVal);
- }
- //Return a copy to prevent direct mutation of time conductor bounds.
- return JSON.parse(JSON.stringify(this.boundsVal));
- };
-
- /**
- * Get or set the time system of the TimeConductor. Time systems determine
- * units, epoch, and other aspects of time representation. When changing
- * the time system in use, new valid bounds must also be provided.
- * @param {TimeSystem} newTimeSystem
- * @param {TimeConductorBounds} bounds
- * @fires TimeConductor#timeSystem
- * @returns {TimeSystem} The currently applied time system
- */
- TimeConductor.prototype.timeSystem = function (newTimeSystem, bounds) {
- if (arguments.length >= 2) {
- this.system = newTimeSystem;
- /**
- * @event TimeConductor#timeSystem The time system used by the time
- * conductor has changed. A change in Time System will always be
- * followed by a bounds event specifying new query bounds
- * @property {TimeSystem} The value of the currently applied
- * Time System
- * */
- this.emit('timeSystem', this.system);
- this.bounds(bounds);
- } else if (arguments.length === 1) {
- throw new Error('Must set bounds when changing time system');
- }
- return this.system;
- };
-
- /**
- * Get or set the Time of Interest. The Time of Interest is the temporal
- * focus of the current view. It can be manipulated by the user from the
- * time conductor or from other views.
- * @fires TimeConductor#timeOfInterest
- * @param newTOI
- * @returns {number} the current time of interest
- */
- TimeConductor.prototype.timeOfInterest = function (newTOI) {
- if (arguments.length > 0) {
- this.toi = newTOI;
- /**
- * @event TimeConductor#timeOfInterest The Time of Interest has moved.
- * @property {number} Current time of interest
- */
- this.emit('timeOfInterest', this.toi);
- }
- return this.toi;
- };
-
- return TimeConductor;
-});
diff --git a/platform/features/conductor-v2/conductor/src/TimeConductorSpec.js b/platform/features/conductor-v2/conductor/src/TimeConductorSpec.js
deleted file mode 100644
index 7701f5e9b4..0000000000
--- a/platform/features/conductor-v2/conductor/src/TimeConductorSpec.js
+++ /dev/null
@@ -1,110 +0,0 @@
-/*****************************************************************************
- * Open MCT Web, Copyright (c) 2014-2015, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT Web is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT Web includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-define(['./TimeConductor'], function (TimeConductor) {
- describe("The Time Conductor", function () {
- var tc,
- timeSystem,
- bounds,
- eventListener,
- toi,
- follow;
-
- beforeEach(function () {
- tc = new TimeConductor();
- timeSystem = {};
- bounds = {start: 0, end: 0};
- eventListener = jasmine.createSpy("eventListener");
- toi = 111;
- follow = true;
- });
-
- it("Supports setting and querying of time of interest and and follow mode", function () {
- expect(tc.timeOfInterest()).not.toBe(toi);
- tc.timeOfInterest(toi);
- expect(tc.timeOfInterest()).toBe(toi);
-
- expect(tc.follow()).not.toBe(follow);
- tc.follow(follow);
- expect(tc.follow()).toBe(follow);
- });
-
- it("Allows setting of valid bounds", function () {
- bounds = {start: 0, end: 1};
- expect(tc.bounds()).not.toEqual(bounds);
- expect(tc.bounds.bind(tc, bounds)).not.toThrow();
- expect(tc.bounds()).toEqual(bounds);
- });
-
- it("Disallows setting of invalid bounds", function () {
- bounds = {start: 1, end: 0};
- expect(tc.bounds()).not.toEqual(bounds);
- expect(tc.bounds.bind(tc, bounds)).toThrow();
- expect(tc.bounds()).not.toEqual(bounds);
-
- bounds = {start: 1};
- expect(tc.bounds()).not.toEqual(bounds);
- expect(tc.bounds.bind(tc, bounds)).toThrow();
- expect(tc.bounds()).not.toEqual(bounds);
- });
-
- it("Allows setting of time system with bounds", function () {
- expect(tc.timeSystem()).not.toBe(timeSystem);
- expect(tc.timeSystem.bind(tc, timeSystem, bounds)).not.toThrow();
- expect(tc.timeSystem()).toBe(timeSystem);
- });
-
- it("Disallows setting of time system without bounds", function () {
- expect(tc.timeSystem()).not.toBe(timeSystem);
- expect(tc.timeSystem.bind(tc, timeSystem)).toThrow();
- expect(tc.timeSystem()).not.toBe(timeSystem);
- });
-
- it("Emits an event when time system changes", function () {
- expect(eventListener).not.toHaveBeenCalled();
- tc.on("timeSystem", eventListener);
- tc.timeSystem(timeSystem, bounds);
- expect(eventListener).toHaveBeenCalledWith(timeSystem);
- });
-
- it("Emits an event when time of interest changes", function () {
- expect(eventListener).not.toHaveBeenCalled();
- tc.on("timeOfInterest", eventListener);
- tc.timeOfInterest(toi);
- expect(eventListener).toHaveBeenCalledWith(toi);
- });
-
- it("Emits an event when bounds change", function () {
- expect(eventListener).not.toHaveBeenCalled();
- tc.on("bounds", eventListener);
- tc.bounds(bounds);
- expect(eventListener).toHaveBeenCalledWith(bounds);
- });
-
- it("Emits an event when follow mode changes", function () {
- expect(eventListener).not.toHaveBeenCalled();
- tc.on("follow", eventListener);
- tc.follow(follow);
- expect(eventListener).toHaveBeenCalledWith(follow);
- });
- });
-});
diff --git a/platform/features/conductor-v2/conductor/src/timeSystems/TimeSystem.js b/platform/features/conductor-v2/conductor/src/timeSystems/TimeSystem.js
index 652ea9ed0f..1a945f4c80 100644
--- a/platform/features/conductor-v2/conductor/src/timeSystems/TimeSystem.js
+++ b/platform/features/conductor-v2/conductor/src/timeSystems/TimeSystem.js
@@ -73,8 +73,22 @@ 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. ie. the span of the time
+ * conductor in its most zoomed out state.
+ * @property {number} max The smallest time span that the time
+ * conductor can display in this time system. ie. the span of the time
+ * conductor bounds in its most zoomed in state.
+ *
+ * @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
new file mode 100644
index 0000000000..b99eaa0e6e
--- /dev/null
+++ b/platform/features/conductor-v2/conductor/src/ui/ConductorAxisController.js
@@ -0,0 +1,240 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+define(
+ [
+ "d3"
+ ],
+ function (d3) {
+ var PADDING = 1;
+
+ /**
+ * Controller that renders a horizontal time scale spanning the current bounds defined in the time conductor.
+ * Used by the mct-conductor-axis directive
+ * @constructor
+ */
+ function ConductorAxisController(openmct, formatService, conductorViewService, scope, element) {
+ // Dependencies
+ this.formatService = formatService;
+ this.conductor = openmct.conductor;
+ this.conductorViewService = conductorViewService;
+
+ this.scope = scope;
+ this.initialized = false;
+
+ this.bounds = this.conductor.bounds();
+ this.timeSystem = this.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));
+
+ this.initialize(element);
+ }
+
+ /**
+ * @private
+ */
+ ConductorAxisController.prototype.destroy = function () {
+ this.conductor.off('timeSystem', this.changeTimeSystem);
+ this.conductor.off('bounds', this.changeBounds);
+ this.conductorViewService.off("zoom", this.onZoom);
+ this.conductorViewService.off("zoom-stop", this.onZoomStop);
+ };
+
+ /**
+ * @private
+ */
+ ConductorAxisController.prototype.initialize = function (element) {
+ this.target = element[0].firstChild;
+ var height = this.target.offsetHeight;
+ var vis = d3.select(this.target)
+ .append("svg:svg")
+ .attr("width", "100%")
+ .attr("height", height);
+
+ this.xAxis = d3.axisTop();
+
+ // draw x axis with labels and move to the bottom of the chart area
+ this.axisElement = vis.append("g")
+ .attr("transform", "translate(0," + (height - PADDING) + ")");
+
+ if (this.timeSystem !== undefined) {
+ this.changeTimeSystem(this.timeSystem);
+ 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.onZoom);
+ this.conductorViewService.on("zoom-stop", this.onZoomStop);
+ };
+
+ /**
+ * @private
+ */
+ ConductorAxisController.prototype.changeBounds = function (bounds) {
+ this.bounds = bounds;
+ if (!this.zooming) {
+ this.setScale();
+ }
+ };
+
+ /**
+ * Set the scale of the axis, based on current conductor bounds.
+ */
+ ConductorAxisController.prototype.setScale = function () {
+ var width = this.target.offsetWidth;
+ var timeSystem = this.conductor.timeSystem();
+ var bounds = this.bounds;
+
+ if (timeSystem.isUTCBased()) {
+ this.xScale = this.xScale || d3.scaleUtc();
+ this.xScale.domain([new Date(bounds.start), new Date(bounds.end)]);
+ } else {
+ this.xScale = this.xScale || d3.scaleLinear();
+ this.xScale.domain([bounds.start, bounds.end]);
+ }
+
+ this.xAxis.scale(this.xScale);
+
+ this.xScale.range([PADDING, width - PADDING * 2]);
+ this.axisElement.call(this.xAxis);
+
+ this.msPerPixel = (bounds.end - bounds.start) / width;
+ };
+
+ /**
+ * When the time system changes, update the scale and formatter used for showing times.
+ * @param timeSystem
+ */
+ ConductorAxisController.prototype.changeTimeSystem = function (timeSystem) {
+ this.timeSystem = timeSystem;
+
+ var key = timeSystem.formats()[0];
+ if (key !== undefined) {
+ var format = this.formatService.getFormat(key);
+ var bounds = this.conductor.bounds();
+
+ //The D3 scale used depends on the type of time system as d3
+ // supports UTC out of the box.
+ if (timeSystem.isUTCBased()) {
+ this.xScale = d3.scaleUtc();
+ } else {
+ this.xScale = d3.scaleLinear();
+ }
+
+ this.xAxis.scale(this.xScale);
+
+ //Define a custom format function
+ this.xAxis.tickFormat(function (tickValue) {
+ // Normalize date representations to numbers
+ if (tickValue instanceof Date) {
+ tickValue = tickValue.getTime();
+ }
+ return format.format(tickValue, {
+ min: bounds.start,
+ max: bounds.end
+ });
+ });
+ this.axisElement.call(this.xAxis);
+ }
+ };
+
+ /**
+ * The user has stopped panning the time conductor scale element.
+ * @event panStop
+ */
+ /**
+ * Called on release of mouse button after dragging the scale left or right.
+ * @fires platform.features.conductor.ConductorAxisController~panStop
+ */
+ ConductorAxisController.prototype.panStop = function () {
+ //resync view bounds with time conductor bounds
+ this.conductorViewService.emit("pan-stop");
+ this.conductor.bounds(this.bounds);
+ };
+
+ /**
+ * Rescales the axis when the user zooms. Although zoom ultimately results in a bounds change once the user
+ * releases the zoom slider, dragging the slider will not immediately change the conductor bounds. It will
+ * however immediately update the scale and the bounds displayed in the UI.
+ * @private
+ * @param {ZoomLevel}
+ */
+ ConductorAxisController.prototype.onZoom = function (zoom) {
+ this.zooming = true;
+
+ this.bounds = zoom.bounds;
+ this.setScale();
+ };
+
+ /**
+ * @private
+ */
+ ConductorAxisController.prototype.onZoomStop = function (zoom) {
+ this.zooming = false;
+ };
+
+ /**
+ * @event platform.features.conductor.ConductorAxisController~pan
+ * Fired when the time conductor is panned
+ */
+ /**
+ * Initiate panning via a click + drag gesture on the time conductor
+ * scale. Panning triggers a "pan" event
+ * @param {number} delta the offset from the original click event
+ * @see TimeConductorViewService#
+ * @fires platform.features.conductor.ConductorAxisController~pan
+ */
+ ConductorAxisController.prototype.pan = function (delta) {
+ if (!this.conductor.follow()) {
+ var deltaInMs = delta[0] * this.msPerPixel;
+ var bounds = this.conductor.bounds();
+ var start = Math.floor((bounds.start - deltaInMs) / 1000) * 1000;
+ var end = Math.floor((bounds.end - deltaInMs) / 1000) * 1000;
+ this.bounds = {
+ start: start,
+ end: end
+ };
+ this.setScale();
+ this.conductorViewService.emit("pan", this.bounds);
+ }
+ };
+
+ /**
+ * Invoked on element resize. Will rebuild the scale based on the new dimensions of the element.
+ */
+ ConductorAxisController.prototype.resize = function () {
+ this.setScale();
+ };
+
+ return ConductorAxisController;
+ }
+);
diff --git a/platform/features/conductor-v2/conductor/src/ui/MctConductorAxisSpec.js b/platform/features/conductor-v2/conductor/src/ui/ConductorAxisControllerSpec.js
similarity index 51%
rename from platform/features/conductor-v2/conductor/src/ui/MctConductorAxisSpec.js
rename to platform/features/conductor-v2/conductor/src/ui/ConductorAxisControllerSpec.js
index 0fe3c4cf3f..2dbbb42c4b 100644
--- a/platform/features/conductor-v2/conductor/src/ui/MctConductorAxisSpec.js
+++ b/platform/features/conductor-v2/conductor/src/ui/ConductorAxisControllerSpec.js
@@ -20,16 +20,33 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
-define(['./MctConductorAxis'], function (MctConductorAxis) {
- describe("The MctConductorAxis directive", function () {
- var directive,
+define([
+ './ConductorAxisController',
+ 'zepto',
+ 'd3'
+], function (
+ ConductorAxisController,
+ $,
+ d3
+) {
+ describe("The ConductorAxisController", function () {
+ var controller,
mockConductor,
+ mockConductorViewService,
mockFormatService,
mockScope,
mockElement,
mockTarget,
mockBounds,
- d3;
+ element,
+ mockTimeSystem,
+ mockFormat;
+
+ function getCallback(target, name) {
+ return target.calls.filter(function (call) {
+ return call.args[0] === name;
+ })[0].args[1];
+ }
beforeEach(function () {
mockScope = jasmine.createSpyObj("scope", [
@@ -52,7 +69,8 @@ define(['./MctConductorAxis'], function (MctConductorAxis) {
"timeSystem",
"bounds",
"on",
- "off"
+ "off",
+ "follow"
]);
mockConductor.bounds.andReturn(mockBounds);
@@ -60,87 +78,101 @@ define(['./MctConductorAxis'], function (MctConductorAxis) {
"getFormat"
]);
- var d3Functions = [
- "scale",
- "scaleUtc",
- "scaleLinear",
- "select",
- "append",
- "attr",
- "axisTop",
- "call",
- "tickFormat",
- "domain",
- "range"
- ];
- d3 = jasmine.createSpyObj("d3", d3Functions);
- d3Functions.forEach(function (func) {
- d3[func].andReturn(d3);
- });
+ mockConductorViewService = jasmine.createSpyObj("conductorViewService", [
+ "on",
+ "off",
+ "emit"
+ ]);
- directive = new MctConductorAxis(mockConductor, mockFormatService);
- directive.d3 = d3;
- directive.link(mockScope, [mockElement]);
+ spyOn(d3, 'scaleUtc').andCallThrough();
+ spyOn(d3, 'scaleLinear').andCallThrough();
+
+ element = $('
');
+ $(document).find('body').append(element);
+ controller = new ConductorAxisController({conductor: mockConductor}, mockFormatService, mockConductorViewService, mockScope, element);
+
+ mockTimeSystem = jasmine.createSpyObj("timeSystem", [
+ "formats",
+ "isUTCBased"
+ ]);
+ mockFormat = jasmine.createSpyObj("format", [
+ "format"
+ ]);
+
+ mockTimeSystem.formats.andReturn(["mockFormat"]);
+ mockFormatService.getFormat.andReturn(mockFormat);
+ mockConductor.timeSystem.andReturn(mockTimeSystem);
+ mockTimeSystem.isUTCBased.andReturn(false);
});
it("listens for changes to time system and bounds", function () {
- expect(mockConductor.on).toHaveBeenCalledWith("timeSystem", directive.changeTimeSystem);
- expect(mockConductor.on).toHaveBeenCalledWith("bounds", directive.setScale);
+ expect(mockConductor.on).toHaveBeenCalledWith("timeSystem", controller.changeTimeSystem);
+ expect(mockConductor.on).toHaveBeenCalledWith("bounds", controller.changeBounds);
});
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);
+ expect(mockScope.$on).toHaveBeenCalledWith("$destroy", controller.destroy);
+ controller.destroy();
+ expect(mockConductor.off).toHaveBeenCalledWith("timeSystem", controller.changeTimeSystem);
+ expect(mockConductor.off).toHaveBeenCalledWith("bounds", controller.changeBounds);
});
describe("when the time system changes", function () {
- var mockTimeSystem;
- var mockFormat;
-
- beforeEach(function () {
- mockTimeSystem = jasmine.createSpyObj("timeSystem", [
- "formats",
- "isUTCBased"
- ]);
- mockFormat = jasmine.createSpyObj("format", [
- "format"
- ]);
-
- mockTimeSystem.formats.andReturn(["mockFormat"]);
- mockFormatService.getFormat.andReturn(mockFormat);
- });
-
it("uses a UTC scale for UTC time systems", function () {
mockTimeSystem.isUTCBased.andReturn(true);
- directive.changeTimeSystem(mockTimeSystem);
+ controller.changeTimeSystem(mockTimeSystem);
+
expect(d3.scaleUtc).toHaveBeenCalled();
expect(d3.scaleLinear).not.toHaveBeenCalled();
});
it("uses a linear scale for non-UTC time systems", function () {
mockTimeSystem.isUTCBased.andReturn(false);
- directive.changeTimeSystem(mockTimeSystem);
+ controller.changeTimeSystem(mockTimeSystem);
expect(d3.scaleLinear).toHaveBeenCalled();
expect(d3.scaleUtc).not.toHaveBeenCalled();
});
it("sets axis domain to time conductor bounds", function () {
mockTimeSystem.isUTCBased.andReturn(false);
- mockConductor.timeSystem.andReturn(mockTimeSystem);
-
- directive.setScale();
- expect(d3.domain).toHaveBeenCalledWith([mockBounds.start, mockBounds.end]);
+ controller.setScale();
+ expect(controller.xScale.domain()).toEqual([mockBounds.start, mockBounds.end]);
});
it("uses the format specified by the time system to format tick" +
" labels", function () {
- directive.changeTimeSystem(mockTimeSystem);
- expect(d3.tickFormat).toHaveBeenCalled();
- d3.tickFormat.mostRecentCall.args[0]();
+ controller.changeTimeSystem(mockTimeSystem);
expect(mockFormat.format).toHaveBeenCalled();
});
+
+ it('responds to zoom events', function () {
+ expect(mockConductorViewService.on).toHaveBeenCalledWith("zoom", controller.onZoom);
+ var cb = getCallback(mockConductorViewService.on, "zoom");
+ spyOn(controller, 'setScale').andCallThrough();
+ cb({bounds: {start: 0, end: 100}});
+ expect(controller.setScale).toHaveBeenCalled();
+ });
+
+ it('adjusts scale on pan', function () {
+ spyOn(controller, 'setScale').andCallThrough();
+ controller.pan(100);
+ expect(controller.setScale).toHaveBeenCalled();
+ });
+
+ it('emits event on pan', function () {
+ spyOn(controller, 'setScale').andCallThrough();
+ controller.pan(100);
+ expect(mockConductorViewService.emit).toHaveBeenCalledWith("pan", jasmine.any(Object));
+ });
+
+ it('cleans up listeners on destruction', function () {
+ controller.destroy();
+ expect(mockConductor.off).toHaveBeenCalledWith("bounds", controller.changeBounds);
+ expect(mockConductor.off).toHaveBeenCalledWith("timeSystem", controller.changeTimeSystem);
+
+ expect(mockConductorViewService.off).toHaveBeenCalledWith("zoom", controller.onZoom);
+ });
+
});
});
});
diff --git a/platform/features/conductor-v2/conductor/src/ui/ConductorTOIController.js b/platform/features/conductor-v2/conductor/src/ui/ConductorTOIController.js
new file mode 100644
index 0000000000..0308800d55
--- /dev/null
+++ b/platform/features/conductor-v2/conductor/src/ui/ConductorTOIController.js
@@ -0,0 +1,124 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+define(
+ ["zepto"],
+ function ($) {
+
+ /**
+ * Controller for the Time of Interest indicator in the conductor itself. Sets the horizontal position of the
+ * TOI indicator based on the current value of the TOI, and the width of the TOI conductor.
+ * @memberof platform.features.conductor
+ */
+ function ConductorTOIController($scope, openmct, conductorViewService) {
+ this.conductor = openmct.conductor;
+ this.conductorViewService = conductorViewService;
+
+ //Bind all class functions to 'this'
+ Object.keys(ConductorTOIController.prototype).filter(function (key) {
+ return typeof ConductorTOIController.prototype[key] === 'function';
+ }).forEach(function (key) {
+ this[key] = ConductorTOIController.prototype[key].bind(this);
+ }.bind(this));
+
+ this.conductor.on('timeOfInterest', this.changeTimeOfInterest);
+ this.conductorViewService.on('zoom', this.setOffsetFromZoom);
+ this.conductorViewService.on('pan', this.setOffsetFromBounds);
+
+ var timeOfInterest = this.conductor.timeOfInterest();
+ if (timeOfInterest) {
+ this.changeTimeOfInterest(timeOfInterest);
+ }
+
+ $scope.$on('$destroy', this.destroy);
+ }
+
+ /**
+ * @private
+ */
+ ConductorTOIController.prototype.destroy = function () {
+ this.conductor.off('timeOfInterest', this.changeTimeOfInterest);
+ this.conductorViewService.off('zoom', this.setOffsetFromZoom);
+ this.conductorViewService.off('pan', this.setOffsetFromBounds);
+ };
+
+ /**
+ * Given some bounds, set horizontal position of TOI indicator based
+ * on current conductor TOI value. Bounds are provided so that
+ * ephemeral bounds from zoom and pan events can be used as well
+ * as current conductor bounds, allowing TOI to be updated in
+ * realtime during scroll and zoom.
+ * @param {TimeConductorBounds} bounds
+ */
+ ConductorTOIController.prototype.setOffsetFromBounds = function (bounds) {
+ var toi = this.conductor.timeOfInterest();
+ if (toi !== undefined) {
+ var offset = toi - bounds.start;
+ var duration = bounds.end - bounds.start;
+ this.left = offset / duration * 100;
+ this.pinned = true;
+ } else {
+ this.left = 0;
+ this.pinned = false;
+ }
+ };
+
+ /**
+ * @private
+ */
+ ConductorTOIController.prototype.setOffsetFromZoom = function (zoom) {
+ return this.setOffsetFromBounds(zoom.bounds);
+ };
+
+ /**
+ * Invoked when time of interest changes. Will set the horizontal offset of the TOI indicator.
+ * @private
+ */
+ ConductorTOIController.prototype.changeTimeOfInterest = function () {
+ var bounds = this.conductor.bounds();
+ if (bounds) {
+ this.setOffsetFromBounds(bounds);
+ }
+ };
+
+ /**
+ * On a mouse click event within the TOI element, convert position within element to a time of interest, and
+ * set the time of interest on the conductor.
+ * @param e The angular $event object
+ */
+ ConductorTOIController.prototype.setTOIFromPosition = function (e) {
+ //TOI is set using the alt key modified + primary click
+ if (e.altKey) {
+ var element = $(e.currentTarget);
+ var width = element.width();
+ var relativeX = e.pageX - element.offset().left;
+ var percX = relativeX / width;
+ var bounds = this.conductor.bounds();
+ var timeRange = bounds.end - bounds.start;
+
+ this.conductor.timeOfInterest(timeRange * percX + bounds.start);
+ }
+ };
+
+ return ConductorTOIController;
+ }
+);
diff --git a/platform/features/conductor-v2/conductor/src/ui/ConductorTOIControllerSpec.js b/platform/features/conductor-v2/conductor/src/ui/ConductorTOIControllerSpec.js
new file mode 100644
index 0000000000..31fb3d6365
--- /dev/null
+++ b/platform/features/conductor-v2/conductor/src/ui/ConductorTOIControllerSpec.js
@@ -0,0 +1,153 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+define([
+ './ConductorTOIController'
+], function (
+ ConductorTOIController
+) {
+ var mockConductor;
+ var mockConductorViewService;
+ var mockScope;
+ var mockAPI;
+ var conductorTOIController;
+
+ function getNamedCallback(thing, name) {
+ return thing.calls.filter(function (call) {
+ return call.args[0] === name;
+ }).map(function (call) {
+ return call.args;
+ })[0][1];
+ }
+
+ describe("The ConductorTOIController", function () {
+ beforeEach(function () {
+ mockConductor = jasmine.createSpyObj("conductor", [
+ "bounds",
+ "timeOfInterest",
+ "on",
+ "off"
+ ]);
+ mockAPI = {conductor: mockConductor};
+
+ mockConductorViewService = jasmine.createSpyObj("conductorViewService", [
+ "on",
+ "off"
+ ]);
+
+ mockScope = jasmine.createSpyObj("openMCT", [
+ "$on"
+ ]);
+
+ conductorTOIController = new ConductorTOIController(mockScope, mockAPI, mockConductorViewService);
+ });
+
+ it("listens to changes in the time of interest on the conductor", function () {
+ expect(mockConductor.on).toHaveBeenCalledWith("timeOfInterest", jasmine.any(Function));
+ });
+
+ describe("when responding to changes in the time of interest", function () {
+ var toiCallback;
+ beforeEach(function () {
+ var bounds = {
+ start: 0,
+ end: 200
+ };
+ mockConductor.bounds.andReturn(bounds);
+ toiCallback = getNamedCallback(mockConductor.on, "timeOfInterest");
+ });
+
+ it("calculates the correct horizontal offset based on bounds and current TOI", function () {
+ //Expect time of interest position to be 50% of element width
+ mockConductor.timeOfInterest.andReturn(100);
+ toiCallback();
+ expect(conductorTOIController.left).toBe(50);
+
+ //Expect time of interest position to be 25% of element width
+ mockConductor.timeOfInterest.andReturn(50);
+ toiCallback();
+ expect(conductorTOIController.left).toBe(25);
+
+ //Expect time of interest position to be 0% of element width
+ mockConductor.timeOfInterest.andReturn(0);
+ toiCallback();
+ expect(conductorTOIController.left).toBe(0);
+
+ //Expect time of interest position to be 100% of element width
+ mockConductor.timeOfInterest.andReturn(200);
+ toiCallback();
+ expect(conductorTOIController.left).toBe(100);
+ });
+
+ it("renders the TOI indicator visible", function () {
+ expect(conductorTOIController.pinned).toBeFalsy();
+ mockConductor.timeOfInterest.andReturn(100);
+ toiCallback();
+ expect(conductorTOIController.pinned).toBe(true);
+ });
+ });
+
+ it("responds to zoom events", function () {
+ var mockZoom = {
+ bounds: {
+ start: 500,
+ end: 1000
+ }
+ };
+ expect(mockConductorViewService.on).toHaveBeenCalledWith("zoom", jasmine.any(Function));
+
+ // Should correspond to horizontal offset of 50%
+ mockConductor.timeOfInterest.andReturn(750);
+ var zoomCallback = getNamedCallback(mockConductorViewService.on, "zoom");
+ zoomCallback(mockZoom);
+ expect(conductorTOIController.left).toBe(50);
+ });
+
+ it("responds to pan events", function () {
+ var mockPanBounds = {
+ start: 1000,
+ end: 3000
+ };
+
+ expect(mockConductorViewService.on).toHaveBeenCalledWith("pan", jasmine.any(Function));
+
+ // Should correspond to horizontal offset of 25%
+ mockConductor.timeOfInterest.andReturn(1500);
+ var panCallback = getNamedCallback(mockConductorViewService.on, "pan");
+ panCallback(mockPanBounds);
+ expect(conductorTOIController.left).toBe(25);
+ });
+
+
+ it("Cleans up all listeners when controller destroyed", function () {
+ var zoomCB = getNamedCallback(mockConductorViewService.on, "zoom");
+ var panCB = getNamedCallback(mockConductorViewService.on, "pan");
+ var toiCB = getNamedCallback(mockConductor.on, "timeOfInterest");
+
+ expect(mockScope.$on).toHaveBeenCalledWith("$destroy", jasmine.any(Function));
+ getNamedCallback(mockScope.$on, "$destroy")();
+ expect(mockConductorViewService.off).toHaveBeenCalledWith("zoom", zoomCB);
+ expect(mockConductorViewService.off).toHaveBeenCalledWith("pan", panCB);
+ expect(mockConductor.off).toHaveBeenCalledWith("timeOfInterest", toiCB);
+ });
+ });
+});
diff --git a/platform/features/conductor-v2/conductor/src/ui/MctConductorAxis.js b/platform/features/conductor-v2/conductor/src/ui/MctConductorAxis.js
index 58cb60befc..7484d6b35f 100644
--- a/platform/features/conductor-v2/conductor/src/ui/MctConductorAxis.js
+++ b/platform/features/conductor-v2/conductor/src/ui/MctConductorAxis.js
@@ -20,127 +20,35 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
-define(
- [
- "d3"
- ],
- function (d3) {
- var PADDING = 1;
-
+define(['./ConductorAxisController'], function (ConductorAxisController) {
+ function MctConductorAxis() {
/**
* The mct-conductor-axis renders a horizontal axis with regular
* labelled 'ticks'. It requires 'start' and 'end' integer values to
* be specified as attributes.
*/
- function MCTConductorAxis(conductor, formatService) {
- // Dependencies
- this.d3 = d3;
- this.conductor = conductor;
- this.formatService = formatService;
- // Runtime properties (set by 'link' function)
- this.target = undefined;
- this.xScale = undefined;
- this.xAxis = undefined;
- this.axisElement = undefined;
+ return {
+ controller: [
+ 'openmct',
+ 'formatService',
+ 'timeConductorViewService',
+ '$scope',
+ '$element',
+ ConductorAxisController
+ ],
+ controllerAs: 'axis',
- // Angular Directive interface
- this.link = this.link.bind(this);
- this.restrict = "E";
- this.template =
- "
";
- this.priority = 1000;
+ restrict: 'E',
+ priority: 1000,
- //Bind all class functions to 'this'
- Object.keys(MCTConductorAxis.prototype).filter(function (key) {
- return typeof MCTConductorAxis.prototype[key] === 'function';
- }).forEach(function (key) {
- this[key] = this[key].bind(this);
- }.bind(this));
- }
-
- MCTConductorAxis.prototype.setScale = function () {
- var width = this.target.offsetWidth;
- var timeSystem = this.conductor.timeSystem();
- var bounds = this.conductor.bounds();
-
- if (timeSystem.isUTCBased()) {
- this.xScale = this.xScale || this.d3.scaleUtc();
- this.xScale.domain([new Date(bounds.start), new Date(bounds.end)]);
- } else {
- this.xScale = this.xScale || this.d3.scaleLinear();
- this.xScale.domain([bounds.start, bounds.end]);
- }
-
- this.xScale.range([PADDING, width - PADDING * 2]);
- this.axisElement.call(this.xAxis);
- };
-
- MCTConductorAxis.prototype.changeTimeSystem = function (timeSystem) {
- var key = timeSystem.formats()[0];
- if (key !== undefined) {
- var format = this.formatService.getFormat(key);
- var bounds = this.conductor.bounds();
-
- if (timeSystem.isUTCBased()) {
- this.xScale = this.d3.scaleUtc();
- } else {
- this.xScale = this.d3.scaleLinear();
- }
-
- this.xAxis.scale(this.xScale);
- //Define a custom format function
- this.xAxis.tickFormat(function (tickValue) {
- // Normalize date representations to numbers
- if (tickValue instanceof Date) {
- tickValue = tickValue.getTime();
- }
- return format.format(tickValue, {
- min: bounds.start,
- max: bounds.end
- });
- });
- this.axisElement.call(this.xAxis);
- }
- };
-
- MCTConductorAxis.prototype.destroy = function () {
- this.conductor.off('timeSystem', this.changeTimeSystem);
- this.conductor.off('bounds', this.setScale);
- };
-
- MCTConductorAxis.prototype.link = function (scope, element) {
- var conductor = this.conductor;
- this.target = element[0].firstChild;
- var height = this.target.offsetHeight;
- var vis = this.d3.select(this.target)
- .append('svg:svg')
- .attr('width', '100%')
- .attr('height', height);
-
- this.xAxis = this.d3.axisTop();
-
- // draw x axis with labels and move to the bottom of the chart area
- this.axisElement = vis.append("g")
- .attr("transform", "translate(0," + (height - PADDING) + ")");
-
- scope.resize = this.setScale;
-
- conductor.on('timeSystem', this.changeTimeSystem);
-
- //On conductor bounds changes, redraw ticks
- conductor.on('bounds', this.setScale);
-
- scope.$on("$destroy", this.destroy);
-
- if (conductor.timeSystem() !== undefined) {
- this.changeTimeSystem(conductor.timeSystem());
- this.setScale();
- }
- };
-
- return function (conductor, formatService) {
- return new MCTConductorAxis(conductor, formatService);
+ template: '
'
};
}
-);
+
+ return MctConductorAxis;
+});
diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js
index 83bc0558c6..d95632391c 100644
--- a/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js
+++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js
@@ -26,7 +26,13 @@ define(
],
function (TimeConductorValidation) {
- function TimeConductorController($scope, $window, timeConductor, conductorViewService, timeSystems) {
+ /**
+ * Controller for the Time Conductor UI element. The Time Conductor includes form fields for specifying time
+ * bounds and relative time deltas for queries, as well as controls for selection mode, time systems, and zooming.
+ * @memberof platform.features.conductor
+ * @constructor
+ */
+ function TimeConductorController($scope, $window, openmct, conductorViewService, timeSystems, formatService) {
var self = this;
@@ -40,9 +46,10 @@ define(
this.$scope = $scope;
this.$window = $window;
this.conductorViewService = conductorViewService;
- this.conductor = timeConductor;
+ this.conductor = openmct.conductor;
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) {
@@ -52,7 +59,7 @@ define(
//Set the initial state of the view based on current time conductor
this.initializeScope();
- this.conductor.on('bounds', this.setFormFromBounds);
+ this.conductor.on('bounds', this.changeBounds);
this.conductor.on('timeSystem', this.changeTimeSystem);
// If no mode selected, select fixed as the default
@@ -71,8 +78,9 @@ define(
//If conductor has a time system selected already, populate the
//form from it
this.$scope.timeSystemModel = {};
- if (this.conductor.timeSystem()) {
- this.setFormFromTimeSystem(this.conductor.timeSystem());
+ var timeSystem = this.conductor.timeSystem();
+ if (timeSystem) {
+ this.setFormFromTimeSystem(timeSystem);
}
//Represents the various modes, and the currently selected mode
@@ -98,34 +106,80 @@ define(
// Watch scope for selection of mode or time system by user
this.$scope.$watch('modeModel.selectedKey', this.setMode);
+ this.conductorViewService.on('pan', this.onPan);
+ this.conductorViewService.on('pan-stop', this.onPanStop);
+
this.$scope.$on('$destroy', this.destroy);
};
+ /**
+ * @private
+ */
TimeConductorController.prototype.destroy = function () {
- this.conductor.off('bounds', this.setFormFromBounds);
+ this.conductor.off('bounds', this.changeBounds);
this.conductor.off('timeSystem', this.changeTimeSystem);
+
+ this.conductorViewService.off('pan', this.onPan);
+ this.conductorViewService.off('pan-stop', this.onPanStop);
+ };
+
+ /**
+ * When the conductor bounds change, set the bounds in the form.
+ * @private
+ * @param {TimeConductorBounds} bounds
+ */
+ TimeConductorController.prototype.changeBounds = function (bounds) {
+ //If a zoom or pan is currently in progress, do not override form values.
+ if (!this.zooming && !this.panning) {
+ this.setFormFromBounds(bounds);
+ }
+ };
+
+ /**
+ * Does the currently selected time system support zooming? To
+ * support zooming a time system must, at a minimum, define some
+ * values for maximum and minimum zoom levels. Additionally
+ * TimeFormats, a related concept, may also support providing time
+ * unit feedback for the zoom level label, eg "seconds, minutes,
+ * hours, etc..."
+ * @returns {boolean}
+ */
+ TimeConductorController.prototype.supportsZoom = function () {
+ var timeSystem = this.conductor.timeSystem();
+ return timeSystem &&
+ timeSystem.defaults() &&
+ timeSystem.defaults().zoom;
};
/**
* Called when the bounds change in the time conductor. Synchronizes
* the bounds values in the time conductor with those in the form
- *
- * @private
+ * @param {TimeConductorBounds}
*/
TimeConductorController.prototype.setFormFromBounds = function (bounds) {
- this.$scope.boundsModel.start = bounds.start;
- this.$scope.boundsModel.end = bounds.end;
- if (!this.pendingUpdate) {
- this.pendingUpdate = true;
- this.$window.requestAnimationFrame(function () {
- this.pendingUpdate = false;
- this.$scope.$digest();
- }.bind(this));
+ if (!this.zooming && !this.panning) {
+ this.$scope.boundsModel.start = bounds.start;
+ this.$scope.boundsModel.end = bounds.end;
+
+ if (this.supportsZoom()) {
+ this.currentZoom = this.toSliderValue(bounds.end - bounds.start);
+ this.toTimeUnits(bounds.end - bounds.start);
+ }
+
+ if (!this.pendingUpdate) {
+ this.pendingUpdate = true;
+ this.$window.requestAnimationFrame(function () {
+ this.pendingUpdate = false;
+ this.$scope.$digest();
+ }.bind(this));
+ }
}
};
/**
- * @private
+ * On mode change, populate form based on time systems available
+ * from the selected mode.
+ * @param mode
*/
TimeConductorController.prototype.setFormFromMode = function (mode) {
this.$scope.modeModel.selectedKey = mode;
@@ -138,6 +192,7 @@ define(
};
/**
+ * When the deltas change, update the values in the UI
* @private
*/
TimeConductorController.prototype.setFormFromDeltas = function (deltas) {
@@ -146,14 +201,20 @@ define(
};
/**
- * @private
+ * Initialize the form when time system changes.
+ * @param {TimeSystem} timeSystem
*/
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();
+ if (this.supportsZoom()) {
+ timeSystemModel.minZoom = timeSystem.defaults().zoom.min;
+ timeSystemModel.maxZoom = timeSystem.defaults().zoom.max;
+ }
+ };
/**
* Called when form values are changed. Synchronizes the form with
@@ -222,11 +283,11 @@ define(
* Sets the selected time system. Will populate form with the default
* bounds and deltas defined in the selected time system.
*
- * @private
* @param newTimeSystem
*/
TimeConductorController.prototype.changeTimeSystem = function (newTimeSystem) {
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};
@@ -234,10 +295,96 @@ define(
this.setFormFromDeltas(deltas);
this.setFormFromBounds(bounds);
}
- this.setFormFromTimeSystem(newTimeSystem);
}
};
+ /**
+ * Takes a time span and calculates a slider increment value, used
+ * to set the horizontal offset of the slider.
+ * @param {number} timeSpan a duration of time, in ms
+ * @returns {number} a value between 0.01 and 0.99, in increments of .01
+ */
+ 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);
+ }
+ };
+
+ /**
+ * Given a time span, set a label for the units of time that it,
+ * roughly, represents. Leverages
+ * @param {TimeSpan} timeSpan
+ */
+ 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);
+ }
+ };
+
+ /**
+ * Zooming occurs when the user manipulates the zoom slider.
+ * Zooming updates the scale and bounds fields immediately, but does
+ * not trigger a bounds change to other views until the mouse button
+ * is released.
+ * @param bounds
+ */
+ TimeConductorController.prototype.onZoom = function (sliderValue) {
+ var zoomDefaults = this.conductor.timeSystem().defaults().zoom;
+ var timeSpan = Math.pow((1 - sliderValue), 4) * (zoomDefaults.min - zoomDefaults.max);
+
+ var zoom = this.conductorViewService.zoom(timeSpan);
+
+ this.$scope.boundsModel.start = zoom.bounds.start;
+ this.$scope.boundsModel.end = zoom.bounds.end;
+ this.toTimeUnits(zoom.bounds.end - zoom.bounds.start);
+
+ if (zoom.deltas) {
+ this.setFormFromDeltas(zoom.deltas);
+ }
+ };
+
+ /**
+ * Fired when user has released the zoom slider
+ * @event platform.features.conductor.TimeConductorController~zoomStop
+ */
+ /**
+ * Invoked when zoom slider is released by user. Will update the time conductor with the new bounds, triggering
+ * a global bounds change event.
+ * @fires platform.features.conductor.TimeConductorController~zoomStop
+ */
+ TimeConductorController.prototype.onZoomStop = function () {
+ this.updateBoundsFromForm(this.$scope.boundsModel);
+ this.updateDeltasFromForm(this.$scope.boundsModel);
+ this.zooming = false;
+
+ this.conductorViewService.emit('zoom-stop');
+ };
+
+ /**
+ * Panning occurs when the user grabs the conductor scale and drags
+ * it left or right to slide the window of time represented by the
+ * conductor. Panning updates the scale and bounds fields
+ * immediately, but does not trigger a bounds change to other views
+ * until the mouse button is released.
+ * @param {TimeConductorBounds} bounds
+ */
+ TimeConductorController.prototype.onPan = function (bounds) {
+ this.panning = true;
+ this.$scope.boundsModel.start = bounds.start;
+ this.$scope.boundsModel.end = bounds.end;
+ };
+
+ /**
+ * Called when the user releases the mouse button after panning.
+ */
+ TimeConductorController.prototype.onPanStop = function () {
+ this.panning = false;
+ };
+
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 39759c60f5..8495336972 100644
--- a/platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js
+++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js
@@ -28,6 +28,8 @@ define(['./TimeConductorController'], function (TimeConductorController) {
var mockConductorViewService;
var mockTimeSystems;
var controller;
+ var mockFormatService;
+ var mockFormat;
beforeEach(function () {
mockScope = jasmine.createSpyObj("$scope", [
@@ -52,36 +54,33 @@ define(['./TimeConductorController'], function (TimeConductorController) {
"availableModes",
"mode",
"availableTimeSystems",
- "deltas"
+ "deltas",
+ "deltas",
+ "on",
+ "off"
]
);
mockConductorViewService.availableModes.andReturn([]);
mockConductorViewService.availableTimeSystems.andReturn([]);
+ mockFormatService = jasmine.createSpyObj('formatService', [
+ 'getFormat'
+ ]);
+ mockFormat = jasmine.createSpyObj('format', [
+ 'format'
+ ]);
+ mockFormatService.getFormat.andReturn(mockFormat);
+
mockTimeSystems = [];
});
- function getListener(name) {
- return mockTimeConductor.on.calls.filter(function (call) {
- return call.args[0] === name;
+ function getListener(target, event) {
+ return target.calls.filter(function (call) {
+ return call.args[0] === event;
})[0].args[1];
}
- describe("", function () {
- beforeEach(function () {
- controller = new TimeConductorController(
- mockScope,
- mockWindow,
- mockTimeConductor,
- mockConductorViewService,
- mockTimeSystems
- );
- });
-
- });
-
describe("when time conductor state changes", function () {
- var mockFormat;
var mockDeltaFormat;
var defaultBounds;
var defaultDeltas;
@@ -119,17 +118,18 @@ define(['./TimeConductorController'], function (TimeConductorController) {
controller = new TimeConductorController(
mockScope,
mockWindow,
- mockTimeConductor,
+ {conductor: mockTimeConductor},
mockConductorViewService,
- mockTimeSystems
+ mockTimeSystems,
+ mockFormatService
);
- tsListener = getListener("timeSystem");
+ tsListener = getListener(mockTimeConductor.on, "timeSystem");
});
it("listens for changes to conductor state", function () {
expect(mockTimeConductor.on).toHaveBeenCalledWith("timeSystem", controller.changeTimeSystem);
- expect(mockTimeConductor.on).toHaveBeenCalledWith("bounds", controller.setFormFromBounds);
+ expect(mockTimeConductor.on).toHaveBeenCalledWith("bounds", controller.changeBounds);
});
it("deregisters conductor listens when scope is destroyed", function () {
@@ -137,7 +137,7 @@ define(['./TimeConductorController'], function (TimeConductorController) {
controller.destroy();
expect(mockTimeConductor.off).toHaveBeenCalledWith("timeSystem", controller.changeTimeSystem);
- expect(mockTimeConductor.off).toHaveBeenCalledWith("bounds", controller.setFormFromBounds);
+ expect(mockTimeConductor.off).toHaveBeenCalledWith("bounds", controller.changeBounds);
});
it("when time system changes, sets time system on scope", function () {
@@ -151,7 +151,11 @@ define(['./TimeConductorController'], function (TimeConductorController) {
});
it("when time system changes, sets defaults on scope", function () {
- expect(tsListener).toBeDefined();
+ mockDefaults.zoom = {
+ min: 100,
+ max: 10
+ };
+ mockTimeConductor.timeSystem.andReturn(timeSystem);
tsListener(timeSystem);
expect(mockScope.boundsModel.start).toEqual(defaultBounds.start);
@@ -159,6 +163,32 @@ define(['./TimeConductorController'], function (TimeConductorController) {
expect(mockScope.boundsModel.startDelta).toEqual(defaultDeltas.start);
expect(mockScope.boundsModel.endDelta).toEqual(defaultDeltas.end);
+
+ expect(mockScope.timeSystemModel.minZoom).toBe(mockDefaults.zoom.min);
+ expect(mockScope.timeSystemModel.maxZoom).toBe(mockDefaults.zoom.max);
+ });
+
+ it("when bounds change, sets the correct zoom slider value", function () {
+ var bounds = {
+ start: 0,
+ end: 50
+ };
+ mockDefaults.zoom = {
+ min: 100,
+ max: 0
+ };
+
+ function exponentializer(rawValue) {
+ return 1 - Math.pow(rawValue, 1 / 4);
+ }
+
+ mockTimeConductor.timeSystem.andReturn(timeSystem);
+ //Set zoom defaults
+ tsListener(timeSystem);
+
+ controller.changeBounds(bounds);
+ expect(controller.currentZoom).toEqual(exponentializer(0.5));
+
});
it("when bounds change, sets them on scope", function () {
@@ -167,7 +197,7 @@ define(['./TimeConductorController'], function (TimeConductorController) {
end: 2
};
- var boundsListener = getListener("bounds");
+ var boundsListener = getListener(mockTimeConductor.on, "bounds");
expect(boundsListener).toBeDefined();
boundsListener(bounds);
@@ -225,9 +255,10 @@ define(['./TimeConductorController'], function (TimeConductorController) {
controller = new TimeConductorController(
mockScope,
mockWindow,
- mockTimeConductor,
+ {conductor: mockTimeConductor},
mockConductorViewService,
- mockTimeSystemConstructors
+ mockTimeSystemConstructors,
+ mockFormatService
);
mockConductorViewService.availableTimeSystems.andReturn(mockTimeSystems);
@@ -240,9 +271,10 @@ define(['./TimeConductorController'], function (TimeConductorController) {
controller = new TimeConductorController(
mockScope,
mockWindow,
- mockTimeConductor,
+ {conductor: mockTimeConductor},
mockConductorViewService,
- mockTimeSystemConstructors
+ mockTimeSystemConstructors,
+ mockFormatService
);
mockConductorViewService.availableTimeSystems.andReturn(mockTimeSystems);
@@ -264,9 +296,10 @@ define(['./TimeConductorController'], function (TimeConductorController) {
controller = new TimeConductorController(
mockScope,
mockWindow,
- mockTimeConductor,
+ {conductor: mockTimeConductor},
mockConductorViewService,
- mockTimeSystemConstructors
+ mockTimeSystemConstructors,
+ mockFormatService
);
controller.updateBoundsFromForm(formModel);
@@ -286,9 +319,10 @@ define(['./TimeConductorController'], function (TimeConductorController) {
controller = new TimeConductorController(
mockScope,
mockWindow,
- mockTimeConductor,
+ {conductor: mockTimeConductor},
mockConductorViewService,
- mockTimeSystemConstructors
+ mockTimeSystemConstructors,
+ mockFormatService
);
controller.updateDeltasFromForm(formModel);
@@ -321,14 +355,33 @@ define(['./TimeConductorController'], function (TimeConductorController) {
controller = new TimeConductorController(
mockScope,
mockWindow,
- mockTimeConductor,
+ {conductor: mockTimeConductor},
mockConductorViewService,
- mockTimeSystems
+ mockTimeSystems,
+ mockFormatService
);
controller.selectTimeSystemByKey('testTimeSystem');
expect(mockTimeConductor.timeSystem).toHaveBeenCalledWith(timeSystem, defaultBounds);
});
+
+ it("updates form bounds during pan events", function () {
+ var testBounds = {
+ start: 10,
+ end: 20
+ };
+
+ expect(controller.$scope.boundsModel.start).not.toBe(testBounds.start);
+ expect(controller.$scope.boundsModel.end).not.toBe(testBounds.end);
+
+ expect(controller.conductorViewService.on).toHaveBeenCalledWith("pan",
+ controller.onPan);
+
+ getListener(controller.conductorViewService.on, "pan")(testBounds);
+
+ expect(controller.$scope.boundsModel.start).toBe(testBounds.start);
+ expect(controller.$scope.boundsModel.end).toBe(testBounds.end);
+ });
});
});
diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorMode.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorMode.js
index 1f9d3656af..ef13106107 100644
--- a/platform/features/conductor-v2/conductor/src/ui/TimeConductorMode.js
+++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorMode.js
@@ -28,13 +28,14 @@ define(
* Supports mode-specific time conductor behavior.
*
* @constructor
+ * @memberof platform.features.conductor
* @param {TimeConductorMetadata} metadata
*/
function TimeConductorMode(metadata, conductor, timeSystems) {
this.conductor = conductor;
this.mdata = metadata;
- this.dlts = undefined;
+ this.deltasVal = undefined;
this.source = undefined;
this.sourceUnlisten = undefined;
this.systems = timeSystems;
@@ -141,6 +142,9 @@ define(
return this.source;
};
+ /**
+ * @private
+ */
TimeConductorMode.prototype.destroy = function () {
this.conductor.off('timeSystem', this.changeTimeSystem);
@@ -177,23 +181,66 @@ define(
*/
TimeConductorMode.prototype.deltas = function (deltas) {
if (arguments.length !== 0) {
- var oldEnd = this.conductor.bounds().end;
-
- if (this.dlts && this.dlts.end !== undefined) {
- //Calculate the previous raw end value (without delta)
- oldEnd = oldEnd - this.dlts.end;
+ var bounds = this.calculateBoundsFromDeltas(deltas);
+ this.deltasVal = deltas;
+ if (this.metadata().key !== 'fixed') {
+ this.conductor.bounds(bounds);
}
-
- this.dlts = deltas;
-
- var newBounds = {
- start: oldEnd - this.dlts.start,
- end: oldEnd + this.dlts.end
- };
-
- this.conductor.bounds(newBounds);
}
- return this.dlts;
+ return this.deltasVal;
+ };
+
+ /**
+ * @param deltas
+ * @returns {TimeConductorBounds}
+ */
+ TimeConductorMode.prototype.calculateBoundsFromDeltas = function (deltas) {
+ var oldEnd = this.conductor.bounds().end;
+
+ if (this.deltasVal && this.deltasVal.end !== undefined) {
+ //Calculate the previous raw end value (without delta)
+ oldEnd = oldEnd - this.deltasVal.end;
+ }
+
+ var bounds = {
+ start: oldEnd - deltas.start,
+ end: oldEnd + deltas.end
+ };
+ return bounds;
+ };
+
+ /**
+ * @typedef {Object} ZoomLevel
+ * @property {TimeConductorBounds} bounds The calculated bounds based on the zoom level
+ * @property {TimeConductorDeltas} deltas The calculated deltas based on the zoom level
+ */
+ /**
+ * Calculates bounds and deltas based on provided timeSpan. Collectively
+ * the bounds and deltas will constitute the new zoom level.
+ * @param {number} timeSpan time duration in ms.
+ * @return {ZoomLevel} The new zoom bounds and delta calculated for the provided time span
+ */
+ TimeConductorMode.prototype.calculateZoom = function (timeSpan) {
+ var zoom = {};
+
+ // If a tick source is defined, then the concept of 'now' is
+ // important. Calculate zoom based on 'now'.
+ if (this.tickSource()) {
+ zoom.deltas = {
+ start: timeSpan,
+ end: this.deltasVal.end
+ };
+ zoom.bounds = this.calculateBoundsFromDeltas(zoom.deltas);
+ // Calculate bounds based on deltas;
+ } else {
+ var bounds = this.conductor.bounds();
+ var center = bounds.start + ((bounds.end - bounds.start)) / 2;
+ bounds.start = center - timeSpan / 2;
+ bounds.end = center + timeSpan / 2;
+ zoom.bounds = bounds;
+ }
+
+ return zoom;
};
return TimeConductorMode;
diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewService.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewService.js
index 8cbc349520..a0e86ac6d8 100644
--- a/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewService.js
+++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewService.js
@@ -22,25 +22,30 @@
define(
[
+ 'EventEmitter',
'./TimeConductorMode'
],
- function (TimeConductorMode) {
+ function (EventEmitter, TimeConductorMode) {
/**
* A class representing the state of the time conductor view. This
* exposes details of the UI that are not represented on the
* TimeConductor API itself such as modes and deltas.
*
+ * @memberof platform.features.conductor
* @param conductor
* @param timeSystems
* @constructor
*/
- function TimeConductorViewService(conductor, timeSystems) {
+ function TimeConductorViewService(openmct, timeSystems) {
+
+ EventEmitter.call(this);
+
this.systems = timeSystems.map(function (timeSystemConstructor) {
return timeSystemConstructor();
});
- this.conductor = conductor;
+ this.conductor = openmct.conductor;
this.currentMode = undefined;
/**
@@ -97,6 +102,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
@@ -144,7 +151,7 @@ define(
};
/**
- * @typedef {object} Delta
+ * @typedef {object} TimeConductorDeltas
* @property {number} start Used to set the start bound of the
* TimeConductor on tick. A positive value that will be subtracted
* from the value provided by a tick source to determine the start
@@ -171,7 +178,7 @@ define(
* tick
* - end: A time in ms after the timestamp of the last data received
* which will be used to determine the 'end' bound on tick
- * @returns {Delta} current value of the deltas
+ * @returns {TimeConductorDeltas} current value of the deltas
*/
TimeConductorViewService.prototype.deltas = function () {
//Deltas stored on mode. Use .apply to preserve arguments
@@ -197,6 +204,26 @@ define(
return this.currentMode.availableTimeSystems();
};
+ /**
+ * An event to indicate that zooming is taking place
+ * @event platform.features.conductor.TimeConductorViewService~zoom
+ * @property {ZoomLevel} zoom the new zoom level.
+ */
+ /**
+ * Zoom to given time span. Will fire a zoom event with new zoom
+ * bounds. Zoom bounds emitted in this way are considered ephemeral
+ * and should be overridden by any time conductor bounds events. Does
+ * not set bounds globally.
+ * @param {number} zoom A time duration in ms
+ * @fires platform.features.conductor.TimeConductorViewService~zoom
+ * @see module:openmct.TimeConductor#bounds
+ */
+ TimeConductorViewService.prototype.zoom = function (timeSpan) {
+ var zoom = this.currentMode.calculateZoom(timeSpan);
+ this.emit("zoom", zoom);
+ return zoom;
+ };
+
return TimeConductorViewService;
}
);
diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewServiceSpec.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewServiceSpec.js
index 39e7810320..a096394ee2 100644
--- a/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewServiceSpec.js
+++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewServiceSpec.js
@@ -87,7 +87,7 @@ define(['./TimeConductorViewService'], function (TimeConductorViewService) {
it("At a minimum supports fixed mode", function () {
var mockTimeSystems = [mockConstructor(basicTimeSystem)];
- viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems);
+ viewService = new TimeConductorViewService({conductor: mockTimeConductor}, mockTimeSystems);
var availableModes = viewService.availableModes();
expect(availableModes.fixed).toBeDefined();
@@ -102,7 +102,7 @@ define(['./TimeConductorViewService'], function (TimeConductorViewService) {
};
tickingTimeSystem.tickSources.andReturn([mockRealtimeTickSource]);
- viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems);
+ viewService = new TimeConductorViewService({conductor: mockTimeConductor}, mockTimeSystems);
var availableModes = viewService.availableModes();
expect(availableModes.realtime).toBeDefined();
@@ -117,7 +117,7 @@ define(['./TimeConductorViewService'], function (TimeConductorViewService) {
};
tickingTimeSystem.tickSources.andReturn([mockLADTickSource]);
- viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems);
+ viewService = new TimeConductorViewService({conductor: mockTimeConductor}, mockTimeSystems);
var availableModes = viewService.availableModes();
expect(availableModes.lad).toBeDefined();
@@ -132,7 +132,7 @@ define(['./TimeConductorViewService'], function (TimeConductorViewService) {
"destroy"
]);
- viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems);
+ viewService = new TimeConductorViewService({conductor: mockTimeConductor}, mockTimeSystems);
viewService.currentMode = oldMode;
viewService.mode('fixed');
expect(oldMode.destroy).toHaveBeenCalled();
@@ -149,7 +149,7 @@ define(['./TimeConductorViewService'], function (TimeConductorViewService) {
};
tickingTimeSystem.tickSources.andReturn([mockRealtimeTickSource]);
- viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems);
+ viewService = new TimeConductorViewService({conductor: mockTimeConductor}, mockTimeSystems);
//Set time system to one known to support realtime mode
mockTimeConductor.timeSystem.andReturn(tickingTimeSystem);
@@ -169,7 +169,7 @@ define(['./TimeConductorViewService'], function (TimeConductorViewService) {
};
tickingTimeSystem.tickSources.andReturn([mockRealtimeTickSource]);
- viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems);
+ viewService = new TimeConductorViewService({conductor: mockTimeConductor}, mockTimeSystems);
//Set time system to one known to not support realtime mode
mockTimeConductor.timeSystem.andReturn(basicTimeSystem);
diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeOfInterestController.js b/platform/features/conductor-v2/conductor/src/ui/TimeOfInterestController.js
new file mode 100644
index 0000000000..39a4b5be6c
--- /dev/null
+++ b/platform/features/conductor-v2/conductor/src/ui/TimeOfInterestController.js
@@ -0,0 +1,109 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+define(
+ [],
+ function () {
+
+ /**
+ * Controller for the Time of Interest element used in various views to display the TOI. Responsible for setting
+ * the text label for the current TOI, and for toggling the (un)pinned state which determines whether the TOI
+ * indicator is visible.
+ * @constructor
+ */
+ function TimeOfInterestController($scope, openmct, formatService) {
+ this.conductor = openmct.conductor;
+ this.formatService = formatService;
+ this.format = undefined;
+ this.toiText = undefined;
+ this.$scope = $scope;
+
+ //Bind all class functions to 'this'
+ Object.keys(TimeOfInterestController.prototype).filter(function (key) {
+ return typeof TimeOfInterestController.prototype[key] === 'function';
+ }).forEach(function (key) {
+ this[key] = TimeOfInterestController.prototype[key].bind(this);
+ }.bind(this));
+
+ this.conductor.on('timeOfInterest', this.changeTimeOfInterest);
+ this.conductor.on('timeSystem', this.changeTimeSystem);
+ if (this.conductor.timeSystem()) {
+ this.changeTimeSystem(this.conductor.timeSystem());
+ var toi = this.conductor.timeOfInterest();
+ if (toi) {
+ this.changeTimeOfInterest(toi);
+ }
+ }
+
+ $scope.$on('$destroy', this.destroy);
+ }
+
+ /**
+ * Called when the time of interest changes on the conductor. Will pin (display) the TOI indicator, and set the
+ * text using the default formatter of the currently active Time System.
+ * @private
+ * @param {integer} toi Current time of interest in ms
+ */
+ TimeOfInterestController.prototype.changeTimeOfInterest = function (toi) {
+ if (toi !== undefined) {
+ this.$scope.pinned = true;
+ this.toiText = this.format.format(toi);
+ } else {
+ this.$scope.pinned = false;
+ }
+ };
+
+ /**
+ * When time system is changed, update the formatter used to
+ * display the current TOI label
+ */
+ TimeOfInterestController.prototype.changeTimeSystem = function (timeSystem) {
+ this.format = this.formatService.getFormat(timeSystem.formats()[0]);
+ };
+
+ /**
+ * @private
+ */
+ TimeOfInterestController.prototype.destroy = function () {
+ this.conductor.off('timeOfInterest', this.changeTimeOfInterest);
+ this.conductor.off('timeSystem', this.changeTimeSystem);
+ };
+
+ /**
+ * Will unpin (hide) the TOI indicator. Has the effect of setting the time of interest to `undefined` on the
+ * Time Conductor
+ */
+ TimeOfInterestController.prototype.dismiss = function () {
+ this.conductor.timeOfInterest(undefined);
+ };
+
+ /**
+ * Sends out a time of interest event with the effect of resetting
+ * the TOI displayed in views.
+ */
+ TimeOfInterestController.prototype.resync = function () {
+ this.conductor.timeOfInterest(this.conductor.timeOfInterest());
+ };
+
+ return TimeOfInterestController;
+ }
+);
diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeOfInterestControllerSpec.js b/platform/features/conductor-v2/conductor/src/ui/TimeOfInterestControllerSpec.js
new file mode 100644
index 0000000000..0062ec79b8
--- /dev/null
+++ b/platform/features/conductor-v2/conductor/src/ui/TimeOfInterestControllerSpec.js
@@ -0,0 +1,115 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+define(['./TimeOfInterestController'], function (TimeOfInterestController) {
+
+ describe("The time of interest controller", function () {
+ var controller;
+ var mockScope;
+ var mockConductor;
+ var mockFormatService;
+ var mockTimeSystem;
+ var mockFormat;
+
+ beforeEach(function () {
+ mockConductor = jasmine.createSpyObj("conductor", [
+ "on",
+ "timeSystem"
+ ]);
+ mockScope = jasmine.createSpyObj("scope", [
+ "$on"
+ ]);
+
+ mockFormat = jasmine.createSpyObj("format", [
+ "format"
+ ]);
+
+ mockFormatService = jasmine.createSpyObj("formatService", [
+ "getFormat"
+ ]);
+
+ mockFormatService.getFormat.andReturn(mockFormat);
+
+ mockTimeSystem = {
+ formats: function () {
+ return ["mockFormat"];
+ }
+ };
+
+ controller = new TimeOfInterestController(mockScope, {conductor: mockConductor}, mockFormatService);
+ });
+
+ function getCallback(target, event) {
+ return target.calls.filter(function (call) {
+ return call.args[0] === event;
+ })[0].args[1];
+ }
+
+ it("Listens for changes to TOI", function () {
+ expect(mockConductor.on).toHaveBeenCalledWith("timeOfInterest", controller.changeTimeOfInterest);
+ });
+
+ it("updates format when time system changes", function () {
+ expect(mockConductor.on).toHaveBeenCalledWith("timeSystem", controller.changeTimeSystem);
+ getCallback(mockConductor.on, "timeSystem")(mockTimeSystem);
+ expect(controller.format).toBe(mockFormat);
+ });
+
+ describe("When TOI changes", function () {
+ var toi;
+ var toiCallback;
+ var formattedTOI;
+
+ beforeEach(function () {
+ var timeSystemCallback = getCallback(mockConductor.on, "timeSystem");
+ toi = 1;
+ mockConductor.timeSystem.andReturn(mockTimeSystem);
+
+ //Set time system
+ timeSystemCallback(mockTimeSystem);
+
+ toiCallback = getCallback(mockConductor.on, "timeOfInterest");
+ formattedTOI = "formatted TOI";
+
+ mockFormatService.getFormat.andReturn("mockFormat");
+ mockFormat.format.andReturn(formattedTOI);
+ });
+ it("Uses the time system formatter to produce TOI text", function () {
+ toiCallback = getCallback(mockConductor.on, "timeOfInterest");
+ //Set TOI
+ toiCallback(toi);
+ expect(mockFormat.format).toHaveBeenCalled();
+ });
+ it("Sets the time of interest text", function () {
+ //Set TOI
+ toiCallback(toi);
+ expect(controller.toiText).toBe(formattedTOI);
+ });
+ it("Pins the time of interest", function () {
+ //Set TOI
+ toiCallback(toi);
+ expect(mockScope.pinned).toBe(true);
+ });
+ });
+
+ });
+});
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}
};
};
diff --git a/platform/features/conductor/res/sass/time-conductor.scss b/platform/features/conductor/res/sass/time-conductor.scss
index bc50b42da5..9ec9ff1dc1 100644
--- a/platform/features/conductor/res/sass/time-conductor.scss
+++ b/platform/features/conductor/res/sass/time-conductor.scss
@@ -164,7 +164,7 @@ $ueTimeConductorH: (33px, 18px, 20px);
margin-left: 0;
}
.l-time-range-tick-label {
- @include webkitProp(transform, translateX(-50%));
+ @include transform(translateX(-50%));
color: $colorPlotLabelFg;
display: inline-block;
font-size: 0.7rem;
diff --git a/platform/features/plot/bundle.js b/platform/features/plot/bundle.js
index ec4e6f375a..e23b4add82 100644
--- a/platform/features/plot/bundle.js
+++ b/platform/features/plot/bundle.js
@@ -77,7 +77,8 @@ define([
"telemetryFormatter",
"telemetryHandler",
"throttle",
- "PLOT_FIXED_DURATION"
+ "PLOT_FIXED_DURATION",
+ "openmct"
]
},
{
diff --git a/platform/features/plot/res/templates/plot.html b/platform/features/plot/res/templates/plot.html
index 53b5d348b0..bcd1d1899b 100644
--- a/platform/features/plot/res/templates/plot.html
+++ b/platform/features/plot/res/templates/plot.html
@@ -40,21 +40,16 @@
ng-style="{ height: 100 / plot.getSubPlots().length + '%'}"
ng-repeat="subplot in plot.getSubPlots()">
-
-
-
+
+
+
+ {{telemetryObject.getModel().name}}
- {{telemetryObject.getModel().name}}
-
-
-
- {{subplot.getHoverCoordinates()}}
+
{{axes[1].active.name}}
@@ -74,81 +69,96 @@