diff --git a/platform/commonUI/general/res/sass/_status.scss b/platform/commonUI/general/res/sass/_status.scss index a07e9a3247..428ee37016 100644 --- a/platform/commonUI/general/res/sass/_status.scss +++ b/platform/commonUI/general/res/sass/_status.scss @@ -50,7 +50,6 @@ content:''; font-family: symbolsfont; font-size: 0.8em; - display: inline; margin-right: $interiorMarginSm; } } diff --git a/platform/commonUI/general/res/sass/controls/_messages.scss b/platform/commonUI/general/res/sass/controls/_messages.scss index fcc0df7665..f637f1e4ed 100644 --- a/platform/commonUI/general/res/sass/controls/_messages.scss +++ b/platform/commonUI/general/res/sass/controls/_messages.scss @@ -120,7 +120,11 @@ } .status-indicator { + background: none !important; margin-right: $interiorMarginSm; + &[class*='s-status']:before { + font-size: 1em; + } } .count { diff --git a/platform/features/clock/bundle.js b/platform/features/clock/bundle.js index d3f98426bf..54908385c0 100644 --- a/platform/features/clock/bundle.js +++ b/platform/features/clock/bundle.js @@ -23,10 +23,13 @@ define([ "moment-timezone", "./src/indicators/ClockIndicator", + "./src/indicators/FollowIndicator", "./src/services/TickerService", + "./src/services/TimerService", "./src/controllers/ClockController", "./src/controllers/TimerController", "./src/controllers/RefreshingController", + "./src/actions/FollowTimerAction", "./src/actions/StartTimerAction", "./src/actions/RestartTimerAction", "./src/actions/StopTimerAction", @@ -37,10 +40,13 @@ define([ ], function ( MomentTimezone, ClockIndicator, + FollowIndicator, TickerService, + TimerService, ClockController, TimerController, RefreshingController, + FollowTimerAction, StartTimerAction, RestartTimerAction, StopTimerAction, @@ -80,6 +86,11 @@ define([ "CLOCK_INDICATOR_FORMAT" ], "priority": "preferred" + }, + { + "implementation": FollowIndicator, + "depends": ["timerService"], + "priority": "fallback" } ], "services": [ @@ -90,6 +101,11 @@ define([ "$timeout", "now" ] + }, + { + "key": "timerService", + "implementation": TimerService, + "depends": ["openmct"] } ], "controllers": [ @@ -134,6 +150,15 @@ define([ } ], "actions": [ + { + "key": "timer.follow", + "implementation": FollowTimerAction, + "depends": ["timerService"], + "category": "contextual", + "name": "Follow Timer", + "cssClass": "icon-clock", + "priority": "optional" + }, { "key": "timer.start", "implementation": StartTimerAction, diff --git a/platform/features/clock/src/actions/FollowTimerAction.js b/platform/features/clock/src/actions/FollowTimerAction.js new file mode 100644 index 0000000000..450cea7286 --- /dev/null +++ b/platform/features/clock/src/actions/FollowTimerAction.js @@ -0,0 +1,56 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2009-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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 () { + + /** + * Designates a specific timer for following. Timelines, for example, + * use the actively followed timer to display a time-of-interest line + * and interpret time conductor bounds in the Timeline's relative + * time frame. + * + * @implements {Action} + * @memberof platform/features/clock + * @constructor + * @param {ActionContext} context the context for this action + */ + function FollowTimerAction(timerService, context) { + var domainObject = + context.domainObject && + context.domainObject.useCapability('adapter'); + this.perform = + timerService.setTimer.bind(timerService, domainObject); + } + + FollowTimerAction.appliesTo = function (context) { + var model = + (context.domainObject && context.domainObject.getModel()) || + {}; + + return model.type === 'timer'; + }; + + return FollowTimerAction; + } +); diff --git a/platform/features/clock/src/indicators/FollowIndicator.js b/platform/features/clock/src/indicators/FollowIndicator.js new file mode 100644 index 0000000000..d20f8975d5 --- /dev/null +++ b/platform/features/clock/src/indicators/FollowIndicator.js @@ -0,0 +1,57 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2009-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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( + ['moment'], + function (moment) { + var NO_TIMER = "No timer being followed"; + + /** + * Indicator that displays the active timer, as well as its + * current state. + * @implements {Indicator} + * @memberof platform/features/clock + */ + function FollowIndicator(timerService) { + this.timerService = timerService; + } + + FollowIndicator.prototype.getGlyphClass = function () { + return ""; + }; + + FollowIndicator.prototype.getCssClass = function () { + return (this.timerService.getTimer()) ? "icon-timer s-status-ok" : "icon-timer"; + }; + + FollowIndicator.prototype.getText = function () { + var timer = this.timerService.getTimer(); + return (timer) ? 'Following timer ' + timer.getModel().name : NO_TIMER; + }; + + FollowIndicator.prototype.getDescription = function () { + return ""; + }; + + return FollowIndicator; + } +); diff --git a/platform/features/clock/src/services/TimerService.js b/platform/features/clock/src/services/TimerService.js new file mode 100644 index 0000000000..32f46d35ab --- /dev/null +++ b/platform/features/clock/src/services/TimerService.js @@ -0,0 +1,113 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2009-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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) { + + /** + * Tracks the currently-followed Timer object. Used by + * timelines et al to synchronize to a particular timer. + * + * The TimerService emits `change` events when the active timer + * is changed. + */ + function TimerService(openmct) { + EventEmitter.apply(this); + this.time = openmct.time; + this.objects = openmct.objects; + } + + TimerService.prototype = Object.create(EventEmitter.prototype); + + /** + * Set (or clear, if `timer` is undefined) the currently active timer. + * @param {DomainObject} timer the new active timer + * @emits change + */ + TimerService.prototype.setTimer = function (timer) { + this.timer = timer; + this.emit('change'); + + if (this.stopObserving) { + this.stopObserving(); + delete this.stopObserving; + } + + if (timer) { + this.stopObserving = + this.objects.observe(timer, '*', this.setTimer.bind(this)); + } + }; + + /** + * Get the currently active timer. + * @return {DomainObject} the active timer + * @emits change + */ + TimerService.prototype.getTimer = function () { + return this.timer; + }; + + + /** + * Check if there is a currently active timer. + * @return {boolean} true if there is a timer + */ + TimerService.prototype.hasTimer = function () { + return !!this.timer; + }; + + /** + * Convert the provided timestamp to milliseconds relative to + * the active timer. + * @return {number} milliseconds since timer start + */ + TimerService.prototype.convert = function (timestamp) { + var clock = this.time.clock(); + var canConvert = this.hasTimer() && + !!clock && + this.timer.timerState !== 'stopped'; + + if (!canConvert) { + return undefined; + } + + var now = clock.currentValue(); + var delta = this.timer.timerState === 'paused' ? + now - this.timer.pausedTime : 0; + var epoch = this.timer.timestamp; + + return timestamp - epoch - delta; + }; + + /** + * Get the value of the active clock, adjusted to be relative to the active + * timer. If there is no clock or no active timer, this will return + * `undefined`. + * @return {number} milliseconds since the start of the active timer + */ + TimerService.prototype.now = function () { + var clock = this.time.clock(); + return clock && this.convert(clock.currentValue()); + }; + + return TimerService; +}); diff --git a/platform/features/clock/test/actions/FollowTimerActionSpec.js b/platform/features/clock/test/actions/FollowTimerActionSpec.js new file mode 100644 index 0000000000..f4b4147adb --- /dev/null +++ b/platform/features/clock/test/actions/FollowTimerActionSpec.js @@ -0,0 +1,87 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2009-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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([ + "../../src/actions/FollowTimerAction" +], function (FollowTimerAction) { + var TIMER_SERVICE_METHODS = + ['setTimer', 'getTimer', 'clearTimer', 'on', 'off']; + + describe("The Follow Timer action", function () { + var testContext; + var testModel; + var testAdaptedObject; + + beforeEach(function () { + testModel = {}; + testContext = { domainObject: jasmine.createSpyObj('domainObject', [ + 'getModel', + 'useCapability' + ]) }; + testAdaptedObject = { foo: 'bar' }; + testContext.domainObject.getModel.andReturn(testModel); + testContext.domainObject.useCapability.andCallFake(function (c) { + return c === 'adapter' && testAdaptedObject; + }); + }); + + it("is applicable to timers", function () { + testModel.type = "timer"; + expect(FollowTimerAction.appliesTo(testContext)).toBe(true); + }); + + it("is inapplicable to non-timers", function () { + testModel.type = "folder"; + expect(FollowTimerAction.appliesTo(testContext)).toBe(false); + }); + + describe("when instantiated", function () { + var mockTimerService; + var action; + + beforeEach(function () { + mockTimerService = jasmine.createSpyObj( + 'timerService', + TIMER_SERVICE_METHODS + ); + action = new FollowTimerAction(mockTimerService, testContext); + }); + + it("does not interact with the timer service", function () { + TIMER_SERVICE_METHODS.forEach(function (method) { + expect(mockTimerService[method]).not.toHaveBeenCalled(); + }); + }); + + describe("and performed", function () { + beforeEach(function () { + action.perform(); + }); + + it("sets the active timer", function () { + expect(mockTimerService.setTimer) + .toHaveBeenCalledWith(testAdaptedObject); + }); + }); + }); + }); +}); diff --git a/platform/features/clock/test/indicators/FollowIndicatorSpec.js b/platform/features/clock/test/indicators/FollowIndicatorSpec.js new file mode 100644 index 0000000000..06710dde03 --- /dev/null +++ b/platform/features/clock/test/indicators/FollowIndicatorSpec.js @@ -0,0 +1,61 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2009-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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(["../../src/indicators/FollowIndicator"], function (FollowIndicator) { + var TIMER_SERVICE_METHODS = + ['setTimer', 'getTimer', 'clearTimer', 'on', 'off']; + + describe("The timer-following indicator", function () { + var mockTimerService; + var indicator; + + beforeEach(function () { + mockTimerService = + jasmine.createSpyObj('timerService', TIMER_SERVICE_METHODS); + indicator = new FollowIndicator(mockTimerService); + }); + + it("implements the Indicator interface", function () { + expect(indicator.getGlyphClass()).toEqual(jasmine.any(String)); + expect(indicator.getCssClass()).toEqual(jasmine.any(String)); + expect(indicator.getText()).toEqual(jasmine.any(String)); + expect(indicator.getDescription()).toEqual(jasmine.any(String)); + }); + + describe("when a timer is set", function () { + var testModel; + var mockDomainObject; + + beforeEach(function () { + testModel = { name: "some timer!" }; + mockDomainObject = jasmine.createSpyObj('timer', ['getModel']); + mockDomainObject.getModel.andReturn(testModel); + mockTimerService.getTimer.andReturn(mockDomainObject); + }); + + it("displays the timer's name", function () { + expect(indicator.getText().indexOf(testModel.name)) + .not.toEqual(-1); + }); + }); + }); +}); diff --git a/platform/features/clock/test/services/TimerServiceSpec.js b/platform/features/clock/test/services/TimerServiceSpec.js new file mode 100644 index 0000000000..08f05c83fd --- /dev/null +++ b/platform/features/clock/test/services/TimerServiceSpec.js @@ -0,0 +1,77 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2009-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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([ + '../../src/services/TimerService' +], function (TimerService) { + describe("TimerService", function () { + var callback; + var mockmct; + var timerService; + + beforeEach(function () { + callback = jasmine.createSpy('callback'); + mockmct = { + time: { clock: jasmine.createSpy('clock') }, + objects: { observe: jasmine.createSpy('observe') } + }; + timerService = new TimerService(mockmct); + timerService.on('change', callback); + }); + + it("initially emits no change events", function () { + expect(callback).not.toHaveBeenCalled(); + }); + + it("reports no current timer", function () { + expect(timerService.getTimer()).toBeUndefined(); + }); + + describe("setTimer", function () { + var testTimer; + + beforeEach(function () { + testTimer = { name: "I am some timer; you are nobody." }; + timerService.setTimer(testTimer); + }); + + it("emits a change event", function () { + expect(callback).toHaveBeenCalled(); + }); + + it("reports the current timer", function () { + expect(timerService.getTimer()).toBe(testTimer); + }); + + it("observes changes to an object", function () { + var newTimer = { name: "I am another timer." }; + expect(mockmct.objects.observe).toHaveBeenCalledWith( + testTimer, + '*', + jasmine.any(Function) + ); + mockmct.objects.observe.mostRecentCall.args[2](newTimer); + expect(timerService.getTimer()).toBe(newTimer); + }); + }); + }); +}); diff --git a/platform/features/timeline/bundle.js b/platform/features/timeline/bundle.js index 82646b55e9..eec6113054 100644 --- a/platform/features/timeline/bundle.js +++ b/platform/features/timeline/bundle.js @@ -29,6 +29,7 @@ define([ "./src/controllers/TimelineTickController", "./src/controllers/TimelineTableController", "./src/controllers/TimelineGanttController", + "./src/controllers/TimelineTOIController", "./src/controllers/ActivityModeValuesController", "./src/capabilities/ActivityTimespanCapability", "./src/capabilities/TimelineTimespanCapability", @@ -59,6 +60,7 @@ define([ TimelineTickController, TimelineTableController, TimelineGanttController, + TimelineTOIController, ActivityModeValuesController, ActivityTimespanCapability, TimelineTimespanCapability, @@ -502,6 +504,15 @@ define([ "TIMELINE_MAXIMUM_OFFSCREEN" ] }, + { + "key": "TimelineTOIController", + "implementation": TimelineTOIController, + "depends": [ + "openmct", + "timerService", + "$scope" + ] + }, { "key": "ActivityModeValuesController", "implementation": ActivityModeValuesController, diff --git a/platform/features/timeline/res/sass/_timeline-thematic.scss b/platform/features/timeline/res/sass/_timeline-thematic.scss index 590e0fd32c..09af6251f2 100644 --- a/platform/features/timeline/res/sass/_timeline-thematic.scss +++ b/platform/features/timeline/res/sass/_timeline-thematic.scss @@ -29,6 +29,44 @@ } } } + + // Follow Line + .l-follow-line { + // TODO: move before and after into l-timeline-gantt so those only render in that pane + pointer-events: none; + position: absolute; + top: 0; bottom: 0; + width: 1px; + z-index: 9; // Just below .l-hover-btns-holder + } +} + +.l-timeline-gantt { + .l-follow-line { + $d: 0.8rem; + top: $interiorMargin; + &:before, + &:after { + content: ''; + display: block; + height: $d; + width: $d; + position: absolute; + top: 0; + @include transform(translateX(-50%)); + } + &:before { + // Icon blocker + width: 2 * $d; + } + &:after { + // Icon + font-size: $d; + line-height: $d; + text-align: center; + } + } + } .s-timeline-gantt { @@ -108,10 +146,9 @@ } .s-hover-btns-holder { $bg: $timelineHeaderColorBg; - $bga: 1; $l: 5%; @include user-select(none); - @include background-image(linear-gradient(-90deg, rgba($bg, $bga), rgba($bg, $bga) 70%, rgba($bg, 0) 100%)); + @include background-image(linear-gradient(-90deg, rgba($bg, 1), rgba($bg, 1) 70%, rgba($bg, 0) 100%)); .s-button { height: 16px; line-height: 16px; @@ -129,4 +166,27 @@ color: $timelineResourceGraphFg; } } + + .s-follow-line { + background: rgba($timeControllerToiLineColor, 0.5); + } + + .s-timeline-gantt { + .s-follow-line { + &:after { + // Icon + color: $timeControllerToiLineColor; + content: $glyph-icon-timer; + font-family: symbolsfont; + text-shadow: $shdwItemText; + } + &:before { + // Blocker + $bg: $timelineHeaderColorBg; + $l: 30%; + @include background-image(linear-gradient(90deg, rgba($bg, 0), rgba($bg, 1) $l, rgba($bg, 1) 100% - $l, rgba($bg, 0))); + + } + } + } } diff --git a/platform/features/timeline/res/sass/_timelines.scss b/platform/features/timeline/res/sass/_timelines.scss index 70b5b43024..0ad22dba21 100644 --- a/platform/features/timeline/res/sass/_timelines.scss +++ b/platform/features/timeline/res/sass/_timelines.scss @@ -75,6 +75,10 @@ } } &.l-timeline-gantt { + .abs.l-timeline-gantt-header-w { + overflow: hidden; + height: $timelineTopPaneHeaderH; + } .l-swimlanes-holder { @include scrollV(scroll); bottom: $scrollbarTrackSize; diff --git a/platform/features/timeline/res/sass/timeline-espresso.scss b/platform/features/timeline/res/sass/timeline-espresso.scss index e7f51e0aac..3a8e9eb7ac 100644 --- a/platform/features/timeline/res/sass/timeline-espresso.scss +++ b/platform/features/timeline/res/sass/timeline-espresso.scss @@ -24,6 +24,7 @@ $output-bourbon-deprecation-warnings: false; @import "../../../../commonUI/general/res/sass/constants"; @import "../../../../commonUI/general/res/sass/mixins"; +@import "../../../../commonUI/general/res/sass/glyphs"; @import "../../../../commonUI/themes/espresso/res/sass/constants"; @import "../../../../commonUI/themes/espresso/res/sass/mixins"; @import "constants"; diff --git a/platform/features/timeline/res/sass/timeline-snow.scss b/platform/features/timeline/res/sass/timeline-snow.scss index 0357f0263f..a99b5bdbca 100644 --- a/platform/features/timeline/res/sass/timeline-snow.scss +++ b/platform/features/timeline/res/sass/timeline-snow.scss @@ -24,6 +24,7 @@ $output-bourbon-deprecation-warnings: false; @import "../../../../commonUI/general/res/sass/constants"; @import "../../../../commonUI/general/res/sass/mixins"; +@import "../../../../commonUI/general/res/sass/glyphs"; @import "../../../../commonUI/themes/snow/res/sass/constants"; @import "../../../../commonUI/themes/snow/res/sass/mixins"; @import "constants"; diff --git a/platform/features/timeline/res/sass/timeline.scss b/platform/features/timeline/res/sass/timeline.scss index 74ab38a5e7..791f7db9f9 100644 --- a/platform/features/timeline/res/sass/timeline.scss +++ b/platform/features/timeline/res/sass/timeline.scss @@ -24,6 +24,7 @@ $output-bourbon-deprecation-warnings: false; @import "../../../../commonUI/general/res/sass/constants"; @import "../../../../commonUI/general/res/sass/mixins"; +@import "../../../../commonUI/general/res/sass/glyphs"; @import "../../../../commonUI/themes/espresso/res/sass/constants"; @import "../../../../commonUI/themes/espresso/res/sass/mixins"; @import "constants"; diff --git a/platform/features/timeline/res/templates/timeline.html b/platform/features/timeline/res/templates/timeline.html index d4c022f150..af71962bca 100644 --- a/platform/features/timeline/res/templates/timeline.html +++ b/platform/features/timeline/res/templates/timeline.html @@ -96,109 +96,124 @@ - + - -
-
- - + +
+ + + -
- - -
+ + +
-
-
-
+
+ + +
+ - - +
+
+
- - - - + + + + + + + +
+
-
-
- - + + - -
-
-
- - + +
+
+
+ + +
+ +
+
+
+
+
-
-
-
-
-
-
- + + + +
diff --git a/platform/features/timeline/src/controllers/TimelineTOIController.js b/platform/features/timeline/src/controllers/TimelineTOIController.js new file mode 100644 index 0000000000..d189e48738 --- /dev/null +++ b/platform/features/timeline/src/controllers/TimelineTOIController.js @@ -0,0 +1,111 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2009-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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 () { + + /** + * Tracks time-of-interest in timelines, updating both scroll state + * (when appropriate) and positioning of the displayed line. + */ + function TimelineTOIController(openmct, timerService, $scope) { + this.openmct = openmct; + this.timerService = timerService; + this.$scope = $scope; + + this.change = this.change.bind(this); + this.bounds = this.bounds.bind(this); + this.destroy = this.destroy.bind(this); + + this.timerService.on('change', this.change); + this.openmct.time.on('bounds', this.bounds); + + this.$scope.$on('$destroy', this.destroy); + + this.$scope.scroll.follow = this.timerService.hasTimer(); + if (this.$scope.zoomController) { + this.bounds(this.openmct.time.bounds()); + } + } + + + /** + * Handle a `change` event from the timer service; track the + * new timer. + */ + TimelineTOIController.prototype.change = function () { + this.$scope.scroll.follow = + this.$scope.scroll.follow || this.timerService.hasTimer(); + }; + + /** + * Handle a `bounds` event from the time API; scroll the timeline + * to match the current bounds, if currently in follow mode. + */ + TimelineTOIController.prototype.bounds = function (bounds) { + if (this.isFollowing()) { + var start = this.timerService.convert(bounds.start); + var end = this.timerService.convert(bounds.end); + this.duration = bounds.end - bounds.start; + this.$scope.zoomController.bounds(start, end); + } + }; + + /** + * Handle a `$destroy` event from scope; detach all observers. + */ + TimelineTOIController.prototype.destroy = function () { + this.timerService.off('change', this.change); + this.openmct.time.off('bounds', this.bounds); + }; + + /** + * Get the x position of the time-of-interest line, + * in pixels from the left edge of the timeline area. + */ + TimelineTOIController.prototype.x = function () { + var now = this.timerService.now(); + + if (now === undefined) { + return undefined; + } + + return this.$scope.zoomController.toPixels(this.timerService.now()); + }; + + /** + * Check if there is an active time-of-interest to be shown. + * @return {boolean} true when active + */ + TimelineTOIController.prototype.isActive = function () { + return this.x() !== undefined; + }; + + /** + * Check if the timeline should be following time conductor bounds. + * @return {boolean} true when following + */ + TimelineTOIController.prototype.isFollowing = function () { + return !!this.$scope.scroll.follow && this.timerService.now() !== undefined; + }; + + return TimelineTOIController; +}); diff --git a/platform/features/timeline/src/controllers/TimelineZoomController.js b/platform/features/timeline/src/controllers/TimelineZoomController.js index df13297f98..b7af026b77 100644 --- a/platform/features/timeline/src/controllers/TimelineZoomController.js +++ b/platform/features/timeline/src/controllers/TimelineZoomController.js @@ -32,7 +32,8 @@ define( // Prefer to start with the middle index var zoomLevels = ZOOM_CONFIGURATION.levels || [1000], zoomIndex = Math.floor(zoomLevels.length / 2), - tickWidth = ZOOM_CONFIGURATION.width || 200; + tickWidth = ZOOM_CONFIGURATION.width || 200, + lastWidth = Number.MAX_VALUE; // Don't constrain prematurely function toMillis(pixels) { return (pixels / tickWidth) * zoomLevels[zoomIndex]; @@ -55,19 +56,29 @@ define( function setScroll(x) { $window.requestAnimationFrame(function () { - $scope.scroll.x = x; + $scope.scroll.x = Math.min( + Math.max(x, 0), + lastWidth - $scope.scroll.width + ); $scope.$apply(); }); } - function initializeZoomFromTimespan(timespan) { - var timelineDuration = timespan.getDuration(); + function initializeZoomFromStartEnd(start, end) { + var duration = end - start; zoomIndex = 0; - while (toMillis($scope.scroll.width) < timelineDuration && + while (toMillis($scope.scroll.width) < duration && zoomIndex < zoomLevels.length - 1) { zoomIndex += 1; } - setScroll(toPixels(timespan.getStart())); + setScroll(toPixels(start)); + } + + function initializeZoomFromTimespan(timespan) { + return initializeZoomFromStartEnd( + timespan.getStart(), + timespan.getEnd() + ); } function initializeZoom() { @@ -101,6 +112,13 @@ define( } return zoomLevels[zoomIndex]; }, + /** + * Adjust the current zoom bounds to fit both the + * start and the end time provided. + * @param {number} start the starting timestamp + * @param {number} end the ending timestamp + */ + bounds: initializeZoomFromStartEnd, /** * Set the zoom level to fit the bounds of the timeline * being viewed. @@ -119,14 +137,14 @@ define( */ toMillis: toMillis, /** - * Get the pixel width necessary to fit the specified - * timestamp, expressed as an offset in milliseconds from - * the start of the timeline. + * Set the maximum timestamp value to be displayed, and get + * the pixel width necessary to display this value. * @param {number} timestamp the time to display */ width: function (timestamp) { var pixels = Math.ceil(toPixels(timestamp * (1 + PADDING))); - return Math.max($scope.scroll.width, pixels); + lastWidth = Math.max($scope.scroll.width, pixels); + return lastWidth; } }; } diff --git a/platform/features/timeline/test/controllers/TimelineTOIControllerSpec.js b/platform/features/timeline/test/controllers/TimelineTOIControllerSpec.js new file mode 100644 index 0000000000..d3345ad64b --- /dev/null +++ b/platform/features/timeline/test/controllers/TimelineTOIControllerSpec.js @@ -0,0 +1,138 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2009-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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([ + "../../src/controllers/TimelineTOIController", + "EventEmitter" +], function (TimelineTOIController, EventEmitter) { + describe("The timeline TOI controller", function () { + var mockmct; + var mockTimerService; + var mockScope; + var controller; + + beforeEach(function () { + mockmct = { time: new EventEmitter() }; + mockmct.time.bounds = jasmine.createSpy('bounds'); + mockTimerService = new EventEmitter(); + mockTimerService.getTimer = jasmine.createSpy('getTimer'); + mockTimerService.hasTimer = jasmine.createSpy('hasTimer'); + mockTimerService.now = jasmine.createSpy('now'); + mockTimerService.convert = jasmine.createSpy('convert'); + mockScope = new EventEmitter(); + mockScope.$on = mockScope.on.bind(mockScope); + mockScope.zoomController = jasmine.createSpyObj('zoom', [ + 'bounds', + 'toPixels' + ]); + mockScope.scroll = { x: 10, width: 1000 }; + + spyOn(mockmct.time, "on").andCallThrough(); + spyOn(mockmct.time, "off").andCallThrough(); + spyOn(mockTimerService, "on").andCallThrough(); + spyOn(mockTimerService, "off").andCallThrough(); + + controller = new TimelineTOIController( + mockmct, + mockTimerService, + mockScope + ); + }); + + it("reports an undefined x position initially", function () { + expect(controller.x()).toBeUndefined(); + }); + + it("listens for bounds changes", function () { + expect(mockmct.time.on) + .toHaveBeenCalledWith('bounds', controller.bounds); + }); + + it("listens for timer changes", function () { + expect(mockTimerService.on) + .toHaveBeenCalledWith('change', controller.change); + }); + + it("is not active", function () { + expect(controller.isActive()).toBe(false); + }); + + describe("on $destroy from scope", function () { + beforeEach(function () { + mockScope.emit("$destroy"); + }); + + it("unregisters listeners", function () { + expect(mockmct.time.off) + .toHaveBeenCalledWith('bounds', controller.bounds); + expect(mockTimerService.off) + .toHaveBeenCalledWith('change', controller.change); + }); + }); + + describe("when a timer and timestamp present", function () { + var mockTimer; + var testNow; + + beforeEach(function () { + testNow = 333221; + mockScope.zoomController.toPixels + .andCallFake(function (millis) { + return millis * 2; + }); + mockTimerService.emit('change', mockTimer); + mockTimerService.now.andReturn(testNow); + }); + + it("reports an x value from the zoomController", function () { + var now = mockTimerService.now(); + var expected = mockScope.zoomController.toPixels(now); + expect(controller.x()).toEqual(expected); + }); + }); + + describe("when follow mode is disabled", function () { + beforeEach(function () { + mockScope.scroll.follow = false; + }); + + it("ignores bounds events", function () { + mockmct.time.emit('bounds', { start: 0, end: 1000 }); + expect(mockScope.zoomController.bounds) + .not.toHaveBeenCalled(); + }); + }); + + describe("when follow mode is enabled", function () { + beforeEach(function () { + mockScope.scroll.follow = true; + mockTimerService.now.andReturn(500); + }); + + it("zooms on bounds events", function () { + mockmct.time.emit('bounds', { start: 0, end: 1000 }); + expect(mockScope.zoomController.bounds) + .toHaveBeenCalled(); + }); + }); + }); +});