[Time Conductor] Remove previous time conductor. Fixes #1234

This commit is contained in:
Henry
2016-11-28 16:46:21 -08:00
parent bf006b45e4
commit e121c0f8ac
53 changed files with 11 additions and 1451 deletions

View File

@@ -0,0 +1,161 @@
/*****************************************************************************
* 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([
"./src/ui/TimeConductorViewService",
"./src/ui/TimeConductorController",
"./src/ui/ConductorAxisController",
"./src/ui/ConductorTOIController",
"./src/ui/TimeOfInterestController",
"./src/ui/MctConductorAxis",
"./src/ui/NumberFormat",
"text!./res/templates/time-conductor.html",
"text!./res/templates/mode-selector/mode-selector.html",
"text!./res/templates/mode-selector/mode-menu.html",
"text!./res/templates/time-of-interest.html",
"legacyRegistry"
], function (
TimeConductorViewService,
TimeConductorController,
ConductorAxisController,
ConductorTOIController,
TimeOfInterestController,
MCTConductorAxis,
NumberFormat,
timeConductorTemplate,
modeSelectorTemplate,
modeMenuTemplate,
timeOfInterest,
legacyRegistry
) {
legacyRegistry.register("platform/features/conductor/core", {
"extensions": {
"services": [
{
"key": "timeConductorViewService",
"implementation": TimeConductorViewService,
"depends": [
"openmct",
"timeSystems[]"
]
}
],
"controllers": [
{
"key": "TimeConductorController",
"implementation": TimeConductorController,
"depends": [
"$scope",
"$window",
"$location",
"openmct",
"timeConductorViewService",
"timeSystems[]",
"formatService"
]
},
{
"key": "ConductorTOIController",
"implementation": ConductorTOIController,
"depends": [
"$scope",
"openmct",
"timeConductorViewService",
"formatService"
]
},
{
"key": "TimeOfInterestController",
"implementation": TimeOfInterestController,
"depends": [
"$scope",
"openmct",
"formatService"
]
}
],
"directives": [
{
"key": "mctConductorAxis",
"implementation": MCTConductorAxis,
"depends": [
"openmct",
"formatService"
]
}
],
"stylesheets": [
{
"stylesheetUrl": "css/time-conductor-espresso.css",
"theme": "espresso"
},
{
"stylesheetUrl": "css/time-conductor-snow.css",
"theme": "snow"
}
],
"templates": [
{
"key": "conductor",
"template": timeConductorTemplate
},
{
"key": "mode-menu",
"template": modeMenuTemplate
},
{
"key": "mode-selector",
"template": modeSelectorTemplate
},
{
"key": "time-of-interest",
"template": timeOfInterest
}
],
"representations": [
{
"key": "time-conductor",
"template": timeConductorTemplate
}
],
"licenses": [
{
"name": "D3: Data-Driven Documents",
"version": "4.1.0",
"author": "Mike Bostock",
"description": "D3 (or D3.js) is a JavaScript library for visualizing data using web standards. D3 helps you bring data to life using SVG, Canvas and HTML. D3 combines powerful visualization and interaction techniques with a data-driven approach to DOM manipulation, giving you the full capabilities of modern browsers and the freedom to design the right visual interface for your data.",
"website": "https://d3js.org/",
"copyright": "Copyright 2010-2016 Mike Bostock",
"license": "BSD-3-Clause",
"link": "https://github.com/d3/d3/blob/master/LICENSE"
}
],
"formats": [
{
"key": "number",
"implementation": NumberFormat
}
]
}
});
});

View File

@@ -0,0 +1,11 @@
$ueTimeConductorH: (25px, 16px, 20px); // Heights for Ticks, Data Visualization, Controls elements
$ueTimeConductorRtH: (25px, 3px, 20px); // Heights for elements in Real-time mode
$timeCondInputTimeSysDefW: 165px; // Default width for datetime value inputs
$timeCondInputDeltaDefW: 60px; // Default width for delta value inputs, typically 00:00:00
$timeCondTOIIconD: 12px; // height and width of icon used for TOI indicator
$timeCondTOIValOffset: 0px;
$ticksBlockerFadeW: 50px;
$toiBlockerFadeW: 10px;
$toiH: 12px; // Needs to be an even number to avoid sub-pixel antialiasing of the vertical line
$toiPad: 4px;
$timeCondAxisLROffset: (($toiH / 2) + $toiPad, ($toiH / 2) + $toiPad); // Margin to left, right of tick axis and vis bar. For paging, use 15, 20px

View File

@@ -0,0 +1,483 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2015, 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.
*****************************************************************************/
@mixin toiLineHovEffects() {
&:before,
&:after {
background-color: $timeControllerToiLineColorHov;
}
}
.l-time-conductor-holder {
border-top: 1px solid $colorInteriorBorder;
min-width: 500px;
padding-top: $interiorMargin;
}
.time-conductor-icon {
$c: $colorObjHdrIc;
$d: 18px;
height: $d !important;
width: $d;
position: relative;
&:before {
@extend .ui-symbol;
color: $c;
content: $glyph-icon-brackets;
font-size: $d;
line-height: normal;
display: block;
width: 100%;
height: 100%;
z-index: 1;
}
// Clock hands
div[class*="hand"] {
$handW: 2px;
$handH: $d * 0.4;
@include transform(translate(-50%, -50%));
@include animation-iteration-count(infinite);
@include animation-timing-function(linear);
position: absolute;
height: $handW;
width: $handW;
left: 50%;
top: 50%;
z-index: 2;
&:before {
background: $colorObjHdrIc;
content: '';
display: block;
position: absolute;
width: 100%;
bottom: -1px;
}
&.hand-little {
z-index: 2;
@include animation-duration(12s);
&:before {
height: ceil($handH * 0.7);
}
}
&.hand-big {
z-index: 1;
@include animation-duration(1s);
&:before {
height: $handH;
}
}
}
}
.l-time-conductor {
$knobHOffset: 0px;
$rangeValPad: $interiorMargin;
$rangeValOffset: $sliderKnobW + $interiorMargin;
$r1H: nth($ueTimeConductorH, 1);
$r2H: nth($ueTimeConductorH, 2);
$r3H: nth($ueTimeConductorH, 3);
position: relative;
> .l-row-elem {
// First order row elements
box-sizing: border-box;
width: 100%;
position: relative;
}
.mode-selector .s-menu-button,
.time-delta {
&:before {
@extend .ui-symbol;
}
}
.time-delta {
&:before {
color: $colorTimeCondKeyBg;
}
}
.l-time-conductor-inputs-holder,
.l-time-conductor-inputs-and-ticks,
.l-time-conductor-zoom-w {
font-size: 0.8rem;
}
.l-time-conductor-inputs-holder {
$iconCalendarW: 16px;
$wBgColor: $colorBodyBg;
height: $r1H;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
pointer-events: none;
.l-time-range-w {
// Wraps a datetime text input field
height: 100%;
position: absolute;
.title {
display: inline-block;
margin-right: $interiorMarginSm;
}
&.start-w {
@include background-image(linear-gradient(270deg, transparent, $wBgColor $ticksBlockerFadeW));
padding-right: $ticksBlockerFadeW;
.title:before {
content: 'Start';
}
}
&.end-w {
@include background-image(linear-gradient(90deg, transparent, $wBgColor $ticksBlockerFadeW));
padding-left: $ticksBlockerFadeW;
right: 0;
.title:before {
content: 'End';
}
}
.l-time-conductor-inputs {
pointer-events: auto;
}
input[type="text"] {
@include trans-prop-nice(padding, 250ms);
}
.time-range-input input[type="text"] {
width: $timeCondInputTimeSysDefW;
}
.hrs-min-input input[type="text"] {
width: $timeCondInputDeltaDefW;
}
.icon-calendar {
margin-top: 4px;
}
}
}
.l-time-conductor-inputs-and-ticks {
$c: $colorTimeCondTicks;
height: $r1H;
mct-conductor-axis {
display: block;
position: relative;
width: 100%;
}
.l-axis-holder {
height: $r1H;
position: absolute;
left: nth($timeCondAxisLROffset, 1);
right: nth($timeCondAxisLROffset, 2);
svg {
text-rendering: geometricPrecision;
width: 100%;
height: 100%;
> g {
font-size: 0.9em;
}
path {
// Line beneath ticks
display: none;
}
line {
// Tick marks
stroke: $c;
}
text {
// Tick labels
fill: $c;
}
}
}
}
.l-data-visualization-holder {
height: $r2H;
z-index: 2; // Must lift above ticks and inputs
.l-page-button,
.l-data-visualization {
position: absolute;
top: 0;
bottom: 0;
}
.l-page-button {
@if nth($timeCondAxisLROffset, 1) + nth($timeCondAxisLROffset, 2) > 30 {
left: 0;
width: nth($timeCondAxisLROffset, 1);
&.align-right {
left: auto;
right: 0;
width: nth($timeCondAxisLROffset, 2);
}
} @else {
// Hide these if the offsets aren't enough
display: none;
}
}
.l-data-visualization {
background: $colorTimeCondDataVisBg;
left: nth($timeCondAxisLROffset, 1);
right: nth($timeCondAxisLROffset, 2);
&:hover {
.l-toi-holder.hover {
opacity: 1;
}
.l-toi-holder.pinned.active {
opacity: 0.4;
.l-toi-val {
pointer-events: none;
opacity: 0;
}
}
}
}
}
.l-time-conductor-controls {
align-items: center;
margin-top: $interiorMargin;
.l-time-conductor-zoom-w {
@include justify-content(flex-end);
.time-conductor-zoom {
height: $r3H;
min-width: 100px;
width: 20%;
}
.time-conductor-zoom-current-range {
color: $colorTick;
}
}
}
// Real-time, latest modes
&.realtime-mode,
&.lad-mode {
.time-conductor-icon {
&:before {
color: $colorTimeCondKeyBg;
}
div[class*="hand"] {
@include animation-name(clock-hands);
&:before {
background: $colorTimeCondKeyBg;
}
}
}
.l-time-conductor-inputs-holder {
.l-time-range-input-w {
input[type="text"]:not(.error) {
background: transparent;
box-shadow: none;
border-radius: 0;
padding-left: 0;
padding-right: 0;
&:hover,
&:focus {
@include nice-input();
padding: $inputTextP;
}
}
.icon-calendar {
display: none;
}
&.start-date {
display: none;
}
&.end-date {
pointer-events: none;
input[type="text"] {
color: pullForward($colorTimeCondKeyBg, 5%);
margin-right: $interiorMargin;
tab-index: -1;
}
}
}
}
.l-data-visualization {
background: $colorTimeCondDataVisRtBg !important;
}
.mode-selector .s-menu-button {
$fg: $colorTimeCondKeyFg;
@include btnSubtle($bg: $colorTimeCondKeyBg, $bgHov: pullForward($colorTimeCondKeyBg, $ltGamma), $fg: $colorTimeCondKeyFg);
&:before {
color: $fg !important;
}
;
color: $fg !important;
}
}
// Fixed mode
&.fixed-mode {
$i: $glyph-icon-calendar;
.time-conductor-icon div[class*="hand"] {
&.hand-little {
@include transform(rotate(120deg));
}
}
.mode-selector .s-menu-button:before {
content: $i;
}
.l-axis-holder {
@include cursorGrab();
}
}
// Realtime mode
&.realtime-mode {
$i: $glyph-icon-clock;
.time-conductor-icon div[class*="hand"] {
@include animation-name(clock-hands);
}
.time-delta:before {
content: $i;
}
.l-time-conductor-inputs-holder .l-time-range-w.end-w .title:before {
content: 'Now';
}
.mode-selector .s-menu-button:before {
content: $i;
}
}
// LAD mode
&.lad-mode {
$i: $glyph-icon-database;
.time-conductor-icon div[class*="hand"] {
@include animation-name(clock-hands-sticky);
&.hand-big {
@include animation-duration(5s);
}
&.hand-little {
@include animation-duration(60s);
}
}
.time-delta:before {
content: $i;
}
.l-time-conductor-inputs-holder .l-time-range-w.end-w .title:before {
content: 'LAD';
}
.mode-selector .s-menu-button:before {
content: $i;
}
}
}
/******************************************************************** MOBILE */
@include phoneandtablet {
.l-time-conductor-holder {
min-width: 0 !important;
}
.super-menu.mini {
width: 200px;
height: 100px;
.pane.menu-item-description {
display: none;
}
}
}
@include phone {
.l-time-conductor {
min-width: 0;
.l-time-conductor-inputs-and-ticks {
.l-time-conductor-inputs-holder {
.l-time-range-w {
background-image: none !important;
}
}
mct-conductor-axis {
display: none;
}
}
}
}
@include phonePortrait {
.l-time-conductor {
.l-data-visualization,
.l-time-conductor-zoom-w,
.time-delta {
display: none;
}
.l-time-conductor-inputs-and-ticks {
height: auto !important;
.l-time-conductor-inputs-holder {
position: relative;
height: auto !important;
.l-time-range-w {
background-image: none !important;
display: block;
height: auto !important;
padding: 0 !important;
position: relative;
text-align: left;
&:not(:first-child) {
margin-top: $interiorMargin;
}
}
}
}
// Fixed mode
&.fixed-mode {
.l-time-conductor-inputs-and-ticks {
.l-time-range-w {
.title {
width: 30px;
}
}
}
}
// Real-time, latest modes
&.realtime-mode,
&.lad-mode {
.l-time-conductor-inputs-and-ticks {
.l-time-range-w {
&.start-w {
display: none;
}
&.end-w {
margin-top: 0;
.end-date input[type="text"] {
margin: 0;
text-align: left;
}
}
}
}
}
}
}

View File

@@ -0,0 +1,271 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-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.
*****************************************************************************/
mct-include.l-toi-holder,
.l-toi-holder:after,
.l-toi-holder:before {
display: block;
position: absolute;
}
mct-include.l-toi-holder {
$blockerFadeW: $toiBlockerFadeW;
@include transform(translateX(-50%));
color: $toiColorBg;
position: absolute;
top: 0;
bottom: 0;
width: $toiH;
&:not(.pinned) {
display: none;
}
&.pinned {
display: block;
}
&:before,
&:after {
// Vertical lines. TC uses both; plot only uses :before
border-left: 1px dashed $toiColorBg;
box-sizing: border-box;
content: '';
display: block;
left: 50%;
position: absolute;
top: 0;
bottom: 0;
width: 2px;
z-index: 2;
}
.l-toi {
// Holds buttons and val. Acts as a blocking element.
@include background-image(linear-gradient(90deg, transparent, $toiColorBlocker 10%, $toiColorBlocker 90%, transparent 100%));
position: absolute;
align-items: center;
box-sizing: content-box;
height: $toiH;
left: $toiPad * -2;
@include transform(translateY(-50%)); top: 50%;
padding: $toiPad;
z-index: 1;
.l-toi-buttons {
@include trans-prop-nice($props: (width, padding), $dur: 250ms);
border-radius: $controlCr;
box-sizing: content-box;
font-size: $toiH;
height: 100%;
line-height: $toiH;
padding: $toiPad;
overflow: hidden;
white-space: nowrap;
justify-content: space-between;
width: $toiH;
&:hover {
// Expand and display controls; clock icon changes to resync
background-color: $toiColorBg;
cursor: pointer;
width: 30px;
.icon-button {
color: rgba($toiColorCtrlFg, 0.5);
opacity: 1;
&:hover {
color: $toiColorCtrlFg;
}
}
.t-button-resync {
order: 1;
&:before {
content: $glyph-icon-resync;
}
}
.t-button-unpin {
order: 2;
&:hover {
color: $toiColorBgAlert;
}
}
& + .l-toi-val {
// Dim the value to emphasize the controls
opacity: 0.5;
}
}
}
.icon-button {
color: $toiColorBg;
}
.t-button-resync {
@extend .icon-clock;
&:hover { color: $toiColorCtrlFg; }
}
.t-button-unpin {
@include trans-prop-nice($props: opacity, $dur: 150ms);
@extend .icon-x-in-circle;
float: right;
opacity: 0;
}
}
.l-toi-val {
display: none; // Hide by default; see .show-val below
}
// TOI is showing value as well
&.show-val {
.l-toi {
.l-toi-buttons {
order: 1;
&:hover {
margin-right: $interiorMarginSm;
}
}
.l-toi-val {
@include trans-prop-nice($props: opacity, $dur: 150ms);
background-color: $toiColorBg;
border-radius: $controlCr;
box-sizing: content-box;
color: $toiColorFg;
display: inline-block;
font-size: 0.7rem;
font-weight: 400;
height: $toiH;
line-height: $toiH;
order: 2;
padding: 1px 3px;
white-space: nowrap;
}
}
&.val-to-left {
.l-toi {
left: auto;
right: $toiPad * -2;
.l-toi-buttons {
order: 2;
&:hover {
.t-button-resync { order: 2; }
.t-button-unpin { order: 1; }
margin-left: $interiorMarginSm;
}
}
.l-toi-val {
order: 1;
}
}
}
}
}
// TOI in tables
.tabular,
table {
tbody, .tbody {
tr, .tr {
&.l-toi-tablerow {
border-top: 1px dashed $toiColorBg;
z-index: 1;
td, .td {
.l-toi-holder {
left: 50% !important;
&:before,
&:after {
display: none;
}
.l-toi {
background: rgba($toiColorBlocker, 0.9);
border-radius: 20%;
height: auto;
padding: $toiPad;
@include transform(translate(-50%, -50%));
left: 50%; right: auto; top: 0;
.l-toi-buttons {
padding: 1px;
&:hover {
padding: $toiPad;
}
}
}
}
}
}
}
}
}
// TOI in plots
.gl-plot {
.gl-plot-wrapper-display-area-and-x-axis {
right: nth($plotDisplayArea, 2) + ($toiH / 2) + $toiPad; // Make room for TOI element
.l-toi-holder {
bottom: nth($plotDisplayArea, 3) - $interiorMargin;
z-index: 3;
.l-toi {
@include transform(translateY(100%));
}
.l-toi {
top: auto;
bottom: $toiPad;
}
}
}
}
// TOI in Time Conductor
.l-time-conductor {
.l-toi-holder {
$linesVOffset: 2px;
&:before,
&:after {
// Vertical lines
border-left-style: solid;
height: ((nth($ueTimeConductorH, 2) - $timeCondTOIIconD)/2) + $linesVOffset;
}
&:before {
@include transform(translate(-50%, $linesVOffset * -1));
top: 0px;
bottom: auto;
}
&:after {
@include transform(translate(-50%, $linesVOffset));
top: auto;
bottom: 0px;
}
}
}

View File

@@ -0,0 +1,50 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2015, 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.
*****************************************************************************/
@import "bourbon";
@import "../../../../../commonUI/general/res/sass/constants";
@import "../../../../../commonUI/general/res/sass/mixins";
@import "../../../../../commonUI/general/res/sass/mobile/constants";
@import "../../../../../commonUI/general/res/sass/mobile/mixins";
@import "../../../../../commonUI/themes/espresso/res/sass/constants";
@import "../../../../../commonUI/themes/espresso/res/sass/mixins";
@import "../../../../../commonUI/general/res/sass/glyphs";
@import "../../../../../commonUI/general/res/sass/icons";
@import "constants";
// Thematic constants
$colorTimeCondTicks: pullForward($colorBodyBg, 30%);
$colorTimeCondKeyBg: #4e70dc;
$colorTimeCondKeyFg: #fff;
$colorTimeCondDataVisBg: pullForward($colorBodyBg, 5%);
$colorTimeCondDataVisRtBg: pushBack($colorTimeCondKeyBg, 10%);
// Time of Interest
$toiColorBg: #6b93c6;
$toiColorBlocker: $colorBodyBg; // Color of blocker element beneath the TOI icons
$toiColorFg: #000; // Used by value display
$toiColorCtrlFg: #fff;
$toiColorBgAlert: #cf2a12; // Used by unpin button on hover
$colorTimeCondTOIBg: darken($toiColorBg, 20%);
$colorTimeCondTOIBgHov: $toiColorBg;
@import "time-conductor-base";
@import "time-of-interest";

View File

@@ -0,0 +1,50 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2015, 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.
*****************************************************************************/
@import "bourbon";
@import "../../../../../commonUI/general/res/sass/constants";
@import "../../../../../commonUI/general/res/sass/mixins";
@import "../../../../../commonUI/general/res/sass/mobile/constants";
@import "../../../../../commonUI/general/res/sass/mobile/mixins";
@import "../../../../../commonUI/themes/snow/res/sass/constants";
@import "../../../../../commonUI/themes/snow/res/sass/mixins";
@import "../../../../../commonUI/general/res/sass/glyphs";
@import "../../../../../commonUI/general/res/sass/icons";
@import "constants";
// Thematic constants
$colorTimeCondTicks: pullForward($colorBodyBg, 30%);
$colorTimeCondKeyBg: #6178dc;
$colorTimeCondKeyFg: #fff;
$colorTimeCondDataVisBg: pullForward($colorBodyBg, 10%);
$colorTimeCondDataVisRtBg: pushBack($colorTimeCondKeyBg, 30%);
// Time of Interest
$toiColorBg: #6b93c6;
$toiColorBlocker: $colorBodyBg; // Color of blocker element beneath the TOI icons
$toiColorFg: #fff; // Used by value display
$toiColorCtrlFg: #fff;
$toiColorBgAlert: #ff9540; // Used by unpin button on hover
$colorTimeCondTOIBg: darken($toiColorBg, 20%);
$colorTimeCondTOIBgHov: $toiColorBg;
@import "time-conductor-base";
@import "time-of-interest";

View File

@@ -0,0 +1,45 @@
<!--
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.
-->
<div class="contents">
<div class="pane left menu-items">
<ul>
<li ng-repeat="(key, metadata) in ngModel.options"
ng-click="ngModel.selectedKey=key">
<a ng-mouseover="ngModel.activeMetadata = metadata"
ng-mouseleave="ngModel.activeMetadata = undefined"
class="menu-item-a {{metadata.cssclass}}">
{{metadata.name}}
</a>
</li>
</ul>
</div>
<div class="pane right menu-item-description">
<div
class="desc-area ui-symbol icon type-icon {{ngModel.activeMetadata.cssclass}}"></div>
<div class="desc-area title">
{{ngModel.activeMetadata.name}}
</div>
<div class="desc-area description">
{{ngModel.activeMetadata.description}}
</div>
</div>
</div>

View File

@@ -0,0 +1,34 @@
<!--
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.
-->
<span ng-controller="ClickAwayController as modeController">
<div class="s-menu-button"
ng-click="modeController.toggle()">
<span class="title-label">{{ngModel.options[ngModel.selectedKey]
.label}}</span>
</div>
<div class="menu super-menu mini mode-selector-menu"
ng-show="modeController.isActive()">
<mct-include key="'mode-menu'"
ng-model="ngModel">
</mct-include>
</div>
</span>

View File

@@ -0,0 +1,132 @@
<!-- Parent holder for time conductor. follow-mode | fixed-mode -->
<div ng-controller="TimeConductorController as tcController"
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}">
<div class="flex-elem holder time-conductor-icon">
<div class="hand-little"></div>
<div class="hand-big"></div>
</div>
<div class="flex-elem holder grows l-flex-col l-time-conductor-inner">
<!-- Holds inputs and ticks -->
<div class="l-time-conductor-inputs-and-ticks l-row-elem flex-elem no-margin">
<form class="l-time-conductor-inputs-holder"
ng-submit="tcController.setBounds(boundsModel)">
<span class="l-time-range-w start-w">
<span class="l-time-conductor-inputs">
<span class="l-time-range-input-w start-date">
<span class="title"></span>
<mct-control key="'datetime-field'"
structure="{
format: timeSystemModel.format,
validate: tcController.validation.validateStart
}"
ng-model="boundsModel"
ng-blur="tcController.setBounds(boundsModel)"
field="'start'"
class="time-range-input">
</mct-control>
</span>
<span class="l-time-range-input-w time-delta start-delta"
ng-class="{'hide':(modeModel.selectedKey === 'fixed')}">
-
<mct-control key="'datetime-field'"
structure="{
format: timeSystemModel.deltaFormat,
validate: tcController.validation.validateStartDelta
}"
ng-model="boundsModel"
ng-blur="tcController.setDeltas(boundsModel)"
field="'startDelta'"
class="hrs-min-input">
</mct-control>
</span>
</span>
</span>
<span class="l-time-range-w end-w">
<span class="l-time-conductor-inputs">
<span class="l-time-range-input-w end-date"
ng-controller="ToggleController as t2">
<span class="title"></span>
<mct-control key="'datetime-field'"
structure="{
format: timeSystemModel.format,
validate: tcController.validation.validateEnd
}"
ng-model="boundsModel"
ng-blur="tcController.setBounds(boundsModel)"
ng-disabled="modeModel.selectedKey !== 'fixed'"
field="'end'"
class="time-range-input">
</mct-control>
</span>
<span class="l-time-range-input-w time-delta end-delta"
ng-class="{'hide':(modeModel.selectedKey === 'fixed')}">
+
<mct-control key="'datetime-field'"
structure="{
format: timeSystemModel.deltaFormat,
validate: tcController.validation.validateEndDelta
}"
ng-model="boundsModel"
ng-blur="tcController.setDeltas(boundsModel)"
field="'endDelta'"
class="hrs-min-input">
</mct-control>
</span>
</span>
</span>
<input type="submit" class="hidden">
</form>
<mct-conductor-axis></mct-conductor-axis>
</div>
<!-- Holds data visualization, time of interest -->
<div class="l-data-visualization-holder l-row-elem flex-elem"
ng-controller="ConductorTOIController as toi">
<a class="l-page-button s-icon-button icon-pointer-left"></a>
<div class="l-data-visualization" ng-click="toi.setTOIFromPosition($event)">
<mct-include key="'time-of-interest'"
class="l-toi-holder show-val"
ng-class="{ pinned: toi.pinned, 'val-to-left': toi.left > 80 }"
ng-style="{'left': toi.left + '%'}"></mct-include>
</div>
<a class="l-page-button align-right s-icon-button icon-pointer-right"></a>
</div>
<!-- Holds time system and session selectors, and zoom control -->
<div class="l-time-conductor-controls l-row-elem l-flex-row flex-elem">
<mct-include
key="'mode-selector'"
ng-model="modeModel"
class="holder flex-elem menus-up mode-selector">
</mct-include>
<mct-control
key="'menu-button'"
class="holder flex-elem menus-up time-system"
structure="{
text: timeSystemModel.selected.metadata.name,
click: tcController.selectTimeSystemByKey,
options: timeSystemModel.options
}">
</mct-control>
<!-- Zoom control -->
<div ng-if="tcController.supportsZoom"
class="l-time-conductor-zoom-w grows flex-elem l-flex-row">
{{currentZoom}}
<span
class="time-conductor-zoom-current-range flex-elem flex-fixed holder">{{timeUnits}}</span>
<input class="time-conductor-zoom flex-elem" type="range"
ng-model="tcController.currentZoom"
ng-mouseUp="tcController.onZoomStop(tcController.currentZoom)"
ng-change="tcController.onZoom(tcController.currentZoom)"
min="0.01"
step="0.01"
max="0.99" />
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
<div class="abs angular-controller"
ng-controller="TimeOfInterestController as toi">
<div class="l-flex-row l-toi">
<span class="flex-elem l-flex-row l-toi-buttons">
<a class="flex-elem t-button-resync icon-button" title="Re-sync Time of Interest"
ng-click="toi.resync()"></a>
<a class="flex-elem t-button-unpin icon-button" title="Unset Time of Interest"
ng-click="toi.dismiss()"></a>
</span>
<span class="flex-elem l-toi-val">{{toi.toiText}}</span>
</div>
</div>

View File

@@ -0,0 +1,89 @@
/*****************************************************************************
* 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(['./TickSource'], function (TickSource) {
/**
* @implements TickSource
* @constructor
*/
function LocalClock($timeout, period) {
TickSource.call(this);
this.metadata = {
key: 'local',
mode: 'realtime',
cssclass: 'icon-clock',
label: 'Real-time',
name: 'Real-time Mode',
description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.'
};
this.period = period;
this.$timeout = $timeout;
this.timeoutHandle = undefined;
}
LocalClock.prototype = Object.create(TickSource.prototype);
LocalClock.prototype.start = function () {
this.timeoutHandle = this.$timeout(this.tick.bind(this), this.period);
};
LocalClock.prototype.stop = function () {
if (this.timeoutHandle) {
this.$timeout.cancel(this.timeoutHandle);
}
};
LocalClock.prototype.tick = function () {
var now = Date.now();
this.listeners.forEach(function (listener) {
listener(now);
});
this.timeoutHandle = this.$timeout(this.tick.bind(this), this.period);
};
/**
* Register a listener for the local clock. When it ticks, the local
* clock will provide the current local system time
*
* @param listener
* @returns {function} a function for deregistering the provided listener
*/
LocalClock.prototype.listen = function (listener) {
var listeners = this.listeners;
listeners.push(listener);
if (listeners.length === 1) {
this.start();
}
return function () {
listeners.splice(listeners.indexOf(listener));
if (listeners.length === 0) {
this.stop();
}
}.bind(this);
};
return LocalClock;
});

View File

@@ -0,0 +1,50 @@
/*****************************************************************************
* 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(["./LocalClock"], function (LocalClock) {
describe("The LocalClock class", function () {
var clock,
mockTimeout,
timeoutHandle = {};
beforeEach(function () {
mockTimeout = jasmine.createSpy("timeout");
mockTimeout.andReturn(timeoutHandle);
mockTimeout.cancel = jasmine.createSpy("cancel");
clock = new LocalClock(mockTimeout, 0);
clock.start();
});
it("calls listeners on tick with current time", function () {
var mockListener = jasmine.createSpy("listener");
clock.listen(mockListener);
clock.tick();
expect(mockListener).toHaveBeenCalledWith(jasmine.any(Number));
});
it("stops ticking when stop is called", function () {
clock.stop();
expect(mockTimeout.cancel).toHaveBeenCalledWith(timeoutHandle);
});
});
});

View File

@@ -0,0 +1,47 @@
/*****************************************************************************
* 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 () {
/**
* A tick source is an event generator such as a timing signal, or
* indicator of data availability, which can be used to advance the Time
* Conductor. Usage is simple, a listener registers a callback which is
* invoked when this source 'ticks'.
*
* @interface
* @constructor
*/
function TickSource() {
this.listeners = [];
}
/**
* @param callback Function to be called when this tick source ticks.
* @returns an 'unlisten' function that will remove the callback from
* the registered listeners
*/
TickSource.prototype.listen = function (callback) {
throw new Error('Not implemented');
};
return TickSource;
});

View File

@@ -0,0 +1,107 @@
/*****************************************************************************
* 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 () {
/**
* @interface
* @constructor
*/
function TimeSystem() {
/**
* @typedef TimeSystemMetadata
* @property {string} key
* @property {string} name
* @property {string} description
*
* @type {TimeSystemMetadata}
*/
this.metadata = undefined;
}
/**
* Time formats are defined as extensions. Time systems that implement
* this interface should provide an array of format keys supported by them.
*
* @returns {string[]} An array of time format keys
*/
TimeSystem.prototype.formats = function () {
throw new Error('Not implemented');
};
/**
* @typedef DeltaFormat
* @property {string} type the type of MctControl used to represent this
* field. Typically 'datetime-field' for UTC based dates, or 'textfield'
* otherwise
* @property {string} [format] An optional field specifying the
* Format to use for delta fields in this time system.
*/
/**
* Specifies a format for deltas in this time system.
*
* @returns {DeltaFormat} a delta format specifier
*/
TimeSystem.prototype.deltaFormat = function () {
throw new Error('Not implemented');
};
/**
* Returns the tick sources supported by this time system. Tick sources
* are event generators that can be used to advance the time conductor
* @returns {TickSource[]} The tick sources supported by this time system.
*/
TimeSystem.prototype.tickSources = 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.
*/
TimeSystem.prototype.defaults = function () {
throw new Error('Not implemented');
};
/**
* @return {boolean}
*/
TimeSystem.prototype.isUTCBased = function () {
return true;
};
return TimeSystem;
});

View File

@@ -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;
}
);

View File

@@ -0,0 +1,178 @@
/*****************************************************************************
* 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([
'./ConductorAxisController',
'zepto',
'd3'
], function (
ConductorAxisController,
$,
d3
) {
describe("The ConductorAxisController", function () {
var controller,
mockConductor,
mockConductorViewService,
mockFormatService,
mockScope,
mockElement,
mockTarget,
mockBounds,
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", [
"$on"
]);
//Add some HTML elements
mockTarget = {
offsetWidth: 0,
offsetHeight: 0
};
mockElement = {
firstChild: mockTarget
};
mockBounds = {
start: 100,
end: 200
};
mockConductor = jasmine.createSpyObj("conductor", [
"timeSystem",
"bounds",
"on",
"off",
"follow"
]);
mockConductor.bounds.andReturn(mockBounds);
mockFormatService = jasmine.createSpyObj("formatService", [
"getFormat"
]);
mockConductorViewService = jasmine.createSpyObj("conductorViewService", [
"on",
"off",
"emit"
]);
spyOn(d3, 'scaleUtc').andCallThrough();
spyOn(d3, 'scaleLinear').andCallThrough();
element = $('<div style="width: 100px;"><div style="width: 100%;"></div></div>');
$(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", controller.changeTimeSystem);
expect(mockConductor.on).toHaveBeenCalledWith("bounds", controller.changeBounds);
});
it("on scope destruction, deregisters listeners", function () {
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 () {
it("uses a UTC scale for UTC time systems", function () {
mockTimeSystem.isUTCBased.andReturn(true);
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);
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);
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 () {
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);
});
});
});
});

View File

@@ -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;
}
);

View File

@@ -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);
});
});
});

View File

@@ -0,0 +1,54 @@
/*****************************************************************************
* 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(['./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.
*/
return {
controller: [
'openmct',
'formatService',
'timeConductorViewService',
'$scope',
'$element',
ConductorAxisController
],
controllerAs: 'axis',
restrict: 'E',
priority: 1000,
template: '<div class="l-axis-holder" ' +
' mct-drag-down="axis.panStart()"' +
' mct-drag-up="axis.panStop(delta)"' +
' mct-drag="axis.pan(delta)"' +
' mct-resize="axis.resize()"></div>'
};
}
return MctConductorAxis;
});

View File

@@ -0,0 +1,53 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-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 () {
/**
* Formatter for basic numbers. Provides basic support for non-UTC
* numbering systems
*
* @implements {Format}
* @constructor
* @memberof platform/commonUI/formats
*/
function NumberFormat() {
}
NumberFormat.prototype.format = function (value) {
if (isNaN(value)) {
return '';
} else {
return '' + value;
}
};
NumberFormat.prototype.parse = function (text) {
return parseFloat(text);
};
NumberFormat.prototype.validate = function (text) {
return !isNaN(text);
};
return NumberFormat;
});

View File

@@ -0,0 +1,49 @@
/*****************************************************************************
* 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(['./NumberFormat'], function (NumberFormat) {
describe("The NumberFormat class", function () {
var format;
beforeEach(function () {
format = new NumberFormat();
});
it("The format function takes a string and produces a number", function () {
var text = format.format(1);
expect(text).toBe("1");
expect(typeof text).toBe("string");
});
it("The parse function takes a string and produces a number", function () {
var number = format.parse("1");
expect(number).toBe(1);
expect(typeof number).toBe("number");
});
it("validates that the input is a number", function () {
expect(format.validate("1")).toBe(true);
expect(format.validate(1)).toBe(true);
expect(format.validate("1.1")).toBe(true);
expect(format.validate("abc")).toBe(false);
});
});
});

View File

@@ -0,0 +1,470 @@
/*****************************************************************************
* 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(
[
'./TimeConductorValidation'
],
function (TimeConductorValidation) {
var SEARCH = {
MODE: 'tc.mode',
TIME_SYSTEM: 'tc.timeSystem',
START_BOUND: 'tc.startBound',
END_BOUND: 'tc.endBound',
START_DELTA: 'tc.startDelta',
END_DELTA: 'tc.endDelta'
};
/**
* Controller for the Time Conductor UI element. The Time Conductor includes form fields for specifying time
* 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, $location, openmct, conductorViewService, timeSystems, formatService) {
var self = this;
//Bind all class functions to 'this'
Object.keys(TimeConductorController.prototype).filter(function (key) {
return typeof TimeConductorController.prototype[key] === 'function';
}).forEach(function (key) {
self[key] = self[key].bind(self);
});
this.$scope = $scope;
this.$window = $window;
this.$location = $location;
this.conductorViewService = conductorViewService;
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) {
return timeSystemConstructor();
});
this.initializeScope();
var searchParams = JSON.parse(JSON.stringify(this.$location.search()));
//Set bounds, time systems, deltas, on conductor from URL
this.setStateFromSearchParams(searchParams);
//Set the initial state of the UI from the conductor state
var timeSystem = this.conductor.timeSystem();
if (timeSystem) {
this.changeTimeSystem(this.conductor.timeSystem());
}
var deltas = this.conductorViewService.deltas();
if (deltas) {
this.setFormFromDeltas(deltas);
}
var bounds = this.conductor.bounds();
if (bounds && bounds.start !== undefined && bounds.end !== undefined) {
this.changeBounds(bounds);
}
//Listen for changes to URL and update state if necessary
this.$scope.$on('$routeUpdate', function () {
this.setStateFromSearchParams(this.$location.search());
}.bind(this));
//Respond to any subsequent conductor changes
this.conductor.on('bounds', this.changeBounds);
this.conductor.on('timeSystem', this.changeTimeSystem);
}
/**
* @private
*/
TimeConductorController.prototype.initializeScope = function () {
//Set time Conductor bounds in the form
this.$scope.boundsModel = this.conductor.bounds();
//If conductor has a time system selected already, populate the
//form from it
this.$scope.timeSystemModel = {};
//Represents the various modes, and the currently selected mode
//in the view
this.$scope.modeModel = {
options: this.conductorViewService.availableModes()
};
// 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);
};
TimeConductorController.prototype.setStateFromSearchParams = function (searchParams) {
//Set mode from url if changed
if (searchParams[SEARCH.MODE] === undefined ||
searchParams[SEARCH.MODE] !== this.$scope.modeModel.selectedKey) {
this.setMode(searchParams[SEARCH.MODE] || "fixed");
}
if (searchParams[SEARCH.TIME_SYSTEM] &&
searchParams[SEARCH.TIME_SYSTEM] !== this.conductor.timeSystem().metadata.key) {
//Will select the specified time system on the conductor
this.selectTimeSystemByKey(searchParams[SEARCH.TIME_SYSTEM]);
}
var validDeltas = searchParams[SEARCH.MODE] !== 'fixed' &&
searchParams[SEARCH.START_DELTA] &&
searchParams[SEARCH.END_DELTA] &&
!isNaN(searchParams[SEARCH.START_DELTA]) &&
!isNaN(searchParams[SEARCH.END_DELTA]);
if (validDeltas) {
//Sets deltas from some form model
this.setDeltas({
startDelta: parseInt(searchParams[SEARCH.START_DELTA]),
endDelta: parseInt(searchParams[SEARCH.END_DELTA])
});
}
var validBounds = searchParams[SEARCH.MODE] === 'fixed' &&
searchParams[SEARCH.START_BOUND] &&
searchParams[SEARCH.END_BOUND] &&
!isNaN(searchParams[SEARCH.START_BOUND]) &&
!isNaN(searchParams[SEARCH.END_BOUND]);
if (validBounds) {
this.conductor.bounds({
start: parseInt(searchParams[SEARCH.START_BOUND]),
end: parseInt(searchParams[SEARCH.END_BOUND])
});
}
};
/**
* @private
*/
TimeConductorController.prototype.destroy = function () {
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);
if (this.conductorViewService.mode() === 'fixed') {
//Set bounds in URL on change
this.$location.search(SEARCH.START_BOUND, bounds.start);
this.$location.search(SEARCH.END_BOUND, bounds.end);
}
}
};
/**
* 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
* @param {TimeConductorBounds}
*/
TimeConductorController.prototype.setFormFromBounds = function (bounds) {
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));
}
}
};
/**
* 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;
//Synchronize scope with time system on mode
this.$scope.timeSystemModel.options =
this.conductorViewService.availableTimeSystems()
.map(function (t) {
return t.metadata;
});
};
/**
* When the deltas change, update the values in the UI
* @private
*/
TimeConductorController.prototype.setFormFromDeltas = function (deltas) {
this.$scope.boundsModel.startDelta = deltas.start;
this.$scope.boundsModel.endDelta = deltas.end;
};
/**
* Initialize the form when time system changes.
* @param {TimeSystem} timeSystem
*/
TimeConductorController.prototype.setFormFromTimeSystem = function (timeSystem) {
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.
* @param formModel
*/
TimeConductorController.prototype.setBounds = function (boundsModel) {
this.conductor.bounds({
start: boundsModel.start,
end: boundsModel.end
});
};
/**
* Called when the delta values in the form change. Validates and
* sets the new deltas on the Mode.
* @param boundsModel
* @see TimeConductorMode
*/
TimeConductorController.prototype.setDeltas = function (boundsFormModel) {
var deltas = {
start: boundsFormModel.startDelta,
end: boundsFormModel.endDelta
};
if (this.validation.validateStartDelta(deltas.start) && this.validation.validateEndDelta(deltas.end)) {
//Sychronize deltas between form and mode
this.conductorViewService.deltas(deltas);
//Set Deltas in URL on change
this.$location.search(SEARCH.START_DELTA, deltas.start);
this.$location.search(SEARCH.END_DELTA, deltas.end);
}
};
/**
* Change the selected Time Conductor mode. This will call destroy
* and initialization functions on the relevant modes, setting
* default values for bound and deltas in the form.
*
* @private
* @param newModeKey
* @param oldModeKey
*/
TimeConductorController.prototype.setMode = function (newModeKey, oldModeKey) {
//Set mode in URL on change
this.$location.search(SEARCH.MODE, newModeKey);
if (newModeKey !== oldModeKey) {
this.conductorViewService.mode(newModeKey);
this.setFormFromMode(newModeKey);
if (newModeKey === "fixed") {
this.$location.search(SEARCH.START_DELTA, null);
this.$location.search(SEARCH.END_DELTA, null);
} else {
this.$location.search(SEARCH.START_BOUND, null);
this.$location.search(SEARCH.END_BOUND, null);
var deltas = this.conductorViewService.deltas();
if (deltas) {
this.$location.search(SEARCH.START_DELTA, deltas.start);
this.$location.search(SEARCH.END_DELTA, deltas.end);
}
}
}
};
/**
* Respond to time system selection from UI
*
* Allows time system to be changed by key. This supports selection
* from the menu. Resolves a TimeSystem object and then invokes
* TimeConductorController#setTimeSystem
* @param key
* @see TimeConductorController#setTimeSystem
*/
TimeConductorController.prototype.selectTimeSystemByKey = function (key) {
var selected = this.timeSystems.filter(function (timeSystem) {
return timeSystem.metadata.key === key;
})[0];
this.conductor.timeSystem(selected, selected.defaults().bounds);
};
/**
* Handles time system change from time conductor
*
* Sets the selected time system. Will populate form with the default
* bounds and deltas defined in the selected time system.
*
* @param newTimeSystem
*/
TimeConductorController.prototype.changeTimeSystem = function (newTimeSystem) {
//Set time system in URL on change
this.$location.search(SEARCH.TIME_SYSTEM, newTimeSystem.metadata.key);
if (newTimeSystem && (newTimeSystem !== this.$scope.timeSystemModel.selected)) {
this.setFormFromTimeSystem(newTimeSystem);
if (newTimeSystem.defaults()) {
var deltas = newTimeSystem.defaults().deltas || {start: 0, end: 0};
var bounds = newTimeSystem.defaults().bounds || {start: 0, end: 0};
this.setFormFromDeltas(deltas);
this.setFormFromBounds(bounds);
}
}
};
/**
* 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.setBounds(this.$scope.boundsModel);
this.setDeltas(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;
}
);

View File

@@ -0,0 +1,543 @@
/*****************************************************************************
* 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(['./TimeConductorController'], function (TimeConductorController) {
describe("The time conductor controller", function () {
var mockScope;
var mockWindow;
var mockTimeConductor;
var mockConductorViewService;
var mockTimeSystems;
var controller;
var mockFormatService;
var mockFormat;
var mockLocation;
beforeEach(function () {
mockScope = jasmine.createSpyObj("$scope", [
"$watch",
"$on"
]);
mockWindow = jasmine.createSpyObj("$window", ["requestAnimationFrame"]);
mockTimeConductor = jasmine.createSpyObj(
"TimeConductor",
[
"bounds",
"timeSystem",
"on",
"off"
]
);
mockTimeConductor.bounds.andReturn({start: undefined, end: undefined});
mockConductorViewService = jasmine.createSpyObj(
"ConductorViewService",
[
"availableModes",
"mode",
"availableTimeSystems",
"deltas",
"deltas",
"on",
"off"
]
);
mockConductorViewService.availableModes.andReturn([]);
mockConductorViewService.availableTimeSystems.andReturn([]);
mockFormatService = jasmine.createSpyObj('formatService', [
'getFormat'
]);
mockFormat = jasmine.createSpyObj('format', [
'format'
]);
mockFormatService.getFormat.andReturn(mockFormat);
mockLocation = jasmine.createSpyObj('location', [
'search'
]);
mockLocation.search.andReturn({});
mockTimeSystems = [];
});
function getListener(target, event) {
return target.calls.filter(function (call) {
return call.args[0] === event;
})[0].args[1];
}
describe("when time conductor state changes", function () {
var mockDeltaFormat;
var defaultBounds;
var defaultDeltas;
var mockDefaults;
var timeSystem;
var tsListener;
beforeEach(function () {
mockFormat = {};
mockDeltaFormat = {};
defaultBounds = {
start: 2,
end: 3
};
defaultDeltas = {
start: 10,
end: 20
};
mockDefaults = {
deltas: defaultDeltas,
bounds: defaultBounds
};
timeSystem = {
metadata: {
key: 'mock'
},
formats: function () {
return [mockFormat];
},
deltaFormat: function () {
return mockDeltaFormat;
},
defaults: function () {
return mockDefaults;
}
};
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
mockTimeSystems,
mockFormatService
);
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.changeBounds);
});
it("deregisters conductor listens when scope is destroyed", function () {
expect(mockScope.$on).toHaveBeenCalledWith("$destroy", controller.destroy);
controller.destroy();
expect(mockTimeConductor.off).toHaveBeenCalledWith("timeSystem", controller.changeTimeSystem);
expect(mockTimeConductor.off).toHaveBeenCalledWith("bounds", controller.changeBounds);
});
it("when time system changes, sets time system on scope", function () {
expect(tsListener).toBeDefined();
tsListener(timeSystem);
expect(mockScope.timeSystemModel).toBeDefined();
expect(mockScope.timeSystemModel.selected).toBe(timeSystem);
expect(mockScope.timeSystemModel.format).toBe(mockFormat);
expect(mockScope.timeSystemModel.deltaFormat).toBe(mockDeltaFormat);
});
it("when time system changes, sets defaults on scope", function () {
mockDefaults.zoom = {
min: 100,
max: 10
};
mockTimeConductor.timeSystem.andReturn(timeSystem);
tsListener(timeSystem);
expect(mockScope.boundsModel.start).toEqual(defaultBounds.start);
expect(mockScope.boundsModel.end).toEqual(defaultBounds.end);
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 () {
var bounds = {
start: 1,
end: 2
};
var boundsListener = getListener(mockTimeConductor.on, "bounds");
expect(boundsListener).toBeDefined();
boundsListener(bounds);
expect(mockScope.boundsModel).toBeDefined();
expect(mockScope.boundsModel.start).toEqual(bounds.start);
expect(mockScope.boundsModel.end).toEqual(bounds.end);
});
});
describe("when user makes changes from UI", function () {
var mode = "realtime";
var ts1Metadata;
var ts2Metadata;
var ts3Metadata;
var mockTimeSystemConstructors;
beforeEach(function () {
mode = "realtime";
ts1Metadata = {
'key': 'ts1',
'name': 'Time System One',
'cssClass': 'cssClassOne'
};
ts2Metadata = {
'key': 'ts2',
'name': 'Time System Two',
'cssClass': 'cssClassTwo'
};
ts3Metadata = {
'key': 'ts3',
'name': 'Time System Three',
'cssClass': 'cssClassThree'
};
mockTimeSystems = [
{
metadata: ts1Metadata
},
{
metadata: ts2Metadata
},
{
metadata: ts3Metadata
}
];
//Wrap in mock constructors
mockTimeSystemConstructors = mockTimeSystems.map(function (mockTimeSystem) {
return function () {
return mockTimeSystem;
};
});
});
it("sets the mode on scope", function () {
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
mockTimeSystemConstructors,
mockFormatService
);
mockConductorViewService.availableTimeSystems.andReturn(mockTimeSystems);
controller.setMode(mode);
expect(mockScope.modeModel.selectedKey).toEqual(mode);
});
it("sets available time systems on scope when mode changes", function () {
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
mockTimeSystemConstructors,
mockFormatService
);
mockConductorViewService.availableTimeSystems.andReturn(mockTimeSystems);
controller.setMode(mode);
expect(mockScope.timeSystemModel.options.length).toEqual(3);
expect(mockScope.timeSystemModel.options[0]).toEqual(ts1Metadata);
expect(mockScope.timeSystemModel.options[1]).toEqual(ts2Metadata);
expect(mockScope.timeSystemModel.options[2]).toEqual(ts3Metadata);
});
it("sets bounds on the time conductor", function () {
var formModel = {
start: 1,
end: 10
};
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
mockTimeSystemConstructors,
mockFormatService
);
controller.setBounds(formModel);
expect(mockTimeConductor.bounds).toHaveBeenCalledWith(formModel);
});
it("applies deltas when they change in form", function () {
var deltas = {
start: 1000,
end: 2000
};
var formModel = {
startDelta: deltas.start,
endDelta: deltas.end
};
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
mockTimeSystemConstructors,
mockFormatService
);
controller.setDeltas(formModel);
expect(mockConductorViewService.deltas).toHaveBeenCalledWith(deltas);
});
it("sets the time system on the time conductor", function () {
var defaultBounds = {
start: 5,
end: 6
};
var timeSystem = {
metadata: {
key: 'testTimeSystem'
},
defaults: function () {
return {
bounds: defaultBounds
};
}
};
mockTimeSystems = [
// Wrap as constructor function
function () {
return timeSystem;
}
];
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
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);
});
});
describe("when the URL defines conductor state", function () {
var urlBounds;
var urlTimeSystem;
var urlMode;
var urlDeltas;
var mockDeltaFormat;
var defaultBounds;
var defaultDeltas;
var mockDefaults;
var timeSystem;
var otherTimeSystem;
var mockSearchObject;
beforeEach(function () {
mockFormat = {};
mockDeltaFormat = {};
defaultBounds = {
start: 2,
end: 3
};
defaultDeltas = {
start: 10,
end: 20
};
mockDefaults = {
deltas: defaultDeltas,
bounds: defaultBounds
};
timeSystem = {
metadata: {
key: 'mockTimeSystem'
},
formats: function () {
return [mockFormat];
},
deltaFormat: function () {
return mockDeltaFormat;
},
defaults: function () {
return mockDefaults;
}
};
otherTimeSystem = {
metadata: {
key: 'otherTimeSystem'
},
formats: function () {
return [mockFormat];
},
deltaFormat: function () {
return mockDeltaFormat;
},
defaults: function () {
return mockDefaults;
}
};
mockTimeSystems.push(function () {
return timeSystem;
});
mockTimeSystems.push(function () {
return otherTimeSystem;
});
urlBounds = {
start: 100,
end: 200
};
urlTimeSystem = "otherTimeSystem";
urlMode = "realtime";
urlDeltas = {
start: 300,
end: 400
};
mockSearchObject = {
"tc.startBound": urlBounds.start,
"tc.endBound": urlBounds.end,
"tc.startDelta": urlDeltas.start,
"tc.endDelta": urlDeltas.end,
"tc.timeSystem": urlTimeSystem
};
mockLocation.search.andReturn(mockSearchObject);
mockTimeConductor.timeSystem.andReturn(timeSystem);
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
mockTimeSystems,
mockFormatService
);
spyOn(controller, "setMode");
spyOn(controller, "selectTimeSystemByKey");
});
it("sets conductor state from URL", function () {
mockSearchObject["tc.mode"] = "fixed";
controller.setStateFromSearchParams(mockSearchObject);
expect(controller.selectTimeSystemByKey).toHaveBeenCalledWith("otherTimeSystem");
expect(mockTimeConductor.bounds).toHaveBeenCalledWith(urlBounds);
});
it("sets mode from URL", function () {
mockTimeConductor.bounds.reset();
mockSearchObject["tc.mode"] = "realtime";
controller.setStateFromSearchParams(mockSearchObject);
expect(controller.setMode).toHaveBeenCalledWith("realtime");
expect(controller.selectTimeSystemByKey).toHaveBeenCalledWith("otherTimeSystem");
expect(mockConductorViewService.deltas).toHaveBeenCalledWith(urlDeltas);
expect(mockTimeConductor.bounds).not.toHaveBeenCalled();
});
describe("when conductor state changes", function () {
it("updates the URL with the mode", function () {
controller.setMode("realtime", "fixed");
expect(mockLocation.search).toHaveBeenCalledWith("tc.mode", "fixed");
});
it("updates the URL with the bounds", function () {
mockConductorViewService.mode.andReturn("fixed");
controller.changeBounds({start: 500, end: 600});
expect(mockLocation.search).toHaveBeenCalledWith("tc.startBound", 500);
expect(mockLocation.search).toHaveBeenCalledWith("tc.endBound", 600);
});
it("updates the URL with the deltas", function () {
controller.setDeltas({startDelta: 700, endDelta: 800});
expect(mockLocation.search).toHaveBeenCalledWith("tc.startDelta", 700);
expect(mockLocation.search).toHaveBeenCalledWith("tc.endDelta", 800);
});
it("updates the URL with the time system", function () {
controller.changeTimeSystem(otherTimeSystem);
expect(mockLocation.search).toHaveBeenCalledWith("tc.timeSystem", "otherTimeSystem");
});
});
});
});
});

View File

@@ -0,0 +1,248 @@
/*****************************************************************************
* 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 () {
/**
* 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.deltasVal = undefined;
this.source = undefined;
this.sourceUnlisten = undefined;
this.systems = timeSystems;
this.availableSources = undefined;
this.changeTimeSystem = this.changeTimeSystem.bind(this);
this.tick = this.tick.bind(this);
//Set the time system initially
if (conductor.timeSystem()) {
this.changeTimeSystem(conductor.timeSystem());
}
//Listen for subsequent changes to time system
conductor.on('timeSystem', this.changeTimeSystem);
if (metadata.key === 'fixed') {
//Fixed automatically supports all time systems
this.availableSystems = timeSystems;
} else {
this.availableSystems = timeSystems.filter(function (timeSystem) {
//Only include time systems that have tick sources that
// support the current mode
return timeSystem.tickSources().some(function (tickSource) {
return metadata.key === tickSource.metadata.mode;
});
});
}
}
/**
* Get or set the currently selected time system
* @param timeSystem
* @returns {TimeSystem} the currently selected time system
*/
TimeConductorMode.prototype.changeTimeSystem = function (timeSystem) {
// On time system change, apply default deltas
var defaults = timeSystem.defaults() || {
bounds: {
start: 0,
end: 0
},
deltas: {
start: 0,
end: 0
}
};
this.conductor.bounds(defaults.bounds);
this.deltas(defaults.deltas);
// Tick sources are mode-specific, so restrict tick sources to only those supported by the current mode.
var key = this.mdata.key;
var tickSources = timeSystem.tickSources();
if (tickSources) {
this.availableSources = tickSources.filter(function (source) {
return source.metadata.mode === key;
});
}
// Set an appropriate tick source from the new time system
this.tickSource(this.availableTickSources(timeSystem)[0]);
};
/**
* @returns {ModeMetadata}
*/
TimeConductorMode.prototype.metadata = function () {
return this.mdata;
};
TimeConductorMode.prototype.availableTimeSystems = function () {
return this.availableSystems;
};
/**
* Tick sources are mode-specific. This returns a filtered list of the tick sources available in the currently selected mode
* @param timeSystem
* @returns {Array.<T>}
*/
TimeConductorMode.prototype.availableTickSources = function (timeSystem) {
return this.availableSources;
};
/**
* Get or set tick source. Setting tick source will also start
* listening to it and unlisten from any existing tick source
* @param tickSource
* @returns {TickSource}
*/
TimeConductorMode.prototype.tickSource = function (tickSource) {
if (arguments.length > 0) {
if (this.sourceUnlisten) {
this.sourceUnlisten();
}
this.source = tickSource;
if (tickSource) {
this.sourceUnlisten = tickSource.listen(this.tick);
//Now following a tick source
this.conductor.follow(true);
} else {
this.conductor.follow(false);
}
}
return this.source;
};
/**
* @private
*/
TimeConductorMode.prototype.destroy = function () {
this.conductor.off('timeSystem', this.changeTimeSystem);
if (this.sourceUnlisten) {
this.sourceUnlisten();
}
};
/**
* @private
* @param {number} time some value that is valid in the current TimeSystem
*/
TimeConductorMode.prototype.tick = function (time) {
var deltas = this.deltas();
var startTime = time;
var endTime = time;
if (deltas) {
startTime = time - deltas.start;
endTime = time + deltas.end;
}
this.conductor.bounds({
start: startTime,
end: endTime
});
};
/**
* Get or set the current value for the deltas used by this time system.
* On change, the new deltas will be used to calculate and set the
* bounds on the time conductor.
* @param deltas
* @returns {TimeSystemDeltas}
*/
TimeConductorMode.prototype.deltas = function (deltas) {
if (arguments.length !== 0) {
var bounds = this.calculateBoundsFromDeltas(deltas);
this.deltasVal = deltas;
if (this.metadata().key !== 'fixed') {
this.conductor.bounds(bounds);
}
}
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;
}
);

View File

@@ -0,0 +1,210 @@
/*****************************************************************************
* 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(['./TimeConductorMode'], function (TimeConductorMode) {
describe("The Time Conductor Mode", function () {
var mockTimeConductor,
fixedModeMetaData,
mockTimeSystems,
fixedTimeSystem,
realtimeModeMetaData,
realtimeTimeSystem,
mockTickSource,
mockBounds,
mode;
beforeEach(function () {
fixedModeMetaData = {
key: "fixed"
};
realtimeModeMetaData = {
key: "realtime"
};
mockBounds = {
start: 0,
end: 1
};
fixedTimeSystem = jasmine.createSpyObj("timeSystem", [
"defaults",
"tickSources"
]);
fixedTimeSystem.tickSources.andReturn([]);
mockTickSource = jasmine.createSpyObj("tickSource", [
"listen"
]);
mockTickSource.metadata = {
mode: "realtime"
};
realtimeTimeSystem = jasmine.createSpyObj("realtimeTimeSystem", [
"defaults",
"tickSources"
]);
realtimeTimeSystem.tickSources.andReturn([mockTickSource]);
//Do not return any time systems initially for a default
// construction configuration that works without any additional work
mockTimeSystems = [];
mockTimeConductor = jasmine.createSpyObj("timeConductor", [
"bounds",
"timeSystem",
"on",
"off",
"follow"
]);
mockTimeConductor.bounds.andReturn(mockBounds);
});
it("Reacts to changes in conductor time system", function () {
mode = new TimeConductorMode(fixedModeMetaData, mockTimeConductor, mockTimeSystems);
expect(mockTimeConductor.on).toHaveBeenCalledWith("timeSystem", mode.changeTimeSystem);
});
it("Stops listening to time system changes on destroy", function () {
mode = new TimeConductorMode(fixedModeMetaData, mockTimeConductor, mockTimeSystems);
mode.destroy();
expect(mockTimeConductor.off).toHaveBeenCalledWith("timeSystem", mode.changeTimeSystem);
});
it("Filters available time systems to those with tick sources that" +
" support this mode", function () {
mockTimeSystems = [fixedTimeSystem, realtimeTimeSystem];
mode = new TimeConductorMode(realtimeModeMetaData, mockTimeConductor, mockTimeSystems);
var availableTimeSystems = mode.availableTimeSystems();
expect(availableTimeSystems.length).toBe(1);
expect(availableTimeSystems.indexOf(fixedTimeSystem)).toBe(-1);
expect(availableTimeSystems.indexOf(realtimeTimeSystem)).toBe(0);
});
describe("Changing the time system", function () {
var defaults;
beforeEach(function () {
defaults = {
bounds: {
start: 1,
end: 2
},
deltas: {
start: 3,
end: 4
}
};
fixedTimeSystem.defaults.andReturn(defaults);
});
it ("sets defaults from new time system", function () {
mode = new TimeConductorMode(fixedModeMetaData, mockTimeConductor, mockTimeSystems);
spyOn(mode, "deltas");
mode.deltas.andCallThrough();
mode.changeTimeSystem(fixedTimeSystem);
expect(mockTimeConductor.bounds).toHaveBeenCalledWith(defaults.bounds);
expect(mode.deltas).toHaveBeenCalledWith(defaults.deltas);
});
it ("If a tick source is available, sets the tick source", function () {
mode = new TimeConductorMode(realtimeModeMetaData, mockTimeConductor, mockTimeSystems);
mode.changeTimeSystem(realtimeTimeSystem);
var currentTickSource = mode.tickSource();
expect(currentTickSource).toBe(mockTickSource);
});
});
describe("Setting a tick source", function () {
var mockUnlistener;
beforeEach(function () {
mockUnlistener = jasmine.createSpy("unlistener");
mockTickSource.listen.andReturn(mockUnlistener);
mode = new TimeConductorMode(realtimeModeMetaData, mockTimeConductor, mockTimeSystems);
mode.tickSource(mockTickSource);
});
it ("Unlistens from old tick source", function () {
mode.tickSource(mockTickSource);
expect(mockUnlistener).toHaveBeenCalled();
});
it ("Listens to new tick source", function () {
expect(mockTickSource.listen).toHaveBeenCalledWith(mode.tick);
});
it ("Sets 'follow' state on time conductor", function () {
expect(mockTimeConductor.follow).toHaveBeenCalledWith(true);
});
it ("on destroy, unlistens from tick source", function () {
mode.destroy();
expect(mockUnlistener).toHaveBeenCalled();
});
});
describe("setting deltas", function () {
beforeEach(function () {
mode = new TimeConductorMode(realtimeModeMetaData, mockTimeConductor, mockTimeSystems);
});
it ("sets the bounds on the time conductor based on new delta" +
" values", function () {
var deltas = {
start: 20,
end: 10
};
mode.deltas(deltas);
expect(mockTimeConductor.bounds).toHaveBeenCalledWith({
start: mockBounds.end - deltas.start,
end: mockBounds.end + deltas.end
});
});
});
describe("ticking", function () {
beforeEach(function () {
mode = new TimeConductorMode(realtimeModeMetaData, mockTimeConductor, mockTimeSystems);
});
it ("sets bounds based on current delta values", function () {
var deltas = {
start: 20,
end: 10
};
var time = 100;
mode.deltas(deltas);
mode.tick(time);
expect(mockTimeConductor.bounds).toHaveBeenCalledWith({
start: time - deltas.start,
end: time + deltas.end
});
});
});
});
});

View File

@@ -0,0 +1,69 @@
/*****************************************************************************
* 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 () {
/**
* Form validation for the TimeConductorController.
* @param conductor
* @constructor
*/
function TimeConductorValidation(conductor) {
var self = this;
this.conductor = conductor;
/*
* Bind all class functions to 'this'
*/
Object.keys(TimeConductorValidation.prototype).filter(function (key) {
return typeof TimeConductorValidation.prototype[key] === 'function';
}).forEach(function (key) {
self[key] = self[key].bind(self);
});
}
/**
* Validation methods below are invoked directly from controls in the TimeConductor form
*/
TimeConductorValidation.prototype.validateStart = function (start) {
var bounds = this.conductor.bounds();
return this.conductor.validateBounds({start: start, end: bounds.end}) === true;
};
TimeConductorValidation.prototype.validateEnd = function (end) {
var bounds = this.conductor.bounds();
return this.conductor.validateBounds({start: bounds.start, end: end}) === true;
};
TimeConductorValidation.prototype.validateStartDelta = function (startDelta) {
return !isNaN(startDelta) && startDelta > 0;
};
TimeConductorValidation.prototype.validateEndDelta = function (endDelta) {
return !isNaN(endDelta) && endDelta >= 0;
};
return TimeConductorValidation;
}
);

View File

@@ -0,0 +1,73 @@
/*****************************************************************************
* 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(['./TimeConductorValidation'], function (TimeConductorValidation) {
describe("The Time Conductor Validation class", function () {
var timeConductorValidation,
mockTimeConductor;
beforeEach(function () {
mockTimeConductor = jasmine.createSpyObj("timeConductor", [
"validateBounds",
"bounds"
]);
timeConductorValidation = new TimeConductorValidation(mockTimeConductor);
});
describe("Validates start and end values using Time Conductor", function () {
beforeEach(function () {
var mockBounds = {
start: 10,
end: 20
};
mockTimeConductor.bounds.andReturn(mockBounds);
});
it("Validates start values using Time Conductor", function () {
var startValue = 30;
timeConductorValidation.validateStart(startValue);
expect(mockTimeConductor.validateBounds).toHaveBeenCalled();
});
it("Validates end values using Time Conductor", function () {
var endValue = 40;
timeConductorValidation.validateEnd(endValue);
expect(mockTimeConductor.validateBounds).toHaveBeenCalled();
});
});
it("Validates that start delta is valid number > 0", function () {
expect(timeConductorValidation.validateStartDelta(-1)).toBe(false);
expect(timeConductorValidation.validateStartDelta("abc")).toBe(false);
expect(timeConductorValidation.validateStartDelta("1")).toBe(true);
expect(timeConductorValidation.validateStartDelta(1)).toBe(true);
});
it("Validates that end delta is valid number >= 0", function () {
expect(timeConductorValidation.validateEndDelta(-1)).toBe(false);
expect(timeConductorValidation.validateEndDelta("abc")).toBe(false);
expect(timeConductorValidation.validateEndDelta("1")).toBe(true);
expect(timeConductorValidation.validateEndDelta(0)).toBe(true);
expect(timeConductorValidation.validateEndDelta(1)).toBe(true);
});
});
});

View File

@@ -0,0 +1,229 @@
/*****************************************************************************
* 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',
'./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(openmct, timeSystems) {
EventEmitter.call(this);
this.systems = timeSystems.map(function (timeSystemConstructor) {
return timeSystemConstructor();
});
this.conductor = openmct.conductor;
this.currentMode = undefined;
/**
* @typedef {object} ModeMetadata
* @property {string} key A unique identifying key for this mode
* @property {string} cssClass The css class for the glyph
* representing this mode
* @property {string} label A short label for this mode
* @property {string} name A longer name for the mode
* @property {string} description A description of the mode
*/
this.availModes = {
'fixed': {
key: 'fixed',
cssclass: 'icon-calendar',
label: 'Fixed',
name: 'Fixed Timespan Mode',
description: 'Query and explore data that falls between two fixed datetimes.'
}
};
function hasTickSource(sourceType, timeSystem) {
return timeSystem.tickSources().some(function (tickSource) {
return tickSource.metadata.mode === sourceType;
});
}
var timeSystemsForMode = function (sourceType) {
return this.systems.filter(hasTickSource.bind(this, sourceType));
}.bind(this);
//Only show 'real-time mode' if appropriate time systems available
if (timeSystemsForMode('realtime').length > 0) {
var realtimeMode = {
key: 'realtime',
cssclass: 'icon-clock',
label: 'Real-time',
name: 'Real-time Mode',
description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.'
};
this.availModes[realtimeMode.key] = realtimeMode;
}
//Only show 'LAD mode' if appropriate time systems available
if (timeSystemsForMode('lad').length > 0) {
var ladMode = {
key: 'lad',
cssclass: 'icon-database',
label: 'LAD',
name: 'LAD Mode',
description: 'Latest Available Data mode monitors real-time streaming data as it comes in. The Time Conductor and displays will only advance when data becomes available.'
};
this.availModes[ladMode.key] = ladMode;
}
}
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
* bounds and how they change with time.
*
* In fixed mode, the bounds do not change with time, but can be
* modified by the used
*
* In realtime mode, the bounds change with time. Bounds are not
* directly modifiable by the user, however deltas can be.
*
* In Latest Available Data (LAD) mode, the bounds are updated when
* data is received. As with realtime mode the
*
* @param {string} newModeKey One of 'fixed', 'realtime', or 'LAD'
* @returns {string} the current mode, one of 'fixed', 'realtime',
* or 'LAD'.
*
*/
TimeConductorViewService.prototype.mode = function (newModeKey) {
function contains(timeSystems, ts) {
return timeSystems.filter(function (t) {
return t.metadata.key === ts.metadata.key;
}).length > 0;
}
if (arguments.length === 1) {
var timeSystem = this.conductor.timeSystem();
var modes = this.availableModes();
var modeMetaData = modes[newModeKey];
if (this.currentMode) {
this.currentMode.destroy();
}
this.currentMode = new TimeConductorMode(modeMetaData, this.conductor, this.systems);
// If no time system set on time conductor, or the currently selected time system is not available in
// the new mode, default to first available time system
if (!timeSystem || !contains(this.currentMode.availableTimeSystems(), timeSystem)) {
timeSystem = this.currentMode.availableTimeSystems()[0];
this.conductor.timeSystem(timeSystem, timeSystem.defaults().bounds);
}
}
return this.currentMode ? this.currentMode.metadata().key : undefined;
};
/**
* @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
* bound.
* @property {number} end Used to set the end bound of the
* TimeConductor on tick. A positive value that will be added
* from the value provided by a tick source to determine the start
* bound.
*/
/**
* Deltas define the offset from the latest time value provided by
* the current tick source. Deltas are only valid in realtime or LAD
* modes.
*
* Realtime mode:
* - start: A time in ms before now which will be used to
* determine the 'start' bound on tick
* - end: A time in ms after now which will be used to determine
* the 'end' bound on tick
*
* LAD mode:
* - start: A time in ms before the timestamp of the last data
* received which will be used to determine the 'start' bound on
* 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 {TimeConductorDeltas} current value of the deltas
*/
TimeConductorViewService.prototype.deltas = function () {
//Deltas stored on mode. Use .apply to preserve arguments
return this.currentMode.deltas.apply(this.currentMode, arguments);
};
/**
* Availability of modes depends on the time systems and tick
* sources available. For example, Latest Available Data mode will
* not be available if there are no time systems and tick sources
* that support LAD mode.
* @returns {ModeMetadata[]}
*/
TimeConductorViewService.prototype.availableModes = function () {
return this.availModes;
};
/**
* Availability of time systems depends on the currently selected
* mode. Time systems and tick sources are mode dependent
*/
TimeConductorViewService.prototype.availableTimeSystems = function () {
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;
}
);

View File

@@ -0,0 +1,185 @@
/*****************************************************************************
* 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(['./TimeConductorViewService'], function (TimeConductorViewService) {
describe("The Time Conductor view service", function () {
var mockTimeConductor;
var basicTimeSystem;
var tickingTimeSystem;
var viewService;
var tickingTimeSystemDefaults;
function mockConstructor(object) {
return function () {
return object;
};
}
beforeEach(function () {
mockTimeConductor = jasmine.createSpyObj("timeConductor", [
"timeSystem",
"bounds",
"follow",
"on",
"off"
]);
basicTimeSystem = jasmine.createSpyObj("basicTimeSystem", [
"tickSources",
"defaults"
]);
basicTimeSystem.metadata = {
key: "basic"
};
basicTimeSystem.tickSources.andReturn([]);
basicTimeSystem.defaults.andReturn({
bounds: {
start: 0,
end: 1
},
deltas: {
start: 0,
end: 0
}
});
//Initialize conductor
mockTimeConductor.timeSystem.andReturn(basicTimeSystem);
mockTimeConductor.bounds.andReturn({start: 0, end: 1});
tickingTimeSystem = jasmine.createSpyObj("tickingTimeSystem", [
"tickSources",
"defaults"
]);
tickingTimeSystem.metadata = {
key: "ticking"
};
tickingTimeSystemDefaults = {
bounds: {
start: 100,
end: 200
},
deltas: {
start: 1000,
end: 500
}
};
tickingTimeSystem.defaults.andReturn(tickingTimeSystemDefaults);
});
it("At a minimum supports fixed mode", function () {
var mockTimeSystems = [mockConstructor(basicTimeSystem)];
viewService = new TimeConductorViewService({conductor: mockTimeConductor}, mockTimeSystems);
var availableModes = viewService.availableModes();
expect(availableModes.fixed).toBeDefined();
});
it("Supports realtime mode if appropriate tick source(s) availables", function () {
var mockTimeSystems = [mockConstructor(tickingTimeSystem)];
var mockRealtimeTickSource = {
metadata: {
mode: 'realtime'
}
};
tickingTimeSystem.tickSources.andReturn([mockRealtimeTickSource]);
viewService = new TimeConductorViewService({conductor: mockTimeConductor}, mockTimeSystems);
var availableModes = viewService.availableModes();
expect(availableModes.realtime).toBeDefined();
});
it("Supports LAD mode if appropriate tick source(s) available", function () {
var mockTimeSystems = [mockConstructor(tickingTimeSystem)];
var mockLADTickSource = {
metadata: {
mode: 'lad'
}
};
tickingTimeSystem.tickSources.andReturn([mockLADTickSource]);
viewService = new TimeConductorViewService({conductor: mockTimeConductor}, mockTimeSystems);
var availableModes = viewService.availableModes();
expect(availableModes.lad).toBeDefined();
});
describe("when mode is changed", function () {
it("destroys previous mode", function () {
var mockTimeSystems = [mockConstructor(basicTimeSystem)];
var oldMode = jasmine.createSpyObj("conductorMode", [
"destroy"
]);
viewService = new TimeConductorViewService({conductor: mockTimeConductor}, mockTimeSystems);
viewService.currentMode = oldMode;
viewService.mode('fixed');
expect(oldMode.destroy).toHaveBeenCalled();
});
describe("the time system", function () {
it("is retained if available in new mode", function () {
var mockTimeSystems = [mockConstructor(basicTimeSystem), mockConstructor(tickingTimeSystem)];
var mockRealtimeTickSource = {
metadata: {
mode: 'realtime'
},
listen: function () {}
};
tickingTimeSystem.tickSources.andReturn([mockRealtimeTickSource]);
viewService = new TimeConductorViewService({conductor: mockTimeConductor}, mockTimeSystems);
//Set time system to one known to support realtime mode
mockTimeConductor.timeSystem.andReturn(tickingTimeSystem);
//Select realtime mode
mockTimeConductor.timeSystem.reset();
viewService.mode('realtime');
expect(mockTimeConductor.timeSystem).not.toHaveBeenCalledWith(tickingTimeSystem, tickingTimeSystemDefaults.bounds);
});
it("is defaulted if selected time system not available in new mode", function () {
var mockTimeSystems = [mockConstructor(basicTimeSystem), mockConstructor(tickingTimeSystem)];
var mockRealtimeTickSource = {
metadata: {
mode: 'realtime'
},
listen: function () {}
};
tickingTimeSystem.tickSources.andReturn([mockRealtimeTickSource]);
viewService = new TimeConductorViewService({conductor: mockTimeConductor}, mockTimeSystems);
//Set time system to one known to not support realtime mode
mockTimeConductor.timeSystem.andReturn(basicTimeSystem);
//Select realtime mode
mockTimeConductor.timeSystem.reset();
viewService.mode('realtime');
expect(mockTimeConductor.timeSystem).toHaveBeenCalledWith(tickingTimeSystem, tickingTimeSystemDefaults.bounds);
});
});
});
});
});

View File

@@ -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;
}
);

View File

@@ -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);
});
});
});
});