Merge branch 'open1515' into open115

This commit is contained in:
Victor Woeltjen 2015-09-23 16:05:09 -07:00
commit 0260e6fff4
42 changed files with 2477 additions and 177 deletions

View File

@ -25,8 +25,8 @@
* Module defining SinewaveTelemetryProvider. Created by vwoeltje on 11/12/14.
*/
define(
["./SinewaveTelemetry"],
function (SinewaveTelemetry) {
["./SinewaveTelemetrySeries"],
function (SinewaveTelemetrySeries) {
"use strict";
/**
@ -45,7 +45,7 @@ define(
function generateData(request) {
return {
key: request.key,
telemetry: new SinewaveTelemetry(request)
telemetry: new SinewaveTelemetrySeries(request)
};
}
@ -112,4 +112,4 @@ define(
return SinewaveTelemetryProvider;
}
);
);

View File

@ -29,35 +29,45 @@ define(
function () {
"use strict";
var firstObservedTime = Date.now();
var firstObservedTime = Math.floor(Date.now() / 1000);
/**
*
* @constructor
*/
function SinewaveTelemetry(request) {
var latestObservedTime = Date.now(),
count = Math.floor((latestObservedTime - firstObservedTime) / 1000),
function SinewaveTelemetrySeries(request) {
var latestObservedTime = Math.floor(Date.now() / 1000),
endTime = (request.end !== undefined) ?
Math.floor(request.end / 1000) : latestObservedTime,
count =
Math.min(endTime, latestObservedTime) - firstObservedTime,
period = request.period || 30,
generatorData = {};
generatorData = {},
offset = (request.start !== undefined) ?
Math.floor(request.start / 1000) - firstObservedTime :
0;
if (request.size !== undefined) {
offset = Math.max(offset, count - request.size);
}
generatorData.getPointCount = function () {
return count;
return count - offset;
};
generatorData.getDomainValue = function (i, domain) {
return i * 1000 +
(domain !== 'delta' ? firstObservedTime : 0);
return (i + offset) * 1000 +
(domain !== 'delta' ? (firstObservedTime * 1000) : 0);
};
generatorData.getRangeValue = function (i, range) {
range = range || "sin";
return Math[range](i * Math.PI * 2 / period);
return Math[range]((i + offset) * Math.PI * 2 / period);
};
return generatorData;
}
return SinewaveTelemetry;
return SinewaveTelemetrySeries;
}
);
);

View File

@ -45,6 +45,16 @@
}
],
"controllers": [
{
"key": "TimeRangeController",
"implementation": "controllers/TimeRangeController.js",
"depends": [ "$scope", "now" ]
},
{
"key": "DateTimePickerController",
"implementation": "controllers/DateTimePickerController.js",
"depends": [ "$scope", "now" ]
},
{
"key": "TreeNodeController",
"implementation": "controllers/TreeNodeController.js",
@ -105,11 +115,21 @@
"implementation": "directives/MCTDrag.js",
"depends": [ "$document" ]
},
{
"key": "mctClickElsewhere",
"implementation": "directives/MCTClickElsewhere.js",
"depends": [ "$document" ]
},
{
"key": "mctResize",
"implementation": "directives/MCTResize.js",
"depends": [ "$timeout" ]
},
{
"key": "mctPopup",
"implementation": "directives/MCTPopup.js",
"depends": [ "$window", "$document", "$compile", "$interval" ]
},
{
"key": "mctScrollX",
"implementation": "directives/MCTScroll.js",
@ -213,6 +233,10 @@
{
"key": "selector",
"templateUrl": "templates/controls/selector.html"
},
{
"key": "datetime-picker",
"templateUrl": "templates/controls/datetime-picker.html"
}
],
"licenses": [

View File

@ -0,0 +1,61 @@
<!--
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 ng-controller="DateTimePickerController">
<div style="vertical-align: top; display: inline-block">
<div style="text-align: center;">
<a ng-click="changeMonth(-1)">&lt;</a>
{{month}} {{year}}
<a ng-click="changeMonth(1)">&gt;</a>
</div>
<div>
<table>
<tr>
<th ng-repeat="day in ['Su','Mo','Tu','We','Th','Fr','Sa']">
{{day}}
</th>
</tr>
<tr ng-repeat="row in table">
<td style="text-align: center;"
ng-repeat="cell in row"
ng-click="select(cell)"
ng-class='{
disabled: !isSelectable(cell),
test: isSelected(cell)
}'>
<div>{{cell.day}}</div>
<div style="font-size: 80%">{{cell.dayOfYear}}</div>
</td>
</tr>
</table>
</div>
</div>
<div style="vertical-align: top; display: inline-block"
ng-repeat="key in ['hours', 'minutes', 'seconds']"
ng-if="options[key]">
<div>{{nameFor(key)}}</div>
<select size="10"
ng-model="time[key]"
ng-options="i for i in optionsFor(key)">
</select>
</div>
</div>

View File

@ -1,69 +1,96 @@
<!--
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.
NOTES
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.
Ticks:
The thinking is to divide whatever the current time span is by 5,
and assign values accordingly to 5 statically-positioned ticks. So the tick x-position is a static percentage
of the total width available, and the labels change dynamically. This is consistent
with our current approach to the time axis of plots.
I'm keeping the number of ticks low so that when the view portal gets narrow,
the tick labels won't collide with each other. For extra credit, add/remove ticks as the user resizes the view area.
Note: this eval needs to be based on the whatever is containing the
time-controller component, not the whole browser window.
Range indicator and slider knobs:
The left and right properties used in .slider .range-holder and the .knobs are
CSS offsets from the left and right of their respective containers. You
may want or need to calculate those positions as pure offsets from the start datetime
(or left, as it were) and set them as left properties. No problem if so, but
we'll need to tweak the CSS tiny bit to get the center of the knobs to line up
properly on the range left and right bounds.
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 ng-init="
notes = 'Temporarily using an array to populate ticks so I can see what I\'m doing';
ticks = [
'00:00',
'00:30',
'01:00',
'01:30',
'02:00'
];
"></div>
<div class="l-time-controller" ng-controller="TimeRangeController">
<div class="l-time-range-inputs-holder">
Start: {{startOuterText}}
<span ng-controller="ToggleController as t">
<a class="ui-symbol" ng-click="t.toggle()">p</a>
<mct-popup ng-if="t.isActive()">
<div style="background: #222;"
mct-click-elsewhere="t.setState(false)">
<mct-control key="'datetime-picker'"
ng-model="ngModel.outer"
field="'start'"
options="{ hours: true }">
</mct-control>
</div>
</mct-popup>
</span>
<div class="l-time-controller">
<div class="l-time-range-inputs-holder">
Start: <input type="date" />
End: <input type="date" />
</div>
End: {{endOuterText}}
<span ng-controller="ToggleController as t2">
<a class="ui-symbol" ng-click="t2.toggle()">p</a>
<mct-popup ng-if="t2.isActive()">
<div style="background: #222;"
mct-click-elsewhere="t2.setState(false)">
<mct-control key="'datetime-picker'"
ng-model="ngModel.outer"
field="'end'"
options="{ hours: true }">
</mct-control>
</div>
</mct-popup>
</span>
<div class="l-time-range-slider-holder">
<div class="l-time-range-slider">
<div class="slider">
<div class="slot range-holder">
<div class="range" style="left: 0%; right: 30%;"></div>
</div>
<div class="knob knob-l" style="left: 0%;">
<div class="range-value">05/22 14:46</div>
</div>
<div class="knob knob-r" style="right: 30%;">
<div class="range-value">07/22 01:21</div>
</div>
</div>
</div>
</div>
<div class="l-time-range-ticks-holder">
<div class="l-time-range-ticks">
<div
ng-repeat="tick in ticks"
ng-style="{ left: $index * 25 + '%' }"
class="tick tick-x"
>
<span class="l-time-range-tick-label">{{tick}}</span>
</div>
</div>
</div>
</div>
</div>
<div class="l-time-range-slider-holder">
<div class="l-time-range-slider">
<div class="slider"
mct-resize="spanWidth = bounds.width">
<div class="slot range-holder">
<div class="range"
mct-drag-down="startMiddleDrag()"
mct-drag="middleDrag(delta[0])"
ng-style="{ left: startInnerPct, right: endInnerPct}">
</div>
</div>
<div class="knob knob-l"
mct-drag-down="startLeftDrag()"
mct-drag="leftDrag(delta[0])"
ng-style="{ left: startInnerPct }">
<div class="range-value">{{startInnerText}}</div>
</div>
<div class="knob knob-r"
mct-drag-down="startRightDrag()"
mct-drag="rightDrag(delta[0])"
ng-style="{ right: endInnerPct }">
<div class="range-value">{{endInnerText}}</div>
</div>
</div>
</div>
</div>
<div class="l-time-range-ticks-holder">
<div class="l-time-range-ticks">
<div
ng-repeat="tick in ticks"
ng-style="{ left: $index * (100 / (ticks.length - 1)) + '%' }"
class="tick tick-x"
>
<span class="l-time-range-tick-label">{{tick}}</span>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,202 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*global define,Promise*/
define(
[ 'moment' ],
function (moment) {
'use strict';
var TIME_NAMES = {
'hours': "Hour",
'minutes': "Minute",
'seconds': "Second"
},
MONTHS = moment.months(),
TIME_OPTIONS = (function makeRanges() {
var arr = [];
while (arr.length < 60) {
arr.push(arr.length);
}
return {
hours: arr.slice(0, 24),
minutes: arr,
seconds: arr
};
}());
/**
* Controller to support the date-time picker.
*
* Adds/uses the following properties in scope:
* * `year`: Year being displayed in picker
* * `month`: Month being displayed
* * `table`: Table being displayed; array of arrays of
* * `day`: Day of month
* * `dayOfYear`: Day of year
* * `month`: Month associated with the day
* * `year`: Year associated with the day.
* * `date`: Date chosen
* * `year`: Year selected
* * `month`: Month selected (0-indexed)
* * `day`: Day of month selected
* * `time`: Chosen time (hours/minutes/seconds)
* * `hours`: Hours chosen
* * `minutes`: Minutes chosen
* * `seconds`: Seconds chosen
*
* Months are zero-indexed, day-of-months are one-indexed.
*/
function DateTimePickerController($scope, now) {
var year,
month, // For picker state, not model state
interacted = false;
function generateTable() {
var m = moment.utc({ year: year, month: month }).day(0),
table = [],
row,
col;
for (row = 0; row < 6; row += 1) {
table.push([]);
for (col = 0; col < 7; col += 1) {
table[row].push({
year: m.year(),
month: m.month(),
day: m.date(),
dayOfYear: m.dayOfYear()
});
m.add(1, 'days'); // Next day!
}
}
return table;
}
function updateScopeForMonth() {
$scope.month = MONTHS[month];
$scope.year = year;
$scope.table = generateTable();
}
function updateFromModel(ngModel) {
var m;
m = moment.utc(ngModel);
$scope.date = {
year: m.year(),
month: m.month(),
day: m.date()
};
$scope.time = {
hours: m.hour(),
minutes: m.minute(),
seconds: m.second()
};
//window.alert($scope.date.day + " " + ngModel);
// Zoom to that date in the picker, but
// only if the user hasn't interacted with it yet.
if (!interacted) {
year = m.year();
month = m.month();
updateScopeForMonth();
}
}
function updateFromView() {
var m = moment.utc({
year: $scope.date.year,
month: $scope.date.month,
day: $scope.date.day,
hour: $scope.time.hours,
minute: $scope.time.minutes,
second: $scope.time.seconds
});
$scope.ngModel[$scope.field] = m.valueOf();
}
$scope.isSelectable = function (cell) {
return cell.month === month;
};
$scope.isSelected = function (cell) {
var date = $scope.date || {};
return cell.day === date.day &&
cell.month === date.month &&
cell.year === date.year;
};
$scope.select = function (cell) {
$scope.date = $scope.date || {};
$scope.date.month = cell.month;
$scope.date.year = cell.year;
$scope.date.day = cell.day;
updateFromView();
};
$scope.dateEquals = function (d1, d2) {
return d1.year === d2.year &&
d1.month === d2.month &&
d1.day === d2.day;
};
$scope.changeMonth = function (delta) {
month += delta;
if (month > 11) {
month = 0;
year += 1;
}
if (month < 0) {
month = 11;
year -= 1;
}
interacted = true;
updateScopeForMonth();
};
$scope.nameFor = function (key) {
return TIME_NAMES[key];
};
$scope.optionsFor = function (key) {
return TIME_OPTIONS[key];
};
updateScopeForMonth();
// Ensure some useful default
$scope.ngModel[$scope.field] =
$scope.ngModel[$scope.field] === undefined ?
now() : $scope.ngModel[$scope.field];
$scope.$watch('ngModel[field]', updateFromModel);
$scope.$watchCollection('date', updateFromView);
$scope.$watchCollection('time', updateFromView);
}
return DateTimePickerController;
}
);

View File

@ -0,0 +1,224 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*global define,Promise*/
define(
['moment'],
function (moment) {
"use strict";
var DATE_FORMAT = "YYYY-MM-DD HH:mm:ss";
/**
* @memberof platform/commonUI/general
* @constructor
*/
function TimeConductorController($scope, now) {
var tickCount = 2,
initialDragValue;
function formatTimestamp(ts) {
return moment.utc(ts).format(DATE_FORMAT);
}
// From 0.0-1.0 to "0%"-"1%"
function toPercent(p) {
return (100 * p) + "%";
}
function updateTicks() {
var i, p, ts, start, end, span;
end = $scope.ngModel.outer.end;
start = $scope.ngModel.outer.start;
span = end - start;
$scope.ticks = [];
for (i = 0; i < tickCount; i += 1) {
p = i / (tickCount - 1);
ts = p * span + start;
$scope.ticks.push(formatTimestamp(ts));
}
}
function updateSpanWidth(w) {
// Space about 100px apart
tickCount = Math.max(Math.floor(w / 100), 2);
updateTicks();
}
function updateViewForInnerSpanFromModel(ngModel) {
var span = ngModel.outer.end - ngModel.outer.start;
// Expose readable dates for the knobs
$scope.startInnerText = formatTimestamp(ngModel.inner.start);
$scope.endInnerText = formatTimestamp(ngModel.inner.end);
// And positions for the knobs
$scope.startInnerPct =
toPercent((ngModel.inner.start - ngModel.outer.start) / span);
$scope.endInnerPct =
toPercent((ngModel.outer.end - ngModel.inner.end) / span);
}
function defaultBounds() {
var t = now();
return {
start: t - 24 * 3600 * 1000, // One day
end: t
};
}
function copyBounds(bounds) {
return { start: bounds.start, end: bounds.end };
}
function updateViewFromModel(ngModel) {
var t = now();
ngModel = ngModel || {};
ngModel.outer = ngModel.outer || defaultBounds();
ngModel.inner = ngModel.inner || copyBounds(ngModel.outer);
// First, dates for the date pickers for outer bounds
$scope.startOuterDate = new Date(ngModel.outer.start);
$scope.endOuterDate = new Date(ngModel.outer.end);
// Then various updates for the inner span
updateViewForInnerSpanFromModel(ngModel);
// Stick it back is scope (in case we just set defaults)
$scope.ngModel = ngModel;
updateTicks();
}
function startLeftDrag() {
initialDragValue = $scope.ngModel.inner.start;
}
function startRightDrag() {
initialDragValue = $scope.ngModel.inner.end;
}
function startMiddleDrag() {
initialDragValue = {
start: $scope.ngModel.inner.start,
end: $scope.ngModel.inner.end
};
}
function toMillis(pixels) {
var span = $scope.ngModel.outer.end - $scope.ngModel.outer.start;
return (pixels / $scope.spanWidth) * span;
}
function clamp(value, low, high) {
return Math.max(low, Math.min(high, value));
}
function leftDrag(pixels) {
var delta = toMillis(pixels);
$scope.ngModel.inner.start = clamp(
initialDragValue + delta,
$scope.ngModel.outer.start,
$scope.ngModel.inner.end
);
updateViewFromModel($scope.ngModel);
}
function rightDrag(pixels) {
var delta = toMillis(pixels);
$scope.ngModel.inner.end = clamp(
initialDragValue + delta,
$scope.ngModel.inner.start,
$scope.ngModel.outer.end
);
updateViewFromModel($scope.ngModel);
}
function middleDrag(pixels) {
var delta = toMillis(pixels),
edge = delta < 0 ? 'start' : 'end',
opposite = delta < 0 ? 'end' : 'start';
// Adjust the position of the edge in the direction of drag
$scope.ngModel.inner[edge] = clamp(
initialDragValue[edge] + delta,
$scope.ngModel.outer.start,
$scope.ngModel.outer.end
);
// Adjust opposite knob to maintain span
$scope.ngModel.inner[opposite] = $scope.ngModel.inner[edge] +
initialDragValue[opposite] - initialDragValue[edge];
updateViewFromModel($scope.ngModel);
}
function updateOuterStart(t) {
var ngModel = $scope.ngModel;
ngModel.outer.end =
Math.max(ngModel.outer.start, ngModel.outer.end);
ngModel.inner.start =
Math.max(ngModel.outer.start, ngModel.inner.start);
ngModel.inner.end =
Math.max(ngModel.outer.start, ngModel.inner.end);
$scope.startOuterText = formatTimestamp(t);
updateViewForInnerSpanFromModel(ngModel);
}
function updateOuterEnd(t) {
var ngModel = $scope.ngModel;
ngModel.outer.start =
Math.min(ngModel.outer.end, ngModel.outer.start);
ngModel.inner.start =
Math.min(ngModel.outer.end, ngModel.inner.start);
ngModel.inner.end =
Math.min(ngModel.outer.end, ngModel.inner.end);
$scope.endOuterText = formatTimestamp(t);
updateViewForInnerSpanFromModel(ngModel);
}
$scope.startLeftDrag = startLeftDrag;
$scope.startRightDrag = startRightDrag;
$scope.startMiddleDrag = startMiddleDrag;
$scope.leftDrag = leftDrag;
$scope.rightDrag = rightDrag;
$scope.middleDrag = middleDrag;
$scope.state = false;
$scope.ticks = [];
// Initialize scope to defaults
updateViewFromModel($scope.ngModel);
$scope.$watchCollection("ngModel", updateViewFromModel);
$scope.$watch("spanWidth", updateSpanWidth);
$scope.$watch("ngModel.outer.start", updateOuterStart);
$scope.$watch("ngModel.outer.end", updateOuterEnd);
}
return TimeConductorController;
}
);

View File

@ -0,0 +1,77 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*global define*/
define(
[],
function () {
"use strict";
/**
* The `mct-click-elsewhere` directive will evaluate its
* associated expression whenever a `mousedown` occurs anywhere
* outside of the element that has the `mct-click-elsewhere`
* directive attached. This is useful for dismissing popups
* and the like.
*/
function MCTClickElsewhere($document) {
// Link; install event handlers.
function link(scope, element, attrs) {
// Keep a reference to the body, to attach/detach
// mouse event handlers; mousedown and mouseup cannot
// only be attached to the element being linked, as the
// mouse may leave this element during the drag.
var body = $document.find('body');
function clickBody(event) {
var x = event.clientX,
y = event.clientY,
rect = element[0].getBoundingClientRect(),
xMin = rect.left,
xMax = xMin + rect.width,
yMin = rect.top,
yMax = yMin + rect.height;
if (x < xMin || x > xMax || y < yMin || y > yMax) {
scope.$eval(attrs.mctClickElsewhere);
}
}
body.on("mousedown", clickBody);
scope.$on("$destroy", function () {
body.off("mousedown", clickBody);
});
}
return {
// mct-drag only makes sense as an attribute
restrict: "A",
// Link function, to install event handlers
link: link
};
}
return MCTClickElsewhere;
}
);

View File

@ -0,0 +1,70 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*global define*/
define(
function () {
'use strict';
var TEMPLATE = "<div></div>";
function MCTPopup($window, $document, $compile) {
function link(scope, element, attrs, ctrl, transclude) {
var body = $document.find('body'),
popup = $compile(TEMPLATE)(scope),
winDim = [$window.innerWidth, $window.innerHeight],
rect = element.parent()[0].getBoundingClientRect(),
position = [ rect.left, rect.top ],
isLeft = position[0] <= (winDim[0] / 2),
isTop = position[1] <= (winDim[1] / 2);
popup.css('position', 'absolute');
popup.css(
isLeft ? 'left' : 'right',
(isLeft ? position[0] : (winDim[0] - position[0])) + 'px'
);
popup.css(
isTop ? 'top' : 'bottom',
(isTop ? position[1] : (winDim[1] - position[1])) + 'px'
);
body.append(popup);
transclude(function (clone) {
popup.append(clone);
});
scope.$on('$destroy', function () {
popup.remove();
});
}
return {
restrict: "E",
transclude: true,
link: link,
scope: {}
};
}
return MCTPopup;
}
);

View File

@ -0,0 +1,63 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../../src/controllers/DateTimePickerController"],
function (DateTimePickerController) {
"use strict";
describe("The DateTimePickerController", function () {
var mockScope,
mockNow,
controller;
function fireWatch(expr, value) {
mockScope.$watch.calls.forEach(function (call) {
if (call.args[0] === expr) {
call.args[1](value);
}
});
}
beforeEach(function () {
mockScope = jasmine.createSpyObj(
"$scope",
[ "$apply", "$watch", "$watchCollection" ]
);
mockScope.ngModel = {};
mockScope.field = "testField";
mockNow = jasmine.createSpy('now');
controller = new DateTimePickerController(mockScope, mockNow);
});
it("watches the model that was passed in", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"ngModel[field]",
jasmine.any(Function)
);
});
});
}
);

View File

@ -0,0 +1,67 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../../src/controllers/TimeRangeController"],
function (TimeRangeController) {
"use strict";
describe("The TimeRangeController", function () {
var mockScope,
mockNow,
controller;
function fireWatch(expr, value) {
mockScope.$watch.calls.forEach(function (call) {
if (call.args[0] === expr) {
call.args[1](value);
}
});
}
function fireWatchCollection(expr, value) {
mockScope.$watchCollection.calls.forEach(function (call) {
if (call.args[0] === expr) {
call.args[1](value);
}
});
}
beforeEach(function () {
mockScope = jasmine.createSpyObj(
"$scope",
[ "$apply", "$watch", "$watchCollection" ]
);
mockNow = jasmine.createSpy('now');
controller = new TimeRangeController(mockScope, mockNow);
});
it("watches the model that was passed in", function () {
expect(mockScope.$watchCollection)
.toHaveBeenCalledWith("ngModel", jasmine.any(Function));
});
});
}
);

View File

@ -0,0 +1,84 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*global define,describe,it,expect,beforeEach,jasmine*/
define(
["../../src/directives/MCTClickElsewhere"],
function (MCTClickElsewhere) {
"use strict";
var JQLITE_METHODS = [ "on", "off", "find", "parent" ];
describe("The mct-click-elsewhere directive", function () {
var mockDocument,
mockScope,
mockElement,
testAttrs,
mockBody,
mockParentEl,
testRect,
mctClickElsewhere;
function testEvent(x, y) {
return {
pageX: x,
pageY: y,
preventDefault: jasmine.createSpy("preventDefault")
};
}
beforeEach(function () {
mockDocument =
jasmine.createSpyObj("$document", JQLITE_METHODS);
mockScope =
jasmine.createSpyObj("$scope", [ "$eval", "$apply", "$on" ]);
mockElement =
jasmine.createSpyObj("element", JQLITE_METHODS);
mockBody =
jasmine.createSpyObj("body", JQLITE_METHODS);
mockParentEl =
jasmine.createSpyObj("parent", ["getBoundingClientRect"]);
testAttrs = {
mctClickElsewhere: "some Angular expression"
};
testRect = {
left: 20,
top: 42,
width: 60,
height: 75
};
mockDocument.find.andReturn(mockBody);
mctClickElsewhere = new MCTClickElsewhere(mockDocument);
mctClickElsewhere.link(mockScope, mockElement, testAttrs);
});
it("is valid as an attribute", function () {
expect(mctClickElsewhere.restrict).toEqual("A");
});
});
}
);

View File

@ -0,0 +1,105 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*global define,describe,it,expect,beforeEach,jasmine*/
define(
["../../src/directives/MCTPopup"],
function (MCTPopup) {
"use strict";
var JQLITE_METHODS = [ "on", "off", "find", "parent", "css", "append" ];
describe("The mct-popup directive", function () {
var testWindow,
mockDocument,
mockCompile,
mockScope,
mockElement,
testAttrs,
mockBody,
mockTransclude,
mockParentEl,
testRect,
mctPopup;
function testEvent(x, y) {
return {
pageX: x,
pageY: y,
preventDefault: jasmine.createSpy("preventDefault")
};
}
beforeEach(function () {
testWindow =
{ innerWidth: 600, innerHeight: 300 };
mockDocument =
jasmine.createSpyObj("$document", JQLITE_METHODS);
mockCompile =
jasmine.createSpy("$compile");
mockScope =
jasmine.createSpyObj("$scope", [ "$eval", "$apply", "$on" ]);
mockElement =
jasmine.createSpyObj("element", JQLITE_METHODS);
mockBody =
jasmine.createSpyObj("body", JQLITE_METHODS);
mockTransclude =
jasmine.createSpy("transclude");
mockParentEl =
jasmine.createSpyObj("parent", ["getBoundingClientRect"]);
testAttrs = {
mctClickElsewhere: "some Angular expression"
};
testRect = {
left: 20,
top: 42,
width: 60,
height: 75
};
mockDocument.find.andReturn(mockBody);
mockCompile.andReturn(jasmine.createSpy());
mockCompile().andCallFake(function () {
return jasmine.createSpyObj("newElement", JQLITE_METHODS);
});
mockElement.parent.andReturn([mockParentEl]);
mockParentEl.getBoundingClientRect.andReturn(testRect);
mctPopup = new MCTPopup(testWindow, mockDocument, mockCompile);
mctPopup.link(
mockScope,
mockElement,
testAttrs,
null,
mockTransclude
);
});
it("is valid as an element", function () {
expect(mctPopup.restrict).toEqual("E");
});
});
}
);

View File

@ -3,14 +3,18 @@
"controllers/BottomBarController",
"controllers/ClickAwayController",
"controllers/ContextMenuController",
"controllers/DateTimePickerController",
"controllers/GetterSetterController",
"controllers/SelectorController",
"controllers/SplitPaneController",
"controllers/TimeRangeController",
"controllers/ToggleController",
"controllers/TreeNodeController",
"controllers/ViewSwitcherController",
"directives/MCTClickElsewhere",
"directives/MCTContainer",
"directives/MCTDrag",
"directives/MCTPopup",
"directives/MCTResize",
"directives/MCTScroll",
"services/UrlService",

View File

@ -36,11 +36,16 @@ define(
*
* Returns a function that, when invoked, will invoke `fn` after
* `delay` milliseconds, only if no other invocations are pending.
* The optional argument `apply` determines whether.
* The optional argument `apply` determines whether or not a
* digest cycle should be triggered.
*
* The returned function will itself return a `Promise` which will
* resolve to the returned value of `fn` whenever that is invoked.
*
* In cases where arguments are provided, only the most recent
* set of arguments will be passed on to the throttled function
* at the time it is executed.
*
* @returns {Function}
* @memberof platform/core
*/
@ -56,12 +61,14 @@ define(
* @memberof platform/core.Throttle#
*/
return function (fn, delay, apply) {
var activeTimeout;
var promise, // Promise for the result of throttled function
args = [];
// Clear active timeout, so that next invocation starts
// a new one.
function clearActiveTimeout() {
activeTimeout = undefined;
function invoke() {
// Clear the active timeout so a new one starts next time.
promise = undefined;
// Invoke the function with the latest supplied arguments.
return fn.apply(null, args);
}
// Defaults
@ -69,14 +76,13 @@ define(
apply = apply || false;
return function () {
// Store arguments from this invocation
args = Array.prototype.slice.apply(arguments, [0]);
// Start a timeout if needed
if (!activeTimeout) {
activeTimeout = $timeout(fn, delay, apply);
activeTimeout.then(clearActiveTimeout);
}
promise = promise || $timeout(invoke, delay, apply);
// Return whichever timeout is active (to get
// a promise for the results of fn)
return activeTimeout;
return promise;
};
};
}

View File

@ -45,7 +45,9 @@ define(
// Verify precondition: Not called at throttle-time
expect(mockTimeout).not.toHaveBeenCalled();
expect(throttled()).toEqual(mockPromise);
expect(mockTimeout).toHaveBeenCalledWith(mockFn, 0, false);
expect(mockFn).not.toHaveBeenCalled();
expect(mockTimeout)
.toHaveBeenCalledWith(jasmine.any(Function), 0, false);
});
it("schedules only one timeout at a time", function () {
@ -59,10 +61,11 @@ define(
it("schedules additional invocations after resolution", function () {
var throttled = throttle(mockFn);
throttled();
mockPromise.then.mostRecentCall.args[0](); // Resolve timeout
mockTimeout.mostRecentCall.args[0](); // Resolve timeout
throttled();
mockPromise.then.mostRecentCall.args[0]();
mockTimeout.mostRecentCall.args[0]();
throttled();
mockTimeout.mostRecentCall.args[0]();
expect(mockTimeout.calls.length).toEqual(3);
});
});

View File

@ -0,0 +1,9 @@
Provides the time conductor, a control which appears at the
bottom of the screen allowing telemetry start and end times
to be modified.
Note that the term "time controller" is generally preferred
outside of the code base (e.g. in UI documents, issues, etc.);
the term "time conductor" is being used in code to avoid
confusion with "controllers" in the Model-View-Controller
sense.

View File

@ -0,0 +1,25 @@
{
"extensions": {
"representers": [
{
"implementation": "ConductorRepresenter.js",
"depends": [ "conductorService", "$compile", "views[]" ]
}
],
"components": [
{
"type": "decorator",
"provides": "telemetryService",
"implementation": "ConductorTelemetryDecorator.js",
"depends": [ "conductorService" ]
}
],
"services": [
{
"key": "conductorService",
"implementation": "ConductorService.js",
"depends": [ "now" ]
}
]
}
}

View File

@ -0,0 +1,167 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*global define*/
define(
[],
function () {
"use strict";
var CONDUCTOR_HEIGHT = "100px",
TEMPLATE = [
'<div style=',
'"position: absolute; bottom: 0; width: 100%; ',
'overflow: hidden; ',
'height: ' + CONDUCTOR_HEIGHT + '">',
"<mct-include key=\"'time-controller'\" ng-model='conductor'>",
"</mct-include>",
'</div>'
].join(''),
GLOBAL_SHOWING = false;
/**
* The ConductorRepresenter attaches the universal time conductor
* to views.
*
* @implements {Representer}
* @constructor
* @memberof platform/features/conductor
* @param {platform/features/conductor.ConductorService} conductorService
* service which provides the active time conductor
* @param $compile Angular's $compile
* @param {ViewDefinition[]} views all defined views
* @param {Scope} the scope of the representation
* @param element the jqLite-wrapped representation element
*/
function ConductorRepresenter(conductorService, $compile, views, scope, element) {
this.scope = scope;
this.conductorService = conductorService;
this.element = element;
this.views = views;
this.$compile = $compile;
}
// Combine start/end times into a single object
function bounds(start, end) {
return { start: start, end: end };
}
// Update the time conductor from the scope
function wireScope(conductor, conductorScope, repScope) {
function updateConductorOuter() {
conductor.queryStart(conductorScope.conductor.outer.start);
conductor.queryEnd(conductorScope.conductor.outer.end);
repScope.$broadcast(
'telemetry:query:bounds',
bounds(conductor.queryStart(), conductor.queryEnd())
);
}
function updateConductorInner() {
conductor.displayStart(conductorScope.conductor.inner.start);
conductor.displayEnd(conductorScope.conductor.inner.end);
repScope.$broadcast(
'telemetry:display:bounds',
bounds(conductor.displayStart(), conductor.displayEnd())
);
}
conductorScope.conductor = {
outer: bounds(conductor.queryStart(), conductor.queryEnd()),
inner: bounds(conductor.displayStart(), conductor.displayEnd())
};
conductorScope
.$watch('conductor.outer.start', updateConductorOuter);
conductorScope
.$watch('conductor.outer.end', updateConductorOuter);
conductorScope
.$watch('conductor.inner.start', updateConductorInner);
conductorScope
.$watch('conductor.inner.end', updateConductorInner);
repScope.$on('telemetry:view', updateConductorInner);
}
ConductorRepresenter.prototype.conductorScope = function (s) {
return (this.cScope = arguments.length > 0 ?
s : this.cScope);
};
// Handle a specific representation of a specific domain object
ConductorRepresenter.prototype.represent = function represent(representation, representedObject) {
this.destroy();
if (this.views.indexOf(representation) !== -1 && !GLOBAL_SHOWING) {
// Track original states
this.originalHeight = this.element.css('height');
this.hadAbs = this.element.hasClass('abs');
// Create a new scope for the conductor
this.conductorScope(this.scope.$new());
wireScope(
this.conductorService.getConductor(),
this.conductorScope(),
this.scope
);
this.conductorElement =
this.$compile(TEMPLATE)(this.conductorScope());
this.element.after(this.conductorElement[0]);
this.element.addClass('abs');
this.element.css('bottom', CONDUCTOR_HEIGHT);
GLOBAL_SHOWING = true;
}
};
// Respond to the destruction of the current representation.
ConductorRepresenter.prototype.destroy = function destroy() {
// We may not have decided to show in the first place,
// so circumvent any unnecessary cleanup
if (!this.conductorElement) {
return;
}
// Restore the original size of the mct-representation
if (!this.hadAbs) {
this.element.removeClass('abs');
}
this.element.css('height', this.originalHeight);
// ...and remove the conductor
if (this.conductorElement) {
this.conductorElement.remove();
this.conductorElement = undefined;
}
// Finally, destroy its scope
if (this.conductorScope()) {
this.conductorScope().$destroy();
this.conductorScope(undefined);
}
GLOBAL_SHOWING = false;
};
return ConductorRepresenter;
}
);

View File

@ -0,0 +1,61 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*global define*/
define(
['./TimeConductor'],
function (TimeConductor) {
'use strict';
var ONE_DAY_IN_MS = 1000 * 60 * 60 * 24,
SIX_HOURS_IN_MS = ONE_DAY_IN_MS / 4;
/**
* Provides a single global instance of the time conductor, which
* controls both query ranges and displayed ranges for telemetry
* data.
*
* @constructor
* @memberof platform/features/conductor
* @param {Function} now a function which returns the current time
* as a UNIX timestamp, in milliseconds
*/
function ConductorService(now) {
var initialEnd =
Math.ceil(now() / SIX_HOURS_IN_MS) * SIX_HOURS_IN_MS;
this.conductor =
new TimeConductor(initialEnd - ONE_DAY_IN_MS, initialEnd);
}
/**
* Get the global instance of the time conductor.
* @returns {platform/features/conductor.TimeConductor} the
* time conductor
*/
ConductorService.prototype.getConductor = function () {
return this.conductor;
};
return ConductorService;
}
);

View File

@ -0,0 +1,108 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*global define*/
define(
['./ConductorTelemetrySeries'],
function (ConductorTelemetrySeries) {
'use strict';
/**
* Decorates the `telemetryService` such that requests are
* mediated by the time conductor.
*
* @constructor
* @memberof platform/features/conductor
* @implements {TelemetryService}
* @param {platform/features/conductor.ConductorService} conductorServe
* the service which exposes the global time conductor
* @param {TelemetryService} telemetryService the decorated service
*/
function ConductorTelemetryDecorator(conductorService, telemetryService) {
this.conductorService = conductorService;
this.telemetryService = telemetryService;
}
// Strip out any realtime data series that is outside of the conductor's
// bounds.
ConductorTelemetryDecorator.prototype.pruneNonDisplayable = function (packaged) {
var conductor = this.conductorService.getConductor(),
repackaged = {};
function filterSource(packagedBySource) {
var repackagedBySource = {};
Object.keys(packagedBySource).forEach(function (k) {
repackagedBySource[k] = new ConductorTelemetrySeries(
packagedBySource[k],
conductor
);
});
return repackagedBySource;
}
Object.keys(packaged).forEach(function (source) {
repackaged[source] = filterSource(packaged[source]);
});
return repackaged;
};
ConductorTelemetryDecorator.prototype.amendRequests = function (requests) {
var conductor = this.conductorService.getConductor(),
start = conductor.displayStart(),
end = conductor.displayEnd();
function amendRequest(request) {
request = request || {};
request.start = start;
request.end = end;
return request;
}
return (requests || []).map(amendRequest);
};
ConductorTelemetryDecorator.prototype.requestTelemetry = function (requests) {
var self = this;
return this.telemetryService
.requestTelemetry(this.amendRequests(requests))
.then(function (packaged) {
return self.pruneNonDisplayable(packaged);
});
};
ConductorTelemetryDecorator.prototype.subscribe = function (callback, requests) {
var self = this;
function internalCallback(packagedSeries) {
return callback(self.pruneNonDisplayable(packagedSeries));
}
return this.telemetryService
.subscribe(internalCallback, this.amendRequests(requests));
};
return ConductorTelemetryDecorator;
}
);

View File

@ -0,0 +1,71 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*global define*/
define(
function () {
'use strict';
/**
* Bound a series of telemetry such that it only includes
* points from within the time conductor's displayable window.
*
* @param {TelemetrySeries} series the telemetry series
* @param {platform/features/conductor.TimeConductor} the
* time conductor instance which bounds this series
* @constructor
* @implements {TelemetrySeries}
*/
function ConductorTelemetrySeries(series, conductor) {
var max = series.getPointCount() - 1;
function binSearch(min, max, value) {
var mid = Math.floor((min + max) / 2);
return min > max ? min :
series.getDomainValue(mid) < value ?
binSearch(mid + 1, max, value) :
binSearch(min, mid - 1, value);
}
this.startIndex = binSearch(0, max, conductor.displayStart());
this.endIndex = binSearch(0, max, conductor.displayEnd());
this.series = series;
}
ConductorTelemetrySeries.prototype.getPointCount = function () {
return Math.max(0, this.endIndex - this.startIndex);
};
ConductorTelemetrySeries.prototype.getDomainValue = function (i, d) {
return this.series.getDomainValue(i + this.startIndex, d);
};
ConductorTelemetrySeries.prototype.getRangeValue = function (i, r) {
return this.series.getRangeValue(i + this.startIndex, r);
};
return ConductorTelemetrySeries;
}
);

View File

@ -0,0 +1,99 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*global define*/
/**
* The time conductor bundle adds a global control to the bottom of the
* outermost viewing area. This controls both the range for time-based
* queries and for time-based displays.
*
* @namespace platform/features/conductor
*/
define(
function () {
'use strict';
/**
* Tracks the current state of the time conductor.
*
* @memberof platform/features/conductor
* @constructor
* @param {number} start the initial start time
* @param {number} end the initial end time
*/
function TimeConductor(start, end) {
this.inner = { start: start, end: end };
this.outer = { start: start, end: end };
}
/**
* Get or set (if called with an argument) the start time for queries.
* @param {number} [value] the start time to set
* @returns {number} the start time
*/
TimeConductor.prototype.queryStart = function (value) {
if (arguments.length > 0) {
this.outer.start = value;
}
return this.outer.start;
};
/**
* Get or set (if called with an argument) the end time for queries.
* @param {number} [value] the end time to set
* @returns {number} the end time
*/
TimeConductor.prototype.queryEnd = function (value) {
if (arguments.length > 0) {
this.outer.end = value;
}
return this.outer.end;
};
/**
* Get or set (if called with an argument) the start time for displays.
* @param {number} [value] the start time to set
* @returns {number} the start time
*/
TimeConductor.prototype.displayStart = function (value) {
if (arguments.length > 0) {
this.inner.start = value;
}
return this.inner.start;
};
/**
* Get or set (if called with an argument) the end time for displays.
* @param {number} [value] the end time to set
* @returns {number} the end time
*/
TimeConductor.prototype.displayEnd = function (value) {
if (arguments.length > 0) {
this.inner.end = value;
}
return this.inner.end;
};
return TimeConductor;
}
);

View File

@ -0,0 +1,167 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*global define,describe,it,expect,beforeEach,waitsFor,afterEach,jasmine*/
/**
* EventSpec. Created by vwoeltje on 11/6/14. Modified by shale on 06/23/2015.
*/
define(
["../src/ConductorRepresenter"],
function (ConductorRepresenter) {
"use strict";
var SCOPE_METHODS = [
'$on',
'$watch',
'$broadcast',
'$emit',
'$new',
'$destroy'
],
ELEMENT_METHODS = [
'hasClass',
'addClass',
'removeClass',
'css',
'after',
'remove'
];
describe("ConductorRepresenter", function () {
var mockConductorService,
mockCompile,
testViews,
mockScope,
mockElement,
mockConductor,
mockCompiledTemplate,
mockNewScope,
mockNewElement,
representer;
function fireWatch(scope, watch, value) {
scope.$watch.calls.forEach(function (call) {
if (call.args[0] === watch) {
call.args[1](value);
}
});
}
beforeEach(function () {
mockConductorService = jasmine.createSpyObj(
'conductorService',
['getConductor']
);
mockCompile = jasmine.createSpy('$compile');
testViews = [ { someKey: "some value" } ];
mockScope = jasmine.createSpyObj('scope', SCOPE_METHODS);
mockElement = jasmine.createSpyObj('element', ELEMENT_METHODS);
mockConductor = jasmine.createSpyObj(
'conductor',
[ 'queryStart', 'queryEnd', 'displayStart', 'displayEnd' ]
);
mockCompiledTemplate = jasmine.createSpy('template');
mockNewScope = jasmine.createSpyObj('newScope', SCOPE_METHODS);
mockNewElement = jasmine.createSpyObj('newElement', ELEMENT_METHODS);
mockNewElement[0] = mockNewElement;
mockConductorService.getConductor.andReturn(mockConductor);
mockCompile.andReturn(mockCompiledTemplate);
mockCompiledTemplate.andReturn(mockNewElement);
mockScope.$new.andReturn(mockNewScope);
representer = new ConductorRepresenter(
mockConductorService,
mockCompile,
testViews,
mockScope,
mockElement
);
});
afterEach(function () {
representer.destroy();
});
it("adds a conductor to views", function () {
representer.represent(testViews[0], {});
expect(mockElement.after).toHaveBeenCalledWith(mockNewElement);
});
it("adds nothing to non-view representations", function () {
representer.represent({ someKey: "something else" }, {});
expect(mockElement.after).not.toHaveBeenCalled();
});
it("removes the conductor when destroyed", function () {
representer.represent(testViews[0], {});
expect(mockNewElement.remove).not.toHaveBeenCalled();
representer.destroy();
expect(mockNewElement.remove).toHaveBeenCalled();
});
it("destroys any new scope created", function () {
representer.represent(testViews[0], {});
representer.destroy();
expect(mockNewScope.$destroy.calls.length)
.toEqual(mockScope.$new.calls.length);
});
it("exposes conductor state in scope", function () {
mockConductor.queryStart.andReturn(42);
mockConductor.queryEnd.andReturn(12321);
mockConductor.displayStart.andReturn(1977);
mockConductor.displayEnd.andReturn(1984);
representer.represent(testViews[0], {});
expect(mockNewScope.conductor).toEqual({
inner: { start: 1977, end: 1984 },
outer: { start: 42, end: 12321 }
});
});
it("updates conductor state from scope", function () {
var testState = {
inner: { start: 42, end: 1984 },
outer: { start: -1977, end: 12321 }
};
representer.represent(testViews[0], {});
mockNewScope.conductor = testState;
fireWatch(mockNewScope, 'conductor.inner.start', testState.inner.start);
expect(mockConductor.displayStart).toHaveBeenCalledWith(42);
fireWatch(mockNewScope, 'conductor.inner.end', testState.inner.end);
expect(mockConductor.displayEnd).toHaveBeenCalledWith(1984);
fireWatch(mockNewScope, 'conductor.outer.start', testState.outer.start);
expect(mockConductor.queryStart).toHaveBeenCalledWith(-1977);
fireWatch(mockNewScope, 'conductor.outer.end', testState.outer.end);
expect(mockConductor.queryEnd).toHaveBeenCalledWith(12321);
});
});
}
);

View File

@ -0,0 +1,58 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/
/**
* EventSpec. Created by vwoeltje on 11/6/14. Modified by shale on 06/23/2015.
*/
define(
["../src/ConductorService"],
function (ConductorService) {
"use strict";
var TEST_NOW = 1020304050;
describe("ConductorService", function () {
var mockNow,
conductorService;
beforeEach(function () {
mockNow = jasmine.createSpy('now');
mockNow.andReturn(TEST_NOW);
conductorService = new ConductorService(mockNow);
});
it("initializes a time conductor around the current time", function () {
var conductor = conductorService.getConductor();
expect(conductor.queryStart() <= TEST_NOW).toBeTruthy();
expect(conductor.queryEnd() >= TEST_NOW).toBeTruthy();
expect(conductor.queryEnd() > conductor.queryStart())
.toBeTruthy();
});
it("provides a single shared time conductor instance", function () {
expect(conductorService.getConductor())
.toBe(conductorService.getConductor());
});
});
}
);

View File

@ -0,0 +1,137 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../src/ConductorTelemetryDecorator"],
function (ConductorTelemetryDecorator) {
"use strict";
describe("ConductorTelemetryDecorator", function () {
var mockTelemetryService,
mockConductorService,
mockConductor,
mockPromise,
mockSeries,
decorator;
function seriesIsInWindow(series) {
var i, v, inWindow = true;
for (i = 0; i < series.getPointCount(); i += 1) {
v = series.getDomainValue(i);
inWindow = inWindow && (v >= mockConductor.displayStart());
inWindow = inWindow && (v <= mockConductor.displayEnd());
}
return inWindow;
}
beforeEach(function () {
mockTelemetryService = jasmine.createSpyObj(
'telemetryService',
[ 'requestTelemetry', 'subscribe' ]
);
mockConductorService = jasmine.createSpyObj(
'conductorService',
['getConductor']
);
mockConductor = jasmine.createSpyObj(
'conductor',
[ 'queryStart', 'queryEnd', 'displayStart', 'displayEnd' ]
);
mockPromise = jasmine.createSpyObj(
'promise',
['then']
);
mockSeries = jasmine.createSpyObj(
'series',
[ 'getPointCount', 'getDomainValue', 'getRangeValue' ]
);
mockTelemetryService.requestTelemetry.andReturn(mockPromise);
mockConductorService.getConductor.andReturn(mockConductor);
// Prepare test series; make sure it has a broad range of
// domain values, with at least some in the query-able range
mockSeries.getPointCount.andReturn(1000);
mockSeries.getDomainValue.andCallFake(function (i) {
var j = i - 500;
return j * j * j;
});
mockConductor.queryStart.andReturn(-12321);
mockConductor.queryEnd.andReturn(-12321);
mockConductor.displayStart.andReturn(42);
mockConductor.displayEnd.andReturn(1977);
decorator = new ConductorTelemetryDecorator(
mockConductorService,
mockTelemetryService
);
});
it("adds display start/end times to historical requests", function () {
decorator.requestTelemetry([{ someKey: "some value" }]);
expect(mockTelemetryService.requestTelemetry)
.toHaveBeenCalledWith([{
someKey: "some value",
start: mockConductor.displayStart(),
end: mockConductor.displayEnd()
}]);
});
it("adds display start/end times to subscription requests", function () {
var mockCallback = jasmine.createSpy('callback');
decorator.subscribe(mockCallback, [{ someKey: "some value" }]);
expect(mockTelemetryService.subscribe)
.toHaveBeenCalledWith(jasmine.any(Function), [{
someKey: "some value",
start: mockConductor.displayStart(),
end: mockConductor.displayEnd()
}]);
});
it("prunes historical values to the displayable range", function () {
var packagedTelemetry;
decorator.requestTelemetry([{ source: "abc", key: "xyz" }]);
packagedTelemetry = mockPromise.then.mostRecentCall.args[0]({
"abc": { "xyz": mockSeries }
});
expect(seriesIsInWindow(packagedTelemetry.abc.xyz))
.toBeTruthy();
});
it("prunes subscribed values to the displayable range", function () {
var mockCallback = jasmine.createSpy('callback'),
packagedTelemetry;
decorator.subscribe(mockCallback, [{ source: "abc", key: "xyz" }]);
mockTelemetryService.subscribe.mostRecentCall.args[0]({
"abc": { "xyz": mockSeries }
});
packagedTelemetry = mockCallback.mostRecentCall.args[0];
expect(seriesIsInWindow(packagedTelemetry.abc.xyz))
.toBeTruthy();
});
});
}
);

View File

@ -0,0 +1,86 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../src/ConductorTelemetrySeries"],
function (ConductorTelemetrySeries) {
"use strict";
describe("ConductorTelemetrySeries", function () {
var mockSeries,
mockConductor,
testArray,
series;
beforeEach(function () {
testArray = [ -10, 0, 42, 1977, 12321 ];
mockSeries = jasmine.createSpyObj(
'series',
[ 'getPointCount', 'getDomainValue', 'getRangeValue' ]
);
mockConductor = jasmine.createSpyObj(
'conductor',
[ 'queryStart', 'queryEnd', 'displayStart', 'displayEnd' ]
);
mockSeries.getPointCount.andCallFake(function () {
return testArray.length;
});
mockSeries.getDomainValue.andCallFake(function (i) {
return testArray[i];
});
mockSeries.getRangeValue.andCallFake(function (i) {
return testArray[i] * 2;
});
mockConductor.displayStart.andReturn(0);
mockConductor.displayEnd.andReturn(2000);
series = new ConductorTelemetrySeries(
mockSeries,
mockConductor
);
});
it("reduces the apparent size of a series", function () {
expect(series.getPointCount()).toEqual(3);
});
it("maps domain value indexes to the displayable range", function () {
[0, 1, 2].forEach(function (i) {
expect(series.getDomainValue(i))
.toEqual(mockSeries.getDomainValue(i + 1));
});
});
it("maps range value indexes to the displayable range", function () {
[0, 1, 2].forEach(function (i) {
expect(series.getRangeValue(i))
.toEqual(mockSeries.getRangeValue(i + 1));
});
});
});
}
);

View File

@ -0,0 +1,63 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/
/**
* EventSpec. Created by vwoeltje on 11/6/14. Modified by shale on 06/23/2015.
*/
define(
["../src/TimeConductor"],
function (TimeConductor) {
"use strict";
describe("TimeConductor", function () {
var testStart,
testEnd,
conductor;
beforeEach(function () {
testStart = 42;
testEnd = 12321;
conductor = new TimeConductor(testStart, testEnd);
});
it("provides accessors for query/display start/end times", function () {
expect(conductor.queryStart()).toEqual(testStart);
expect(conductor.queryEnd()).toEqual(testEnd);
expect(conductor.displayStart()).toEqual(testStart);
expect(conductor.displayEnd()).toEqual(testEnd);
});
it("provides setters for query/display start/end times", function () {
expect(conductor.queryStart(1)).toEqual(1);
expect(conductor.queryEnd(2)).toEqual(2);
expect(conductor.displayStart(3)).toEqual(3);
expect(conductor.displayEnd(4)).toEqual(4);
expect(conductor.queryStart()).toEqual(1);
expect(conductor.queryEnd()).toEqual(2);
expect(conductor.displayStart()).toEqual(3);
expect(conductor.displayEnd()).toEqual(4);
});
});
}
);

View File

@ -0,0 +1,7 @@
[
"ConductorRepresenter",
"ConductorService",
"ConductorTelemetryDecorator",
"ConductorTelemetrySeries",
"TimeConductor"
]

View File

@ -167,8 +167,9 @@
"$scope",
"$q",
"dialogService",
"telemetrySubscriber",
"telemetryFormatter"
"telemetryHandler",
"telemetryFormatter",
"throttle"
]
}
],

View File

@ -38,12 +38,13 @@ define(
* @constructor
* @param {Scope} $scope the controller's Angular scope
*/
function FixedController($scope, $q, dialogService, telemetrySubscriber, telemetryFormatter) {
function FixedController($scope, $q, dialogService, telemetryHandler, telemetryFormatter, throttle) {
var self = this,
subscription,
handle,
names = {}, // Cache names by ID
values = {}, // Cache values by ID
elementProxiesById = {};
elementProxiesById = {},
maxDomainValue = Number.POSITIVE_INFINITY;
// Convert from element x/y/width/height to an
// appropriate ng-style argument, to position elements.
@ -81,25 +82,50 @@ define(
return element.handles().map(generateDragHandle);
}
// Update the displayed value for this object
function updateValue(telemetryObject) {
var id = telemetryObject && telemetryObject.getId(),
// Update the value displayed in elements of this telemetry object
function setDisplayedValue(telemetryObject, value, alarm) {
var id = telemetryObject.getId();
(elementProxiesById[id] || []).forEach(function (element) {
names[id] = telemetryObject.getModel().name;
values[id] = telemetryFormatter.formatRangeValue(value);
element.name = names[id];
element.value = values[id];
element.cssClass = alarm && alarm.cssClass;
});
}
// Update the displayed value for this object, from a specific
// telemetry series
function updateValueFromSeries(telemetryObject, telemetrySeries) {
var index = telemetrySeries.getPointCount() - 1,
limit = telemetryObject &&
telemetryObject.getCapability('limit'),
datum = telemetryObject &&
subscription.getDatum(telemetryObject),
alarm = limit && datum && limit.evaluate(datum);
datum = telemetryObject && handle.getDatum(
telemetryObject,
index
);
if (id) {
(elementProxiesById[id] || []).forEach(function (element) {
names[id] = telemetryObject.getModel().name;
values[id] = telemetryFormatter.formatRangeValue(
subscription.getRangeValue(telemetryObject)
);
element.name = names[id];
element.value = values[id];
element.cssClass = alarm && alarm.cssClass;
});
setDisplayedValue(
telemetryObject,
telemetrySeries.getRangeValue(index),
limit && datum && limit.evaluate(datum)
);
}
// Update the displayed value for this object
function updateValue(telemetryObject) {
var limit = telemetryObject &&
telemetryObject.getCapability('limit'),
datum = telemetryObject &&
handle.getDatum(telemetryObject);
if (telemetryObject &&
(handle.getDomainValue(telemetryObject) < maxDomainValue)) {
setDisplayedValue(
telemetryObject,
handle.getRangeValue(telemetryObject),
limit && datum && limit.evaluate(datum)
);
}
}
@ -115,8 +141,8 @@ define(
// Update telemetry values based on new data available
function updateValues() {
if (subscription) {
subscription.getTelemetryObjects().forEach(updateValue);
if (handle) {
handle.getTelemetryObjects().forEach(updateValue);
}
}
@ -178,22 +204,29 @@ define(
// Free up subscription to telemetry
function releaseSubscription() {
if (subscription) {
subscription.unsubscribe();
subscription = undefined;
if (handle) {
handle.unsubscribe();
handle = undefined;
}
}
// Subscribe to telemetry updates for this domain object
function subscribe(domainObject) {
// Release existing subscription (if any)
if (subscription) {
subscription.unsubscribe();
if (handle) {
handle.unsubscribe();
}
// Make a new subscription
subscription = domainObject &&
telemetrySubscriber.subscribe(domainObject, updateValues);
handle = domainObject && telemetryHandler.handle(
domainObject,
updateValues
);
// Request an initial historical telemetry value
handle.request(
{ size: 1 }, // Only need a single data point
updateValueFromSeries
);
}
// Handle changes in the object's composition
@ -204,6 +237,17 @@ define(
subscribe($scope.domainObject);
}
// Trigger a new query for telemetry data
function updateDisplayBounds(event, bounds) {
maxDomainValue = bounds.end;
if (handle) {
handle.request(
{ size: 1 }, // Only need a single data point
updateValueFromSeries
);
}
}
// Add an element to this view
function addElement(element) {
// Ensure that configuration field is populated
@ -278,6 +322,9 @@ define(
// Position panes where they are dropped
$scope.$on("mctDrop", handleDrop);
// Respond to external bounds changes
$scope.$on("telemetry:display:bounds", updateDisplayBounds);
}
/**

View File

@ -30,10 +30,10 @@ define(
var mockScope,
mockQ,
mockDialogService,
mockSubscriber,
mockHandler,
mockFormatter,
mockDomainObject,
mockSubscription,
mockHandle,
mockEvent,
testGrid,
testModel,
@ -78,9 +78,9 @@ define(
'$scope',
[ "$on", "$watch", "commit" ]
);
mockSubscriber = jasmine.createSpyObj(
'telemetrySubscriber',
[ 'subscribe' ]
mockHandler = jasmine.createSpyObj(
'telemetryHandler',
[ 'handle' ]
);
mockQ = jasmine.createSpyObj('$q', ['when']);
mockDialogService = jasmine.createSpyObj(
@ -95,9 +95,16 @@ define(
'domainObject',
[ 'getId', 'getModel', 'getCapability' ]
);
mockSubscription = jasmine.createSpyObj(
mockHandle = jasmine.createSpyObj(
'subscription',
[ 'unsubscribe', 'getTelemetryObjects', 'getRangeValue', 'getDatum' ]
[
'unsubscribe',
'getDomainValue',
'getTelemetryObjects',
'getRangeValue',
'getDatum',
'request'
]
);
mockEvent = jasmine.createSpyObj(
'event',
@ -116,13 +123,14 @@ define(
{ type: "fixed.telemetry", id: 'c', x: 1, y: 1 }
]};
mockSubscriber.subscribe.andReturn(mockSubscription);
mockSubscription.getTelemetryObjects.andReturn(
mockHandler.handle.andReturn(mockHandle);
mockHandle.getTelemetryObjects.andReturn(
testModel.composition.map(makeMockDomainObject)
);
mockSubscription.getRangeValue.andCallFake(function (o) {
mockHandle.getRangeValue.andCallFake(function (o) {
return testValues[o.getId()];
});
mockHandle.getDomainValue.andReturn(12321);
mockFormatter.formatRangeValue.andCallFake(function (v) {
return "Formatted " + v;
});
@ -137,7 +145,7 @@ define(
mockScope,
mockQ,
mockDialogService,
mockSubscriber,
mockHandler,
mockFormatter
);
});
@ -145,7 +153,7 @@ define(
it("subscribes when a domain object is available", function () {
mockScope.domainObject = mockDomainObject;
findWatch("domainObject")(mockDomainObject);
expect(mockSubscriber.subscribe).toHaveBeenCalledWith(
expect(mockHandler.handle).toHaveBeenCalledWith(
mockDomainObject,
jasmine.any(Function)
);
@ -156,13 +164,13 @@ define(
// First pass - should simply should subscribe
findWatch("domainObject")(mockDomainObject);
expect(mockSubscription.unsubscribe).not.toHaveBeenCalled();
expect(mockSubscriber.subscribe.calls.length).toEqual(1);
expect(mockHandle.unsubscribe).not.toHaveBeenCalled();
expect(mockHandler.handle.calls.length).toEqual(1);
// Object changes - should unsubscribe then resubscribe
findWatch("domainObject")(mockDomainObject);
expect(mockSubscription.unsubscribe).toHaveBeenCalled();
expect(mockSubscriber.subscribe.calls.length).toEqual(2);
expect(mockHandle.unsubscribe).toHaveBeenCalled();
expect(mockHandler.handle.calls.length).toEqual(2);
});
it("exposes visible elements based on configuration", function () {
@ -255,7 +263,7 @@ define(
findWatch("model.composition")(mockScope.model.composition);
// Invoke the subscription callback
mockSubscriber.subscribe.mostRecentCall.args[1]();
mockHandler.handle.mostRecentCall.args[1]();
// Get elements that controller is now exposing
elements = controller.getElements();
@ -333,11 +341,11 @@ define(
// Make an object available
findWatch('domainObject')(mockDomainObject);
// Also verify precondition
expect(mockSubscription.unsubscribe).not.toHaveBeenCalled();
expect(mockHandle.unsubscribe).not.toHaveBeenCalled();
// Destroy the scope
findOn('$destroy')();
// Should have unsubscribed
expect(mockSubscription.unsubscribe).toHaveBeenCalled();
expect(mockHandle.unsubscribe).toHaveBeenCalled();
});
it("exposes its grid size", function () {

View File

@ -65,6 +65,8 @@ define(
subPlotFactory = new SubPlotFactory(telemetryFormatter),
cachedObjects = [],
updater,
lastBounds,
throttledRequery,
handle;
// Populate the scope with axis information (specifically, options
@ -94,6 +96,17 @@ define(
}
}
// Change the displayable bounds
function setBasePanZoom(bounds) {
var start = bounds.start,
end = bounds.end;
if (updater) {
updater.setDomainBounds(start, end);
self.update();
}
lastBounds = bounds;
}
// Reinstantiate the plot updater (e.g. because we have a
// new subscription.) This will clear the plot.
function recreateUpdater() {
@ -107,10 +120,15 @@ define(
handle,
($scope.axes[1].active || {}).key
);
// Keep any externally-provided bounds
if (lastBounds) {
setBasePanZoom(lastBounds);
}
}
// Handle new telemetry data in this plot
function updateValues() {
self.pending = false;
if (handle) {
setupModes(handle.getTelemetryObjects());
}
@ -126,6 +144,7 @@ define(
// Display new historical data as it becomes available
function addHistoricalData(domainObject, series) {
self.pending = false;
updater.addHistorical(domainObject, series);
self.modeOptions.getModeHandler().plotTelemetry(updater);
self.update();
@ -165,6 +184,19 @@ define(
}
}
// Respond to a display bounds change (requery for data)
function changeDisplayBounds(event, bounds) {
self.pending = true;
releaseSubscription();
throttledRequery();
setBasePanZoom(bounds);
}
// Reestablish/reissue request for telemetry
throttledRequery = throttle(function () {
subscribe($scope.domainObject);
}, 250);
this.modeOptions = new PlotModeOptions([], subPlotFactory);
this.updateValues = updateValues;
@ -174,12 +206,19 @@ define(
.forEach(updateSubplot);
});
self.pending = true;
// Subscribe to telemetry when a domain object becomes available
$scope.$watch('domainObject', subscribe);
// Respond to external bounds changes
$scope.$on("telemetry:display:bounds", changeDisplayBounds);
// Unsubscribe when the plot is destroyed
$scope.$on("$destroy", releaseSubscription);
// Notify any external observers that a new telemetry view is here
$scope.$emit("telemetry:view");
}
/**
@ -275,7 +314,7 @@ define(
PlotController.prototype.isRequestPending = function () {
// Placeholder; this should reflect request state
// when requesting historical telemetry
return false;
return this.pending;
};
return PlotController;

View File

@ -143,8 +143,7 @@ define(
PlotPanZoomStackGroup.prototype.getDepth = function () {
// All stacks are kept in sync, so look up depth
// from the first one.
return this.stacks.length > 0 ?
this.stacks[0].getDepth() : 0;
return this.stacks.length > 0 ? this.stacks[0].getDepth() : 0;
};
/**

View File

@ -141,10 +141,10 @@ define(
PlotUpdater.prototype.initializeDomainOffset = function (values) {
this.domainOffset =
((this.domainOffset === undefined) && (values.length > 0)) ?
(values.reduce(function (a, b) {
return (a || 0) + (b || 0);
}, 0) / values.length) :
this.domainOffset;
(values.reduce(function (a, b) {
return (a || 0) + (b || 0);
}, 0) / values.length) :
this.domainOffset;
};
// Expand range slightly so points near edges are visible
@ -159,7 +159,10 @@ define(
// Update dimensions and origin based on extrema of plots
PlotUpdater.prototype.updateBounds = function () {
var bufferArray = this.bufferArray;
var bufferArray = this.bufferArray,
priorDomainOrigin = this.origin[0],
priorDomainDimensions = this.dimensions[0];
if (bufferArray.length > 0) {
this.domainExtrema = bufferArray.map(function (lineBuffer) {
return lineBuffer.getDomainExtrema();
@ -178,6 +181,18 @@ define(
// Enforce some minimum visible area
this.expandRange();
// Suppress domain changes when pinned
if (this.hasSpecificDomainBounds) {
this.origin[0] = priorDomainOrigin;
this.dimensions[0] = priorDomainDimensions;
if (this.following) {
this.origin[0] = Math.max(
this.domainExtrema[1] - this.dimensions[0],
this.origin[0]
);
}
}
// ...then enforce a fixed duration if needed
if (this.fixedDuration !== undefined) {
this.origin[0] = this.origin[0] + this.dimensions[0] -
@ -281,6 +296,21 @@ define(
return this.bufferArray;
};
/**
* Set the start and end boundaries (usually time) for the
* domain axis of this updater.
*/
PlotUpdater.prototype.setDomainBounds = function (start, end) {
this.fixedDuration = end - start;
this.origin[0] = start;
this.dimensions[0] = this.fixedDuration;
// Suppress follow behavior if we have windowed in on the past
this.hasSpecificDomainBounds = true;
this.following =
!this.domainExtrema || (end >= this.domainExtrema[1]);
};
/**
* Fill in historical data.
*/

View File

@ -45,11 +45,19 @@ define(
};
}
function fireEvent(name, args) {
mockScope.$on.calls.forEach(function (call) {
if (call.args[0] === name) {
call.args[1].apply(null, args || []);
}
});
}
beforeEach(function () {
mockScope = jasmine.createSpyObj(
"$scope",
[ "$watch", "$on" ]
[ "$watch", "$on", "$emit" ]
);
mockFormatter = jasmine.createSpyObj(
"formatter",
@ -87,6 +95,7 @@ define(
mockHandle.getMetadata.andReturn([{}]);
mockHandle.getDomainValue.andReturn(123);
mockHandle.getRangeValue.andReturn(42);
mockScope.domainObject = mockDomainObject;
controller = new PlotController(
mockScope,
@ -212,7 +221,12 @@ define(
});
it("indicates if a request is pending", function () {
// Placeholder; need to support requesting telemetry
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(controller.isRequestPending()).toBeTruthy();
mockHandle.request.mostRecentCall.args[1](
mockDomainObject,
mockSeries
);
expect(controller.isRequestPending()).toBeFalsy();
});
@ -233,10 +247,20 @@ define(
// Also verify precondition
expect(mockHandle.unsubscribe).not.toHaveBeenCalled();
// Destroy the scope
mockScope.$on.mostRecentCall.args[1]();
fireEvent("$destroy");
// Should have unsubscribed
expect(mockHandle.unsubscribe).toHaveBeenCalled();
});
it("requeries when displayable bounds change", function () {
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(mockHandle.request.calls.length).toEqual(1);
fireEvent("telemetry:display:bounds", [
{},
{ start: 10, end: 100 }
]);
expect(mockHandle.request.calls.length).toEqual(2);
});
});
}
);

View File

@ -55,7 +55,7 @@ define(
var range = this.rangeMetadata.key,
limit = domainObject.getCapability('limit'),
value = datum[range],
alarm = limit.evaluate(datum, range);
alarm = limit && limit.evaluate(datum, range);
return {
cssClass: alarm && alarm.cssClass,

View File

@ -136,6 +136,14 @@ define(
}
}
// Destroy (deallocate any resources associated with) any
// active representers.
function destroyRepresenters() {
activeRepresenters.forEach(function (activeRepresenter) {
activeRepresenter.destroy();
});
}
// General-purpose refresh mechanism; should set up the scope
// as appropriate for current representation key and
// domain object.
@ -152,10 +160,8 @@ define(
// via the "inclusion" field
$scope.inclusion = representation && getPath(representation);
// Any existing gestures are no longer valid; release them.
activeRepresenters.forEach(function (activeRepresenter) {
activeRepresenter.destroy();
});
// Any existing representers are no longer valid; release them.
destroyRepresenters();
// Log if a key was given, but no matching representation
// was found.
@ -209,6 +215,10 @@ define(
// model's "modified" field, by the mutation capability.
$scope.$watch("domainObject.getModel().modified", refreshCapabilities);
// Make sure any resources allocated by representers also get
// released.
$scope.$on("$destroy", destroyRepresenters);
// Do one initial refresh, so that we don't need another
// digest iteration just to populate the scope. Failure to
// do this can result in unstable digest cycles, which

View File

@ -106,7 +106,7 @@ define(
mockSce.trustAsResourceUrl.andCallFake(function (url) {
return url;
});
mockScope = jasmine.createSpyObj("scope", [ "$watch" ]);
mockScope = jasmine.createSpyObj("scope", [ "$watch", "$on" ]);
mockElement = jasmine.createSpyObj("element", JQLITE_FUNCTIONS);
mockDomainObject = jasmine.createSpyObj("domainObject", DOMAIN_OBJECT_METHODS);

View File

@ -31,6 +31,30 @@ define(
function () {
"use strict";
/**
* Describes a request for telemetry data. Note that responses
* may contain either a sub- or superset of the requested data.
* @typedef TelemetryRequest
* @property {string} source an identifier for the relevant
* source of telemetry data
* @property {string} key an identifier for the specific
* series of telemetry data provided by that source
* @property {number} [start] the earliest domain value of
* interest for that telemetry data; for time-based
* domains, this is in milliseconds since the start
* of 1970
* @property {number} [end] the latest domain value of interest
* for that telemetry data; for time-based domains,
* this is in milliseconds since 1970
* @property {string} [domain] the domain for the query; if
* omitted, this will be whatever the "normal"
* domain is for a given telemetry series (the
* first domain from its metadata)
* @property {number} [size] if set, indicates the maximum number
* of data points of interest for this request (more
* recent domain values will be preferred)
*/
/**
* Request telemetry data.
* @param {TelemetryRequest[]} requests and array of

View File

@ -79,8 +79,7 @@ define(
/**
* Change the request duration.
* @param {object|number} request the duration of historical
* data to look at; or, the request to issue
* @param {TelemetryRequest} request the request to issue
* @param {Function} [callback] a callback that will be
* invoked as new data becomes available, with the
* domain object for which new data is available.
@ -107,6 +106,26 @@ define(
.then(issueRequests);
};
/**
* Get the latest telemetry datum for this domain object. This
* will be from real-time telemetry, unless an index is specified,
* in which case it will be pulled from the historical telemetry
* series at the specified index.
*
* @param {DomainObject} domainObject the object of interest
* @param {number} [index] the index of the data of interest
* @returns {TelemetryDatum} the most recent datum
*/
self.getDatum = function (telemetryObject, index) {
return typeof index !== 'number' ?
subscription.getDatum(telemetryObject) :
subscription.makeDatum(
telemetryObject,
this.getSeries(telemetryObject),
index
);
};
return self;
}

View File

@ -123,25 +123,6 @@ define(
telemetryCapability.getMetadata();
}
// From a telemetry series, retrieve a single data point
// containing all fields for domains/ranges
function makeDatum(domainObject, series, index) {
var metadata = lookupMetadata(domainObject),
result = {};
(metadata.domains || []).forEach(function (domain) {
result[domain.key] =
series.getDomainValue(index, domain.key);
});
(metadata.ranges || []).forEach(function (range) {
result[range.key] =
series.getRangeValue(index, range.key);
});
return result;
}
// Update the latest telemetry data for a specific
// domain object. This will notify listeners.
function update(domainObject, series) {
@ -160,7 +141,7 @@ define(
pool.put(domainObject.getId(), {
domain: series.getDomainValue(count - 1),
range: series.getRangeValue(count - 1),
datum: makeDatum(domainObject, series, count - 1)
datum: self.makeDatum(domainObject, series, count - 1)
});
}
}
@ -188,6 +169,11 @@ define(
function cacheObjectReferences(objects) {
self.telemetryObjects = objects;
self.metadatas = objects.map(lookupMetadata);
self.metadataById = {};
objects.forEach(function (obj, i) {
self.metadataById[obj.getId()] = self.metadatas[i];
});
// Fire callback, as this will be the first time that
// telemetry objects are available, or these objects
// will have changed.
@ -241,6 +227,34 @@ define(
this.unlistenToMutation = addMutationListener();
}
/**
* From a telemetry series, retrieve a single data point
* containing all fields for domains/ranges
* @private
*/
TelemetrySubscription.prototype.makeDatum = function (domainObject, series, index) {
var id = domainObject && domainObject.getId(),
metadata = (id && this.metadataById[id]) || {},
result = {};
(metadata.domains || []).forEach(function (domain) {
result[domain.key] =
series.getDomainValue(index, domain.key);
});
(metadata.ranges || []).forEach(function (range) {
result[range.key] =
series.getRangeValue(index, range.key);
});
return result;
};
/**
* Terminate all underlying subscriptions.
* @private
*/
TelemetrySubscription.prototype.unsubscribeAll = function () {
var $q = this.$q;
return this.unsubscribePromise.then(function (unsubscribes) {