diff --git a/example/generator/src/SinewaveTelemetryProvider.js b/example/generator/src/SinewaveTelemetryProvider.js
index 014510f67c..c4062e659c 100644
--- a/example/generator/src/SinewaveTelemetryProvider.js
+++ b/example/generator/src/SinewaveTelemetryProvider.js
@@ -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;
}
-);
\ No newline at end of file
+);
diff --git a/example/generator/src/SinewaveTelemetry.js b/example/generator/src/SinewaveTelemetrySeries.js
similarity index 63%
rename from example/generator/src/SinewaveTelemetry.js
rename to example/generator/src/SinewaveTelemetrySeries.js
index 6c255bf56a..5b7914a867 100644
--- a/example/generator/src/SinewaveTelemetry.js
+++ b/example/generator/src/SinewaveTelemetrySeries.js
@@ -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;
}
-);
\ No newline at end of file
+);
diff --git a/platform/commonUI/general/bundle.json b/platform/commonUI/general/bundle.json
index b55ce03b32..d6614697a0 100644
--- a/platform/commonUI/general/bundle.json
+++ b/platform/commonUI/general/bundle.json
@@ -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": [
diff --git a/platform/commonUI/general/res/templates/controls/datetime-picker.html b/platform/commonUI/general/res/templates/controls/datetime-picker.html
new file mode 100644
index 0000000000..dd774588d6
--- /dev/null
+++ b/platform/commonUI/general/res/templates/controls/datetime-picker.html
@@ -0,0 +1,61 @@
+
+
+
+
+
+
<
+ {{month}} {{year}}
+
>
+
+
+
+
+ |
+ {{day}}
+ |
+
+
+ |
+ {{cell.day}}
+ {{cell.dayOfYear}}
+ |
+
+
+
+
+
+
diff --git a/platform/commonUI/general/res/templates/controls/time-controller.html b/platform/commonUI/general/res/templates/controls/time-controller.html
index fe1b6ff14a..b223513859 100644
--- a/platform/commonUI/general/res/templates/controls/time-controller.html
+++ b/platform/commonUI/general/res/templates/controls/time-controller.html
@@ -1,69 +1,96 @@
-
+
diff --git a/platform/commonUI/general/src/controllers/DateTimePickerController.js b/platform/commonUI/general/src/controllers/DateTimePickerController.js
new file mode 100644
index 0000000000..11214d9357
--- /dev/null
+++ b/platform/commonUI/general/src/controllers/DateTimePickerController.js
@@ -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;
+ }
+);
diff --git a/platform/commonUI/general/src/controllers/TimeRangeController.js b/platform/commonUI/general/src/controllers/TimeRangeController.js
new file mode 100644
index 0000000000..ac111ab1a9
--- /dev/null
+++ b/platform/commonUI/general/src/controllers/TimeRangeController.js
@@ -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;
+ }
+);
diff --git a/platform/commonUI/general/src/directives/MCTClickElsewhere.js b/platform/commonUI/general/src/directives/MCTClickElsewhere.js
new file mode 100644
index 0000000000..1bcdbbe6b5
--- /dev/null
+++ b/platform/commonUI/general/src/directives/MCTClickElsewhere.js
@@ -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;
+ }
+);
+
diff --git a/platform/commonUI/general/src/directives/MCTPopup.js b/platform/commonUI/general/src/directives/MCTPopup.js
new file mode 100644
index 0000000000..33860204c6
--- /dev/null
+++ b/platform/commonUI/general/src/directives/MCTPopup.js
@@ -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 = "";
+
+ 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;
+ }
+);
diff --git a/platform/commonUI/general/test/controllers/DateTimePickerControllerSpec.js b/platform/commonUI/general/test/controllers/DateTimePickerControllerSpec.js
new file mode 100644
index 0000000000..957df1b36d
--- /dev/null
+++ b/platform/commonUI/general/test/controllers/DateTimePickerControllerSpec.js
@@ -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)
+ );
+ });
+
+
+ });
+ }
+);
diff --git a/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js b/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js
new file mode 100644
index 0000000000..48411756b0
--- /dev/null
+++ b/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js
@@ -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));
+ });
+
+
+ });
+ }
+);
diff --git a/platform/commonUI/general/test/directives/MCTClickElsewhereSpec.js b/platform/commonUI/general/test/directives/MCTClickElsewhereSpec.js
new file mode 100644
index 0000000000..9fa17763fe
--- /dev/null
+++ b/platform/commonUI/general/test/directives/MCTClickElsewhereSpec.js
@@ -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");
+ });
+
+
+ });
+ }
+);
diff --git a/platform/commonUI/general/test/directives/MCTPopupSpec.js b/platform/commonUI/general/test/directives/MCTPopupSpec.js
new file mode 100644
index 0000000000..94fd3f0d0e
--- /dev/null
+++ b/platform/commonUI/general/test/directives/MCTPopupSpec.js
@@ -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");
+ });
+
+
+ });
+ }
+);
diff --git a/platform/commonUI/general/test/suite.json b/platform/commonUI/general/test/suite.json
index 45c554d2c2..1427d70f3a 100644
--- a/platform/commonUI/general/test/suite.json
+++ b/platform/commonUI/general/test/suite.json
@@ -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",
diff --git a/platform/core/src/services/Throttle.js b/platform/core/src/services/Throttle.js
index 3d68988d6b..60444ad6c4 100644
--- a/platform/core/src/services/Throttle.js
+++ b/platform/core/src/services/Throttle.js
@@ -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;
};
};
}
diff --git a/platform/core/test/services/ThrottleSpec.js b/platform/core/test/services/ThrottleSpec.js
index bcaf2af363..3b361f70bb 100644
--- a/platform/core/test/services/ThrottleSpec.js
+++ b/platform/core/test/services/ThrottleSpec.js
@@ -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);
});
});
diff --git a/platform/features/conductor/README.md b/platform/features/conductor/README.md
new file mode 100644
index 0000000000..196093ae1f
--- /dev/null
+++ b/platform/features/conductor/README.md
@@ -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.
diff --git a/platform/features/conductor/bundle.json b/platform/features/conductor/bundle.json
new file mode 100644
index 0000000000..b230f9d370
--- /dev/null
+++ b/platform/features/conductor/bundle.json
@@ -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" ]
+ }
+ ]
+ }
+}
diff --git a/platform/features/conductor/src/ConductorRepresenter.js b/platform/features/conductor/src/ConductorRepresenter.js
new file mode 100644
index 0000000000..b1b35477b6
--- /dev/null
+++ b/platform/features/conductor/src/ConductorRepresenter.js
@@ -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 = [
+ '',
+ "",
+ "",
+ '
'
+ ].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;
+ }
+);
+
diff --git a/platform/features/conductor/src/ConductorService.js b/platform/features/conductor/src/ConductorService.js
new file mode 100644
index 0000000000..59cfa95e3c
--- /dev/null
+++ b/platform/features/conductor/src/ConductorService.js
@@ -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;
+ }
+);
diff --git a/platform/features/conductor/src/ConductorTelemetryDecorator.js b/platform/features/conductor/src/ConductorTelemetryDecorator.js
new file mode 100644
index 0000000000..9c8126c33f
--- /dev/null
+++ b/platform/features/conductor/src/ConductorTelemetryDecorator.js
@@ -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;
+ }
+);
diff --git a/platform/features/conductor/src/ConductorTelemetrySeries.js b/platform/features/conductor/src/ConductorTelemetrySeries.js
new file mode 100644
index 0000000000..aa6ec0ec63
--- /dev/null
+++ b/platform/features/conductor/src/ConductorTelemetrySeries.js
@@ -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;
+ }
+);
diff --git a/platform/features/conductor/src/TimeConductor.js b/platform/features/conductor/src/TimeConductor.js
new file mode 100644
index 0000000000..fcf8dbae04
--- /dev/null
+++ b/platform/features/conductor/src/TimeConductor.js
@@ -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;
+ }
+);
diff --git a/platform/features/conductor/test/ConductorRepresenterSpec.js b/platform/features/conductor/test/ConductorRepresenterSpec.js
new file mode 100644
index 0000000000..2b1003f3c7
--- /dev/null
+++ b/platform/features/conductor/test/ConductorRepresenterSpec.js
@@ -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);
+ });
+
+ });
+ }
+);
diff --git a/platform/features/conductor/test/ConductorServiceSpec.js b/platform/features/conductor/test/ConductorServiceSpec.js
new file mode 100644
index 0000000000..5146ca5f42
--- /dev/null
+++ b/platform/features/conductor/test/ConductorServiceSpec.js
@@ -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());
+ });
+ });
+ }
+);
diff --git a/platform/features/conductor/test/ConductorTelemetryDecoratorSpec.js b/platform/features/conductor/test/ConductorTelemetryDecoratorSpec.js
new file mode 100644
index 0000000000..84812b541a
--- /dev/null
+++ b/platform/features/conductor/test/ConductorTelemetryDecoratorSpec.js
@@ -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();
+ });
+
+ });
+ }
+);
diff --git a/platform/features/conductor/test/ConductorTelemetrySeriesSpec.js b/platform/features/conductor/test/ConductorTelemetrySeriesSpec.js
new file mode 100644
index 0000000000..ea884f74f3
--- /dev/null
+++ b/platform/features/conductor/test/ConductorTelemetrySeriesSpec.js
@@ -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));
+ });
+ });
+
+ });
+ }
+);
diff --git a/platform/features/conductor/test/TimeConductorSpec.js b/platform/features/conductor/test/TimeConductorSpec.js
new file mode 100644
index 0000000000..558322329e
--- /dev/null
+++ b/platform/features/conductor/test/TimeConductorSpec.js
@@ -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);
+ });
+
+ });
+ }
+);
diff --git a/platform/features/conductor/test/suite.json b/platform/features/conductor/test/suite.json
new file mode 100644
index 0000000000..0c469617de
--- /dev/null
+++ b/platform/features/conductor/test/suite.json
@@ -0,0 +1,7 @@
+[
+ "ConductorRepresenter",
+ "ConductorService",
+ "ConductorTelemetryDecorator",
+ "ConductorTelemetrySeries",
+ "TimeConductor"
+]
diff --git a/platform/features/layout/bundle.json b/platform/features/layout/bundle.json
index c32ac5eeed..df5bd9413c 100644
--- a/platform/features/layout/bundle.json
+++ b/platform/features/layout/bundle.json
@@ -167,8 +167,9 @@
"$scope",
"$q",
"dialogService",
- "telemetrySubscriber",
- "telemetryFormatter"
+ "telemetryHandler",
+ "telemetryFormatter",
+ "throttle"
]
}
],
diff --git a/platform/features/layout/src/FixedController.js b/platform/features/layout/src/FixedController.js
index 2bece35391..f608f490f8 100644
--- a/platform/features/layout/src/FixedController.js
+++ b/platform/features/layout/src/FixedController.js
@@ -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);
}
/**
diff --git a/platform/features/layout/test/FixedControllerSpec.js b/platform/features/layout/test/FixedControllerSpec.js
index b95bbc9eb5..31d5e06659 100644
--- a/platform/features/layout/test/FixedControllerSpec.js
+++ b/platform/features/layout/test/FixedControllerSpec.js
@@ -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 () {
diff --git a/platform/features/plot/src/PlotController.js b/platform/features/plot/src/PlotController.js
index a54fff83dd..5555d93659 100644
--- a/platform/features/plot/src/PlotController.js
+++ b/platform/features/plot/src/PlotController.js
@@ -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;
diff --git a/platform/features/plot/src/elements/PlotPanZoomStackGroup.js b/platform/features/plot/src/elements/PlotPanZoomStackGroup.js
index 3746958ecb..e1b61a06eb 100644
--- a/platform/features/plot/src/elements/PlotPanZoomStackGroup.js
+++ b/platform/features/plot/src/elements/PlotPanZoomStackGroup.js
@@ -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;
};
/**
diff --git a/platform/features/plot/src/elements/PlotUpdater.js b/platform/features/plot/src/elements/PlotUpdater.js
index 851fa56096..d4b4ad3eec 100644
--- a/platform/features/plot/src/elements/PlotUpdater.js
+++ b/platform/features/plot/src/elements/PlotUpdater.js
@@ -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.
*/
diff --git a/platform/features/plot/test/PlotControllerSpec.js b/platform/features/plot/test/PlotControllerSpec.js
index e6c79b4e54..dcae177920 100644
--- a/platform/features/plot/test/PlotControllerSpec.js
+++ b/platform/features/plot/test/PlotControllerSpec.js
@@ -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);
+ });
});
}
);
diff --git a/platform/features/scrolling/src/RangeColumn.js b/platform/features/scrolling/src/RangeColumn.js
index 637a68517d..0b76fbfda0 100644
--- a/platform/features/scrolling/src/RangeColumn.js
+++ b/platform/features/scrolling/src/RangeColumn.js
@@ -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,
diff --git a/platform/representation/src/MCTRepresentation.js b/platform/representation/src/MCTRepresentation.js
index 49b2ae0f57..98a814c362 100644
--- a/platform/representation/src/MCTRepresentation.js
+++ b/platform/representation/src/MCTRepresentation.js
@@ -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
diff --git a/platform/representation/test/MCTRepresentationSpec.js b/platform/representation/test/MCTRepresentationSpec.js
index 337f214a86..a50347df70 100644
--- a/platform/representation/test/MCTRepresentationSpec.js
+++ b/platform/representation/test/MCTRepresentationSpec.js
@@ -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);
diff --git a/platform/telemetry/src/TelemetryAggregator.js b/platform/telemetry/src/TelemetryAggregator.js
index fb4cf81fe0..86257befb7 100644
--- a/platform/telemetry/src/TelemetryAggregator.js
+++ b/platform/telemetry/src/TelemetryAggregator.js
@@ -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
diff --git a/platform/telemetry/src/TelemetryHandle.js b/platform/telemetry/src/TelemetryHandle.js
index 145edfc5d7..ae25fd9bfa 100644
--- a/platform/telemetry/src/TelemetryHandle.js
+++ b/platform/telemetry/src/TelemetryHandle.js
@@ -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;
}
diff --git a/platform/telemetry/src/TelemetrySubscription.js b/platform/telemetry/src/TelemetrySubscription.js
index 8b4d7d7a9c..5dcab54b94 100644
--- a/platform/telemetry/src/TelemetrySubscription.js
+++ b/platform/telemetry/src/TelemetrySubscription.js
@@ -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) {