mirror of
https://github.com/nasa/openmct.git
synced 2025-04-17 15:59:09 +00:00
[Imagery] Implemented historical view for imagery
Implemented auto-scrolling historical imagery view in ImageryController. Imagery domain objects now request historical data on each manual bounds change. Added new specs for ensuring that historical data is requested on bounds change and duplicate bounds / datum are ignored.
This commit is contained in:
parent
34ef98e0cd
commit
218ef16160
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -191,6 +191,9 @@ $colorItemTreeVCHover: $colorKey;
|
||||
$colorItemTreeSelectedVC: $colorBodyBg;
|
||||
$shdwItemTreeIcon: none;
|
||||
|
||||
// Images
|
||||
$colorThumbHoverBg: $colorItemTreeHoverBg;
|
||||
|
||||
// Scrollbar
|
||||
$scrollbarTrackSize: 10px;
|
||||
$scrollbarTrackShdw: rgba(#000, 0.2) 0 1px 2px;
|
||||
|
@ -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,7 @@ define([
|
||||
"implementation": ImageryController,
|
||||
"depends": [
|
||||
"$scope",
|
||||
"$window",
|
||||
"openmct"
|
||||
]
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
|
||||
<div class="l-image-thumbs-wrapper" ng-controller="ImageryController as imagery">
|
||||
<div class="l-image-thumb-item" ng-repeat="image in imageHistory track by $index">
|
||||
<img class="l-thumb" ng-init="imagery.scrollToRight()"
|
||||
ng-src={{imagery.getImageUrl(image)}} >
|
||||
<div class="l-time">{{imagery.getTime(image)}}</div>
|
||||
</div>
|
||||
</div>
|
@ -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,22 @@ define(
|
||||
* @constructor
|
||||
* @memberof platform/features/imagery
|
||||
*/
|
||||
function ImageryController($scope, openmct) {
|
||||
|
||||
function ImageryController($scope, $window, openmct) {
|
||||
this.$scope = $scope;
|
||||
this.$window = $window;
|
||||
this.openmct = openmct;
|
||||
this.date = "";
|
||||
this.time = "";
|
||||
this.zone = "";
|
||||
this.imageUrl = "";
|
||||
this.requestCount = 0;
|
||||
this.lastBound = undefined;
|
||||
this.autoScroll = false;
|
||||
this.scrollable =
|
||||
$(document.getElementsByClassName('l-image-thumbs-wrapper')[0]);
|
||||
|
||||
this.$scope.imageHistory = [];
|
||||
this.$scope.filters = {
|
||||
brightness: 100,
|
||||
contrast: 100
|
||||
@ -50,12 +62,17 @@ 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.openmct.time.on('bounds', this.onBoundsChange);
|
||||
this.scrollable.on('scroll', this.onScroll);
|
||||
}
|
||||
|
||||
ImageryController.prototype.subscribe = function (domainObject) {
|
||||
@ -75,50 +92,133 @@ 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.reject('Stale request');
|
||||
}
|
||||
values.forEach(function (datum) {
|
||||
this.updateHistory(datum);
|
||||
}.bind(this));
|
||||
this.requestLad(true);
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
// Optional addToHistory argument allows for two use cases:
|
||||
// updating url and timestamp only for standard imagery view,
|
||||
// i.e to populate the view before history is requested OR
|
||||
// appending to the running imagery 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;
|
||||
}
|
||||
};
|
||||
|
||||
// Query for new historical data on manual bound change
|
||||
ImageryController.prototype.onBoundsChange = function (newBounds, tick) {
|
||||
if (this.domainObject && !tick &&
|
||||
!_.isEqual(this.lastBound, newBounds)) {
|
||||
this.lastBound = newBounds;
|
||||
this.requestHistory(newBounds);
|
||||
}
|
||||
};
|
||||
|
||||
// Update displayable values to reflect latest image telemetry
|
||||
ImageryController.prototype.updateValues = function (datum) {
|
||||
if (this.isPaused) {
|
||||
this.nextDatum = datum;
|
||||
return;
|
||||
}
|
||||
|
||||
this.time = this.timeFormat.format(datum);
|
||||
this.imageUrl = this.imageFormat.format(datum);
|
||||
|
||||
};
|
||||
|
||||
// Update displayable values and append datum to running history
|
||||
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 +228,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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,8 @@ define(
|
||||
metadata,
|
||||
prefix,
|
||||
controller,
|
||||
hasLoaded;
|
||||
hasLoaded,
|
||||
mockWindow;
|
||||
|
||||
beforeEach(function () {
|
||||
$scope = jasmine.createSpyObj('$scope', ['$on', '$watch']);
|
||||
@ -42,14 +43,15 @@ define(
|
||||
['getId']
|
||||
);
|
||||
newDomainObject = { name: 'foo' };
|
||||
|
||||
oldDomainObject.getId.andReturn('testID');
|
||||
openmct = {
|
||||
objects: jasmine.createSpyObj('objectAPI', [
|
||||
'get'
|
||||
]),
|
||||
time: jasmine.createSpyObj('timeAPI', [
|
||||
'timeSystem'
|
||||
'timeSystem',
|
||||
'on',
|
||||
'off'
|
||||
]),
|
||||
telemetry: jasmine.createSpyObj('telemetryAPI', [
|
||||
'subscribe',
|
||||
@ -92,13 +94,19 @@ define(
|
||||
});
|
||||
metadata.value.andReturn("timestamp");
|
||||
metadata.valuesForHints.andReturn(["value"]);
|
||||
mockWindow = jasmine.createSpyObj('$window', ['requestAnimationFrame']);
|
||||
mockWindow.requestAnimationFrame.andCallFake(function (f) {
|
||||
return f();
|
||||
});
|
||||
|
||||
controller = new ImageryController($scope, openmct);
|
||||
|
||||
controller = new ImageryController($scope, mockWindow, openmct);
|
||||
});
|
||||
|
||||
describe("when loaded", function () {
|
||||
var callback;
|
||||
var callback,
|
||||
boundsListener;
|
||||
var mockBounds = {start: 1434600000000, end: 1434600500000};
|
||||
|
||||
beforeEach(function () {
|
||||
waitsFor(function () {
|
||||
return hasLoaded;
|
||||
@ -106,12 +114,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 +177,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 +193,32 @@ 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 () {
|
||||
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("recognizes duplicate bounds", function () {
|
||||
openmct.telemetry.request.reset();
|
||||
boundsListener(mockBounds, false);
|
||||
boundsListener(mockBounds, false);
|
||||
boundsListener(mockBounds, false);
|
||||
expect(openmct.telemetry.request.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user