diff --git a/platform/commonUI/general/res/sass/features/_imagery.scss b/platform/commonUI/general/res/sass/features/_imagery.scss
index 49fcc9c98c..e3d1b58e49 100644
--- a/platform/commonUI/general/res/sass/features/_imagery.scss
+++ b/platform/commonUI/general/res/sass/features/_imagery.scss
@@ -83,12 +83,10 @@
.l-image-thumbs-wrapper {
//@include test(green);
- direction: rtl;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: $interiorMargin;
white-space: nowrap;
- z-index: 70;
}
.l-image-thumb-item {
@@ -114,7 +112,7 @@
width: $imageThumbsD + $imageThumbPad*2;
white-space: normal;
&:hover {
- background: rgba(#fff, 0.2);
+ background: $colorThumbHoverBg;
.l-date,
.l-time {
color: #fff;
diff --git a/platform/commonUI/themes/espresso/res/sass/_constants.scss b/platform/commonUI/themes/espresso/res/sass/_constants.scss
index 1a269a22c7..9d1886c13a 100644
--- a/platform/commonUI/themes/espresso/res/sass/_constants.scss
+++ b/platform/commonUI/themes/espresso/res/sass/_constants.scss
@@ -191,6 +191,9 @@ $colorItemTreeVCHover: pullForward($colorItemTreeVC, 20%);
$colorItemTreeSelectedVC: $colorItemTreeVC;
$shdwItemTreeIcon: 0.6;
+// Images
+$colorThumbHoverBg: $colorItemTreeHoverBg;
+
// Scrollbar
$scrollbarTrackSize: 10px;
$scrollbarTrackShdw: rgba(#000, 0.7) 0 1px 5px;
diff --git a/platform/commonUI/themes/snow/res/sass/_constants.scss b/platform/commonUI/themes/snow/res/sass/_constants.scss
index 99e09048a3..4501228e08 100644
--- a/platform/commonUI/themes/snow/res/sass/_constants.scss
+++ b/platform/commonUI/themes/snow/res/sass/_constants.scss
@@ -191,6 +191,9 @@ $colorItemTreeVCHover: $colorKey;
$colorItemTreeSelectedVC: $colorBodyBg;
$shdwItemTreeIcon: none;
+// Images
+$colorThumbHoverBg: $colorItemTreeHoverBg;
+
// Scrollbar
$scrollbarTrackSize: 10px;
$scrollbarTrackShdw: rgba(#000, 0.2) 0 1px 2px;
diff --git a/platform/features/imagery/bundle.js b/platform/features/imagery/bundle.js
index 85aba508ff..ddca767501 100644
--- a/platform/features/imagery/bundle.js
+++ b/platform/features/imagery/bundle.js
@@ -25,12 +25,14 @@ define([
"./src/controllers/ImageryController",
"./src/directives/MCTBackgroundImage",
"text!./res/templates/imagery.html",
+ "text!./res/templates/imageryTimeline.html",
'legacyRegistry'
], function (
ImageryViewPolicy,
ImageryController,
MCTBackgroundImage,
imageryTemplate,
+ imageryTimelineTemplate,
legacyRegistry
) {
@@ -48,6 +50,17 @@ define([
"telemetry"
],
"editable": false
+ },
+ {
+ "name": "Historical Imagery",
+ "key": "historical-imagery",
+ "cssClass": "icon-image",
+ "template": imageryTimelineTemplate,
+ "priority": "preferred",
+ "needs": [
+ "telemetry"
+ ],
+ "editable": false
}
],
"policies": [
@@ -65,6 +78,8 @@ define([
"implementation": ImageryController,
"depends": [
"$scope",
+ "$window",
+ "$element",
"openmct"
]
}
diff --git a/platform/features/imagery/res/templates/imageryTimeline.html b/platform/features/imagery/res/templates/imageryTimeline.html
new file mode 100644
index 0000000000..a0a2723d86
--- /dev/null
+++ b/platform/features/imagery/res/templates/imageryTimeline.html
@@ -0,0 +1,8 @@
+
+
+
+
+
{{imagery.getTime(image)}}
+
+
diff --git a/platform/features/imagery/src/controllers/ImageryController.js b/platform/features/imagery/src/controllers/ImageryController.js
index f5456f2f97..d8be8d46dc 100644
--- a/platform/features/imagery/src/controllers/ImageryController.js
+++ b/platform/features/imagery/src/controllers/ImageryController.js
@@ -24,9 +24,13 @@
* This bundle implements views of image telemetry.
* @namespace platform/features/imagery
*/
+
define(
- ['moment'],
- function (moment) {
+ [
+ 'zepto',
+ 'lodash'
+ ],
+ function ($, _) {
/**
* Controller for the "Imagery" view of a domain object which
@@ -34,14 +38,20 @@ define(
* @constructor
* @memberof platform/features/imagery
*/
- function ImageryController($scope, openmct) {
+
+ function ImageryController($scope, $window, element, openmct) {
this.$scope = $scope;
+ this.$window = $window;
this.openmct = openmct;
this.date = "";
this.time = "";
this.zone = "";
this.imageUrl = "";
+ this.requestCount = 0;
+ this.scrollable = $(element[0]);
+ this.autoScroll = openmct.time.clock() ? true : false;
+ this.$scope.imageHistory = [];
this.$scope.filters = {
brightness: 100,
contrast: 100
@@ -50,12 +60,15 @@ define(
this.subscribe = this.subscribe.bind(this);
this.stopListening = this.stopListening.bind(this);
this.updateValues = this.updateValues.bind(this);
+ this.updateHistory = this.updateHistory.bind(this);
+ this.onBoundsChange = this.onBoundsChange.bind(this);
+ this.onScroll = this.onScroll.bind(this);
- // Subscribe to telemetry when a domain object becomes available
this.subscribe(this.$scope.domainObject);
- // Unsubscribe when the plot is destroyed
- this.$scope.$on("$destroy", this.stopListening);
+ this.$scope.$on('$destroy', this.stopListening);
+ this.openmct.time.on('bounds', this.onBoundsChange);
+ this.scrollable.on('scroll', this.onScroll);
}
ImageryController.prototype.subscribe = function (domainObject) {
@@ -75,50 +88,152 @@ define(
.telemetry
.getValueFormatter(metadata.valuesForHints(['image'])[0]);
this.unsubscribe = this.openmct.telemetry
- .subscribe(this.domainObject, this.updateValues);
- this.openmct.telemetry
- .request(this.domainObject, {
- strategy: 'latest',
- size: 1
- })
- .then(function (values) {
- this.updateValues(values[0]);
+ .subscribe(this.domainObject, function (datum) {
+ this.updateHistory(datum);
+ this.updateValues(datum);
}.bind(this));
+ this.requestLad(false);
+ this.requestHistory(this.openmct.time.bounds());
+ }.bind(this));
+ };
+
+ ImageryController.prototype.requestHistory = function (bounds) {
+ this.requestCount++;
+ this.$scope.imageHistory = [];
+ var requestId = this.requestCount;
+ this.openmct.telemetry
+ .request(this.domainObject, bounds)
+ .then(function (values) {
+ if (this.requestCount > requestId) {
+ return Promise.resolve('Stale request');
+ }
+ values.forEach(function (datum) {
+ this.updateHistory(datum);
+ }, this);
+ this.requestLad(true);
+ }.bind(this));
+ };
+
+ /**
+ * Makes a request for the most recent datum in the
+ * telelmetry store. Optional addToHistory argument
+ * determines whether the requested telemetry should
+ * be added to history or only used to update the current
+ * image url and timestamp.
+ * @private
+ * @param {boolean} [addToHistory] if true, adds to history
+ */
+ ImageryController.prototype.requestLad = function (addToHistory) {
+ this.openmct.telemetry
+ .request(this.domainObject, {
+ strategy: 'latest',
+ size: 1
+ })
+ .then(function (values) {
+ this.updateValues(values[0]);
+ if (addToHistory !== false) {
+ this.updateHistory(values[0]);
+ }
}.bind(this));
};
ImageryController.prototype.stopListening = function () {
+ this.openmct.time.off('bounds', this.onBoundsChange);
+ this.scrollable.off('scroll', this.onScroll);
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
};
- // Update displayable values to reflect latest image telemetry
+ /**
+ * Responds to bound change event be requesting new
+ * historical data if the bound change was manual.
+ * @private
+ * @param {object} [newBounds] new bounds object
+ * @param {boolean} [tick] true when change is automatic
+ */
+ ImageryController.prototype.onBoundsChange = function (newBounds, tick) {
+ if (this.domainObject && !tick) {
+ this.requestHistory(newBounds);
+ }
+ };
+
+ /**
+ * Updates displayable values to match those of the most
+ * recently recieved datum.
+ * @param {object} [datum] the datum
+ * @private
+ */
ImageryController.prototype.updateValues = function (datum) {
if (this.isPaused) {
this.nextDatum = datum;
return;
}
+
this.time = this.timeFormat.format(datum);
this.imageUrl = this.imageFormat.format(datum);
+
+ };
+
+ /**
+ * Appends given imagery datum to running history.
+ * @private
+ * @param {object} [datum] target telemetry datum
+ * @returns {boolean} falsy when a duplicate datum is given
+ */
+ ImageryController.prototype.updateHistory = function (datum) {
+ if (this.$scope.imageHistory.length === 0 ||
+ !_.isEqual(this.$scope.imageHistory.slice(-1)[0], datum)) {
+
+ var index = _.sortedIndex(this.$scope.imageHistory, datum, 'utc');
+ this.$scope.imageHistory.splice(index, 0, datum);
+ return true;
+ }
+
+ return false;
+ };
+
+ ImageryController.prototype.onScroll = function (event) {
+ this.$window.requestAnimationFrame(function () {
+ if (this.scrollable[0].scrollLeft <
+ (this.scrollable[0].scrollWidth - this.scrollable[0].clientWidth) - 20) {
+ this.autoScroll = false;
+ } else {
+ this.autoScroll = true;
+ }
+ }.bind(this));
+ };
+
+ ImageryController.prototype.scrollToRight = function () {
+ if (this.autoScroll) {
+ this.scrollable[0].scrollLeft = this.scrollable[0].scrollWidth;
+ }
};
/**
* Get the time portion (hours, minutes, seconds) of the
- * timestamp associated with the incoming image telemetry.
+ * timestamp associated with the incoming image telemetry
+ * if no parameter is given, or of a provided datum.
+ * @param {object} [datum] target telemetry datum
* @returns {string} the time
*/
- ImageryController.prototype.getTime = function () {
- return this.time;
+ ImageryController.prototype.getTime = function (datum) {
+ return datum ?
+ this.timeFormat.format(datum) :
+ this.time;
};
/**
- * Get the URL of the image telemetry to display.
+ * Get the URL of the most recent image telemetry if no
+ * parameter is given, or of a provided datum.
+ * @param {object} [datum] target telemetry datum
* @returns {string} URL for telemetry image
*/
- ImageryController.prototype.getImageUrl = function () {
- return this.imageUrl;
+ ImageryController.prototype.getImageUrl = function (datum) {
+ return datum ?
+ this.imageFormat.format(datum) :
+ this.imageUrl;
};
/**
@@ -128,15 +243,15 @@ define(
* @returns {boolean} the current state
*/
ImageryController.prototype.paused = function (state) {
- if (arguments.length > 0 && state !== this.isPaused) {
- this.isPaused = state;
- if (this.nextDatum) {
- this.updateValues(this.nextDatum);
- delete this.nextDatum;
+ if (arguments.length > 0 && state !== this.isPaused) {
+ this.isPaused = state;
+ if (this.nextDatum) {
+ this.updateValues(this.nextDatum);
+ delete this.nextDatum;
+ }
}
- }
- return this.isPaused;
- };
+ return this.isPaused;
+ };
return ImageryController;
}
diff --git a/platform/features/imagery/src/policies/ImageryViewPolicy.js b/platform/features/imagery/src/policies/ImageryViewPolicy.js
index 4811f1e55a..f23f745a2b 100644
--- a/platform/features/imagery/src/policies/ImageryViewPolicy.js
+++ b/platform/features/imagery/src/policies/ImageryViewPolicy.js
@@ -47,7 +47,7 @@ define([
};
ImageryViewPolicy.prototype.allow = function (view, domainObject) {
- if (view.key === 'imagery') {
+ if (view.key === 'imagery' || view.key === 'historical-imagery') {
return this.hasImageTelemetry(domainObject);
}
diff --git a/platform/features/imagery/test/controllers/ImageryControllerSpec.js b/platform/features/imagery/test/controllers/ImageryControllerSpec.js
index c5ff9929d4..ce81e6ef67 100644
--- a/platform/features/imagery/test/controllers/ImageryControllerSpec.js
+++ b/platform/features/imagery/test/controllers/ImageryControllerSpec.js
@@ -21,8 +21,14 @@
*****************************************************************************/
define(
- ["../../src/controllers/ImageryController"],
- function (ImageryController) {
+ [
+ "zepto",
+ "../../src/controllers/ImageryController"
+ ],
+ function ($, ImageryController) {
+
+ var MOCK_ELEMENT_TEMPLATE =
+ '';
describe("The Imagery controller", function () {
var $scope,
@@ -33,7 +39,9 @@ define(
metadata,
prefix,
controller,
- hasLoaded;
+ hasLoaded,
+ mockWindow,
+ mockElement;
beforeEach(function () {
$scope = jasmine.createSpyObj('$scope', ['$on', '$watch']);
@@ -42,14 +50,16 @@ define(
['getId']
);
newDomainObject = { name: 'foo' };
-
oldDomainObject.getId.andReturn('testID');
openmct = {
objects: jasmine.createSpyObj('objectAPI', [
'get'
]),
time: jasmine.createSpyObj('timeAPI', [
- 'timeSystem'
+ 'timeSystem',
+ 'clock',
+ 'on',
+ 'off'
]),
telemetry: jasmine.createSpyObj('telemetryAPI', [
'subscribe',
@@ -92,13 +102,24 @@ define(
});
metadata.value.andReturn("timestamp");
metadata.valuesForHints.andReturn(["value"]);
+ mockElement = $(MOCK_ELEMENT_TEMPLATE);
+ mockWindow = jasmine.createSpyObj('$window', ['requestAnimationFrame']);
+ mockWindow.requestAnimationFrame.andCallFake(function (f) {
+ return f();
+ });
- controller = new ImageryController($scope, openmct);
-
+ controller = new ImageryController(
+ $scope,
+ mockWindow,
+ mockElement,
+ openmct
+ );
});
describe("when loaded", function () {
- var callback;
+ var callback,
+ boundsListener;
+
beforeEach(function () {
waitsFor(function () {
return hasLoaded;
@@ -106,12 +127,16 @@ define(
runs(function () {
+ openmct.time.on.calls.forEach(function (call) {
+ if (call.args[0] === "bounds") {
+ boundsListener = call.args[1];
+ }
+ });
callback =
openmct.telemetry.subscribe.mostRecentCall.args[1];
});
});
-
it("uses LAD telemetry", function () {
expect(openmct.telemetry.request).toHaveBeenCalledWith(
newDomainObject,
@@ -165,7 +190,14 @@ define(
);
});
- it("unsubscribes when scope is destroyed", function () {
+ it("requests telemetry", function () {
+ expect(openmct.telemetry.request).toHaveBeenCalledWith(
+ newDomainObject,
+ jasmine.any(Object)
+ );
+ });
+
+ it("unsubscribes and unlistens when scope is destroyed", function () {
expect(unsubscribe).not.toHaveBeenCalled();
$scope.$on.calls.forEach(function (call) {
@@ -174,6 +206,25 @@ define(
}
});
expect(unsubscribe).toHaveBeenCalled();
+ expect(openmct.time.off)
+ .toHaveBeenCalledWith('bounds', jasmine.any(Function));
+ });
+
+ it("listens for bounds event and responds to tick and manual change", function () {
+ var mockBounds = {start: 1434600000000, end: 1434600500000};
+ expect(openmct.time.on).toHaveBeenCalled();
+ openmct.telemetry.request.reset();
+ boundsListener(mockBounds, true);
+ expect(openmct.telemetry.request).not.toHaveBeenCalled();
+ boundsListener(mockBounds, false);
+ expect(openmct.telemetry.request).toHaveBeenCalledWith(newDomainObject, mockBounds);
+ });
+
+ it ("doesnt append duplicate datum", function () {
+ var mockDatum = {url: 'image/url', utc: 1434600000000};
+ expect(controller.updateHistory(mockDatum)).toBe(true);
+ expect(controller.updateHistory(mockDatum)).toBe(false);
+ expect(controller.updateHistory(mockDatum)).toBe(false);
});
});